PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.5
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.5
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / vendor_prefixed / guzzlehttp / guzzle / src / Handler / CurlMultiHandler.php

CurlMultiHandler.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 28.5, at vendor_prefixed/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php

1,162 lines 55.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace YoastSEO_Vendor\GuzzleHttp\Handler;
4
5 use Closure;
6 use YoastSEO_Vendor\GuzzleHttp\Exception\RequestException;
7 use YoastSEO_Vendor\GuzzleHttp\Multiplexing;
8 use YoastSEO_Vendor\GuzzleHttp\Promise as P;
9 use YoastSEO_Vendor\GuzzleHttp\Promise\Promise;
10 use YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface;
11 use YoastSEO_Vendor\GuzzleHttp\Psr7;
12 use YoastSEO_Vendor\GuzzleHttp\TransportSharing;
13 use YoastSEO_Vendor\GuzzleHttp\Utils;
14 use YoastSEO_Vendor\Psr\Http\Message\RequestInterface;
15 /**
16 * Returns an asynchronous response using curl_multi_* functions.
17 *
18 * When using the CurlMultiHandler, custom curl options can be specified as an
19 * associative array of curl option constants mapping to values in the
20 * **curl** key of the provided request options.
21 *
22 * @final
23 */
24 class CurlMultiHandler
25 {
26 private const KNOWN_CONSTRUCTOR_OPTIONS = ['handle_factory' => \true, 'max_host_connections' => \true, 'max_total_connections' => \true, 'multiplex' => \true, 'options' => \true, 'select_timeout' => \true, 'transport_sharing' => \true];
27 private const CONNECTION_CAP_OPTIONS = ['max_host_connections' => 'CURLMOPT_MAX_HOST_CONNECTIONS', 'max_total_connections' => 'CURLMOPT_MAX_TOTAL_CONNECTIONS'];
28 /**
29 * cURL options that isolate a transfer from foreign proxy tunnel
30 * connections. Failing to apply either one would fall open into
31 * credential-bearing connection reuse.
32 */
33 private const PROXY_TUNNEL_ISOLATION_OPTIONS = ['CURLOPT_FRESH_CONNECT', 'CURLOPT_FORBID_REUSE'];
34 /**
35 * @var CurlFactoryInterface
36 */
37 private $factory;
38 /**
39 * @var CurlShareHandleState|null
40 */
41 private $shareHandleState;
42 /**
43 * @var int
44 */
45 private $selectTimeout;
46 /**
47 * @var int Will be higher than 0 when `curl_multi_exec` is still running.
48 */
49 private $active = 0;
50 /**
51 * @var array Request entry handles, indexed by handle id in `addRequest`.
52 *
53 * @see CurlMultiHandler::addRequest
54 */
55 private $handles = [];
56 /**
57 * @var array<int, float> An array of delay times, indexed by handle id in `addRequest`.
58 *
59 * @see CurlMultiHandler::addRequest
60 */
61 private $delays = [];
62 /**
63 * @var array<mixed> An associative array of CURLMOPT_* options and corresponding values for curl_multi_setopt()
64 */
65 private $options = [];
66 /**
67 * @var array<int, true> Native options derived from first-class
68 * constructor options; failing to apply one is an
69 * error rather than a compatibility warning.
70 */
71 private $requiredOptions = [];
72 /**
73 * @var bool Whether any connection cap constructor option was applied
74 */
75 private $connectionCapsApplied = \false;
76 /**
77 * @var bool Whether the "multiplex" constructor option disabled
78 * multiplexing on this handler's multi handle
79 */
80 private $multiplexDisabled = \false;
81 /**
82 * @var bool Whether a custom "handle_factory" constructor option supplies
83 * the easy handles
84 */
85 private $customHandleFactory = \false;
86 /** @var resource|\CurlMultiHandle */
87 private $_mh;
88 /**
89 * @var int Depth of nested guarded native operations (execution and
90 * handle removal, both of which can run user callbacks). A
91 * callback can re-enter tick(), and the nested frame must not
92 * clear the outer frame's guard; deferred work stays parked
93 * until the outermost frame unwinds.
94 */
95 private $multiExecDepth = 0;
96 /**
97 * @var bool Guards finishDeferredWork() against re-entry from the
98 * guarded native removals it performs while flushing.
99 */
100 private $finishingDeferredWork = \false;
101 /**
102 * @var array<int, array{easy: EasyHandle, attached: bool}>
103 */
104 private $deferredCancels = [];
105 /**
106 * @var array<int, object|null> Wait tokens of requests created from inside
107 * a cURL callback, keyed by handle id; native
108 * attachment is deferred until the outermost
109 * native execution unwinds.
110 */
111 private $deferredAdds = [];
112 /**
113 * @var string|null Owner signature of the proxy tunnels the multi handle's
114 * connection cache may hold
115 */
116 private $proxyTunnelOwner;
117 /** @var array<string, int> Count of attached transfers per proxy tunnel signature. */
118 private $activeProxyTunnelSignatures = [];
119 /** @var array<int, string> Maps an attached handle id to its proxy tunnel signature. */
120 private $activeProxyTunnelHandles = [];
121 /**
122 * @var int Depth of nested processMessages() calls. Guards against
123 * multi-handle recreation re-entrancy from processMessages (a
124 * retried transfer re-invokes the handler); a depth is tracked
125 * because a completion callback can re-enter tick().
126 */
127 private $messageProcessingDepth = 0;
128 /**
129 * This handler accepts the following options:
130 *
131 * - handle_factory: An optional factory used to create curl handles
132 * - transport_sharing: Optional transport sharing mode.
133 * - select_timeout: Optional timeout (in seconds) to block before timing
134 * out while selecting curl handles. Defaults to 1 second.
135 * - max_host_connections: Optional maximum concurrent connections per host.
136 * - max_total_connections: Optional maximum concurrent connections overall.
137 * - multiplex: Optional Multiplexing::NONE to disallow multiplexing on
138 * this handler's multi handle. The eager, wait, and required modes are
139 * request options, not handler options; Multiplexing::NONE is also
140 * conditionally accepted as a request option value.
141 * - options: An associative array of CURLMOPT_* options and
142 * corresponding values for curl_multi_setopt()
143 */
144 public function __construct(array $options = [])
145 {
146 foreach ($options as $name => $_) {
147 if (!isset(self::KNOWN_CONSTRUCTOR_OPTIONS[$name])) {
148 \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/guzzle', '7.14', \sprintf('The "%s" CurlMultiHandler constructor option is unknown; guzzlehttp/guzzle 8.0 will reject unknown constructor options.', (string) $name));
149 }
150 }
151 $handlerMultiplex = $options['multiplex'] ?? null;
152 if (null !== $handlerMultiplex && \YoastSEO_Vendor\GuzzleHttp\Multiplexing::NONE !== $handlerMultiplex) {
153 if (\in_array($handlerMultiplex, [\YoastSEO_Vendor\GuzzleHttp\Multiplexing::EAGER, \YoastSEO_Vendor\GuzzleHttp\Multiplexing::WAIT, \YoastSEO_Vendor\GuzzleHttp\Multiplexing::REQUIRE_EAGER, \YoastSEO_Vendor\GuzzleHttp\Multiplexing::REQUIRE_WAIT], \true)) {
154 throw new \InvalidArgumentException('The "multiplex" CurlMultiHandler option only accepts Multiplexing::NONE; the eager, wait, and required modes are request options.');
155 }
156 throw new \InvalidArgumentException(\sprintf('The "multiplex" CurlMultiHandler option must be null or Multiplexing::NONE; received %s.', \get_debug_type($handlerMultiplex)));
157 }
158 $this->multiplexDisabled = null !== $handlerMultiplex;
159 if ($this->multiplexDisabled && !\defined('CURLMOPT_PIPELINING')) {
160 // ext-curl only defines the constant when built against libcurl
161 // 7.16 or newer headers, and such builds compile out the matching
162 // curl_multi_setopt() case, so the guarantee cannot be applied.
163 throw new \InvalidArgumentException('The "multiplex" CurlMultiHandler option requires CURLMOPT_PIPELINING, but it is not available in the installed PHP cURL extension.');
164 }
165 \YoastSEO_Vendor\GuzzleHttp\Handler\CurlShareHandleState::assertNoRequiredSharingCustomFactoryConflict($options, 'CurlMultiHandler');
166 $transportSharing = $options['transport_sharing'] ?? null;
167 $sharingMode = \YoastSEO_Vendor\GuzzleHttp\Handler\CurlShareHandleState::normalizeMode($transportSharing, 'transport_sharing');
168 if (\array_key_exists('handle_factory', $options) && $options['handle_factory'] !== null) {
169 $this->shareHandleState = null;
170 $this->factory = $options['handle_factory'];
171 $this->customHandleFactory = \true;
172 } else {
173 $this->shareHandleState = $sharingMode !== \YoastSEO_Vendor\GuzzleHttp\TransportSharing::NONE ? \YoastSEO_Vendor\GuzzleHttp\Handler\CurlShareHandleState::fromOption($transportSharing) : null;
174 $this->factory = $this->shareHandleState !== null ? new \YoastSEO_Vendor\GuzzleHttp\Handler\CurlFactory(50, $this->shareHandleState->mode, $this->shareHandleState) : new \YoastSEO_Vendor\GuzzleHttp\Handler\CurlFactory(50);
175 }
176 if (isset($options['select_timeout'])) {
177 $selectTimeout = $options['select_timeout'];
178 if (!\is_int($selectTimeout) && !\is_float($selectTimeout) && (!\is_string($selectTimeout) || !\is_numeric($selectTimeout))) {
179 \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/guzzle', '7.14', 'Passing a non-numeric "select_timeout" CurlMultiHandler option is deprecated; guzzlehttp/guzzle 8.0 will reject it.');
180 } else {
181 $seconds = (float) $selectTimeout;
182 if (!\is_finite($seconds) || $seconds < 0 || $seconds > 0 && (int) ($seconds * 1000) === 0) {
183 \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/guzzle', '7.14', 'Passing a "select_timeout" CurlMultiHandler option that is not 0 or greater than or equal to 0.001 seconds is deprecated; guzzlehttp/guzzle 8.0 will reject it.');
184 }
185 }
186 $this->selectTimeout = $selectTimeout;
187 } elseif ($selectTimeout = \YoastSEO_Vendor\GuzzleHttp\Utils::getenv('GUZZLE_CURL_SELECT_TIMEOUT')) {
188 \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/guzzle', '7.2', 'The GUZZLE_CURL_SELECT_TIMEOUT environment variable is deprecated; use the "select_timeout" option instead.');
189 $this->selectTimeout = (int) $selectTimeout;
190 } else {
191 $this->selectTimeout = 1;
192 }
193 $multiOptions = $options['options'] ?? [];
194 if (\is_array($multiOptions)) {
195 self::rejectConnectionCapOptionConflicts($options, $multiOptions);
196 if ($this->multiplexDisabled && \array_key_exists(\CURLMOPT_PIPELINING, $multiOptions)) {
197 // Key presence alone conflicts, even with an agreeing value:
198 // the named option is the single multiplexing authority.
199 throw new \InvalidArgumentException('multiplex conflicts with a CURLMOPT_PIPELINING entry in the "options" array.');
200 }
201 self::triggerConflictingCurlMultiOptionDeprecations($multiOptions);
202 } elseif (self::hasConnectionCapOption($options)) {
203 throw new \InvalidArgumentException('options must be an array of cURL multi options when using connection cap options.');
204 } elseif ($this->multiplexDisabled) {
205 throw new \InvalidArgumentException('options must be an array of cURL multi options when using the "multiplex" option.');
206 }
207 $this->options = $multiOptions;
208 if (\is_array($multiOptions)) {
209 $this->addConnectionCapOptions($options);
210 if ($this->multiplexDisabled) {
211 // CURLPIPE_NOTHING; the constant itself needs libcurl 7.43
212 // headers, newer than the oldest supported runtimes. The
213 // option is required: a handler-wide guarantee must fail
214 // closed rather than warn like the deprecated raw options.
215 $this->options[\CURLMOPT_PIPELINING] = 0;
216 $this->requiredOptions[\CURLMOPT_PIPELINING] = \true;
217 }
218 }
219 // unsetting the property forces the first access to go through
220 // __get().
221 unset($this->_mh);
222 }
223 /**
224 * @param string $name
225 *
226 * @return resource|\CurlMultiHandle
227 *
228 * @throws \BadMethodCallException when another field as `_mh` will be gotten
229 * @throws \RuntimeException when curl can not initialize a multi handle
230 * @throws \InvalidArgumentException when a required cURL multi option cannot be applied
231 */
232 public function __get($name)
233 {
234 if ($name !== '_mh') {
235 throw new \BadMethodCallException("Can not get other property as '_mh'.");
236 }
237 $multiHandle = \curl_multi_init();
238 if (\false === $multiHandle) {
239 throw new \RuntimeException('Can not initialize curl multi handle.');
240 }
241 try {
242 foreach ($this->options as $option => $value) {
243 if (\true === @\curl_multi_setopt($multiHandle, $option, $value)) {
244 continue;
245 }
246 if (isset($this->requiredOptions[$option])) {
247 // A first-class option such as a connection cap must
248 // never be silently dropped.
249 throw new \InvalidArgumentException(\sprintf('Unable to apply the cURL multi option %s; it was rejected by the runtime libcurl.', self::formatCurlMultiOption($option)));
250 }
251 \trigger_error(\sprintf('Unable to apply the cURL multi option %s; it was ignored by the runtime libcurl.', self::formatCurlMultiOption($option)), \E_USER_WARNING);
252 }
253 } catch (\Throwable $e) {
254 // Do not publish a partially configured handle; a later access
255 // retries the initialization from scratch.
256 try {
257 \curl_multi_close($multiHandle);
258 } catch (\Throwable $ignored) {
259 // Preserve the original failure.
260 }
261 throw $e;
262 }
263 $this->_mh = $multiHandle;
264 return $this->_mh;
265 }
266 public function __destruct()
267 {
268 if (isset($this->_mh)) {
269 try {
270 \curl_multi_close($this->_mh);
271 } catch (\Throwable $e) {
272 // Destructors must not throw.
273 } finally {
274 unset($this->_mh);
275 }
276 }
277 }
278 public function __invoke(\YoastSEO_Vendor\Psr\Http\Message\RequestInterface $request, array $options) : \YoastSEO_Vendor\GuzzleHttp\Promise\PromiseInterface
279 {
280 \YoastSEO_Vendor\GuzzleHttp\Handler\HostValidator::assertRequestHost($request);
281 if ($this->connectionCapsApplied && \defined('CURLOPT_SHARE') && isset($options['curl']) && \is_array($options['curl']) && \array_key_exists((int) \constant('CURLOPT_SHARE'), $options['curl'])) {
282 // Key presence alone conflicts: Guzzle cannot verify that a
283 // caller-managed shared connection pool honors the caps.
284 throw new \InvalidArgumentException('The request-level CURLOPT_SHARE cURL option cannot be combined with CurlMultiHandler connection cap options because Guzzle cannot verify that an external shared connection pool honors cURL multi connection caps.');
285 }
286 $easy = $this->factory->create($request, $options);
287 try {
288 $this->rejectMultiplexPipeliningConflict($easy, $options);
289 $this->applyMultiplexNone($easy, $options);
290 $this->applyProxyTunnelOwnership($easy);
291 } catch (\Throwable $e) {
292 try {
293 $this->factory->release($easy);
294 } catch (\Throwable $releaseFailure) {
295 // Preserve the original failure.
296 }
297 throw $e;
298 }
299 $id = (int) $easy->handle;
300 $waitToken = new \stdClass();
301 $promise = new \YoastSEO_Vendor\GuzzleHttp\Promise\Promise(function () use($id, $waitToken) : void {
302 if ($this->multiExecDepth > 0) {
303 // Waiting cannot drive native cURL while a callback has
304 // the multi handle busy; fail the wait promptly instead
305 // of self-deadlocking.
306 $this->failNestedWait($id, $waitToken);
307 return;
308 }
309 $this->executeUntil($id, $waitToken);
310 }, function () use($id, $waitToken) {
311 return $this->cancel($id, $waitToken);
312 });
313 $entry = ['easy' => $easy, 'deferred' => $promise, 'wait_token' => $waitToken];
314 try {
315 $this->addRequest($entry);
316 } catch (\Throwable $e) {
317 throw $this->discardPendingRequest($id, $entry, $e);
318 }
319 return $promise;
320 }
321 /**
322 * The "multiplex" request option sets CURLOPT_PIPEWAIT, which libcurl
323 * ignores entirely when the multi handle's CURLMOPT_PIPELINING option
324 * disables multiplexing, so an explicit request for multiplexing on a
325 * handler configured against it is a configuration error. The required
326 * family conflicts marker-independently: a required guarantee on a handler
327 * that disables multiplexing is contradictory even when the transfer would
328 * not wait. A raw CURLOPT_PIPEWAIT cURL option conflicts with every
329 * explicit mode on this handler, where waiting is operationally
330 * meaningful: whatever its value, it is a second wait/eager authority
331 * applied after the mode's own decision.
332 */
333 private function rejectMultiplexPipeliningConflict(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy, array $options) : void
334 {
335 $multiplex = $options['multiplex'] ?? null;
336 if (null === $multiplex) {
337 return;
338 }
339 if (\defined('CURLOPT_PIPEWAIT') && isset($options['curl']) && \is_array($options['curl']) && \array_key_exists((int) \constant('CURLOPT_PIPEWAIT'), $options['curl'])) {
340 // Key presence alone conflicts, and it must be rejected before
341 // the marker below is consulted: the marker reflects the final
342 // merged configuration, which the raw value has falsified.
343 throw new \InvalidArgumentException('The "multiplex" request option cannot be combined with the raw CURLOPT_PIPEWAIT cURL option on the cURL multi handler; remove the raw option.');
344 }
345 if (\YoastSEO_Vendor\GuzzleHttp\Multiplexing::WAIT === $multiplex && !$easy->usesPipewait) {
346 // Explicit wait only conflicts when the transfer would actually
347 // wait; an HTTP/1.1 wait request never sets the marker.
348 return;
349 }
350 if (!\in_array($multiplex, [\YoastSEO_Vendor\GuzzleHttp\Multiplexing::WAIT, \YoastSEO_Vendor\GuzzleHttp\Multiplexing::REQUIRE_EAGER, \YoastSEO_Vendor\GuzzleHttp\Multiplexing::REQUIRE_WAIT], \true)) {
351 return;
352 }
353 if ($this->multiplexDisabled) {
354 // Checked before the raw option: the handler wrote its own
355 // CURLMOPT_PIPELINING value when "multiplex" disabled it.
356 throw new \InvalidArgumentException('The "multiplex" request option cannot be combined with a CurlMultiHandler whose "multiplex" option is Multiplexing::NONE; remove the handler option or set the request option to "eager".');
357 }
358 if (!\is_array($this->options) || !\array_key_exists(\CURLMOPT_PIPELINING, $this->options)) {
359 // A legacy non-array "options" value is tolerated by the
360 // constructor and cannot contain the option.
361 return;
362 }
363 $pipelining = $this->options[\CURLMOPT_PIPELINING];
364 if (!\is_scalar($pipelining)) {
365 // ext-curl derives the integer mask from non-scalar values with
366 // type-dependent zval semantics, so the effective mask cannot be
367 // predicted here; require an explicit integer instead.
368 throw new \InvalidArgumentException('The CurlMultiHandler CURLMOPT_PIPELINING option must be an integer when combined with the "multiplex" request option.');
369 }
370 $multiplexBit = \defined('CURLPIPE_MULTIPLEX') ? \CURLPIPE_MULTIPLEX : 2;
371 if (((int) $pipelining & $multiplexBit) !== 0) {
372 return;
373 }
374 throw new \InvalidArgumentException('The "multiplex" request option cannot be combined with a CurlMultiHandler CURLMOPT_PIPELINING option that disables multiplexing; set CURLMOPT_PIPELINING to CURLPIPE_MULTIPLEX, remove the option, or set the "multiplex" option to "eager".');
375 }
376 /**
377 * A Multiplexing::NONE request option is a sole-use guarantee: the
378 * transfer must not share its connection with any concurrent transfer.
379 * It holds structurally on a handler whose "multiplex" option is
380 * Multiplexing::NONE, and for HTTP/1.x transfers, which never join a
381 * multiplexed connection and open connections nothing can join. An
382 * HTTP/2 request on a handler that multiplexes is rejected, as is any
383 * configuration under which the guarantee cannot be verified (custom
384 * handle factories control the native handle) or cannot be hardened
385 * (challenge-response authentication retries and Expect 417 retries
386 * re-enter connection selection as internal follows, which disarm
387 * CURLOPT_FRESH_CONNECT). A raw CURLMOPT_PIPELINING multi option, and
388 * deprecated-but-applied raw cURL options that can defeat the declared
389 * protocol version, retry through internal follows, or replace the
390 * managed header list, are rejected by key presence. On runtimes whose
391 * matcher can hand an HTTP/1.x transfer an idle multiplexed connection
392 * (below libcurl 7.77.0, and 8.11.0-8.12.1), accepted transfers force
393 * a fresh connection.
394 */
395 private function applyMultiplexNone(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy, array $options) : void
396 {
397 if (\YoastSEO_Vendor\GuzzleHttp\Multiplexing::NONE !== ($options['multiplex'] ?? null) || $this->multiplexDisabled) {
398 return;
399 }
400 if (\defined('CURLMOPT_PIPELINING') && \is_array($this->options) && \array_key_exists(\CURLMOPT_PIPELINING, $this->options)) {
401 // Key presence alone conflicts, matching the constructor's rule
402 // for the named option: raw multi options that fail to apply only
403 // warn (they are not in requiredOptions), so even an agreeing
404 // zero mask cannot prove the guarantee. is_array: legacy non-array
405 // "options" values are deprecated but still stored.
406 throw new \InvalidArgumentException('The "multiplex" request option cannot be Multiplexing::NONE alongside a raw CURLMOPT_PIPELINING cURL multi option; replace the raw option with the "multiplex" cURL multi handler option.');
407 }
408 if ($this->customHandleFactory) {
409 throw new \InvalidArgumentException('The "multiplex" request option can only be Multiplexing::NONE on a CurlMultiHandler with a custom "handle_factory" when the handler\'s own "multiplex" option is Multiplexing::NONE, because the guarantee is enforced against the native easy handle the factory controls.');
410 }
411 $version = $easy->request->getProtocolVersion();
412 if ('2' === $version || '2.0' === $version) {
413 throw new \InvalidArgumentException('The "multiplex" request option can only be Multiplexing::NONE for an HTTP/1.x request on a CurlMultiHandler that permits multiplexing; set the "multiplex" client or CurlMultiHandler constructor option to Multiplexing::NONE to disable multiplexing for every transfer, or send the request with its "version" option set to "1.1".');
414 }
415 if (isset($options['curl']) && \is_array($options['curl'])) {
416 foreach (['CURLOPT_HTTP_VERSION', 'CURLOPT_HTTPAUTH', 'CURLOPT_PROXYAUTH', 'CURLOPT_FOLLOWLOCATION', 'CURLOPT_HTTPHEADER', 'CURLOPT_ALTSVC', 'CURLOPT_ALTSVC_CTRL', 'CURLOPT_PROXYTYPE'] as $constant) {
417 if (\defined($constant) && \array_key_exists((int) \constant($constant), $options['curl'])) {
418 // Key presence alone conflicts. A raw CURLOPT_HTTP_VERSION
419 // overrides the declared version after the factory
420 // mapping, and raw alt-svc options or an HTTPS2 proxy
421 // type can put a declared-HTTP/1.x transfer on a joinable
422 // HTTP/2 connection; raw challenge-response
423 // authentication (origin 401 or proxy 407) and native
424 // redirects re-enter connection selection as internal
425 // follows, which disarm CURLOPT_FRESH_CONNECT, so the
426 // hardening below cannot cover them; a raw
427 // CURLOPT_HTTPHEADER replaces the managed header list,
428 // including the Expect suppression the check below
429 // relies on.
430 throw new \InvalidArgumentException(\sprintf('The "multiplex" request option cannot be Multiplexing::NONE combined with the raw %s cURL option on a CurlMultiHandler that permits multiplexing; remove the raw option, or set the "multiplex" client or CurlMultiHandler constructor option to Multiplexing::NONE.', $constant));
431 }
432 }
433 }
434 if (\YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::caselessContains($easy->request->getHeaderLine('Expect'), '100-continue')) {
435 // libcurl arms its Expect handling by a caseless substring scan
436 // of the header value (Curl_compareheader), so any value
437 // containing 100-continue can make a 417 response retry as an
438 // internal follow, which disarms CURLOPT_FRESH_CONNECT; requests
439 // without the header are safe because the factory suppresses
440 // libcurl's automatic Expect.
441 throw new \InvalidArgumentException('The "multiplex" request option cannot be Multiplexing::NONE for a request carrying an "Expect: 100-continue" header on a CurlMultiHandler that permits multiplexing; remove the explicitly supplied "Expect" header, set the "expect" request option to false to prevent it being added automatically, or set the "multiplex" client or CurlMultiHandler constructor option to Multiplexing::NONE.');
442 }
443 if (\YoastSEO_Vendor\GuzzleHttp\Handler\CurlVersion::supportsHttpVersionReuseMatching()) {
444 return;
445 }
446 // Unqualified curl_setopt so the test bootstrap shadow records it.
447 if (\true !== \curl_setopt($easy->handle, \CURLOPT_FRESH_CONNECT, \true)) {
448 // The hardening is the guarantee on these runtimes; failing to
449 // apply it must fail closed, mirroring applyCurlOptions().
450 throw new \InvalidArgumentException('Unable to set cURL option CURLOPT_FRESH_CONNECT.');
451 }
452 }
453 /**
454 * @param array<mixed> $options
455 */
456 private static function triggerConflictingCurlMultiOptionDeprecations(array $options) : void
457 {
458 if ($options === []) {
459 return;
460 }
461 $conflictingOptions = self::conflictingCurlMultiOptions();
462 $sinceOverrides = self::conflictingCurlMultiOptionSinceOverrides();
463 foreach ($options as $option => $_) {
464 if (\array_key_exists($option, $conflictingOptions)) {
465 \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/guzzle', $sinceOverrides[$option] ?? '7.14', \sprintf('Passing %s in the cURL multi handler "options" is deprecated; guzzlehttp/guzzle 8.0 will reject this option. Use %s instead.', self::formatCurlMultiOption($option), $conflictingOptions[$option]));
466 }
467 }
468 }
469 /**
470 * @return array<int, string>
471 */
472 private static function conflictingCurlMultiOptionSinceOverrides() : array
473 {
474 if (!\defined('CURLMOPT_PIPELINING')) {
475 // Matches conflictingCurlMultiOptions(): ext-curl builds against
476 // pre-7.16 libcurl headers do not define the constant.
477 return [];
478 }
479 return [\CURLMOPT_PIPELINING => '7.15'];
480 }
481 /**
482 * @param array<mixed> $options
483 */
484 private static function hasConnectionCapOption(array $options) : bool
485 {
486 foreach (self::CONNECTION_CAP_OPTIONS as $name => $_) {
487 if (($options[$name] ?? null) !== null) {
488 return \true;
489 }
490 }
491 return \false;
492 }
493 /**
494 * @param array<mixed> $constructorOptions
495 * @param array<mixed> $multiOptions
496 */
497 private static function rejectConnectionCapOptionConflicts(array $constructorOptions, array $multiOptions) : void
498 {
499 foreach (self::CONNECTION_CAP_OPTIONS as $name => $constant) {
500 if (($constructorOptions[$name] ?? null) === null || !\defined($constant)) {
501 continue;
502 }
503 $option = \constant($constant);
504 if (\array_key_exists($option, $multiOptions)) {
505 throw new \InvalidArgumentException(\sprintf('%s conflicts with a %s entry in the "options" array.', $name, $constant));
506 }
507 }
508 }
509 /**
510 * @param array<mixed> $options
511 */
512 private function addConnectionCapOptions(array $options) : void
513 {
514 foreach (self::CONNECTION_CAP_OPTIONS as $name => $constant) {
515 $value = $options[$name] ?? null;
516 if ($value === null) {
517 continue;
518 }
519 if (!\is_int($value) || $value < 1) {
520 throw new \InvalidArgumentException(\sprintf('%s must be a positive integer.', $name));
521 }
522 \YoastSEO_Vendor\GuzzleHttp\Handler\CurlVersion::ensureConnectionCapsSupported($name);
523 $option = \constant($constant);
524 if (\array_key_exists($option, $this->options)) {
525 throw new \InvalidArgumentException(\sprintf('%s conflicts with a %s entry in the "options" array.', $name, $constant));
526 }
527 $this->options[$option] = $value;
528 $this->requiredOptions[$option] = \true;
529 $this->connectionCapsApplied = \true;
530 }
531 }
532 /**
533 * @param int|string $option
534 */
535 private static function formatCurlMultiOption($option) : string
536 {
537 if (!\is_int($option)) {
538 return \sprintf('"%s"', $option);
539 }
540 static $names = null;
541 if (null === $names) {
542 $names = [];
543 foreach (\get_defined_constants(\true)['curl'] ?? [] as $name => $value) {
544 if (\is_int($value) && \strpos($name, 'CURLMOPT_') === 0 && !isset($names[$value])) {
545 $names[$value] = $name;
546 }
547 }
548 }
549 if (isset($names[$option])) {
550 return \sprintf('%s (%d)', $names[$option], $option);
551 }
552 return (string) $option;
553 }
554 /**
555 * @return array<int, string>
556 */
557 private static function conflictingCurlMultiOptions() : array
558 {
559 static $options = null;
560 if ($options !== null) {
561 return $options;
562 }
563 $options = [];
564 self::addConflictingCurlMultiOption($options, 'CURLMOPT_MAX_HOST_CONNECTIONS', 'the "max_host_connections" client option or cURL multi handler option');
565 self::addConflictingCurlMultiOption($options, 'CURLMOPT_MAX_TOTAL_CONNECTIONS', 'the "max_total_connections" client option or cURL multi handler option');
566 self::addConflictingCurlMultiOption($options, 'CURLMOPT_PIPELINING', 'Multiplexing::NONE via the "multiplex" cURL multi handler or client option to disable multiplexing, or remove the raw option for the runtime default (multiplexing defaults on from libcurl 7.62, except 7.65.0 and 7.65.1)');
567 return $options;
568 }
569 /**
570 * @param array<int, string> $options
571 */
572 private static function addConflictingCurlMultiOption(array &$options, string $constant, string $replacement) : void
573 {
574 if (!\defined($constant)) {
575 return;
576 }
577 $value = \constant($constant);
578 if (\is_int($value)) {
579 $options[$value] = $replacement;
580 }
581 }
582 /**
583 * Isolates the connection cache when the request's proxy tunnel section
584 * differs from the one the multi handle's cache may already hold.
585 */
586 private function applyProxyTunnelOwnership(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy) : void
587 {
588 $signature = $easy->proxyTunnelSignature;
589 if ($signature === null || $signature === $this->proxyTunnelOwner) {
590 return;
591 }
592 if ($this->proxyTunnelOwner === null) {
593 // No in-domain transfer has ever run on this multi handle: latch
594 // the owner without destroying pooled direct connections.
595 $this->proxyTunnelOwner = $signature;
596 return;
597 }
598 if ($this->handles === [] && 0 === $this->multiExecDepth && 0 === $this->messageProcessingDepth && $this->deferredCancels === []) {
599 // Idle: hand the connection cache over by recreating the multi
600 // handle (unsetting re-arms the lazy __get initializer, which
601 // re-applies the CURLMOPT_* options).
602 if (isset($this->_mh)) {
603 \curl_multi_close($this->_mh);
604 unset($this->_mh);
605 }
606 $this->proxyTunnelOwner = $signature;
607 return;
608 }
609 // Busy: isolate this transfer from the owner's pooled tunnels.
610 $this->isolateProxyTunnelTransfer($easy);
611 }
612 private function addCurlHandle(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy) : void
613 {
614 $this->isolateFromForeignActiveProxyTunnel($easy);
615 // Unqualified curl_multi_add_handle so the test bootstrap shadow can
616 // override the result.
617 $result = \curl_multi_add_handle($this->_mh, $easy->handle);
618 if (\CURLM_OK !== $result) {
619 if (\PHP_VERSION_ID < 80226 || \PHP_VERSION_ID >= 80300 && \PHP_VERSION_ID < 80314) {
620 // Before PHP 8.2.26 and 8.3.14, ext-curl kept the easy handle
621 // in its multi bookkeeping even when the native add failed
622 // (https://github.com/php/php-src/pull/16302); remove it so
623 // the handle can be pooled or closed safely.
624 \curl_multi_remove_handle($this->_mh, $easy->handle);
625 }
626 throw new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException(\sprintf('Unable to add the cURL handle to the cURL multi handler: %s (%d).', (string) \curl_multi_strerror($result), $result), $easy->request);
627 }
628 $this->markProxyTunnelActive($easy);
629 $id = (int) $easy->handle;
630 if (isset($this->handles[$id])) {
631 $this->handles[$id]['attached'] = \true;
632 }
633 }
634 /**
635 * @param resource|\CurlHandle $handle
636 */
637 private function removeCompletedHandleFromMulti(int $id, $handle) : void
638 {
639 $this->removeHandleFromMulti($handle);
640 $this->unmarkProxyTunnelActiveById($id);
641 }
642 /**
643 * Removes a transfer from the multi handle under the native execution
644 * guard: removing a still-running transfer performs a final progress
645 * update that can run a user progress callback.
646 *
647 * @param resource|\CurlHandle $handle
648 */
649 private function removeHandleFromMulti($handle) : void
650 {
651 ++$this->multiExecDepth;
652 try {
653 \curl_multi_remove_handle($this->_mh, $handle);
654 } finally {
655 --$this->multiExecDepth;
656 $this->finishDeferredWork();
657 }
658 }
659 private function isolateFromForeignActiveProxyTunnel(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy) : void
660 {
661 $signature = $easy->proxyTunnelSignature;
662 if ($signature === null || $this->activeProxyTunnelSignatures === []) {
663 return;
664 }
665 if (\count($this->activeProxyTunnelSignatures) === 1 && isset($this->activeProxyTunnelSignatures[$signature])) {
666 return;
667 }
668 $this->isolateProxyTunnelTransfer($easy);
669 }
670 private function isolateProxyTunnelTransfer(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy) : void
671 {
672 foreach (self::PROXY_TUNNEL_ISOLATION_OPTIONS as $name) {
673 try {
674 // Unqualified curl_setopt so the test bootstrap shadow records it.
675 $applied = \curl_setopt($easy->handle, (int) \constant($name), \true);
676 } catch (\Throwable $e) {
677 throw new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException(self::proxyTunnelIsolationFailureMessage($name), $easy->request, null, $e);
678 }
679 if (\true !== $applied) {
680 throw new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException(self::proxyTunnelIsolationFailureMessage($name), $easy->request);
681 }
682 }
683 }
684 private static function proxyTunnelIsolationFailureMessage(string $name) : string
685 {
686 return \sprintf('Unable to apply the %s cURL option required to isolate the transfer from foreign proxy tunnel connections.', $name);
687 }
688 private function markProxyTunnelActive(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy) : void
689 {
690 $signature = $easy->proxyTunnelSignature;
691 if ($signature === null) {
692 return;
693 }
694 $id = (int) $easy->handle;
695 if (isset($this->activeProxyTunnelHandles[$id])) {
696 if ($this->activeProxyTunnelHandles[$id] === $signature) {
697 return;
698 }
699 $this->unmarkProxyTunnelActiveById($id);
700 }
701 $this->activeProxyTunnelHandles[$id] = $signature;
702 $this->activeProxyTunnelSignatures[$signature] = ($this->activeProxyTunnelSignatures[$signature] ?? 0) + 1;
703 }
704 private function unmarkProxyTunnelActive(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy) : void
705 {
706 $this->unmarkProxyTunnelActiveById((int) $easy->handle);
707 }
708 private function unmarkProxyTunnelActiveById(int $id) : void
709 {
710 if (!isset($this->activeProxyTunnelHandles[$id])) {
711 return;
712 }
713 $signature = $this->activeProxyTunnelHandles[$id];
714 unset($this->activeProxyTunnelHandles[$id]);
715 if (!isset($this->activeProxyTunnelSignatures[$signature])) {
716 return;
717 }
718 --$this->activeProxyTunnelSignatures[$signature];
719 if ($this->activeProxyTunnelSignatures[$signature] <= 0) {
720 unset($this->activeProxyTunnelSignatures[$signature]);
721 }
722 }
723 /**
724 * Ticks the curl event loop.
725 */
726 public function tick() : void
727 {
728 $this->tickFor(null, null);
729 }
730 /**
731 * Ticks the curl event loop, returning before the blocking select if the
732 * targeted transfer has settled, been canceled, or been replaced by a
733 * request that reused its native handle ID.
734 */
735 private function tickFor(?int $targetId, ?object $waitToken) : void
736 {
737 // Add any delayed handles if needed. Attachment is skipped while a
738 // callback has native execution busy; the outer frame attaches due
739 // transfers once it unwinds.
740 if ($this->delays && 0 === $this->multiExecDepth) {
741 $currentTime = \YoastSEO_Vendor\GuzzleHttp\Utils::currentTime();
742 foreach ($this->delays as $id => $delay) {
743 if ($currentTime >= $delay) {
744 $entry = $this->handles[$id];
745 unset($this->delays[$id]);
746 try {
747 $this->addCurlHandle($entry['easy']);
748 } catch (\Throwable $e) {
749 // The promise has already escaped, so reject it
750 // rather than throw.
751 $rejection = $this->discardPendingRequest($id, $entry, $e);
752 if (\YoastSEO_Vendor\GuzzleHttp\Promise\Is::pending($entry['deferred'])) {
753 $entry['deferred']->reject($rejection);
754 }
755 }
756 }
757 }
758 }
759 // Run curl_multi_exec in the queue to enable other async tasks to
760 // run, surface completions, and drain any work they queued so a
761 // ready cancellation or new transfer is not held behind the select.
762 do {
763 \YoastSEO_Vendor\GuzzleHttp\Promise\Utils::queue()->add(\Closure::fromCallable([$this, 'tickInQueue']));
764 // Step through the task queue which may add additional requests.
765 \YoastSEO_Vendor\GuzzleHttp\Promise\Utils::queue()->run();
766 if ($this->multiExecDepth > 0) {
767 // A cURL callback re-entered the handler while native
768 // execution is running; the outer frame drives native cURL
769 // once it unwinds.
770 return;
771 }
772 if (isset($this->_mh)) {
773 $this->processMessages();
774 }
775 } while (!\YoastSEO_Vendor\GuzzleHttp\Promise\Utils::queue()->isEmpty());
776 if (!isset($this->_mh)) {
777 // Nothing is attached natively (or initialization just failed);
778 // there is nothing to run and nothing to recreate the handle for.
779 return;
780 }
781 if ($targetId !== null && !$this->hasRequest($targetId, $waitToken)) {
782 return;
783 }
784 if ($this->active && \curl_multi_select($this->_mh, $this->effectiveSelectTimeout()) === -1) {
785 // Perform a usleep if a select returns -1.
786 // See: https://bugs.php.net/bug.php?id=61141
787 \usleep(250);
788 }
789 do {
790 $exec = $this->executeMulti();
791 // Prevent busy looping for slow HTTP requests.
792 if ($exec === \CURLM_CALL_MULTI_PERFORM) {
793 \curl_multi_select($this->_mh, $this->effectiveSelectTimeout());
794 }
795 } while ($exec === \CURLM_CALL_MULTI_PERFORM);
796 $this->processMessages();
797 }
798 /**
799 * Runs \curl_multi_exec() inside the event loop, to prevent busy looping
800 */
801 private function tickInQueue() : void
802 {
803 if ($this->multiExecDepth > 0) {
804 // A cURL callback re-entered the handler while native execution
805 // is running; the outer frame drives native cURL once it unwinds.
806 return;
807 }
808 if (!isset($this->_mh)) {
809 // Nothing is attached natively (or initialization just failed);
810 // there is nothing to run and nothing to recreate the handle for.
811 return;
812 }
813 $exec = $this->executeMulti();
814 if ($exec === \CURLM_CALL_MULTI_PERFORM) {
815 \curl_multi_select($this->_mh, 0);
816 \YoastSEO_Vendor\GuzzleHttp\Promise\Utils::queue()->add(\Closure::fromCallable([$this, 'tickInQueue']));
817 }
818 }
819 /**
820 * @phpstan-impure
821 */
822 private function executeMulti() : int
823 {
824 ++$this->multiExecDepth;
825 try {
826 return \curl_multi_exec($this->_mh, $this->active);
827 } finally {
828 --$this->multiExecDepth;
829 $this->finishDeferredWork();
830 }
831 }
832 /**
833 * Flushes cancels and attachments deferred while the multi handle was
834 * busy executing transfers or removing a handle.
835 */
836 private function finishDeferredWork() : void
837 {
838 if ($this->multiExecDepth > 0 || $this->finishingDeferredWork) {
839 // A nested frame (a cURL callback re-entered the handler) must
840 // not flush while an outer frame is still using the multi
841 // handle; the outermost frame flushes once it unwinds.
842 return;
843 }
844 $this->finishingDeferredWork = \true;
845 try {
846 $failure = null;
847 // Removing a cancelled transfer runs its final progress update,
848 // whose callback can cancel other transfers or create requests;
849 // drain until no deferred work remains.
850 do {
851 $this->cleanupDeferredCancels($failure);
852 $this->flushDeferredAdds();
853 } while ($this->deferredCancels !== [] || $this->deferredAdds !== []);
854 if ($failure !== null) {
855 throw $failure;
856 }
857 } finally {
858 $this->finishingDeferredWork = \false;
859 }
860 }
861 /**
862 * Runs until all outstanding connections have completed.
863 */
864 public function execute() : void
865 {
866 if ($this->multiExecDepth > 0) {
867 // Native cURL cannot be driven while a callback has it busy, so
868 // the loop would spin without ever progressing.
869 throw new \LogicException('Cannot run the cURL multi event loop from inside a cURL callback; the callback must return before transfers can progress.');
870 }
871 $queue = \YoastSEO_Vendor\GuzzleHttp\Promise\Utils::queue();
872 while ($this->handles || !$queue->isEmpty()) {
873 // If there are no transfers, then sleep for the next delay,
874 // unless ready queue work could change what is pending.
875 if (!$this->active && $this->delays && $queue->isEmpty()) {
876 \usleep($this->timeToNext());
877 }
878 $this->tick();
879 }
880 }
881 /**
882 * Runs the event loop until the given transfer has finished, so waiting
883 * on a promise does not wait for every other transfer on the handler
884 * like execute() does.
885 *
886 * The native cURL handle ID can be reused by a request created from a
887 * completion callback, so the wait token guards against waiting on an
888 * unrelated transfer that inherited the ID.
889 */
890 private function executeUntil(int $id, object $waitToken) : void
891 {
892 $queue = \YoastSEO_Vendor\GuzzleHttp\Promise\Utils::queue();
893 while ($this->hasRequest($id, $waitToken)) {
894 // If the transfer is delayed, then sleep until it is due, unless
895 // ready queue work could cancel or replace it first.
896 if (!$this->active && isset($this->delays[$id]) && $queue->isEmpty()) {
897 \usleep($this->timeToNext());
898 }
899 $this->tickFor($id, $waitToken);
900 }
901 if (!$queue->isEmpty()) {
902 $queue->run();
903 }
904 }
905 /**
906 * Checks that the request with the given handle ID is still pending and,
907 * when a wait token is given, has not been replaced by a request that
908 * reused the ID.
909 */
910 private function hasRequest(int $id, ?object $waitToken = null) : bool
911 {
912 if (!isset($this->handles[$id])) {
913 return \false;
914 }
915 return $waitToken === null || ($this->handles[$id]['wait_token'] ?? null) === $waitToken;
916 }
917 private function addRequest(array $entry) : void
918 {
919 $easy = $entry['easy'];
920 $id = (int) $easy->handle;
921 $entry['attached'] = \false;
922 $this->handles[$id] = $entry;
923 if (!empty($easy->options['delay'])) {
924 $this->delays[$id] = \YoastSEO_Vendor\GuzzleHttp\Utils::currentTime() + $easy->options['delay'] / 1000;
925 } elseif ($this->multiExecDepth > 0) {
926 // A request created from inside a cURL callback cannot be added
927 // natively while curl_multi_exec() is running; libcurl 7.59+
928 // rejects the recursive call. Attach it once the outermost
929 // native execution unwinds.
930 $this->deferredAdds[$id] = $entry['wait_token'] ?? null;
931 } else {
932 $this->addCurlHandle($easy);
933 }
934 }
935 /**
936 * Rolls back a request that can no longer be attached, releasing the
937 * easy handle exactly once and preserving the original failure.
938 *
939 * @param array{easy: EasyHandle, deferred: Promise, wait_token?: object|null, attached?: bool} $entry
940 */
941 private function discardPendingRequest(int $id, array $entry, \Throwable $failure) : \Throwable
942 {
943 unset($this->handles[$id], $this->delays[$id], $this->deferredAdds[$id]);
944 try {
945 $this->factory->release($entry['easy']);
946 } catch (\Throwable $e) {
947 // Preserve the original failure.
948 }
949 return $failure;
950 }
951 /**
952 * Fails a synchronous wait attempted from inside a cURL callback, where
953 * native execution cannot progress until the callback returns.
954 */
955 private function failNestedWait(int $id, object $token) : void
956 {
957 if (!$this->hasRequest($id, $token)) {
958 return;
959 }
960 $entry = $this->handles[$id];
961 $failure = new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException('Cannot synchronously wait for a transfer from inside a cURL callback on the same cURL multi handler; the callback must return before the transfer can progress.', $entry['easy']->request, $entry['easy']->response);
962 if (!empty($entry['attached'])) {
963 // Native removal must wait until the outermost execution unwinds.
964 unset($this->handles[$id], $this->delays[$id], $this->deferredAdds[$id]);
965 $this->deferredCancels[$id] = ['easy' => $entry['easy'], 'attached' => \true];
966 } else {
967 $this->discardPendingRequest($id, $entry, $failure);
968 }
969 $entry['deferred']->reject($failure);
970 }
971 /**
972 * Attaches requests whose native attachment was deferred because they
973 * were created from inside a cURL callback.
974 */
975 private function flushDeferredAdds() : void
976 {
977 if ($this->deferredAdds === []) {
978 return;
979 }
980 $adds = $this->deferredAdds;
981 $this->deferredAdds = [];
982 foreach ($adds as $id => $token) {
983 if (!$this->hasRequest($id, $token)) {
984 // Cancelled or replaced while the attachment was deferred.
985 continue;
986 }
987 $entry = $this->handles[$id];
988 try {
989 $this->addCurlHandle($entry['easy']);
990 } catch (\Throwable $e) {
991 // The promise has already escaped, so reject it rather than
992 // throw. User code may have settled it directly; a settled
993 // promise must not abort the rest of the snapshot.
994 $rejection = $this->discardPendingRequest($id, $entry, $e);
995 if (\YoastSEO_Vendor\GuzzleHttp\Promise\Is::pending($entry['deferred'])) {
996 $entry['deferred']->reject($rejection);
997 }
998 }
999 }
1000 }
1001 /**
1002 * Cancels a handle from sending and removes references to it.
1003 *
1004 * @param int $id Handle ID to cancel and remove.
1005 * @param object|null $waitToken Identity token that must still match the
1006 * entry when given.
1007 *
1008 * @return bool True on success, false on failure.
1009 */
1010 private function cancel($id, ?object $waitToken = null) : bool
1011 {
1012 if (!\is_int($id)) {
1013 \YoastSEO_Vendor\trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an int to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
1014 }
1015 // Cannot cancel if it has been processed or replaced by a request
1016 // that reused the native handle ID.
1017 if (!isset($this->handles[$id]) || $waitToken !== null && ($this->handles[$id]['wait_token'] ?? null) !== $waitToken) {
1018 return \false;
1019 }
1020 $entry = $this->handles[$id];
1021 $easy = $entry['easy'];
1022 $attached = !empty($entry['attached']);
1023 unset($this->delays[$id], $this->deferredAdds[$id], $this->handles[$id]);
1024 if ($this->multiExecDepth > 0) {
1025 $this->deferredCancels[$id] = ['easy' => $easy, 'attached' => $attached];
1026 return \true;
1027 }
1028 $this->cleanupCancelledHandle($easy, $attached);
1029 return \true;
1030 }
1031 private function cleanupDeferredCancels(?\Throwable &$failure) : void
1032 {
1033 if ($this->deferredCancels === []) {
1034 return;
1035 }
1036 $entries = $this->deferredCancels;
1037 $this->deferredCancels = [];
1038 foreach ($entries as $entry) {
1039 try {
1040 $this->cleanupCancelledHandle($entry['easy'], $entry['attached']);
1041 } catch (\Throwable $e) {
1042 // A final progress update can run a throwing user callback;
1043 // clean the remaining entries and surface the first failure
1044 // once the drain completes.
1045 if ($failure === null) {
1046 $failure = $e;
1047 }
1048 }
1049 }
1050 }
1051 private function cleanupCancelledHandle(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy, bool $attached) : void
1052 {
1053 $handle = $easy->handle;
1054 $failure = null;
1055 if ($attached) {
1056 try {
1057 $this->removeHandleFromMulti($handle);
1058 } catch (\Throwable $e) {
1059 // The native detach completes even when its final progress
1060 // callback throws; finish this entry before rethrowing.
1061 $failure = $e;
1062 }
1063 }
1064 $this->unmarkProxyTunnelActive($easy);
1065 if (\PHP_VERSION_ID < 80000) {
1066 try {
1067 \curl_close($handle);
1068 } catch (\Throwable $e) {
1069 // An error handler can promote the close warning; keep the
1070 // first failure.
1071 if ($failure === null) {
1072 $failure = $e;
1073 }
1074 }
1075 }
1076 if ($failure !== null) {
1077 throw $failure;
1078 }
1079 }
1080 private function processMessages() : void
1081 {
1082 // CurlFactory::finish can retry a transfer by re-invoking this handler
1083 // from inside this loop; the guard keeps that re-entry from recreating
1084 // the multi handle mid-iteration (see applyProxyTunnelOwnership). A
1085 // depth is tracked because a completion callback can re-enter tick(),
1086 // and the nested frame must not clear the outer loop's guard.
1087 ++$this->messageProcessingDepth;
1088 try {
1089 while ($done = \curl_multi_info_read($this->_mh)) {
1090 if ($done['msg'] !== \CURLMSG_DONE) {
1091 // if it's not done, then it would be premature to remove the handle. ref https://github.com/guzzle/guzzle/pull/2892#issuecomment-945150216
1092 continue;
1093 }
1094 if (!isset($done['handle'])) {
1095 // Work around a PHP issue where cancelled transfers may omit the handle.
1096 // Remove this once we no longer support PHP versions before the fix in
1097 // https://github.com/php/php-src/pull/16302.
1098 continue;
1099 }
1100 $id = (int) $done['handle'];
1101 $this->removeCompletedHandleFromMulti($id, $done['handle']);
1102 if (!isset($this->handles[$id])) {
1103 // Probably was cancelled.
1104 continue;
1105 }
1106 $entry = $this->handles[$id];
1107 unset($this->handles[$id], $this->delays[$id]);
1108 $entry['easy']->errno = $done['result'];
1109 // finish() can run completion callbacks that cancel this
1110 // promise; a settled promise must not be settled again.
1111 try {
1112 $result = \YoastSEO_Vendor\GuzzleHttp\Handler\CurlFactory::finish($this, $entry['easy'], $this->factory);
1113 } catch (\Throwable $e) {
1114 if (\YoastSEO_Vendor\GuzzleHttp\Promise\Is::pending($entry['deferred'])) {
1115 $entry['deferred']->reject($e);
1116 }
1117 continue;
1118 }
1119 if (\YoastSEO_Vendor\GuzzleHttp\Promise\Is::pending($entry['deferred'])) {
1120 $entry['deferred']->resolve($result);
1121 }
1122 }
1123 } finally {
1124 --$this->messageProcessingDepth;
1125 }
1126 }
1127 /**
1128 * Bounds a blocking select by the earliest pending request delay so a
1129 * delayed transfer becoming due does not wait out an unrelated
1130 * transfer's full select timeout.
1131 *
1132 * @return float|int
1133 */
1134 private function effectiveSelectTimeout()
1135 {
1136 if ($this->delays === []) {
1137 return $this->selectTimeout;
1138 }
1139 return \min($this->selectTimeout, $this->secondsToNext());
1140 }
1141 /**
1142 * @return float Seconds until the earliest pending delay is due
1143 */
1144 private function secondsToNext() : float
1145 {
1146 $currentTime = \YoastSEO_Vendor\GuzzleHttp\Utils::currentTime();
1147 $nextTime = \PHP_FLOAT_MAX;
1148 foreach ($this->delays as $time) {
1149 if ($time < $nextTime) {
1150 $nextTime = $time;
1151 }
1152 }
1153 return \max(0.0, $nextTime - $currentTime);
1154 }
1155 private function timeToNext() : int
1156 {
1157 // PHP_INT_MAX first: min() then returns the int operand whenever the
1158 // microseconds exceed it, so the cast never sees an oversized float.
1159 return (int) \min(\PHP_INT_MAX, $this->secondsToNext() * 1000000);
1160 }
1161 }
1162