Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
199 lars 1
<?php namespace Clockwork\Support\Vanilla;
2
 
3
use Clockwork\Clockwork as BaseClockwork;
4
use Clockwork\Authentication\NullAuthenticator;
5
use Clockwork\Authentication\SimpleAuthenticator;
6
use Clockwork\DataSource\PhpDataSource;
7
use Clockwork\DataSource\PsrMessageDataSource;
8
use Clockwork\Helpers\Serializer;
9
use Clockwork\Helpers\ServerTiming;
10
use Clockwork\Helpers\StackFilter;
11
use Clockwork\Request\IncomingRequest;
12
use Clockwork\Storage\FileStorage;
13
use Clockwork\Storage\Search;
14
use Clockwork\Storage\SqlStorage;
15
 
16
use Psr\Http\Message\ServerRequestInterface as PsrRequest;
17
use Psr\Http\Message\ResponseInterface as PsrResponse;
18
 
19
// Clockwork integration for vanilla php and unsupported frameworks
20
class Clockwork
21
{
22
	// Clockwork config
23
	protected $config;
24
	// Clockwork instance
25
	protected $clockwork;
26
 
27
	// PSR-7 request and response
28
	protected $psrRequest;
29
	protected $psrResponse;
30
 
31
	// Whether the headers were already sent (header can be sent manually)
32
	protected $headersSent = false;
33
 
34
	// Static instance when used as singleton
35
	protected static $defaultInstance;
36
 
37
	// Create new instance, takes an additional config
38
	public function __construct($config = [])
39
	{
40
		$this->config = array_merge(include __DIR__ . '/config.php', $config);
41
 
42
		$this->clockwork = new BaseClockwork;
43
 
44
		$this->clockwork->addDataSource(new PhpDataSource);
45
		$this->clockwork->storage($this->makeStorage());
46
		$this->clockwork->authenticator($this->makeAuthenticator());
47
 
48
		$this->configureSerializer();
49
		$this->configureShouldCollect();
50
		$this->configureShouldRecord();
51
 
52
		if ($this->config['register_helpers']) include __DIR__ . '/helpers.php';
53
	}
54
 
55
	// Initialize a singleton instance, takes an additional config
56
	public static function init($config = [])
57
	{
58
		return static::$defaultInstance = new static($config);
59
	}
60
 
61
	// Return the singleton instance
62
	public static function instance()
63
	{
64
		return static::$defaultInstance;
65
	}
66
 
67
	// Resolves and records the current request and sends Clockwork headers, should be called at the end of app
68
	// execution, return PSR-7 response if one was set
69
	public function requestProcessed()
70
	{
71
		if (! $this->config['enable'] && ! $this->config['collect_data_always']) return $this->psrResponse;
72
 
73
		if (! $this->clockwork->shouldCollect()->filter($this->incomingRequest())) return $this->psrResponse;
74
		if (! $this->clockwork->shouldRecord()->filter($this->clockwork->request())) return $this->psrResponse;
75
 
76
		$this->clockwork->resolveRequest()->storeRequest();
77
 
78
		if (! $this->config['enable']) return $this->psrResponse;
79
 
80
		$this->sendHeaders();
81
 
82
		if (($eventsCount = $this->config['server_timing']) !== false) {
83
			$this->setHeader('Server-Timing', ServerTiming::fromRequest($this->clockwork->request(), $eventsCount)->value());
84
		}
85
 
86
		return $this->psrResponse;
87
	}
88
 
89
	// Resolves and records the current request as a command, should be called at the end of app execution
90
	public function commandExecuted($name, $exitCode = null, $arguments = [], $options = [], $argumentsDefaults = [], $optionsDefaults = [], $output = null)
91
	{
92
		if (! $this->config['enable'] && ! $this->config['collect_data_always']) return;
93
 
94
		if (! $this->clockwork->shouldRecord()->filter($this->clockwork->request())) return;
95
 
96
		$this->clockwork
97
			->resolveAsCommand($name, $exitCode, $arguments, $options, $argumentsDefaults, $optionsDefaults, $output)
98
			->storeRequest();
99
	}
100
 
101
	// Resolves and records the current request as a queue job, should be called at the end of app execution
102
	public function queueJobExecuted($name, $description = null, $status = 'processed', $payload = [], $queue = null, $connection = null, $options = [])
103
	{
104
		if (! $this->config['enable'] && ! $this->config['collect_data_always']) return;
105
 
106
		if (! $this->clockwork->shouldRecord()->filter($this->clockwork->request())) return;
107
 
108
		$this->clockwork
109
			->resolveAsQueueJob($name, $description, $status, $payload, $queue, $connection, $options)
110
			->storeRequest();
111
	}
112
 
113
	// Manually send the Clockwork headers, this should be manually called only when the headers need to be sent early
114
	// in the request processing
115
	public function sendHeaders()
116
	{
117
		if (! $this->config['enable'] || $this->headersSent) return;
118
 
119
		$this->headersSent = true;
120
 
121
		$clockworkRequest = $this->request();
122
 
123
		$this->setHeader('X-Clockwork-Id', $clockworkRequest->id);
124
		$this->setHeader('X-Clockwork-Version', BaseClockwork::VERSION);
125
 
126
		if ($this->config['api'] != '/__clockwork/') {
127
			$this->setHeader('X-Clockwork-Path', $this->config['api']);
128
		}
129
 
130
		foreach ($this->config['headers'] as $headerName => $headerValue) {
131
			$this->setHeader("X-Clockwork-Header-{$headerName}", $headerValue);
132
		}
133
 
134
		if ($this->config['features']['performance']['client_metrics'] || $this->config['toolbar']) {
135
			$this->setCookie('x-clockwork', $this->getCookiePayload(), time() + 60);
136
		}
137
	}
138
 
139
	// Returns the x-clockwork cookie payload in case you need to set the cookie yourself (cookie can't be http only,
140
	// expiration time should be 60 seconds)
141
	public function getCookiePayload()
142
	{
143
		$clockworkRequest = $this->request();
144
 
145
		return json_encode([
146
			'requestId' => $clockworkRequest->id,
147
			'version'   => BaseClockwork::VERSION,
148
			'path'      => $this->config['api'],
149
			'webPath'   => $this->config['web']['enable'],
150
			'token'     => $clockworkRequest->updateToken,
151
			'metrics'   => $this->config['features']['performance']['client_metrics'],
152
			'toolbar'   => $this->config['toolbar']
153
		]);
154
	}
155
 
156
	// Handle Clockwork REST api request, retrieves or updates Clockwork metadata
157
	public function handleMetadata($request = null, $method = null)
158
	{
159
		if (! $request) $request = isset($_GET['request']) ? $_GET['request'] : '';
160
		if (! $method) $method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET';
161
 
162
		if ($method == 'POST' && $request == 'auth') return $this->authenticate();
163
 
164
		return $method == 'POST' ? $this->updateMetadata($request) : $this->returnMetadata($request);
165
	}
166
 
167
	// Retrieve metadata based on the passed Clockwork REST api request and send HTTP response
168
	public function returnMetadata($request = null)
169
	{
170
		if (! $this->config['enable']) return $this->response(null, 404);
171
 
172
		$authenticator = $this->clockwork->authenticator();
173
		$authenticated = $authenticator->check(isset($_SERVER['HTTP_X_CLOCKWORK_AUTH']) ? $_SERVER['HTTP_X_CLOCKWORK_AUTH'] : '');
174
 
175
		if ($authenticated !== true) {
176
			return $this->response([ 'message' => $authenticated, 'requires' => $authenticator->requires() ], 403);
177
		}
178
 
179
		return $this->response($this->getMetadata($request));
180
	}
181
 
182
	// Returns metadata based on the passed Clockwork REST api request
183
	public function getMetadata($request = null)
184
	{
185
		if (! $this->config['enable']) return;
186
 
187
		$authenticator = $this->clockwork->authenticator();
188
		$authenticated = $authenticator->check(isset($_SERVER['HTTP_X_CLOCKWORK_AUTH']) ? $_SERVER['HTTP_X_CLOCKWORK_AUTH'] : '');
189
 
190
		if ($authenticated !== true) return;
191
 
192
		if (! $request) $request = isset($_GET['request']) ? $_GET['request'] : '';
193
 
194
		preg_match('#(?<id>[0-9-]+|latest)(?:/(?<direction>next|previous))?(?:/(?<count>\d+))?#', $request, $matches);
195
 
196
		$id = isset($matches['id']) ? $matches['id'] : null;
197
		$direction = isset($matches['direction']) ? $matches['direction'] : null;
198
		$count = isset($matches['count']) ? $matches['count'] : null;
199
 
200
		if ($direction == 'previous') {
201
			$data = $this->clockwork->storage()->previous($id, $count, Search::fromRequest($_GET));
202
		} elseif ($direction == 'next') {
203
			$data = $this->clockwork->storage()->next($id, $count, Search::fromRequest($_GET));
204
		} elseif ($id == 'latest') {
205
			$data = $this->clockwork->storage()->latest(Search::fromRequest($_GET));
206
		} else {
207
			$data = $this->clockwork->storage()->find($id);
208
		}
209
 
210
		if (preg_match('#(?<id>[0-9-]+|latest)/extended#', $request)) {
211
			$this->clockwork->extendRequest($data);
212
		}
213
 
214
		if ($data) {
215
			$data = is_array($data) ? array_map(function ($item) { return $item->toArray(); }, $data) : $data->toArray();
216
		}
217
 
218
		return $data;
219
	}
220
 
221
	// Update metadata based on the passed Clockwork REST api request and send HTTP response
222
	public function updateMetadata($request = null)
223
	{
224
		if (! $this->config['enable'] || ! $this->config['features']['performance']['client_metrics']) {
225
			return $this->response(null, 404);
226
		}
227
 
228
		if (! $request) $request = isset($_GET['request']) ? $_GET['request'] : '';
229
 
230
		$storage = $this->clockwork->storage();
231
 
232
		$request = $storage->find($request);
233
 
234
		if (! $request) {
235
			return $this->response([ 'message' => 'Request not found.' ], 404);
236
		}
237
 
238
		$input = json_decode(file_get_contents('php://input'), true);
239
 
240
		$token = isset($input['_token']) ? $input['_token'] : '';
241
 
242
		if (! $request->updateToken || ! hash_equals($request->updateToken, $token)) {
243
			return $this->response([ 'message' => 'Invalid update token.' ], 403);
244
		}
245
 
246
		foreach ($input as $key => $value) {
247
			if (in_array($key, [ 'clientMetrics', 'webVitals' ])) {
248
				$request->$key = $value;
249
			}
250
		}
251
 
252
		$storage->update($request);
253
 
254
		return $this->response();
255
	}
256
 
257
	// Authanticates access to Clockwork REST api
258
	public function authenticate($request = null)
259
	{
260
		if (! $this->config['enable']) return;
261
 
262
		if (! $request) $request = isset($_GET['request']) ? $_GET['request'] : '';
263
 
264
		$token = $this->clockwork->authenticator()->attempt([
265
			'username' => isset($_POST['username']) ? $_POST['username'] : '',
266
			'password' => isset($_POST['password']) ? $_POST['password'] : ''
267
		]);
268
 
269
		return $this->response([ 'token' => $token ], $token ? 200 : 403);
270
	}
271
 
272
	// Returns the Clockwork Web UI as a HTTP response, installs the Web UI on the first run
273
	public function returnWeb()
274
	{
275
		if (! $this->config['web']['enable']) return;
276
 
277
		$this->installWeb();
278
 
279
		$asset = function ($uri) { return "{$this->config['web']['uri']}/{$uri}"; };
280
		$metadataPath = $this->config['api'];
281
		$url = $this->config['web']['uri'];
282
 
283
		if (! preg_match('#/index.html$#', $url)) {
284
			$url = rtrim($url, '/') . '/index.html';
285
		}
286
 
287
		ob_start();
288
 
289
		include __DIR__ . '/iframe.html.php';
290
 
291
		$html = ob_get_clean();
292
 
293
		return $this->response($html, null, false);
294
	}
295
 
296
	// Installs the Web UI by copying the assets to the public directory, no-op if already installed
297
	public function installWeb()
298
	{
299
		$path = $this->config['web']['path'];
300
		$source = __DIR__ . '/../../Web/public';
301
 
302
		if (file_exists("{$path}/index.html")) return;
303
 
304
		@mkdir($path, 0755, true);
305
 
306
		$iterator = new \RecursiveIteratorIterator(
307
			new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
308
			\RecursiveIteratorIterator::SELF_FIRST
309
		);
310
 
311
		foreach ($iterator as $item) {
312
			if ($item->isDir()) {
313
				mkdir("{$path}/" . $iterator->getSubPathName());
314
			} else {
315
				copy($item, "{$path}/" . $iterator->getSubPathName());
316
			}
317
		}
318
	}
319
 
320
	// Use a PSR-7 request and response instances instead of vanilla php HTTP apis
321
	public function usePsrMessage(PsrRequest $request, PsrResponse $response = null)
322
	{
323
		$this->psrRequest = $request;
324
		$this->psrResponse = $response;
325
 
326
		$this->clockwork->addDataSource(new PsrMessageDataSource($request, $response));
327
 
328
		return $this;
329
	}
330
 
331
	// Make a storage implementation based on user configuration
332
	protected function makeStorage()
333
	{
334
		if ($this->config['storage'] == 'sql') {
335
			$database = $this->config['storage_sql_database'];
336
			$table = $this->config['storage_sql_table'];
337
 
338
			$storage = new SqlStorage(
339
				$this->config['storage_sql_database'],
340
				$this->config['storage_sql_table'],
341
				$this->config['storage_sql_username'],
342
				$this->config['storage_sql_password'],
343
				$this->config['storage_expiration']
344
			);
345
		} else {
346
			$storage = new FileStorage(
347
				$this->config['storage_files_path'],
348
				0700,
349
				$this->config['storage_expiration'],
350
				$this->config['storage_files_compress']
351
			);
352
		}
353
 
354
		return $storage;
355
	}
356
 
357
	// Make an authenticator implementation based on user configuration
358
	protected function makeAuthenticator()
359
	{
360
		$authenticator = $this->config['authentication'];
361
 
362
		if (is_string($authenticator)) {
363
			return new $authenticator;
364
		} elseif ($authenticator) {
365
			return new SimpleAuthenticator($this->config['authentication_password']);
366
		} else {
367
			return new NullAuthenticator;
368
		}
369
	}
370
 
371
	// Configure serializer defaults based on user configuration
372
	protected function configureSerializer()
373
	{
374
		Serializer::defaults([
375
			'limit'       => $this->config['serialization_depth'],
376
			'blackbox'    => $this->config['serialization_blackbox'],
377
			'traces'      => $this->config['stack_traces']['enabled'],
378
			'tracesSkip'  => StackFilter::make()
379
				->isNotVendor(array_merge(
380
					$this->config['stack_traces']['skip_vendors'],
381
					[ 'itsgoingd', 'laravel', 'illuminate' ]
382
				))
383
				->isNotNamespace($this->config['stack_traces']['skip_namespaces'])
384
				->isNotFunction([ 'call_user_func', 'call_user_func_array' ])
385
				->isNotClass($this->config['stack_traces']['skip_classes']),
386
			'tracesLimit' => $this->config['stack_traces']['limit']
387
		]);
388
	}
389
 
390
	// Configure should collect rules based on user configuration
391
	public function configureShouldCollect()
392
	{
393
		$this->clockwork->shouldCollect([
394
			'onDemand'        => $this->config['requests']['on_demand'],
395
			'sample'          => $this->config['requests']['sample'],
396
			'except'          => $this->config['requests']['except'],
397
			'only'            => $this->config['requests']['only'],
398
			'exceptPreflight' => $this->config['requests']['except_preflight']
399
		]);
400
 
401
		// don't collect data for Clockwork requests
402
		$this->clockwork->shouldCollect()->except(preg_quote(rtrim($this->config['api'], '/'), '#'));
403
	}
404
 
405
	// Configure should record rules based on user configuration
406
	public function configureShouldRecord()
407
	{
408
		$this->clockwork->shouldRecord([
409
			'errorsOnly' => $this->config['requests']['errors_only'],
410
			'slowOnly'   => $this->config['requests']['slow_only'] ? $this->config['requests']['slow_threshold'] : false
411
		]);
412
	}
413
 
414
	// Set a cookie on PSR-7 response or using vanilla php
415
	protected function setCookie($name, $value, $expires) {
416
		if ($this->psrResponse) {
417
			$this->psrResponse = $this->psrResponse->withAddedHeader(
418
				'Set-Cookie', "{$name}=" . urlencode($value) . '; expires=' . gmdate('D, d M Y H:i:s T', $expires)
419
			);
420
		} else {
421
			setcookie($name, $value, $expires);
422
		}
423
	}
424
 
425
	// Set a header on PSR-7 response or using vanilla php
426
	protected function setHeader($header, $value)
427
	{
428
		if ($this->psrResponse) {
429
			$this->psrResponse = $this->psrResponse->withHeader($header, $value);
430
		} else {
431
			header("{$header}: {$value}");
432
		}
433
	}
434
 
435
	// Send a json response, uses the PSR-7 response if set
436
	protected function response($data = null, $status = null, $json = true)
437
	{
438
		if ($json) $this->setHeader('Content-Type', 'application/json');
439
 
440
		if ($this->psrResponse) {
441
			if ($status) $this->psrResponse = $this->psrResponse->withStatus($status);
442
			$this->psrResponse->getBody()->write($json ? json_encode($data, \JSON_PARTIAL_OUTPUT_ON_ERROR) : $data);
443
			return $this->psrResponse;
444
		} else {
445
			if ($status) http_response_code($status);
446
			echo $json ? json_encode($data, \JSON_PARTIAL_OUTPUT_ON_ERROR) : $data;
447
		}
448
	}
449
 
450
	// Make a Clockwork incoming request instance
451
	protected function incomingRequest()
452
	{
453
		return new IncomingRequest([
454
			'method'  => $_SERVER['REQUEST_METHOD'],
455
			'uri'     => $_SERVER['REQUEST_URI'],
456
			'input'   => $_REQUEST,
457
			'cookies' => $_COOKIE
458
		]);
459
	}
460
 
461
	// Return the underlying Clockwork instance
462
	public function getClockwork()
463
	{
464
		return $this->clockwork;
465
	}
466
 
467
	// Pass any method calls to the underlying Clockwork instance
468
	public function __call($method, $args = [])
469
	{
470
		return $this->clockwork->$method(...$args);
471
	}
472
 
473
	// Pass any static method calls to the underlying Clockwork instance
474
	public static function __callStatic($method, $args = [])
475
	{
476
		return static::instance()->$method(...$args);
477
	}
478
}