@@ -36,133 +36,133 @@ |
||
36 | 36 | */ |
37 | 37 | class MessageFormatter implements MessageFormatterInterface |
38 | 38 | { |
39 | - /** |
|
40 | - * Apache Common Log Format. |
|
41 | - * |
|
42 | - * @see https://httpd.apache.org/docs/2.4/logs.html#common |
|
43 | - * |
|
44 | - * @var string |
|
45 | - */ |
|
46 | - public const CLF = '{hostname} {req_header_User-Agent} - [{date_common_log}] "{method} {target} HTTP/{version}" {code} {res_header_Content-Length}'; |
|
47 | - public const DEBUG = ">>>>>>>>\n{request}\n<<<<<<<<\n{response}\n--------\n{error}"; |
|
48 | - public const SHORT = '[{ts}] "{method} {target} HTTP/{version}" {code}'; |
|
49 | - /** |
|
50 | - * @var string Template used to format log messages |
|
51 | - */ |
|
52 | - private $template; |
|
53 | - /** |
|
54 | - * @param string $template Log message template |
|
55 | - */ |
|
56 | - public function __construct(?string $template = self::CLF) |
|
57 | - { |
|
58 | - $this->template = $template ?: self::CLF; |
|
59 | - } |
|
60 | - /** |
|
61 | - * Returns a formatted message string. |
|
62 | - * |
|
63 | - * @param RequestInterface $request Request that was sent |
|
64 | - * @param ResponseInterface|null $response Response that was received |
|
65 | - * @param \Throwable|null $error Exception that was received |
|
66 | - */ |
|
67 | - public function format(RequestInterface $request, ?ResponseInterface $response = null, ?\Throwable $error = null) : string |
|
68 | - { |
|
69 | - $cache = []; |
|
70 | - /** @var string */ |
|
71 | - return \preg_replace_callback('/{\\s*([A-Za-z_\\-\\.0-9]+)\\s*}/', function (array $matches) use($request, $response, $error, &$cache) { |
|
72 | - if (isset($cache[$matches[1]])) { |
|
73 | - return $cache[$matches[1]]; |
|
74 | - } |
|
75 | - $result = ''; |
|
76 | - switch ($matches[1]) { |
|
77 | - case 'request': |
|
78 | - $result = Psr7\Message::toString($request); |
|
79 | - break; |
|
80 | - case 'response': |
|
81 | - $result = $response ? Psr7\Message::toString($response) : ''; |
|
82 | - break; |
|
83 | - case 'req_headers': |
|
84 | - $result = \trim($request->getMethod() . ' ' . $request->getRequestTarget()) . ' HTTP/' . $request->getProtocolVersion() . "\r\n" . $this->headers($request); |
|
85 | - break; |
|
86 | - case 'res_headers': |
|
87 | - $result = $response ? \sprintf('HTTP/%s %d %s', $response->getProtocolVersion(), $response->getStatusCode(), $response->getReasonPhrase()) . "\r\n" . $this->headers($response) : 'NULL'; |
|
88 | - break; |
|
89 | - case 'req_body': |
|
90 | - $result = $request->getBody()->__toString(); |
|
91 | - break; |
|
92 | - case 'res_body': |
|
93 | - if (!$response instanceof ResponseInterface) { |
|
94 | - $result = 'NULL'; |
|
95 | - break; |
|
96 | - } |
|
97 | - $body = $response->getBody(); |
|
98 | - if (!$body->isSeekable()) { |
|
99 | - $result = 'RESPONSE_NOT_LOGGEABLE'; |
|
100 | - break; |
|
101 | - } |
|
102 | - $result = $response->getBody()->__toString(); |
|
103 | - break; |
|
104 | - case 'ts': |
|
105 | - case 'date_iso_8601': |
|
106 | - $result = \gmdate('c'); |
|
107 | - break; |
|
108 | - case 'date_common_log': |
|
109 | - $result = \date('d/M/Y:H:i:s O'); |
|
110 | - break; |
|
111 | - case 'method': |
|
112 | - $result = $request->getMethod(); |
|
113 | - break; |
|
114 | - case 'version': |
|
115 | - $result = $request->getProtocolVersion(); |
|
116 | - break; |
|
117 | - case 'uri': |
|
118 | - case 'url': |
|
119 | - $result = $request->getUri()->__toString(); |
|
120 | - break; |
|
121 | - case 'target': |
|
122 | - $result = $request->getRequestTarget(); |
|
123 | - break; |
|
124 | - case 'req_version': |
|
125 | - $result = $request->getProtocolVersion(); |
|
126 | - break; |
|
127 | - case 'res_version': |
|
128 | - $result = $response ? $response->getProtocolVersion() : 'NULL'; |
|
129 | - break; |
|
130 | - case 'host': |
|
131 | - $result = $request->getHeaderLine('Host'); |
|
132 | - break; |
|
133 | - case 'hostname': |
|
134 | - $result = \gethostname(); |
|
135 | - break; |
|
136 | - case 'code': |
|
137 | - $result = $response ? $response->getStatusCode() : 'NULL'; |
|
138 | - break; |
|
139 | - case 'phrase': |
|
140 | - $result = $response ? $response->getReasonPhrase() : 'NULL'; |
|
141 | - break; |
|
142 | - case 'error': |
|
143 | - $result = $error ? $error->getMessage() : 'NULL'; |
|
144 | - break; |
|
145 | - default: |
|
146 | - // handle prefixed dynamic headers |
|
147 | - if (\strpos($matches[1], 'req_header_') === 0) { |
|
148 | - $result = $request->getHeaderLine(\substr($matches[1], 11)); |
|
149 | - } elseif (\strpos($matches[1], 'res_header_') === 0) { |
|
150 | - $result = $response ? $response->getHeaderLine(\substr($matches[1], 11)) : 'NULL'; |
|
151 | - } |
|
152 | - } |
|
153 | - $cache[$matches[1]] = $result; |
|
154 | - return $result; |
|
155 | - }, $this->template); |
|
156 | - } |
|
157 | - /** |
|
158 | - * Get headers from message as string |
|
159 | - */ |
|
160 | - private function headers(MessageInterface $message) : string |
|
161 | - { |
|
162 | - $result = ''; |
|
163 | - foreach ($message->getHeaders() as $name => $values) { |
|
164 | - $result .= $name . ': ' . \implode(', ', $values) . "\r\n"; |
|
165 | - } |
|
166 | - return \trim($result); |
|
167 | - } |
|
39 | + /** |
|
40 | + * Apache Common Log Format. |
|
41 | + * |
|
42 | + * @see https://httpd.apache.org/docs/2.4/logs.html#common |
|
43 | + * |
|
44 | + * @var string |
|
45 | + */ |
|
46 | + public const CLF = '{hostname} {req_header_User-Agent} - [{date_common_log}] "{method} {target} HTTP/{version}" {code} {res_header_Content-Length}'; |
|
47 | + public const DEBUG = ">>>>>>>>\n{request}\n<<<<<<<<\n{response}\n--------\n{error}"; |
|
48 | + public const SHORT = '[{ts}] "{method} {target} HTTP/{version}" {code}'; |
|
49 | + /** |
|
50 | + * @var string Template used to format log messages |
|
51 | + */ |
|
52 | + private $template; |
|
53 | + /** |
|
54 | + * @param string $template Log message template |
|
55 | + */ |
|
56 | + public function __construct(?string $template = self::CLF) |
|
57 | + { |
|
58 | + $this->template = $template ?: self::CLF; |
|
59 | + } |
|
60 | + /** |
|
61 | + * Returns a formatted message string. |
|
62 | + * |
|
63 | + * @param RequestInterface $request Request that was sent |
|
64 | + * @param ResponseInterface|null $response Response that was received |
|
65 | + * @param \Throwable|null $error Exception that was received |
|
66 | + */ |
|
67 | + public function format(RequestInterface $request, ?ResponseInterface $response = null, ?\Throwable $error = null) : string |
|
68 | + { |
|
69 | + $cache = []; |
|
70 | + /** @var string */ |
|
71 | + return \preg_replace_callback('/{\\s*([A-Za-z_\\-\\.0-9]+)\\s*}/', function (array $matches) use($request, $response, $error, &$cache) { |
|
72 | + if (isset($cache[$matches[1]])) { |
|
73 | + return $cache[$matches[1]]; |
|
74 | + } |
|
75 | + $result = ''; |
|
76 | + switch ($matches[1]) { |
|
77 | + case 'request': |
|
78 | + $result = Psr7\Message::toString($request); |
|
79 | + break; |
|
80 | + case 'response': |
|
81 | + $result = $response ? Psr7\Message::toString($response) : ''; |
|
82 | + break; |
|
83 | + case 'req_headers': |
|
84 | + $result = \trim($request->getMethod() . ' ' . $request->getRequestTarget()) . ' HTTP/' . $request->getProtocolVersion() . "\r\n" . $this->headers($request); |
|
85 | + break; |
|
86 | + case 'res_headers': |
|
87 | + $result = $response ? \sprintf('HTTP/%s %d %s', $response->getProtocolVersion(), $response->getStatusCode(), $response->getReasonPhrase()) . "\r\n" . $this->headers($response) : 'NULL'; |
|
88 | + break; |
|
89 | + case 'req_body': |
|
90 | + $result = $request->getBody()->__toString(); |
|
91 | + break; |
|
92 | + case 'res_body': |
|
93 | + if (!$response instanceof ResponseInterface) { |
|
94 | + $result = 'NULL'; |
|
95 | + break; |
|
96 | + } |
|
97 | + $body = $response->getBody(); |
|
98 | + if (!$body->isSeekable()) { |
|
99 | + $result = 'RESPONSE_NOT_LOGGEABLE'; |
|
100 | + break; |
|
101 | + } |
|
102 | + $result = $response->getBody()->__toString(); |
|
103 | + break; |
|
104 | + case 'ts': |
|
105 | + case 'date_iso_8601': |
|
106 | + $result = \gmdate('c'); |
|
107 | + break; |
|
108 | + case 'date_common_log': |
|
109 | + $result = \date('d/M/Y:H:i:s O'); |
|
110 | + break; |
|
111 | + case 'method': |
|
112 | + $result = $request->getMethod(); |
|
113 | + break; |
|
114 | + case 'version': |
|
115 | + $result = $request->getProtocolVersion(); |
|
116 | + break; |
|
117 | + case 'uri': |
|
118 | + case 'url': |
|
119 | + $result = $request->getUri()->__toString(); |
|
120 | + break; |
|
121 | + case 'target': |
|
122 | + $result = $request->getRequestTarget(); |
|
123 | + break; |
|
124 | + case 'req_version': |
|
125 | + $result = $request->getProtocolVersion(); |
|
126 | + break; |
|
127 | + case 'res_version': |
|
128 | + $result = $response ? $response->getProtocolVersion() : 'NULL'; |
|
129 | + break; |
|
130 | + case 'host': |
|
131 | + $result = $request->getHeaderLine('Host'); |
|
132 | + break; |
|
133 | + case 'hostname': |
|
134 | + $result = \gethostname(); |
|
135 | + break; |
|
136 | + case 'code': |
|
137 | + $result = $response ? $response->getStatusCode() : 'NULL'; |
|
138 | + break; |
|
139 | + case 'phrase': |
|
140 | + $result = $response ? $response->getReasonPhrase() : 'NULL'; |
|
141 | + break; |
|
142 | + case 'error': |
|
143 | + $result = $error ? $error->getMessage() : 'NULL'; |
|
144 | + break; |
|
145 | + default: |
|
146 | + // handle prefixed dynamic headers |
|
147 | + if (\strpos($matches[1], 'req_header_') === 0) { |
|
148 | + $result = $request->getHeaderLine(\substr($matches[1], 11)); |
|
149 | + } elseif (\strpos($matches[1], 'res_header_') === 0) { |
|
150 | + $result = $response ? $response->getHeaderLine(\substr($matches[1], 11)) : 'NULL'; |
|
151 | + } |
|
152 | + } |
|
153 | + $cache[$matches[1]] = $result; |
|
154 | + return $result; |
|
155 | + }, $this->template); |
|
156 | + } |
|
157 | + /** |
|
158 | + * Get headers from message as string |
|
159 | + */ |
|
160 | + private function headers(MessageInterface $message) : string |
|
161 | + { |
|
162 | + $result = ''; |
|
163 | + foreach ($message->getHeaders() as $name => $values) { |
|
164 | + $result .= $name . ': ' . \implode(', ', $values) . "\r\n"; |
|
165 | + } |
|
166 | + return \trim($result); |
|
167 | + } |
|
168 | 168 | } |
@@ -14,214 +14,214 @@ |
||
14 | 14 | */ |
15 | 15 | final class Middleware |
16 | 16 | { |
17 | - /** |
|
18 | - * Middleware that adds cookies to requests. |
|
19 | - * |
|
20 | - * The options array must be set to a CookieJarInterface in order to use |
|
21 | - * cookies. This is typically handled for you by a client. |
|
22 | - * |
|
23 | - * @return callable Returns a function that accepts the next handler. |
|
24 | - */ |
|
25 | - public static function cookies() : callable |
|
26 | - { |
|
27 | - return static function (callable $handler) : callable { |
|
28 | - return static function ($request, array $options) use($handler) { |
|
29 | - if (empty($options['cookies'])) { |
|
30 | - return $handler($request, $options); |
|
31 | - } elseif (!$options['cookies'] instanceof CookieJarInterface) { |
|
32 | - throw new \InvalidArgumentException('OCA\\FullTextSearch_Elasticsearch\\Vendor\\cookies must be an instance of GuzzleHttp\\Cookie\\CookieJarInterface'); |
|
33 | - } |
|
34 | - $cookieJar = $options['cookies']; |
|
35 | - $request = $cookieJar->withCookieHeader($request); |
|
36 | - return $handler($request, $options)->then(static function (ResponseInterface $response) use($cookieJar, $request) : ResponseInterface { |
|
37 | - $cookieJar->extractCookies($request, $response); |
|
38 | - return $response; |
|
39 | - }); |
|
40 | - }; |
|
41 | - }; |
|
42 | - } |
|
43 | - /** |
|
44 | - * Middleware that throws exceptions for 4xx or 5xx responses when the |
|
45 | - * "http_errors" request option is set to true. |
|
46 | - * |
|
47 | - * @param BodySummarizerInterface|null $bodySummarizer The body summarizer to use in exception messages. |
|
48 | - * |
|
49 | - * @return callable(callable): callable Returns a function that accepts the next handler. |
|
50 | - */ |
|
51 | - public static function httpErrors(?BodySummarizerInterface $bodySummarizer = null) : callable |
|
52 | - { |
|
53 | - return static function (callable $handler) use($bodySummarizer) : callable { |
|
54 | - return static function ($request, array $options) use($handler, $bodySummarizer) { |
|
55 | - if (empty($options['http_errors'])) { |
|
56 | - return $handler($request, $options); |
|
57 | - } |
|
58 | - return $handler($request, $options)->then(static function (ResponseInterface $response) use($request, $bodySummarizer) { |
|
59 | - $code = $response->getStatusCode(); |
|
60 | - if ($code < 400) { |
|
61 | - return $response; |
|
62 | - } |
|
63 | - throw RequestException::create($request, $response, null, [], $bodySummarizer); |
|
64 | - }); |
|
65 | - }; |
|
66 | - }; |
|
67 | - } |
|
68 | - /** |
|
69 | - * Middleware that pushes history data to an ArrayAccess container. |
|
70 | - * |
|
71 | - * @param array|\ArrayAccess<int, array> $container Container to hold the history (by reference). |
|
72 | - * |
|
73 | - * @return callable(callable): callable Returns a function that accepts the next handler. |
|
74 | - * |
|
75 | - * @throws \InvalidArgumentException if container is not an array or ArrayAccess. |
|
76 | - */ |
|
77 | - public static function history(&$container) : callable |
|
78 | - { |
|
79 | - if (!\is_array($container) && !$container instanceof \ArrayAccess) { |
|
80 | - throw new \InvalidArgumentException('history container must be an array or object implementing ArrayAccess'); |
|
81 | - } |
|
82 | - return static function (callable $handler) use(&$container) : callable { |
|
83 | - return static function (RequestInterface $request, array $options) use($handler, &$container) { |
|
84 | - return $handler($request, $options)->then(static function ($value) use($request, &$container, $options) { |
|
85 | - $container[] = ['request' => $request, 'response' => $value, 'error' => null, 'options' => $options]; |
|
86 | - return $value; |
|
87 | - }, static function ($reason) use($request, &$container, $options) { |
|
88 | - $container[] = ['request' => $request, 'response' => null, 'error' => $reason, 'options' => $options]; |
|
89 | - return P\Create::rejectionFor($reason); |
|
90 | - }); |
|
91 | - }; |
|
92 | - }; |
|
93 | - } |
|
94 | - /** |
|
95 | - * Middleware that invokes a callback before and after sending a request. |
|
96 | - * |
|
97 | - * The provided listener cannot modify or alter the response. It simply |
|
98 | - * "taps" into the chain to be notified before returning the promise. The |
|
99 | - * before listener accepts a request and options array, and the after |
|
100 | - * listener accepts a request, options array, and response promise. |
|
101 | - * |
|
102 | - * @param callable $before Function to invoke before forwarding the request. |
|
103 | - * @param callable $after Function invoked after forwarding. |
|
104 | - * |
|
105 | - * @return callable Returns a function that accepts the next handler. |
|
106 | - */ |
|
107 | - public static function tap(?callable $before = null, ?callable $after = null) : callable |
|
108 | - { |
|
109 | - return static function (callable $handler) use($before, $after) : callable { |
|
110 | - return static function (RequestInterface $request, array $options) use($handler, $before, $after) { |
|
111 | - if ($before) { |
|
112 | - $before($request, $options); |
|
113 | - } |
|
114 | - $response = $handler($request, $options); |
|
115 | - if ($after) { |
|
116 | - $after($request, $options, $response); |
|
117 | - } |
|
118 | - return $response; |
|
119 | - }; |
|
120 | - }; |
|
121 | - } |
|
122 | - /** |
|
123 | - * Middleware that handles request redirects. |
|
124 | - * |
|
125 | - * @return callable Returns a function that accepts the next handler. |
|
126 | - */ |
|
127 | - public static function redirect() : callable |
|
128 | - { |
|
129 | - return static function (callable $handler) : RedirectMiddleware { |
|
130 | - return new RedirectMiddleware($handler); |
|
131 | - }; |
|
132 | - } |
|
133 | - /** |
|
134 | - * Middleware that retries requests based on the boolean result of |
|
135 | - * invoking the provided "decider" function. |
|
136 | - * |
|
137 | - * If no delay function is provided, a simple implementation of exponential |
|
138 | - * backoff will be utilized. |
|
139 | - * |
|
140 | - * @param callable $decider Function that accepts the number of retries, |
|
141 | - * a request, [response], and [exception] and |
|
142 | - * returns true if the request is to be retried. |
|
143 | - * @param callable $delay Function that accepts the number of retries and |
|
144 | - * returns the number of milliseconds to delay. |
|
145 | - * |
|
146 | - * @return callable Returns a function that accepts the next handler. |
|
147 | - */ |
|
148 | - public static function retry(callable $decider, ?callable $delay = null) : callable |
|
149 | - { |
|
150 | - return static function (callable $handler) use($decider, $delay) : RetryMiddleware { |
|
151 | - return new RetryMiddleware($decider, $handler, $delay); |
|
152 | - }; |
|
153 | - } |
|
154 | - /** |
|
155 | - * Middleware that logs requests, responses, and errors using a message |
|
156 | - * formatter. |
|
157 | - * |
|
158 | - * @phpstan-param \Psr\Log\LogLevel::* $logLevel Level at which to log requests. |
|
159 | - * |
|
160 | - * @param LoggerInterface $logger Logs messages. |
|
161 | - * @param MessageFormatterInterface|MessageFormatter $formatter Formatter used to create message strings. |
|
162 | - * @param string $logLevel Level at which to log requests. |
|
163 | - * |
|
164 | - * @return callable Returns a function that accepts the next handler. |
|
165 | - */ |
|
166 | - public static function log(LoggerInterface $logger, $formatter, string $logLevel = 'info') : callable |
|
167 | - { |
|
168 | - // To be compatible with Guzzle 7.1.x we need to allow users to pass a MessageFormatter |
|
169 | - if (!$formatter instanceof MessageFormatter && !$formatter instanceof MessageFormatterInterface) { |
|
170 | - throw new \LogicException(\sprintf('Argument 2 to %s::log() must be of type %s', self::class, MessageFormatterInterface::class)); |
|
171 | - } |
|
172 | - return static function (callable $handler) use($logger, $formatter, $logLevel) : callable { |
|
173 | - return static function (RequestInterface $request, array $options = []) use($handler, $logger, $formatter, $logLevel) { |
|
174 | - return $handler($request, $options)->then(static function ($response) use($logger, $request, $formatter, $logLevel) : ResponseInterface { |
|
175 | - $message = $formatter->format($request, $response); |
|
176 | - $logger->log($logLevel, $message); |
|
177 | - return $response; |
|
178 | - }, static function ($reason) use($logger, $request, $formatter) : PromiseInterface { |
|
179 | - $response = $reason instanceof RequestException ? $reason->getResponse() : null; |
|
180 | - $message = $formatter->format($request, $response, P\Create::exceptionFor($reason)); |
|
181 | - $logger->error($message); |
|
182 | - return P\Create::rejectionFor($reason); |
|
183 | - }); |
|
184 | - }; |
|
185 | - }; |
|
186 | - } |
|
187 | - /** |
|
188 | - * This middleware adds a default content-type if possible, a default |
|
189 | - * content-length or transfer-encoding header, and the expect header. |
|
190 | - */ |
|
191 | - public static function prepareBody() : callable |
|
192 | - { |
|
193 | - return static function (callable $handler) : PrepareBodyMiddleware { |
|
194 | - return new PrepareBodyMiddleware($handler); |
|
195 | - }; |
|
196 | - } |
|
197 | - /** |
|
198 | - * Middleware that applies a map function to the request before passing to |
|
199 | - * the next handler. |
|
200 | - * |
|
201 | - * @param callable $fn Function that accepts a RequestInterface and returns |
|
202 | - * a RequestInterface. |
|
203 | - */ |
|
204 | - public static function mapRequest(callable $fn) : callable |
|
205 | - { |
|
206 | - return static function (callable $handler) use($fn) : callable { |
|
207 | - return static function (RequestInterface $request, array $options) use($handler, $fn) { |
|
208 | - return $handler($fn($request), $options); |
|
209 | - }; |
|
210 | - }; |
|
211 | - } |
|
212 | - /** |
|
213 | - * Middleware that applies a map function to the resolved promise's |
|
214 | - * response. |
|
215 | - * |
|
216 | - * @param callable $fn Function that accepts a ResponseInterface and |
|
217 | - * returns a ResponseInterface. |
|
218 | - */ |
|
219 | - public static function mapResponse(callable $fn) : callable |
|
220 | - { |
|
221 | - return static function (callable $handler) use($fn) : callable { |
|
222 | - return static function (RequestInterface $request, array $options) use($handler, $fn) { |
|
223 | - return $handler($request, $options)->then($fn); |
|
224 | - }; |
|
225 | - }; |
|
226 | - } |
|
17 | + /** |
|
18 | + * Middleware that adds cookies to requests. |
|
19 | + * |
|
20 | + * The options array must be set to a CookieJarInterface in order to use |
|
21 | + * cookies. This is typically handled for you by a client. |
|
22 | + * |
|
23 | + * @return callable Returns a function that accepts the next handler. |
|
24 | + */ |
|
25 | + public static function cookies() : callable |
|
26 | + { |
|
27 | + return static function (callable $handler) : callable { |
|
28 | + return static function ($request, array $options) use($handler) { |
|
29 | + if (empty($options['cookies'])) { |
|
30 | + return $handler($request, $options); |
|
31 | + } elseif (!$options['cookies'] instanceof CookieJarInterface) { |
|
32 | + throw new \InvalidArgumentException('OCA\\FullTextSearch_Elasticsearch\\Vendor\\cookies must be an instance of GuzzleHttp\\Cookie\\CookieJarInterface'); |
|
33 | + } |
|
34 | + $cookieJar = $options['cookies']; |
|
35 | + $request = $cookieJar->withCookieHeader($request); |
|
36 | + return $handler($request, $options)->then(static function (ResponseInterface $response) use($cookieJar, $request) : ResponseInterface { |
|
37 | + $cookieJar->extractCookies($request, $response); |
|
38 | + return $response; |
|
39 | + }); |
|
40 | + }; |
|
41 | + }; |
|
42 | + } |
|
43 | + /** |
|
44 | + * Middleware that throws exceptions for 4xx or 5xx responses when the |
|
45 | + * "http_errors" request option is set to true. |
|
46 | + * |
|
47 | + * @param BodySummarizerInterface|null $bodySummarizer The body summarizer to use in exception messages. |
|
48 | + * |
|
49 | + * @return callable(callable): callable Returns a function that accepts the next handler. |
|
50 | + */ |
|
51 | + public static function httpErrors(?BodySummarizerInterface $bodySummarizer = null) : callable |
|
52 | + { |
|
53 | + return static function (callable $handler) use($bodySummarizer) : callable { |
|
54 | + return static function ($request, array $options) use($handler, $bodySummarizer) { |
|
55 | + if (empty($options['http_errors'])) { |
|
56 | + return $handler($request, $options); |
|
57 | + } |
|
58 | + return $handler($request, $options)->then(static function (ResponseInterface $response) use($request, $bodySummarizer) { |
|
59 | + $code = $response->getStatusCode(); |
|
60 | + if ($code < 400) { |
|
61 | + return $response; |
|
62 | + } |
|
63 | + throw RequestException::create($request, $response, null, [], $bodySummarizer); |
|
64 | + }); |
|
65 | + }; |
|
66 | + }; |
|
67 | + } |
|
68 | + /** |
|
69 | + * Middleware that pushes history data to an ArrayAccess container. |
|
70 | + * |
|
71 | + * @param array|\ArrayAccess<int, array> $container Container to hold the history (by reference). |
|
72 | + * |
|
73 | + * @return callable(callable): callable Returns a function that accepts the next handler. |
|
74 | + * |
|
75 | + * @throws \InvalidArgumentException if container is not an array or ArrayAccess. |
|
76 | + */ |
|
77 | + public static function history(&$container) : callable |
|
78 | + { |
|
79 | + if (!\is_array($container) && !$container instanceof \ArrayAccess) { |
|
80 | + throw new \InvalidArgumentException('history container must be an array or object implementing ArrayAccess'); |
|
81 | + } |
|
82 | + return static function (callable $handler) use(&$container) : callable { |
|
83 | + return static function (RequestInterface $request, array $options) use($handler, &$container) { |
|
84 | + return $handler($request, $options)->then(static function ($value) use($request, &$container, $options) { |
|
85 | + $container[] = ['request' => $request, 'response' => $value, 'error' => null, 'options' => $options]; |
|
86 | + return $value; |
|
87 | + }, static function ($reason) use($request, &$container, $options) { |
|
88 | + $container[] = ['request' => $request, 'response' => null, 'error' => $reason, 'options' => $options]; |
|
89 | + return P\Create::rejectionFor($reason); |
|
90 | + }); |
|
91 | + }; |
|
92 | + }; |
|
93 | + } |
|
94 | + /** |
|
95 | + * Middleware that invokes a callback before and after sending a request. |
|
96 | + * |
|
97 | + * The provided listener cannot modify or alter the response. It simply |
|
98 | + * "taps" into the chain to be notified before returning the promise. The |
|
99 | + * before listener accepts a request and options array, and the after |
|
100 | + * listener accepts a request, options array, and response promise. |
|
101 | + * |
|
102 | + * @param callable $before Function to invoke before forwarding the request. |
|
103 | + * @param callable $after Function invoked after forwarding. |
|
104 | + * |
|
105 | + * @return callable Returns a function that accepts the next handler. |
|
106 | + */ |
|
107 | + public static function tap(?callable $before = null, ?callable $after = null) : callable |
|
108 | + { |
|
109 | + return static function (callable $handler) use($before, $after) : callable { |
|
110 | + return static function (RequestInterface $request, array $options) use($handler, $before, $after) { |
|
111 | + if ($before) { |
|
112 | + $before($request, $options); |
|
113 | + } |
|
114 | + $response = $handler($request, $options); |
|
115 | + if ($after) { |
|
116 | + $after($request, $options, $response); |
|
117 | + } |
|
118 | + return $response; |
|
119 | + }; |
|
120 | + }; |
|
121 | + } |
|
122 | + /** |
|
123 | + * Middleware that handles request redirects. |
|
124 | + * |
|
125 | + * @return callable Returns a function that accepts the next handler. |
|
126 | + */ |
|
127 | + public static function redirect() : callable |
|
128 | + { |
|
129 | + return static function (callable $handler) : RedirectMiddleware { |
|
130 | + return new RedirectMiddleware($handler); |
|
131 | + }; |
|
132 | + } |
|
133 | + /** |
|
134 | + * Middleware that retries requests based on the boolean result of |
|
135 | + * invoking the provided "decider" function. |
|
136 | + * |
|
137 | + * If no delay function is provided, a simple implementation of exponential |
|
138 | + * backoff will be utilized. |
|
139 | + * |
|
140 | + * @param callable $decider Function that accepts the number of retries, |
|
141 | + * a request, [response], and [exception] and |
|
142 | + * returns true if the request is to be retried. |
|
143 | + * @param callable $delay Function that accepts the number of retries and |
|
144 | + * returns the number of milliseconds to delay. |
|
145 | + * |
|
146 | + * @return callable Returns a function that accepts the next handler. |
|
147 | + */ |
|
148 | + public static function retry(callable $decider, ?callable $delay = null) : callable |
|
149 | + { |
|
150 | + return static function (callable $handler) use($decider, $delay) : RetryMiddleware { |
|
151 | + return new RetryMiddleware($decider, $handler, $delay); |
|
152 | + }; |
|
153 | + } |
|
154 | + /** |
|
155 | + * Middleware that logs requests, responses, and errors using a message |
|
156 | + * formatter. |
|
157 | + * |
|
158 | + * @phpstan-param \Psr\Log\LogLevel::* $logLevel Level at which to log requests. |
|
159 | + * |
|
160 | + * @param LoggerInterface $logger Logs messages. |
|
161 | + * @param MessageFormatterInterface|MessageFormatter $formatter Formatter used to create message strings. |
|
162 | + * @param string $logLevel Level at which to log requests. |
|
163 | + * |
|
164 | + * @return callable Returns a function that accepts the next handler. |
|
165 | + */ |
|
166 | + public static function log(LoggerInterface $logger, $formatter, string $logLevel = 'info') : callable |
|
167 | + { |
|
168 | + // To be compatible with Guzzle 7.1.x we need to allow users to pass a MessageFormatter |
|
169 | + if (!$formatter instanceof MessageFormatter && !$formatter instanceof MessageFormatterInterface) { |
|
170 | + throw new \LogicException(\sprintf('Argument 2 to %s::log() must be of type %s', self::class, MessageFormatterInterface::class)); |
|
171 | + } |
|
172 | + return static function (callable $handler) use($logger, $formatter, $logLevel) : callable { |
|
173 | + return static function (RequestInterface $request, array $options = []) use($handler, $logger, $formatter, $logLevel) { |
|
174 | + return $handler($request, $options)->then(static function ($response) use($logger, $request, $formatter, $logLevel) : ResponseInterface { |
|
175 | + $message = $formatter->format($request, $response); |
|
176 | + $logger->log($logLevel, $message); |
|
177 | + return $response; |
|
178 | + }, static function ($reason) use($logger, $request, $formatter) : PromiseInterface { |
|
179 | + $response = $reason instanceof RequestException ? $reason->getResponse() : null; |
|
180 | + $message = $formatter->format($request, $response, P\Create::exceptionFor($reason)); |
|
181 | + $logger->error($message); |
|
182 | + return P\Create::rejectionFor($reason); |
|
183 | + }); |
|
184 | + }; |
|
185 | + }; |
|
186 | + } |
|
187 | + /** |
|
188 | + * This middleware adds a default content-type if possible, a default |
|
189 | + * content-length or transfer-encoding header, and the expect header. |
|
190 | + */ |
|
191 | + public static function prepareBody() : callable |
|
192 | + { |
|
193 | + return static function (callable $handler) : PrepareBodyMiddleware { |
|
194 | + return new PrepareBodyMiddleware($handler); |
|
195 | + }; |
|
196 | + } |
|
197 | + /** |
|
198 | + * Middleware that applies a map function to the request before passing to |
|
199 | + * the next handler. |
|
200 | + * |
|
201 | + * @param callable $fn Function that accepts a RequestInterface and returns |
|
202 | + * a RequestInterface. |
|
203 | + */ |
|
204 | + public static function mapRequest(callable $fn) : callable |
|
205 | + { |
|
206 | + return static function (callable $handler) use($fn) : callable { |
|
207 | + return static function (RequestInterface $request, array $options) use($handler, $fn) { |
|
208 | + return $handler($fn($request), $options); |
|
209 | + }; |
|
210 | + }; |
|
211 | + } |
|
212 | + /** |
|
213 | + * Middleware that applies a map function to the resolved promise's |
|
214 | + * response. |
|
215 | + * |
|
216 | + * @param callable $fn Function that accepts a ResponseInterface and |
|
217 | + * returns a ResponseInterface. |
|
218 | + */ |
|
219 | + public static function mapResponse(callable $fn) : callable |
|
220 | + { |
|
221 | + return static function (callable $handler) use($fn) : callable { |
|
222 | + return static function (RequestInterface $request, array $options) use($handler, $fn) { |
|
223 | + return $handler($request, $options)->then($fn); |
|
224 | + }; |
|
225 | + }; |
|
226 | + } |
|
227 | 227 | } |
@@ -24,8 +24,8 @@ discard block |
||
24 | 24 | */ |
25 | 25 | public static function cookies() : callable |
26 | 26 | { |
27 | - return static function (callable $handler) : callable { |
|
28 | - return static function ($request, array $options) use($handler) { |
|
27 | + return static function(callable $handler) : callable { |
|
28 | + return static function($request, array $options) use($handler) { |
|
29 | 29 | if (empty($options['cookies'])) { |
30 | 30 | return $handler($request, $options); |
31 | 31 | } elseif (!$options['cookies'] instanceof CookieJarInterface) { |
@@ -33,7 +33,7 @@ discard block |
||
33 | 33 | } |
34 | 34 | $cookieJar = $options['cookies']; |
35 | 35 | $request = $cookieJar->withCookieHeader($request); |
36 | - return $handler($request, $options)->then(static function (ResponseInterface $response) use($cookieJar, $request) : ResponseInterface { |
|
36 | + return $handler($request, $options)->then(static function(ResponseInterface $response) use($cookieJar, $request) : ResponseInterface { |
|
37 | 37 | $cookieJar->extractCookies($request, $response); |
38 | 38 | return $response; |
39 | 39 | }); |
@@ -50,12 +50,12 @@ discard block |
||
50 | 50 | */ |
51 | 51 | public static function httpErrors(?BodySummarizerInterface $bodySummarizer = null) : callable |
52 | 52 | { |
53 | - return static function (callable $handler) use($bodySummarizer) : callable { |
|
54 | - return static function ($request, array $options) use($handler, $bodySummarizer) { |
|
53 | + return static function(callable $handler) use($bodySummarizer) : callable { |
|
54 | + return static function($request, array $options) use($handler, $bodySummarizer) { |
|
55 | 55 | if (empty($options['http_errors'])) { |
56 | 56 | return $handler($request, $options); |
57 | 57 | } |
58 | - return $handler($request, $options)->then(static function (ResponseInterface $response) use($request, $bodySummarizer) { |
|
58 | + return $handler($request, $options)->then(static function(ResponseInterface $response) use($request, $bodySummarizer) { |
|
59 | 59 | $code = $response->getStatusCode(); |
60 | 60 | if ($code < 400) { |
61 | 61 | return $response; |
@@ -79,12 +79,12 @@ discard block |
||
79 | 79 | if (!\is_array($container) && !$container instanceof \ArrayAccess) { |
80 | 80 | throw new \InvalidArgumentException('history container must be an array or object implementing ArrayAccess'); |
81 | 81 | } |
82 | - return static function (callable $handler) use(&$container) : callable { |
|
83 | - return static function (RequestInterface $request, array $options) use($handler, &$container) { |
|
84 | - return $handler($request, $options)->then(static function ($value) use($request, &$container, $options) { |
|
82 | + return static function(callable $handler) use(&$container) : callable { |
|
83 | + return static function(RequestInterface $request, array $options) use($handler, &$container) { |
|
84 | + return $handler($request, $options)->then(static function($value) use($request, &$container, $options) { |
|
85 | 85 | $container[] = ['request' => $request, 'response' => $value, 'error' => null, 'options' => $options]; |
86 | 86 | return $value; |
87 | - }, static function ($reason) use($request, &$container, $options) { |
|
87 | + }, static function($reason) use($request, &$container, $options) { |
|
88 | 88 | $container[] = ['request' => $request, 'response' => null, 'error' => $reason, 'options' => $options]; |
89 | 89 | return P\Create::rejectionFor($reason); |
90 | 90 | }); |
@@ -106,8 +106,8 @@ discard block |
||
106 | 106 | */ |
107 | 107 | public static function tap(?callable $before = null, ?callable $after = null) : callable |
108 | 108 | { |
109 | - return static function (callable $handler) use($before, $after) : callable { |
|
110 | - return static function (RequestInterface $request, array $options) use($handler, $before, $after) { |
|
109 | + return static function(callable $handler) use($before, $after) : callable { |
|
110 | + return static function(RequestInterface $request, array $options) use($handler, $before, $after) { |
|
111 | 111 | if ($before) { |
112 | 112 | $before($request, $options); |
113 | 113 | } |
@@ -126,7 +126,7 @@ discard block |
||
126 | 126 | */ |
127 | 127 | public static function redirect() : callable |
128 | 128 | { |
129 | - return static function (callable $handler) : RedirectMiddleware { |
|
129 | + return static function(callable $handler) : RedirectMiddleware { |
|
130 | 130 | return new RedirectMiddleware($handler); |
131 | 131 | }; |
132 | 132 | } |
@@ -147,7 +147,7 @@ discard block |
||
147 | 147 | */ |
148 | 148 | public static function retry(callable $decider, ?callable $delay = null) : callable |
149 | 149 | { |
150 | - return static function (callable $handler) use($decider, $delay) : RetryMiddleware { |
|
150 | + return static function(callable $handler) use($decider, $delay) : RetryMiddleware { |
|
151 | 151 | return new RetryMiddleware($decider, $handler, $delay); |
152 | 152 | }; |
153 | 153 | } |
@@ -169,13 +169,13 @@ discard block |
||
169 | 169 | if (!$formatter instanceof MessageFormatter && !$formatter instanceof MessageFormatterInterface) { |
170 | 170 | throw new \LogicException(\sprintf('Argument 2 to %s::log() must be of type %s', self::class, MessageFormatterInterface::class)); |
171 | 171 | } |
172 | - return static function (callable $handler) use($logger, $formatter, $logLevel) : callable { |
|
173 | - return static function (RequestInterface $request, array $options = []) use($handler, $logger, $formatter, $logLevel) { |
|
174 | - return $handler($request, $options)->then(static function ($response) use($logger, $request, $formatter, $logLevel) : ResponseInterface { |
|
172 | + return static function(callable $handler) use($logger, $formatter, $logLevel) : callable { |
|
173 | + return static function(RequestInterface $request, array $options = []) use($handler, $logger, $formatter, $logLevel) { |
|
174 | + return $handler($request, $options)->then(static function($response) use($logger, $request, $formatter, $logLevel) : ResponseInterface { |
|
175 | 175 | $message = $formatter->format($request, $response); |
176 | 176 | $logger->log($logLevel, $message); |
177 | 177 | return $response; |
178 | - }, static function ($reason) use($logger, $request, $formatter) : PromiseInterface { |
|
178 | + }, static function($reason) use($logger, $request, $formatter) : PromiseInterface { |
|
179 | 179 | $response = $reason instanceof RequestException ? $reason->getResponse() : null; |
180 | 180 | $message = $formatter->format($request, $response, P\Create::exceptionFor($reason)); |
181 | 181 | $logger->error($message); |
@@ -190,7 +190,7 @@ discard block |
||
190 | 190 | */ |
191 | 191 | public static function prepareBody() : callable |
192 | 192 | { |
193 | - return static function (callable $handler) : PrepareBodyMiddleware { |
|
193 | + return static function(callable $handler) : PrepareBodyMiddleware { |
|
194 | 194 | return new PrepareBodyMiddleware($handler); |
195 | 195 | }; |
196 | 196 | } |
@@ -203,8 +203,8 @@ discard block |
||
203 | 203 | */ |
204 | 204 | public static function mapRequest(callable $fn) : callable |
205 | 205 | { |
206 | - return static function (callable $handler) use($fn) : callable { |
|
207 | - return static function (RequestInterface $request, array $options) use($handler, $fn) { |
|
206 | + return static function(callable $handler) use($fn) : callable { |
|
207 | + return static function(RequestInterface $request, array $options) use($handler, $fn) { |
|
208 | 208 | return $handler($fn($request), $options); |
209 | 209 | }; |
210 | 210 | }; |
@@ -218,8 +218,8 @@ discard block |
||
218 | 218 | */ |
219 | 219 | public static function mapResponse(callable $fn) : callable |
220 | 220 | { |
221 | - return static function (callable $handler) use($fn) : callable { |
|
222 | - return static function (RequestInterface $request, array $options) use($handler, $fn) { |
|
221 | + return static function(callable $handler) use($fn) : callable { |
|
222 | + return static function(RequestInterface $request, array $options) use($handler, $fn) { |
|
223 | 223 | return $handler($request, $options)->then($fn); |
224 | 224 | }; |
225 | 225 | }; |
@@ -21,439 +21,439 @@ |
||
21 | 21 | */ |
22 | 22 | class StreamHandler |
23 | 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(RequestInterface $request, array $options) : 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 ConnectException(\sprintf('HTTP/%s is not supported by the stream handler.', $protocolVersion), $request); |
|
43 | - } |
|
44 | - $startTime = isset($options['on_stats']) ? 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 | - // cURL's behavior. |
|
50 | - if (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 ConnectException($e->getMessage(), $request, $e); |
|
62 | - } else { |
|
63 | - $e = RequestException::wrapException($request, $e); |
|
64 | - } |
|
65 | - $this->invokeStats($options, $request, $startTime, null, $e); |
|
66 | - return P\Create::rejectionFor($e); |
|
67 | - } |
|
68 | - } |
|
69 | - private function invokeStats(array $options, RequestInterface $request, ?float $startTime, ?ResponseInterface $response = null, ?\Throwable $error = null) : void |
|
70 | - { |
|
71 | - if (isset($options['on_stats'])) { |
|
72 | - $stats = new TransferStats($request, $response, Utils::currentTime() - $startTime, $error, []); |
|
73 | - $options['on_stats']($stats); |
|
74 | - } |
|
75 | - } |
|
76 | - /** |
|
77 | - * @param resource $stream |
|
78 | - */ |
|
79 | - private function createResponse(RequestInterface $request, array $options, $stream, ?float $startTime) : PromiseInterface |
|
80 | - { |
|
81 | - $hdrs = $this->lastHeaders; |
|
82 | - $this->lastHeaders = []; |
|
83 | - try { |
|
84 | - [$ver, $status, $reason, $headers] = HeaderProcessor::parseHeaders($hdrs); |
|
85 | - } catch (\Exception $e) { |
|
86 | - return P\Create::rejectionFor(new RequestException('An error was encountered while creating the response', $request, null, $e)); |
|
87 | - } |
|
88 | - [$stream, $headers] = $this->checkDecode($options, $headers, $stream); |
|
89 | - $stream = 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 Psr7\Response($status, $headers, $sink, $ver, $reason); |
|
96 | - } catch (\Exception $e) { |
|
97 | - return P\Create::rejectionFor(new 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 P\Create::rejectionFor(new 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 FulfilledPromise($response); |
|
113 | - } |
|
114 | - private function createSink(StreamInterface $stream, array $options) : StreamInterface |
|
115 | - { |
|
116 | - if (!empty($options['stream'])) { |
|
117 | - return $stream; |
|
118 | - } |
|
119 | - $sink = $options['sink'] ?? Psr7\Utils::tryFopen('php://temp', 'r+'); |
|
120 | - return \is_string($sink) ? new Psr7\LazyOpenStream($sink, 'w+') : 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 = 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 Psr7\InflateStream(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(StreamInterface $source, StreamInterface $sink, string $contentLength) : 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 | - 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(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 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, &$http_response_header, $contextResource, $context, $options, $request) { |
|
252 | - $resource = @\fopen((string) $uri, 'r', \false, $contextResource); |
|
253 | - $this->lastHeaders = $http_response_header ?? []; |
|
254 | - if (\false === $resource) { |
|
255 | - throw new ConnectException(\sprintf('Connection refused for URI %s', $uri), $request, null, $context); |
|
256 | - } |
|
257 | - if (isset($options['read_timeout'])) { |
|
258 | - $readTimeout = $options['read_timeout']; |
|
259 | - $sec = (int) $readTimeout; |
|
260 | - $usec = ($readTimeout - $sec) * 100000; |
|
261 | - \stream_set_timeout($resource, $sec, $usec); |
|
262 | - } |
|
263 | - return $resource; |
|
264 | - }); |
|
265 | - } |
|
266 | - private function resolveHost(RequestInterface $request, array $options) : UriInterface |
|
267 | - { |
|
268 | - $uri = $request->getUri(); |
|
269 | - if (isset($options['force_ip_resolve']) && !\filter_var($uri->getHost(), \FILTER_VALIDATE_IP)) { |
|
270 | - if ('v4' === $options['force_ip_resolve']) { |
|
271 | - $records = \dns_get_record($uri->getHost(), \DNS_A); |
|
272 | - if (\false === $records || !isset($records[0]['ip'])) { |
|
273 | - throw new ConnectException(\sprintf("Could not resolve IPv4 address for host '%s'", $uri->getHost()), $request); |
|
274 | - } |
|
275 | - return $uri->withHost($records[0]['ip']); |
|
276 | - } |
|
277 | - if ('v6' === $options['force_ip_resolve']) { |
|
278 | - $records = \dns_get_record($uri->getHost(), \DNS_AAAA); |
|
279 | - if (\false === $records || !isset($records[0]['ipv6'])) { |
|
280 | - throw new ConnectException(\sprintf("Could not resolve IPv6 address for host '%s'", $uri->getHost()), $request); |
|
281 | - } |
|
282 | - return $uri->withHost('[' . $records[0]['ipv6'] . ']'); |
|
283 | - } |
|
284 | - } |
|
285 | - return $uri; |
|
286 | - } |
|
287 | - private function getDefaultContext(RequestInterface $request) : array |
|
288 | - { |
|
289 | - $headers = ''; |
|
290 | - foreach ($request->getHeaders() as $name => $value) { |
|
291 | - foreach ($value as $val) { |
|
292 | - $headers .= "{$name}: {$val}\r\n"; |
|
293 | - } |
|
294 | - } |
|
295 | - $context = ['http' => ['method' => $request->getMethod(), 'header' => $headers, 'protocol_version' => $request->getProtocolVersion(), 'ignore_errors' => \true, 'follow_location' => 0], 'ssl' => ['peer_name' => $request->getUri()->getHost()]]; |
|
296 | - $body = (string) $request->getBody(); |
|
297 | - if ('' !== $body) { |
|
298 | - $context['http']['content'] = $body; |
|
299 | - // Prevent the HTTP handler from adding a Content-Type header. |
|
300 | - if (!$request->hasHeader('Content-Type')) { |
|
301 | - $context['http']['header'] .= "Content-Type:\r\n"; |
|
302 | - } |
|
303 | - } |
|
304 | - $context['http']['header'] = \rtrim($context['http']['header']); |
|
305 | - return $context; |
|
306 | - } |
|
307 | - /** |
|
308 | - * @param mixed $value as passed via Request transfer options. |
|
309 | - */ |
|
310 | - private function add_proxy(RequestInterface $request, array &$options, $value, array &$params) : void |
|
311 | - { |
|
312 | - $uri = null; |
|
313 | - if (!\is_array($value)) { |
|
314 | - $uri = $value; |
|
315 | - } else { |
|
316 | - $scheme = $request->getUri()->getScheme(); |
|
317 | - if (isset($value[$scheme])) { |
|
318 | - if (!isset($value['no']) || !Utils::isHostInNoProxy($request->getUri()->getHost(), $value['no'])) { |
|
319 | - $uri = $value[$scheme]; |
|
320 | - } |
|
321 | - } |
|
322 | - } |
|
323 | - if (!$uri) { |
|
324 | - return; |
|
325 | - } |
|
326 | - $parsed = $this->parse_proxy($uri); |
|
327 | - $options['http']['proxy'] = $parsed['proxy']; |
|
328 | - if ($parsed['auth']) { |
|
329 | - if (!isset($options['http']['header'])) { |
|
330 | - $options['http']['header'] = []; |
|
331 | - } |
|
332 | - $options['http']['header'] .= "\r\nProxy-Authorization: {$parsed['auth']}"; |
|
333 | - } |
|
334 | - } |
|
335 | - /** |
|
336 | - * Parses the given proxy URL to make it compatible with the format PHP's stream context expects. |
|
337 | - */ |
|
338 | - private function parse_proxy(string $url) : array |
|
339 | - { |
|
340 | - $parsed = \parse_url($url); |
|
341 | - if ($parsed !== \false && isset($parsed['scheme']) && $parsed['scheme'] === 'http') { |
|
342 | - if (isset($parsed['host']) && isset($parsed['port'])) { |
|
343 | - $auth = null; |
|
344 | - if (isset($parsed['user']) && isset($parsed['pass'])) { |
|
345 | - $auth = \base64_encode("{$parsed['user']}:{$parsed['pass']}"); |
|
346 | - } |
|
347 | - return ['proxy' => "tcp://{$parsed['host']}:{$parsed['port']}", 'auth' => $auth ? "Basic {$auth}" : null]; |
|
348 | - } |
|
349 | - } |
|
350 | - // Return proxy as-is. |
|
351 | - return ['proxy' => $url, 'auth' => null]; |
|
352 | - } |
|
353 | - /** |
|
354 | - * @param mixed $value as passed via Request transfer options. |
|
355 | - */ |
|
356 | - private function add_timeout(RequestInterface $request, array &$options, $value, array &$params) : void |
|
357 | - { |
|
358 | - if ($value > 0) { |
|
359 | - $options['http']['timeout'] = $value; |
|
360 | - } |
|
361 | - } |
|
362 | - /** |
|
363 | - * @param mixed $value as passed via Request transfer options. |
|
364 | - */ |
|
365 | - private function add_crypto_method(RequestInterface $request, array &$options, $value, array &$params) : void |
|
366 | - { |
|
367 | - 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) { |
|
368 | - $options['http']['crypto_method'] = $value; |
|
369 | - return; |
|
370 | - } |
|
371 | - throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided'); |
|
372 | - } |
|
373 | - /** |
|
374 | - * @param mixed $value as passed via Request transfer options. |
|
375 | - */ |
|
376 | - private function add_verify(RequestInterface $request, array &$options, $value, array &$params) : void |
|
377 | - { |
|
378 | - if ($value === \false) { |
|
379 | - $options['ssl']['verify_peer'] = \false; |
|
380 | - $options['ssl']['verify_peer_name'] = \false; |
|
381 | - return; |
|
382 | - } |
|
383 | - if (\is_string($value)) { |
|
384 | - $options['ssl']['cafile'] = $value; |
|
385 | - if (!\file_exists($value)) { |
|
386 | - throw new \RuntimeException("SSL CA bundle not found: {$value}"); |
|
387 | - } |
|
388 | - } elseif ($value !== \true) { |
|
389 | - throw new \InvalidArgumentException('Invalid verify request option'); |
|
390 | - } |
|
391 | - $options['ssl']['verify_peer'] = \true; |
|
392 | - $options['ssl']['verify_peer_name'] = \true; |
|
393 | - $options['ssl']['allow_self_signed'] = \false; |
|
394 | - } |
|
395 | - /** |
|
396 | - * @param mixed $value as passed via Request transfer options. |
|
397 | - */ |
|
398 | - private function add_cert(RequestInterface $request, array &$options, $value, array &$params) : void |
|
399 | - { |
|
400 | - if (\is_array($value)) { |
|
401 | - $options['ssl']['passphrase'] = $value[1]; |
|
402 | - $value = $value[0]; |
|
403 | - } |
|
404 | - if (!\file_exists($value)) { |
|
405 | - throw new \RuntimeException("SSL certificate not found: {$value}"); |
|
406 | - } |
|
407 | - $options['ssl']['local_cert'] = $value; |
|
408 | - } |
|
409 | - /** |
|
410 | - * @param mixed $value as passed via Request transfer options. |
|
411 | - */ |
|
412 | - private function add_progress(RequestInterface $request, array &$options, $value, array &$params) : void |
|
413 | - { |
|
414 | - self::addNotification($params, static function ($code, $a, $b, $c, $transferred, $total) use($value) { |
|
415 | - if ($code == \STREAM_NOTIFY_PROGRESS) { |
|
416 | - // The upload progress cannot be determined. Use 0 for cURL compatibility: |
|
417 | - // https://curl.se/libcurl/c/CURLOPT_PROGRESSFUNCTION.html |
|
418 | - $value($total, $transferred, 0, 0); |
|
419 | - } |
|
420 | - }); |
|
421 | - } |
|
422 | - /** |
|
423 | - * @param mixed $value as passed via Request transfer options. |
|
424 | - */ |
|
425 | - private function add_debug(RequestInterface $request, array &$options, $value, array &$params) : void |
|
426 | - { |
|
427 | - if ($value === \false) { |
|
428 | - return; |
|
429 | - } |
|
430 | - 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']; |
|
431 | - static $args = ['severity', 'message', 'message_code', 'bytes_transferred', 'bytes_max']; |
|
432 | - $value = Utils::debugResource($value); |
|
433 | - $ident = $request->getMethod() . ' ' . $request->getUri()->withFragment(''); |
|
434 | - self::addNotification($params, static function (int $code, ...$passed) use($ident, $value, $map, $args) : void { |
|
435 | - \fprintf($value, '<%s> [%s] ', $ident, $map[$code]); |
|
436 | - foreach (\array_filter($passed) as $i => $v) { |
|
437 | - \fwrite($value, $args[$i] . ': "' . $v . '" '); |
|
438 | - } |
|
439 | - \fwrite($value, "\n"); |
|
440 | - }); |
|
441 | - } |
|
442 | - private static function addNotification(array &$params, callable $notify) : void |
|
443 | - { |
|
444 | - // Wrap the existing function if needed. |
|
445 | - if (!isset($params['notification'])) { |
|
446 | - $params['notification'] = $notify; |
|
447 | - } else { |
|
448 | - $params['notification'] = self::callArray([$params['notification'], $notify]); |
|
449 | - } |
|
450 | - } |
|
451 | - private static function callArray(array $functions) : callable |
|
452 | - { |
|
453 | - return static function (...$args) use($functions) { |
|
454 | - foreach ($functions as $fn) { |
|
455 | - $fn(...$args); |
|
456 | - } |
|
457 | - }; |
|
458 | - } |
|
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(RequestInterface $request, array $options) : 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 ConnectException(\sprintf('HTTP/%s is not supported by the stream handler.', $protocolVersion), $request); |
|
43 | + } |
|
44 | + $startTime = isset($options['on_stats']) ? 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 | + // cURL's behavior. |
|
50 | + if (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 ConnectException($e->getMessage(), $request, $e); |
|
62 | + } else { |
|
63 | + $e = RequestException::wrapException($request, $e); |
|
64 | + } |
|
65 | + $this->invokeStats($options, $request, $startTime, null, $e); |
|
66 | + return P\Create::rejectionFor($e); |
|
67 | + } |
|
68 | + } |
|
69 | + private function invokeStats(array $options, RequestInterface $request, ?float $startTime, ?ResponseInterface $response = null, ?\Throwable $error = null) : void |
|
70 | + { |
|
71 | + if (isset($options['on_stats'])) { |
|
72 | + $stats = new TransferStats($request, $response, Utils::currentTime() - $startTime, $error, []); |
|
73 | + $options['on_stats']($stats); |
|
74 | + } |
|
75 | + } |
|
76 | + /** |
|
77 | + * @param resource $stream |
|
78 | + */ |
|
79 | + private function createResponse(RequestInterface $request, array $options, $stream, ?float $startTime) : PromiseInterface |
|
80 | + { |
|
81 | + $hdrs = $this->lastHeaders; |
|
82 | + $this->lastHeaders = []; |
|
83 | + try { |
|
84 | + [$ver, $status, $reason, $headers] = HeaderProcessor::parseHeaders($hdrs); |
|
85 | + } catch (\Exception $e) { |
|
86 | + return P\Create::rejectionFor(new RequestException('An error was encountered while creating the response', $request, null, $e)); |
|
87 | + } |
|
88 | + [$stream, $headers] = $this->checkDecode($options, $headers, $stream); |
|
89 | + $stream = 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 Psr7\Response($status, $headers, $sink, $ver, $reason); |
|
96 | + } catch (\Exception $e) { |
|
97 | + return P\Create::rejectionFor(new 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 P\Create::rejectionFor(new 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 FulfilledPromise($response); |
|
113 | + } |
|
114 | + private function createSink(StreamInterface $stream, array $options) : StreamInterface |
|
115 | + { |
|
116 | + if (!empty($options['stream'])) { |
|
117 | + return $stream; |
|
118 | + } |
|
119 | + $sink = $options['sink'] ?? Psr7\Utils::tryFopen('php://temp', 'r+'); |
|
120 | + return \is_string($sink) ? new Psr7\LazyOpenStream($sink, 'w+') : 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 = 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 Psr7\InflateStream(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(StreamInterface $source, StreamInterface $sink, string $contentLength) : 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 | + 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(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 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, &$http_response_header, $contextResource, $context, $options, $request) { |
|
252 | + $resource = @\fopen((string) $uri, 'r', \false, $contextResource); |
|
253 | + $this->lastHeaders = $http_response_header ?? []; |
|
254 | + if (\false === $resource) { |
|
255 | + throw new ConnectException(\sprintf('Connection refused for URI %s', $uri), $request, null, $context); |
|
256 | + } |
|
257 | + if (isset($options['read_timeout'])) { |
|
258 | + $readTimeout = $options['read_timeout']; |
|
259 | + $sec = (int) $readTimeout; |
|
260 | + $usec = ($readTimeout - $sec) * 100000; |
|
261 | + \stream_set_timeout($resource, $sec, $usec); |
|
262 | + } |
|
263 | + return $resource; |
|
264 | + }); |
|
265 | + } |
|
266 | + private function resolveHost(RequestInterface $request, array $options) : UriInterface |
|
267 | + { |
|
268 | + $uri = $request->getUri(); |
|
269 | + if (isset($options['force_ip_resolve']) && !\filter_var($uri->getHost(), \FILTER_VALIDATE_IP)) { |
|
270 | + if ('v4' === $options['force_ip_resolve']) { |
|
271 | + $records = \dns_get_record($uri->getHost(), \DNS_A); |
|
272 | + if (\false === $records || !isset($records[0]['ip'])) { |
|
273 | + throw new ConnectException(\sprintf("Could not resolve IPv4 address for host '%s'", $uri->getHost()), $request); |
|
274 | + } |
|
275 | + return $uri->withHost($records[0]['ip']); |
|
276 | + } |
|
277 | + if ('v6' === $options['force_ip_resolve']) { |
|
278 | + $records = \dns_get_record($uri->getHost(), \DNS_AAAA); |
|
279 | + if (\false === $records || !isset($records[0]['ipv6'])) { |
|
280 | + throw new ConnectException(\sprintf("Could not resolve IPv6 address for host '%s'", $uri->getHost()), $request); |
|
281 | + } |
|
282 | + return $uri->withHost('[' . $records[0]['ipv6'] . ']'); |
|
283 | + } |
|
284 | + } |
|
285 | + return $uri; |
|
286 | + } |
|
287 | + private function getDefaultContext(RequestInterface $request) : array |
|
288 | + { |
|
289 | + $headers = ''; |
|
290 | + foreach ($request->getHeaders() as $name => $value) { |
|
291 | + foreach ($value as $val) { |
|
292 | + $headers .= "{$name}: {$val}\r\n"; |
|
293 | + } |
|
294 | + } |
|
295 | + $context = ['http' => ['method' => $request->getMethod(), 'header' => $headers, 'protocol_version' => $request->getProtocolVersion(), 'ignore_errors' => \true, 'follow_location' => 0], 'ssl' => ['peer_name' => $request->getUri()->getHost()]]; |
|
296 | + $body = (string) $request->getBody(); |
|
297 | + if ('' !== $body) { |
|
298 | + $context['http']['content'] = $body; |
|
299 | + // Prevent the HTTP handler from adding a Content-Type header. |
|
300 | + if (!$request->hasHeader('Content-Type')) { |
|
301 | + $context['http']['header'] .= "Content-Type:\r\n"; |
|
302 | + } |
|
303 | + } |
|
304 | + $context['http']['header'] = \rtrim($context['http']['header']); |
|
305 | + return $context; |
|
306 | + } |
|
307 | + /** |
|
308 | + * @param mixed $value as passed via Request transfer options. |
|
309 | + */ |
|
310 | + private function add_proxy(RequestInterface $request, array &$options, $value, array &$params) : void |
|
311 | + { |
|
312 | + $uri = null; |
|
313 | + if (!\is_array($value)) { |
|
314 | + $uri = $value; |
|
315 | + } else { |
|
316 | + $scheme = $request->getUri()->getScheme(); |
|
317 | + if (isset($value[$scheme])) { |
|
318 | + if (!isset($value['no']) || !Utils::isHostInNoProxy($request->getUri()->getHost(), $value['no'])) { |
|
319 | + $uri = $value[$scheme]; |
|
320 | + } |
|
321 | + } |
|
322 | + } |
|
323 | + if (!$uri) { |
|
324 | + return; |
|
325 | + } |
|
326 | + $parsed = $this->parse_proxy($uri); |
|
327 | + $options['http']['proxy'] = $parsed['proxy']; |
|
328 | + if ($parsed['auth']) { |
|
329 | + if (!isset($options['http']['header'])) { |
|
330 | + $options['http']['header'] = []; |
|
331 | + } |
|
332 | + $options['http']['header'] .= "\r\nProxy-Authorization: {$parsed['auth']}"; |
|
333 | + } |
|
334 | + } |
|
335 | + /** |
|
336 | + * Parses the given proxy URL to make it compatible with the format PHP's stream context expects. |
|
337 | + */ |
|
338 | + private function parse_proxy(string $url) : array |
|
339 | + { |
|
340 | + $parsed = \parse_url($url); |
|
341 | + if ($parsed !== \false && isset($parsed['scheme']) && $parsed['scheme'] === 'http') { |
|
342 | + if (isset($parsed['host']) && isset($parsed['port'])) { |
|
343 | + $auth = null; |
|
344 | + if (isset($parsed['user']) && isset($parsed['pass'])) { |
|
345 | + $auth = \base64_encode("{$parsed['user']}:{$parsed['pass']}"); |
|
346 | + } |
|
347 | + return ['proxy' => "tcp://{$parsed['host']}:{$parsed['port']}", 'auth' => $auth ? "Basic {$auth}" : null]; |
|
348 | + } |
|
349 | + } |
|
350 | + // Return proxy as-is. |
|
351 | + return ['proxy' => $url, 'auth' => null]; |
|
352 | + } |
|
353 | + /** |
|
354 | + * @param mixed $value as passed via Request transfer options. |
|
355 | + */ |
|
356 | + private function add_timeout(RequestInterface $request, array &$options, $value, array &$params) : void |
|
357 | + { |
|
358 | + if ($value > 0) { |
|
359 | + $options['http']['timeout'] = $value; |
|
360 | + } |
|
361 | + } |
|
362 | + /** |
|
363 | + * @param mixed $value as passed via Request transfer options. |
|
364 | + */ |
|
365 | + private function add_crypto_method(RequestInterface $request, array &$options, $value, array &$params) : void |
|
366 | + { |
|
367 | + 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) { |
|
368 | + $options['http']['crypto_method'] = $value; |
|
369 | + return; |
|
370 | + } |
|
371 | + throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided'); |
|
372 | + } |
|
373 | + /** |
|
374 | + * @param mixed $value as passed via Request transfer options. |
|
375 | + */ |
|
376 | + private function add_verify(RequestInterface $request, array &$options, $value, array &$params) : void |
|
377 | + { |
|
378 | + if ($value === \false) { |
|
379 | + $options['ssl']['verify_peer'] = \false; |
|
380 | + $options['ssl']['verify_peer_name'] = \false; |
|
381 | + return; |
|
382 | + } |
|
383 | + if (\is_string($value)) { |
|
384 | + $options['ssl']['cafile'] = $value; |
|
385 | + if (!\file_exists($value)) { |
|
386 | + throw new \RuntimeException("SSL CA bundle not found: {$value}"); |
|
387 | + } |
|
388 | + } elseif ($value !== \true) { |
|
389 | + throw new \InvalidArgumentException('Invalid verify request option'); |
|
390 | + } |
|
391 | + $options['ssl']['verify_peer'] = \true; |
|
392 | + $options['ssl']['verify_peer_name'] = \true; |
|
393 | + $options['ssl']['allow_self_signed'] = \false; |
|
394 | + } |
|
395 | + /** |
|
396 | + * @param mixed $value as passed via Request transfer options. |
|
397 | + */ |
|
398 | + private function add_cert(RequestInterface $request, array &$options, $value, array &$params) : void |
|
399 | + { |
|
400 | + if (\is_array($value)) { |
|
401 | + $options['ssl']['passphrase'] = $value[1]; |
|
402 | + $value = $value[0]; |
|
403 | + } |
|
404 | + if (!\file_exists($value)) { |
|
405 | + throw new \RuntimeException("SSL certificate not found: {$value}"); |
|
406 | + } |
|
407 | + $options['ssl']['local_cert'] = $value; |
|
408 | + } |
|
409 | + /** |
|
410 | + * @param mixed $value as passed via Request transfer options. |
|
411 | + */ |
|
412 | + private function add_progress(RequestInterface $request, array &$options, $value, array &$params) : void |
|
413 | + { |
|
414 | + self::addNotification($params, static function ($code, $a, $b, $c, $transferred, $total) use($value) { |
|
415 | + if ($code == \STREAM_NOTIFY_PROGRESS) { |
|
416 | + // The upload progress cannot be determined. Use 0 for cURL compatibility: |
|
417 | + // https://curl.se/libcurl/c/CURLOPT_PROGRESSFUNCTION.html |
|
418 | + $value($total, $transferred, 0, 0); |
|
419 | + } |
|
420 | + }); |
|
421 | + } |
|
422 | + /** |
|
423 | + * @param mixed $value as passed via Request transfer options. |
|
424 | + */ |
|
425 | + private function add_debug(RequestInterface $request, array &$options, $value, array &$params) : void |
|
426 | + { |
|
427 | + if ($value === \false) { |
|
428 | + return; |
|
429 | + } |
|
430 | + 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']; |
|
431 | + static $args = ['severity', 'message', 'message_code', 'bytes_transferred', 'bytes_max']; |
|
432 | + $value = Utils::debugResource($value); |
|
433 | + $ident = $request->getMethod() . ' ' . $request->getUri()->withFragment(''); |
|
434 | + self::addNotification($params, static function (int $code, ...$passed) use($ident, $value, $map, $args) : void { |
|
435 | + \fprintf($value, '<%s> [%s] ', $ident, $map[$code]); |
|
436 | + foreach (\array_filter($passed) as $i => $v) { |
|
437 | + \fwrite($value, $args[$i] . ': "' . $v . '" '); |
|
438 | + } |
|
439 | + \fwrite($value, "\n"); |
|
440 | + }); |
|
441 | + } |
|
442 | + private static function addNotification(array &$params, callable $notify) : void |
|
443 | + { |
|
444 | + // Wrap the existing function if needed. |
|
445 | + if (!isset($params['notification'])) { |
|
446 | + $params['notification'] = $notify; |
|
447 | + } else { |
|
448 | + $params['notification'] = self::callArray([$params['notification'], $notify]); |
|
449 | + } |
|
450 | + } |
|
451 | + private static function callArray(array $functions) : callable |
|
452 | + { |
|
453 | + return static function (...$args) use($functions) { |
|
454 | + foreach ($functions as $fn) { |
|
455 | + $fn(...$args); |
|
456 | + } |
|
457 | + }; |
|
458 | + } |
|
459 | 459 | } |
@@ -19,217 +19,217 @@ |
||
19 | 19 | */ |
20 | 20 | class CurlMultiHandler |
21 | 21 | { |
22 | - /** |
|
23 | - * @var CurlFactoryInterface |
|
24 | - */ |
|
25 | - private $factory; |
|
26 | - /** |
|
27 | - * @var int |
|
28 | - */ |
|
29 | - private $selectTimeout; |
|
30 | - /** |
|
31 | - * @var int Will be higher than 0 when `curl_multi_exec` is still running. |
|
32 | - */ |
|
33 | - private $active = 0; |
|
34 | - /** |
|
35 | - * @var array Request entry handles, indexed by handle id in `addRequest`. |
|
36 | - * |
|
37 | - * @see CurlMultiHandler::addRequest |
|
38 | - */ |
|
39 | - private $handles = []; |
|
40 | - /** |
|
41 | - * @var array<int, float> An array of delay times, indexed by handle id in `addRequest`. |
|
42 | - * |
|
43 | - * @see CurlMultiHandler::addRequest |
|
44 | - */ |
|
45 | - private $delays = []; |
|
46 | - /** |
|
47 | - * @var array<mixed> An associative array of CURLMOPT_* options and corresponding values for curl_multi_setopt() |
|
48 | - */ |
|
49 | - private $options = []; |
|
50 | - /** @var resource|\CurlMultiHandle */ |
|
51 | - private $_mh; |
|
52 | - /** |
|
53 | - * This handler accepts the following options: |
|
54 | - * |
|
55 | - * - handle_factory: An optional factory used to create curl handles |
|
56 | - * - select_timeout: Optional timeout (in seconds) to block before timing |
|
57 | - * out while selecting curl handles. Defaults to 1 second. |
|
58 | - * - options: An associative array of CURLMOPT_* options and |
|
59 | - * corresponding values for curl_multi_setopt() |
|
60 | - */ |
|
61 | - public function __construct(array $options = []) |
|
62 | - { |
|
63 | - $this->factory = $options['handle_factory'] ?? new CurlFactory(50); |
|
64 | - if (isset($options['select_timeout'])) { |
|
65 | - $this->selectTimeout = $options['select_timeout']; |
|
66 | - } elseif ($selectTimeout = Utils::getenv('GUZZLE_CURL_SELECT_TIMEOUT')) { |
|
67 | - @\trigger_error('Since guzzlehttp/guzzle 7.2.0: Using environment variable GUZZLE_CURL_SELECT_TIMEOUT is deprecated. Use option "select_timeout" instead.', \E_USER_DEPRECATED); |
|
68 | - $this->selectTimeout = (int) $selectTimeout; |
|
69 | - } else { |
|
70 | - $this->selectTimeout = 1; |
|
71 | - } |
|
72 | - $this->options = $options['options'] ?? []; |
|
73 | - // unsetting the property forces the first access to go through |
|
74 | - // __get(). |
|
75 | - unset($this->_mh); |
|
76 | - } |
|
77 | - /** |
|
78 | - * @param string $name |
|
79 | - * |
|
80 | - * @return resource|\CurlMultiHandle |
|
81 | - * |
|
82 | - * @throws \BadMethodCallException when another field as `_mh` will be gotten |
|
83 | - * @throws \RuntimeException when curl can not initialize a multi handle |
|
84 | - */ |
|
85 | - public function __get($name) |
|
86 | - { |
|
87 | - if ($name !== '_mh') { |
|
88 | - throw new \BadMethodCallException("Can not get other property as '_mh'."); |
|
89 | - } |
|
90 | - $multiHandle = \curl_multi_init(); |
|
91 | - if (\false === $multiHandle) { |
|
92 | - throw new \RuntimeException('Can not initialize curl multi handle.'); |
|
93 | - } |
|
94 | - $this->_mh = $multiHandle; |
|
95 | - foreach ($this->options as $option => $value) { |
|
96 | - // A warning is raised in case of a wrong option. |
|
97 | - \curl_multi_setopt($this->_mh, $option, $value); |
|
98 | - } |
|
99 | - return $this->_mh; |
|
100 | - } |
|
101 | - public function __destruct() |
|
102 | - { |
|
103 | - if (isset($this->_mh)) { |
|
104 | - \curl_multi_close($this->_mh); |
|
105 | - unset($this->_mh); |
|
106 | - } |
|
107 | - } |
|
108 | - public function __invoke(RequestInterface $request, array $options) : PromiseInterface |
|
109 | - { |
|
110 | - $easy = $this->factory->create($request, $options); |
|
111 | - $id = (int) $easy->handle; |
|
112 | - $promise = new Promise([$this, 'execute'], function () use($id) { |
|
113 | - return $this->cancel($id); |
|
114 | - }); |
|
115 | - $this->addRequest(['easy' => $easy, 'deferred' => $promise]); |
|
116 | - return $promise; |
|
117 | - } |
|
118 | - /** |
|
119 | - * Ticks the curl event loop. |
|
120 | - */ |
|
121 | - public function tick() : void |
|
122 | - { |
|
123 | - // Add any delayed handles if needed. |
|
124 | - if ($this->delays) { |
|
125 | - $currentTime = Utils::currentTime(); |
|
126 | - foreach ($this->delays as $id => $delay) { |
|
127 | - if ($currentTime >= $delay) { |
|
128 | - unset($this->delays[$id]); |
|
129 | - \curl_multi_add_handle($this->_mh, $this->handles[$id]['easy']->handle); |
|
130 | - } |
|
131 | - } |
|
132 | - } |
|
133 | - // Run curl_multi_exec in the queue to enable other async tasks to run |
|
134 | - P\Utils::queue()->add(Closure::fromCallable([$this, 'tickInQueue'])); |
|
135 | - // Step through the task queue which may add additional requests. |
|
136 | - P\Utils::queue()->run(); |
|
137 | - if ($this->active && \curl_multi_select($this->_mh, $this->selectTimeout) === -1) { |
|
138 | - // Perform a usleep if a select returns -1. |
|
139 | - // See: https://bugs.php.net/bug.php?id=61141 |
|
140 | - \usleep(250); |
|
141 | - } |
|
142 | - while (\curl_multi_exec($this->_mh, $this->active) === \CURLM_CALL_MULTI_PERFORM) { |
|
143 | - // Prevent busy looping for slow HTTP requests. |
|
144 | - \curl_multi_select($this->_mh, $this->selectTimeout); |
|
145 | - } |
|
146 | - $this->processMessages(); |
|
147 | - } |
|
148 | - /** |
|
149 | - * Runs \curl_multi_exec() inside the event loop, to prevent busy looping |
|
150 | - */ |
|
151 | - private function tickInQueue() : void |
|
152 | - { |
|
153 | - if (\curl_multi_exec($this->_mh, $this->active) === \CURLM_CALL_MULTI_PERFORM) { |
|
154 | - \curl_multi_select($this->_mh, 0); |
|
155 | - P\Utils::queue()->add(Closure::fromCallable([$this, 'tickInQueue'])); |
|
156 | - } |
|
157 | - } |
|
158 | - /** |
|
159 | - * Runs until all outstanding connections have completed. |
|
160 | - */ |
|
161 | - public function execute() : void |
|
162 | - { |
|
163 | - $queue = P\Utils::queue(); |
|
164 | - while ($this->handles || !$queue->isEmpty()) { |
|
165 | - // If there are no transfers, then sleep for the next delay |
|
166 | - if (!$this->active && $this->delays) { |
|
167 | - \usleep($this->timeToNext()); |
|
168 | - } |
|
169 | - $this->tick(); |
|
170 | - } |
|
171 | - } |
|
172 | - private function addRequest(array $entry) : void |
|
173 | - { |
|
174 | - $easy = $entry['easy']; |
|
175 | - $id = (int) $easy->handle; |
|
176 | - $this->handles[$id] = $entry; |
|
177 | - if (empty($easy->options['delay'])) { |
|
178 | - \curl_multi_add_handle($this->_mh, $easy->handle); |
|
179 | - } else { |
|
180 | - $this->delays[$id] = Utils::currentTime() + $easy->options['delay'] / 1000; |
|
181 | - } |
|
182 | - } |
|
183 | - /** |
|
184 | - * Cancels a handle from sending and removes references to it. |
|
185 | - * |
|
186 | - * @param int $id Handle ID to cancel and remove. |
|
187 | - * |
|
188 | - * @return bool True on success, false on failure. |
|
189 | - */ |
|
190 | - private function cancel($id) : bool |
|
191 | - { |
|
192 | - if (!\is_int($id)) { |
|
193 | - trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an integer to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); |
|
194 | - } |
|
195 | - // Cannot cancel if it has been processed. |
|
196 | - if (!isset($this->handles[$id])) { |
|
197 | - return \false; |
|
198 | - } |
|
199 | - $handle = $this->handles[$id]['easy']->handle; |
|
200 | - unset($this->delays[$id], $this->handles[$id]); |
|
201 | - \curl_multi_remove_handle($this->_mh, $handle); |
|
202 | - \curl_close($handle); |
|
203 | - return \true; |
|
204 | - } |
|
205 | - private function processMessages() : void |
|
206 | - { |
|
207 | - while ($done = \curl_multi_info_read($this->_mh)) { |
|
208 | - if ($done['msg'] !== \CURLMSG_DONE) { |
|
209 | - // if it's not done, then it would be premature to remove the handle. ref https://github.com/guzzle/guzzle/pull/2892#issuecomment-945150216 |
|
210 | - continue; |
|
211 | - } |
|
212 | - $id = (int) $done['handle']; |
|
213 | - \curl_multi_remove_handle($this->_mh, $done['handle']); |
|
214 | - if (!isset($this->handles[$id])) { |
|
215 | - // Probably was cancelled. |
|
216 | - continue; |
|
217 | - } |
|
218 | - $entry = $this->handles[$id]; |
|
219 | - unset($this->handles[$id], $this->delays[$id]); |
|
220 | - $entry['easy']->errno = $done['result']; |
|
221 | - $entry['deferred']->resolve(CurlFactory::finish($this, $entry['easy'], $this->factory)); |
|
222 | - } |
|
223 | - } |
|
224 | - private function timeToNext() : int |
|
225 | - { |
|
226 | - $currentTime = Utils::currentTime(); |
|
227 | - $nextTime = \PHP_INT_MAX; |
|
228 | - foreach ($this->delays as $time) { |
|
229 | - if ($time < $nextTime) { |
|
230 | - $nextTime = $time; |
|
231 | - } |
|
232 | - } |
|
233 | - return (int) \max(0, $nextTime - $currentTime) * 1000000; |
|
234 | - } |
|
22 | + /** |
|
23 | + * @var CurlFactoryInterface |
|
24 | + */ |
|
25 | + private $factory; |
|
26 | + /** |
|
27 | + * @var int |
|
28 | + */ |
|
29 | + private $selectTimeout; |
|
30 | + /** |
|
31 | + * @var int Will be higher than 0 when `curl_multi_exec` is still running. |
|
32 | + */ |
|
33 | + private $active = 0; |
|
34 | + /** |
|
35 | + * @var array Request entry handles, indexed by handle id in `addRequest`. |
|
36 | + * |
|
37 | + * @see CurlMultiHandler::addRequest |
|
38 | + */ |
|
39 | + private $handles = []; |
|
40 | + /** |
|
41 | + * @var array<int, float> An array of delay times, indexed by handle id in `addRequest`. |
|
42 | + * |
|
43 | + * @see CurlMultiHandler::addRequest |
|
44 | + */ |
|
45 | + private $delays = []; |
|
46 | + /** |
|
47 | + * @var array<mixed> An associative array of CURLMOPT_* options and corresponding values for curl_multi_setopt() |
|
48 | + */ |
|
49 | + private $options = []; |
|
50 | + /** @var resource|\CurlMultiHandle */ |
|
51 | + private $_mh; |
|
52 | + /** |
|
53 | + * This handler accepts the following options: |
|
54 | + * |
|
55 | + * - handle_factory: An optional factory used to create curl handles |
|
56 | + * - select_timeout: Optional timeout (in seconds) to block before timing |
|
57 | + * out while selecting curl handles. Defaults to 1 second. |
|
58 | + * - options: An associative array of CURLMOPT_* options and |
|
59 | + * corresponding values for curl_multi_setopt() |
|
60 | + */ |
|
61 | + public function __construct(array $options = []) |
|
62 | + { |
|
63 | + $this->factory = $options['handle_factory'] ?? new CurlFactory(50); |
|
64 | + if (isset($options['select_timeout'])) { |
|
65 | + $this->selectTimeout = $options['select_timeout']; |
|
66 | + } elseif ($selectTimeout = Utils::getenv('GUZZLE_CURL_SELECT_TIMEOUT')) { |
|
67 | + @\trigger_error('Since guzzlehttp/guzzle 7.2.0: Using environment variable GUZZLE_CURL_SELECT_TIMEOUT is deprecated. Use option "select_timeout" instead.', \E_USER_DEPRECATED); |
|
68 | + $this->selectTimeout = (int) $selectTimeout; |
|
69 | + } else { |
|
70 | + $this->selectTimeout = 1; |
|
71 | + } |
|
72 | + $this->options = $options['options'] ?? []; |
|
73 | + // unsetting the property forces the first access to go through |
|
74 | + // __get(). |
|
75 | + unset($this->_mh); |
|
76 | + } |
|
77 | + /** |
|
78 | + * @param string $name |
|
79 | + * |
|
80 | + * @return resource|\CurlMultiHandle |
|
81 | + * |
|
82 | + * @throws \BadMethodCallException when another field as `_mh` will be gotten |
|
83 | + * @throws \RuntimeException when curl can not initialize a multi handle |
|
84 | + */ |
|
85 | + public function __get($name) |
|
86 | + { |
|
87 | + if ($name !== '_mh') { |
|
88 | + throw new \BadMethodCallException("Can not get other property as '_mh'."); |
|
89 | + } |
|
90 | + $multiHandle = \curl_multi_init(); |
|
91 | + if (\false === $multiHandle) { |
|
92 | + throw new \RuntimeException('Can not initialize curl multi handle.'); |
|
93 | + } |
|
94 | + $this->_mh = $multiHandle; |
|
95 | + foreach ($this->options as $option => $value) { |
|
96 | + // A warning is raised in case of a wrong option. |
|
97 | + \curl_multi_setopt($this->_mh, $option, $value); |
|
98 | + } |
|
99 | + return $this->_mh; |
|
100 | + } |
|
101 | + public function __destruct() |
|
102 | + { |
|
103 | + if (isset($this->_mh)) { |
|
104 | + \curl_multi_close($this->_mh); |
|
105 | + unset($this->_mh); |
|
106 | + } |
|
107 | + } |
|
108 | + public function __invoke(RequestInterface $request, array $options) : PromiseInterface |
|
109 | + { |
|
110 | + $easy = $this->factory->create($request, $options); |
|
111 | + $id = (int) $easy->handle; |
|
112 | + $promise = new Promise([$this, 'execute'], function () use($id) { |
|
113 | + return $this->cancel($id); |
|
114 | + }); |
|
115 | + $this->addRequest(['easy' => $easy, 'deferred' => $promise]); |
|
116 | + return $promise; |
|
117 | + } |
|
118 | + /** |
|
119 | + * Ticks the curl event loop. |
|
120 | + */ |
|
121 | + public function tick() : void |
|
122 | + { |
|
123 | + // Add any delayed handles if needed. |
|
124 | + if ($this->delays) { |
|
125 | + $currentTime = Utils::currentTime(); |
|
126 | + foreach ($this->delays as $id => $delay) { |
|
127 | + if ($currentTime >= $delay) { |
|
128 | + unset($this->delays[$id]); |
|
129 | + \curl_multi_add_handle($this->_mh, $this->handles[$id]['easy']->handle); |
|
130 | + } |
|
131 | + } |
|
132 | + } |
|
133 | + // Run curl_multi_exec in the queue to enable other async tasks to run |
|
134 | + P\Utils::queue()->add(Closure::fromCallable([$this, 'tickInQueue'])); |
|
135 | + // Step through the task queue which may add additional requests. |
|
136 | + P\Utils::queue()->run(); |
|
137 | + if ($this->active && \curl_multi_select($this->_mh, $this->selectTimeout) === -1) { |
|
138 | + // Perform a usleep if a select returns -1. |
|
139 | + // See: https://bugs.php.net/bug.php?id=61141 |
|
140 | + \usleep(250); |
|
141 | + } |
|
142 | + while (\curl_multi_exec($this->_mh, $this->active) === \CURLM_CALL_MULTI_PERFORM) { |
|
143 | + // Prevent busy looping for slow HTTP requests. |
|
144 | + \curl_multi_select($this->_mh, $this->selectTimeout); |
|
145 | + } |
|
146 | + $this->processMessages(); |
|
147 | + } |
|
148 | + /** |
|
149 | + * Runs \curl_multi_exec() inside the event loop, to prevent busy looping |
|
150 | + */ |
|
151 | + private function tickInQueue() : void |
|
152 | + { |
|
153 | + if (\curl_multi_exec($this->_mh, $this->active) === \CURLM_CALL_MULTI_PERFORM) { |
|
154 | + \curl_multi_select($this->_mh, 0); |
|
155 | + P\Utils::queue()->add(Closure::fromCallable([$this, 'tickInQueue'])); |
|
156 | + } |
|
157 | + } |
|
158 | + /** |
|
159 | + * Runs until all outstanding connections have completed. |
|
160 | + */ |
|
161 | + public function execute() : void |
|
162 | + { |
|
163 | + $queue = P\Utils::queue(); |
|
164 | + while ($this->handles || !$queue->isEmpty()) { |
|
165 | + // If there are no transfers, then sleep for the next delay |
|
166 | + if (!$this->active && $this->delays) { |
|
167 | + \usleep($this->timeToNext()); |
|
168 | + } |
|
169 | + $this->tick(); |
|
170 | + } |
|
171 | + } |
|
172 | + private function addRequest(array $entry) : void |
|
173 | + { |
|
174 | + $easy = $entry['easy']; |
|
175 | + $id = (int) $easy->handle; |
|
176 | + $this->handles[$id] = $entry; |
|
177 | + if (empty($easy->options['delay'])) { |
|
178 | + \curl_multi_add_handle($this->_mh, $easy->handle); |
|
179 | + } else { |
|
180 | + $this->delays[$id] = Utils::currentTime() + $easy->options['delay'] / 1000; |
|
181 | + } |
|
182 | + } |
|
183 | + /** |
|
184 | + * Cancels a handle from sending and removes references to it. |
|
185 | + * |
|
186 | + * @param int $id Handle ID to cancel and remove. |
|
187 | + * |
|
188 | + * @return bool True on success, false on failure. |
|
189 | + */ |
|
190 | + private function cancel($id) : bool |
|
191 | + { |
|
192 | + if (!\is_int($id)) { |
|
193 | + trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an integer to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); |
|
194 | + } |
|
195 | + // Cannot cancel if it has been processed. |
|
196 | + if (!isset($this->handles[$id])) { |
|
197 | + return \false; |
|
198 | + } |
|
199 | + $handle = $this->handles[$id]['easy']->handle; |
|
200 | + unset($this->delays[$id], $this->handles[$id]); |
|
201 | + \curl_multi_remove_handle($this->_mh, $handle); |
|
202 | + \curl_close($handle); |
|
203 | + return \true; |
|
204 | + } |
|
205 | + private function processMessages() : void |
|
206 | + { |
|
207 | + while ($done = \curl_multi_info_read($this->_mh)) { |
|
208 | + if ($done['msg'] !== \CURLMSG_DONE) { |
|
209 | + // if it's not done, then it would be premature to remove the handle. ref https://github.com/guzzle/guzzle/pull/2892#issuecomment-945150216 |
|
210 | + continue; |
|
211 | + } |
|
212 | + $id = (int) $done['handle']; |
|
213 | + \curl_multi_remove_handle($this->_mh, $done['handle']); |
|
214 | + if (!isset($this->handles[$id])) { |
|
215 | + // Probably was cancelled. |
|
216 | + continue; |
|
217 | + } |
|
218 | + $entry = $this->handles[$id]; |
|
219 | + unset($this->handles[$id], $this->delays[$id]); |
|
220 | + $entry['easy']->errno = $done['result']; |
|
221 | + $entry['deferred']->resolve(CurlFactory::finish($this, $entry['easy'], $this->factory)); |
|
222 | + } |
|
223 | + } |
|
224 | + private function timeToNext() : int |
|
225 | + { |
|
226 | + $currentTime = Utils::currentTime(); |
|
227 | + $nextTime = \PHP_INT_MAX; |
|
228 | + foreach ($this->delays as $time) { |
|
229 | + if ($time < $nextTime) { |
|
230 | + $nextTime = $time; |
|
231 | + } |
|
232 | + } |
|
233 | + return (int) \max(0, $nextTime - $currentTime) * 1000000; |
|
234 | + } |
|
235 | 235 | } |
@@ -18,157 +18,157 @@ |
||
18 | 18 | */ |
19 | 19 | class MockHandler implements \Countable |
20 | 20 | { |
21 | - /** |
|
22 | - * @var array |
|
23 | - */ |
|
24 | - private $queue = []; |
|
25 | - /** |
|
26 | - * @var RequestInterface|null |
|
27 | - */ |
|
28 | - private $lastRequest; |
|
29 | - /** |
|
30 | - * @var array |
|
31 | - */ |
|
32 | - private $lastOptions = []; |
|
33 | - /** |
|
34 | - * @var callable|null |
|
35 | - */ |
|
36 | - private $onFulfilled; |
|
37 | - /** |
|
38 | - * @var callable|null |
|
39 | - */ |
|
40 | - private $onRejected; |
|
41 | - /** |
|
42 | - * Creates a new MockHandler that uses the default handler stack list of |
|
43 | - * middlewares. |
|
44 | - * |
|
45 | - * @param array|null $queue Array of responses, callables, or exceptions. |
|
46 | - * @param callable|null $onFulfilled Callback to invoke when the return value is fulfilled. |
|
47 | - * @param callable|null $onRejected Callback to invoke when the return value is rejected. |
|
48 | - */ |
|
49 | - public static function createWithMiddleware(?array $queue = null, ?callable $onFulfilled = null, ?callable $onRejected = null) : HandlerStack |
|
50 | - { |
|
51 | - return HandlerStack::create(new self($queue, $onFulfilled, $onRejected)); |
|
52 | - } |
|
53 | - /** |
|
54 | - * The passed in value must be an array of |
|
55 | - * {@see ResponseInterface} objects, Exceptions, |
|
56 | - * callables, or Promises. |
|
57 | - * |
|
58 | - * @param array<int, mixed>|null $queue The parameters to be passed to the append function, as an indexed array. |
|
59 | - * @param callable|null $onFulfilled Callback to invoke when the return value is fulfilled. |
|
60 | - * @param callable|null $onRejected Callback to invoke when the return value is rejected. |
|
61 | - */ |
|
62 | - public function __construct(?array $queue = null, ?callable $onFulfilled = null, ?callable $onRejected = null) |
|
63 | - { |
|
64 | - $this->onFulfilled = $onFulfilled; |
|
65 | - $this->onRejected = $onRejected; |
|
66 | - if ($queue) { |
|
67 | - // array_values included for BC |
|
68 | - $this->append(...\array_values($queue)); |
|
69 | - } |
|
70 | - } |
|
71 | - public function __invoke(RequestInterface $request, array $options) : PromiseInterface |
|
72 | - { |
|
73 | - if (!$this->queue) { |
|
74 | - throw new \OutOfBoundsException('Mock queue is empty'); |
|
75 | - } |
|
76 | - if (isset($options['delay']) && \is_numeric($options['delay'])) { |
|
77 | - \usleep((int) $options['delay'] * 1000); |
|
78 | - } |
|
79 | - $this->lastRequest = $request; |
|
80 | - $this->lastOptions = $options; |
|
81 | - $response = \array_shift($this->queue); |
|
82 | - if (isset($options['on_headers'])) { |
|
83 | - if (!\is_callable($options['on_headers'])) { |
|
84 | - throw new \InvalidArgumentException('on_headers must be callable'); |
|
85 | - } |
|
86 | - try { |
|
87 | - $options['on_headers']($response); |
|
88 | - } catch (\Exception $e) { |
|
89 | - $msg = 'An error was encountered during the on_headers event'; |
|
90 | - $response = new RequestException($msg, $request, $response, $e); |
|
91 | - } |
|
92 | - } |
|
93 | - if (\is_callable($response)) { |
|
94 | - $response = $response($request, $options); |
|
95 | - } |
|
96 | - $response = $response instanceof \Throwable ? P\Create::rejectionFor($response) : P\Create::promiseFor($response); |
|
97 | - return $response->then(function (?ResponseInterface $value) use($request, $options) { |
|
98 | - $this->invokeStats($request, $options, $value); |
|
99 | - if ($this->onFulfilled) { |
|
100 | - ($this->onFulfilled)($value); |
|
101 | - } |
|
102 | - if ($value !== null && isset($options['sink'])) { |
|
103 | - $contents = (string) $value->getBody(); |
|
104 | - $sink = $options['sink']; |
|
105 | - if (\is_resource($sink)) { |
|
106 | - \fwrite($sink, $contents); |
|
107 | - } elseif (\is_string($sink)) { |
|
108 | - \file_put_contents($sink, $contents); |
|
109 | - } elseif ($sink instanceof StreamInterface) { |
|
110 | - $sink->write($contents); |
|
111 | - } |
|
112 | - } |
|
113 | - return $value; |
|
114 | - }, function ($reason) use($request, $options) { |
|
115 | - $this->invokeStats($request, $options, null, $reason); |
|
116 | - if ($this->onRejected) { |
|
117 | - ($this->onRejected)($reason); |
|
118 | - } |
|
119 | - return P\Create::rejectionFor($reason); |
|
120 | - }); |
|
121 | - } |
|
122 | - /** |
|
123 | - * Adds one or more variadic requests, exceptions, callables, or promises |
|
124 | - * to the queue. |
|
125 | - * |
|
126 | - * @param mixed ...$values |
|
127 | - */ |
|
128 | - public function append(...$values) : void |
|
129 | - { |
|
130 | - foreach ($values as $value) { |
|
131 | - if ($value instanceof ResponseInterface || $value instanceof \Throwable || $value instanceof PromiseInterface || \is_callable($value)) { |
|
132 | - $this->queue[] = $value; |
|
133 | - } else { |
|
134 | - throw new \TypeError('Expected a Response, Promise, Throwable or callable. Found ' . Utils::describeType($value)); |
|
135 | - } |
|
136 | - } |
|
137 | - } |
|
138 | - /** |
|
139 | - * Get the last received request. |
|
140 | - */ |
|
141 | - public function getLastRequest() : ?RequestInterface |
|
142 | - { |
|
143 | - return $this->lastRequest; |
|
144 | - } |
|
145 | - /** |
|
146 | - * Get the last received request options. |
|
147 | - */ |
|
148 | - public function getLastOptions() : array |
|
149 | - { |
|
150 | - return $this->lastOptions; |
|
151 | - } |
|
152 | - /** |
|
153 | - * Returns the number of remaining items in the queue. |
|
154 | - */ |
|
155 | - public function count() : int |
|
156 | - { |
|
157 | - return \count($this->queue); |
|
158 | - } |
|
159 | - public function reset() : void |
|
160 | - { |
|
161 | - $this->queue = []; |
|
162 | - } |
|
163 | - /** |
|
164 | - * @param mixed $reason Promise or reason. |
|
165 | - */ |
|
166 | - private function invokeStats(RequestInterface $request, array $options, ?ResponseInterface $response = null, $reason = null) : void |
|
167 | - { |
|
168 | - if (isset($options['on_stats'])) { |
|
169 | - $transferTime = $options['transfer_time'] ?? 0; |
|
170 | - $stats = new TransferStats($request, $response, $transferTime, $reason); |
|
171 | - $options['on_stats']($stats); |
|
172 | - } |
|
173 | - } |
|
21 | + /** |
|
22 | + * @var array |
|
23 | + */ |
|
24 | + private $queue = []; |
|
25 | + /** |
|
26 | + * @var RequestInterface|null |
|
27 | + */ |
|
28 | + private $lastRequest; |
|
29 | + /** |
|
30 | + * @var array |
|
31 | + */ |
|
32 | + private $lastOptions = []; |
|
33 | + /** |
|
34 | + * @var callable|null |
|
35 | + */ |
|
36 | + private $onFulfilled; |
|
37 | + /** |
|
38 | + * @var callable|null |
|
39 | + */ |
|
40 | + private $onRejected; |
|
41 | + /** |
|
42 | + * Creates a new MockHandler that uses the default handler stack list of |
|
43 | + * middlewares. |
|
44 | + * |
|
45 | + * @param array|null $queue Array of responses, callables, or exceptions. |
|
46 | + * @param callable|null $onFulfilled Callback to invoke when the return value is fulfilled. |
|
47 | + * @param callable|null $onRejected Callback to invoke when the return value is rejected. |
|
48 | + */ |
|
49 | + public static function createWithMiddleware(?array $queue = null, ?callable $onFulfilled = null, ?callable $onRejected = null) : HandlerStack |
|
50 | + { |
|
51 | + return HandlerStack::create(new self($queue, $onFulfilled, $onRejected)); |
|
52 | + } |
|
53 | + /** |
|
54 | + * The passed in value must be an array of |
|
55 | + * {@see ResponseInterface} objects, Exceptions, |
|
56 | + * callables, or Promises. |
|
57 | + * |
|
58 | + * @param array<int, mixed>|null $queue The parameters to be passed to the append function, as an indexed array. |
|
59 | + * @param callable|null $onFulfilled Callback to invoke when the return value is fulfilled. |
|
60 | + * @param callable|null $onRejected Callback to invoke when the return value is rejected. |
|
61 | + */ |
|
62 | + public function __construct(?array $queue = null, ?callable $onFulfilled = null, ?callable $onRejected = null) |
|
63 | + { |
|
64 | + $this->onFulfilled = $onFulfilled; |
|
65 | + $this->onRejected = $onRejected; |
|
66 | + if ($queue) { |
|
67 | + // array_values included for BC |
|
68 | + $this->append(...\array_values($queue)); |
|
69 | + } |
|
70 | + } |
|
71 | + public function __invoke(RequestInterface $request, array $options) : PromiseInterface |
|
72 | + { |
|
73 | + if (!$this->queue) { |
|
74 | + throw new \OutOfBoundsException('Mock queue is empty'); |
|
75 | + } |
|
76 | + if (isset($options['delay']) && \is_numeric($options['delay'])) { |
|
77 | + \usleep((int) $options['delay'] * 1000); |
|
78 | + } |
|
79 | + $this->lastRequest = $request; |
|
80 | + $this->lastOptions = $options; |
|
81 | + $response = \array_shift($this->queue); |
|
82 | + if (isset($options['on_headers'])) { |
|
83 | + if (!\is_callable($options['on_headers'])) { |
|
84 | + throw new \InvalidArgumentException('on_headers must be callable'); |
|
85 | + } |
|
86 | + try { |
|
87 | + $options['on_headers']($response); |
|
88 | + } catch (\Exception $e) { |
|
89 | + $msg = 'An error was encountered during the on_headers event'; |
|
90 | + $response = new RequestException($msg, $request, $response, $e); |
|
91 | + } |
|
92 | + } |
|
93 | + if (\is_callable($response)) { |
|
94 | + $response = $response($request, $options); |
|
95 | + } |
|
96 | + $response = $response instanceof \Throwable ? P\Create::rejectionFor($response) : P\Create::promiseFor($response); |
|
97 | + return $response->then(function (?ResponseInterface $value) use($request, $options) { |
|
98 | + $this->invokeStats($request, $options, $value); |
|
99 | + if ($this->onFulfilled) { |
|
100 | + ($this->onFulfilled)($value); |
|
101 | + } |
|
102 | + if ($value !== null && isset($options['sink'])) { |
|
103 | + $contents = (string) $value->getBody(); |
|
104 | + $sink = $options['sink']; |
|
105 | + if (\is_resource($sink)) { |
|
106 | + \fwrite($sink, $contents); |
|
107 | + } elseif (\is_string($sink)) { |
|
108 | + \file_put_contents($sink, $contents); |
|
109 | + } elseif ($sink instanceof StreamInterface) { |
|
110 | + $sink->write($contents); |
|
111 | + } |
|
112 | + } |
|
113 | + return $value; |
|
114 | + }, function ($reason) use($request, $options) { |
|
115 | + $this->invokeStats($request, $options, null, $reason); |
|
116 | + if ($this->onRejected) { |
|
117 | + ($this->onRejected)($reason); |
|
118 | + } |
|
119 | + return P\Create::rejectionFor($reason); |
|
120 | + }); |
|
121 | + } |
|
122 | + /** |
|
123 | + * Adds one or more variadic requests, exceptions, callables, or promises |
|
124 | + * to the queue. |
|
125 | + * |
|
126 | + * @param mixed ...$values |
|
127 | + */ |
|
128 | + public function append(...$values) : void |
|
129 | + { |
|
130 | + foreach ($values as $value) { |
|
131 | + if ($value instanceof ResponseInterface || $value instanceof \Throwable || $value instanceof PromiseInterface || \is_callable($value)) { |
|
132 | + $this->queue[] = $value; |
|
133 | + } else { |
|
134 | + throw new \TypeError('Expected a Response, Promise, Throwable or callable. Found ' . Utils::describeType($value)); |
|
135 | + } |
|
136 | + } |
|
137 | + } |
|
138 | + /** |
|
139 | + * Get the last received request. |
|
140 | + */ |
|
141 | + public function getLastRequest() : ?RequestInterface |
|
142 | + { |
|
143 | + return $this->lastRequest; |
|
144 | + } |
|
145 | + /** |
|
146 | + * Get the last received request options. |
|
147 | + */ |
|
148 | + public function getLastOptions() : array |
|
149 | + { |
|
150 | + return $this->lastOptions; |
|
151 | + } |
|
152 | + /** |
|
153 | + * Returns the number of remaining items in the queue. |
|
154 | + */ |
|
155 | + public function count() : int |
|
156 | + { |
|
157 | + return \count($this->queue); |
|
158 | + } |
|
159 | + public function reset() : void |
|
160 | + { |
|
161 | + $this->queue = []; |
|
162 | + } |
|
163 | + /** |
|
164 | + * @param mixed $reason Promise or reason. |
|
165 | + */ |
|
166 | + private function invokeStats(RequestInterface $request, array $options, ?ResponseInterface $response = null, $reason = null) : void |
|
167 | + { |
|
168 | + if (isset($options['on_stats'])) { |
|
169 | + $transferTime = $options['transfer_time'] ?? 0; |
|
170 | + $stats = new TransferStats($request, $response, $transferTime, $reason); |
|
171 | + $options['on_stats']($stats); |
|
172 | + } |
|
173 | + } |
|
174 | 174 | } |
@@ -19,545 +19,545 @@ |
||
19 | 19 | */ |
20 | 20 | class CurlFactory implements CurlFactoryInterface |
21 | 21 | { |
22 | - public const CURL_VERSION_STR = 'curl_version'; |
|
23 | - /** |
|
24 | - * @deprecated |
|
25 | - */ |
|
26 | - public const LOW_CURL_VERSION_NUMBER = '7.21.2'; |
|
27 | - /** |
|
28 | - * @var resource[]|\CurlHandle[] |
|
29 | - */ |
|
30 | - private $handles = []; |
|
31 | - /** |
|
32 | - * @var int Total number of idle handles to keep in cache |
|
33 | - */ |
|
34 | - private $maxHandles; |
|
35 | - /** |
|
36 | - * @param int $maxHandles Maximum number of idle handles. |
|
37 | - */ |
|
38 | - public function __construct(int $maxHandles) |
|
39 | - { |
|
40 | - $this->maxHandles = $maxHandles; |
|
41 | - } |
|
42 | - public function create(RequestInterface $request, array $options) : EasyHandle |
|
43 | - { |
|
44 | - $protocolVersion = $request->getProtocolVersion(); |
|
45 | - if ('2' === $protocolVersion || '2.0' === $protocolVersion) { |
|
46 | - if (!self::supportsHttp2()) { |
|
47 | - throw new ConnectException('HTTP/2 is supported by the cURL handler, however libcurl is built without HTTP/2 support.', $request); |
|
48 | - } |
|
49 | - } elseif ('1.0' !== $protocolVersion && '1.1' !== $protocolVersion) { |
|
50 | - throw new ConnectException(\sprintf('HTTP/%s is not supported by the cURL handler.', $protocolVersion), $request); |
|
51 | - } |
|
52 | - if (isset($options['curl']['body_as_string'])) { |
|
53 | - $options['_body_as_string'] = $options['curl']['body_as_string']; |
|
54 | - unset($options['curl']['body_as_string']); |
|
55 | - } |
|
56 | - $easy = new EasyHandle(); |
|
57 | - $easy->request = $request; |
|
58 | - $easy->options = $options; |
|
59 | - $conf = $this->getDefaultConf($easy); |
|
60 | - $this->applyMethod($easy, $conf); |
|
61 | - $this->applyHandlerOptions($easy, $conf); |
|
62 | - $this->applyHeaders($easy, $conf); |
|
63 | - unset($conf['_headers']); |
|
64 | - // Add handler options from the request configuration options |
|
65 | - if (isset($options['curl'])) { |
|
66 | - $conf = \array_replace($conf, $options['curl']); |
|
67 | - } |
|
68 | - $conf[\CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy); |
|
69 | - $easy->handle = $this->handles ? \array_pop($this->handles) : \curl_init(); |
|
70 | - \curl_setopt_array($easy->handle, $conf); |
|
71 | - return $easy; |
|
72 | - } |
|
73 | - private static function supportsHttp2() : bool |
|
74 | - { |
|
75 | - static $supportsHttp2 = null; |
|
76 | - if (null === $supportsHttp2) { |
|
77 | - $supportsHttp2 = self::supportsTls12() && \defined('CURL_VERSION_HTTP2') && \CURL_VERSION_HTTP2 & \curl_version()['features']; |
|
78 | - } |
|
79 | - return $supportsHttp2; |
|
80 | - } |
|
81 | - private static function supportsTls12() : bool |
|
82 | - { |
|
83 | - static $supportsTls12 = null; |
|
84 | - if (null === $supportsTls12) { |
|
85 | - $supportsTls12 = \CURL_SSLVERSION_TLSv1_2 & \curl_version()['features']; |
|
86 | - } |
|
87 | - return $supportsTls12; |
|
88 | - } |
|
89 | - private static function supportsTls13() : bool |
|
90 | - { |
|
91 | - static $supportsTls13 = null; |
|
92 | - if (null === $supportsTls13) { |
|
93 | - $supportsTls13 = \defined('CURL_SSLVERSION_TLSv1_3') && \CURL_SSLVERSION_TLSv1_3 & \curl_version()['features']; |
|
94 | - } |
|
95 | - return $supportsTls13; |
|
96 | - } |
|
97 | - public function release(EasyHandle $easy) : void |
|
98 | - { |
|
99 | - $resource = $easy->handle; |
|
100 | - unset($easy->handle); |
|
101 | - if (\count($this->handles) >= $this->maxHandles) { |
|
102 | - \curl_close($resource); |
|
103 | - } else { |
|
104 | - // Remove all callback functions as they can hold onto references |
|
105 | - // and are not cleaned up by curl_reset. Using curl_setopt_array |
|
106 | - // does not work for some reason, so removing each one |
|
107 | - // individually. |
|
108 | - \curl_setopt($resource, \CURLOPT_HEADERFUNCTION, null); |
|
109 | - \curl_setopt($resource, \CURLOPT_READFUNCTION, null); |
|
110 | - \curl_setopt($resource, \CURLOPT_WRITEFUNCTION, null); |
|
111 | - \curl_setopt($resource, \CURLOPT_PROGRESSFUNCTION, null); |
|
112 | - \curl_reset($resource); |
|
113 | - $this->handles[] = $resource; |
|
114 | - } |
|
115 | - } |
|
116 | - /** |
|
117 | - * Completes a cURL transaction, either returning a response promise or a |
|
118 | - * rejected promise. |
|
119 | - * |
|
120 | - * @param callable(RequestInterface, array): PromiseInterface $handler |
|
121 | - * @param CurlFactoryInterface $factory Dictates how the handle is released |
|
122 | - */ |
|
123 | - public static function finish(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory) : PromiseInterface |
|
124 | - { |
|
125 | - if (isset($easy->options['on_stats'])) { |
|
126 | - self::invokeStats($easy); |
|
127 | - } |
|
128 | - if (!$easy->response || $easy->errno) { |
|
129 | - return self::finishError($handler, $easy, $factory); |
|
130 | - } |
|
131 | - // Return the response if it is present and there is no error. |
|
132 | - $factory->release($easy); |
|
133 | - // Rewind the body of the response if possible. |
|
134 | - $body = $easy->response->getBody(); |
|
135 | - if ($body->isSeekable()) { |
|
136 | - $body->rewind(); |
|
137 | - } |
|
138 | - return new FulfilledPromise($easy->response); |
|
139 | - } |
|
140 | - private static function invokeStats(EasyHandle $easy) : void |
|
141 | - { |
|
142 | - $curlStats = \curl_getinfo($easy->handle); |
|
143 | - $curlStats['appconnect_time'] = \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME); |
|
144 | - $stats = new TransferStats($easy->request, $easy->response, $curlStats['total_time'], $easy->errno, $curlStats); |
|
145 | - $easy->options['on_stats']($stats); |
|
146 | - } |
|
147 | - /** |
|
148 | - * @param callable(RequestInterface, array): PromiseInterface $handler |
|
149 | - */ |
|
150 | - private static function finishError(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory) : PromiseInterface |
|
151 | - { |
|
152 | - // Get error information and release the handle to the factory. |
|
153 | - $ctx = ['errno' => $easy->errno, 'error' => \curl_error($easy->handle), 'appconnect_time' => \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME)] + \curl_getinfo($easy->handle); |
|
154 | - $ctx[self::CURL_VERSION_STR] = self::getCurlVersion(); |
|
155 | - $factory->release($easy); |
|
156 | - // Retry when nothing is present or when curl failed to rewind. |
|
157 | - if (empty($easy->options['_err_message']) && (!$easy->errno || $easy->errno == 65)) { |
|
158 | - return self::retryFailedRewind($handler, $easy, $ctx); |
|
159 | - } |
|
160 | - return self::createRejection($easy, $ctx); |
|
161 | - } |
|
162 | - private static function getCurlVersion() : string |
|
163 | - { |
|
164 | - static $curlVersion = null; |
|
165 | - if (null === $curlVersion) { |
|
166 | - $curlVersion = \curl_version()['version']; |
|
167 | - } |
|
168 | - return $curlVersion; |
|
169 | - } |
|
170 | - private static function createRejection(EasyHandle $easy, array $ctx) : PromiseInterface |
|
171 | - { |
|
172 | - static $connectionErrors = [\CURLE_OPERATION_TIMEOUTED => \true, \CURLE_COULDNT_RESOLVE_HOST => \true, \CURLE_COULDNT_CONNECT => \true, \CURLE_SSL_CONNECT_ERROR => \true, \CURLE_GOT_NOTHING => \true]; |
|
173 | - if ($easy->createResponseException) { |
|
174 | - return P\Create::rejectionFor(new RequestException('An error was encountered while creating the response', $easy->request, $easy->response, $easy->createResponseException, $ctx)); |
|
175 | - } |
|
176 | - // If an exception was encountered during the onHeaders event, then |
|
177 | - // return a rejected promise that wraps that exception. |
|
178 | - if ($easy->onHeadersException) { |
|
179 | - return P\Create::rejectionFor(new RequestException('An error was encountered during the on_headers event', $easy->request, $easy->response, $easy->onHeadersException, $ctx)); |
|
180 | - } |
|
181 | - $uri = $easy->request->getUri(); |
|
182 | - $sanitizedError = self::sanitizeCurlError($ctx['error'] ?? '', $uri); |
|
183 | - $message = \sprintf('cURL error %s: %s (%s)', $ctx['errno'], $sanitizedError, 'see https://curl.haxx.se/libcurl/c/libcurl-errors.html'); |
|
184 | - if ('' !== $sanitizedError) { |
|
185 | - $redactedUriString = \OCA\FullTextSearch_Elasticsearch\Vendor\GuzzleHttp\Psr7\Utils::redactUserInfo($uri)->__toString(); |
|
186 | - if ($redactedUriString !== '' && \false === \strpos($sanitizedError, $redactedUriString)) { |
|
187 | - $message .= \sprintf(' for %s', $redactedUriString); |
|
188 | - } |
|
189 | - } |
|
190 | - // Create a connection exception if it was a specific error code. |
|
191 | - $error = isset($connectionErrors[$easy->errno]) ? new ConnectException($message, $easy->request, null, $ctx) : new RequestException($message, $easy->request, $easy->response, null, $ctx); |
|
192 | - return P\Create::rejectionFor($error); |
|
193 | - } |
|
194 | - private static function sanitizeCurlError(string $error, UriInterface $uri) : string |
|
195 | - { |
|
196 | - if ('' === $error) { |
|
197 | - return $error; |
|
198 | - } |
|
199 | - $baseUri = $uri->withQuery('')->withFragment(''); |
|
200 | - $baseUriString = $baseUri->__toString(); |
|
201 | - if ('' === $baseUriString) { |
|
202 | - return $error; |
|
203 | - } |
|
204 | - $redactedUriString = \OCA\FullTextSearch_Elasticsearch\Vendor\GuzzleHttp\Psr7\Utils::redactUserInfo($baseUri)->__toString(); |
|
205 | - return \str_replace($baseUriString, $redactedUriString, $error); |
|
206 | - } |
|
207 | - /** |
|
208 | - * @return array<int|string, mixed> |
|
209 | - */ |
|
210 | - private function getDefaultConf(EasyHandle $easy) : array |
|
211 | - { |
|
212 | - $conf = ['_headers' => $easy->request->getHeaders(), \CURLOPT_CUSTOMREQUEST => $easy->request->getMethod(), \CURLOPT_URL => (string) $easy->request->getUri()->withFragment(''), \CURLOPT_RETURNTRANSFER => \false, \CURLOPT_HEADER => \false, \CURLOPT_CONNECTTIMEOUT => 300]; |
|
213 | - if (\defined('CURLOPT_PROTOCOLS')) { |
|
214 | - $conf[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTP | \CURLPROTO_HTTPS; |
|
215 | - } |
|
216 | - $version = $easy->request->getProtocolVersion(); |
|
217 | - if ('2' === $version || '2.0' === $version) { |
|
218 | - $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0; |
|
219 | - } elseif ('1.1' === $version) { |
|
220 | - $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1; |
|
221 | - } else { |
|
222 | - $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0; |
|
223 | - } |
|
224 | - return $conf; |
|
225 | - } |
|
226 | - private function applyMethod(EasyHandle $easy, array &$conf) : void |
|
227 | - { |
|
228 | - $body = $easy->request->getBody(); |
|
229 | - $size = $body->getSize(); |
|
230 | - if ($size === null || $size > 0) { |
|
231 | - $this->applyBody($easy->request, $easy->options, $conf); |
|
232 | - return; |
|
233 | - } |
|
234 | - $method = $easy->request->getMethod(); |
|
235 | - if ($method === 'PUT' || $method === 'POST') { |
|
236 | - // See https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2 |
|
237 | - if (!$easy->request->hasHeader('Content-Length')) { |
|
238 | - $conf[\CURLOPT_HTTPHEADER][] = 'Content-Length: 0'; |
|
239 | - } |
|
240 | - } elseif ($method === 'HEAD') { |
|
241 | - $conf[\CURLOPT_NOBODY] = \true; |
|
242 | - unset($conf[\CURLOPT_WRITEFUNCTION], $conf[\CURLOPT_READFUNCTION], $conf[\CURLOPT_FILE], $conf[\CURLOPT_INFILE]); |
|
243 | - } |
|
244 | - } |
|
245 | - private function applyBody(RequestInterface $request, array $options, array &$conf) : void |
|
246 | - { |
|
247 | - $size = $request->hasHeader('Content-Length') ? (int) $request->getHeaderLine('Content-Length') : null; |
|
248 | - // Send the body as a string if the size is less than 1MB OR if the |
|
249 | - // [curl][body_as_string] request value is set. |
|
250 | - if ($size !== null && $size < 1000000 || !empty($options['_body_as_string'])) { |
|
251 | - $conf[\CURLOPT_POSTFIELDS] = (string) $request->getBody(); |
|
252 | - // Don't duplicate the Content-Length header |
|
253 | - $this->removeHeader('Content-Length', $conf); |
|
254 | - $this->removeHeader('Transfer-Encoding', $conf); |
|
255 | - } else { |
|
256 | - $conf[\CURLOPT_UPLOAD] = \true; |
|
257 | - if ($size !== null) { |
|
258 | - $conf[\CURLOPT_INFILESIZE] = $size; |
|
259 | - $this->removeHeader('Content-Length', $conf); |
|
260 | - } |
|
261 | - $body = $request->getBody(); |
|
262 | - if ($body->isSeekable()) { |
|
263 | - $body->rewind(); |
|
264 | - } |
|
265 | - $conf[\CURLOPT_READFUNCTION] = static function ($ch, $fd, $length) use($body) { |
|
266 | - return $body->read($length); |
|
267 | - }; |
|
268 | - } |
|
269 | - // If the Expect header is not present, prevent curl from adding it |
|
270 | - if (!$request->hasHeader('Expect')) { |
|
271 | - $conf[\CURLOPT_HTTPHEADER][] = 'Expect:'; |
|
272 | - } |
|
273 | - // cURL sometimes adds a content-type by default. Prevent this. |
|
274 | - if (!$request->hasHeader('Content-Type')) { |
|
275 | - $conf[\CURLOPT_HTTPHEADER][] = 'Content-Type:'; |
|
276 | - } |
|
277 | - } |
|
278 | - private function applyHeaders(EasyHandle $easy, array &$conf) : void |
|
279 | - { |
|
280 | - foreach ($conf['_headers'] as $name => $values) { |
|
281 | - foreach ($values as $value) { |
|
282 | - $value = (string) $value; |
|
283 | - if ($value === '') { |
|
284 | - // cURL requires a special format for empty headers. |
|
285 | - // See https://github.com/guzzle/guzzle/issues/1882 for more details. |
|
286 | - $conf[\CURLOPT_HTTPHEADER][] = "{$name};"; |
|
287 | - } else { |
|
288 | - $conf[\CURLOPT_HTTPHEADER][] = "{$name}: {$value}"; |
|
289 | - } |
|
290 | - } |
|
291 | - } |
|
292 | - // Remove the Accept header if one was not set |
|
293 | - if (!$easy->request->hasHeader('Accept')) { |
|
294 | - $conf[\CURLOPT_HTTPHEADER][] = 'Accept:'; |
|
295 | - } |
|
296 | - } |
|
297 | - /** |
|
298 | - * Remove a header from the options array. |
|
299 | - * |
|
300 | - * @param string $name Case-insensitive header to remove |
|
301 | - * @param array $options Array of options to modify |
|
302 | - */ |
|
303 | - private function removeHeader(string $name, array &$options) : void |
|
304 | - { |
|
305 | - foreach (\array_keys($options['_headers']) as $key) { |
|
306 | - if (!\strcasecmp($key, $name)) { |
|
307 | - unset($options['_headers'][$key]); |
|
308 | - return; |
|
309 | - } |
|
310 | - } |
|
311 | - } |
|
312 | - private function applyHandlerOptions(EasyHandle $easy, array &$conf) : void |
|
313 | - { |
|
314 | - $options = $easy->options; |
|
315 | - if (isset($options['verify'])) { |
|
316 | - if ($options['verify'] === \false) { |
|
317 | - unset($conf[\CURLOPT_CAINFO]); |
|
318 | - $conf[\CURLOPT_SSL_VERIFYHOST] = 0; |
|
319 | - $conf[\CURLOPT_SSL_VERIFYPEER] = \false; |
|
320 | - } else { |
|
321 | - $conf[\CURLOPT_SSL_VERIFYHOST] = 2; |
|
322 | - $conf[\CURLOPT_SSL_VERIFYPEER] = \true; |
|
323 | - if (\is_string($options['verify'])) { |
|
324 | - // Throw an error if the file/folder/link path is not valid or doesn't exist. |
|
325 | - if (!\file_exists($options['verify'])) { |
|
326 | - throw new \InvalidArgumentException("SSL CA bundle not found: {$options['verify']}"); |
|
327 | - } |
|
328 | - // If it's a directory or a link to a directory use CURLOPT_CAPATH. |
|
329 | - // If not, it's probably a file, or a link to a file, so use CURLOPT_CAINFO. |
|
330 | - if (\is_dir($options['verify']) || \is_link($options['verify']) === \true && ($verifyLink = \readlink($options['verify'])) !== \false && \is_dir($verifyLink)) { |
|
331 | - $conf[\CURLOPT_CAPATH] = $options['verify']; |
|
332 | - } else { |
|
333 | - $conf[\CURLOPT_CAINFO] = $options['verify']; |
|
334 | - } |
|
335 | - } |
|
336 | - } |
|
337 | - } |
|
338 | - if (!isset($options['curl'][\CURLOPT_ENCODING]) && !empty($options['decode_content'])) { |
|
339 | - $accept = $easy->request->getHeaderLine('Accept-Encoding'); |
|
340 | - if ($accept) { |
|
341 | - $conf[\CURLOPT_ENCODING] = $accept; |
|
342 | - } else { |
|
343 | - // The empty string enables all available decoders and implicitly |
|
344 | - // sets a matching 'Accept-Encoding' header. |
|
345 | - $conf[\CURLOPT_ENCODING] = ''; |
|
346 | - // But as the user did not specify any encoding preference, |
|
347 | - // let's leave it up to server by preventing curl from sending |
|
348 | - // the header, which will be interpreted as 'Accept-Encoding: *'. |
|
349 | - // https://www.rfc-editor.org/rfc/rfc9110#field.accept-encoding |
|
350 | - $conf[\CURLOPT_HTTPHEADER][] = 'Accept-Encoding:'; |
|
351 | - } |
|
352 | - } |
|
353 | - if (!isset($options['sink'])) { |
|
354 | - // Use a default temp stream if no sink was set. |
|
355 | - $options['sink'] = \OCA\FullTextSearch_Elasticsearch\Vendor\GuzzleHttp\Psr7\Utils::tryFopen('php://temp', 'w+'); |
|
356 | - } |
|
357 | - $sink = $options['sink']; |
|
358 | - if (!\is_string($sink)) { |
|
359 | - $sink = \OCA\FullTextSearch_Elasticsearch\Vendor\GuzzleHttp\Psr7\Utils::streamFor($sink); |
|
360 | - } elseif (!\is_dir(\dirname($sink))) { |
|
361 | - // Ensure that the directory exists before failing in curl. |
|
362 | - throw new \RuntimeException(\sprintf('Directory %s does not exist for sink value of %s', \dirname($sink), $sink)); |
|
363 | - } else { |
|
364 | - $sink = new LazyOpenStream($sink, 'w+'); |
|
365 | - } |
|
366 | - $easy->sink = $sink; |
|
367 | - $conf[\CURLOPT_WRITEFUNCTION] = static function ($ch, $write) use($sink) : int { |
|
368 | - return $sink->write($write); |
|
369 | - }; |
|
370 | - $timeoutRequiresNoSignal = \false; |
|
371 | - if (isset($options['timeout'])) { |
|
372 | - $timeoutRequiresNoSignal |= $options['timeout'] < 1; |
|
373 | - $conf[\CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000; |
|
374 | - } |
|
375 | - // CURL default value is CURL_IPRESOLVE_WHATEVER |
|
376 | - if (isset($options['force_ip_resolve'])) { |
|
377 | - if ('v4' === $options['force_ip_resolve']) { |
|
378 | - $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V4; |
|
379 | - } elseif ('v6' === $options['force_ip_resolve']) { |
|
380 | - $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V6; |
|
381 | - } |
|
382 | - } |
|
383 | - if (isset($options['connect_timeout'])) { |
|
384 | - $timeoutRequiresNoSignal |= $options['connect_timeout'] < 1; |
|
385 | - $conf[\CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000; |
|
386 | - } |
|
387 | - if ($timeoutRequiresNoSignal && \strtoupper(\substr(\PHP_OS, 0, 3)) !== 'WIN') { |
|
388 | - $conf[\CURLOPT_NOSIGNAL] = \true; |
|
389 | - } |
|
390 | - if (isset($options['proxy'])) { |
|
391 | - if (!\is_array($options['proxy'])) { |
|
392 | - $conf[\CURLOPT_PROXY] = $options['proxy']; |
|
393 | - } else { |
|
394 | - $scheme = $easy->request->getUri()->getScheme(); |
|
395 | - if (isset($options['proxy'][$scheme])) { |
|
396 | - $host = $easy->request->getUri()->getHost(); |
|
397 | - if (isset($options['proxy']['no']) && Utils::isHostInNoProxy($host, $options['proxy']['no'])) { |
|
398 | - unset($conf[\CURLOPT_PROXY]); |
|
399 | - } else { |
|
400 | - $conf[\CURLOPT_PROXY] = $options['proxy'][$scheme]; |
|
401 | - } |
|
402 | - } |
|
403 | - } |
|
404 | - } |
|
405 | - if (isset($options['crypto_method'])) { |
|
406 | - $protocolVersion = $easy->request->getProtocolVersion(); |
|
407 | - // If HTTP/2, upgrade TLS 1.0 and 1.1 to 1.2 |
|
408 | - if ('2' === $protocolVersion || '2.0' === $protocolVersion) { |
|
409 | - if (\STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT === $options['crypto_method'] || \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT === $options['crypto_method'] || \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT === $options['crypto_method']) { |
|
410 | - $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_2; |
|
411 | - } elseif (\defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT === $options['crypto_method']) { |
|
412 | - if (!self::supportsTls13()) { |
|
413 | - throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.3 not supported by your version of cURL'); |
|
414 | - } |
|
415 | - $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_3; |
|
416 | - } else { |
|
417 | - throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided'); |
|
418 | - } |
|
419 | - } elseif (\STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT === $options['crypto_method']) { |
|
420 | - $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_0; |
|
421 | - } elseif (\STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT === $options['crypto_method']) { |
|
422 | - $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_1; |
|
423 | - } elseif (\STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT === $options['crypto_method']) { |
|
424 | - if (!self::supportsTls12()) { |
|
425 | - throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.2 not supported by your version of cURL'); |
|
426 | - } |
|
427 | - $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_2; |
|
428 | - } elseif (\defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT === $options['crypto_method']) { |
|
429 | - if (!self::supportsTls13()) { |
|
430 | - throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.3 not supported by your version of cURL'); |
|
431 | - } |
|
432 | - $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_3; |
|
433 | - } else { |
|
434 | - throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided'); |
|
435 | - } |
|
436 | - } |
|
437 | - if (isset($options['cert'])) { |
|
438 | - $cert = $options['cert']; |
|
439 | - if (\is_array($cert)) { |
|
440 | - $conf[\CURLOPT_SSLCERTPASSWD] = $cert[1]; |
|
441 | - $cert = $cert[0]; |
|
442 | - } |
|
443 | - if (!\file_exists($cert)) { |
|
444 | - throw new \InvalidArgumentException("SSL certificate not found: {$cert}"); |
|
445 | - } |
|
446 | - // OpenSSL (versions 0.9.3 and later) also support "P12" for PKCS#12-encoded files. |
|
447 | - // see https://curl.se/libcurl/c/CURLOPT_SSLCERTTYPE.html |
|
448 | - $ext = \pathinfo($cert, \PATHINFO_EXTENSION); |
|
449 | - if (\preg_match('#^(der|p12)$#i', $ext)) { |
|
450 | - $conf[\CURLOPT_SSLCERTTYPE] = \strtoupper($ext); |
|
451 | - } |
|
452 | - $conf[\CURLOPT_SSLCERT] = $cert; |
|
453 | - } |
|
454 | - if (isset($options['ssl_key'])) { |
|
455 | - if (\is_array($options['ssl_key'])) { |
|
456 | - if (\count($options['ssl_key']) === 2) { |
|
457 | - [$sslKey, $conf[\CURLOPT_SSLKEYPASSWD]] = $options['ssl_key']; |
|
458 | - } else { |
|
459 | - [$sslKey] = $options['ssl_key']; |
|
460 | - } |
|
461 | - } |
|
462 | - $sslKey = $sslKey ?? $options['ssl_key']; |
|
463 | - if (!\file_exists($sslKey)) { |
|
464 | - throw new \InvalidArgumentException("SSL private key not found: {$sslKey}"); |
|
465 | - } |
|
466 | - $conf[\CURLOPT_SSLKEY] = $sslKey; |
|
467 | - } |
|
468 | - if (isset($options['progress'])) { |
|
469 | - $progress = $options['progress']; |
|
470 | - if (!\is_callable($progress)) { |
|
471 | - throw new \InvalidArgumentException('progress client option must be callable'); |
|
472 | - } |
|
473 | - $conf[\CURLOPT_NOPROGRESS] = \false; |
|
474 | - $conf[\CURLOPT_PROGRESSFUNCTION] = static function ($resource, int $downloadSize, int $downloaded, int $uploadSize, int $uploaded) use($progress) { |
|
475 | - $progress($downloadSize, $downloaded, $uploadSize, $uploaded); |
|
476 | - }; |
|
477 | - } |
|
478 | - if (!empty($options['debug'])) { |
|
479 | - $conf[\CURLOPT_STDERR] = Utils::debugResource($options['debug']); |
|
480 | - $conf[\CURLOPT_VERBOSE] = \true; |
|
481 | - } |
|
482 | - } |
|
483 | - /** |
|
484 | - * This function ensures that a response was set on a transaction. If one |
|
485 | - * was not set, then the request is retried if possible. This error |
|
486 | - * typically means you are sending a payload, curl encountered a |
|
487 | - * "Connection died, retrying a fresh connect" error, tried to rewind the |
|
488 | - * stream, and then encountered a "necessary data rewind wasn't possible" |
|
489 | - * error, causing the request to be sent through curl_multi_info_read() |
|
490 | - * without an error status. |
|
491 | - * |
|
492 | - * @param callable(RequestInterface, array): PromiseInterface $handler |
|
493 | - */ |
|
494 | - private static function retryFailedRewind(callable $handler, EasyHandle $easy, array $ctx) : PromiseInterface |
|
495 | - { |
|
496 | - try { |
|
497 | - // Only rewind if the body has been read from. |
|
498 | - $body = $easy->request->getBody(); |
|
499 | - if ($body->tell() > 0) { |
|
500 | - $body->rewind(); |
|
501 | - } |
|
502 | - } catch (\RuntimeException $e) { |
|
503 | - $ctx['error'] = 'The connection unexpectedly failed without ' . 'providing an error. The request would have been retried, ' . 'but attempting to rewind the request body failed. ' . 'Exception: ' . $e; |
|
504 | - return self::createRejection($easy, $ctx); |
|
505 | - } |
|
506 | - // Retry no more than 3 times before giving up. |
|
507 | - if (!isset($easy->options['_curl_retries'])) { |
|
508 | - $easy->options['_curl_retries'] = 1; |
|
509 | - } elseif ($easy->options['_curl_retries'] == 2) { |
|
510 | - $ctx['error'] = 'The cURL request was retried 3 times ' . 'and did not succeed. The most likely reason for the failure ' . 'is that cURL was unable to rewind the body of the request ' . 'and subsequent retries resulted in the same error. Turn on ' . 'the debug option to see what went wrong. See ' . 'https://bugs.php.net/bug.php?id=47204 for more information.'; |
|
511 | - return self::createRejection($easy, $ctx); |
|
512 | - } else { |
|
513 | - ++$easy->options['_curl_retries']; |
|
514 | - } |
|
515 | - return $handler($easy->request, $easy->options); |
|
516 | - } |
|
517 | - private function createHeaderFn(EasyHandle $easy) : callable |
|
518 | - { |
|
519 | - if (isset($easy->options['on_headers'])) { |
|
520 | - $onHeaders = $easy->options['on_headers']; |
|
521 | - if (!\is_callable($onHeaders)) { |
|
522 | - throw new \InvalidArgumentException('on_headers must be callable'); |
|
523 | - } |
|
524 | - } else { |
|
525 | - $onHeaders = null; |
|
526 | - } |
|
527 | - return static function ($ch, $h) use($onHeaders, $easy, &$startingResponse) { |
|
528 | - $value = \trim($h); |
|
529 | - if ($value === '') { |
|
530 | - $startingResponse = \true; |
|
531 | - try { |
|
532 | - $easy->createResponse(); |
|
533 | - } catch (\Exception $e) { |
|
534 | - $easy->createResponseException = $e; |
|
535 | - return -1; |
|
536 | - } |
|
537 | - if ($onHeaders !== null) { |
|
538 | - try { |
|
539 | - $onHeaders($easy->response); |
|
540 | - } catch (\Exception $e) { |
|
541 | - // Associate the exception with the handle and trigger |
|
542 | - // a curl header write error by returning 0. |
|
543 | - $easy->onHeadersException = $e; |
|
544 | - return -1; |
|
545 | - } |
|
546 | - } |
|
547 | - } elseif ($startingResponse) { |
|
548 | - $startingResponse = \false; |
|
549 | - $easy->headers = [$value]; |
|
550 | - } else { |
|
551 | - $easy->headers[] = $value; |
|
552 | - } |
|
553 | - return \strlen($h); |
|
554 | - }; |
|
555 | - } |
|
556 | - public function __destruct() |
|
557 | - { |
|
558 | - foreach ($this->handles as $id => $handle) { |
|
559 | - \curl_close($handle); |
|
560 | - unset($this->handles[$id]); |
|
561 | - } |
|
562 | - } |
|
22 | + public const CURL_VERSION_STR = 'curl_version'; |
|
23 | + /** |
|
24 | + * @deprecated |
|
25 | + */ |
|
26 | + public const LOW_CURL_VERSION_NUMBER = '7.21.2'; |
|
27 | + /** |
|
28 | + * @var resource[]|\CurlHandle[] |
|
29 | + */ |
|
30 | + private $handles = []; |
|
31 | + /** |
|
32 | + * @var int Total number of idle handles to keep in cache |
|
33 | + */ |
|
34 | + private $maxHandles; |
|
35 | + /** |
|
36 | + * @param int $maxHandles Maximum number of idle handles. |
|
37 | + */ |
|
38 | + public function __construct(int $maxHandles) |
|
39 | + { |
|
40 | + $this->maxHandles = $maxHandles; |
|
41 | + } |
|
42 | + public function create(RequestInterface $request, array $options) : EasyHandle |
|
43 | + { |
|
44 | + $protocolVersion = $request->getProtocolVersion(); |
|
45 | + if ('2' === $protocolVersion || '2.0' === $protocolVersion) { |
|
46 | + if (!self::supportsHttp2()) { |
|
47 | + throw new ConnectException('HTTP/2 is supported by the cURL handler, however libcurl is built without HTTP/2 support.', $request); |
|
48 | + } |
|
49 | + } elseif ('1.0' !== $protocolVersion && '1.1' !== $protocolVersion) { |
|
50 | + throw new ConnectException(\sprintf('HTTP/%s is not supported by the cURL handler.', $protocolVersion), $request); |
|
51 | + } |
|
52 | + if (isset($options['curl']['body_as_string'])) { |
|
53 | + $options['_body_as_string'] = $options['curl']['body_as_string']; |
|
54 | + unset($options['curl']['body_as_string']); |
|
55 | + } |
|
56 | + $easy = new EasyHandle(); |
|
57 | + $easy->request = $request; |
|
58 | + $easy->options = $options; |
|
59 | + $conf = $this->getDefaultConf($easy); |
|
60 | + $this->applyMethod($easy, $conf); |
|
61 | + $this->applyHandlerOptions($easy, $conf); |
|
62 | + $this->applyHeaders($easy, $conf); |
|
63 | + unset($conf['_headers']); |
|
64 | + // Add handler options from the request configuration options |
|
65 | + if (isset($options['curl'])) { |
|
66 | + $conf = \array_replace($conf, $options['curl']); |
|
67 | + } |
|
68 | + $conf[\CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy); |
|
69 | + $easy->handle = $this->handles ? \array_pop($this->handles) : \curl_init(); |
|
70 | + \curl_setopt_array($easy->handle, $conf); |
|
71 | + return $easy; |
|
72 | + } |
|
73 | + private static function supportsHttp2() : bool |
|
74 | + { |
|
75 | + static $supportsHttp2 = null; |
|
76 | + if (null === $supportsHttp2) { |
|
77 | + $supportsHttp2 = self::supportsTls12() && \defined('CURL_VERSION_HTTP2') && \CURL_VERSION_HTTP2 & \curl_version()['features']; |
|
78 | + } |
|
79 | + return $supportsHttp2; |
|
80 | + } |
|
81 | + private static function supportsTls12() : bool |
|
82 | + { |
|
83 | + static $supportsTls12 = null; |
|
84 | + if (null === $supportsTls12) { |
|
85 | + $supportsTls12 = \CURL_SSLVERSION_TLSv1_2 & \curl_version()['features']; |
|
86 | + } |
|
87 | + return $supportsTls12; |
|
88 | + } |
|
89 | + private static function supportsTls13() : bool |
|
90 | + { |
|
91 | + static $supportsTls13 = null; |
|
92 | + if (null === $supportsTls13) { |
|
93 | + $supportsTls13 = \defined('CURL_SSLVERSION_TLSv1_3') && \CURL_SSLVERSION_TLSv1_3 & \curl_version()['features']; |
|
94 | + } |
|
95 | + return $supportsTls13; |
|
96 | + } |
|
97 | + public function release(EasyHandle $easy) : void |
|
98 | + { |
|
99 | + $resource = $easy->handle; |
|
100 | + unset($easy->handle); |
|
101 | + if (\count($this->handles) >= $this->maxHandles) { |
|
102 | + \curl_close($resource); |
|
103 | + } else { |
|
104 | + // Remove all callback functions as they can hold onto references |
|
105 | + // and are not cleaned up by curl_reset. Using curl_setopt_array |
|
106 | + // does not work for some reason, so removing each one |
|
107 | + // individually. |
|
108 | + \curl_setopt($resource, \CURLOPT_HEADERFUNCTION, null); |
|
109 | + \curl_setopt($resource, \CURLOPT_READFUNCTION, null); |
|
110 | + \curl_setopt($resource, \CURLOPT_WRITEFUNCTION, null); |
|
111 | + \curl_setopt($resource, \CURLOPT_PROGRESSFUNCTION, null); |
|
112 | + \curl_reset($resource); |
|
113 | + $this->handles[] = $resource; |
|
114 | + } |
|
115 | + } |
|
116 | + /** |
|
117 | + * Completes a cURL transaction, either returning a response promise or a |
|
118 | + * rejected promise. |
|
119 | + * |
|
120 | + * @param callable(RequestInterface, array): PromiseInterface $handler |
|
121 | + * @param CurlFactoryInterface $factory Dictates how the handle is released |
|
122 | + */ |
|
123 | + public static function finish(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory) : PromiseInterface |
|
124 | + { |
|
125 | + if (isset($easy->options['on_stats'])) { |
|
126 | + self::invokeStats($easy); |
|
127 | + } |
|
128 | + if (!$easy->response || $easy->errno) { |
|
129 | + return self::finishError($handler, $easy, $factory); |
|
130 | + } |
|
131 | + // Return the response if it is present and there is no error. |
|
132 | + $factory->release($easy); |
|
133 | + // Rewind the body of the response if possible. |
|
134 | + $body = $easy->response->getBody(); |
|
135 | + if ($body->isSeekable()) { |
|
136 | + $body->rewind(); |
|
137 | + } |
|
138 | + return new FulfilledPromise($easy->response); |
|
139 | + } |
|
140 | + private static function invokeStats(EasyHandle $easy) : void |
|
141 | + { |
|
142 | + $curlStats = \curl_getinfo($easy->handle); |
|
143 | + $curlStats['appconnect_time'] = \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME); |
|
144 | + $stats = new TransferStats($easy->request, $easy->response, $curlStats['total_time'], $easy->errno, $curlStats); |
|
145 | + $easy->options['on_stats']($stats); |
|
146 | + } |
|
147 | + /** |
|
148 | + * @param callable(RequestInterface, array): PromiseInterface $handler |
|
149 | + */ |
|
150 | + private static function finishError(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory) : PromiseInterface |
|
151 | + { |
|
152 | + // Get error information and release the handle to the factory. |
|
153 | + $ctx = ['errno' => $easy->errno, 'error' => \curl_error($easy->handle), 'appconnect_time' => \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME)] + \curl_getinfo($easy->handle); |
|
154 | + $ctx[self::CURL_VERSION_STR] = self::getCurlVersion(); |
|
155 | + $factory->release($easy); |
|
156 | + // Retry when nothing is present or when curl failed to rewind. |
|
157 | + if (empty($easy->options['_err_message']) && (!$easy->errno || $easy->errno == 65)) { |
|
158 | + return self::retryFailedRewind($handler, $easy, $ctx); |
|
159 | + } |
|
160 | + return self::createRejection($easy, $ctx); |
|
161 | + } |
|
162 | + private static function getCurlVersion() : string |
|
163 | + { |
|
164 | + static $curlVersion = null; |
|
165 | + if (null === $curlVersion) { |
|
166 | + $curlVersion = \curl_version()['version']; |
|
167 | + } |
|
168 | + return $curlVersion; |
|
169 | + } |
|
170 | + private static function createRejection(EasyHandle $easy, array $ctx) : PromiseInterface |
|
171 | + { |
|
172 | + static $connectionErrors = [\CURLE_OPERATION_TIMEOUTED => \true, \CURLE_COULDNT_RESOLVE_HOST => \true, \CURLE_COULDNT_CONNECT => \true, \CURLE_SSL_CONNECT_ERROR => \true, \CURLE_GOT_NOTHING => \true]; |
|
173 | + if ($easy->createResponseException) { |
|
174 | + return P\Create::rejectionFor(new RequestException('An error was encountered while creating the response', $easy->request, $easy->response, $easy->createResponseException, $ctx)); |
|
175 | + } |
|
176 | + // If an exception was encountered during the onHeaders event, then |
|
177 | + // return a rejected promise that wraps that exception. |
|
178 | + if ($easy->onHeadersException) { |
|
179 | + return P\Create::rejectionFor(new RequestException('An error was encountered during the on_headers event', $easy->request, $easy->response, $easy->onHeadersException, $ctx)); |
|
180 | + } |
|
181 | + $uri = $easy->request->getUri(); |
|
182 | + $sanitizedError = self::sanitizeCurlError($ctx['error'] ?? '', $uri); |
|
183 | + $message = \sprintf('cURL error %s: %s (%s)', $ctx['errno'], $sanitizedError, 'see https://curl.haxx.se/libcurl/c/libcurl-errors.html'); |
|
184 | + if ('' !== $sanitizedError) { |
|
185 | + $redactedUriString = \OCA\FullTextSearch_Elasticsearch\Vendor\GuzzleHttp\Psr7\Utils::redactUserInfo($uri)->__toString(); |
|
186 | + if ($redactedUriString !== '' && \false === \strpos($sanitizedError, $redactedUriString)) { |
|
187 | + $message .= \sprintf(' for %s', $redactedUriString); |
|
188 | + } |
|
189 | + } |
|
190 | + // Create a connection exception if it was a specific error code. |
|
191 | + $error = isset($connectionErrors[$easy->errno]) ? new ConnectException($message, $easy->request, null, $ctx) : new RequestException($message, $easy->request, $easy->response, null, $ctx); |
|
192 | + return P\Create::rejectionFor($error); |
|
193 | + } |
|
194 | + private static function sanitizeCurlError(string $error, UriInterface $uri) : string |
|
195 | + { |
|
196 | + if ('' === $error) { |
|
197 | + return $error; |
|
198 | + } |
|
199 | + $baseUri = $uri->withQuery('')->withFragment(''); |
|
200 | + $baseUriString = $baseUri->__toString(); |
|
201 | + if ('' === $baseUriString) { |
|
202 | + return $error; |
|
203 | + } |
|
204 | + $redactedUriString = \OCA\FullTextSearch_Elasticsearch\Vendor\GuzzleHttp\Psr7\Utils::redactUserInfo($baseUri)->__toString(); |
|
205 | + return \str_replace($baseUriString, $redactedUriString, $error); |
|
206 | + } |
|
207 | + /** |
|
208 | + * @return array<int|string, mixed> |
|
209 | + */ |
|
210 | + private function getDefaultConf(EasyHandle $easy) : array |
|
211 | + { |
|
212 | + $conf = ['_headers' => $easy->request->getHeaders(), \CURLOPT_CUSTOMREQUEST => $easy->request->getMethod(), \CURLOPT_URL => (string) $easy->request->getUri()->withFragment(''), \CURLOPT_RETURNTRANSFER => \false, \CURLOPT_HEADER => \false, \CURLOPT_CONNECTTIMEOUT => 300]; |
|
213 | + if (\defined('CURLOPT_PROTOCOLS')) { |
|
214 | + $conf[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTP | \CURLPROTO_HTTPS; |
|
215 | + } |
|
216 | + $version = $easy->request->getProtocolVersion(); |
|
217 | + if ('2' === $version || '2.0' === $version) { |
|
218 | + $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0; |
|
219 | + } elseif ('1.1' === $version) { |
|
220 | + $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1; |
|
221 | + } else { |
|
222 | + $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0; |
|
223 | + } |
|
224 | + return $conf; |
|
225 | + } |
|
226 | + private function applyMethod(EasyHandle $easy, array &$conf) : void |
|
227 | + { |
|
228 | + $body = $easy->request->getBody(); |
|
229 | + $size = $body->getSize(); |
|
230 | + if ($size === null || $size > 0) { |
|
231 | + $this->applyBody($easy->request, $easy->options, $conf); |
|
232 | + return; |
|
233 | + } |
|
234 | + $method = $easy->request->getMethod(); |
|
235 | + if ($method === 'PUT' || $method === 'POST') { |
|
236 | + // See https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2 |
|
237 | + if (!$easy->request->hasHeader('Content-Length')) { |
|
238 | + $conf[\CURLOPT_HTTPHEADER][] = 'Content-Length: 0'; |
|
239 | + } |
|
240 | + } elseif ($method === 'HEAD') { |
|
241 | + $conf[\CURLOPT_NOBODY] = \true; |
|
242 | + unset($conf[\CURLOPT_WRITEFUNCTION], $conf[\CURLOPT_READFUNCTION], $conf[\CURLOPT_FILE], $conf[\CURLOPT_INFILE]); |
|
243 | + } |
|
244 | + } |
|
245 | + private function applyBody(RequestInterface $request, array $options, array &$conf) : void |
|
246 | + { |
|
247 | + $size = $request->hasHeader('Content-Length') ? (int) $request->getHeaderLine('Content-Length') : null; |
|
248 | + // Send the body as a string if the size is less than 1MB OR if the |
|
249 | + // [curl][body_as_string] request value is set. |
|
250 | + if ($size !== null && $size < 1000000 || !empty($options['_body_as_string'])) { |
|
251 | + $conf[\CURLOPT_POSTFIELDS] = (string) $request->getBody(); |
|
252 | + // Don't duplicate the Content-Length header |
|
253 | + $this->removeHeader('Content-Length', $conf); |
|
254 | + $this->removeHeader('Transfer-Encoding', $conf); |
|
255 | + } else { |
|
256 | + $conf[\CURLOPT_UPLOAD] = \true; |
|
257 | + if ($size !== null) { |
|
258 | + $conf[\CURLOPT_INFILESIZE] = $size; |
|
259 | + $this->removeHeader('Content-Length', $conf); |
|
260 | + } |
|
261 | + $body = $request->getBody(); |
|
262 | + if ($body->isSeekable()) { |
|
263 | + $body->rewind(); |
|
264 | + } |
|
265 | + $conf[\CURLOPT_READFUNCTION] = static function ($ch, $fd, $length) use($body) { |
|
266 | + return $body->read($length); |
|
267 | + }; |
|
268 | + } |
|
269 | + // If the Expect header is not present, prevent curl from adding it |
|
270 | + if (!$request->hasHeader('Expect')) { |
|
271 | + $conf[\CURLOPT_HTTPHEADER][] = 'Expect:'; |
|
272 | + } |
|
273 | + // cURL sometimes adds a content-type by default. Prevent this. |
|
274 | + if (!$request->hasHeader('Content-Type')) { |
|
275 | + $conf[\CURLOPT_HTTPHEADER][] = 'Content-Type:'; |
|
276 | + } |
|
277 | + } |
|
278 | + private function applyHeaders(EasyHandle $easy, array &$conf) : void |
|
279 | + { |
|
280 | + foreach ($conf['_headers'] as $name => $values) { |
|
281 | + foreach ($values as $value) { |
|
282 | + $value = (string) $value; |
|
283 | + if ($value === '') { |
|
284 | + // cURL requires a special format for empty headers. |
|
285 | + // See https://github.com/guzzle/guzzle/issues/1882 for more details. |
|
286 | + $conf[\CURLOPT_HTTPHEADER][] = "{$name};"; |
|
287 | + } else { |
|
288 | + $conf[\CURLOPT_HTTPHEADER][] = "{$name}: {$value}"; |
|
289 | + } |
|
290 | + } |
|
291 | + } |
|
292 | + // Remove the Accept header if one was not set |
|
293 | + if (!$easy->request->hasHeader('Accept')) { |
|
294 | + $conf[\CURLOPT_HTTPHEADER][] = 'Accept:'; |
|
295 | + } |
|
296 | + } |
|
297 | + /** |
|
298 | + * Remove a header from the options array. |
|
299 | + * |
|
300 | + * @param string $name Case-insensitive header to remove |
|
301 | + * @param array $options Array of options to modify |
|
302 | + */ |
|
303 | + private function removeHeader(string $name, array &$options) : void |
|
304 | + { |
|
305 | + foreach (\array_keys($options['_headers']) as $key) { |
|
306 | + if (!\strcasecmp($key, $name)) { |
|
307 | + unset($options['_headers'][$key]); |
|
308 | + return; |
|
309 | + } |
|
310 | + } |
|
311 | + } |
|
312 | + private function applyHandlerOptions(EasyHandle $easy, array &$conf) : void |
|
313 | + { |
|
314 | + $options = $easy->options; |
|
315 | + if (isset($options['verify'])) { |
|
316 | + if ($options['verify'] === \false) { |
|
317 | + unset($conf[\CURLOPT_CAINFO]); |
|
318 | + $conf[\CURLOPT_SSL_VERIFYHOST] = 0; |
|
319 | + $conf[\CURLOPT_SSL_VERIFYPEER] = \false; |
|
320 | + } else { |
|
321 | + $conf[\CURLOPT_SSL_VERIFYHOST] = 2; |
|
322 | + $conf[\CURLOPT_SSL_VERIFYPEER] = \true; |
|
323 | + if (\is_string($options['verify'])) { |
|
324 | + // Throw an error if the file/folder/link path is not valid or doesn't exist. |
|
325 | + if (!\file_exists($options['verify'])) { |
|
326 | + throw new \InvalidArgumentException("SSL CA bundle not found: {$options['verify']}"); |
|
327 | + } |
|
328 | + // If it's a directory or a link to a directory use CURLOPT_CAPATH. |
|
329 | + // If not, it's probably a file, or a link to a file, so use CURLOPT_CAINFO. |
|
330 | + if (\is_dir($options['verify']) || \is_link($options['verify']) === \true && ($verifyLink = \readlink($options['verify'])) !== \false && \is_dir($verifyLink)) { |
|
331 | + $conf[\CURLOPT_CAPATH] = $options['verify']; |
|
332 | + } else { |
|
333 | + $conf[\CURLOPT_CAINFO] = $options['verify']; |
|
334 | + } |
|
335 | + } |
|
336 | + } |
|
337 | + } |
|
338 | + if (!isset($options['curl'][\CURLOPT_ENCODING]) && !empty($options['decode_content'])) { |
|
339 | + $accept = $easy->request->getHeaderLine('Accept-Encoding'); |
|
340 | + if ($accept) { |
|
341 | + $conf[\CURLOPT_ENCODING] = $accept; |
|
342 | + } else { |
|
343 | + // The empty string enables all available decoders and implicitly |
|
344 | + // sets a matching 'Accept-Encoding' header. |
|
345 | + $conf[\CURLOPT_ENCODING] = ''; |
|
346 | + // But as the user did not specify any encoding preference, |
|
347 | + // let's leave it up to server by preventing curl from sending |
|
348 | + // the header, which will be interpreted as 'Accept-Encoding: *'. |
|
349 | + // https://www.rfc-editor.org/rfc/rfc9110#field.accept-encoding |
|
350 | + $conf[\CURLOPT_HTTPHEADER][] = 'Accept-Encoding:'; |
|
351 | + } |
|
352 | + } |
|
353 | + if (!isset($options['sink'])) { |
|
354 | + // Use a default temp stream if no sink was set. |
|
355 | + $options['sink'] = \OCA\FullTextSearch_Elasticsearch\Vendor\GuzzleHttp\Psr7\Utils::tryFopen('php://temp', 'w+'); |
|
356 | + } |
|
357 | + $sink = $options['sink']; |
|
358 | + if (!\is_string($sink)) { |
|
359 | + $sink = \OCA\FullTextSearch_Elasticsearch\Vendor\GuzzleHttp\Psr7\Utils::streamFor($sink); |
|
360 | + } elseif (!\is_dir(\dirname($sink))) { |
|
361 | + // Ensure that the directory exists before failing in curl. |
|
362 | + throw new \RuntimeException(\sprintf('Directory %s does not exist for sink value of %s', \dirname($sink), $sink)); |
|
363 | + } else { |
|
364 | + $sink = new LazyOpenStream($sink, 'w+'); |
|
365 | + } |
|
366 | + $easy->sink = $sink; |
|
367 | + $conf[\CURLOPT_WRITEFUNCTION] = static function ($ch, $write) use($sink) : int { |
|
368 | + return $sink->write($write); |
|
369 | + }; |
|
370 | + $timeoutRequiresNoSignal = \false; |
|
371 | + if (isset($options['timeout'])) { |
|
372 | + $timeoutRequiresNoSignal |= $options['timeout'] < 1; |
|
373 | + $conf[\CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000; |
|
374 | + } |
|
375 | + // CURL default value is CURL_IPRESOLVE_WHATEVER |
|
376 | + if (isset($options['force_ip_resolve'])) { |
|
377 | + if ('v4' === $options['force_ip_resolve']) { |
|
378 | + $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V4; |
|
379 | + } elseif ('v6' === $options['force_ip_resolve']) { |
|
380 | + $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V6; |
|
381 | + } |
|
382 | + } |
|
383 | + if (isset($options['connect_timeout'])) { |
|
384 | + $timeoutRequiresNoSignal |= $options['connect_timeout'] < 1; |
|
385 | + $conf[\CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000; |
|
386 | + } |
|
387 | + if ($timeoutRequiresNoSignal && \strtoupper(\substr(\PHP_OS, 0, 3)) !== 'WIN') { |
|
388 | + $conf[\CURLOPT_NOSIGNAL] = \true; |
|
389 | + } |
|
390 | + if (isset($options['proxy'])) { |
|
391 | + if (!\is_array($options['proxy'])) { |
|
392 | + $conf[\CURLOPT_PROXY] = $options['proxy']; |
|
393 | + } else { |
|
394 | + $scheme = $easy->request->getUri()->getScheme(); |
|
395 | + if (isset($options['proxy'][$scheme])) { |
|
396 | + $host = $easy->request->getUri()->getHost(); |
|
397 | + if (isset($options['proxy']['no']) && Utils::isHostInNoProxy($host, $options['proxy']['no'])) { |
|
398 | + unset($conf[\CURLOPT_PROXY]); |
|
399 | + } else { |
|
400 | + $conf[\CURLOPT_PROXY] = $options['proxy'][$scheme]; |
|
401 | + } |
|
402 | + } |
|
403 | + } |
|
404 | + } |
|
405 | + if (isset($options['crypto_method'])) { |
|
406 | + $protocolVersion = $easy->request->getProtocolVersion(); |
|
407 | + // If HTTP/2, upgrade TLS 1.0 and 1.1 to 1.2 |
|
408 | + if ('2' === $protocolVersion || '2.0' === $protocolVersion) { |
|
409 | + if (\STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT === $options['crypto_method'] || \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT === $options['crypto_method'] || \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT === $options['crypto_method']) { |
|
410 | + $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_2; |
|
411 | + } elseif (\defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT === $options['crypto_method']) { |
|
412 | + if (!self::supportsTls13()) { |
|
413 | + throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.3 not supported by your version of cURL'); |
|
414 | + } |
|
415 | + $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_3; |
|
416 | + } else { |
|
417 | + throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided'); |
|
418 | + } |
|
419 | + } elseif (\STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT === $options['crypto_method']) { |
|
420 | + $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_0; |
|
421 | + } elseif (\STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT === $options['crypto_method']) { |
|
422 | + $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_1; |
|
423 | + } elseif (\STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT === $options['crypto_method']) { |
|
424 | + if (!self::supportsTls12()) { |
|
425 | + throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.2 not supported by your version of cURL'); |
|
426 | + } |
|
427 | + $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_2; |
|
428 | + } elseif (\defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT === $options['crypto_method']) { |
|
429 | + if (!self::supportsTls13()) { |
|
430 | + throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.3 not supported by your version of cURL'); |
|
431 | + } |
|
432 | + $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_3; |
|
433 | + } else { |
|
434 | + throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided'); |
|
435 | + } |
|
436 | + } |
|
437 | + if (isset($options['cert'])) { |
|
438 | + $cert = $options['cert']; |
|
439 | + if (\is_array($cert)) { |
|
440 | + $conf[\CURLOPT_SSLCERTPASSWD] = $cert[1]; |
|
441 | + $cert = $cert[0]; |
|
442 | + } |
|
443 | + if (!\file_exists($cert)) { |
|
444 | + throw new \InvalidArgumentException("SSL certificate not found: {$cert}"); |
|
445 | + } |
|
446 | + // OpenSSL (versions 0.9.3 and later) also support "P12" for PKCS#12-encoded files. |
|
447 | + // see https://curl.se/libcurl/c/CURLOPT_SSLCERTTYPE.html |
|
448 | + $ext = \pathinfo($cert, \PATHINFO_EXTENSION); |
|
449 | + if (\preg_match('#^(der|p12)$#i', $ext)) { |
|
450 | + $conf[\CURLOPT_SSLCERTTYPE] = \strtoupper($ext); |
|
451 | + } |
|
452 | + $conf[\CURLOPT_SSLCERT] = $cert; |
|
453 | + } |
|
454 | + if (isset($options['ssl_key'])) { |
|
455 | + if (\is_array($options['ssl_key'])) { |
|
456 | + if (\count($options['ssl_key']) === 2) { |
|
457 | + [$sslKey, $conf[\CURLOPT_SSLKEYPASSWD]] = $options['ssl_key']; |
|
458 | + } else { |
|
459 | + [$sslKey] = $options['ssl_key']; |
|
460 | + } |
|
461 | + } |
|
462 | + $sslKey = $sslKey ?? $options['ssl_key']; |
|
463 | + if (!\file_exists($sslKey)) { |
|
464 | + throw new \InvalidArgumentException("SSL private key not found: {$sslKey}"); |
|
465 | + } |
|
466 | + $conf[\CURLOPT_SSLKEY] = $sslKey; |
|
467 | + } |
|
468 | + if (isset($options['progress'])) { |
|
469 | + $progress = $options['progress']; |
|
470 | + if (!\is_callable($progress)) { |
|
471 | + throw new \InvalidArgumentException('progress client option must be callable'); |
|
472 | + } |
|
473 | + $conf[\CURLOPT_NOPROGRESS] = \false; |
|
474 | + $conf[\CURLOPT_PROGRESSFUNCTION] = static function ($resource, int $downloadSize, int $downloaded, int $uploadSize, int $uploaded) use($progress) { |
|
475 | + $progress($downloadSize, $downloaded, $uploadSize, $uploaded); |
|
476 | + }; |
|
477 | + } |
|
478 | + if (!empty($options['debug'])) { |
|
479 | + $conf[\CURLOPT_STDERR] = Utils::debugResource($options['debug']); |
|
480 | + $conf[\CURLOPT_VERBOSE] = \true; |
|
481 | + } |
|
482 | + } |
|
483 | + /** |
|
484 | + * This function ensures that a response was set on a transaction. If one |
|
485 | + * was not set, then the request is retried if possible. This error |
|
486 | + * typically means you are sending a payload, curl encountered a |
|
487 | + * "Connection died, retrying a fresh connect" error, tried to rewind the |
|
488 | + * stream, and then encountered a "necessary data rewind wasn't possible" |
|
489 | + * error, causing the request to be sent through curl_multi_info_read() |
|
490 | + * without an error status. |
|
491 | + * |
|
492 | + * @param callable(RequestInterface, array): PromiseInterface $handler |
|
493 | + */ |
|
494 | + private static function retryFailedRewind(callable $handler, EasyHandle $easy, array $ctx) : PromiseInterface |
|
495 | + { |
|
496 | + try { |
|
497 | + // Only rewind if the body has been read from. |
|
498 | + $body = $easy->request->getBody(); |
|
499 | + if ($body->tell() > 0) { |
|
500 | + $body->rewind(); |
|
501 | + } |
|
502 | + } catch (\RuntimeException $e) { |
|
503 | + $ctx['error'] = 'The connection unexpectedly failed without ' . 'providing an error. The request would have been retried, ' . 'but attempting to rewind the request body failed. ' . 'Exception: ' . $e; |
|
504 | + return self::createRejection($easy, $ctx); |
|
505 | + } |
|
506 | + // Retry no more than 3 times before giving up. |
|
507 | + if (!isset($easy->options['_curl_retries'])) { |
|
508 | + $easy->options['_curl_retries'] = 1; |
|
509 | + } elseif ($easy->options['_curl_retries'] == 2) { |
|
510 | + $ctx['error'] = 'The cURL request was retried 3 times ' . 'and did not succeed. The most likely reason for the failure ' . 'is that cURL was unable to rewind the body of the request ' . 'and subsequent retries resulted in the same error. Turn on ' . 'the debug option to see what went wrong. See ' . 'https://bugs.php.net/bug.php?id=47204 for more information.'; |
|
511 | + return self::createRejection($easy, $ctx); |
|
512 | + } else { |
|
513 | + ++$easy->options['_curl_retries']; |
|
514 | + } |
|
515 | + return $handler($easy->request, $easy->options); |
|
516 | + } |
|
517 | + private function createHeaderFn(EasyHandle $easy) : callable |
|
518 | + { |
|
519 | + if (isset($easy->options['on_headers'])) { |
|
520 | + $onHeaders = $easy->options['on_headers']; |
|
521 | + if (!\is_callable($onHeaders)) { |
|
522 | + throw new \InvalidArgumentException('on_headers must be callable'); |
|
523 | + } |
|
524 | + } else { |
|
525 | + $onHeaders = null; |
|
526 | + } |
|
527 | + return static function ($ch, $h) use($onHeaders, $easy, &$startingResponse) { |
|
528 | + $value = \trim($h); |
|
529 | + if ($value === '') { |
|
530 | + $startingResponse = \true; |
|
531 | + try { |
|
532 | + $easy->createResponse(); |
|
533 | + } catch (\Exception $e) { |
|
534 | + $easy->createResponseException = $e; |
|
535 | + return -1; |
|
536 | + } |
|
537 | + if ($onHeaders !== null) { |
|
538 | + try { |
|
539 | + $onHeaders($easy->response); |
|
540 | + } catch (\Exception $e) { |
|
541 | + // Associate the exception with the handle and trigger |
|
542 | + // a curl header write error by returning 0. |
|
543 | + $easy->onHeadersException = $e; |
|
544 | + return -1; |
|
545 | + } |
|
546 | + } |
|
547 | + } elseif ($startingResponse) { |
|
548 | + $startingResponse = \false; |
|
549 | + $easy->headers = [$value]; |
|
550 | + } else { |
|
551 | + $easy->headers[] = $value; |
|
552 | + } |
|
553 | + return \strlen($h); |
|
554 | + }; |
|
555 | + } |
|
556 | + public function __destruct() |
|
557 | + { |
|
558 | + foreach ($this->handles as $id => $handle) { |
|
559 | + \curl_close($handle); |
|
560 | + unset($this->handles[$id]); |
|
561 | + } |
|
562 | + } |
|
563 | 563 | } |
@@ -209,7 +209,7 @@ discard block |
||
209 | 209 | */ |
210 | 210 | private function getDefaultConf(EasyHandle $easy) : array |
211 | 211 | { |
212 | - $conf = ['_headers' => $easy->request->getHeaders(), \CURLOPT_CUSTOMREQUEST => $easy->request->getMethod(), \CURLOPT_URL => (string) $easy->request->getUri()->withFragment(''), \CURLOPT_RETURNTRANSFER => \false, \CURLOPT_HEADER => \false, \CURLOPT_CONNECTTIMEOUT => 300]; |
|
212 | + $conf = ['_headers' => $easy->request->getHeaders(), \CURLOPT_CUSTOMREQUEST => $easy->request->getMethod(), \CURLOPT_URL => (string)$easy->request->getUri()->withFragment(''), \CURLOPT_RETURNTRANSFER => \false, \CURLOPT_HEADER => \false, \CURLOPT_CONNECTTIMEOUT => 300]; |
|
213 | 213 | if (\defined('CURLOPT_PROTOCOLS')) { |
214 | 214 | $conf[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTP | \CURLPROTO_HTTPS; |
215 | 215 | } |
@@ -244,11 +244,11 @@ discard block |
||
244 | 244 | } |
245 | 245 | private function applyBody(RequestInterface $request, array $options, array &$conf) : void |
246 | 246 | { |
247 | - $size = $request->hasHeader('Content-Length') ? (int) $request->getHeaderLine('Content-Length') : null; |
|
247 | + $size = $request->hasHeader('Content-Length') ? (int)$request->getHeaderLine('Content-Length') : null; |
|
248 | 248 | // Send the body as a string if the size is less than 1MB OR if the |
249 | 249 | // [curl][body_as_string] request value is set. |
250 | 250 | if ($size !== null && $size < 1000000 || !empty($options['_body_as_string'])) { |
251 | - $conf[\CURLOPT_POSTFIELDS] = (string) $request->getBody(); |
|
251 | + $conf[\CURLOPT_POSTFIELDS] = (string)$request->getBody(); |
|
252 | 252 | // Don't duplicate the Content-Length header |
253 | 253 | $this->removeHeader('Content-Length', $conf); |
254 | 254 | $this->removeHeader('Transfer-Encoding', $conf); |
@@ -262,7 +262,7 @@ discard block |
||
262 | 262 | if ($body->isSeekable()) { |
263 | 263 | $body->rewind(); |
264 | 264 | } |
265 | - $conf[\CURLOPT_READFUNCTION] = static function ($ch, $fd, $length) use($body) { |
|
265 | + $conf[\CURLOPT_READFUNCTION] = static function($ch, $fd, $length) use($body) { |
|
266 | 266 | return $body->read($length); |
267 | 267 | }; |
268 | 268 | } |
@@ -279,7 +279,7 @@ discard block |
||
279 | 279 | { |
280 | 280 | foreach ($conf['_headers'] as $name => $values) { |
281 | 281 | foreach ($values as $value) { |
282 | - $value = (string) $value; |
|
282 | + $value = (string)$value; |
|
283 | 283 | if ($value === '') { |
284 | 284 | // cURL requires a special format for empty headers. |
285 | 285 | // See https://github.com/guzzle/guzzle/issues/1882 for more details. |
@@ -364,7 +364,7 @@ discard block |
||
364 | 364 | $sink = new LazyOpenStream($sink, 'w+'); |
365 | 365 | } |
366 | 366 | $easy->sink = $sink; |
367 | - $conf[\CURLOPT_WRITEFUNCTION] = static function ($ch, $write) use($sink) : int { |
|
367 | + $conf[\CURLOPT_WRITEFUNCTION] = static function($ch, $write) use($sink) : int { |
|
368 | 368 | return $sink->write($write); |
369 | 369 | }; |
370 | 370 | $timeoutRequiresNoSignal = \false; |
@@ -471,7 +471,7 @@ discard block |
||
471 | 471 | throw new \InvalidArgumentException('progress client option must be callable'); |
472 | 472 | } |
473 | 473 | $conf[\CURLOPT_NOPROGRESS] = \false; |
474 | - $conf[\CURLOPT_PROGRESSFUNCTION] = static function ($resource, int $downloadSize, int $downloaded, int $uploadSize, int $uploaded) use($progress) { |
|
474 | + $conf[\CURLOPT_PROGRESSFUNCTION] = static function($resource, int $downloadSize, int $downloaded, int $uploadSize, int $uploaded) use($progress) { |
|
475 | 475 | $progress($downloadSize, $downloaded, $uploadSize, $uploaded); |
476 | 476 | }; |
477 | 477 | } |
@@ -500,14 +500,14 @@ discard block |
||
500 | 500 | $body->rewind(); |
501 | 501 | } |
502 | 502 | } catch (\RuntimeException $e) { |
503 | - $ctx['error'] = 'The connection unexpectedly failed without ' . 'providing an error. The request would have been retried, ' . 'but attempting to rewind the request body failed. ' . 'Exception: ' . $e; |
|
503 | + $ctx['error'] = 'The connection unexpectedly failed without '.'providing an error. The request would have been retried, '.'but attempting to rewind the request body failed. '.'Exception: '.$e; |
|
504 | 504 | return self::createRejection($easy, $ctx); |
505 | 505 | } |
506 | 506 | // Retry no more than 3 times before giving up. |
507 | 507 | if (!isset($easy->options['_curl_retries'])) { |
508 | 508 | $easy->options['_curl_retries'] = 1; |
509 | 509 | } elseif ($easy->options['_curl_retries'] == 2) { |
510 | - $ctx['error'] = 'The cURL request was retried 3 times ' . 'and did not succeed. The most likely reason for the failure ' . 'is that cURL was unable to rewind the body of the request ' . 'and subsequent retries resulted in the same error. Turn on ' . 'the debug option to see what went wrong. See ' . 'https://bugs.php.net/bug.php?id=47204 for more information.'; |
|
510 | + $ctx['error'] = 'The cURL request was retried 3 times '.'and did not succeed. The most likely reason for the failure '.'is that cURL was unable to rewind the body of the request '.'and subsequent retries resulted in the same error. Turn on '.'the debug option to see what went wrong. See '.'https://bugs.php.net/bug.php?id=47204 for more information.'; |
|
511 | 511 | return self::createRejection($easy, $ctx); |
512 | 512 | } else { |
513 | 513 | ++$easy->options['_curl_retries']; |
@@ -524,7 +524,7 @@ discard block |
||
524 | 524 | } else { |
525 | 525 | $onHeaders = null; |
526 | 526 | } |
527 | - return static function ($ch, $h) use($onHeaders, $easy, &$startingResponse) { |
|
527 | + return static function($ch, $h) use($onHeaders, $easy, &$startingResponse) { |
|
528 | 528 | $value = \trim($h); |
529 | 529 | if ($value === '') { |
530 | 530 | $startingResponse = \true; |
@@ -43,101 +43,101 @@ |
||
43 | 43 | */ |
44 | 44 | final class Coroutine implements PromiseInterface |
45 | 45 | { |
46 | - /** |
|
47 | - * @var PromiseInterface|null |
|
48 | - */ |
|
49 | - private $currentPromise; |
|
50 | - /** |
|
51 | - * @var Generator |
|
52 | - */ |
|
53 | - private $generator; |
|
54 | - /** |
|
55 | - * @var Promise |
|
56 | - */ |
|
57 | - private $result; |
|
58 | - public function __construct(callable $generatorFn) |
|
59 | - { |
|
60 | - $this->generator = $generatorFn(); |
|
61 | - $this->result = new Promise(function () : void { |
|
62 | - while (isset($this->currentPromise)) { |
|
63 | - $this->currentPromise->wait(); |
|
64 | - } |
|
65 | - }); |
|
66 | - try { |
|
67 | - $this->nextCoroutine($this->generator->current()); |
|
68 | - } catch (Throwable $throwable) { |
|
69 | - $this->result->reject($throwable); |
|
70 | - } |
|
71 | - } |
|
72 | - /** |
|
73 | - * Create a new coroutine. |
|
74 | - */ |
|
75 | - public static function of(callable $generatorFn) : self |
|
76 | - { |
|
77 | - return new self($generatorFn); |
|
78 | - } |
|
79 | - public function then(?callable $onFulfilled = null, ?callable $onRejected = null) : PromiseInterface |
|
80 | - { |
|
81 | - return $this->result->then($onFulfilled, $onRejected); |
|
82 | - } |
|
83 | - public function otherwise(callable $onRejected) : PromiseInterface |
|
84 | - { |
|
85 | - return $this->result->otherwise($onRejected); |
|
86 | - } |
|
87 | - public function wait(bool $unwrap = \true) |
|
88 | - { |
|
89 | - return $this->result->wait($unwrap); |
|
90 | - } |
|
91 | - public function getState() : string |
|
92 | - { |
|
93 | - return $this->result->getState(); |
|
94 | - } |
|
95 | - public function resolve($value) : void |
|
96 | - { |
|
97 | - $this->result->resolve($value); |
|
98 | - } |
|
99 | - public function reject($reason) : void |
|
100 | - { |
|
101 | - $this->result->reject($reason); |
|
102 | - } |
|
103 | - public function cancel() : void |
|
104 | - { |
|
105 | - $this->currentPromise->cancel(); |
|
106 | - $this->result->cancel(); |
|
107 | - } |
|
108 | - private function nextCoroutine($yielded) : void |
|
109 | - { |
|
110 | - $this->currentPromise = Create::promiseFor($yielded)->then([$this, '_handleSuccess'], [$this, '_handleFailure']); |
|
111 | - } |
|
112 | - /** |
|
113 | - * @internal |
|
114 | - */ |
|
115 | - public function _handleSuccess($value) : void |
|
116 | - { |
|
117 | - unset($this->currentPromise); |
|
118 | - try { |
|
119 | - $next = $this->generator->send($value); |
|
120 | - if ($this->generator->valid()) { |
|
121 | - $this->nextCoroutine($next); |
|
122 | - } else { |
|
123 | - $this->result->resolve($value); |
|
124 | - } |
|
125 | - } catch (Throwable $throwable) { |
|
126 | - $this->result->reject($throwable); |
|
127 | - } |
|
128 | - } |
|
129 | - /** |
|
130 | - * @internal |
|
131 | - */ |
|
132 | - public function _handleFailure($reason) : void |
|
133 | - { |
|
134 | - unset($this->currentPromise); |
|
135 | - try { |
|
136 | - $nextYield = $this->generator->throw(Create::exceptionFor($reason)); |
|
137 | - // The throw was caught, so keep iterating on the coroutine |
|
138 | - $this->nextCoroutine($nextYield); |
|
139 | - } catch (Throwable $throwable) { |
|
140 | - $this->result->reject($throwable); |
|
141 | - } |
|
142 | - } |
|
46 | + /** |
|
47 | + * @var PromiseInterface|null |
|
48 | + */ |
|
49 | + private $currentPromise; |
|
50 | + /** |
|
51 | + * @var Generator |
|
52 | + */ |
|
53 | + private $generator; |
|
54 | + /** |
|
55 | + * @var Promise |
|
56 | + */ |
|
57 | + private $result; |
|
58 | + public function __construct(callable $generatorFn) |
|
59 | + { |
|
60 | + $this->generator = $generatorFn(); |
|
61 | + $this->result = new Promise(function () : void { |
|
62 | + while (isset($this->currentPromise)) { |
|
63 | + $this->currentPromise->wait(); |
|
64 | + } |
|
65 | + }); |
|
66 | + try { |
|
67 | + $this->nextCoroutine($this->generator->current()); |
|
68 | + } catch (Throwable $throwable) { |
|
69 | + $this->result->reject($throwable); |
|
70 | + } |
|
71 | + } |
|
72 | + /** |
|
73 | + * Create a new coroutine. |
|
74 | + */ |
|
75 | + public static function of(callable $generatorFn) : self |
|
76 | + { |
|
77 | + return new self($generatorFn); |
|
78 | + } |
|
79 | + public function then(?callable $onFulfilled = null, ?callable $onRejected = null) : PromiseInterface |
|
80 | + { |
|
81 | + return $this->result->then($onFulfilled, $onRejected); |
|
82 | + } |
|
83 | + public function otherwise(callable $onRejected) : PromiseInterface |
|
84 | + { |
|
85 | + return $this->result->otherwise($onRejected); |
|
86 | + } |
|
87 | + public function wait(bool $unwrap = \true) |
|
88 | + { |
|
89 | + return $this->result->wait($unwrap); |
|
90 | + } |
|
91 | + public function getState() : string |
|
92 | + { |
|
93 | + return $this->result->getState(); |
|
94 | + } |
|
95 | + public function resolve($value) : void |
|
96 | + { |
|
97 | + $this->result->resolve($value); |
|
98 | + } |
|
99 | + public function reject($reason) : void |
|
100 | + { |
|
101 | + $this->result->reject($reason); |
|
102 | + } |
|
103 | + public function cancel() : void |
|
104 | + { |
|
105 | + $this->currentPromise->cancel(); |
|
106 | + $this->result->cancel(); |
|
107 | + } |
|
108 | + private function nextCoroutine($yielded) : void |
|
109 | + { |
|
110 | + $this->currentPromise = Create::promiseFor($yielded)->then([$this, '_handleSuccess'], [$this, '_handleFailure']); |
|
111 | + } |
|
112 | + /** |
|
113 | + * @internal |
|
114 | + */ |
|
115 | + public function _handleSuccess($value) : void |
|
116 | + { |
|
117 | + unset($this->currentPromise); |
|
118 | + try { |
|
119 | + $next = $this->generator->send($value); |
|
120 | + if ($this->generator->valid()) { |
|
121 | + $this->nextCoroutine($next); |
|
122 | + } else { |
|
123 | + $this->result->resolve($value); |
|
124 | + } |
|
125 | + } catch (Throwable $throwable) { |
|
126 | + $this->result->reject($throwable); |
|
127 | + } |
|
128 | + } |
|
129 | + /** |
|
130 | + * @internal |
|
131 | + */ |
|
132 | + public function _handleFailure($reason) : void |
|
133 | + { |
|
134 | + unset($this->currentPromise); |
|
135 | + try { |
|
136 | + $nextYield = $this->generator->throw(Create::exceptionFor($reason)); |
|
137 | + // The throw was caught, so keep iterating on the coroutine |
|
138 | + $this->nextCoroutine($nextYield); |
|
139 | + } catch (Throwable $throwable) { |
|
140 | + $this->result->reject($throwable); |
|
141 | + } |
|
142 | + } |
|
143 | 143 | } |
@@ -5,215 +5,215 @@ |
||
5 | 5 | |
6 | 6 | final class Utils |
7 | 7 | { |
8 | - /** |
|
9 | - * Get the global task queue used for promise resolution. |
|
10 | - * |
|
11 | - * This task queue MUST be run in an event loop in order for promises to be |
|
12 | - * settled asynchronously. It will be automatically run when synchronously |
|
13 | - * waiting on a promise. |
|
14 | - * |
|
15 | - * <code> |
|
16 | - * while ($eventLoop->isRunning()) { |
|
17 | - * GuzzleHttp\Promise\Utils::queue()->run(); |
|
18 | - * } |
|
19 | - * </code> |
|
20 | - * |
|
21 | - * @param TaskQueueInterface|null $assign Optionally specify a new queue instance. |
|
22 | - */ |
|
23 | - public static function queue(?TaskQueueInterface $assign = null) : TaskQueueInterface |
|
24 | - { |
|
25 | - static $queue; |
|
26 | - if ($assign) { |
|
27 | - $queue = $assign; |
|
28 | - } elseif (!$queue) { |
|
29 | - $queue = new TaskQueue(); |
|
30 | - } |
|
31 | - return $queue; |
|
32 | - } |
|
33 | - /** |
|
34 | - * Adds a function to run in the task queue when it is next `run()` and |
|
35 | - * returns a promise that is fulfilled or rejected with the result. |
|
36 | - * |
|
37 | - * @param callable $task Task function to run. |
|
38 | - */ |
|
39 | - public static function task(callable $task) : PromiseInterface |
|
40 | - { |
|
41 | - $queue = self::queue(); |
|
42 | - $promise = new Promise([$queue, 'run']); |
|
43 | - $queue->add(function () use($task, $promise) : void { |
|
44 | - try { |
|
45 | - if (Is::pending($promise)) { |
|
46 | - $promise->resolve($task()); |
|
47 | - } |
|
48 | - } catch (\Throwable $e) { |
|
49 | - $promise->reject($e); |
|
50 | - } |
|
51 | - }); |
|
52 | - return $promise; |
|
53 | - } |
|
54 | - /** |
|
55 | - * Synchronously waits on a promise to resolve and returns an inspection |
|
56 | - * state array. |
|
57 | - * |
|
58 | - * Returns a state associative array containing a "state" key mapping to a |
|
59 | - * valid promise state. If the state of the promise is "fulfilled", the |
|
60 | - * array will contain a "value" key mapping to the fulfilled value of the |
|
61 | - * promise. If the promise is rejected, the array will contain a "reason" |
|
62 | - * key mapping to the rejection reason of the promise. |
|
63 | - * |
|
64 | - * @param PromiseInterface $promise Promise or value. |
|
65 | - */ |
|
66 | - public static function inspect(PromiseInterface $promise) : array |
|
67 | - { |
|
68 | - try { |
|
69 | - return ['state' => PromiseInterface::FULFILLED, 'value' => $promise->wait()]; |
|
70 | - } catch (RejectionException $e) { |
|
71 | - return ['state' => PromiseInterface::REJECTED, 'reason' => $e->getReason()]; |
|
72 | - } catch (\Throwable $e) { |
|
73 | - return ['state' => PromiseInterface::REJECTED, 'reason' => $e]; |
|
74 | - } |
|
75 | - } |
|
76 | - /** |
|
77 | - * Waits on all of the provided promises, but does not unwrap rejected |
|
78 | - * promises as thrown exception. |
|
79 | - * |
|
80 | - * Returns an array of inspection state arrays. |
|
81 | - * |
|
82 | - * @see inspect for the inspection state array format. |
|
83 | - * |
|
84 | - * @param PromiseInterface[] $promises Traversable of promises to wait upon. |
|
85 | - */ |
|
86 | - public static function inspectAll($promises) : array |
|
87 | - { |
|
88 | - $results = []; |
|
89 | - foreach ($promises as $key => $promise) { |
|
90 | - $results[$key] = self::inspect($promise); |
|
91 | - } |
|
92 | - return $results; |
|
93 | - } |
|
94 | - /** |
|
95 | - * Waits on all of the provided promises and returns the fulfilled values. |
|
96 | - * |
|
97 | - * Returns an array that contains the value of each promise (in the same |
|
98 | - * order the promises were provided). An exception is thrown if any of the |
|
99 | - * promises are rejected. |
|
100 | - * |
|
101 | - * @param iterable<PromiseInterface> $promises Iterable of PromiseInterface objects to wait on. |
|
102 | - * |
|
103 | - * @throws \Throwable on error |
|
104 | - */ |
|
105 | - public static function unwrap($promises) : array |
|
106 | - { |
|
107 | - $results = []; |
|
108 | - foreach ($promises as $key => $promise) { |
|
109 | - $results[$key] = $promise->wait(); |
|
110 | - } |
|
111 | - return $results; |
|
112 | - } |
|
113 | - /** |
|
114 | - * Given an array of promises, return a promise that is fulfilled when all |
|
115 | - * the items in the array are fulfilled. |
|
116 | - * |
|
117 | - * The promise's fulfillment value is an array with fulfillment values at |
|
118 | - * respective positions to the original array. If any promise in the array |
|
119 | - * rejects, the returned promise is rejected with the rejection reason. |
|
120 | - * |
|
121 | - * @param mixed $promises Promises or values. |
|
122 | - * @param bool $recursive If true, resolves new promises that might have been added to the stack during its own resolution. |
|
123 | - */ |
|
124 | - public static function all($promises, bool $recursive = \false) : PromiseInterface |
|
125 | - { |
|
126 | - $results = []; |
|
127 | - $promise = Each::of($promises, function ($value, $idx) use(&$results) : void { |
|
128 | - $results[$idx] = $value; |
|
129 | - }, function ($reason, $idx, Promise $aggregate) : void { |
|
130 | - if (Is::pending($aggregate)) { |
|
131 | - $aggregate->reject($reason); |
|
132 | - } |
|
133 | - })->then(function () use(&$results) { |
|
134 | - \ksort($results); |
|
135 | - return $results; |
|
136 | - }); |
|
137 | - if (\true === $recursive) { |
|
138 | - $promise = $promise->then(function ($results) use($recursive, &$promises) { |
|
139 | - foreach ($promises as $promise) { |
|
140 | - if (Is::pending($promise)) { |
|
141 | - return self::all($promises, $recursive); |
|
142 | - } |
|
143 | - } |
|
144 | - return $results; |
|
145 | - }); |
|
146 | - } |
|
147 | - return $promise; |
|
148 | - } |
|
149 | - /** |
|
150 | - * Initiate a competitive race between multiple promises or values (values |
|
151 | - * will become immediately fulfilled promises). |
|
152 | - * |
|
153 | - * When count amount of promises have been fulfilled, the returned promise |
|
154 | - * is fulfilled with an array that contains the fulfillment values of the |
|
155 | - * winners in order of resolution. |
|
156 | - * |
|
157 | - * This promise is rejected with a {@see AggregateException} if the number |
|
158 | - * of fulfilled promises is less than the desired $count. |
|
159 | - * |
|
160 | - * @param int $count Total number of promises. |
|
161 | - * @param mixed $promises Promises or values. |
|
162 | - */ |
|
163 | - public static function some(int $count, $promises) : PromiseInterface |
|
164 | - { |
|
165 | - $results = []; |
|
166 | - $rejections = []; |
|
167 | - return Each::of($promises, function ($value, $idx, PromiseInterface $p) use(&$results, $count) : void { |
|
168 | - if (Is::settled($p)) { |
|
169 | - return; |
|
170 | - } |
|
171 | - $results[$idx] = $value; |
|
172 | - if (\count($results) >= $count) { |
|
173 | - $p->resolve(null); |
|
174 | - } |
|
175 | - }, function ($reason) use(&$rejections) : void { |
|
176 | - $rejections[] = $reason; |
|
177 | - })->then(function () use(&$results, &$rejections, $count) { |
|
178 | - if (\count($results) !== $count) { |
|
179 | - throw new AggregateException('Not enough promises to fulfill count', $rejections); |
|
180 | - } |
|
181 | - \ksort($results); |
|
182 | - return \array_values($results); |
|
183 | - }); |
|
184 | - } |
|
185 | - /** |
|
186 | - * Like some(), with 1 as count. However, if the promise fulfills, the |
|
187 | - * fulfillment value is not an array of 1 but the value directly. |
|
188 | - * |
|
189 | - * @param mixed $promises Promises or values. |
|
190 | - */ |
|
191 | - public static function any($promises) : PromiseInterface |
|
192 | - { |
|
193 | - return self::some(1, $promises)->then(function ($values) { |
|
194 | - return $values[0]; |
|
195 | - }); |
|
196 | - } |
|
197 | - /** |
|
198 | - * Returns a promise that is fulfilled when all of the provided promises have |
|
199 | - * been fulfilled or rejected. |
|
200 | - * |
|
201 | - * The returned promise is fulfilled with an array of inspection state arrays. |
|
202 | - * |
|
203 | - * @see inspect for the inspection state array format. |
|
204 | - * |
|
205 | - * @param mixed $promises Promises or values. |
|
206 | - */ |
|
207 | - public static function settle($promises) : PromiseInterface |
|
208 | - { |
|
209 | - $results = []; |
|
210 | - return Each::of($promises, function ($value, $idx) use(&$results) : void { |
|
211 | - $results[$idx] = ['state' => PromiseInterface::FULFILLED, 'value' => $value]; |
|
212 | - }, function ($reason, $idx) use(&$results) : void { |
|
213 | - $results[$idx] = ['state' => PromiseInterface::REJECTED, 'reason' => $reason]; |
|
214 | - })->then(function () use(&$results) { |
|
215 | - \ksort($results); |
|
216 | - return $results; |
|
217 | - }); |
|
218 | - } |
|
8 | + /** |
|
9 | + * Get the global task queue used for promise resolution. |
|
10 | + * |
|
11 | + * This task queue MUST be run in an event loop in order for promises to be |
|
12 | + * settled asynchronously. It will be automatically run when synchronously |
|
13 | + * waiting on a promise. |
|
14 | + * |
|
15 | + * <code> |
|
16 | + * while ($eventLoop->isRunning()) { |
|
17 | + * GuzzleHttp\Promise\Utils::queue()->run(); |
|
18 | + * } |
|
19 | + * </code> |
|
20 | + * |
|
21 | + * @param TaskQueueInterface|null $assign Optionally specify a new queue instance. |
|
22 | + */ |
|
23 | + public static function queue(?TaskQueueInterface $assign = null) : TaskQueueInterface |
|
24 | + { |
|
25 | + static $queue; |
|
26 | + if ($assign) { |
|
27 | + $queue = $assign; |
|
28 | + } elseif (!$queue) { |
|
29 | + $queue = new TaskQueue(); |
|
30 | + } |
|
31 | + return $queue; |
|
32 | + } |
|
33 | + /** |
|
34 | + * Adds a function to run in the task queue when it is next `run()` and |
|
35 | + * returns a promise that is fulfilled or rejected with the result. |
|
36 | + * |
|
37 | + * @param callable $task Task function to run. |
|
38 | + */ |
|
39 | + public static function task(callable $task) : PromiseInterface |
|
40 | + { |
|
41 | + $queue = self::queue(); |
|
42 | + $promise = new Promise([$queue, 'run']); |
|
43 | + $queue->add(function () use($task, $promise) : void { |
|
44 | + try { |
|
45 | + if (Is::pending($promise)) { |
|
46 | + $promise->resolve($task()); |
|
47 | + } |
|
48 | + } catch (\Throwable $e) { |
|
49 | + $promise->reject($e); |
|
50 | + } |
|
51 | + }); |
|
52 | + return $promise; |
|
53 | + } |
|
54 | + /** |
|
55 | + * Synchronously waits on a promise to resolve and returns an inspection |
|
56 | + * state array. |
|
57 | + * |
|
58 | + * Returns a state associative array containing a "state" key mapping to a |
|
59 | + * valid promise state. If the state of the promise is "fulfilled", the |
|
60 | + * array will contain a "value" key mapping to the fulfilled value of the |
|
61 | + * promise. If the promise is rejected, the array will contain a "reason" |
|
62 | + * key mapping to the rejection reason of the promise. |
|
63 | + * |
|
64 | + * @param PromiseInterface $promise Promise or value. |
|
65 | + */ |
|
66 | + public static function inspect(PromiseInterface $promise) : array |
|
67 | + { |
|
68 | + try { |
|
69 | + return ['state' => PromiseInterface::FULFILLED, 'value' => $promise->wait()]; |
|
70 | + } catch (RejectionException $e) { |
|
71 | + return ['state' => PromiseInterface::REJECTED, 'reason' => $e->getReason()]; |
|
72 | + } catch (\Throwable $e) { |
|
73 | + return ['state' => PromiseInterface::REJECTED, 'reason' => $e]; |
|
74 | + } |
|
75 | + } |
|
76 | + /** |
|
77 | + * Waits on all of the provided promises, but does not unwrap rejected |
|
78 | + * promises as thrown exception. |
|
79 | + * |
|
80 | + * Returns an array of inspection state arrays. |
|
81 | + * |
|
82 | + * @see inspect for the inspection state array format. |
|
83 | + * |
|
84 | + * @param PromiseInterface[] $promises Traversable of promises to wait upon. |
|
85 | + */ |
|
86 | + public static function inspectAll($promises) : array |
|
87 | + { |
|
88 | + $results = []; |
|
89 | + foreach ($promises as $key => $promise) { |
|
90 | + $results[$key] = self::inspect($promise); |
|
91 | + } |
|
92 | + return $results; |
|
93 | + } |
|
94 | + /** |
|
95 | + * Waits on all of the provided promises and returns the fulfilled values. |
|
96 | + * |
|
97 | + * Returns an array that contains the value of each promise (in the same |
|
98 | + * order the promises were provided). An exception is thrown if any of the |
|
99 | + * promises are rejected. |
|
100 | + * |
|
101 | + * @param iterable<PromiseInterface> $promises Iterable of PromiseInterface objects to wait on. |
|
102 | + * |
|
103 | + * @throws \Throwable on error |
|
104 | + */ |
|
105 | + public static function unwrap($promises) : array |
|
106 | + { |
|
107 | + $results = []; |
|
108 | + foreach ($promises as $key => $promise) { |
|
109 | + $results[$key] = $promise->wait(); |
|
110 | + } |
|
111 | + return $results; |
|
112 | + } |
|
113 | + /** |
|
114 | + * Given an array of promises, return a promise that is fulfilled when all |
|
115 | + * the items in the array are fulfilled. |
|
116 | + * |
|
117 | + * The promise's fulfillment value is an array with fulfillment values at |
|
118 | + * respective positions to the original array. If any promise in the array |
|
119 | + * rejects, the returned promise is rejected with the rejection reason. |
|
120 | + * |
|
121 | + * @param mixed $promises Promises or values. |
|
122 | + * @param bool $recursive If true, resolves new promises that might have been added to the stack during its own resolution. |
|
123 | + */ |
|
124 | + public static function all($promises, bool $recursive = \false) : PromiseInterface |
|
125 | + { |
|
126 | + $results = []; |
|
127 | + $promise = Each::of($promises, function ($value, $idx) use(&$results) : void { |
|
128 | + $results[$idx] = $value; |
|
129 | + }, function ($reason, $idx, Promise $aggregate) : void { |
|
130 | + if (Is::pending($aggregate)) { |
|
131 | + $aggregate->reject($reason); |
|
132 | + } |
|
133 | + })->then(function () use(&$results) { |
|
134 | + \ksort($results); |
|
135 | + return $results; |
|
136 | + }); |
|
137 | + if (\true === $recursive) { |
|
138 | + $promise = $promise->then(function ($results) use($recursive, &$promises) { |
|
139 | + foreach ($promises as $promise) { |
|
140 | + if (Is::pending($promise)) { |
|
141 | + return self::all($promises, $recursive); |
|
142 | + } |
|
143 | + } |
|
144 | + return $results; |
|
145 | + }); |
|
146 | + } |
|
147 | + return $promise; |
|
148 | + } |
|
149 | + /** |
|
150 | + * Initiate a competitive race between multiple promises or values (values |
|
151 | + * will become immediately fulfilled promises). |
|
152 | + * |
|
153 | + * When count amount of promises have been fulfilled, the returned promise |
|
154 | + * is fulfilled with an array that contains the fulfillment values of the |
|
155 | + * winners in order of resolution. |
|
156 | + * |
|
157 | + * This promise is rejected with a {@see AggregateException} if the number |
|
158 | + * of fulfilled promises is less than the desired $count. |
|
159 | + * |
|
160 | + * @param int $count Total number of promises. |
|
161 | + * @param mixed $promises Promises or values. |
|
162 | + */ |
|
163 | + public static function some(int $count, $promises) : PromiseInterface |
|
164 | + { |
|
165 | + $results = []; |
|
166 | + $rejections = []; |
|
167 | + return Each::of($promises, function ($value, $idx, PromiseInterface $p) use(&$results, $count) : void { |
|
168 | + if (Is::settled($p)) { |
|
169 | + return; |
|
170 | + } |
|
171 | + $results[$idx] = $value; |
|
172 | + if (\count($results) >= $count) { |
|
173 | + $p->resolve(null); |
|
174 | + } |
|
175 | + }, function ($reason) use(&$rejections) : void { |
|
176 | + $rejections[] = $reason; |
|
177 | + })->then(function () use(&$results, &$rejections, $count) { |
|
178 | + if (\count($results) !== $count) { |
|
179 | + throw new AggregateException('Not enough promises to fulfill count', $rejections); |
|
180 | + } |
|
181 | + \ksort($results); |
|
182 | + return \array_values($results); |
|
183 | + }); |
|
184 | + } |
|
185 | + /** |
|
186 | + * Like some(), with 1 as count. However, if the promise fulfills, the |
|
187 | + * fulfillment value is not an array of 1 but the value directly. |
|
188 | + * |
|
189 | + * @param mixed $promises Promises or values. |
|
190 | + */ |
|
191 | + public static function any($promises) : PromiseInterface |
|
192 | + { |
|
193 | + return self::some(1, $promises)->then(function ($values) { |
|
194 | + return $values[0]; |
|
195 | + }); |
|
196 | + } |
|
197 | + /** |
|
198 | + * Returns a promise that is fulfilled when all of the provided promises have |
|
199 | + * been fulfilled or rejected. |
|
200 | + * |
|
201 | + * The returned promise is fulfilled with an array of inspection state arrays. |
|
202 | + * |
|
203 | + * @see inspect for the inspection state array format. |
|
204 | + * |
|
205 | + * @param mixed $promises Promises or values. |
|
206 | + */ |
|
207 | + public static function settle($promises) : PromiseInterface |
|
208 | + { |
|
209 | + $results = []; |
|
210 | + return Each::of($promises, function ($value, $idx) use(&$results) : void { |
|
211 | + $results[$idx] = ['state' => PromiseInterface::FULFILLED, 'value' => $value]; |
|
212 | + }, function ($reason, $idx) use(&$results) : void { |
|
213 | + $results[$idx] = ['state' => PromiseInterface::REJECTED, 'reason' => $reason]; |
|
214 | + })->then(function () use(&$results) { |
|
215 | + \ksort($results); |
|
216 | + return $results; |
|
217 | + }); |
|
218 | + } |
|
219 | 219 | } |
@@ -1,6 +1,6 @@ discard block |
||
1 | 1 | <?php |
2 | 2 | |
3 | -declare (strict_types=1); |
|
3 | +declare(strict_types=1); |
|
4 | 4 | namespace OCA\FullTextSearch_Elasticsearch\Vendor\GuzzleHttp\Promise; |
5 | 5 | |
6 | 6 | final class Utils |
@@ -40,7 +40,7 @@ discard block |
||
40 | 40 | { |
41 | 41 | $queue = self::queue(); |
42 | 42 | $promise = new Promise([$queue, 'run']); |
43 | - $queue->add(function () use($task, $promise) : void { |
|
43 | + $queue->add(function() use($task, $promise) : void { |
|
44 | 44 | try { |
45 | 45 | if (Is::pending($promise)) { |
46 | 46 | $promise->resolve($task()); |
@@ -124,18 +124,18 @@ discard block |
||
124 | 124 | public static function all($promises, bool $recursive = \false) : PromiseInterface |
125 | 125 | { |
126 | 126 | $results = []; |
127 | - $promise = Each::of($promises, function ($value, $idx) use(&$results) : void { |
|
127 | + $promise = Each::of($promises, function($value, $idx) use(&$results) : void { |
|
128 | 128 | $results[$idx] = $value; |
129 | - }, function ($reason, $idx, Promise $aggregate) : void { |
|
129 | + }, function($reason, $idx, Promise $aggregate) : void { |
|
130 | 130 | if (Is::pending($aggregate)) { |
131 | 131 | $aggregate->reject($reason); |
132 | 132 | } |
133 | - })->then(function () use(&$results) { |
|
133 | + })->then(function() use(&$results) { |
|
134 | 134 | \ksort($results); |
135 | 135 | return $results; |
136 | 136 | }); |
137 | 137 | if (\true === $recursive) { |
138 | - $promise = $promise->then(function ($results) use($recursive, &$promises) { |
|
138 | + $promise = $promise->then(function($results) use($recursive, &$promises) { |
|
139 | 139 | foreach ($promises as $promise) { |
140 | 140 | if (Is::pending($promise)) { |
141 | 141 | return self::all($promises, $recursive); |
@@ -164,7 +164,7 @@ discard block |
||
164 | 164 | { |
165 | 165 | $results = []; |
166 | 166 | $rejections = []; |
167 | - return Each::of($promises, function ($value, $idx, PromiseInterface $p) use(&$results, $count) : void { |
|
167 | + return Each::of($promises, function($value, $idx, PromiseInterface $p) use(&$results, $count) : void { |
|
168 | 168 | if (Is::settled($p)) { |
169 | 169 | return; |
170 | 170 | } |
@@ -172,9 +172,9 @@ discard block |
||
172 | 172 | if (\count($results) >= $count) { |
173 | 173 | $p->resolve(null); |
174 | 174 | } |
175 | - }, function ($reason) use(&$rejections) : void { |
|
175 | + }, function($reason) use(&$rejections) : void { |
|
176 | 176 | $rejections[] = $reason; |
177 | - })->then(function () use(&$results, &$rejections, $count) { |
|
177 | + })->then(function() use(&$results, &$rejections, $count) { |
|
178 | 178 | if (\count($results) !== $count) { |
179 | 179 | throw new AggregateException('Not enough promises to fulfill count', $rejections); |
180 | 180 | } |
@@ -190,7 +190,7 @@ discard block |
||
190 | 190 | */ |
191 | 191 | public static function any($promises) : PromiseInterface |
192 | 192 | { |
193 | - return self::some(1, $promises)->then(function ($values) { |
|
193 | + return self::some(1, $promises)->then(function($values) { |
|
194 | 194 | return $values[0]; |
195 | 195 | }); |
196 | 196 | } |
@@ -207,11 +207,11 @@ discard block |
||
207 | 207 | public static function settle($promises) : PromiseInterface |
208 | 208 | { |
209 | 209 | $results = []; |
210 | - return Each::of($promises, function ($value, $idx) use(&$results) : void { |
|
210 | + return Each::of($promises, function($value, $idx) use(&$results) : void { |
|
211 | 211 | $results[$idx] = ['state' => PromiseInterface::FULFILLED, 'value' => $value]; |
212 | - }, function ($reason, $idx) use(&$results) : void { |
|
212 | + }, function($reason, $idx) use(&$results) : void { |
|
213 | 213 | $results[$idx] = ['state' => PromiseInterface::REJECTED, 'reason' => $reason]; |
214 | - })->then(function () use(&$results) { |
|
214 | + })->then(function() use(&$results) { |
|
215 | 215 | \ksort($results); |
216 | 216 | return $results; |
217 | 217 | }); |
@@ -12,225 +12,225 @@ |
||
12 | 12 | */ |
13 | 13 | class Promise implements PromiseInterface |
14 | 14 | { |
15 | - private $state = self::PENDING; |
|
16 | - private $result; |
|
17 | - private $cancelFn; |
|
18 | - private $waitFn; |
|
19 | - private $waitList; |
|
20 | - private $handlers = []; |
|
21 | - /** |
|
22 | - * @param callable $waitFn Fn that when invoked resolves the promise. |
|
23 | - * @param callable $cancelFn Fn that when invoked cancels the promise. |
|
24 | - */ |
|
25 | - public function __construct(?callable $waitFn = null, ?callable $cancelFn = null) |
|
26 | - { |
|
27 | - $this->waitFn = $waitFn; |
|
28 | - $this->cancelFn = $cancelFn; |
|
29 | - } |
|
30 | - public function then(?callable $onFulfilled = null, ?callable $onRejected = null) : PromiseInterface |
|
31 | - { |
|
32 | - if ($this->state === self::PENDING) { |
|
33 | - $p = new Promise(null, [$this, 'cancel']); |
|
34 | - $this->handlers[] = [$p, $onFulfilled, $onRejected]; |
|
35 | - $p->waitList = $this->waitList; |
|
36 | - $p->waitList[] = $this; |
|
37 | - return $p; |
|
38 | - } |
|
39 | - // Return a fulfilled promise and immediately invoke any callbacks. |
|
40 | - if ($this->state === self::FULFILLED) { |
|
41 | - $promise = Create::promiseFor($this->result); |
|
42 | - return $onFulfilled ? $promise->then($onFulfilled) : $promise; |
|
43 | - } |
|
44 | - // It's either cancelled or rejected, so return a rejected promise |
|
45 | - // and immediately invoke any callbacks. |
|
46 | - $rejection = Create::rejectionFor($this->result); |
|
47 | - return $onRejected ? $rejection->then(null, $onRejected) : $rejection; |
|
48 | - } |
|
49 | - public function otherwise(callable $onRejected) : PromiseInterface |
|
50 | - { |
|
51 | - return $this->then(null, $onRejected); |
|
52 | - } |
|
53 | - public function wait(bool $unwrap = \true) |
|
54 | - { |
|
55 | - $this->waitIfPending(); |
|
56 | - if ($this->result instanceof PromiseInterface) { |
|
57 | - return $this->result->wait($unwrap); |
|
58 | - } |
|
59 | - if ($unwrap) { |
|
60 | - if ($this->state === self::FULFILLED) { |
|
61 | - return $this->result; |
|
62 | - } |
|
63 | - // It's rejected so "unwrap" and throw an exception. |
|
64 | - throw Create::exceptionFor($this->result); |
|
65 | - } |
|
66 | - } |
|
67 | - public function getState() : string |
|
68 | - { |
|
69 | - return $this->state; |
|
70 | - } |
|
71 | - public function cancel() : void |
|
72 | - { |
|
73 | - if ($this->state !== self::PENDING) { |
|
74 | - return; |
|
75 | - } |
|
76 | - $this->waitFn = $this->waitList = null; |
|
77 | - if ($this->cancelFn) { |
|
78 | - $fn = $this->cancelFn; |
|
79 | - $this->cancelFn = null; |
|
80 | - try { |
|
81 | - $fn(); |
|
82 | - } catch (\Throwable $e) { |
|
83 | - $this->reject($e); |
|
84 | - } |
|
85 | - } |
|
86 | - // Reject the promise only if it wasn't rejected in a then callback. |
|
87 | - /** @psalm-suppress RedundantCondition */ |
|
88 | - if ($this->state === self::PENDING) { |
|
89 | - $this->reject(new CancellationException('Promise has been cancelled')); |
|
90 | - } |
|
91 | - } |
|
92 | - public function resolve($value) : void |
|
93 | - { |
|
94 | - $this->settle(self::FULFILLED, $value); |
|
95 | - } |
|
96 | - public function reject($reason) : void |
|
97 | - { |
|
98 | - $this->settle(self::REJECTED, $reason); |
|
99 | - } |
|
100 | - private function settle(string $state, $value) : void |
|
101 | - { |
|
102 | - if ($this->state !== self::PENDING) { |
|
103 | - // Ignore calls with the same resolution. |
|
104 | - if ($state === $this->state && $value === $this->result) { |
|
105 | - return; |
|
106 | - } |
|
107 | - throw $this->state === $state ? new \LogicException("The promise is already {$state}.") : new \LogicException("Cannot change a {$this->state} promise to {$state}"); |
|
108 | - } |
|
109 | - if ($value === $this) { |
|
110 | - throw new \LogicException('Cannot fulfill or reject a promise with itself'); |
|
111 | - } |
|
112 | - // Clear out the state of the promise but stash the handlers. |
|
113 | - $this->state = $state; |
|
114 | - $this->result = $value; |
|
115 | - $handlers = $this->handlers; |
|
116 | - $this->handlers = null; |
|
117 | - $this->waitList = $this->waitFn = null; |
|
118 | - $this->cancelFn = null; |
|
119 | - if (!$handlers) { |
|
120 | - return; |
|
121 | - } |
|
122 | - // If the value was not a settled promise or a thenable, then resolve |
|
123 | - // it in the task queue using the correct ID. |
|
124 | - if (!\is_object($value) || !\method_exists($value, 'then')) { |
|
125 | - $id = $state === self::FULFILLED ? 1 : 2; |
|
126 | - // It's a success, so resolve the handlers in the queue. |
|
127 | - Utils::queue()->add(static function () use($id, $value, $handlers) : void { |
|
128 | - foreach ($handlers as $handler) { |
|
129 | - self::callHandler($id, $value, $handler); |
|
130 | - } |
|
131 | - }); |
|
132 | - } elseif ($value instanceof Promise && Is::pending($value)) { |
|
133 | - // We can just merge our handlers onto the next promise. |
|
134 | - $value->handlers = \array_merge($value->handlers, $handlers); |
|
135 | - } else { |
|
136 | - // Resolve the handlers when the forwarded promise is resolved. |
|
137 | - $value->then(static function ($value) use($handlers) : void { |
|
138 | - foreach ($handlers as $handler) { |
|
139 | - self::callHandler(1, $value, $handler); |
|
140 | - } |
|
141 | - }, static function ($reason) use($handlers) : void { |
|
142 | - foreach ($handlers as $handler) { |
|
143 | - self::callHandler(2, $reason, $handler); |
|
144 | - } |
|
145 | - }); |
|
146 | - } |
|
147 | - } |
|
148 | - /** |
|
149 | - * Call a stack of handlers using a specific callback index and value. |
|
150 | - * |
|
151 | - * @param int $index 1 (resolve) or 2 (reject). |
|
152 | - * @param mixed $value Value to pass to the callback. |
|
153 | - * @param array $handler Array of handler data (promise and callbacks). |
|
154 | - */ |
|
155 | - private static function callHandler(int $index, $value, array $handler) : void |
|
156 | - { |
|
157 | - /** @var PromiseInterface $promise */ |
|
158 | - $promise = $handler[0]; |
|
159 | - // The promise may have been cancelled or resolved before placing |
|
160 | - // this thunk in the queue. |
|
161 | - if (Is::settled($promise)) { |
|
162 | - return; |
|
163 | - } |
|
164 | - try { |
|
165 | - if (isset($handler[$index])) { |
|
166 | - /* |
|
15 | + private $state = self::PENDING; |
|
16 | + private $result; |
|
17 | + private $cancelFn; |
|
18 | + private $waitFn; |
|
19 | + private $waitList; |
|
20 | + private $handlers = []; |
|
21 | + /** |
|
22 | + * @param callable $waitFn Fn that when invoked resolves the promise. |
|
23 | + * @param callable $cancelFn Fn that when invoked cancels the promise. |
|
24 | + */ |
|
25 | + public function __construct(?callable $waitFn = null, ?callable $cancelFn = null) |
|
26 | + { |
|
27 | + $this->waitFn = $waitFn; |
|
28 | + $this->cancelFn = $cancelFn; |
|
29 | + } |
|
30 | + public function then(?callable $onFulfilled = null, ?callable $onRejected = null) : PromiseInterface |
|
31 | + { |
|
32 | + if ($this->state === self::PENDING) { |
|
33 | + $p = new Promise(null, [$this, 'cancel']); |
|
34 | + $this->handlers[] = [$p, $onFulfilled, $onRejected]; |
|
35 | + $p->waitList = $this->waitList; |
|
36 | + $p->waitList[] = $this; |
|
37 | + return $p; |
|
38 | + } |
|
39 | + // Return a fulfilled promise and immediately invoke any callbacks. |
|
40 | + if ($this->state === self::FULFILLED) { |
|
41 | + $promise = Create::promiseFor($this->result); |
|
42 | + return $onFulfilled ? $promise->then($onFulfilled) : $promise; |
|
43 | + } |
|
44 | + // It's either cancelled or rejected, so return a rejected promise |
|
45 | + // and immediately invoke any callbacks. |
|
46 | + $rejection = Create::rejectionFor($this->result); |
|
47 | + return $onRejected ? $rejection->then(null, $onRejected) : $rejection; |
|
48 | + } |
|
49 | + public function otherwise(callable $onRejected) : PromiseInterface |
|
50 | + { |
|
51 | + return $this->then(null, $onRejected); |
|
52 | + } |
|
53 | + public function wait(bool $unwrap = \true) |
|
54 | + { |
|
55 | + $this->waitIfPending(); |
|
56 | + if ($this->result instanceof PromiseInterface) { |
|
57 | + return $this->result->wait($unwrap); |
|
58 | + } |
|
59 | + if ($unwrap) { |
|
60 | + if ($this->state === self::FULFILLED) { |
|
61 | + return $this->result; |
|
62 | + } |
|
63 | + // It's rejected so "unwrap" and throw an exception. |
|
64 | + throw Create::exceptionFor($this->result); |
|
65 | + } |
|
66 | + } |
|
67 | + public function getState() : string |
|
68 | + { |
|
69 | + return $this->state; |
|
70 | + } |
|
71 | + public function cancel() : void |
|
72 | + { |
|
73 | + if ($this->state !== self::PENDING) { |
|
74 | + return; |
|
75 | + } |
|
76 | + $this->waitFn = $this->waitList = null; |
|
77 | + if ($this->cancelFn) { |
|
78 | + $fn = $this->cancelFn; |
|
79 | + $this->cancelFn = null; |
|
80 | + try { |
|
81 | + $fn(); |
|
82 | + } catch (\Throwable $e) { |
|
83 | + $this->reject($e); |
|
84 | + } |
|
85 | + } |
|
86 | + // Reject the promise only if it wasn't rejected in a then callback. |
|
87 | + /** @psalm-suppress RedundantCondition */ |
|
88 | + if ($this->state === self::PENDING) { |
|
89 | + $this->reject(new CancellationException('Promise has been cancelled')); |
|
90 | + } |
|
91 | + } |
|
92 | + public function resolve($value) : void |
|
93 | + { |
|
94 | + $this->settle(self::FULFILLED, $value); |
|
95 | + } |
|
96 | + public function reject($reason) : void |
|
97 | + { |
|
98 | + $this->settle(self::REJECTED, $reason); |
|
99 | + } |
|
100 | + private function settle(string $state, $value) : void |
|
101 | + { |
|
102 | + if ($this->state !== self::PENDING) { |
|
103 | + // Ignore calls with the same resolution. |
|
104 | + if ($state === $this->state && $value === $this->result) { |
|
105 | + return; |
|
106 | + } |
|
107 | + throw $this->state === $state ? new \LogicException("The promise is already {$state}.") : new \LogicException("Cannot change a {$this->state} promise to {$state}"); |
|
108 | + } |
|
109 | + if ($value === $this) { |
|
110 | + throw new \LogicException('Cannot fulfill or reject a promise with itself'); |
|
111 | + } |
|
112 | + // Clear out the state of the promise but stash the handlers. |
|
113 | + $this->state = $state; |
|
114 | + $this->result = $value; |
|
115 | + $handlers = $this->handlers; |
|
116 | + $this->handlers = null; |
|
117 | + $this->waitList = $this->waitFn = null; |
|
118 | + $this->cancelFn = null; |
|
119 | + if (!$handlers) { |
|
120 | + return; |
|
121 | + } |
|
122 | + // If the value was not a settled promise or a thenable, then resolve |
|
123 | + // it in the task queue using the correct ID. |
|
124 | + if (!\is_object($value) || !\method_exists($value, 'then')) { |
|
125 | + $id = $state === self::FULFILLED ? 1 : 2; |
|
126 | + // It's a success, so resolve the handlers in the queue. |
|
127 | + Utils::queue()->add(static function () use($id, $value, $handlers) : void { |
|
128 | + foreach ($handlers as $handler) { |
|
129 | + self::callHandler($id, $value, $handler); |
|
130 | + } |
|
131 | + }); |
|
132 | + } elseif ($value instanceof Promise && Is::pending($value)) { |
|
133 | + // We can just merge our handlers onto the next promise. |
|
134 | + $value->handlers = \array_merge($value->handlers, $handlers); |
|
135 | + } else { |
|
136 | + // Resolve the handlers when the forwarded promise is resolved. |
|
137 | + $value->then(static function ($value) use($handlers) : void { |
|
138 | + foreach ($handlers as $handler) { |
|
139 | + self::callHandler(1, $value, $handler); |
|
140 | + } |
|
141 | + }, static function ($reason) use($handlers) : void { |
|
142 | + foreach ($handlers as $handler) { |
|
143 | + self::callHandler(2, $reason, $handler); |
|
144 | + } |
|
145 | + }); |
|
146 | + } |
|
147 | + } |
|
148 | + /** |
|
149 | + * Call a stack of handlers using a specific callback index and value. |
|
150 | + * |
|
151 | + * @param int $index 1 (resolve) or 2 (reject). |
|
152 | + * @param mixed $value Value to pass to the callback. |
|
153 | + * @param array $handler Array of handler data (promise and callbacks). |
|
154 | + */ |
|
155 | + private static function callHandler(int $index, $value, array $handler) : void |
|
156 | + { |
|
157 | + /** @var PromiseInterface $promise */ |
|
158 | + $promise = $handler[0]; |
|
159 | + // The promise may have been cancelled or resolved before placing |
|
160 | + // this thunk in the queue. |
|
161 | + if (Is::settled($promise)) { |
|
162 | + return; |
|
163 | + } |
|
164 | + try { |
|
165 | + if (isset($handler[$index])) { |
|
166 | + /* |
|
167 | 167 | * If $f throws an exception, then $handler will be in the exception |
168 | 168 | * stack trace. Since $handler contains a reference to the callable |
169 | 169 | * itself we get a circular reference. We clear the $handler |
170 | 170 | * here to avoid that memory leak. |
171 | 171 | */ |
172 | - $f = $handler[$index]; |
|
173 | - unset($handler); |
|
174 | - $promise->resolve($f($value)); |
|
175 | - } elseif ($index === 1) { |
|
176 | - // Forward resolution values as-is. |
|
177 | - $promise->resolve($value); |
|
178 | - } else { |
|
179 | - // Forward rejections down the chain. |
|
180 | - $promise->reject($value); |
|
181 | - } |
|
182 | - } catch (\Throwable $reason) { |
|
183 | - $promise->reject($reason); |
|
184 | - } |
|
185 | - } |
|
186 | - private function waitIfPending() : void |
|
187 | - { |
|
188 | - if ($this->state !== self::PENDING) { |
|
189 | - return; |
|
190 | - } elseif ($this->waitFn) { |
|
191 | - $this->invokeWaitFn(); |
|
192 | - } elseif ($this->waitList) { |
|
193 | - $this->invokeWaitList(); |
|
194 | - } else { |
|
195 | - // If there's no wait function, then reject the promise. |
|
196 | - $this->reject('Cannot wait on a promise that has ' . 'no internal wait function. You must provide a wait ' . 'function when constructing the promise to be able to ' . 'wait on a promise.'); |
|
197 | - } |
|
198 | - Utils::queue()->run(); |
|
199 | - /** @psalm-suppress RedundantCondition */ |
|
200 | - if ($this->state === self::PENDING) { |
|
201 | - $this->reject('Invoking the wait callback did not resolve the promise'); |
|
202 | - } |
|
203 | - } |
|
204 | - private function invokeWaitFn() : void |
|
205 | - { |
|
206 | - try { |
|
207 | - $wfn = $this->waitFn; |
|
208 | - $this->waitFn = null; |
|
209 | - $wfn(\true); |
|
210 | - } catch (\Throwable $reason) { |
|
211 | - if ($this->state === self::PENDING) { |
|
212 | - // The promise has not been resolved yet, so reject the promise |
|
213 | - // with the exception. |
|
214 | - $this->reject($reason); |
|
215 | - } else { |
|
216 | - // The promise was already resolved, so there's a problem in |
|
217 | - // the application. |
|
218 | - throw $reason; |
|
219 | - } |
|
220 | - } |
|
221 | - } |
|
222 | - private function invokeWaitList() : void |
|
223 | - { |
|
224 | - $waitList = $this->waitList; |
|
225 | - $this->waitList = null; |
|
226 | - foreach ($waitList as $result) { |
|
227 | - do { |
|
228 | - $result->waitIfPending(); |
|
229 | - $result = $result->result; |
|
230 | - } while ($result instanceof Promise); |
|
231 | - if ($result instanceof PromiseInterface) { |
|
232 | - $result->wait(\false); |
|
233 | - } |
|
234 | - } |
|
235 | - } |
|
172 | + $f = $handler[$index]; |
|
173 | + unset($handler); |
|
174 | + $promise->resolve($f($value)); |
|
175 | + } elseif ($index === 1) { |
|
176 | + // Forward resolution values as-is. |
|
177 | + $promise->resolve($value); |
|
178 | + } else { |
|
179 | + // Forward rejections down the chain. |
|
180 | + $promise->reject($value); |
|
181 | + } |
|
182 | + } catch (\Throwable $reason) { |
|
183 | + $promise->reject($reason); |
|
184 | + } |
|
185 | + } |
|
186 | + private function waitIfPending() : void |
|
187 | + { |
|
188 | + if ($this->state !== self::PENDING) { |
|
189 | + return; |
|
190 | + } elseif ($this->waitFn) { |
|
191 | + $this->invokeWaitFn(); |
|
192 | + } elseif ($this->waitList) { |
|
193 | + $this->invokeWaitList(); |
|
194 | + } else { |
|
195 | + // If there's no wait function, then reject the promise. |
|
196 | + $this->reject('Cannot wait on a promise that has ' . 'no internal wait function. You must provide a wait ' . 'function when constructing the promise to be able to ' . 'wait on a promise.'); |
|
197 | + } |
|
198 | + Utils::queue()->run(); |
|
199 | + /** @psalm-suppress RedundantCondition */ |
|
200 | + if ($this->state === self::PENDING) { |
|
201 | + $this->reject('Invoking the wait callback did not resolve the promise'); |
|
202 | + } |
|
203 | + } |
|
204 | + private function invokeWaitFn() : void |
|
205 | + { |
|
206 | + try { |
|
207 | + $wfn = $this->waitFn; |
|
208 | + $this->waitFn = null; |
|
209 | + $wfn(\true); |
|
210 | + } catch (\Throwable $reason) { |
|
211 | + if ($this->state === self::PENDING) { |
|
212 | + // The promise has not been resolved yet, so reject the promise |
|
213 | + // with the exception. |
|
214 | + $this->reject($reason); |
|
215 | + } else { |
|
216 | + // The promise was already resolved, so there's a problem in |
|
217 | + // the application. |
|
218 | + throw $reason; |
|
219 | + } |
|
220 | + } |
|
221 | + } |
|
222 | + private function invokeWaitList() : void |
|
223 | + { |
|
224 | + $waitList = $this->waitList; |
|
225 | + $this->waitList = null; |
|
226 | + foreach ($waitList as $result) { |
|
227 | + do { |
|
228 | + $result->waitIfPending(); |
|
229 | + $result = $result->result; |
|
230 | + } while ($result instanceof Promise); |
|
231 | + if ($result instanceof PromiseInterface) { |
|
232 | + $result->wait(\false); |
|
233 | + } |
|
234 | + } |
|
235 | + } |
|
236 | 236 | } |