PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.2
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.2
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.2, at vendor_prefixed/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php

1,161 lines 55.6 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 if ($this->connectionCapsApplied && \defined('CURLOPT_SHARE') && isset($options['curl']) && \is_array($options['curl']) && \array_key_exists((int) \constant('CURLOPT_SHARE'), $options['curl'])) {
281 // Key presence alone conflicts: Guzzle cannot verify that a
282 // caller-managed shared connection pool honors the caps.
283 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.');
284 }
285 $easy = $this->factory->create($request, $options);
286 try {
287 $this->rejectMultiplexPipeliningConflict($easy, $options);
288 $this->applyMultiplexNone($easy, $options);
289 $this->applyProxyTunnelOwnership($easy);
290 } catch (\Throwable $e) {
291 try {
292 $this->factory->release($easy);
293 } catch (\Throwable $releaseFailure) {
294 // Preserve the original failure.
295 }
296 throw $e;
297 }
298 $id = (int) $easy->handle;
299 $waitToken = new \stdClass();
300 $promise = new \YoastSEO_Vendor\GuzzleHttp\Promise\Promise(function () use($id, $waitToken) : void {
301 if ($this->multiExecDepth > 0) {
302 // Waiting cannot drive native cURL while a callback has
303 // the multi handle busy; fail the wait promptly instead
304 // of self-deadlocking.
305 $this->failNestedWait($id, $waitToken);
306 return;
307 }
308 $this->executeUntil($id, $waitToken);
309 }, function () use($id, $waitToken) {
310 return $this->cancel($id, $waitToken);
311 });
312 $entry = ['easy' => $easy, 'deferred' => $promise, 'wait_token' => $waitToken];
313 try {
314 $this->addRequest($entry);
315 } catch (\Throwable $e) {
316 throw $this->discardPendingRequest($id, $entry, $e);
317 }
318 return $promise;
319 }
320 /**
321 * The "multiplex" request option sets CURLOPT_PIPEWAIT, which libcurl
322 * ignores entirely when the multi handle's CURLMOPT_PIPELINING option
323 * disables multiplexing, so an explicit request for multiplexing on a
324 * handler configured against it is a configuration error. The required
325 * family conflicts marker-independently: a required guarantee on a handler
326 * that disables multiplexing is contradictory even when the transfer would
327 * not wait. A raw CURLOPT_PIPEWAIT cURL option conflicts with every
328 * explicit mode on this handler, where waiting is operationally
329 * meaningful: whatever its value, it is a second wait/eager authority
330 * applied after the mode's own decision.
331 */
332 private function rejectMultiplexPipeliningConflict(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy, array $options) : void
333 {
334 $multiplex = $options['multiplex'] ?? null;
335 if (null === $multiplex) {
336 return;
337 }
338 if (\defined('CURLOPT_PIPEWAIT') && isset($options['curl']) && \is_array($options['curl']) && \array_key_exists((int) \constant('CURLOPT_PIPEWAIT'), $options['curl'])) {
339 // Key presence alone conflicts, and it must be rejected before
340 // the marker below is consulted: the marker reflects the final
341 // merged configuration, which the raw value has falsified.
342 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.');
343 }
344 if (\YoastSEO_Vendor\GuzzleHttp\Multiplexing::WAIT === $multiplex && !$easy->usesPipewait) {
345 // Explicit wait only conflicts when the transfer would actually
346 // wait; an HTTP/1.1 wait request never sets the marker.
347 return;
348 }
349 if (!\in_array($multiplex, [\YoastSEO_Vendor\GuzzleHttp\Multiplexing::WAIT, \YoastSEO_Vendor\GuzzleHttp\Multiplexing::REQUIRE_EAGER, \YoastSEO_Vendor\GuzzleHttp\Multiplexing::REQUIRE_WAIT], \true)) {
350 return;
351 }
352 if ($this->multiplexDisabled) {
353 // Checked before the raw option: the handler wrote its own
354 // CURLMOPT_PIPELINING value when "multiplex" disabled it.
355 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".');
356 }
357 if (!\is_array($this->options) || !\array_key_exists(\CURLMOPT_PIPELINING, $this->options)) {
358 // A legacy non-array "options" value is tolerated by the
359 // constructor and cannot contain the option.
360 return;
361 }
362 $pipelining = $this->options[\CURLMOPT_PIPELINING];
363 if (!\is_scalar($pipelining)) {
364 // ext-curl derives the integer mask from non-scalar values with
365 // type-dependent zval semantics, so the effective mask cannot be
366 // predicted here; require an explicit integer instead.
367 throw new \InvalidArgumentException('The CurlMultiHandler CURLMOPT_PIPELINING option must be an integer when combined with the "multiplex" request option.');
368 }
369 $multiplexBit = \defined('CURLPIPE_MULTIPLEX') ? \CURLPIPE_MULTIPLEX : 2;
370 if (((int) $pipelining & $multiplexBit) !== 0) {
371 return;
372 }
373 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".');
374 }
375 /**
376 * A Multiplexing::NONE request option is a sole-use guarantee: the
377 * transfer must not share its connection with any concurrent transfer.
378 * It holds structurally on a handler whose "multiplex" option is
379 * Multiplexing::NONE, and for HTTP/1.x transfers, which never join a
380 * multiplexed connection and open connections nothing can join. An
381 * HTTP/2 request on a handler that multiplexes is rejected, as is any
382 * configuration under which the guarantee cannot be verified (custom
383 * handle factories control the native handle) or cannot be hardened
384 * (challenge-response authentication retries and Expect 417 retries
385 * re-enter connection selection as internal follows, which disarm
386 * CURLOPT_FRESH_CONNECT). A raw CURLMOPT_PIPELINING multi option, and
387 * deprecated-but-applied raw cURL options that can defeat the declared
388 * protocol version, retry through internal follows, or replace the
389 * managed header list, are rejected by key presence. On runtimes whose
390 * matcher can hand an HTTP/1.x transfer an idle multiplexed connection
391 * (below libcurl 7.77.0, and 8.11.0-8.12.1), accepted transfers force
392 * a fresh connection.
393 */
394 private function applyMultiplexNone(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy, array $options) : void
395 {
396 if (\YoastSEO_Vendor\GuzzleHttp\Multiplexing::NONE !== ($options['multiplex'] ?? null) || $this->multiplexDisabled) {
397 return;
398 }
399 if (\defined('CURLMOPT_PIPELINING') && \is_array($this->options) && \array_key_exists(\CURLMOPT_PIPELINING, $this->options)) {
400 // Key presence alone conflicts, matching the constructor's rule
401 // for the named option: raw multi options that fail to apply only
402 // warn (they are not in requiredOptions), so even an agreeing
403 // zero mask cannot prove the guarantee. is_array: legacy non-array
404 // "options" values are deprecated but still stored.
405 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.');
406 }
407 if ($this->customHandleFactory) {
408 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.');
409 }
410 $version = $easy->request->getProtocolVersion();
411 if ('2' === $version || '2.0' === $version) {
412 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".');
413 }
414 if (isset($options['curl']) && \is_array($options['curl'])) {
415 foreach (['CURLOPT_HTTP_VERSION', 'CURLOPT_HTTPAUTH', 'CURLOPT_PROXYAUTH', 'CURLOPT_FOLLOWLOCATION', 'CURLOPT_HTTPHEADER', 'CURLOPT_ALTSVC', 'CURLOPT_ALTSVC_CTRL', 'CURLOPT_PROXYTYPE'] as $constant) {
416 if (\defined($constant) && \array_key_exists((int) \constant($constant), $options['curl'])) {
417 // Key presence alone conflicts. A raw CURLOPT_HTTP_VERSION
418 // overrides the declared version after the factory
419 // mapping, and raw alt-svc options or an HTTPS2 proxy
420 // type can put a declared-HTTP/1.x transfer on a joinable
421 // HTTP/2 connection; raw challenge-response
422 // authentication (origin 401 or proxy 407) and native
423 // redirects re-enter connection selection as internal
424 // follows, which disarm CURLOPT_FRESH_CONNECT, so the
425 // hardening below cannot cover them; a raw
426 // CURLOPT_HTTPHEADER replaces the managed header list,
427 // including the Expect suppression the check below
428 // relies on.
429 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));
430 }
431 }
432 }
433 if (\YoastSEO_Vendor\GuzzleHttp\Psr7\Utils::caselessContains($easy->request->getHeaderLine('Expect'), '100-continue')) {
434 // libcurl arms its Expect handling by a caseless substring scan
435 // of the header value (Curl_compareheader), so any value
436 // containing 100-continue can make a 417 response retry as an
437 // internal follow, which disarms CURLOPT_FRESH_CONNECT; requests
438 // without the header are safe because the factory suppresses
439 // libcurl's automatic Expect.
440 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.');
441 }
442 if (\YoastSEO_Vendor\GuzzleHttp\Handler\CurlVersion::supportsHttpVersionReuseMatching()) {
443 return;
444 }
445 // Unqualified curl_setopt so the test bootstrap shadow records it.
446 if (\true !== \curl_setopt($easy->handle, \CURLOPT_FRESH_CONNECT, \true)) {
447 // The hardening is the guarantee on these runtimes; failing to
448 // apply it must fail closed, mirroring applyCurlOptions().
449 throw new \InvalidArgumentException('Unable to set cURL option CURLOPT_FRESH_CONNECT.');
450 }
451 }
452 /**
453 * @param array<mixed> $options
454 */
455 private static function triggerConflictingCurlMultiOptionDeprecations(array $options) : void
456 {
457 if ($options === []) {
458 return;
459 }
460 $conflictingOptions = self::conflictingCurlMultiOptions();
461 $sinceOverrides = self::conflictingCurlMultiOptionSinceOverrides();
462 foreach ($options as $option => $_) {
463 if (\array_key_exists($option, $conflictingOptions)) {
464 \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]));
465 }
466 }
467 }
468 /**
469 * @return array<int, string>
470 */
471 private static function conflictingCurlMultiOptionSinceOverrides() : array
472 {
473 if (!\defined('CURLMOPT_PIPELINING')) {
474 // Matches conflictingCurlMultiOptions(): ext-curl builds against
475 // pre-7.16 libcurl headers do not define the constant.
476 return [];
477 }
478 return [\CURLMOPT_PIPELINING => '7.15'];
479 }
480 /**
481 * @param array<mixed> $options
482 */
483 private static function hasConnectionCapOption(array $options) : bool
484 {
485 foreach (self::CONNECTION_CAP_OPTIONS as $name => $_) {
486 if (($options[$name] ?? null) !== null) {
487 return \true;
488 }
489 }
490 return \false;
491 }
492 /**
493 * @param array<mixed> $constructorOptions
494 * @param array<mixed> $multiOptions
495 */
496 private static function rejectConnectionCapOptionConflicts(array $constructorOptions, array $multiOptions) : void
497 {
498 foreach (self::CONNECTION_CAP_OPTIONS as $name => $constant) {
499 if (($constructorOptions[$name] ?? null) === null || !\defined($constant)) {
500 continue;
501 }
502 $option = \constant($constant);
503 if (\array_key_exists($option, $multiOptions)) {
504 throw new \InvalidArgumentException(\sprintf('%s conflicts with a %s entry in the "options" array.', $name, $constant));
505 }
506 }
507 }
508 /**
509 * @param array<mixed> $options
510 */
511 private function addConnectionCapOptions(array $options) : void
512 {
513 foreach (self::CONNECTION_CAP_OPTIONS as $name => $constant) {
514 $value = $options[$name] ?? null;
515 if ($value === null) {
516 continue;
517 }
518 if (!\is_int($value) || $value < 1) {
519 throw new \InvalidArgumentException(\sprintf('%s must be a positive integer.', $name));
520 }
521 \YoastSEO_Vendor\GuzzleHttp\Handler\CurlVersion::ensureConnectionCapsSupported($name);
522 $option = \constant($constant);
523 if (\array_key_exists($option, $this->options)) {
524 throw new \InvalidArgumentException(\sprintf('%s conflicts with a %s entry in the "options" array.', $name, $constant));
525 }
526 $this->options[$option] = $value;
527 $this->requiredOptions[$option] = \true;
528 $this->connectionCapsApplied = \true;
529 }
530 }
531 /**
532 * @param int|string $option
533 */
534 private static function formatCurlMultiOption($option) : string
535 {
536 if (!\is_int($option)) {
537 return \sprintf('"%s"', $option);
538 }
539 static $names = null;
540 if (null === $names) {
541 $names = [];
542 foreach (\get_defined_constants(\true)['curl'] ?? [] as $name => $value) {
543 if (\is_int($value) && \strpos($name, 'CURLMOPT_') === 0 && !isset($names[$value])) {
544 $names[$value] = $name;
545 }
546 }
547 }
548 if (isset($names[$option])) {
549 return \sprintf('%s (%d)', $names[$option], $option);
550 }
551 return (string) $option;
552 }
553 /**
554 * @return array<int, string>
555 */
556 private static function conflictingCurlMultiOptions() : array
557 {
558 static $options = null;
559 if ($options !== null) {
560 return $options;
561 }
562 $options = [];
563 self::addConflictingCurlMultiOption($options, 'CURLMOPT_MAX_HOST_CONNECTIONS', 'the "max_host_connections" client option or cURL multi handler option');
564 self::addConflictingCurlMultiOption($options, 'CURLMOPT_MAX_TOTAL_CONNECTIONS', 'the "max_total_connections" client option or cURL multi handler option');
565 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)');
566 return $options;
567 }
568 /**
569 * @param array<int, string> $options
570 */
571 private static function addConflictingCurlMultiOption(array &$options, string $constant, string $replacement) : void
572 {
573 if (!\defined($constant)) {
574 return;
575 }
576 $value = \constant($constant);
577 if (\is_int($value)) {
578 $options[$value] = $replacement;
579 }
580 }
581 /**
582 * Isolates the connection cache when the request's proxy tunnel section
583 * differs from the one the multi handle's cache may already hold.
584 */
585 private function applyProxyTunnelOwnership(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy) : void
586 {
587 $signature = $easy->proxyTunnelSignature;
588 if ($signature === null || $signature === $this->proxyTunnelOwner) {
589 return;
590 }
591 if ($this->proxyTunnelOwner === null) {
592 // No in-domain transfer has ever run on this multi handle: latch
593 // the owner without destroying pooled direct connections.
594 $this->proxyTunnelOwner = $signature;
595 return;
596 }
597 if ($this->handles === [] && 0 === $this->multiExecDepth && 0 === $this->messageProcessingDepth && $this->deferredCancels === []) {
598 // Idle: hand the connection cache over by recreating the multi
599 // handle (unsetting re-arms the lazy __get initializer, which
600 // re-applies the CURLMOPT_* options).
601 if (isset($this->_mh)) {
602 \curl_multi_close($this->_mh);
603 unset($this->_mh);
604 }
605 $this->proxyTunnelOwner = $signature;
606 return;
607 }
608 // Busy: isolate this transfer from the owner's pooled tunnels.
609 $this->isolateProxyTunnelTransfer($easy);
610 }
611 private function addCurlHandle(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy) : void
612 {
613 $this->isolateFromForeignActiveProxyTunnel($easy);
614 // Unqualified curl_multi_add_handle so the test bootstrap shadow can
615 // override the result.
616 $result = \curl_multi_add_handle($this->_mh, $easy->handle);
617 if (\CURLM_OK !== $result) {
618 if (\PHP_VERSION_ID < 80226 || \PHP_VERSION_ID >= 80300 && \PHP_VERSION_ID < 80314) {
619 // Before PHP 8.2.26 and 8.3.14, ext-curl kept the easy handle
620 // in its multi bookkeeping even when the native add failed
621 // (https://github.com/php/php-src/pull/16302); remove it so
622 // the handle can be pooled or closed safely.
623 \curl_multi_remove_handle($this->_mh, $easy->handle);
624 }
625 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);
626 }
627 $this->markProxyTunnelActive($easy);
628 $id = (int) $easy->handle;
629 if (isset($this->handles[$id])) {
630 $this->handles[$id]['attached'] = \true;
631 }
632 }
633 /**
634 * @param resource|\CurlHandle $handle
635 */
636 private function removeCompletedHandleFromMulti(int $id, $handle) : void
637 {
638 $this->removeHandleFromMulti($handle);
639 $this->unmarkProxyTunnelActiveById($id);
640 }
641 /**
642 * Removes a transfer from the multi handle under the native execution
643 * guard: removing a still-running transfer performs a final progress
644 * update that can run a user progress callback.
645 *
646 * @param resource|\CurlHandle $handle
647 */
648 private function removeHandleFromMulti($handle) : void
649 {
650 ++$this->multiExecDepth;
651 try {
652 \curl_multi_remove_handle($this->_mh, $handle);
653 } finally {
654 --$this->multiExecDepth;
655 $this->finishDeferredWork();
656 }
657 }
658 private function isolateFromForeignActiveProxyTunnel(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy) : void
659 {
660 $signature = $easy->proxyTunnelSignature;
661 if ($signature === null || $this->activeProxyTunnelSignatures === []) {
662 return;
663 }
664 if (\count($this->activeProxyTunnelSignatures) === 1 && isset($this->activeProxyTunnelSignatures[$signature])) {
665 return;
666 }
667 $this->isolateProxyTunnelTransfer($easy);
668 }
669 private function isolateProxyTunnelTransfer(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy) : void
670 {
671 foreach (self::PROXY_TUNNEL_ISOLATION_OPTIONS as $name) {
672 try {
673 // Unqualified curl_setopt so the test bootstrap shadow records it.
674 $applied = \curl_setopt($easy->handle, (int) \constant($name), \true);
675 } catch (\Throwable $e) {
676 throw new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException(self::proxyTunnelIsolationFailureMessage($name), $easy->request, null, $e);
677 }
678 if (\true !== $applied) {
679 throw new \YoastSEO_Vendor\GuzzleHttp\Exception\RequestException(self::proxyTunnelIsolationFailureMessage($name), $easy->request);
680 }
681 }
682 }
683 private static function proxyTunnelIsolationFailureMessage(string $name) : string
684 {
685 return \sprintf('Unable to apply the %s cURL option required to isolate the transfer from foreign proxy tunnel connections.', $name);
686 }
687 private function markProxyTunnelActive(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy) : void
688 {
689 $signature = $easy->proxyTunnelSignature;
690 if ($signature === null) {
691 return;
692 }
693 $id = (int) $easy->handle;
694 if (isset($this->activeProxyTunnelHandles[$id])) {
695 if ($this->activeProxyTunnelHandles[$id] === $signature) {
696 return;
697 }
698 $this->unmarkProxyTunnelActiveById($id);
699 }
700 $this->activeProxyTunnelHandles[$id] = $signature;
701 $this->activeProxyTunnelSignatures[$signature] = ($this->activeProxyTunnelSignatures[$signature] ?? 0) + 1;
702 }
703 private function unmarkProxyTunnelActive(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy) : void
704 {
705 $this->unmarkProxyTunnelActiveById((int) $easy->handle);
706 }
707 private function unmarkProxyTunnelActiveById(int $id) : void
708 {
709 if (!isset($this->activeProxyTunnelHandles[$id])) {
710 return;
711 }
712 $signature = $this->activeProxyTunnelHandles[$id];
713 unset($this->activeProxyTunnelHandles[$id]);
714 if (!isset($this->activeProxyTunnelSignatures[$signature])) {
715 return;
716 }
717 --$this->activeProxyTunnelSignatures[$signature];
718 if ($this->activeProxyTunnelSignatures[$signature] <= 0) {
719 unset($this->activeProxyTunnelSignatures[$signature]);
720 }
721 }
722 /**
723 * Ticks the curl event loop.
724 */
725 public function tick() : void
726 {
727 $this->tickFor(null, null);
728 }
729 /**
730 * Ticks the curl event loop, returning before the blocking select if the
731 * targeted transfer has settled, been canceled, or been replaced by a
732 * request that reused its native handle ID.
733 */
734 private function tickFor(?int $targetId, ?object $waitToken) : void
735 {
736 // Add any delayed handles if needed. Attachment is skipped while a
737 // callback has native execution busy; the outer frame attaches due
738 // transfers once it unwinds.
739 if ($this->delays && 0 === $this->multiExecDepth) {
740 $currentTime = \YoastSEO_Vendor\GuzzleHttp\Utils::currentTime();
741 foreach ($this->delays as $id => $delay) {
742 if ($currentTime >= $delay) {
743 $entry = $this->handles[$id];
744 unset($this->delays[$id]);
745 try {
746 $this->addCurlHandle($entry['easy']);
747 } catch (\Throwable $e) {
748 // The promise has already escaped, so reject it
749 // rather than throw.
750 $rejection = $this->discardPendingRequest($id, $entry, $e);
751 if (\YoastSEO_Vendor\GuzzleHttp\Promise\Is::pending($entry['deferred'])) {
752 $entry['deferred']->reject($rejection);
753 }
754 }
755 }
756 }
757 }
758 // Run curl_multi_exec in the queue to enable other async tasks to
759 // run, surface completions, and drain any work they queued so a
760 // ready cancellation or new transfer is not held behind the select.
761 do {
762 \YoastSEO_Vendor\GuzzleHttp\Promise\Utils::queue()->add(\Closure::fromCallable([$this, 'tickInQueue']));
763 // Step through the task queue which may add additional requests.
764 \YoastSEO_Vendor\GuzzleHttp\Promise\Utils::queue()->run();
765 if ($this->multiExecDepth > 0) {
766 // A cURL callback re-entered the handler while native
767 // execution is running; the outer frame drives native cURL
768 // once it unwinds.
769 return;
770 }
771 if (isset($this->_mh)) {
772 $this->processMessages();
773 }
774 } while (!\YoastSEO_Vendor\GuzzleHttp\Promise\Utils::queue()->isEmpty());
775 if (!isset($this->_mh)) {
776 // Nothing is attached natively (or initialization just failed);
777 // there is nothing to run and nothing to recreate the handle for.
778 return;
779 }
780 if ($targetId !== null && !$this->hasRequest($targetId, $waitToken)) {
781 return;
782 }
783 if ($this->active && \curl_multi_select($this->_mh, $this->effectiveSelectTimeout()) === -1) {
784 // Perform a usleep if a select returns -1.
785 // See: https://bugs.php.net/bug.php?id=61141
786 \usleep(250);
787 }
788 do {
789 $exec = $this->executeMulti();
790 // Prevent busy looping for slow HTTP requests.
791 if ($exec === \CURLM_CALL_MULTI_PERFORM) {
792 \curl_multi_select($this->_mh, $this->effectiveSelectTimeout());
793 }
794 } while ($exec === \CURLM_CALL_MULTI_PERFORM);
795 $this->processMessages();
796 }
797 /**
798 * Runs \curl_multi_exec() inside the event loop, to prevent busy looping
799 */
800 private function tickInQueue() : void
801 {
802 if ($this->multiExecDepth > 0) {
803 // A cURL callback re-entered the handler while native execution
804 // is running; the outer frame drives native cURL once it unwinds.
805 return;
806 }
807 if (!isset($this->_mh)) {
808 // Nothing is attached natively (or initialization just failed);
809 // there is nothing to run and nothing to recreate the handle for.
810 return;
811 }
812 $exec = $this->executeMulti();
813 if ($exec === \CURLM_CALL_MULTI_PERFORM) {
814 \curl_multi_select($this->_mh, 0);
815 \YoastSEO_Vendor\GuzzleHttp\Promise\Utils::queue()->add(\Closure::fromCallable([$this, 'tickInQueue']));
816 }
817 }
818 /**
819 * @phpstan-impure
820 */
821 private function executeMulti() : int
822 {
823 ++$this->multiExecDepth;
824 try {
825 return \curl_multi_exec($this->_mh, $this->active);
826 } finally {
827 --$this->multiExecDepth;
828 $this->finishDeferredWork();
829 }
830 }
831 /**
832 * Flushes cancels and attachments deferred while the multi handle was
833 * busy executing transfers or removing a handle.
834 */
835 private function finishDeferredWork() : void
836 {
837 if ($this->multiExecDepth > 0 || $this->finishingDeferredWork) {
838 // A nested frame (a cURL callback re-entered the handler) must
839 // not flush while an outer frame is still using the multi
840 // handle; the outermost frame flushes once it unwinds.
841 return;
842 }
843 $this->finishingDeferredWork = \true;
844 try {
845 $failure = null;
846 // Removing a cancelled transfer runs its final progress update,
847 // whose callback can cancel other transfers or create requests;
848 // drain until no deferred work remains.
849 do {
850 $this->cleanupDeferredCancels($failure);
851 $this->flushDeferredAdds();
852 } while ($this->deferredCancels !== [] || $this->deferredAdds !== []);
853 if ($failure !== null) {
854 throw $failure;
855 }
856 } finally {
857 $this->finishingDeferredWork = \false;
858 }
859 }
860 /**
861 * Runs until all outstanding connections have completed.
862 */
863 public function execute() : void
864 {
865 if ($this->multiExecDepth > 0) {
866 // Native cURL cannot be driven while a callback has it busy, so
867 // the loop would spin without ever progressing.
868 throw new \LogicException('Cannot run the cURL multi event loop from inside a cURL callback; the callback must return before transfers can progress.');
869 }
870 $queue = \YoastSEO_Vendor\GuzzleHttp\Promise\Utils::queue();
871 while ($this->handles || !$queue->isEmpty()) {
872 // If there are no transfers, then sleep for the next delay,
873 // unless ready queue work could change what is pending.
874 if (!$this->active && $this->delays && $queue->isEmpty()) {
875 \usleep($this->timeToNext());
876 }
877 $this->tick();
878 }
879 }
880 /**
881 * Runs the event loop until the given transfer has finished, so waiting
882 * on a promise does not wait for every other transfer on the handler
883 * like execute() does.
884 *
885 * The native cURL handle ID can be reused by a request created from a
886 * completion callback, so the wait token guards against waiting on an
887 * unrelated transfer that inherited the ID.
888 */
889 private function executeUntil(int $id, object $waitToken) : void
890 {
891 $queue = \YoastSEO_Vendor\GuzzleHttp\Promise\Utils::queue();
892 while ($this->hasRequest($id, $waitToken)) {
893 // If the transfer is delayed, then sleep until it is due, unless
894 // ready queue work could cancel or replace it first.
895 if (!$this->active && isset($this->delays[$id]) && $queue->isEmpty()) {
896 \usleep($this->timeToNext());
897 }
898 $this->tickFor($id, $waitToken);
899 }
900 if (!$queue->isEmpty()) {
901 $queue->run();
902 }
903 }
904 /**
905 * Checks that the request with the given handle ID is still pending and,
906 * when a wait token is given, has not been replaced by a request that
907 * reused the ID.
908 */
909 private function hasRequest(int $id, ?object $waitToken = null) : bool
910 {
911 if (!isset($this->handles[$id])) {
912 return \false;
913 }
914 return $waitToken === null || ($this->handles[$id]['wait_token'] ?? null) === $waitToken;
915 }
916 private function addRequest(array $entry) : void
917 {
918 $easy = $entry['easy'];
919 $id = (int) $easy->handle;
920 $entry['attached'] = \false;
921 $this->handles[$id] = $entry;
922 if (!empty($easy->options['delay'])) {
923 $this->delays[$id] = \YoastSEO_Vendor\GuzzleHttp\Utils::currentTime() + $easy->options['delay'] / 1000;
924 } elseif ($this->multiExecDepth > 0) {
925 // A request created from inside a cURL callback cannot be added
926 // natively while curl_multi_exec() is running; libcurl 7.59+
927 // rejects the recursive call. Attach it once the outermost
928 // native execution unwinds.
929 $this->deferredAdds[$id] = $entry['wait_token'] ?? null;
930 } else {
931 $this->addCurlHandle($easy);
932 }
933 }
934 /**
935 * Rolls back a request that can no longer be attached, releasing the
936 * easy handle exactly once and preserving the original failure.
937 *
938 * @param array{easy: EasyHandle, deferred: Promise, wait_token?: object|null, attached?: bool} $entry
939 */
940 private function discardPendingRequest(int $id, array $entry, \Throwable $failure) : \Throwable
941 {
942 unset($this->handles[$id], $this->delays[$id], $this->deferredAdds[$id]);
943 try {
944 $this->factory->release($entry['easy']);
945 } catch (\Throwable $e) {
946 // Preserve the original failure.
947 }
948 return $failure;
949 }
950 /**
951 * Fails a synchronous wait attempted from inside a cURL callback, where
952 * native execution cannot progress until the callback returns.
953 */
954 private function failNestedWait(int $id, object $token) : void
955 {
956 if (!$this->hasRequest($id, $token)) {
957 return;
958 }
959 $entry = $this->handles[$id];
960 $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);
961 if (!empty($entry['attached'])) {
962 // Native removal must wait until the outermost execution unwinds.
963 unset($this->handles[$id], $this->delays[$id], $this->deferredAdds[$id]);
964 $this->deferredCancels[$id] = ['easy' => $entry['easy'], 'attached' => \true];
965 } else {
966 $this->discardPendingRequest($id, $entry, $failure);
967 }
968 $entry['deferred']->reject($failure);
969 }
970 /**
971 * Attaches requests whose native attachment was deferred because they
972 * were created from inside a cURL callback.
973 */
974 private function flushDeferredAdds() : void
975 {
976 if ($this->deferredAdds === []) {
977 return;
978 }
979 $adds = $this->deferredAdds;
980 $this->deferredAdds = [];
981 foreach ($adds as $id => $token) {
982 if (!$this->hasRequest($id, $token)) {
983 // Cancelled or replaced while the attachment was deferred.
984 continue;
985 }
986 $entry = $this->handles[$id];
987 try {
988 $this->addCurlHandle($entry['easy']);
989 } catch (\Throwable $e) {
990 // The promise has already escaped, so reject it rather than
991 // throw. User code may have settled it directly; a settled
992 // promise must not abort the rest of the snapshot.
993 $rejection = $this->discardPendingRequest($id, $entry, $e);
994 if (\YoastSEO_Vendor\GuzzleHttp\Promise\Is::pending($entry['deferred'])) {
995 $entry['deferred']->reject($rejection);
996 }
997 }
998 }
999 }
1000 /**
1001 * Cancels a handle from sending and removes references to it.
1002 *
1003 * @param int $id Handle ID to cancel and remove.
1004 * @param object|null $waitToken Identity token that must still match the
1005 * entry when given.
1006 *
1007 * @return bool True on success, false on failure.
1008 */
1009 private function cancel($id, ?object $waitToken = null) : bool
1010 {
1011 if (!\is_int($id)) {
1012 \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__);
1013 }
1014 // Cannot cancel if it has been processed or replaced by a request
1015 // that reused the native handle ID.
1016 if (!isset($this->handles[$id]) || $waitToken !== null && ($this->handles[$id]['wait_token'] ?? null) !== $waitToken) {
1017 return \false;
1018 }
1019 $entry = $this->handles[$id];
1020 $easy = $entry['easy'];
1021 $attached = !empty($entry['attached']);
1022 unset($this->delays[$id], $this->deferredAdds[$id], $this->handles[$id]);
1023 if ($this->multiExecDepth > 0) {
1024 $this->deferredCancels[$id] = ['easy' => $easy, 'attached' => $attached];
1025 return \true;
1026 }
1027 $this->cleanupCancelledHandle($easy, $attached);
1028 return \true;
1029 }
1030 private function cleanupDeferredCancels(?\Throwable &$failure) : void
1031 {
1032 if ($this->deferredCancels === []) {
1033 return;
1034 }
1035 $entries = $this->deferredCancels;
1036 $this->deferredCancels = [];
1037 foreach ($entries as $entry) {
1038 try {
1039 $this->cleanupCancelledHandle($entry['easy'], $entry['attached']);
1040 } catch (\Throwable $e) {
1041 // A final progress update can run a throwing user callback;
1042 // clean the remaining entries and surface the first failure
1043 // once the drain completes.
1044 if ($failure === null) {
1045 $failure = $e;
1046 }
1047 }
1048 }
1049 }
1050 private function cleanupCancelledHandle(\YoastSEO_Vendor\GuzzleHttp\Handler\EasyHandle $easy, bool $attached) : void
1051 {
1052 $handle = $easy->handle;
1053 $failure = null;
1054 if ($attached) {
1055 try {
1056 $this->removeHandleFromMulti($handle);
1057 } catch (\Throwable $e) {
1058 // The native detach completes even when its final progress
1059 // callback throws; finish this entry before rethrowing.
1060 $failure = $e;
1061 }
1062 }
1063 $this->unmarkProxyTunnelActive($easy);
1064 if (\PHP_VERSION_ID < 80000) {
1065 try {
1066 \curl_close($handle);
1067 } catch (\Throwable $e) {
1068 // An error handler can promote the close warning; keep the
1069 // first failure.
1070 if ($failure === null) {
1071 $failure = $e;
1072 }
1073 }
1074 }
1075 if ($failure !== null) {
1076 throw $failure;
1077 }
1078 }
1079 private function processMessages() : void
1080 {
1081 // CurlFactory::finish can retry a transfer by re-invoking this handler
1082 // from inside this loop; the guard keeps that re-entry from recreating
1083 // the multi handle mid-iteration (see applyProxyTunnelOwnership). A
1084 // depth is tracked because a completion callback can re-enter tick(),
1085 // and the nested frame must not clear the outer loop's guard.
1086 ++$this->messageProcessingDepth;
1087 try {
1088 while ($done = \curl_multi_info_read($this->_mh)) {
1089 if ($done['msg'] !== \CURLMSG_DONE) {
1090 // if it's not done, then it would be premature to remove the handle. ref https://github.com/guzzle/guzzle/pull/2892#issuecomment-945150216
1091 continue;
1092 }
1093 if (!isset($done['handle'])) {
1094 // Work around a PHP issue where cancelled transfers may omit the handle.
1095 // Remove this once we no longer support PHP versions before the fix in
1096 // https://github.com/php/php-src/pull/16302.
1097 continue;
1098 }
1099 $id = (int) $done['handle'];
1100 $this->removeCompletedHandleFromMulti($id, $done['handle']);
1101 if (!isset($this->handles[$id])) {
1102 // Probably was cancelled.
1103 continue;
1104 }
1105 $entry = $this->handles[$id];
1106 unset($this->handles[$id], $this->delays[$id]);
1107 $entry['easy']->errno = $done['result'];
1108 // finish() can run completion callbacks that cancel this
1109 // promise; a settled promise must not be settled again.
1110 try {
1111 $result = \YoastSEO_Vendor\GuzzleHttp\Handler\CurlFactory::finish($this, $entry['easy'], $this->factory);
1112 } catch (\Throwable $e) {
1113 if (\YoastSEO_Vendor\GuzzleHttp\Promise\Is::pending($entry['deferred'])) {
1114 $entry['deferred']->reject($e);
1115 }
1116 continue;
1117 }
1118 if (\YoastSEO_Vendor\GuzzleHttp\Promise\Is::pending($entry['deferred'])) {
1119 $entry['deferred']->resolve($result);
1120 }
1121 }
1122 } finally {
1123 --$this->messageProcessingDepth;
1124 }
1125 }
1126 /**
1127 * Bounds a blocking select by the earliest pending request delay so a
1128 * delayed transfer becoming due does not wait out an unrelated
1129 * transfer's full select timeout.
1130 *
1131 * @return float|int
1132 */
1133 private function effectiveSelectTimeout()
1134 {
1135 if ($this->delays === []) {
1136 return $this->selectTimeout;
1137 }
1138 return \min($this->selectTimeout, $this->secondsToNext());
1139 }
1140 /**
1141 * @return float Seconds until the earliest pending delay is due
1142 */
1143 private function secondsToNext() : float
1144 {
1145 $currentTime = \YoastSEO_Vendor\GuzzleHttp\Utils::currentTime();
1146 $nextTime = \PHP_FLOAT_MAX;
1147 foreach ($this->delays as $time) {
1148 if ($time < $nextTime) {
1149 $nextTime = $time;
1150 }
1151 }
1152 return \max(0.0, $nextTime - $currentTime);
1153 }
1154 private function timeToNext() : int
1155 {
1156 // PHP_INT_MAX first: min() then returns the int operand whenever the
1157 // microseconds exceed it, so the cast never sees an oversized float.
1158 return (int) \min(\PHP_INT_MAX, $this->secondsToNext() * 1000000);
1159 }
1160 }
1161