diff --git a/Clockwork/Support/Mezzio/Clockwork.php b/Clockwork/Support/Mezzio/Clockwork.php new file mode 100644 index 00000000..18fa2d24 --- /dev/null +++ b/Clockwork/Support/Mezzio/Clockwork.php @@ -0,0 +1,28 @@ +config['web']['host']; + } + + public function getAuthenticationAPI() + { + return $this->config['authentication_api']; + } +} diff --git a/Clockwork/Support/Mezzio/ClockworkMiddleware.php b/Clockwork/Support/Mezzio/ClockworkMiddleware.php new file mode 100644 index 00000000..67a1811e --- /dev/null +++ b/Clockwork/Support/Mezzio/ClockworkMiddleware.php @@ -0,0 +1,48 @@ +clockwork = $clockwork ?? Clockwork::init(); + } + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + $requestPath = rtrim($request->getUri()->getPath(), '/'); + + if ($this->clockwork->isEnabled()) { + $apiAuthentication = ltrim($this->clockwork->getAuthenticationAPI(), '/'); + if ($this->clockwork->isAuthenticationEnabled() && $requestPath == $apiAuthentication) { + return $this->clockwork->usePsrMessage($request, new Response())->authenticate($requestPath); + } + + $apiPath = ltrim($this->clockwork->getApiPath(), '/'); + $clockworkDataUri = "#/$apiPath(?:/(?[0-9-]+))?(?:/(?(?:previous|next)))?(?:/(?\d+))?#"; + if (preg_match($clockworkDataUri, $requestPath, $matches)) { + return $this->clockwork->usePsrMessage($request, new Response())->returnMetadata($requestPath); + } + } + + if ($this->clockwork->isWebEnabled()) { + if ($requestPath == $this->clockwork->getWebHost()) { + return $this->clockwork->usePsrMessage($request, new Response())->returnWeb(); + } + } + + // Inject clockwork instance in the request, useful to log new events or timeline + $request = $request->withAttribute('Clockwork', $this->clockwork->getClockwork()); + return $this->clockwork->usePsrMessage($request, $handler->handle($request))->requestProcessed(); + } +} diff --git a/Clockwork/Support/Mezzio/config.php b/Clockwork/Support/Mezzio/config.php new file mode 100644 index 00000000..24254b7b --- /dev/null +++ b/Clockwork/Support/Mezzio/config.php @@ -0,0 +1,278 @@ + getenv('CLOCKWORK_ENABLE') !== false ? getenv('CLOCKWORK_ENABLE') : false, + + /* + |------------------------------------------------------------------------------------------------------------------ + | Features + |------------------------------------------------------------------------------------------------------------------ + | + | You can enable or disable various Clockwork features here. Some features have additional settings (eg. slow query + | threshold for database queries). + | + */ + + 'features' => [ + + // Performance metrics + 'performance' => [ + // Allow collecting of client metrics. Requires separate clockwork-browser npm package. + 'client_metrics' => getenv('CLOCKWORK_PERFORMANCE_CLIENT_METRICS') !== false ? getenv('CLOCKWORK_PERFORMANCE_CLIENT_METRICS') : true + ] + + ], + + /* + |------------------------------------------------------------------------------------------------------------------ + | Enable toolbar + |------------------------------------------------------------------------------------------------------------------ + | + | Clockwork can show a toolbar with basic metrics on all responses. Here you can enable or disable this feature. + | Requires a separate clockwork-browser npm library. + | + */ + + 'toolbar' => getenv('CLOCKWORK_TOOLBAR') !== false ? getenv('CLOCKWORK_TOOLBAR') : true, + + /* + |------------------------------------------------------------------------------------------------------------------ + | HTTP requests collection + |------------------------------------------------------------------------------------------------------------------ + | + | Clockwork collects data about HTTP requests to your app. Here you can choose which requests should be collected. + | + */ + + 'requests' => [ + // With on-demand mode enabled, Clockwork will only profile requests when the browser extension is open or you + // manually pass a "clockwork-profile" cookie or get/post data key. + // Optionally you can specify a "secret" that has to be passed as the value to enable profiling. + 'on_demand' => getenv('CLOCKWORK_REQUESTS_ON_DEMAND') !== false ? getenv('CLOCKWORK_REQUESTS_ON_DEMAND') : false, + + // Collect only errors (requests with HTTP 4xx and 5xx responses) + 'errors_only' => getenv('CLOCKWORK_REQUESTS_ERRORS_ONLY') !== false ? getenv('CLOCKWORK_REQUESTS_ERRORS_ONLY') : false, + + // Response time threshold in milliseconds after which the request will be marked as slow + 'slow_threshold' => getenv('CLOCKWORK_REQUESTS_SLOW_THRESHOLD') !== false ? getenv('CLOCKWORK_REQUESTS_SLOW_THRESHOLD') : null, + + // Collect only slow requests + 'slow_only' => getenv('CLOCKWORK_REQUESTS_SLOW_ONLY') !== false ? getenv('CLOCKWORK_REQUESTS_SLOW_ONLY') : false, + + // Sample the collected requests (eg. set to 100 to collect only 1 in 100 requests) + 'sample' => getenv('CLOCKWORK_REQUESTS_SAMPLE') !== false ? getenv('CLOCKWORK_REQUESTS_SAMPLE') : false, + + // List of URIs that should not be collected + 'except' => [ + // '/api/.*' + ], + + // List of URIs that should be collected, any other URI will not be collected if not empty + 'only' => [ + // '/api/.*' + ], + + // Don't collect OPTIONS requests, mostly used in the CSRF pre-flight requests and are rarely of interest + 'except_preflight' => getenv('CLOCKWORK_REQUESTS_EXCEPT_PREFLIGHT') !== false ? getenv('CLOCKWORK_REQUESTS_EXCEPT_PREFLIGHT') : true + ], + + /* + |------------------------------------------------------------------------------------------------------------------ + | Enable data collection when Clockwork is disabled + |------------------------------------------------------------------------------------------------------------------ + | + | You can enable this setting to collect data even when Clockwork is disabled. Eg. for future analysis. + | + */ + + 'collect_data_always' => getenv('CLOCKWORK_COLLECT_DATA_ALWAYS') !== false ? getenv('CLOCKWORK_COLLECT_DATA_ALWAYS') : false, + + /* + |------------------------------------------------------------------------------------------------------------------ + | Clockwork API URI + |------------------------------------------------------------------------------------------------------------------ + | + | Path of the script calling returnRequest to return Clockwork metadata to the client app. See installation + | instructions for details. + | + */ + + 'api' => getenv('CLOCKWORK_API') !== false ? getenv('CLOCKWORK_API') : '/__clockwork/', + + /* + |------------------------------------------------------------------------------------------------------------------ + | Clockwork web UI + |------------------------------------------------------------------------------------------------------------------ + | + | Clockwork comes bundled with a full Clockwork App accessible as a Web UI. Here you can enable and configure this + | feature. + | Clockwork::returnWeb api is used to expose the Web UI in your vanilla app, see the installation instructions for + | details. + | + */ + + 'web' => [ + // Enable or disable the Web UI, set to the public uri where Clockwork Web UI is accessible + 'enable' => getenv('CLOCKWORK_WEB_ENABLE') !== false ? getenv('CLOCKWORK_WEB_ENABLE') : true, + + // Path where to install the Web UI assets, should be publicly accessible + 'path' => getenv('CLOCKWORK_WEB_PATH') !== false ? getenv('CLOCKWORK_WEB_PATH') : 'public/vendor/clockwork', + + // Public URI where the installed Web UI assets will be accessible + 'uri' => getenv('CLOCKWORK_WEB_URI') !== false ? getenv('CLOCKWORK_WEB_URI') : '/vendor/clockwork', + + // host where web clockwork is accessible, used when a server side request return the html + 'host' => getenv('CLOCKWORK_WEB_HOST') !== false ? getenv('CLOCKWORK_WEB_HOST') : '/clockwork' + ], + + /* + |------------------------------------------------------------------------------------------------------------------ + | Metadata storage + |------------------------------------------------------------------------------------------------------------------ + | + | Configure how is the metadata collected by Clockwork stored. Two options are available: + | - files - A simple fast storage implementation storing data in one-per-request files. + | - sql - Stores requests in a sql database. Supports MySQL, Postgresql, Sqlite and requires PDO. + | + */ + + 'storage' => getenv('CLOCKWORK_STORAGE') !== false ? getenv('CLOCKWORK_STORAGE') : 'files', + + // Path where the Clockwork metadata is stored + 'storage_files_path' => getenv('CLOCKWORK_STORAGE_FILES_PATH') !== false ? getenv('CLOCKWORK_STORAGE_FILES_PATH') : 'data/clockwork', + + // Compress the metadata files using gzip, trading a little bit of performance for lower disk usage + 'storage_files_compress' => getenv('CLOCKWORK_STORAGE_FILES_COMPRESS') !== false ? getenv('CLOCKWORK_STORAGE_FILES_COMPRESS') : false, + + // SQL database to use, can be a PDO connection string or a path to a sqlite file + 'storage_sql_database' => getenv('CLOCKWORK_STORAGE_SQL_DATABASE') !== false ? getenv('CLOCKWORK_STORAGE_SQL_DATABASE') : 'sqlite:' . __DIR__ . '/../../../../../clockwork.sqlite', + 'storage_sql_username' => getenv('CLOCKWORK_STORAGE_SQL_USERNAME') !== false ? getenv('CLOCKWORK_STORAGE_SQL_USERNAME') : null, + 'storage_sql_password' => getenv('CLOCKWORK_STORAGE_SQL_PASSWORD') !== false ? getenv('CLOCKWORK_STORAGE_SQL_PASSWORD') : null, + + // SQL table name to use, the table is automatically created and updated when needed + 'storage_sql_table' => getenv('CLOCKWORK_STORAGE_SQL_TABLE') !== false ? getenv('CLOCKWORK_STORAGE_SQL_TABLE') : 'clockwork', + + // Maximum lifetime of collected metadata in minutes, older requests will automatically be deleted, false to disable + 'storage_expiration' => getenv('CLOCKWORK_STORAGE_EXPIRATION') !== false ? getenv('CLOCKWORK_STORAGE_EXPIRATION') : 60 * 24 * 7, + + /* + |------------------------------------------------------------------------------------------------------------------ + | Authentication + |------------------------------------------------------------------------------------------------------------------ + | + | Clockwork can be configured to require authentication before allowing access to the collected data. This might be + | useful when the application is publicly accessible. Setting to true will enable a simple authentication with a + | pre-configured password. You can also pass a class name of a custom implementation. + | + */ + + 'authentication' => getenv('CLOCKWORK_AUTHENTICATION') !== false ? getenv('CLOCKWORK_AUTHENTICATION') : false, + 'authentication_api' => getenv('CLOCKWORK_AUTHENTICATION_API') !== false ? getenv('CLOCKWORK_AUTHENTICATION_API') : '/__clockwork/auth', + + // Password for the simple authentication + 'authentication_password' => getenv('CLOCKWORK_AUTHENTICATION_PASSWORD') !== false ? getenv('CLOCKWORK_AUTHENTICATION_PASSWORD') : 'VerySecretPassword', + + /* + |------------------------------------------------------------------------------------------------------------------ + | Stack traces collection + |------------------------------------------------------------------------------------------------------------------ + | + | Clockwork can collect stack traces for log messages and certain data like database queries. Here you can set + | whether to collect stack traces, limit the number of collected frames and set further configuration. Collecting + | long stack traces considerably increases metadata size. + | + */ + + 'stack_traces' => [ + // Enable or disable collecting of stack traces + 'enabled' => getenv('CLOCKWORK_STACK_TRACES_ENABLED') !== false ? getenv('CLOCKWORK_STACK_TRACES_ENABLED') : true, + + // Limit the number of frames to be collected + 'limit' => getenv('CLOCKWORK_STACK_TRACES_LIMIT') !== false ? getenv('CLOCKWORK_STACK_TRACES_LIMIT') : 10, + + // List of vendor names to skip when determining caller, common vendor are automatically added + 'skip_vendors' => [ + // 'phpunit' + ], + + // List of namespaces to skip when determining caller + 'skip_namespaces' => [ + // 'Vendor' + ], + + // List of class names to skip when determining caller + 'skip_classes' => [ + // App\CustomLog::class + ] + ], + + /* + |------------------------------------------------------------------------------------------------------------------ + | Serialization + |------------------------------------------------------------------------------------------------------------------ + | + | Clockwork serializes the collected data to json for storage and transfer. Here you can configure certain aspects + | of serialization. Serialization has a large effect on the cpu time and memory usage. + | + */ + + // Maximum depth of serialized multi-level arrays and objects + 'serialization_depth' => getenv('CLOCKWORK_SERIALIZATION_DEPTH') !== false ? getenv('CLOCKWORK_SERIALIZATION_DEPTH') : 10, + + // A list of classes that will never be serialized (eg. a common service container class) + 'serialization_blackbox' => [ + // \App\ServiceContainer::class + ], + + /* + |------------------------------------------------------------------------------------------------------------------ + | Register helpers + |------------------------------------------------------------------------------------------------------------------ + | + | Clockwork comes with a "clock" global helper function. You can use this helper to quickly log something and to + | access the Clockwork instance. + | + */ + + 'register_helpers' => getenv('CLOCKWORK_REGISTER_HELPERS') !== false ? getenv('CLOCKWORK_REGISTER_HELPERS') : false, + + /* + |------------------------------------------------------------------------------------------------------------------ + | Send Headers for AJAX request + |------------------------------------------------------------------------------------------------------------------ + | + | When trying to collect data the AJAX method can sometimes fail if it is missing required headers. For example, an + | API might require a version number using Accept headers to route the HTTP request to the correct codebase. + | + */ + + 'headers' => [ + // 'Accept' => 'application/vnd.com.whatever.v1+json', + ], + /* + |------------------------------------------------------------------------------------------------------------------ + | Server-Timing + |------------------------------------------------------------------------------------------------------------------ + | + | Clockwork supports the W3C Server Timing specification, which allows for collecting a simple performance metrics + | in a cross-browser way. Eg. in Chrome, your app, database and timeline event timings will be shown in the Dev + | Tools network tab. This setting specifies the max number of timeline events that will be sent. Setting to false + | will disable the feature. + | + */ + + 'server_timing' => getenv('CLOCKWORK_SERVER_TIMING') !== false ? getenv('CLOCKWORK_SERVER_TIMING') : 10 + +]; diff --git a/Clockwork/Support/Vanilla/Clockwork.php b/Clockwork/Support/Vanilla/Clockwork.php index 1b9cf072..1621fe4f 100644 --- a/Clockwork/Support/Vanilla/Clockwork.php +++ b/Clockwork/Support/Vanilla/Clockwork.php @@ -1,4 +1,6 @@ -config = array_merge(include __DIR__ . '/config.php', $config); - $this->clockwork = new BaseClockwork; + $this->clockwork = new BaseClockwork(); - $this->clockwork->addDataSource(new PhpDataSource); + $this->clockwork->addDataSource(new PhpDataSource()); $this->clockwork->storage($this->makeStorage()); $this->clockwork->authenticator($this->makeAuthenticator()); @@ -50,7 +51,9 @@ public function __construct($config = []) $this->configureShouldCollect(); $this->configureShouldRecord(); - if ($this->config['register_helpers']) include __DIR__ . '/helpers.php'; + if ($this->config['register_helpers']) { + include __DIR__ . '/helpers.php'; + } } // Initialize a singleton instance, takes an additional config @@ -69,14 +72,22 @@ public static function instance() // execution, return PSR-7 response if one was set public function requestProcessed() { - if (! $this->config['enable'] && ! $this->config['collect_data_always']) return $this->psrResponse; + if (! $this->config['enable'] && ! $this->config['collect_data_always']) { + return $this->psrResponse; + } - if (! $this->clockwork->shouldCollect()->filter($this->incomingRequest())) return $this->psrResponse; - if (! $this->clockwork->shouldRecord()->filter($this->clockwork->request())) return $this->psrResponse; + if (! $this->clockwork->shouldCollect()->filter($this->incomingRequest())) { + return $this->psrResponse; + } + if (! $this->clockwork->shouldRecord()->filter($this->clockwork->request())) { + return $this->psrResponse; + } $this->clockwork->resolveRequest()->storeRequest(); - if (! $this->config['enable']) return $this->psrResponse; + if (! $this->config['enable']) { + return $this->psrResponse; + } $this->sendHeaders(); @@ -90,9 +101,13 @@ public function requestProcessed() // Resolves and records the current request as a command, should be called at the end of app execution public function commandExecuted($name, $exitCode = null, $arguments = [], $options = [], $argumentsDefaults = [], $optionsDefaults = [], $output = null) { - if (! $this->config['enable'] && ! $this->config['collect_data_always']) return; + if (! $this->config['enable'] && ! $this->config['collect_data_always']) { + return; + } - if (! $this->clockwork->shouldRecord()->filter($this->clockwork->request())) return; + if (! $this->clockwork->shouldRecord()->filter($this->clockwork->request())) { + return; + } $this->clockwork ->resolveAsCommand($name, $exitCode, $arguments, $options, $argumentsDefaults, $optionsDefaults, $output) @@ -102,9 +117,13 @@ public function commandExecuted($name, $exitCode = null, $arguments = [], $optio // Resolves and records the current request as a queue job, should be called at the end of app execution public function queueJobExecuted($name, $description = null, $status = 'processed', $payload = [], $queue = null, $connection = null, $options = []) { - if (! $this->config['enable'] && ! $this->config['collect_data_always']) return; + if (! $this->config['enable'] && ! $this->config['collect_data_always']) { + return; + } - if (! $this->clockwork->shouldRecord()->filter($this->clockwork->request())) return; + if (! $this->clockwork->shouldRecord()->filter($this->clockwork->request())) { + return; + } $this->clockwork ->resolveAsQueueJob($name, $description, $status, $payload, $queue, $connection, $options) @@ -115,7 +134,9 @@ public function queueJobExecuted($name, $description = null, $status = 'processe // in the request processing public function sendHeaders() { - if (! $this->config['enable'] || $this->headersSent) return; + if (! $this->config['enable'] || $this->headersSent) { + return; + } $this->headersSent = true; @@ -157,10 +178,16 @@ public function getCookiePayload() // Handle Clockwork REST api request, retrieves or updates Clockwork metadata public function handleMetadata($request = null, $method = null) { - if (! $request) $request = isset($_GET['request']) ? $_GET['request'] : ''; - if (! $method) $method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET'; + if (! $request) { + $request = isset($_GET['request']) ? $_GET['request'] : ''; + } + if (! $method) { + $method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET'; + } - if ($method == 'POST' && $request == 'auth') return $this->authenticate(); + if ($method == 'POST' && $request == 'auth') { + return $this->authenticate(); + } return $method == 'POST' ? $this->updateMetadata($request) : $this->returnMetadata($request); } @@ -168,7 +195,9 @@ public function handleMetadata($request = null, $method = null) // Retrieve metadata based on the passed Clockwork REST api request and send HTTP response public function returnMetadata($request = null) { - if (! $this->config['enable']) return $this->response(null, 404); + if (! $this->config['enable']) { + return $this->response(null, 404); + } $authenticator = $this->clockwork->authenticator(); $authenticated = $authenticator->check(isset($_SERVER['HTTP_X_CLOCKWORK_AUTH']) ? $_SERVER['HTTP_X_CLOCKWORK_AUTH'] : ''); @@ -183,14 +212,20 @@ public function returnMetadata($request = null) // Returns metadata based on the passed Clockwork REST api request public function getMetadata($request = null) { - if (! $this->config['enable']) return; + if (! $this->config['enable']) { + return; + } $authenticator = $this->clockwork->authenticator(); $authenticated = $authenticator->check(isset($_SERVER['HTTP_X_CLOCKWORK_AUTH']) ? $_SERVER['HTTP_X_CLOCKWORK_AUTH'] : ''); - if ($authenticated !== true) return; + if ($authenticated !== true) { + return; + } - if (! $request) $request = isset($_GET['request']) ? $_GET['request'] : ''; + if (! $request) { + $request = isset($_GET['request']) ? $_GET['request'] : ''; + } preg_match('#(?[0-9-]+|latest)(?:/(?next|previous))?(?:/(?\d+))?#', $request, $matches); @@ -213,7 +248,9 @@ public function getMetadata($request = null) } if ($data) { - $data = is_array($data) ? array_map(function ($item) { return $item->toArray(); }, $data) : $data->toArray(); + $data = is_array($data) ? array_map(function ($item) { + return $item->toArray(); + }, $data) : $data->toArray(); } return $data; @@ -226,7 +263,9 @@ public function updateMetadata($request = null) return $this->response(null, 404); } - if (! $request) $request = isset($_GET['request']) ? $_GET['request'] : ''; + if (! $request) { + $request = isset($_GET['request']) ? $_GET['request'] : ''; + } $storage = $this->clockwork->storage(); @@ -258,9 +297,13 @@ public function updateMetadata($request = null) // Authanticates access to Clockwork REST api public function authenticate($request = null) { - if (! $this->config['enable']) return; + if (! $this->config['enable']) { + return; + } - if (! $request) $request = isset($_GET['request']) ? $_GET['request'] : ''; + if (! $request) { + $request = isset($_GET['request']) ? $_GET['request'] : ''; + } $token = $this->clockwork->authenticator()->attempt([ 'username' => isset($_POST['username']) ? $_POST['username'] : '', @@ -273,11 +316,15 @@ public function authenticate($request = null) // Returns the Clockwork Web UI as a HTTP response, installs the Web UI on the first run public function returnWeb() { - if (! $this->config['web']['enable']) return; + if (! $this->config['web']['enable']) { + return; + } $this->installWeb(); - $asset = function ($uri) { return "{$this->config['web']['uri']}/{$uri}"; }; + $asset = function ($uri) { + return "{$this->config['web']['uri']}/{$uri}"; + }; $metadataPath = $this->config['api']; $url = $this->config['web']['uri']; @@ -300,7 +347,9 @@ public function installWeb() $path = $this->config['web']['path']; $source = __DIR__ . '/../../Web/public'; - if (file_exists("{$path}/index.html")) return; + if (file_exists("{$path}/index.html")) { + return; + } @mkdir($path, 0755, true); @@ -366,11 +415,11 @@ protected function makeAuthenticator() $authenticator = $this->config['authentication']; if (is_string($authenticator)) { - return new $authenticator; + return new $authenticator(); } elseif ($authenticator) { return new SimpleAuthenticator($this->config['authentication_password']); } else { - return new NullAuthenticator; + return new NullAuthenticator(); } } @@ -418,10 +467,12 @@ public function configureShouldRecord() } // Set a cookie on PSR-7 response or using vanilla php - protected function setCookie($name, $value, $expires) { + protected function setCookie($name, $value, $expires) + { if ($this->psrResponse) { $this->psrResponse = $this->psrResponse->withAddedHeader( - 'Set-Cookie', "{$name}=" . urlencode($value) . '; expires=' . gmdate('D, d M Y H:i:s T', $expires) + 'Set-Cookie', + "{$name}=" . urlencode($value) . '; expires=' . gmdate('D, d M Y H:i:s T', $expires) ); } else { setcookie($name, $value, $expires); @@ -441,14 +492,20 @@ protected function setHeader($header, $value) // Send a json response, uses the PSR-7 response if set protected function response($data = null, $status = null, $json = true) { - if ($json) $this->setHeader('Content-Type', 'application/json'); + if ($json) { + $this->setHeader('Content-Type', 'application/json'); + } if ($this->psrResponse) { - if ($status) $this->psrResponse = $this->psrResponse->withStatus($status); + if ($status) { + $this->psrResponse = $this->psrResponse->withStatus($status); + } $this->psrResponse->getBody()->write($json ? json_encode($data, \JSON_PARTIAL_OUTPUT_ON_ERROR) : $data); return $this->psrResponse; } else { - if ($status) http_response_code($status); + if ($status) { + http_response_code($status); + } echo $json ? json_encode($data, \JSON_PARTIAL_OUTPUT_ON_ERROR) : $data; } } @@ -481,4 +538,24 @@ public static function __callStatic($method, $args = []) { return static::instance()->$method(...$args); } + + public function getApiPath() + { + return $this->config['api']; + } + + public function isWebEnabled() + { + return $this->config['web']['enable']; + } + + public function isEnabled() + { + return $this->config['enable']; + } + + public function isAuthenticationEnabled() + { + return $this->config['authentication']; + } }