Skip to content

Commit 0cb8fa4

Browse files
Clockwork/Support/Psr/Middleware.php
1 parent 0f50ac2 commit 0cb8fa4

1 file changed

Lines changed: 244 additions & 0 deletions

File tree

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Clockwork\Support\Psr;
6+
7+
use Clockwork\Clockwork;
8+
use Clockwork\DataSource\PsrMessageDataSource;
9+
use Clockwork\Request\IncomingRequest;
10+
use Clockwork\Storage\Search;
11+
use Clockwork\Support\Vanilla\Clockwork as VanillaClockwork;
12+
use Clockwork\Web\Web;
13+
use Psr\Http\Message\ResponseFactoryInterface;
14+
use Psr\Http\Message\ResponseInterface;
15+
use Psr\Http\Message\ServerRequestInterface;
16+
use Psr\Http\Message\StreamFactoryInterface;
17+
use Psr\Http\Server\MiddlewareInterface;
18+
use Psr\Http\Server\RequestHandlerInterface;
19+
20+
/**
21+
* middleware based on PHP's PSR specifications
22+
*
23+
* Many frameworks are built on the PSR specifications, which makes
24+
* this implementation interoperable with them.
25+
*
26+
* TODO:
27+
* - It would be preferable to detach the `VanillaClockwork` from its
28+
* contained `Clockwork` instance. The former is currently only used
29+
* to access the configuration.
30+
*
31+
* References:
32+
* - https://www.php-fig.org/psr/psr-7/ -- request/response interfaces
33+
* - https://www.php-fig.org/psr/psr-15/ -- middleware interface
34+
* - https://www.php-fig.org/psr/psr-17/ -- response factory interface
35+
*/
36+
class Middleware implements MiddlewareInterface
37+
{
38+
private VanillaClockwork $clockwork;
39+
private ResponseFactoryInterface $responseFactory;
40+
private StreamFactoryInterface $streamFactory;
41+
42+
public function __construct(
43+
VanillaClockwork $clockwork,
44+
ResponseFactoryInterface $responseFactory,
45+
StreamFactoryInterface $streamFactory
46+
) {
47+
$this->clockwork = $clockwork;
48+
$this->responseFactory = $responseFactory;
49+
$this->streamFactory = $streamFactory;
50+
}
51+
52+
/**
53+
* this methods implements the PSR-15 MiddlewareInterface
54+
*/
55+
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
56+
{
57+
if (!$this->clockwork->isEnabled()) {
58+
return $handler->handle($request);
59+
}
60+
61+
if ($this->clockwork->isWebEnabled() && $this->isWebRequest($request)) {
62+
$response = $this->handleWebRequest($request);
63+
} elseif ($this->isApiRequest($request)) {
64+
$response = $this->handleApiRequest($request);
65+
} else {
66+
// Inject clockwork instance into the request, useful to log new events or timeline
67+
$response = $handler->handle(
68+
$request->withAttribute('Clockwork', $this->clockwork->getClockwork())
69+
);
70+
71+
$uri = $request->getUri();
72+
parse_str($uri->getQuery(), $input);
73+
$rx = new IncomingRequest([
74+
'method' => $request->getMethod(),
75+
'uri' => $uri->__toString(),
76+
'input' => $input,
77+
'cookies' => $request->getCookieParams(),
78+
]);
79+
80+
if (!$this->clockwork->getClockwork()->shouldCollect()->filter($rx)) {
81+
return $response;
82+
}
83+
if (!$this->clockwork->getClockwork()->shouldRecord()->filter($this->clockwork->request())) {
84+
return $response;
85+
}
86+
87+
$this->clockwork->getClockwork()->addDataSource(new PsrMessageDataSource($request, $response));
88+
$this->clockwork->getClockwork()->resolveRequest()->storeRequest();
89+
}
90+
91+
// enrich response with clockwork data
92+
$clockworkRequest = $this->clockwork->request();
93+
$response = $response->withHeader('X-Clockwork-Id', $clockworkRequest->id);
94+
$response = $response->withHeader('X-Clockwork-Version', Clockwork::VERSION);
95+
$response = $response->withHeader('X-Clockwork-Path', $this->clockwork->getApiPath());
96+
97+
// TODO: reactivate/reimplement
98+
// foreach ($this->config['headers'] as $headerName => $headerValue) {
99+
// $this->setHeader("X-Clockwork-Header-{$headerName}", $headerValue);
100+
// }
101+
102+
// TODO: reactivate/reimplement
103+
// if ($this->config['features']['performance']['client_metrics'] || $this->config['toolbar']) {
104+
// $this->setCookie('x-clockwork', $this->getCookiePayload(), time() + 60);
105+
// }
106+
107+
// TODO: reactivate/reimplement
108+
// if (($eventsCount = $this->config['server_timing']) !== false) {
109+
// $this->setHeader('Server-Timing', ServerTiming::fromRequest($this->clockwork->request(), $eventsCount)->value());
110+
// }
111+
112+
return $response;
113+
}
114+
115+
private function isWebRequest(ServerRequestInterface $request): bool
116+
{
117+
$requestPath = $request->getUri()->getPath();
118+
$webPath = $this->clockwork->getWebPath();
119+
// handle "/web" case
120+
if ($requestPath === $webPath) {
121+
return true;
122+
}
123+
// Handle other "/web/something" cases. Note that we don't want
124+
// e.g. "/webcam".
125+
return str_starts_with($requestPath, $webPath . '/');
126+
}
127+
128+
private function isApiRequest(ServerRequestInterface $request): bool
129+
{
130+
$requestPath = $request->getUri()->getPath();
131+
$apiPath = rtrim($this->clockwork->getApiPath(), '/');
132+
return str_starts_with($requestPath, $apiPath . '/');
133+
}
134+
135+
private function handleWebRequest(ServerRequestInterface $request): ResponseInterface
136+
{
137+
$requestPath = $request->getUri()->getPath();
138+
$webPath = $this->clockwork->getWebPath();
139+
140+
$relativePath = ltrim(substr($requestPath, strlen($webPath)), '/');
141+
142+
// handle "/web" and "/web/" cases
143+
if ($relativePath === '') {
144+
return $this->responseFactory->createResponse(302)
145+
->withHeader(
146+
'location',
147+
$request->getUri()->withPath($webPath . '/index.html')->__toString()
148+
);
149+
}
150+
151+
$web = new Web();
152+
$asset = $web->asset($relativePath);
153+
if ($asset === null) {
154+
return $this->responseFactory->createResponse(404);
155+
}
156+
return $this->responseFactory->createResponse()
157+
->withAddedHeader('Content-type', $asset['mime'])
158+
->withBody($this->streamFactory->createStreamFromFile($asset['path']));
159+
}
160+
161+
private function handleApiRequest(ServerRequestInterface $request): ResponseInterface
162+
{
163+
$apiPath = rtrim($this->clockwork->getApiPath(), '/');
164+
165+
// auth API is a single endpoint
166+
// Note that this must be handled before checking authentication,
167+
// lest we end up with a circular dependency.
168+
$requestPath = $request->getUri()->getPath();
169+
if ($requestPath === $apiPath . '/auth') {
170+
$username = $request->getHeader('username')[0] ?? '';
171+
$password = $request->getHeader('password')[0] ?? '';
172+
173+
$token = $this->clockwork->getClockwork()->authenticator()->attempt([
174+
'username' => $username,
175+
'password' => $password,
176+
]);
177+
178+
return $this->responseFactory->createResponse($token ? 200 : 403)
179+
->withAddedHeader('Content-type', 'application/json')
180+
->withBody($this->streamFactory->createStream(
181+
json_encode(
182+
['token' => $token],
183+
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
184+
)
185+
));
186+
}
187+
188+
// check request authentication
189+
$authHeaders = $request->getHeader('HTTP_X_CLOCKWORK_AUTH');
190+
$authenticator = $this->clockwork->getClockwork()->authenticator();
191+
if (!$authenticator->check($authHeaders[0] ?? '')) {
192+
return $this->responseFactory->createResponse(403)
193+
->withAddedHeader('Content-type', 'application/json')
194+
->withBody($this->streamFactory->createStream(
195+
json_encode(
196+
[ 'requires' => $authenticator->requires() ],
197+
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
198+
)
199+
));
200+
}
201+
202+
// load metadata
203+
if (!preg_match(
204+
"#$apiPath/(?<id>[0-9-]+|latest)(?:/(?<direction>next|previous))?(?:/(?<count>\d+))?#",
205+
$request->getUri()->__toString(),
206+
$matches
207+
)) {
208+
return $this->responseFactory->createResponse(404);
209+
}
210+
$id = isset($matches['id']) ? $matches['id'] : null;
211+
$direction = isset($matches['direction']) ? $matches['direction'] : null;
212+
$count = isset($matches['count']) ? $matches['count'] : null;
213+
$storage = $this->clockwork->getClockwork()->storage();
214+
if ($direction == 'previous') {
215+
$data = $storage->previous($id, $count, Search::fromRequest($_GET));
216+
} elseif ($direction == 'next') {
217+
$data = $storage->next($id, $count, Search::fromRequest($_GET));
218+
} elseif ($id == 'latest') {
219+
$data = $storage->latest(Search::fromRequest($_GET));
220+
} else {
221+
$data = $storage->find($id);
222+
}
223+
if ($data === null) {
224+
return $this->responseFactory->createResponse(404);
225+
}
226+
227+
// load extended metadata if requested
228+
if (preg_match("#$apiPath/(?<id>[0-9-]+|latest)/extended#", $request->getUri()->__toString())) {
229+
$this->clockwork->getClockwork()->extendRequest($data);
230+
}
231+
232+
$body = is_array($data)
233+
? array_map(static function ($item) { return $item->toArray(); }, $data)
234+
: $data->toArray();
235+
return $this->responseFactory->createResponse()
236+
->withAddedHeader('Content-type', 'application/json')
237+
->withBody($this->streamFactory->createStream(
238+
json_encode(
239+
$body,
240+
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
241+
)
242+
));
243+
}
244+
}

0 commit comments

Comments
 (0)