PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / trunk
Yoast SEO – Advanced SEO with real-time guidance and built-in AI vtrunk
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
← All changes | vendor_prefixed/guzzlehttp/guzzle/src/Handler/StreamHandler.php +0 -464 27.7 → trunk View file →
@@ -1,464 +1,0 @@
1 -<?php
2 -
3 -namespace YoastSEO_Vendor\GuzzleHttp\Handler;
4 -
5 -use YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException;
6 -use YoastSEO_Vendor\GuzzleHttp\Exception\RequestException;
7 -use YoastSEO_Vendor\GuzzleHttp\Promise as P;
8 -use YoastSEO_Vendor\GuzzleHttp\Promise\FulfilledPromise;
9 -use YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface;
10 -use YoastSEO_Vendor\GuzzleHttp\Psr7;
11 -use YoastSEO_Vendor\GuzzleHttp\TransferStats;
12 -use YoastSEO_Vendor\GuzzleHttp\Utils;
13 -use YoastSEO_Vendor\Psr\Http\Message\RequestInterface;
14 -use YoastSEO_Vendor\Psr\Http\Message\ResponseInterface;
15 -use YoastSEO_Vendor\Psr\Http\Message\StreamInterface;
16 -use YoastSEO_Vendor\Psr\Http\Message\UriInterface;
17 -/**
18 - * HTTP handler that uses PHP's HTTP stream wrapper.
19 - *
20 - * @final
21 - */
22 -class StreamHandler
23 -{
24 - /**
25 - * @var array
26 - */
27 - private $lastHeaders = [];
28 - /**
29 - * Sends an HTTP request.
30 - *
31 - * @param RequestInterface $request Request to send.
32 - * @param array $options Request transfer options.
33 - */
34 - public function __invoke(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface
35 - {
36 - // Sleep if there is a delay specified.
37 - if (isset($options['delay'])) {
38 - \usleep($options['delay'] * 1000);
39 - }
40 - $protocolVersion = $request->getProtocolVersion();
41 - if ('1.0' !== $protocolVersion && '1.1' !== $protocolVersion) {
42 - throw new \YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException(\sprintf('HTTP/%s is not supported by the stream handler.', $protocolVersion), $request);
43 - }
44 - $startTime = isset($options['on_stats']) ? \YoastSEO_Vendor\GuzzleHttp\Utils::currentTime() : null;
45 - try {
46 - // Does not support the expect header.
47 - $request = $request->withoutHeader('Expect');
48 - // Append a content-length header if body size is zero to match
49 - // the behavior of `CurlHandler`
50 - if ((0 === \strcasecmp('PUT', $request->getMethod()) || 0 === \strcasecmp('POST', $request->getMethod())) && 0 === $request->getBody()->getSize()) {
51 - $request = $request->withHeader('Content-Length', '0');
52 - }
53 - return $this->createResponse($request, $options, $this->createStream($request, $options), $startTime);
54 - } catch (\InvalidArgumentException $e) {
55 - throw $e;
56 - } catch (\Exception $e) {
57 - // Determine if the error was a networking error.
58 - $message = $e->getMessage();
59 - // This list can probably get more comprehensive.
60 - if (\false !== \strpos($message, 'getaddrinfo') || \false !== \strpos($message, 'Connection refused') || \false !== \strpos($message, "couldn't connect to host") || \false !== \strpos($message, 'connection attempt failed')) {
61 - $e = new \YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException($e->getMessage(), $request, $e);
62 - } else {
63 - $e = \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException::wrapException($request, $e);
64 - }
65 - $this->invokeStats($options, $request, $startTime, null, $e);
66 - return \YoastSEO_Vendor\GuzzleHttp\Promise\Create::rejectionFor($e);
67 - }
68 - }
69 - private function invokeStats(array $options, \YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, ?float $startTime, ?\YoastSEO_Vendor\Psr\Http\Message\ResponseInterface $response = null, ?\Throwable $error = null) : void
70 - {
71 - if (isset($options['on_stats'])) {
72 - $stats = new \YoastSEO_Vendor\GuzzleHttp\TransferStats($request, $response, \YoastSEO_Vendor\GuzzleHttp\Utils::currentTime() - $startTime, $error, []);
73 - $options['on_stats']($stats);
74 - }
75 - }
76 - /**
77 - * @param resource $stream
78 - */
79 - private function createResponse(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options, $stream, ?float $startTime) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface
80 - {
81 - $hdrs = $this->lastHeaders;
82 - $this->lastHeaders = [];
83 - try {
84 - [$ver, $status, $reason, $headers] = \YoastSEO_Vendor\GuzzleHttp\Handler\HeaderProcessor::parseHeaders($hdrs);
85 - } catch (\Exception $e) {
86 - return \YoastSEO_Vendor\GuzzleHttp\Promise\Create::rejectionFor(new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException('An error was encountered while creating the response', $request, null, $e));
87 - }
88 - [$stream, $headers] = $this->checkDecode($options, $headers, $stream);
89 - $stream = \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor($stream);
90 - $sink = $stream;
91 - if (\strcasecmp('HEAD', $request->getMethod())) {
92 - $sink = $this->createSink($stream, $options);
93 - }
94 - try {
95 - $response = new \YoastSEO_Vendor\GuzzleHttp\Psr7\Response($status, $headers, $sink, $ver, $reason);
96 - } catch (\Exception $e) {
97 - return \YoastSEO_Vendor\GuzzleHttp\Promise\Create::rejectionFor(new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException('An error was encountered while creating the response', $request, null, $e));
98 - }
99 - if (isset($options['on_headers'])) {
100 - try {
101 - $options['on_headers']($response);
102 - } catch (\Exception $e) {
103 - return \YoastSEO_Vendor\GuzzleHttp\Promise\Create::rejectionFor(new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException('An error was encountered during the on_headers event', $request, $response, $e));
104 - }
105 - }
106 - // Do not drain when the request is a HEAD request because they have
107 - // no body.
108 - if ($sink !== $stream) {
109 - $this->drain($stream, $sink, $response->getHeaderLine('Content-Length'));
110 - }
111 - $this->invokeStats($options, $request, $startTime, $response, null);
112 - return new \YoastSEO_Vendor\GuzzleHttp\Promise\FulfilledPromise($response);
113 - }
114 - private function createSink(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $stream, array $options) : \YoastSEO_Vendor\Psr\Http\Message\StreamInterface
115 - {
116 - if (!empty($options['stream'])) {
117 - return $stream;
118 - }
119 - $sink = $options['sink'] ?? \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::tryFopen('php://temp', 'r+');
120 - return \is_string($sink) ? new \YoastSEO_Vendor\GuzzleHttp\Psr7\LazyOpenStream($sink, 'w+') : \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor($sink);
121 - }
122 - /**
123 - * @param resource $stream
124 - */
125 - private function checkDecode(array $options, array $headers, $stream) : array
126 - {
127 - // Automatically decode responses when instructed.
128 - if (!empty($options['decode_content'])) {
129 - $normalizedKeys = \YoastSEO_Vendor\GuzzleHttp\Utils::normalizeHeaderKeys($headers);
130 - if (isset($normalizedKeys['content-encoding'])) {
131 - $encoding = $headers[$normalizedKeys['content-encoding']];
132 - if ($encoding[0] === 'gzip' || $encoding[0] === 'deflate') {
133 - $stream = new \YoastSEO_Vendor\GuzzleHttp\Psr7\InflateStream(\YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::streamFor($stream));
134 - $headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']];
135 - // Remove content-encoding header
136 - unset($headers[$normalizedKeys['content-encoding']]);
137 - // Fix content-length header
138 - if (isset($normalizedKeys['content-length'])) {
139 - $headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']];
140 - $length = (int) $stream->getSize();
141 - if ($length === 0) {
142 - unset($headers[$normalizedKeys['content-length']]);
143 - } else {
144 - $headers[$normalizedKeys['content-length']] = [$length];
145 - }
146 - }
147 - }
148 - }
149 - }
150 - return [$stream, $headers];
151 - }
152 - /**
153 - * Drains the source stream into the "sink" client option.
154 - *
155 - * @param string $contentLength Header specifying the amount of
156 - * data to read.
157 - *
158 - * @throws \RuntimeException when the sink option is invalid.
159 - */
160 - private function drain(\YoastSEO_Vendor\Psr\Http\Message\StreamInterface $source, \YoastSEO_Vendor\Psr\Http\Message\StreamInterface $sink, string $contentLength) : \YoastSEO_Vendor\Psr\Http\Message\StreamInterface
161 - {
162 - // If a content-length header is provided, then stop reading once
163 - // that number of bytes has been read. This can prevent infinitely
164 - // reading from a stream when dealing with servers that do not honor
165 - // Connection: Close headers.
166 - \YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::copyToStream($source, $sink, \strlen($contentLength) > 0 && (int) $contentLength > 0 ? (int) $contentLength : -1);
167 - $sink->seek(0);
168 - $source->close();
169 - return $sink;
170 - }
171 - /**
172 - * Create a resource and check to ensure it was created successfully
173 - *
174 - * @param callable $callback Callable that returns stream resource
175 - *
176 - * @return resource
177 - *
178 - * @throws \RuntimeException on error
179 - */
180 - private function createResource(callable $callback)
181 - {
182 - $errors = [];
183 - \set_error_handler(static function ($_, $msg, $file, $line) use(&$errors) : bool {
184 - $errors[] = ['message' => $msg, 'file' => $file, 'line' => $line];
185 - return \true;
186 - });
187 - try {
188 - $resource = $callback();
189 - } finally {
190 - \restore_error_handler();
191 - }
192 - if (!$resource) {
193 - $message = 'Error creating resource: ';
194 - foreach ($errors as $err) {
195 - foreach ($err as $key => $value) {
196 - $message .= "[{$key}] {$value}" . \PHP_EOL;
197 - }
198 - }
199 - throw new \RuntimeException(\trim($message));
200 - }
201 - return $resource;
202 - }
203 - /**
204 - * @return resource
205 - */
206 - private function createStream(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options)
207 - {
208 - static $methods;
209 - if (!$methods) {
210 - $methods = \array_flip(\get_class_methods(__CLASS__));
211 - }
212 - if (!\in_array($request->getUri()->getScheme(), ['http', 'https'])) {
213 - throw new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException(\sprintf("The scheme '%s' is not supported.", $request->getUri()->getScheme()), $request);
214 - }
215 - // HTTP/1.1 streams using the PHP stream wrapper require a
216 - // Connection: close header
217 - if ($request->getProtocolVersion() === '1.1' && !$request->hasHeader('Connection')) {
218 - $request = $request->withHeader('Connection', 'close');
219 - }
220 - // Ensure SSL is verified by default
221 - if (!isset($options['verify'])) {
222 - $options['verify'] = \true;
223 - }
224 - $params = [];
225 - $context = $this->getDefaultContext($request);
226 - if (isset($options['on_headers']) && !\is_callable($options['on_headers'])) {
227 - throw new \InvalidArgumentException('on_headers must be callable');
228 - }
229 - if (!empty($options)) {
230 - foreach ($options as $key => $value) {
231 - $method = "add_{$key}";
232 - if (isset($methods[$method])) {
233 - $this->{$method}($request, $context, $value, $params);
234 - }
235 - }
236 - }
237 - if (isset($options['stream_context'])) {
238 - if (!\is_array($options['stream_context'])) {
239 - throw new \InvalidArgumentException('stream_context must be an array');
240 - }
241 - $context = \array_replace_recursive($context, $options['stream_context']);
242 - }
243 - // Microsoft NTLM authentication only supported with curl handler
244 - if (isset($options['auth'][2]) && 'ntlm' === $options['auth'][2]) {
245 - throw new \InvalidArgumentException('Microsoft NTLM authentication only supported with curl handler');
246 - }
247 - $uri = $this->resolveHost($request, $options);
248 - $contextResource = $this->createResource(static function () use($context, $params) {
249 - return \stream_context_create($context, $params);
250 - });
251 - return $this->createResource(function () use($uri, $contextResource, $context, $options, $request) {
252 - $resource = @\fopen((string) $uri, 'r', \false, $contextResource);
253 - // See https://wiki.php.net/rfc/deprecations_php_8_5#deprecate_the_http_response_header_predefined_variable
254 - if (\function_exists('YoastSEO_Vendor\\http_get_last_response_headers')) {
255 - /** @var array|null */
256 - $http_response_header = \YoastSEO_Vendor\http_get_last_response_headers();
257 - }
258 - $this->lastHeaders = $http_response_header ?? [];
259 - if (\false === $resource) {
260 - throw new \YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException(\sprintf('Connection refused for URI %s', $uri), $request, null, $context);
261 - }
262 - if (isset($options['read_timeout'])) {
263 - $readTimeout = $options['read_timeout'];
264 - $sec = (int) $readTimeout;
265 - $usec = ($readTimeout - $sec) * 100000;
266 - \stream_set_timeout($resource, $sec, $usec);
267 - }
268 - return $resource;
269 - });
270 - }
271 - private function resolveHost(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) : \YoastSEO_Vendor\Psr\Http\Message\UriInterface
272 - {
273 - $uri = $request->getUri();
274 - if (isset($options['force_ip_resolve']) && !\filter_var($uri->getHost(), \FILTER_VALIDATE_IP)) {
275 - if ('v4' === $options['force_ip_resolve']) {
276 - $records = \dns_get_record($uri->getHost(), \DNS_A);
277 - if (\false === $records || !isset($records[0]['ip'])) {
278 - throw new \YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException(\sprintf("Could not resolve IPv4 address for host '%s'", $uri->getHost()), $request);
279 - }
280 - return $uri->withHost($records[0]['ip']);
281 - }
282 - if ('v6' === $options['force_ip_resolve']) {
283 - $records = \dns_get_record($uri->getHost(), \DNS_AAAA);
284 - if (\false === $records || !isset($records[0]['ipv6'])) {
285 - throw new \YoastSEO_Vendor\GuzzleHttp\Exception\ConnectException(\sprintf("Could not resolve IPv6 address for host '%s'", $uri->getHost()), $request);
286 - }
287 - return $uri->withHost('[' . $records[0]['ipv6'] . ']');
288 - }
289 - }
290 - return $uri;
291 - }
292 - private function getDefaultContext(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request) : array
293 - {
294 - $headers = '';
295 - foreach ($request->getHeaders() as $name => $value) {
296 - foreach ($value as $val) {
297 - $headers .= "{$name}: {$val}\r\n";
298 - }
299 - }
300 - $context = ['http' => ['method' => $request->getMethod(), 'header' => $headers, 'protocol_version' => $request->getProtocolVersion(), 'ignore_errors' => \true, 'follow_location' => 0], 'ssl' => ['peer_name' => $request->getUri()->getHost()]];
301 - $body = (string) $request->getBody();
302 - if ('' !== $body) {
303 - $context['http']['content'] = $body;
304 - // Prevent the HTTP handler from adding a Content-Type header.
305 - if (!$request->hasHeader('Content-Type')) {
306 - $context['http']['header'] .= "Content-Type:\r\n";
307 - }
308 - }
309 - $context['http']['header'] = \rtrim($context['http']['header']);
310 - return $context;
311 - }
312 - /**
313 - * @param mixed $value as passed via Request transfer options.
314 - */
315 - private function add_proxy(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void
316 - {
317 - $uri = null;
318 - if (!\is_array($value)) {
319 - $uri = $value;
320 - } else {
321 - $scheme = $request->getUri()->getScheme();
322 - if (isset($value[$scheme])) {
323 - if (!isset($value['no']) || !\YoastSEO_Vendor\GuzzleHttp\Utils::isHostInNoProxy($request->getUri()->getHost(), $value['no'])) {
324 - $uri = $value[$scheme];
325 - }
326 - }
327 - }
328 - if (!$uri) {
329 - return;
330 - }
331 - $parsed = $this->parse_proxy($uri);
332 - $options['http']['proxy'] = $parsed['proxy'];
333 - if ($parsed['auth']) {
334 - if (!isset($options['http']['header'])) {
335 - $options['http']['header'] = [];
336 - }
337 - $options['http']['header'] .= "\r\nProxy-Authorization: {$parsed['auth']}";
338 - }
339 - }
340 - /**
341 - * Parses the given proxy URL to make it compatible with the format PHP's stream context expects.
342 - */
343 - private function parse_proxy(string $url) : array
344 - {
345 - $parsed = \parse_url($url);
346 - if ($parsed !== \false && isset($parsed['scheme']) && $parsed['scheme'] === 'http') {
347 - if (isset($parsed['host']) && isset($parsed['port'])) {
348 - $auth = null;
349 - if (isset($parsed['user']) && isset($parsed['pass'])) {
350 - $auth = \base64_encode("{$parsed['user']}:{$parsed['pass']}");
351 - }
352 - return ['proxy' => "tcp://{$parsed['host']}:{$parsed['port']}", 'auth' => $auth ? "Basic {$auth}" : null];
353 - }
354 - }
355 - // Return proxy as-is.
356 - return ['proxy' => $url, 'auth' => null];
357 - }
358 - /**
359 - * @param mixed $value as passed via Request transfer options.
360 - */
361 - private function add_timeout(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void
362 - {
363 - if ($value > 0) {
364 - $options['http']['timeout'] = $value;
365 - }
366 - }
367 - /**
368 - * @param mixed $value as passed via Request transfer options.
369 - */
370 - private function add_crypto_method(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void
371 - {
372 - if ($value === \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT || $value === \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT || $value === \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT || \defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && $value === \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT) {
373 - $options['http']['crypto_method'] = $value;
374 - return;
375 - }
376 - throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided');
377 - }
378 - /**
379 - * @param mixed $value as passed via Request transfer options.
380 - */
381 - private function add_verify(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void
382 - {
383 - if ($value === \false) {
384 - $options['ssl']['verify_peer'] = \false;
385 - $options['ssl']['verify_peer_name'] = \false;
386 - return;
387 - }
388 - if (\is_string($value)) {
389 - $options['ssl']['cafile'] = $value;
390 - if (!\file_exists($value)) {
391 - throw new \RuntimeException("SSL CA bundle not found: {$value}");
392 - }
393 - } elseif ($value !== \true) {
394 - throw new \InvalidArgumentException('Invalid verify request option');
395 - }
396 - $options['ssl']['verify_peer'] = \true;
397 - $options['ssl']['verify_peer_name'] = \true;
398 - $options['ssl']['allow_self_signed'] = \false;
399 - }
400 - /**
401 - * @param mixed $value as passed via Request transfer options.
402 - */
403 - private function add_cert(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void
404 - {
405 - if (\is_array($value)) {
406 - $options['ssl']['passphrase'] = $value[1];
407 - $value = $value[0];
408 - }
409 - if (!\file_exists($value)) {
410 - throw new \RuntimeException("SSL certificate not found: {$value}");
411 - }
412 - $options['ssl']['local_cert'] = $value;
413 - }
414 - /**
415 - * @param mixed $value as passed via Request transfer options.
416 - */
417 - private function add_progress(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void
418 - {
419 - self::addNotification($params, static function ($code, $a, $b, $c, $transferred, $total) use($value) {
420 - if ($code == \STREAM_NOTIFY_PROGRESS) {
421 - // The upload progress cannot be determined. Use 0 for cURL compatibility:
422 - // https://curl.se/libcurl/c/CURLOPT_PROGRESSFUNCTION.html
423 - $value($total, $transferred, 0, 0);
424 - }
425 - });
426 - }
427 - /**
428 - * @param mixed $value as passed via Request transfer options.
429 - */
430 - private function add_debug(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array &$options, $value, array &$params) : void
431 - {
432 - if ($value === \false) {
433 - return;
434 - }
435 - static $map = [\STREAM_NOTIFY_CONNECT => 'CONNECT', \STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED', \STREAM_NOTIFY_AUTH_RESULT => 'AUTH_RESULT', \STREAM_NOTIFY_MIME_TYPE_IS => 'MIME_TYPE_IS', \STREAM_NOTIFY_FILE_SIZE_IS => 'FILE_SIZE_IS', \STREAM_NOTIFY_REDIRECTED => 'REDIRECTED', \STREAM_NOTIFY_PROGRESS => 'PROGRESS', \STREAM_NOTIFY_FAILURE => 'FAILURE', \STREAM_NOTIFY_COMPLETED => 'COMPLETED', \STREAM_NOTIFY_RESOLVE => 'RESOLVE'];
436 - static $args = ['severity', 'message', 'message_code', 'bytes_transferred', 'bytes_max'];
437 - $value = \YoastSEO_Vendor\GuzzleHttp\Utils::debugResource($value);
438 - $ident = $request->getMethod() . ' ' . $request->getUri()->withFragment('');
439 - self::addNotification($params, static function (int $code, ...$passed) use($ident, $value, $map, $args) : void {
440 - \fprintf($value, '<%s> [%s] ', $ident, $map[$code]);
441 - foreach (\array_filter($passed) as $i => $v) {
442 - \fwrite($value, $args[$i] . ': "' . $v . '" ');
443 - }
444 - \fwrite($value, "\n");
445 - });
446 - }
447 - private static function addNotification(array &$params, callable $notify) : void
448 - {
449 - // Wrap the existing function if needed.
450 - if (!isset($params['notification'])) {
451 - $params['notification'] = $notify;
452 - } else {
453 - $params['notification'] = self::callArray([$params['notification'], $notify]);
454 - }
455 - }
456 - private static function callArray(array $functions) : callable
457 - {
458 - return static function (...$args) use($functions) {
459 - foreach ($functions as $fn) {
460 - $fn(...$args);
461 - }
462 - };
463 - }
464 -}