feat: initial ACRIB WordPress deployment

- WordPress 6.9.4 (es_ES) with Kadence theme
- Homepage: Hero, La Asociación, Pilares, Beneficios, Eventos, Miembros, Hazte Miembro, Contacto
- Brand identity: #13294b navy, #a12932 burgundy, #c69c48 gold
- Fonts: Raleway (headings) + Source Sans 3 (body) + Lato (UI)
- Plugins: Kadence Blocks, Polylang, Contact Form 7
- Custom CSS with full brand styling and responsive layout
- HTTPS enforced via wp-config.php proxy detection
This commit is contained in:
Malin
2026-05-19 19:25:59 +02:00
commit f3ff7b7186
6119 changed files with 1984255 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
CHANGELOG
=========
The changelog is maintained for all Symfony contracts at the following URL:
https://github.com/symfony/contracts/blob/main/CHANGELOG.md

View File

@@ -0,0 +1,19 @@
Copyright (c) 2020-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,26 @@
Symfony Deprecation Contracts
=============================
A generic function and convention to trigger deprecation notices.
This package provides a single global function named `trigger_deprecation()` that triggers silenced deprecation notices.
By using a custom PHP error handler such as the one provided by the Symfony ErrorHandler component,
the triggered deprecations can be caught and logged for later discovery, both on dev and prod environments.
The function requires at least 3 arguments:
- the name of the Composer package that is triggering the deprecation
- the version of the package that introduced the deprecation
- the message of the deprecation
- more arguments can be provided: they will be inserted in the message using `printf()` formatting
Example:
```php
trigger_deprecation('symfony/blockchain', '8.9', 'Using "%s" is deprecated, use "%s" instead.', 'bitcoin', 'fabcoin');
```
This will generate the following message:
`Since symfony/blockchain 8.9: Using "bitcoin" is deprecated, use "fabcoin" instead.`
While not necessarily recommended, the deprecation notices can be completely ignored by declaring an empty
`function trigger_deprecation() {}` in your application.

View File

@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
if (!function_exists('kadence_blocks_trigger_deprecation')) {
/**
* Triggers a silenced deprecation notice.
*
* @param string $package The name of the Composer package that is triggering the deprecation
* @param string $version The version of the package that introduced the deprecation
* @param string $message The message of the deprecation
* @param mixed ...$args Values to insert in the message using printf() formatting
*
* @author Nicolas Grekas <p@tchwork.com>
*/
function kadence_blocks_trigger_deprecation(string $package, string $version, string $message, ...$args): void
{
@trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED);
}
}

View File

@@ -0,0 +1,82 @@
CHANGELOG
=========
5.4
---
* Add `Path` class
* Add `$lock` argument to `Filesystem::appendToFile()`
5.0.0
-----
* `Filesystem::dumpFile()` and `appendToFile()` don't accept arrays anymore
4.4.0
-----
* support for passing a `null` value to `Filesystem::isAbsolutePath()` is deprecated and will be removed in 5.0
* `tempnam()` now accepts a third argument `$suffix`.
4.3.0
-----
* support for passing arrays to `Filesystem::dumpFile()` is deprecated and will be removed in 5.0
* support for passing arrays to `Filesystem::appendToFile()` is deprecated and will be removed in 5.0
4.0.0
-----
* removed `LockHandler`
* Support for passing relative paths to `Filesystem::makePathRelative()` has been removed.
3.4.0
-----
* support for passing relative paths to `Filesystem::makePathRelative()` is deprecated and will be removed in 4.0
3.3.0
-----
* added `appendToFile()` to append contents to existing files
3.2.0
-----
* added `readlink()` as a platform independent method to read links
3.0.0
-----
* removed `$mode` argument from `Filesystem::dumpFile()`
2.8.0
-----
* added tempnam() a stream aware version of PHP's native tempnam()
2.6.0
-----
* added LockHandler
2.3.12
------
* deprecated dumpFile() file mode argument.
2.3.0
-----
* added the dumpFile() method to atomically write files
2.2.0
-----
* added a delete option for the mirror() method
2.1.0
-----
* 24eb396 : BC Break : mkdir() function now throws exception in case of failure instead of returning Boolean value
* created the component

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\Filesystem\Exception;
/**
* Exception interface for all exceptions thrown by the component.
*
* @author Romain Neutron <imprec@gmail.com>
*/
interface ExceptionInterface extends \Throwable
{
}

View File

@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\Filesystem\Exception;
/**
* Exception class thrown when a file couldn't be found.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Christian Gärtner <christiangaertner.film@googlemail.com>
*/
class FileNotFoundException extends IOException
{
public function __construct(?string $message = null, int $code = 0, ?\Throwable $previous = null, ?string $path = null)
{
if (null === $message) {
if (null === $path) {
$message = 'File could not be found.';
} else {
$message = sprintf('File "%s" could not be found.', $path);
}
}
parent::__construct($message, $code, $previous, $path);
}
}

View File

@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\Filesystem\Exception;
/**
* Exception class thrown when a filesystem operation failure happens.
*
* @author Romain Neutron <imprec@gmail.com>
* @author Christian Gärtner <christiangaertner.film@googlemail.com>
* @author Fabien Potencier <fabien@symfony.com>
*/
class IOException extends \RuntimeException implements IOExceptionInterface
{
private $path;
public function __construct(string $message, int $code = 0, ?\Throwable $previous = null, ?string $path = null)
{
$this->path = $path;
parent::__construct($message, $code, $previous);
}
/**
* {@inheritdoc}
*/
public function getPath()
{
return $this->path;
}
}

View File

@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\Filesystem\Exception;
/**
* IOException interface for file and input/output stream related exceptions thrown by the component.
*
* @author Christian Gärtner <christiangaertner.film@googlemail.com>
*/
interface IOExceptionInterface extends ExceptionInterface
{
/**
* Returns the associated path for the exception.
*
* @return string|null
*/
public function getPath();
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\Filesystem\Exception;
/**
* @author Christian Flothmann <christian.flothmann@sensiolabs.de>
*/
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,19 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\Filesystem\Exception;
/**
* @author Théo Fidry <theo.fidry@gmail.com>
*/
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View File

@@ -0,0 +1,790 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\Filesystem;
use KadenceWP\KadenceBlocks\Symfony\Component\Filesystem\Exception\FileNotFoundException;
use KadenceWP\KadenceBlocks\Symfony\Component\Filesystem\Exception\InvalidArgumentException;
use KadenceWP\KadenceBlocks\Symfony\Component\Filesystem\Exception\IOException;
/**
* Provides basic utility to manipulate the file system.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Filesystem
{
private static $lastError;
/**
* Copies a file.
*
* If the target file is older than the origin file, it's always overwritten.
* If the target file is newer, it is overwritten only when the
* $overwriteNewerFiles option is set to true.
*
* @throws FileNotFoundException When originFile doesn't exist
* @throws IOException When copy fails
*/
public function copy(string $originFile, string $targetFile, bool $overwriteNewerFiles = false)
{
$originIsLocal = stream_is_local($originFile) || 0 === stripos($originFile, 'file://');
if ($originIsLocal && !is_file($originFile)) {
throw new FileNotFoundException(sprintf('Failed to copy "%s" because file does not exist.', $originFile), 0, null, $originFile);
}
$this->mkdir(\dirname($targetFile));
$doCopy = true;
if (!$overwriteNewerFiles && !parse_url($originFile, \PHP_URL_HOST) && is_file($targetFile)) {
$doCopy = filemtime($originFile) > filemtime($targetFile);
}
if ($doCopy) {
// https://bugs.php.net/64634
if (!$source = self::box('fopen', $originFile, 'r')) {
throw new IOException(sprintf('Failed to copy "%s" to "%s" because source file could not be opened for reading: ', $originFile, $targetFile).self::$lastError, 0, null, $originFile);
}
// Stream context created to allow files overwrite when using FTP stream wrapper - disabled by default
if (!$target = self::box('fopen', $targetFile, 'w', false, stream_context_create(['ftp' => ['overwrite' => true]]))) {
throw new IOException(sprintf('Failed to copy "%s" to "%s" because target file could not be opened for writing: ', $originFile, $targetFile).self::$lastError, 0, null, $originFile);
}
$bytesCopied = stream_copy_to_stream($source, $target);
fclose($source);
fclose($target);
unset($source, $target);
if (!is_file($targetFile)) {
throw new IOException(sprintf('Failed to copy "%s" to "%s".', $originFile, $targetFile), 0, null, $originFile);
}
if ($originIsLocal) {
// Like `cp`, preserve executable permission bits
self::box('chmod', $targetFile, fileperms($targetFile) | (fileperms($originFile) & 0111));
// Like `cp`, preserve the file modification time
self::box('touch', $targetFile, filemtime($originFile));
if ($bytesCopied !== $bytesOrigin = filesize($originFile)) {
throw new IOException(sprintf('Failed to copy the whole content of "%s" to "%s" (%g of %g bytes copied).', $originFile, $targetFile, $bytesCopied, $bytesOrigin), 0, null, $originFile);
}
}
}
}
/**
* Creates a directory recursively.
*
* @param string|iterable $dirs The directory path
*
* @throws IOException On any directory creation failure
*/
public function mkdir($dirs, int $mode = 0777)
{
foreach ($this->toIterable($dirs) as $dir) {
if (is_dir($dir)) {
continue;
}
if (!self::box('mkdir', $dir, $mode, true) && !is_dir($dir)) {
throw new IOException(sprintf('Failed to create "%s": ', $dir).self::$lastError, 0, null, $dir);
}
}
}
/**
* Checks the existence of files or directories.
*
* @param string|iterable $files A filename, an array of files, or a \Traversable instance to check
*
* @return bool
*/
public function exists($files)
{
$maxPathLength = \PHP_MAXPATHLEN - 2;
foreach ($this->toIterable($files) as $file) {
if (\strlen($file) > $maxPathLength) {
throw new IOException(sprintf('Could not check if file exist because path length exceeds %d characters.', $maxPathLength), 0, null, $file);
}
if (!file_exists($file)) {
return false;
}
}
return true;
}
/**
* Sets access and modification time of file.
*
* @param string|iterable $files A filename, an array of files, or a \Traversable instance to create
* @param int|null $time The touch time as a Unix timestamp, if not supplied the current system time is used
* @param int|null $atime The access time as a Unix timestamp, if not supplied the current system time is used
*
* @throws IOException When touch fails
*/
public function touch($files, ?int $time = null, ?int $atime = null)
{
foreach ($this->toIterable($files) as $file) {
if (!($time ? self::box('touch', $file, $time, $atime) : self::box('touch', $file))) {
throw new IOException(sprintf('Failed to touch "%s": ', $file).self::$lastError, 0, null, $file);
}
}
}
/**
* Removes files or directories.
*
* @param string|iterable $files A filename, an array of files, or a \Traversable instance to remove
*
* @throws IOException When removal fails
*/
public function remove($files)
{
if ($files instanceof \Traversable) {
$files = iterator_to_array($files, false);
} elseif (!\is_array($files)) {
$files = [$files];
}
self::doRemove($files, false);
}
private static function doRemove(array $files, bool $isRecursive): void
{
$files = array_reverse($files);
foreach ($files as $file) {
if (is_link($file)) {
// See https://bugs.php.net/52176
if (!(self::box('unlink', $file) || '\\' !== \DIRECTORY_SEPARATOR || self::box('rmdir', $file)) && file_exists($file)) {
throw new IOException(sprintf('Failed to remove symlink "%s": ', $file).self::$lastError);
}
} elseif (is_dir($file)) {
if (!$isRecursive) {
$tmpName = \dirname(realpath($file)).'/.!'.strrev(strtr(base64_encode(random_bytes(2)), '/=', '-!'));
if (file_exists($tmpName)) {
try {
self::doRemove([$tmpName], true);
} catch (IOException $e) {
}
}
if (!file_exists($tmpName) && self::box('rename', $file, $tmpName)) {
$origFile = $file;
$file = $tmpName;
} else {
$origFile = null;
}
}
$files = new \FilesystemIterator($file, \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS);
self::doRemove(iterator_to_array($files, true), true);
if (!self::box('rmdir', $file) && file_exists($file) && !$isRecursive) {
$lastError = self::$lastError;
if (null !== $origFile && self::box('rename', $file, $origFile)) {
$file = $origFile;
}
throw new IOException(sprintf('Failed to remove directory "%s": ', $file).$lastError);
}
} elseif (!self::box('unlink', $file) && ((self::$lastError && str_contains(self::$lastError, 'Permission denied')) || file_exists($file))) {
throw new IOException(sprintf('Failed to remove file "%s": ', $file).self::$lastError);
}
}
}
/**
* Change mode for an array of files or directories.
*
* @param string|iterable $files A filename, an array of files, or a \Traversable instance to change mode
* @param int $mode The new mode (octal)
* @param int $umask The mode mask (octal)
* @param bool $recursive Whether change the mod recursively or not
*
* @throws IOException When the change fails
*/
public function chmod($files, int $mode, int $umask = 0000, bool $recursive = false)
{
foreach ($this->toIterable($files) as $file) {
if ((\PHP_VERSION_ID < 80000 || \is_int($mode)) && !self::box('chmod', $file, $mode & ~$umask)) {
throw new IOException(sprintf('Failed to chmod file "%s": ', $file).self::$lastError, 0, null, $file);
}
if ($recursive && is_dir($file) && !is_link($file)) {
$this->chmod(new \FilesystemIterator($file), $mode, $umask, true);
}
}
}
/**
* Change the owner of an array of files or directories.
*
* This method always throws on Windows, as the underlying PHP function is not supported.
*
* @see https://www.php.net/chown
*
* @param string|iterable $files A filename, an array of files, or a \Traversable instance to change owner
* @param string|int $user A user name or number
* @param bool $recursive Whether change the owner recursively or not
*
* @throws IOException When the change fails
*/
public function chown($files, $user, bool $recursive = false)
{
foreach ($this->toIterable($files) as $file) {
if ($recursive && is_dir($file) && !is_link($file)) {
$this->chown(new \FilesystemIterator($file), $user, true);
}
if (is_link($file) && \function_exists('lchown')) {
if (!self::box('lchown', $file, $user)) {
throw new IOException(sprintf('Failed to chown file "%s": ', $file).self::$lastError, 0, null, $file);
}
} else {
if (!self::box('chown', $file, $user)) {
throw new IOException(sprintf('Failed to chown file "%s": ', $file).self::$lastError, 0, null, $file);
}
}
}
}
/**
* Change the group of an array of files or directories.
*
* This method always throws on Windows, as the underlying PHP function is not supported.
*
* @see https://www.php.net/chgrp
*
* @param string|iterable $files A filename, an array of files, or a \Traversable instance to change group
* @param string|int $group A group name or number
* @param bool $recursive Whether change the group recursively or not
*
* @throws IOException When the change fails
*/
public function chgrp($files, $group, bool $recursive = false)
{
foreach ($this->toIterable($files) as $file) {
if ($recursive && is_dir($file) && !is_link($file)) {
$this->chgrp(new \FilesystemIterator($file), $group, true);
}
if (is_link($file) && \function_exists('lchgrp')) {
if (!self::box('lchgrp', $file, $group)) {
throw new IOException(sprintf('Failed to chgrp file "%s": ', $file).self::$lastError, 0, null, $file);
}
} else {
if (!self::box('chgrp', $file, $group)) {
throw new IOException(sprintf('Failed to chgrp file "%s": ', $file).self::$lastError, 0, null, $file);
}
}
}
}
/**
* Renames a file or a directory.
*
* @throws IOException When target file or directory already exists
* @throws IOException When origin cannot be renamed
*/
public function rename(string $origin, string $target, bool $overwrite = false)
{
// we check that target does not exist
if (!$overwrite && $this->isReadable($target)) {
throw new IOException(sprintf('Cannot rename because the target "%s" already exists.', $target), 0, null, $target);
}
if (!self::box('rename', $origin, $target)) {
if (is_dir($origin)) {
// See https://bugs.php.net/54097 & https://php.net/rename#113943
$this->mirror($origin, $target, null, ['override' => $overwrite, 'delete' => $overwrite]);
$this->remove($origin);
return;
}
throw new IOException(sprintf('Cannot rename "%s" to "%s": ', $origin, $target).self::$lastError, 0, null, $target);
}
}
/**
* Tells whether a file exists and is readable.
*
* @throws IOException When windows path is longer than 258 characters
*/
private function isReadable(string $filename): bool
{
$maxPathLength = \PHP_MAXPATHLEN - 2;
if (\strlen($filename) > $maxPathLength) {
throw new IOException(sprintf('Could not check if file is readable because path length exceeds %d characters.', $maxPathLength), 0, null, $filename);
}
return is_readable($filename);
}
/**
* Creates a symbolic link or copy a directory.
*
* @throws IOException When symlink fails
*/
public function symlink(string $originDir, string $targetDir, bool $copyOnWindows = false)
{
self::assertFunctionExists('symlink');
if ('\\' === \DIRECTORY_SEPARATOR) {
$originDir = strtr($originDir, '/', '\\');
$targetDir = strtr($targetDir, '/', '\\');
if ($copyOnWindows) {
$this->mirror($originDir, $targetDir);
return;
}
}
$this->mkdir(\dirname($targetDir));
if (is_link($targetDir)) {
if (readlink($targetDir) === $originDir) {
return;
}
$this->remove($targetDir);
}
if (!self::box('symlink', $originDir, $targetDir)) {
$this->linkException($originDir, $targetDir, 'symbolic');
}
}
/**
* Creates a hard link, or several hard links to a file.
*
* @param string|string[] $targetFiles The target file(s)
*
* @throws FileNotFoundException When original file is missing or not a file
* @throws IOException When link fails, including if link already exists
*/
public function hardlink(string $originFile, $targetFiles)
{
self::assertFunctionExists('link');
if (!$this->exists($originFile)) {
throw new FileNotFoundException(null, 0, null, $originFile);
}
if (!is_file($originFile)) {
throw new FileNotFoundException(sprintf('Origin file "%s" is not a file.', $originFile));
}
foreach ($this->toIterable($targetFiles) as $targetFile) {
if (is_file($targetFile)) {
if (fileinode($originFile) === fileinode($targetFile)) {
continue;
}
$this->remove($targetFile);
}
if (!self::box('link', $originFile, $targetFile)) {
$this->linkException($originFile, $targetFile, 'hard');
}
}
}
/**
* @param string $linkType Name of the link type, typically 'symbolic' or 'hard'
*/
private function linkException(string $origin, string $target, string $linkType)
{
if (self::$lastError) {
if ('\\' === \DIRECTORY_SEPARATOR && str_contains(self::$lastError, 'error code(1314)')) {
throw new IOException(sprintf('Unable to create "%s" link due to error code 1314: \'A required privilege is not held by the client\'. Do you have the required Administrator-rights?', $linkType), 0, null, $target);
}
}
throw new IOException(sprintf('Failed to create "%s" link from "%s" to "%s": ', $linkType, $origin, $target).self::$lastError, 0, null, $target);
}
/**
* Resolves links in paths.
*
* With $canonicalize = false (default)
* - if $path does not exist or is not a link, returns null
* - if $path is a link, returns the next direct target of the link without considering the existence of the target
*
* With $canonicalize = true
* - if $path does not exist, returns null
* - if $path exists, returns its absolute fully resolved final version
*
* @return string|null
*/
public function readlink(string $path, bool $canonicalize = false)
{
if (!$canonicalize && !is_link($path)) {
return null;
}
if ($canonicalize) {
if (!$this->exists($path)) {
return null;
}
if ('\\' === \DIRECTORY_SEPARATOR && \PHP_VERSION_ID < 70410) {
$path = readlink($path);
}
return realpath($path);
}
if ('\\' === \DIRECTORY_SEPARATOR && \PHP_VERSION_ID < 70400) {
return realpath($path);
}
return readlink($path);
}
/**
* Given an existing path, convert it to a path relative to a given starting path.
*
* @return string
*/
public function makePathRelative(string $endPath, string $startPath)
{
if (!$this->isAbsolutePath($startPath)) {
throw new InvalidArgumentException(sprintf('The start path "%s" is not absolute.', $startPath));
}
if (!$this->isAbsolutePath($endPath)) {
throw new InvalidArgumentException(sprintf('The end path "%s" is not absolute.', $endPath));
}
// Normalize separators on Windows
if ('\\' === \DIRECTORY_SEPARATOR) {
$endPath = str_replace('\\', '/', $endPath);
$startPath = str_replace('\\', '/', $startPath);
}
$splitDriveLetter = function ($path) {
return (\strlen($path) > 2 && ':' === $path[1] && '/' === $path[2] && ctype_alpha($path[0]))
? [substr($path, 2), strtoupper($path[0])]
: [$path, null];
};
$splitPath = function ($path) {
$result = [];
foreach (explode('/', trim($path, '/')) as $segment) {
if ('..' === $segment) {
array_pop($result);
} elseif ('.' !== $segment && '' !== $segment) {
$result[] = $segment;
}
}
return $result;
};
[$endPath, $endDriveLetter] = $splitDriveLetter($endPath);
[$startPath, $startDriveLetter] = $splitDriveLetter($startPath);
$startPathArr = $splitPath($startPath);
$endPathArr = $splitPath($endPath);
if ($endDriveLetter && $startDriveLetter && $endDriveLetter != $startDriveLetter) {
// End path is on another drive, so no relative path exists
return $endDriveLetter.':/'.($endPathArr ? implode('/', $endPathArr).'/' : '');
}
// Find for which directory the common path stops
$index = 0;
while (isset($startPathArr[$index]) && isset($endPathArr[$index]) && $startPathArr[$index] === $endPathArr[$index]) {
++$index;
}
// Determine how deep the start path is relative to the common path (ie, "web/bundles" = 2 levels)
if (1 === \count($startPathArr) && '' === $startPathArr[0]) {
$depth = 0;
} else {
$depth = \count($startPathArr) - $index;
}
// Repeated "../" for each level need to reach the common path
$traverser = str_repeat('../', $depth);
$endPathRemainder = implode('/', \array_slice($endPathArr, $index));
// Construct $endPath from traversing to the common path, then to the remaining $endPath
$relativePath = $traverser.('' !== $endPathRemainder ? $endPathRemainder.'/' : '');
return '' === $relativePath ? './' : $relativePath;
}
/**
* Mirrors a directory to another.
*
* Copies files and directories from the origin directory into the target directory. By default:
*
* - existing files in the target directory will be overwritten, except if they are newer (see the `override` option)
* - files in the target directory that do not exist in the source directory will not be deleted (see the `delete` option)
*
* @param \Traversable|null $iterator Iterator that filters which files and directories to copy, if null a recursive iterator is created
* @param array $options An array of boolean options
* Valid options are:
* - $options['override'] If true, target files newer than origin files are overwritten (see copy(), defaults to false)
* - $options['copy_on_windows'] Whether to copy files instead of links on Windows (see symlink(), defaults to false)
* - $options['delete'] Whether to delete files that are not in the source directory (defaults to false)
*
* @throws IOException When file type is unknown
*/
public function mirror(string $originDir, string $targetDir, ?\Traversable $iterator = null, array $options = [])
{
$targetDir = rtrim($targetDir, '/\\');
$originDir = rtrim($originDir, '/\\');
$originDirLen = \strlen($originDir);
if (!$this->exists($originDir)) {
throw new IOException(sprintf('The origin directory specified "%s" was not found.', $originDir), 0, null, $originDir);
}
// Iterate in destination folder to remove obsolete entries
if ($this->exists($targetDir) && isset($options['delete']) && $options['delete']) {
$deleteIterator = $iterator;
if (null === $deleteIterator) {
$flags = \FilesystemIterator::SKIP_DOTS;
$deleteIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($targetDir, $flags), \RecursiveIteratorIterator::CHILD_FIRST);
}
$targetDirLen = \strlen($targetDir);
foreach ($deleteIterator as $file) {
$origin = $originDir.substr($file->getPathname(), $targetDirLen);
if (!$this->exists($origin)) {
$this->remove($file);
}
}
}
$copyOnWindows = $options['copy_on_windows'] ?? false;
if (null === $iterator) {
$flags = $copyOnWindows ? \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS : \FilesystemIterator::SKIP_DOTS;
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($originDir, $flags), \RecursiveIteratorIterator::SELF_FIRST);
}
$this->mkdir($targetDir);
$filesCreatedWhileMirroring = [];
foreach ($iterator as $file) {
if ($file->getPathname() === $targetDir || $file->getRealPath() === $targetDir || isset($filesCreatedWhileMirroring[$file->getRealPath()])) {
continue;
}
$target = $targetDir.substr($file->getPathname(), $originDirLen);
$filesCreatedWhileMirroring[$target] = true;
if (!$copyOnWindows && is_link($file)) {
$this->symlink($file->getLinkTarget(), $target);
} elseif (is_dir($file)) {
$this->mkdir($target);
} elseif (is_file($file)) {
$this->copy($file, $target, $options['override'] ?? false);
} else {
throw new IOException(sprintf('Unable to guess "%s" file type.', $file), 0, null, $file);
}
}
}
/**
* Returns whether the file path is an absolute path.
*
* @return bool
*/
public function isAbsolutePath(string $file)
{
return '' !== $file && (strspn($file, '/\\', 0, 1)
|| (\strlen($file) > 3 && ctype_alpha($file[0])
&& ':' === $file[1]
&& strspn($file, '/\\', 2, 1)
)
|| null !== parse_url($file, \PHP_URL_SCHEME)
);
}
/**
* Creates a temporary file with support for custom stream wrappers.
*
* @param string $prefix The prefix of the generated temporary filename
* Note: Windows uses only the first three characters of prefix
* @param string $suffix The suffix of the generated temporary filename
*
* @return string The new temporary filename (with path), or throw an exception on failure
*/
public function tempnam(string $dir, string $prefix/* , string $suffix = '' */)
{
$suffix = \func_num_args() > 2 ? func_get_arg(2) : '';
[$scheme, $hierarchy] = $this->getSchemeAndHierarchy($dir);
// If no scheme or scheme is "file" or "gs" (Google Cloud) create temp file in local filesystem
if ((null === $scheme || 'file' === $scheme || 'gs' === $scheme) && '' === $suffix) {
// If tempnam failed or no scheme return the filename otherwise prepend the scheme
if ($tmpFile = self::box('tempnam', $hierarchy, $prefix)) {
if (null !== $scheme && 'gs' !== $scheme) {
return $scheme.'://'.$tmpFile;
}
return $tmpFile;
}
throw new IOException('A temporary file could not be created: '.self::$lastError);
}
// Loop until we create a valid temp file or have reached 10 attempts
for ($i = 0; $i < 10; ++$i) {
// Create a unique filename
$tmpFile = $dir.'/'.$prefix.uniqid(mt_rand(), true).$suffix;
// Use fopen instead of file_exists as some streams do not support stat
// Use mode 'x+' to atomically check existence and create to avoid a TOCTOU vulnerability
if (!$handle = self::box('fopen', $tmpFile, 'x+')) {
continue;
}
// Close the file if it was successfully opened
self::box('fclose', $handle);
return $tmpFile;
}
throw new IOException('A temporary file could not be created: '.self::$lastError);
}
/**
* Atomically dumps content into a file.
*
* @param string|resource $content The data to write into the file
*
* @throws IOException if the file cannot be written to
*/
public function dumpFile(string $filename, $content)
{
if (\is_array($content)) {
throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be string or resource, array given.', __METHOD__));
}
$dir = \dirname($filename);
if (is_link($filename) && $linkTarget = $this->readlink($filename)) {
$this->dumpFile(Path::makeAbsolute($linkTarget, $dir), $content);
return;
}
if (!is_dir($dir)) {
$this->mkdir($dir);
}
// Will create a temp file with 0600 access rights
// when the filesystem supports chmod.
$tmpFile = $this->tempnam($dir, basename($filename));
try {
if (false === self::box('file_put_contents', $tmpFile, $content)) {
throw new IOException(sprintf('Failed to write file "%s": ', $filename).self::$lastError, 0, null, $filename);
}
self::box('chmod', $tmpFile, self::box('fileperms', $filename) ?: 0666 & ~umask());
$this->rename($tmpFile, $filename, true);
} finally {
if (file_exists($tmpFile)) {
if ('\\' === \DIRECTORY_SEPARATOR && !is_writable($tmpFile)) {
self::box('chmod', $tmpFile, self::box('fileperms', $tmpFile) | 0200);
}
self::box('unlink', $tmpFile);
}
}
}
/**
* Appends content to an existing file.
*
* @param string|resource $content The content to append
* @param bool $lock Whether the file should be locked when writing to it
*
* @throws IOException If the file is not writable
*/
public function appendToFile(string $filename, $content/* , bool $lock = false */)
{
if (\is_array($content)) {
throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be string or resource, array given.', __METHOD__));
}
$dir = \dirname($filename);
if (!is_dir($dir)) {
$this->mkdir($dir);
}
$lock = \func_num_args() > 2 && func_get_arg(2);
if (false === self::box('file_put_contents', $filename, $content, \FILE_APPEND | ($lock ? \LOCK_EX : 0))) {
throw new IOException(sprintf('Failed to write file "%s": ', $filename).self::$lastError, 0, null, $filename);
}
}
private function toIterable($files): iterable
{
return is_iterable($files) ? $files : [$files];
}
/**
* Gets a 2-tuple of scheme (may be null) and hierarchical part of a filename (e.g. file:///tmp -> [file, tmp]).
*/
private function getSchemeAndHierarchy(string $filename): array
{
$components = explode('://', $filename, 2);
return 2 === \count($components) ? [$components[0], $components[1]] : [null, $components[0]];
}
private static function assertFunctionExists(string $func): void
{
if (!\function_exists($func)) {
throw new IOException(sprintf('Unable to perform filesystem operation because the "%s()" function has been disabled.', $func));
}
}
/**
* @param mixed ...$args
*
* @return mixed
*/
private static function box(string $func, ...$args)
{
self::assertFunctionExists($func);
self::$lastError = null;
set_error_handler(__CLASS__.'::handleError');
try {
return $func(...$args);
} finally {
restore_error_handler();
}
}
/**
* @internal
*/
public static function handleError(int $type, string $msg)
{
self::$lastError = $msg;
}
}

View File

@@ -0,0 +1,19 @@
Copyright (c) 2004-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,819 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\Filesystem;
use KadenceWP\KadenceBlocks\Symfony\Component\Filesystem\Exception\InvalidArgumentException;
use KadenceWP\KadenceBlocks\Symfony\Component\Filesystem\Exception\RuntimeException;
/**
* Contains utility methods for handling path strings.
*
* The methods in this class are able to deal with both UNIX and Windows paths
* with both forward and backward slashes. All methods return normalized parts
* containing only forward slashes and no excess "." and ".." segments.
*
* @author Bernhard Schussek <bschussek@gmail.com>
* @author Thomas Schulz <mail@king2500.net>
* @author Théo Fidry <theo.fidry@gmail.com>
*/
final class Path
{
/**
* The number of buffer entries that triggers a cleanup operation.
*/
private const CLEANUP_THRESHOLD = 1250;
/**
* The buffer size after the cleanup operation.
*/
private const CLEANUP_SIZE = 1000;
/**
* Buffers input/output of {@link canonicalize()}.
*
* @var array<string, string>
*/
private static $buffer = [];
/**
* @var int
*/
private static $bufferSize = 0;
/**
* Canonicalizes the given path.
*
* During normalization, all slashes are replaced by forward slashes ("/").
* Furthermore, all "." and ".." segments are removed as far as possible.
* ".." segments at the beginning of relative paths are not removed.
*
* ```php
* echo Path::canonicalize("\symfony\puli\..\css\style.css");
* // => /symfony/css/style.css
*
* echo Path::canonicalize("../css/./style.css");
* // => ../css/style.css
* ```
*
* This method is able to deal with both UNIX and Windows paths.
*/
public static function canonicalize(string $path): string
{
if ('' === $path) {
return '';
}
// This method is called by many other methods in this class. Buffer
// the canonicalized paths to make up for the severe performance
// decrease.
if (isset(self::$buffer[$path])) {
return self::$buffer[$path];
}
// Replace "~" with user's home directory.
if ('~' === $path[0]) {
$path = self::getHomeDirectory().substr($path, 1);
}
$path = self::normalize($path);
[$root, $pathWithoutRoot] = self::split($path);
$canonicalParts = self::findCanonicalParts($root, $pathWithoutRoot);
// Add the root directory again
self::$buffer[$path] = $canonicalPath = $root.implode('/', $canonicalParts);
++self::$bufferSize;
// Clean up regularly to prevent memory leaks
if (self::$bufferSize > self::CLEANUP_THRESHOLD) {
self::$buffer = \array_slice(self::$buffer, -self::CLEANUP_SIZE, null, true);
self::$bufferSize = self::CLEANUP_SIZE;
}
return $canonicalPath;
}
/**
* Normalizes the given path.
*
* During normalization, all slashes are replaced by forward slashes ("/").
* Contrary to {@link canonicalize()}, this method does not remove invalid
* or dot path segments. Consequently, it is much more efficient and should
* be used whenever the given path is known to be a valid, absolute system
* path.
*
* This method is able to deal with both UNIX and Windows paths.
*/
public static function normalize(string $path): string
{
return str_replace('\\', '/', $path);
}
/**
* Returns the directory part of the path.
*
* This method is similar to PHP's dirname(), but handles various cases
* where dirname() returns a weird result:
*
* - dirname() does not accept backslashes on UNIX
* - dirname("C:/symfony") returns "C:", not "C:/"
* - dirname("C:/") returns ".", not "C:/"
* - dirname("C:") returns ".", not "C:/"
* - dirname("symfony") returns ".", not ""
* - dirname() does not canonicalize the result
*
* This method fixes these shortcomings and behaves like dirname()
* otherwise.
*
* The result is a canonical path.
*
* @return string The canonical directory part. Returns the root directory
* if the root directory is passed. Returns an empty string
* if a relative path is passed that contains no slashes.
* Returns an empty string if an empty string is passed.
*/
public static function getDirectory(string $path): string
{
if ('' === $path) {
return '';
}
$path = self::canonicalize($path);
// Maintain scheme
if (false !== $schemeSeparatorPosition = strpos($path, '://')) {
$scheme = substr($path, 0, $schemeSeparatorPosition + 3);
$path = substr($path, $schemeSeparatorPosition + 3);
} else {
$scheme = '';
}
if (false === $dirSeparatorPosition = strrpos($path, '/')) {
return '';
}
// Directory equals root directory "/"
if (0 === $dirSeparatorPosition) {
return $scheme.'/';
}
// Directory equals Windows root "C:/"
if (2 === $dirSeparatorPosition && ctype_alpha($path[0]) && ':' === $path[1]) {
return $scheme.substr($path, 0, 3);
}
return $scheme.substr($path, 0, $dirSeparatorPosition);
}
/**
* Returns canonical path of the user's home directory.
*
* Supported operating systems:
*
* - UNIX
* - Windows8 and upper
*
* If your operating system or environment isn't supported, an exception is thrown.
*
* The result is a canonical path.
*
* @throws RuntimeException If your operating system or environment isn't supported
*/
public static function getHomeDirectory(): string
{
// For UNIX support
if (getenv('HOME')) {
return self::canonicalize(getenv('HOME'));
}
// For >= Windows8 support
if (getenv('HOMEDRIVE') && getenv('HOMEPATH')) {
return self::canonicalize(getenv('HOMEDRIVE').getenv('HOMEPATH'));
}
throw new RuntimeException("Cannot find the home directory path: Your environment or operating system isn't supported.");
}
/**
* Returns the root directory of a path.
*
* The result is a canonical path.
*
* @return string The canonical root directory. Returns an empty string if
* the given path is relative or empty.
*/
public static function getRoot(string $path): string
{
if ('' === $path) {
return '';
}
// Maintain scheme
if (false !== $schemeSeparatorPosition = strpos($path, '://')) {
$scheme = substr($path, 0, $schemeSeparatorPosition + 3);
$path = substr($path, $schemeSeparatorPosition + 3);
} else {
$scheme = '';
}
$firstCharacter = $path[0];
// UNIX root "/" or "\" (Windows style)
if ('/' === $firstCharacter || '\\' === $firstCharacter) {
return $scheme.'/';
}
$length = \strlen($path);
// Windows root
if ($length > 1 && ':' === $path[1] && ctype_alpha($firstCharacter)) {
// Special case: "C:"
if (2 === $length) {
return $scheme.$path.'/';
}
// Normal case: "C:/ or "C:\"
if ('/' === $path[2] || '\\' === $path[2]) {
return $scheme.$firstCharacter.$path[1].'/';
}
}
return '';
}
/**
* Returns the file name without the extension from a file path.
*
* @param string|null $extension if specified, only that extension is cut
* off (may contain leading dot)
*/
public static function getFilenameWithoutExtension(string $path, ?string $extension = null): string
{
if ('' === $path) {
return '';
}
if (null !== $extension) {
// remove extension and trailing dot
return rtrim(basename($path, $extension), '.');
}
return pathinfo($path, \PATHINFO_FILENAME);
}
/**
* Returns the extension from a file path (without leading dot).
*
* @param bool $forceLowerCase forces the extension to be lower-case
*/
public static function getExtension(string $path, bool $forceLowerCase = false): string
{
if ('' === $path) {
return '';
}
$extension = pathinfo($path, \PATHINFO_EXTENSION);
if ($forceLowerCase) {
$extension = self::toLower($extension);
}
return $extension;
}
/**
* Returns whether the path has an (or the specified) extension.
*
* @param string $path the path string
* @param string|string[]|null $extensions if null or not provided, checks if
* an extension exists, otherwise
* checks for the specified extension
* or array of extensions (with or
* without leading dot)
* @param bool $ignoreCase whether to ignore case-sensitivity
*/
public static function hasExtension(string $path, $extensions = null, bool $ignoreCase = false): bool
{
if ('' === $path) {
return false;
}
$actualExtension = self::getExtension($path, $ignoreCase);
// Only check if path has any extension
if ([] === $extensions || null === $extensions) {
return '' !== $actualExtension;
}
if (\is_string($extensions)) {
$extensions = [$extensions];
}
foreach ($extensions as $key => $extension) {
if ($ignoreCase) {
$extension = self::toLower($extension);
}
// remove leading '.' in extensions array
$extensions[$key] = ltrim($extension, '.');
}
return \in_array($actualExtension, $extensions, true);
}
/**
* Changes the extension of a path string.
*
* @param string $path The path string with filename.ext to change.
* @param string $extension new extension (with or without leading dot)
*
* @return string the path string with new file extension
*/
public static function changeExtension(string $path, string $extension): string
{
if ('' === $path) {
return '';
}
$actualExtension = self::getExtension($path);
$extension = ltrim($extension, '.');
// No extension for paths
if ('/' === substr($path, -1)) {
return $path;
}
// No actual extension in path
if (empty($actualExtension)) {
return $path.('.' === substr($path, -1) ? '' : '.').$extension;
}
return substr($path, 0, -\strlen($actualExtension)).$extension;
}
public static function isAbsolute(string $path): bool
{
if ('' === $path) {
return false;
}
// Strip scheme
if (false !== ($schemeSeparatorPosition = strpos($path, '://')) && 1 !== $schemeSeparatorPosition) {
$path = substr($path, $schemeSeparatorPosition + 3);
}
$firstCharacter = $path[0];
// UNIX root "/" or "\" (Windows style)
if ('/' === $firstCharacter || '\\' === $firstCharacter) {
return true;
}
// Windows root
if (\strlen($path) > 1 && ctype_alpha($firstCharacter) && ':' === $path[1]) {
// Special case: "C:"
if (2 === \strlen($path)) {
return true;
}
// Normal case: "C:/ or "C:\"
if ('/' === $path[2] || '\\' === $path[2]) {
return true;
}
}
return false;
}
public static function isRelative(string $path): bool
{
return !self::isAbsolute($path);
}
/**
* Turns a relative path into an absolute path in canonical form.
*
* Usually, the relative path is appended to the given base path. Dot
* segments ("." and "..") are removed/collapsed and all slashes turned
* into forward slashes.
*
* ```php
* echo Path::makeAbsolute("../style.css", "/symfony/puli/css");
* // => /symfony/puli/style.css
* ```
*
* If an absolute path is passed, that path is returned unless its root
* directory is different than the one of the base path. In that case, an
* exception is thrown.
*
* ```php
* Path::makeAbsolute("/style.css", "/symfony/puli/css");
* // => /style.css
*
* Path::makeAbsolute("C:/style.css", "C:/symfony/puli/css");
* // => C:/style.css
*
* Path::makeAbsolute("C:/style.css", "/symfony/puli/css");
* // InvalidArgumentException
* ```
*
* If the base path is not an absolute path, an exception is thrown.
*
* The result is a canonical path.
*
* @param string $basePath an absolute base path
*
* @throws InvalidArgumentException if the base path is not absolute or if
* the given path is an absolute path with
* a different root than the base path
*/
public static function makeAbsolute(string $path, string $basePath): string
{
if ('' === $basePath) {
throw new InvalidArgumentException(sprintf('The base path must be a non-empty string. Got: "%s".', $basePath));
}
if (!self::isAbsolute($basePath)) {
throw new InvalidArgumentException(sprintf('The base path "%s" is not an absolute path.', $basePath));
}
if (self::isAbsolute($path)) {
return self::canonicalize($path);
}
if (false !== $schemeSeparatorPosition = strpos($basePath, '://')) {
$scheme = substr($basePath, 0, $schemeSeparatorPosition + 3);
$basePath = substr($basePath, $schemeSeparatorPosition + 3);
} else {
$scheme = '';
}
return $scheme.self::canonicalize(rtrim($basePath, '/\\').'/'.$path);
}
/**
* Turns a path into a relative path.
*
* The relative path is created relative to the given base path:
*
* ```php
* echo Path::makeRelative("/symfony/style.css", "/symfony/puli");
* // => ../style.css
* ```
*
* If a relative path is passed and the base path is absolute, the relative
* path is returned unchanged:
*
* ```php
* Path::makeRelative("style.css", "/symfony/puli/css");
* // => style.css
* ```
*
* If both paths are relative, the relative path is created with the
* assumption that both paths are relative to the same directory:
*
* ```php
* Path::makeRelative("style.css", "symfony/puli/css");
* // => ../../../style.css
* ```
*
* If both paths are absolute, their root directory must be the same,
* otherwise an exception is thrown:
*
* ```php
* Path::makeRelative("C:/symfony/style.css", "/symfony/puli");
* // InvalidArgumentException
* ```
*
* If the passed path is absolute, but the base path is not, an exception
* is thrown as well:
*
* ```php
* Path::makeRelative("/symfony/style.css", "symfony/puli");
* // InvalidArgumentException
* ```
*
* If the base path is not an absolute path, an exception is thrown.
*
* The result is a canonical path.
*
* @throws InvalidArgumentException if the base path is not absolute or if
* the given path has a different root
* than the base path
*/
public static function makeRelative(string $path, string $basePath): string
{
$path = self::canonicalize($path);
$basePath = self::canonicalize($basePath);
[$root, $relativePath] = self::split($path);
[$baseRoot, $relativeBasePath] = self::split($basePath);
// If the base path is given as absolute path and the path is already
// relative, consider it to be relative to the given absolute path
// already
if ('' === $root && '' !== $baseRoot) {
// If base path is already in its root
if ('' === $relativeBasePath) {
$relativePath = ltrim($relativePath, './\\');
}
return $relativePath;
}
// If the passed path is absolute, but the base path is not, we
// cannot generate a relative path
if ('' !== $root && '' === $baseRoot) {
throw new InvalidArgumentException(sprintf('The absolute path "%s" cannot be made relative to the relative path "%s". You should provide an absolute base path instead.', $path, $basePath));
}
// Fail if the roots of the two paths are different
if ($baseRoot && $root !== $baseRoot) {
throw new InvalidArgumentException(sprintf('The path "%s" cannot be made relative to "%s", because they have different roots ("%s" and "%s").', $path, $basePath, $root, $baseRoot));
}
if ('' === $relativeBasePath) {
return $relativePath;
}
// Build a "../../" prefix with as many "../" parts as necessary
$parts = explode('/', $relativePath);
$baseParts = explode('/', $relativeBasePath);
$dotDotPrefix = '';
// Once we found a non-matching part in the prefix, we need to add
// "../" parts for all remaining parts
$match = true;
foreach ($baseParts as $index => $basePart) {
if ($match && isset($parts[$index]) && $basePart === $parts[$index]) {
unset($parts[$index]);
continue;
}
$match = false;
$dotDotPrefix .= '../';
}
return rtrim($dotDotPrefix.implode('/', $parts), '/');
}
/**
* Returns whether the given path is on the local filesystem.
*/
public static function isLocal(string $path): bool
{
return '' !== $path && false === strpos($path, '://');
}
/**
* Returns the longest common base path in canonical form of a set of paths or
* `null` if the paths are on different Windows partitions.
*
* Dot segments ("." and "..") are removed/collapsed and all slashes turned
* into forward slashes.
*
* ```php
* $basePath = Path::getLongestCommonBasePath(
* '/symfony/css/style.css',
* '/symfony/css/..'
* );
* // => /symfony
* ```
*
* The root is returned if no common base path can be found:
*
* ```php
* $basePath = Path::getLongestCommonBasePath(
* '/symfony/css/style.css',
* '/puli/css/..'
* );
* // => /
* ```
*
* If the paths are located on different Windows partitions, `null` is
* returned.
*
* ```php
* $basePath = Path::getLongestCommonBasePath(
* 'C:/symfony/css/style.css',
* 'D:/symfony/css/..'
* );
* // => null
* ```
*/
public static function getLongestCommonBasePath(string ...$paths): ?string
{
[$bpRoot, $basePath] = self::split(self::canonicalize(reset($paths)));
for (next($paths); null !== key($paths) && '' !== $basePath; next($paths)) {
[$root, $path] = self::split(self::canonicalize(current($paths)));
// If we deal with different roots (e.g. C:/ vs. D:/), it's time
// to quit
if ($root !== $bpRoot) {
return null;
}
// Make the base path shorter until it fits into path
while (true) {
if ('.' === $basePath) {
// No more base paths
$basePath = '';
// next path
continue 2;
}
// Prevent false positives for common prefixes
// see isBasePath()
if (0 === strpos($path.'/', $basePath.'/')) {
// next path
continue 2;
}
$basePath = \dirname($basePath);
}
}
return $bpRoot.$basePath;
}
/**
* Joins two or more path strings into a canonical path.
*/
public static function join(string ...$paths): string
{
$finalPath = null;
$wasScheme = false;
foreach ($paths as $path) {
if ('' === $path) {
continue;
}
if (null === $finalPath) {
// For first part we keep slashes, like '/top', 'C:\' or 'phar://'
$finalPath = $path;
$wasScheme = (false !== strpos($path, '://'));
continue;
}
// Only add slash if previous part didn't end with '/' or '\'
if (!\in_array(substr($finalPath, -1), ['/', '\\'])) {
$finalPath .= '/';
}
// If first part included a scheme like 'phar://' we allow \current part to start with '/', otherwise trim
$finalPath .= $wasScheme ? $path : ltrim($path, '/');
$wasScheme = false;
}
if (null === $finalPath) {
return '';
}
return self::canonicalize($finalPath);
}
/**
* Returns whether a path is a base path of another path.
*
* Dot segments ("." and "..") are removed/collapsed and all slashes turned
* into forward slashes.
*
* ```php
* Path::isBasePath('/symfony', '/symfony/css');
* // => true
*
* Path::isBasePath('/symfony', '/symfony');
* // => true
*
* Path::isBasePath('/symfony', '/symfony/..');
* // => false
*
* Path::isBasePath('/symfony', '/puli');
* // => false
* ```
*/
public static function isBasePath(string $basePath, string $ofPath): bool
{
$basePath = self::canonicalize($basePath);
$ofPath = self::canonicalize($ofPath);
// Append slashes to prevent false positives when two paths have
// a common prefix, for example /base/foo and /base/foobar.
// Don't append a slash for the root "/", because then that root
// won't be discovered as common prefix ("//" is not a prefix of
// "/foobar/").
return 0 === strpos($ofPath.'/', rtrim($basePath, '/').'/');
}
/**
* @return string[]
*/
private static function findCanonicalParts(string $root, string $pathWithoutRoot): array
{
$parts = explode('/', $pathWithoutRoot);
$canonicalParts = [];
// Collapse "." and "..", if possible
foreach ($parts as $part) {
if ('.' === $part || '' === $part) {
continue;
}
// Collapse ".." with the previous part, if one exists
// Don't collapse ".." if the previous part is also ".."
if ('..' === $part && \count($canonicalParts) > 0 && '..' !== $canonicalParts[\count($canonicalParts) - 1]) {
array_pop($canonicalParts);
continue;
}
// Only add ".." prefixes for relative paths
if ('..' !== $part || '' === $root) {
$canonicalParts[] = $part;
}
}
return $canonicalParts;
}
/**
* Splits a canonical path into its root directory and the remainder.
*
* If the path has no root directory, an empty root directory will be
* returned.
*
* If the root directory is a Windows style partition, the resulting root
* will always contain a trailing slash.
*
* list ($root, $path) = Path::split("C:/symfony")
* // => ["C:/", "symfony"]
*
* list ($root, $path) = Path::split("C:")
* // => ["C:/", ""]
*
* @return array{string, string} an array with the root directory and the remaining relative path
*/
private static function split(string $path): array
{
if ('' === $path) {
return ['', ''];
}
// Remember scheme as part of the root, if any
if (false !== $schemeSeparatorPosition = strpos($path, '://')) {
$root = substr($path, 0, $schemeSeparatorPosition + 3);
$path = substr($path, $schemeSeparatorPosition + 3);
} else {
$root = '';
}
$length = \strlen($path);
// Remove and remember root directory
if (0 === strpos($path, '/')) {
$root .= '/';
$path = $length > 1 ? substr($path, 1) : '';
} elseif ($length > 1 && ctype_alpha($path[0]) && ':' === $path[1]) {
if (2 === $length) {
// Windows special case: "C:"
$root .= $path.'/';
$path = '';
} elseif ('/' === $path[2]) {
// Windows normal case: "C:/"..
$root .= substr($path, 0, 3);
$path = $length > 3 ? substr($path, 3) : '';
}
}
return [$root, $path];
}
private static function toLower(string $string): string
{
if (false !== $encoding = mb_detect_encoding($string, null, true)) {
return mb_strtolower($string, $encoding);
}
return strtolower($string);
}
private function __construct()
{
}
}

View File

@@ -0,0 +1,13 @@
Filesystem Component
====================
The Filesystem component provides basic utilities for the filesystem.
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/filesystem.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)

View File

@@ -0,0 +1,5 @@
CHANGELOG
=========
The changelog is maintained for all Symfony contracts at the following URL:
https://github.com/symfony/contracts/blob/main/CHANGELOG.md

View File

@@ -0,0 +1,71 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* The interface of chunks returned by ResponseStreamInterface::current().
*
* When the chunk is first, last or timeout, the content MUST be empty.
* When an unchecked timeout or a network error occurs, a TransportExceptionInterface
* MUST be thrown by the destructor unless one was already thrown by another method.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface ChunkInterface
{
/**
* Tells when the idle timeout has been reached.
*
* @throws TransportExceptionInterface on a network error
*/
public function isTimeout(): bool;
/**
* Tells when headers just arrived.
*
* @throws TransportExceptionInterface on a network error or when the idle timeout is reached
*/
public function isFirst(): bool;
/**
* Tells when the body just completed.
*
* @throws TransportExceptionInterface on a network error or when the idle timeout is reached
*/
public function isLast(): bool;
/**
* Returns a [status code, headers] tuple when a 1xx status code was just received.
*
* @throws TransportExceptionInterface on a network error or when the idle timeout is reached
*/
public function getInformationalStatus(): ?array;
/**
* Returns the content of the response chunk.
*
* @throws TransportExceptionInterface on a network error or when the idle timeout is reached
*/
public function getContent(): string;
/**
* Returns the offset of the chunk in the response body.
*/
public function getOffset(): int;
/**
* In case of error, returns the message that describes it.
*/
public function getError(): ?string;
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception;
/**
* When a 4xx response is returned.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface ClientExceptionInterface extends HttpExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception;
/**
* When a content-type cannot be decoded to the expected representation.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface DecodingExceptionInterface extends ExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception;
/**
* The base interface for all exceptions in the contract.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface ExceptionInterface extends \Throwable
{
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface;
/**
* Base interface for HTTP-related exceptions.
*
* @author Anton Chernikov <anton_ch1989@mail.ru>
*/
interface HttpExceptionInterface extends ExceptionInterface
{
public function getResponse(): ResponseInterface;
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception;
/**
* When a 3xx response is returned and the "max_redirects" option has been reached.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface RedirectionExceptionInterface extends HttpExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception;
/**
* When a 5xx response is returned.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface ServerExceptionInterface extends HttpExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception;
/**
* When an idle timeout occurs.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface TimeoutExceptionInterface extends TransportExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception;
/**
* When any error happens at the transport level.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface TransportExceptionInterface extends ExceptionInterface
{
}

View File

@@ -0,0 +1,95 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Test\HttpClientTestCase;
/**
* Provides flexible methods for requesting HTTP resources synchronously or asynchronously.
*
* @see HttpClientTestCase for a reference test suite
*
* @method static withOptions(array $options) Returns a new instance of the client with new default options
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface HttpClientInterface
{
public const OPTIONS_DEFAULTS = [
'auth_basic' => null, // array|string - an array containing the username as first value, and optionally the
// password as the second one; or string like username:password - enabling HTTP Basic
// authentication (RFC 7617)
'auth_bearer' => null, // string - a token enabling HTTP Bearer authorization (RFC 6750)
'query' => [], // string[] - associative array of query string values to merge with the request's URL
'headers' => [], // iterable|string[]|string[][] - headers names provided as keys or as part of values
'body' => '', // array|string|resource|\Traversable|\Closure - the callback SHOULD yield a string
// smaller than the amount requested as argument; the empty string signals EOF; if
// an array is passed, it is meant as a form payload of field names and values
'json' => null, // mixed - if set, implementations MUST set the "body" option to the JSON-encoded
// value and set the "content-type" header to a JSON-compatible value if it is not
// explicitly defined in the headers option - typically "application/json"
'user_data' => null, // mixed - any extra data to attach to the request (scalar, callable, object...) that
// MUST be available via $response->getInfo('user_data') - not used internally
'max_redirects' => 20, // int - the maximum number of redirects to follow; a value lower than or equal to 0
// means redirects should not be followed; "Authorization" and "Cookie" headers MUST
// NOT follow except for the initial host name
'http_version' => null, // string - defaults to the best supported version, typically 1.1 or 2.0
'base_uri' => null, // string - the URI to resolve relative URLs, following rules in RFC 3986, section 2
'buffer' => true, // bool|resource|\Closure - whether the content of the response should be buffered or not,
// or a stream resource where the response body should be written,
// or a closure telling if/where the response should be buffered based on its headers
'on_progress' => null, // callable(int $dlNow, int $dlSize, array $info) - throwing any exceptions MUST abort the
// request; it MUST be called on connection, on headers and on completion; it SHOULD be
// called on upload/download of data and at least 1/s
'resolve' => [], // string[] - a map of host to IP address that SHOULD replace DNS resolution
'proxy' => null, // string - by default, the proxy-related env vars handled by curl SHOULD be honored
'no_proxy' => null, // string - a comma separated list of hosts that do not require a proxy to be reached
'timeout' => null, // float - the idle timeout (in seconds) - defaults to ini_get('default_socket_timeout')
'max_duration' => 0, // float - the maximum execution time (in seconds) for the request+response as a whole;
// a value lower than or equal to 0 means it is unlimited
'bindto' => '0', // string - the interface or the local socket to bind to
'verify_peer' => true, // see https://php.net/context.ssl for the following options
'verify_host' => true,
'cafile' => null,
'capath' => null,
'local_cert' => null,
'local_pk' => null,
'passphrase' => null,
'ciphers' => null,
'peer_fingerprint' => null,
'capture_peer_cert_chain' => false,
'extra' => [], // array - additional options that can be ignored if unsupported, unlike regular options
];
/**
* Requests an HTTP resource.
*
* Responses MUST be lazy, but their status code MUST be
* checked even if none of their public methods are called.
*
* Implementations are not required to support all options described above; they can also
* support more custom options; but in any case, they MUST throw a TransportExceptionInterface
* when an unsupported option is passed.
*
* @throws TransportExceptionInterface When an unsupported option is passed
*/
public function request(string $method, string $url, array $options = []): ResponseInterface;
/**
* Yields responses chunk by chunk as they complete.
*
* @param ResponseInterface|iterable<array-key, ResponseInterface> $responses One or more responses created by the current HTTP client
* @param float|null $timeout The idle timeout before yielding timeout chunks
*/
public function stream($responses, ?float $timeout = null): ResponseStreamInterface;
}

View File

@@ -0,0 +1,19 @@
Copyright (c) 2018-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,9 @@
Symfony HttpClient Contracts
============================
A set of abstractions extracted out of the Symfony components.
Can be used to build on semantics that the Symfony components proved useful - and
that already have battle tested implementations.
See https://github.com/symfony/contracts/blob/main/README.md for more information.

View File

@@ -0,0 +1,109 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* A (lazily retrieved) HTTP response.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface ResponseInterface
{
/**
* Gets the HTTP status code of the response.
*
* @throws TransportExceptionInterface when a network error occurs
*/
public function getStatusCode(): int;
/**
* Gets the HTTP headers of the response.
*
* @param bool $throw Whether an exception should be thrown on 3/4/5xx status codes
*
* @return string[][] The headers of the response keyed by header names in lowercase
*
* @throws TransportExceptionInterface When a network error occurs
* @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
* @throws ClientExceptionInterface On a 4xx when $throw is true
* @throws ServerExceptionInterface On a 5xx when $throw is true
*/
public function getHeaders(bool $throw = true): array;
/**
* Gets the response body as a string.
*
* @param bool $throw Whether an exception should be thrown on 3/4/5xx status codes
*
* @throws TransportExceptionInterface When a network error occurs
* @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
* @throws ClientExceptionInterface On a 4xx when $throw is true
* @throws ServerExceptionInterface On a 5xx when $throw is true
*/
public function getContent(bool $throw = true): string;
/**
* Gets the response body decoded as array, typically from a JSON payload.
*
* @param bool $throw Whether an exception should be thrown on 3/4/5xx status codes
*
* @throws DecodingExceptionInterface When the body cannot be decoded to an array
* @throws TransportExceptionInterface When a network error occurs
* @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
* @throws ClientExceptionInterface On a 4xx when $throw is true
* @throws ServerExceptionInterface On a 5xx when $throw is true
*/
public function toArray(bool $throw = true): array;
/**
* Closes the response stream and all related buffers.
*
* No further chunk will be yielded after this method has been called.
*/
public function cancel(): void;
/**
* Returns info coming from the transport layer.
*
* This method SHOULD NOT throw any ExceptionInterface and SHOULD be non-blocking.
* The returned info is "live": it can be empty and can change from one call to
* another, as the request/response progresses.
*
* The following info MUST be returned:
* - canceled (bool) - true if the response was canceled using ResponseInterface::cancel(), false otherwise
* - error (string|null) - the error message when the transfer was aborted, null otherwise
* - http_code (int) - the last response code or 0 when it is not known yet
* - http_method (string) - the HTTP verb of the last request
* - redirect_count (int) - the number of redirects followed while executing the request
* - redirect_url (string|null) - the resolved location of redirect responses, null otherwise
* - response_headers (array) - an array modelled after the special $http_response_header variable
* - start_time (float) - the time when the request was sent or 0.0 when it's pending
* - url (string) - the last effective URL of the request
* - user_data (mixed) - the value of the "user_data" request option, null if not set
*
* When the "capture_peer_cert_chain" option is true, the "peer_certificate_chain"
* attribute SHOULD list the peer certificates as an array of OpenSSL X.509 resources.
*
* Other info SHOULD be named after curl_getinfo()'s associative return value.
*
* @return mixed An array of all available info, or one of them when $type is
* provided, or null when an unsupported type is requested
*/
public function getInfo(?string $type = null);
}

View File

@@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient;
/**
* Yields response chunks, returned by HttpClientInterface::stream().
*
* @author Nicolas Grekas <p@tchwork.com>
*
* @extends \Iterator<ResponseInterface, ChunkInterface>
*/
interface ResponseStreamInterface extends \Iterator
{
public function key(): ResponseInterface;
public function current(): ChunkInterface;
}

View File

@@ -0,0 +1,205 @@
<?php
if ('cli-server' !== \PHP_SAPI) {
// safe guard against unwanted execution
throw new \Exception("You cannot run this script directly, it's a fixture for TestHttpServer.");
}
$vars = [];
if (!$_POST) {
$_POST = json_decode(file_get_contents('php://input'), true);
$_POST['content-type'] = $_SERVER['HTTP_CONTENT_TYPE'] ?? '?';
}
$headers = [
'SERVER_PROTOCOL',
'SERVER_NAME',
'REQUEST_URI',
'REQUEST_METHOD',
'PHP_AUTH_USER',
'PHP_AUTH_PW',
'REMOTE_ADDR',
'REMOTE_PORT',
];
foreach ($headers as $k) {
if (isset($_SERVER[$k])) {
$vars[$k] = $_SERVER[$k];
}
}
foreach ($_SERVER as $k => $v) {
if (0 === strpos($k, 'HTTP_')) {
$vars[$k] = $v;
}
}
$json = json_encode($vars, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE);
switch (parse_url($vars['REQUEST_URI'], \PHP_URL_PATH)) {
default:
exit;
case '/head':
header('Content-Length: '.strlen($json), true);
break;
case '/':
case '/?a=a&b=b':
case 'http://127.0.0.1:8057/':
case 'http://localhost:8057/':
ob_start('ob_gzhandler');
break;
case '/103':
header('HTTP/1.1 103 Early Hints');
header('Link: </style.css>; rel=preload; as=style', false);
header('Link: </script.js>; rel=preload; as=script', false);
flush();
usleep(1000);
echo "HTTP/1.1 200 OK\r\n";
echo "Date: Fri, 26 May 2017 10:02:11 GMT\r\n";
echo "Content-Length: 13\r\n";
echo "\r\n";
echo 'Here the body';
exit;
case '/404':
header('Content-Type: application/json', true, 404);
break;
case '/404-gzipped':
header('Content-Type: text/plain', true, 404);
ob_start('ob_gzhandler');
@ob_flush();
flush();
usleep(300000);
echo 'some text';
exit;
case '/301':
if ('Basic Zm9vOmJhcg==' === $vars['HTTP_AUTHORIZATION']) {
header('Location: http://127.0.0.1:8057/302', true, 301);
}
break;
case '/301/bad-tld':
header('Location: http://foo.example.', true, 301);
break;
case '/301/invalid':
header('Location: //?foo=bar', true, 301);
break;
case '/301/proxy':
case 'http://localhost:8057/301/proxy':
case 'http://127.0.0.1:8057/301/proxy':
header('Location: http://localhost:8057/', true, 301);
break;
case '/302':
if (!isset($vars['HTTP_AUTHORIZATION'])) {
$location = $_GET['location'] ?? 'http://localhost:8057/';
header('Location: '.$location, true, 302);
}
break;
case '/302/relative':
header('Location: ..', true, 302);
break;
case '/304':
header('Content-Length: 10', true, 304);
echo '12345';
return;
case '/307':
header('Location: http://localhost:8057/post', true, 307);
break;
case '/length-broken':
header('Content-Length: 1000');
break;
case '/post':
$output = json_encode($_POST + ['REQUEST_METHOD' => $vars['REQUEST_METHOD']], \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE);
header('Content-Type: application/json', true);
header('Content-Length: '.strlen($output));
echo $output;
exit;
case '/timeout-header':
usleep(300000);
break;
case '/timeout-body':
echo '<1>';
@ob_flush();
flush();
usleep(500000);
echo '<2>';
exit;
case '/timeout-long':
ignore_user_abort(false);
sleep(1);
while (true) {
echo '<1>';
@ob_flush();
flush();
usleep(500);
}
exit;
case '/chunked':
header('Transfer-Encoding: chunked');
echo "8\r\nSymfony \r\n5\r\nis aw\r\n6\r\nesome!\r\n0\r\n\r\n";
exit;
case '/chunked-broken':
header('Transfer-Encoding: chunked');
echo "8\r\nSymfony \r\n5\r\nis aw\r\n6\r\ne";
exit;
case '/gzip-broken':
header('Content-Encoding: gzip');
echo str_repeat('-', 1000);
exit;
case '/max-duration':
ignore_user_abort(false);
while (true) {
echo '<1>';
@ob_flush();
flush();
usleep(500);
}
exit;
case '/json':
header('Content-Type: application/json');
echo json_encode([
'documents' => [
['id' => '/json/1'],
['id' => '/json/2'],
['id' => '/json/3'],
],
]);
exit;
case '/json/1':
case '/json/2':
case '/json/3':
header('Content-Type: application/json');
echo json_encode([
'title' => $vars['REQUEST_URI'],
]);
exit;
}
header('Content-Type: application/json', true);
echo $json;

View File

@@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Test;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\Process;
class TestHttpServer
{
private static $process = [];
/**
* @return Process
*/
public static function start(int $port = 8057)
{
if (0 > $port) {
$port = -$port;
$ip = '[::1]';
} else {
$ip = '127.0.0.1';
}
if (isset(self::$process[$port])) {
self::$process[$port]->stop();
} else {
register_shutdown_function(static function () use ($port) {
self::$process[$port]->stop();
});
}
$finder = new PhpExecutableFinder();
$process = new Process(array_merge([$finder->find(false)], $finder->findArguments(), ['-dopcache.enable=0', '-dvariables_order=EGPCS', '-S', $ip.':'.$port]));
$process->setWorkingDirectory(__DIR__.'/Fixtures/web');
$process->start();
self::$process[$port] = $process;
do {
usleep(50000);
} while (!@fopen('http://'.$ip.':'.$port, 'r'));
return $process;
}
public static function stop(int $port = 8057)
{
if (isset(self::$process[$port])) {
self::$process[$port]->stop();
}
}
}

View File

@@ -0,0 +1,182 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient;
use Amp\CancelledException;
use Amp\Http\Client\DelegateHttpClient;
use Amp\Http\Client\InterceptedHttpClient;
use Amp\Http\Client\PooledHttpClient;
use Amp\Http\Client\Request;
use Amp\Http\Tunnel\Http1TunnelConnector;
use Amp\Promise;
use KadenceWP\KadenceBlocks\Psr\Log\LoggerAwareInterface;
use KadenceWP\KadenceBlocks\Psr\Log\LoggerAwareTrait;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\TransportException;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal\AmpClientState;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\AmpResponse;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\ResponseStream;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\HttpClientInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseStreamInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\Service\ResetInterface;
if (!interface_exists(DelegateHttpClient::class)) {
throw new \LogicException('You cannot use "KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\AmpHttpClient" as the "amphp/http-client" package is not installed. Try running "composer require amphp/http-client:^4.2.1".');
}
if (!interface_exists(Promise::class)) {
throw new \LogicException('You cannot use "KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\AmpHttpClient" as the installed "amphp/http-client" is not compatible with this version of "symfony/http-client". Try downgrading "amphp/http-client" to "^4.2.1".');
}
/**
* A portable implementation of the HttpClientInterface contracts based on Amp's HTTP client.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class AmpHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface
{
use HttpClientTrait;
use LoggerAwareTrait;
private $defaultOptions = self::OPTIONS_DEFAULTS;
private static $emptyDefaults = self::OPTIONS_DEFAULTS;
/** @var AmpClientState */
private $multi;
/**
* @param array $defaultOptions Default requests' options
* @param callable|null $clientConfigurator A callable that builds a {@see DelegateHttpClient} from a {@see PooledHttpClient};
* passing null builds an {@see InterceptedHttpClient} with 2 retries on failures
* @param int $maxHostConnections The maximum number of connections to a single host
* @param int $maxPendingPushes The maximum number of pushed responses to accept in the queue
*
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
*/
public function __construct(array $defaultOptions = [], ?callable $clientConfigurator = null, int $maxHostConnections = 6, int $maxPendingPushes = 50)
{
$this->defaultOptions['buffer'] = $this->defaultOptions['buffer'] ?? \Closure::fromCallable([__CLASS__, 'shouldBuffer']);
if ($defaultOptions) {
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
}
$this->multi = new AmpClientState($clientConfigurator, $maxHostConnections, $maxPendingPushes, $this->logger);
}
/**
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
*
* {@inheritdoc}
*/
public function request(string $method, string $url, array $options = []): ResponseInterface
{
[$url, $options] = self::prepareRequest($method, $url, $options, $this->defaultOptions);
$options['proxy'] = self::getProxy($options['proxy'], $url, $options['no_proxy']);
if (null !== $options['proxy'] && !class_exists(Http1TunnelConnector::class)) {
throw new \LogicException('You cannot use the "proxy" option as the "amphp/http-tunnel" package is not installed. Try running "composer require amphp/http-tunnel".');
}
if ($options['bindto']) {
if (0 === strpos($options['bindto'], 'if!')) {
throw new TransportException(__CLASS__.' cannot bind to network interfaces, use e.g. CurlHttpClient instead.');
}
if (0 === strpos($options['bindto'], 'host!')) {
$options['bindto'] = substr($options['bindto'], 5);
}
}
if (('' !== $options['body'] || 'POST' === $method || isset($options['normalized_headers']['content-length'])) && !isset($options['normalized_headers']['content-type'])) {
$options['headers'][] = 'Content-Type: application/x-www-form-urlencoded';
}
if (!isset($options['normalized_headers']['user-agent'])) {
$options['headers'][] = 'User-Agent: Symfony HttpClient/Amp';
}
if (0 < $options['max_duration']) {
$options['timeout'] = min($options['max_duration'], $options['timeout']);
}
if ($options['resolve']) {
$this->multi->dnsCache = $options['resolve'] + $this->multi->dnsCache;
}
if ($options['peer_fingerprint'] && !isset($options['peer_fingerprint']['pin-sha256'])) {
throw new TransportException(__CLASS__.' supports only "pin-sha256" fingerprints.');
}
$request = new Request(implode('', $url), $method);
$request->setBodySizeLimit(0);
if ($options['http_version']) {
switch ((float) $options['http_version']) {
case 1.0: $request->setProtocolVersions(['1.0']); break;
case 1.1: $request->setProtocolVersions(['1.1', '1.0']); break;
default: $request->setProtocolVersions(['2', '1.1', '1.0']); break;
}
}
foreach ($options['headers'] as $v) {
$h = explode(': ', $v, 2);
$request->addHeader($h[0], $h[1]);
}
$request->setTcpConnectTimeout(1000 * $options['timeout']);
$request->setTlsHandshakeTimeout(1000 * $options['timeout']);
$request->setTransferTimeout(1000 * $options['max_duration']);
if (method_exists($request, 'setInactivityTimeout')) {
$request->setInactivityTimeout(0);
}
if ('' !== $request->getUri()->getUserInfo() && !$request->hasHeader('authorization')) {
$auth = explode(':', $request->getUri()->getUserInfo(), 2);
$auth = array_map('rawurldecode', $auth) + [1 => ''];
$request->setHeader('Authorization', 'Basic '.base64_encode(implode(':', $auth)));
}
return new AmpResponse($this->multi, $request, $options, $this->logger);
}
/**
* {@inheritdoc}
*/
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
{
if ($responses instanceof AmpResponse) {
$responses = [$responses];
} elseif (!is_iterable($responses)) {
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of AmpResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
}
return new ResponseStream(AmpResponse::stream($responses, $timeout));
}
public function reset()
{
$this->multi->dnsCache = [];
foreach ($this->multi->pushedResponses as $authority => $pushedResponses) {
foreach ($pushedResponses as [$pushedUrl, $pushDeferred]) {
$pushDeferred->fail(new CancelledException());
if ($this->logger) {
$this->logger->debug(sprintf('Unused pushed response: "%s"', $pushedUrl));
}
}
}
$this->multi->pushedResponses = [];
}
}

View File

@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\AsyncResponse;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\ResponseStream;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseStreamInterface;
/**
* Eases with processing responses while streaming them.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
trait AsyncDecoratorTrait
{
use DecoratorTrait;
/**
* {@inheritdoc}
*
* @return AsyncResponse
*/
abstract public function request(string $method, string $url, array $options = []): ResponseInterface;
/**
* {@inheritdoc}
*/
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
{
if ($responses instanceof AsyncResponse) {
$responses = [$responses];
} elseif (!is_iterable($responses)) {
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of AsyncResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
}
return new ResponseStream(AsyncResponse::stream($responses, $timeout, static::class));
}
}

View File

@@ -0,0 +1,54 @@
CHANGELOG
=========
5.4
---
* Add `MockHttpClient::setResponseFactory()` method to be able to set response factory after client creating
5.3
---
* Implement `HttpClientInterface::withOptions()` from `symfony/contracts` v2.4
* Add `DecoratorTrait` to ease writing simple decorators
5.2.0
-----
* added `AsyncDecoratorTrait` to ease processing responses without breaking async
* added support for pausing responses with a new `pause_handler` callable exposed as an info item
* added `StreamableInterface` to ease turning responses into PHP streams
* added `MockResponse::getRequestMethod()` and `getRequestUrl()` to allow inspecting which request has been sent
* added `EventSourceHttpClient` a Server-Sent events stream implementing the [EventSource specification](https://www.w3.org/TR/eventsource/#eventsource)
* added option "extra.curl" to allow setting additional curl options in `CurlHttpClient`
* added `RetryableHttpClient` to automatically retry failed HTTP requests.
* added `extra.trace_content` option to `TraceableHttpClient` to prevent it from keeping the content in memory
5.1.0
-----
* added `NoPrivateNetworkHttpClient` decorator
* added `AmpHttpClient`, a portable HTTP/2 implementation based on Amp
* added `LoggerAwareInterface` to `ScopingHttpClient` and `TraceableHttpClient`
* made `HttpClient::create()` return an `AmpHttpClient` when `amphp/http-client` is found but curl is not or too old
4.4.0
-----
* added `canceled` to `ResponseInterface::getInfo()`
* added `HttpClient::createForBaseUri()`
* added `HttplugClient` with support for sync and async requests
* added `max_duration` option
* added support for NTLM authentication
* added `StreamWrapper` to cast any `ResponseInterface` instances to PHP streams.
* added `$response->toStream()` to cast responses to regular PHP streams
* made `Psr18Client` implement relevant PSR-17 factories and have streaming responses
* added `TraceableHttpClient`, `HttpClientDataCollector` and `HttpClientPass` to integrate with the web profiler
* allow enabling buffering conditionally with a Closure
* allow option "buffer" to be a stream resource
* allow arbitrary values for the "json" option
4.3.0
-----
* added the component

View File

@@ -0,0 +1,152 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\MockResponse;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\ResponseStream;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
use Symfony\Component\HttpKernel\HttpCache\StoreInterface;
use Symfony\Component\HttpKernel\HttpClientKernel;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\HttpClientInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseStreamInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\Service\ResetInterface;
/**
* Adds caching on top of an HTTP client.
*
* The implementation buffers responses in memory and doesn't stream directly from the network.
* You can disable/enable this layer by setting option "no_cache" under "extra" to true/false.
* By default, caching is enabled unless the "buffer" option is set to false.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class CachingHttpClient implements HttpClientInterface, ResetInterface
{
use HttpClientTrait;
private $client;
private $cache;
private $defaultOptions = self::OPTIONS_DEFAULTS;
public function __construct(HttpClientInterface $client, StoreInterface $store, array $defaultOptions = [])
{
if (!class_exists(HttpClientKernel::class)) {
throw new \LogicException(sprintf('Using "%s" requires that the HttpKernel component version 4.3 or higher is installed, try running "composer require symfony/http-kernel:^5.4".', __CLASS__));
}
$this->client = $client;
$kernel = new HttpClientKernel($client);
$this->cache = new HttpCache($kernel, $store, null, $defaultOptions);
unset($defaultOptions['debug']);
unset($defaultOptions['default_ttl']);
unset($defaultOptions['private_headers']);
unset($defaultOptions['allow_reload']);
unset($defaultOptions['allow_revalidate']);
unset($defaultOptions['stale_while_revalidate']);
unset($defaultOptions['stale_if_error']);
unset($defaultOptions['trace_level']);
unset($defaultOptions['trace_header']);
if ($defaultOptions) {
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
}
}
/**
* {@inheritdoc}
*/
public function request(string $method, string $url, array $options = []): ResponseInterface
{
[$url, $options] = $this->prepareRequest($method, $url, $options, $this->defaultOptions, true);
$url = implode('', $url);
if (!empty($options['body']) || !empty($options['extra']['no_cache']) || !\in_array($method, ['GET', 'HEAD', 'OPTIONS'])) {
return $this->client->request($method, $url, $options);
}
$request = Request::create($url, $method);
$request->attributes->set('http_client_options', $options);
foreach ($options['normalized_headers'] as $name => $values) {
if ('cookie' !== $name) {
foreach ($values as $value) {
$request->headers->set($name, substr($value, 2 + \strlen($name)), false);
}
continue;
}
foreach ($values as $cookies) {
foreach (explode('; ', substr($cookies, \strlen('Cookie: '))) as $cookie) {
if ('' !== $cookie) {
$cookie = explode('=', $cookie, 2);
$request->cookies->set($cookie[0], $cookie[1] ?? '');
}
}
}
}
$response = $this->cache->handle($request);
$response = new MockResponse($response->getContent(), [
'http_code' => $response->getStatusCode(),
'response_headers' => $response->headers->allPreserveCase(),
]);
return MockResponse::fromRequest($method, $url, $options, $response);
}
/**
* {@inheritdoc}
*/
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
{
if ($responses instanceof ResponseInterface) {
$responses = [$responses];
} elseif (!is_iterable($responses)) {
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of ResponseInterface objects, "%s" given.', __METHOD__, get_debug_type($responses)));
}
$mockResponses = [];
$clientResponses = [];
foreach ($responses as $response) {
if ($response instanceof MockResponse) {
$mockResponses[] = $response;
} else {
$clientResponses[] = $response;
}
}
if (!$mockResponses) {
return $this->client->stream($clientResponses, $timeout);
}
if (!$clientResponses) {
return new ResponseStream(MockResponse::stream($mockResponses, $timeout));
}
return new ResponseStream((function () use ($mockResponses, $clientResponses, $timeout) {
yield from MockResponse::stream($mockResponses, $timeout);
yield $this->client->stream($clientResponses, $timeout);
})());
}
public function reset()
{
if ($this->client instanceof ResetInterface) {
$this->client->reset();
}
}
}

View File

@@ -0,0 +1,87 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Chunk;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ChunkInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class DataChunk implements ChunkInterface
{
private $offset = 0;
private $content = '';
public function __construct(int $offset = 0, string $content = '')
{
$this->offset = $offset;
$this->content = $content;
}
/**
* {@inheritdoc}
*/
public function isTimeout(): bool
{
return false;
}
/**
* {@inheritdoc}
*/
public function isFirst(): bool
{
return false;
}
/**
* {@inheritdoc}
*/
public function isLast(): bool
{
return false;
}
/**
* {@inheritdoc}
*/
public function getInformationalStatus(): ?array
{
return null;
}
/**
* {@inheritdoc}
*/
public function getContent(): string
{
return $this->content;
}
/**
* {@inheritdoc}
*/
public function getOffset(): int
{
return $this->offset;
}
/**
* {@inheritdoc}
*/
public function getError(): ?string
{
return null;
}
}

View File

@@ -0,0 +1,140 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Chunk;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\TimeoutException;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\TransportException;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ChunkInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class ErrorChunk implements ChunkInterface
{
private $didThrow = false;
private $offset;
private $errorMessage;
private $error;
/**
* @param \Throwable|string $error
*/
public function __construct(int $offset, $error)
{
$this->offset = $offset;
if (\is_string($error)) {
$this->errorMessage = $error;
} else {
$this->error = $error;
$this->errorMessage = $error->getMessage();
}
}
/**
* {@inheritdoc}
*/
public function isTimeout(): bool
{
$this->didThrow = true;
if (null !== $this->error) {
throw new TransportException($this->errorMessage, 0, $this->error);
}
return true;
}
/**
* {@inheritdoc}
*/
public function isFirst(): bool
{
$this->didThrow = true;
throw null !== $this->error ? new TransportException($this->errorMessage, 0, $this->error) : new TimeoutException($this->errorMessage);
}
/**
* {@inheritdoc}
*/
public function isLast(): bool
{
$this->didThrow = true;
throw null !== $this->error ? new TransportException($this->errorMessage, 0, $this->error) : new TimeoutException($this->errorMessage);
}
/**
* {@inheritdoc}
*/
public function getInformationalStatus(): ?array
{
$this->didThrow = true;
throw null !== $this->error ? new TransportException($this->errorMessage, 0, $this->error) : new TimeoutException($this->errorMessage);
}
/**
* {@inheritdoc}
*/
public function getContent(): string
{
$this->didThrow = true;
throw null !== $this->error ? new TransportException($this->errorMessage, 0, $this->error) : new TimeoutException($this->errorMessage);
}
/**
* {@inheritdoc}
*/
public function getOffset(): int
{
return $this->offset;
}
/**
* {@inheritdoc}
*/
public function getError(): ?string
{
return $this->errorMessage;
}
/**
* @return bool Whether the wrapped error has been thrown or not
*/
public function didThrow(?bool $didThrow = null): bool
{
if (null !== $didThrow && $this->didThrow !== $didThrow) {
return !$this->didThrow = $didThrow;
}
return $this->didThrow;
}
public function __sleep(): array
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
if (!$this->didThrow) {
$this->didThrow = true;
throw null !== $this->error ? new TransportException($this->errorMessage, 0, $this->error) : new TimeoutException($this->errorMessage);
}
}
}

View File

@@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Chunk;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class FirstChunk extends DataChunk
{
/**
* {@inheritdoc}
*/
public function isFirst(): bool
{
return true;
}
}

View File

@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Chunk;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class InformationalChunk extends DataChunk
{
private $status;
public function __construct(int $statusCode, array $headers)
{
$this->status = [$statusCode, $headers];
}
/**
* {@inheritdoc}
*/
public function getInformationalStatus(): ?array
{
return $this->status;
}
}

View File

@@ -0,0 +1,28 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Chunk;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class LastChunk extends DataChunk
{
/**
* {@inheritdoc}
*/
public function isLast(): bool
{
return true;
}
}

View File

@@ -0,0 +1,79 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Chunk;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ChunkInterface;
/**
* @author Antoine Bluchet <soyuka@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
final class ServerSentEvent extends DataChunk implements ChunkInterface
{
private $data = '';
private $id = '';
private $type = 'message';
private $retry = 0;
public function __construct(string $content)
{
parent::__construct(-1, $content);
// remove BOM
if (0 === strpos($content, "\xEF\xBB\xBF")) {
$content = substr($content, 3);
}
foreach (preg_split("/(?:\r\n|[\r\n])/", $content) as $line) {
if (0 === $i = strpos($line, ':')) {
continue;
}
$i = false === $i ? \strlen($line) : $i;
$field = substr($line, 0, $i);
$i += 1 + (' ' === ($line[1 + $i] ?? ''));
switch ($field) {
case 'id': $this->id = substr($line, $i); break;
case 'event': $this->type = substr($line, $i); break;
case 'data': $this->data .= ('' === $this->data ? '' : "\n").substr($line, $i); break;
case 'retry':
$retry = substr($line, $i);
if ('' !== $retry && \strlen($retry) === strspn($retry, '0123456789')) {
$this->retry = $retry / 1000.0;
}
break;
}
}
}
public function getId(): string
{
return $this->id;
}
public function getType(): string
{
return $this->type;
}
public function getData(): string
{
return $this->data;
}
public function getRetry(): float
{
return $this->retry;
}
}

View File

@@ -0,0 +1,574 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient;
use KadenceWP\KadenceBlocks\Psr\Log\LoggerAwareInterface;
use KadenceWP\KadenceBlocks\Psr\Log\LoggerInterface;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\InvalidArgumentException;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\TransportException;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal\CurlClientState;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal\PushedResponse;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\CurlResponse;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\ResponseStream;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\HttpClientInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseStreamInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\Service\ResetInterface;
/**
* A performant implementation of the HttpClientInterface contracts based on the curl extension.
*
* This provides fully concurrent HTTP requests, with transparent
* HTTP/2 push when a curl version that supports it is installed.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface
{
use HttpClientTrait;
private $defaultOptions = self::OPTIONS_DEFAULTS + [
'auth_ntlm' => null, // array|string - an array containing the username as first value, and optionally the
// password as the second one; or string like username:password - enabling NTLM auth
'extra' => [
'curl' => [], // A list of extra curl options indexed by their corresponding CURLOPT_*
],
];
private static $emptyDefaults = self::OPTIONS_DEFAULTS + ['auth_ntlm' => null];
/**
* @var LoggerInterface|null
*/
private $logger;
private $maxHostConnections;
private $maxPendingPushes;
/**
* An internal object to share state between the client and its responses.
*
* @var CurlClientState
*/
private $multi;
/**
* @param array $defaultOptions Default request's options
* @param int $maxHostConnections The maximum number of connections to a single host
* @param int $maxPendingPushes The maximum number of pushed responses to accept in the queue
*
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
*/
public function __construct(array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 0)
{
if (!\extension_loaded('curl')) {
throw new \LogicException('You cannot use the "KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\CurlHttpClient" as the "curl" extension is not installed.');
}
$this->maxHostConnections = $maxHostConnections;
$this->maxPendingPushes = $maxPendingPushes;
$this->defaultOptions['buffer'] = $this->defaultOptions['buffer'] ?? \Closure::fromCallable([__CLASS__, 'shouldBuffer']);
if ($defaultOptions) {
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
}
}
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
if (isset($this->multi)) {
$this->multi->logger = $logger;
}
}
/**
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
*
* {@inheritdoc}
*/
public function request(string $method, string $url, array $options = []): ResponseInterface
{
$multi = $this->ensureState();
[$url, $options] = self::prepareRequest($method, $url, $options, $this->defaultOptions);
$scheme = $url['scheme'];
$authority = $url['authority'];
$host = parse_url($authority, \PHP_URL_HOST);
$proxy = self::getProxyUrl($options['proxy'], $url);
$url = implode('', $url);
if (!isset($options['normalized_headers']['user-agent'])) {
$options['headers'][] = 'User-Agent: Symfony HttpClient/Curl';
}
$curlopts = [
\CURLOPT_URL => $url,
\CURLOPT_TCP_NODELAY => true,
\CURLOPT_PROTOCOLS => \CURLPROTO_HTTP | \CURLPROTO_HTTPS,
\CURLOPT_REDIR_PROTOCOLS => \CURLPROTO_HTTP | \CURLPROTO_HTTPS,
\CURLOPT_FOLLOWLOCATION => true,
\CURLOPT_MAXREDIRS => 0 < $options['max_redirects'] ? $options['max_redirects'] : 0,
\CURLOPT_COOKIEFILE => '', // Keep track of cookies during redirects
\CURLOPT_TIMEOUT => 0,
\CURLOPT_PROXY => $proxy,
\CURLOPT_NOPROXY => $options['no_proxy'] ?? $_SERVER['no_proxy'] ?? $_SERVER['NO_PROXY'] ?? '',
\CURLOPT_SSL_VERIFYPEER => $options['verify_peer'],
\CURLOPT_SSL_VERIFYHOST => $options['verify_host'] ? 2 : 0,
\CURLOPT_CAINFO => $options['cafile'],
\CURLOPT_CAPATH => $options['capath'],
\CURLOPT_SSL_CIPHER_LIST => $options['ciphers'],
\CURLOPT_SSLCERT => $options['local_cert'],
\CURLOPT_SSLKEY => $options['local_pk'],
\CURLOPT_KEYPASSWD => $options['passphrase'],
\CURLOPT_CERTINFO => $options['capture_peer_cert_chain'],
];
if (1.0 === (float) $options['http_version']) {
$curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0;
} elseif (1.1 === (float) $options['http_version']) {
$curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
} elseif (\defined('CURL_VERSION_HTTP2') && (\CURL_VERSION_HTTP2 & CurlClientState::$curlVersion['features']) && ('https:' === $scheme || 2.0 === (float) $options['http_version'])) {
$curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0;
}
if (isset($options['auth_ntlm'])) {
$curlopts[\CURLOPT_HTTPAUTH] = \CURLAUTH_NTLM;
$curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
if (\is_array($options['auth_ntlm'])) {
$count = \count($options['auth_ntlm']);
if ($count <= 0 || $count > 2) {
throw new InvalidArgumentException(sprintf('Option "auth_ntlm" must contain 1 or 2 elements, %d given.', $count));
}
$options['auth_ntlm'] = implode(':', $options['auth_ntlm']);
}
if (!\is_string($options['auth_ntlm'])) {
throw new InvalidArgumentException(sprintf('Option "auth_ntlm" must be a string or an array, "%s" given.', get_debug_type($options['auth_ntlm'])));
}
$curlopts[\CURLOPT_USERPWD] = $options['auth_ntlm'];
}
if (!\ZEND_THREAD_SAFE) {
$curlopts[\CURLOPT_DNS_USE_GLOBAL_CACHE] = false;
}
if (\defined('CURLOPT_HEADEROPT') && \defined('CURLHEADER_SEPARATE')) {
$curlopts[\CURLOPT_HEADEROPT] = \CURLHEADER_SEPARATE;
}
// curl's resolve feature varies by host:port but ours varies by host only, let's handle this with our own DNS map
if (isset($multi->dnsCache->hostnames[$host])) {
$options['resolve'] += [$host => $multi->dnsCache->hostnames[$host]];
}
if ($options['resolve'] || $multi->dnsCache->evictions) {
// First reset any old DNS cache entries then add the new ones
$resolve = $multi->dnsCache->evictions;
$multi->dnsCache->evictions = [];
$port = parse_url($authority, \PHP_URL_PORT) ?: ('http:' === $scheme ? 80 : 443);
if ($resolve && 0x072A00 > CurlClientState::$curlVersion['version_number']) {
// DNS cache removals require curl 7.42 or higher
$multi->reset();
}
foreach ($options['resolve'] as $resolveHost => $ip) {
$resolve[] = null === $ip ? "-$resolveHost:$port" : "$resolveHost:$port:$ip";
$multi->dnsCache->hostnames[$resolveHost] = $ip;
$multi->dnsCache->removals["-$resolveHost:$port"] = "-$resolveHost:$port";
}
$curlopts[\CURLOPT_RESOLVE] = $resolve;
}
if ('POST' === $method) {
// Use CURLOPT_POST to have browser-like POST-to-GET redirects for 301, 302 and 303
$curlopts[\CURLOPT_POST] = true;
} elseif ('HEAD' === $method) {
$curlopts[\CURLOPT_NOBODY] = true;
} else {
$curlopts[\CURLOPT_CUSTOMREQUEST] = $method;
}
if ('\\' !== \DIRECTORY_SEPARATOR && $options['timeout'] < 1) {
$curlopts[\CURLOPT_NOSIGNAL] = true;
}
if (\extension_loaded('zlib') && !isset($options['normalized_headers']['accept-encoding'])) {
$options['headers'][] = 'Accept-Encoding: gzip'; // Expose only one encoding, some servers mess up when more are provided
}
$body = $options['body'];
foreach ($options['headers'] as $i => $header) {
if (\is_string($body) && '' !== $body && 0 === stripos($header, 'Content-Length: ')) {
// Let curl handle Content-Length headers
unset($options['headers'][$i]);
continue;
}
if (':' === $header[-2] && \strlen($header) - 2 === strpos($header, ': ')) {
// curl requires a special syntax to send empty headers
$curlopts[\CURLOPT_HTTPHEADER][] = substr_replace($header, ';', -2);
} else {
$curlopts[\CURLOPT_HTTPHEADER][] = $header;
}
}
// Prevent curl from sending its default Accept and Expect headers
foreach (['accept', 'expect'] as $header) {
if (!isset($options['normalized_headers'][$header][0])) {
$curlopts[\CURLOPT_HTTPHEADER][] = $header.':';
}
}
if (!\is_string($body)) {
if (\is_resource($body)) {
$curlopts[\CURLOPT_INFILE] = $body;
} else {
$eof = false;
$buffer = '';
$curlopts[\CURLOPT_READFUNCTION] = static function ($ch, $fd, $length) use ($body, &$buffer, &$eof) {
return self::readRequestBody($length, $body, $buffer, $eof);
};
}
if (isset($options['normalized_headers']['content-length'][0])) {
$curlopts[\CURLOPT_INFILESIZE] = (int) substr($options['normalized_headers']['content-length'][0], \strlen('Content-Length: '));
}
if (!isset($options['normalized_headers']['transfer-encoding'])) {
$curlopts[\CURLOPT_HTTPHEADER][] = 'Transfer-Encoding:'.(isset($curlopts[\CURLOPT_INFILESIZE]) ? '' : ' chunked');
}
if ('POST' !== $method) {
$curlopts[\CURLOPT_UPLOAD] = true;
if (!isset($options['normalized_headers']['content-type']) && 0 !== ($curlopts[\CURLOPT_INFILESIZE] ?? null)) {
$curlopts[\CURLOPT_HTTPHEADER][] = 'Content-Type: application/x-www-form-urlencoded';
}
}
} elseif ('' !== $body || 'POST' === $method) {
$curlopts[\CURLOPT_POSTFIELDS] = $body;
}
if ($options['peer_fingerprint']) {
if (!isset($options['peer_fingerprint']['pin-sha256'])) {
throw new TransportException(__CLASS__.' supports only "pin-sha256" fingerprints.');
}
$curlopts[\CURLOPT_PINNEDPUBLICKEY] = 'sha256//'.implode(';sha256//', $options['peer_fingerprint']['pin-sha256']);
}
if ($options['bindto']) {
if (file_exists($options['bindto'])) {
$curlopts[\CURLOPT_UNIX_SOCKET_PATH] = $options['bindto'];
} elseif (!str_starts_with($options['bindto'], 'if!') && preg_match('/^(.*):(\d+)$/', $options['bindto'], $matches)) {
$curlopts[\CURLOPT_INTERFACE] = trim($matches[1], '[]');
$curlopts[\CURLOPT_LOCALPORT] = $matches[2];
} else {
$curlopts[\CURLOPT_INTERFACE] = $options['bindto'];
}
}
if (0 < $options['max_duration']) {
$curlopts[\CURLOPT_TIMEOUT_MS] = 1000 * $options['max_duration'];
}
if (!empty($options['extra']['curl']) && \is_array($options['extra']['curl'])) {
$this->validateExtraCurlOptions($options['extra']['curl']);
$curlopts += $options['extra']['curl'];
}
if ($pushedResponse = $multi->pushedResponses[$url] ?? null) {
unset($multi->pushedResponses[$url]);
if (self::acceptPushForRequest($method, $options, $pushedResponse)) {
$this->logger && $this->logger->debug(sprintf('Accepting pushed response: "%s %s"', $method, $url));
// Reinitialize the pushed response with request's options
$ch = $pushedResponse->handle;
$pushedResponse = $pushedResponse->response;
$pushedResponse->__construct($multi, $url, $options, $this->logger);
} else {
$this->logger && $this->logger->debug(sprintf('Rejecting pushed response: "%s"', $url));
$pushedResponse = null;
}
}
if (!$pushedResponse) {
$ch = curl_init();
$this->logger && $this->logger->info(sprintf('Request: "%s %s"', $method, $url));
$curlopts += [\CURLOPT_SHARE => $multi->share];
}
foreach ($curlopts as $opt => $value) {
if (null !== $value && !curl_setopt($ch, $opt, $value) && \CURLOPT_CERTINFO !== $opt && (!\defined('CURLOPT_HEADEROPT') || \CURLOPT_HEADEROPT !== $opt)) {
$constantName = $this->findConstantName($opt);
throw new TransportException(sprintf('Curl option "%s" is not supported.', $constantName ?? $opt));
}
}
return $pushedResponse ?? new CurlResponse($multi, $ch, $options, $this->logger, $method, self::createRedirectResolver($options, $host), CurlClientState::$curlVersion['version_number']);
}
/**
* {@inheritdoc}
*/
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
{
if ($responses instanceof CurlResponse) {
$responses = [$responses];
} elseif (!is_iterable($responses)) {
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of CurlResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
}
$multi = $this->ensureState();
if (\is_resource($multi->handle) || $multi->handle instanceof \CurlMultiHandle) {
$active = 0;
while (\CURLM_CALL_MULTI_PERFORM === curl_multi_exec($multi->handle, $active)) {
}
}
return new ResponseStream(CurlResponse::stream($responses, $timeout));
}
public function reset()
{
if (isset($this->multi)) {
$this->multi->reset();
}
}
/**
* Accepts pushed responses only if their headers related to authentication match the request.
*/
private static function acceptPushForRequest(string $method, array $options, PushedResponse $pushedResponse): bool
{
if ('' !== $options['body'] || $method !== $pushedResponse->requestHeaders[':method'][0]) {
return false;
}
foreach (['proxy', 'no_proxy', 'bindto', 'local_cert', 'local_pk'] as $k) {
if ($options[$k] !== $pushedResponse->parentOptions[$k]) {
return false;
}
}
foreach (['authorization', 'cookie', 'range', 'proxy-authorization'] as $k) {
$normalizedHeaders = $options['normalized_headers'][$k] ?? [];
foreach ($normalizedHeaders as $i => $v) {
$normalizedHeaders[$i] = substr($v, \strlen($k) + 2);
}
if (($pushedResponse->requestHeaders[$k] ?? []) !== $normalizedHeaders) {
return false;
}
}
return true;
}
/**
* Wraps the request's body callback to allow it to return strings longer than curl requested.
*/
private static function readRequestBody(int $length, \Closure $body, string &$buffer, bool &$eof): string
{
if (!$eof && \strlen($buffer) < $length) {
if (!\is_string($data = $body($length))) {
throw new TransportException(sprintf('The return value of the "body" option callback must be a string, "%s" returned.', get_debug_type($data)));
}
$buffer .= $data;
$eof = '' === $data;
}
$data = substr($buffer, 0, $length);
$buffer = substr($buffer, $length);
return $data;
}
/**
* Resolves relative URLs on redirects and deals with authentication headers.
*
* Work around CVE-2018-1000007: Authorization and Cookie headers should not follow redirects - fixed in Curl 7.64
*/
private static function createRedirectResolver(array $options, string $host): \Closure
{
$redirectHeaders = [];
if (0 < $options['max_redirects']) {
$redirectHeaders['host'] = $host;
$redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
return 0 !== stripos($h, 'Host:');
});
if (isset($options['normalized_headers']['authorization'][0]) || isset($options['normalized_headers']['cookie'][0])) {
$redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
return 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:');
});
}
}
return static function ($ch, string $location, bool $noContent) use (&$redirectHeaders, $options) {
try {
$location = self::parseUrl($location);
$url = self::parseUrl(curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL));
$url = self::resolveUrl($location, $url);
} catch (InvalidArgumentException $e) {
return null;
}
if ($noContent && $redirectHeaders) {
$filterContentHeaders = static function ($h) {
return 0 !== stripos($h, 'Content-Length:') && 0 !== stripos($h, 'Content-Type:') && 0 !== stripos($h, 'Transfer-Encoding:');
};
$redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], $filterContentHeaders);
$redirectHeaders['with_auth'] = array_filter($redirectHeaders['with_auth'], $filterContentHeaders);
}
if ($redirectHeaders && isset($location['authority'])) {
$requestHeaders = parse_url($location['authority'], \PHP_URL_HOST) === $redirectHeaders['host'] ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
curl_setopt($ch, \CURLOPT_HTTPHEADER, $requestHeaders);
} elseif ($noContent && $redirectHeaders) {
curl_setopt($ch, \CURLOPT_HTTPHEADER, $redirectHeaders['with_auth']);
}
curl_setopt($ch, \CURLOPT_PROXY, self::getProxyUrl($options['proxy'], $url));
return implode('', $url);
};
}
private function ensureState(): CurlClientState
{
if (!isset($this->multi)) {
$this->multi = new CurlClientState($this->maxHostConnections, $this->maxPendingPushes);
$this->multi->logger = $this->logger;
}
return $this->multi;
}
private function findConstantName(int $opt): ?string
{
$constants = array_filter(get_defined_constants(), static function ($v, $k) use ($opt) {
return $v === $opt && 'C' === $k[0] && (str_starts_with($k, 'CURLOPT_') || str_starts_with($k, 'CURLINFO_'));
}, \ARRAY_FILTER_USE_BOTH);
return key($constants);
}
/**
* Prevents overriding options that are set internally throughout the request.
*/
private function validateExtraCurlOptions(array $options): void
{
$curloptsToConfig = [
// options used in CurlHttpClient
\CURLOPT_HTTPAUTH => 'auth_ntlm',
\CURLOPT_USERPWD => 'auth_ntlm',
\CURLOPT_RESOLVE => 'resolve',
\CURLOPT_NOSIGNAL => 'timeout',
\CURLOPT_HTTPHEADER => 'headers',
\CURLOPT_INFILE => 'body',
\CURLOPT_READFUNCTION => 'body',
\CURLOPT_INFILESIZE => 'body',
\CURLOPT_POSTFIELDS => 'body',
\CURLOPT_UPLOAD => 'body',
\CURLOPT_INTERFACE => 'bindto',
\CURLOPT_TIMEOUT_MS => 'max_duration',
\CURLOPT_TIMEOUT => 'max_duration',
\CURLOPT_MAXREDIRS => 'max_redirects',
\CURLOPT_POSTREDIR => 'max_redirects',
\CURLOPT_PROXY => 'proxy',
\CURLOPT_NOPROXY => 'no_proxy',
\CURLOPT_SSL_VERIFYPEER => 'verify_peer',
\CURLOPT_SSL_VERIFYHOST => 'verify_host',
\CURLOPT_CAINFO => 'cafile',
\CURLOPT_CAPATH => 'capath',
\CURLOPT_SSL_CIPHER_LIST => 'ciphers',
\CURLOPT_SSLCERT => 'local_cert',
\CURLOPT_SSLKEY => 'local_pk',
\CURLOPT_KEYPASSWD => 'passphrase',
\CURLOPT_CERTINFO => 'capture_peer_cert_chain',
\CURLOPT_USERAGENT => 'normalized_headers',
\CURLOPT_REFERER => 'headers',
// options used in CurlResponse
\CURLOPT_NOPROGRESS => 'on_progress',
\CURLOPT_PROGRESSFUNCTION => 'on_progress',
];
if (\defined('CURLOPT_UNIX_SOCKET_PATH')) {
$curloptsToConfig[\CURLOPT_UNIX_SOCKET_PATH] = 'bindto';
}
if (\defined('CURLOPT_PINNEDPUBLICKEY')) {
$curloptsToConfig[\CURLOPT_PINNEDPUBLICKEY] = 'peer_fingerprint';
}
$curloptsToCheck = [
\CURLOPT_PRIVATE,
\CURLOPT_HEADERFUNCTION,
\CURLOPT_WRITEFUNCTION,
\CURLOPT_VERBOSE,
\CURLOPT_STDERR,
\CURLOPT_RETURNTRANSFER,
\CURLOPT_URL,
\CURLOPT_FOLLOWLOCATION,
\CURLOPT_HEADER,
\CURLOPT_CONNECTTIMEOUT,
\CURLOPT_CONNECTTIMEOUT_MS,
\CURLOPT_HTTP_VERSION,
\CURLOPT_PORT,
\CURLOPT_DNS_USE_GLOBAL_CACHE,
\CURLOPT_PROTOCOLS,
\CURLOPT_REDIR_PROTOCOLS,
\CURLOPT_COOKIEFILE,
\CURLINFO_REDIRECT_COUNT,
];
if (\defined('CURLOPT_HTTP09_ALLOWED')) {
$curloptsToCheck[] = \CURLOPT_HTTP09_ALLOWED;
}
if (\defined('CURLOPT_HEADEROPT')) {
$curloptsToCheck[] = \CURLOPT_HEADEROPT;
}
$methodOpts = [
\CURLOPT_POST,
\CURLOPT_PUT,
\CURLOPT_CUSTOMREQUEST,
\CURLOPT_HTTPGET,
\CURLOPT_NOBODY,
];
foreach ($options as $opt => $optValue) {
if (isset($curloptsToConfig[$opt])) {
$constName = $this->findConstantName($opt) ?? $opt;
throw new InvalidArgumentException(sprintf('Cannot set "%s" with "extra.curl", use option "%s" instead.', $constName, $curloptsToConfig[$opt]));
}
if (\in_array($opt, $methodOpts)) {
throw new InvalidArgumentException('The HTTP method cannot be overridden using "extra.curl".');
}
if (\in_array($opt, $curloptsToCheck)) {
$constName = $this->findConstantName($opt) ?? $opt;
throw new InvalidArgumentException(sprintf('Cannot set "%s" with "extra.curl".', $constName));
}
}
}
}

View File

@@ -0,0 +1,176 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\DataCollector;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\TraceableHttpClient;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpFoundation\Request;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
use Symfony\Component\VarDumper\Caster\ImgStub;
/**
* @author Jérémy Romey <jeremy@free-agent.fr>
*/
final class HttpClientDataCollector extends DataCollector implements LateDataCollectorInterface
{
/**
* @var TraceableHttpClient[]
*/
private $clients = [];
public function registerClient(string $name, TraceableHttpClient $client)
{
$this->clients[$name] = $client;
}
/**
* {@inheritdoc}
*/
public function collect(Request $request, Response $response, ?\Throwable $exception = null)
{
$this->lateCollect();
}
public function lateCollect()
{
$this->data['request_count'] = $this->data['request_count'] ?? 0;
$this->data['error_count'] = $this->data['error_count'] ?? 0;
$this->data += ['clients' => []];
foreach ($this->clients as $name => $client) {
[$errorCount, $traces] = $this->collectOnClient($client);
$this->data['clients'] += [
$name => [
'traces' => [],
'error_count' => 0,
],
];
$this->data['clients'][$name]['traces'] = array_merge($this->data['clients'][$name]['traces'], $traces);
$this->data['request_count'] += \count($traces);
$this->data['error_count'] += $errorCount;
$this->data['clients'][$name]['error_count'] += $errorCount;
$client->reset();
}
}
public function getClients(): array
{
return $this->data['clients'] ?? [];
}
public function getRequestCount(): int
{
return $this->data['request_count'] ?? 0;
}
public function getErrorCount(): int
{
return $this->data['error_count'] ?? 0;
}
/**
* {@inheritdoc}
*/
public function getName(): string
{
return 'http_client';
}
public function reset()
{
$this->data = [
'clients' => [],
'request_count' => 0,
'error_count' => 0,
];
}
private function collectOnClient(TraceableHttpClient $client): array
{
$traces = $client->getTracedRequests();
$errorCount = 0;
$baseInfo = [
'response_headers' => 1,
'retry_count' => 1,
'redirect_count' => 1,
'redirect_url' => 1,
'user_data' => 1,
'error' => 1,
'url' => 1,
];
foreach ($traces as $i => $trace) {
if (400 <= ($trace['info']['http_code'] ?? 0)) {
++$errorCount;
}
$info = $trace['info'];
$traces[$i]['http_code'] = $info['http_code'] ?? 0;
unset($info['filetime'], $info['http_code'], $info['ssl_verify_result'], $info['content_type']);
if (($info['http_method'] ?? null) === $trace['method']) {
unset($info['http_method']);
}
if (($info['url'] ?? null) === $trace['url']) {
unset($info['url']);
}
foreach ($info as $k => $v) {
if (!$v || (is_numeric($v) && 0 > $v)) {
unset($info[$k]);
}
}
if (\is_string($content = $trace['content'])) {
$contentType = 'application/octet-stream';
foreach ($info['response_headers'] ?? [] as $h) {
if (0 === stripos($h, 'content-type: ')) {
$contentType = substr($h, \strlen('content-type: '));
break;
}
}
if (0 === strpos($contentType, 'image/') && class_exists(ImgStub::class)) {
$content = new ImgStub($content, $contentType, '');
} else {
$content = [$content];
}
$content = ['response_content' => $content];
} elseif (\is_array($content)) {
$content = ['response_json' => $content];
} else {
$content = [];
}
if (isset($info['retry_count'])) {
$content['retries'] = $info['previous_info'];
unset($info['previous_info']);
}
$debugInfo = array_diff_key($info, $baseInfo);
$info = ['info' => $debugInfo] + array_diff_key($info, $debugInfo) + $content;
unset($traces[$i]['info']); // break PHP reference used by TraceableHttpClient
$traces[$i]['info'] = $this->cloneVar($info);
$traces[$i]['options'] = $this->cloneVar($trace['options']);
}
return [$errorCount, $traces];
}
}

View File

@@ -0,0 +1,66 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\HttpClientInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseStreamInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\Service\ResetInterface;
/**
* Eases with writing decorators.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
trait DecoratorTrait
{
private $client;
public function __construct(?HttpClientInterface $client = null)
{
$this->client = $client ?? HttpClient::create();
}
/**
* {@inheritdoc}
*/
public function request(string $method, string $url, array $options = []): ResponseInterface
{
return $this->client->request($method, $url, $options);
}
/**
* {@inheritdoc}
*/
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
{
return $this->client->stream($responses, $timeout);
}
/**
* {@inheritdoc}
*/
public function withOptions(array $options): self
{
$clone = clone $this;
$clone->client = $this->client->withOptions($options);
return $clone;
}
public function reset()
{
if ($this->client instanceof ResetInterface) {
$this->client->reset();
}
}
}

View File

@@ -0,0 +1,51 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Reference;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\TraceableHttpClient;
final class HttpClientPass implements CompilerPassInterface
{
private $clientTag;
public function __construct(string $clientTag = 'http_client.client')
{
if (0 < \func_num_args()) {
kadence_blocks_trigger_deprecation('symfony/http-client', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
}
$this->clientTag = $clientTag;
}
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition('data_collector.http_client')) {
return;
}
foreach ($container->findTaggedServiceIds($this->clientTag) as $id => $tags) {
$container->register('.debug.'.$id, TraceableHttpClient::class)
->setArguments([new Reference('.debug.'.$id.'.inner'), new Reference('debug.stopwatch', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)])
->addTag('kernel.reset', ['method' => 'reset'])
->setDecoratedService($id, null, 5);
$container->getDefinition('data_collector.http_client')
->addMethodCall('registerClient', [$id, new Reference('.debug.'.$id)]);
}
}
}

View File

@@ -0,0 +1,162 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Chunk\DataChunk;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Chunk\ServerSentEvent;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\EventSourceException;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\AsyncContext;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\AsyncResponse;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ChunkInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\HttpClientInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\Service\ResetInterface;
/**
* @author Antoine Bluchet <soyuka@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
final class EventSourceHttpClient implements HttpClientInterface, ResetInterface
{
use AsyncDecoratorTrait, HttpClientTrait {
AsyncDecoratorTrait::withOptions insteadof HttpClientTrait;
}
private $reconnectionTime;
public function __construct(?HttpClientInterface $client = null, float $reconnectionTime = 10.0)
{
$this->client = $client ?? HttpClient::create();
$this->reconnectionTime = $reconnectionTime;
}
public function connect(string $url, array $options = []): ResponseInterface
{
return $this->request('GET', $url, self::mergeDefaultOptions($options, [
'buffer' => false,
'headers' => [
'Accept' => 'text/event-stream',
'Cache-Control' => 'no-cache',
],
], true));
}
public function request(string $method, string $url, array $options = []): ResponseInterface
{
$state = new class() {
public $buffer = null;
public $lastEventId = null;
public $reconnectionTime;
public $lastError = null;
};
$state->reconnectionTime = $this->reconnectionTime;
if ($accept = self::normalizeHeaders($options['headers'] ?? [])['accept'] ?? []) {
$state->buffer = \in_array($accept, [['Accept: text/event-stream'], ['accept: text/event-stream']], true) ? '' : null;
if (null !== $state->buffer) {
$options['extra']['trace_content'] = false;
}
}
return new AsyncResponse($this->client, $method, $url, $options, static function (ChunkInterface $chunk, AsyncContext $context) use ($state, $method, $url, $options) {
if (null !== $state->buffer) {
$context->setInfo('reconnection_time', $state->reconnectionTime);
$isTimeout = false;
}
$lastError = $state->lastError;
$state->lastError = null;
try {
$isTimeout = $chunk->isTimeout();
if (null !== $chunk->getInformationalStatus() || $context->getInfo('canceled')) {
yield $chunk;
return;
}
} catch (TransportExceptionInterface $e) {
$state->lastError = $lastError ?? microtime(true);
if (null === $state->buffer || ($isTimeout && microtime(true) - $state->lastError < $state->reconnectionTime)) {
yield $chunk;
} else {
$options['headers']['Last-Event-ID'] = $state->lastEventId;
$state->buffer = '';
$state->lastError = microtime(true);
$context->getResponse()->cancel();
$context->replaceRequest($method, $url, $options);
if ($isTimeout) {
yield $chunk;
} else {
$context->pause($state->reconnectionTime);
}
}
return;
}
if ($chunk->isFirst()) {
if (preg_match('/^text\/event-stream(;|$)/i', $context->getHeaders()['content-type'][0] ?? '')) {
$state->buffer = '';
} elseif (null !== $lastError || (null !== $state->buffer && 200 === $context->getStatusCode())) {
throw new EventSourceException(sprintf('Response content-type is "%s" while "text/event-stream" was expected for "%s".', $context->getHeaders()['content-type'][0] ?? '', $context->getInfo('url')));
} else {
$context->passthru();
}
if (null === $lastError) {
yield $chunk;
}
return;
}
if ($chunk->isLast()) {
if ('' !== $content = $state->buffer) {
$state->buffer = '';
yield new DataChunk(-1, $content);
}
yield $chunk;
return;
}
$content = $state->buffer.$chunk->getContent();
$events = preg_split('/((?:\r\n){2,}|\r{2,}|\n{2,})/', $content, -1, \PREG_SPLIT_DELIM_CAPTURE);
$state->buffer = array_pop($events);
for ($i = 0; isset($events[$i]); $i += 2) {
$content = $events[$i].$events[1 + $i];
if (!preg_match('/(?:^|\r\n|[\r\n])[^:\r\n]/', $content)) {
yield new DataChunk(-1, $content);
continue;
}
$event = new ServerSentEvent($content);
if ('' !== $event->getId()) {
$context->setInfo('last_event_id', $state->lastEventId = $event->getId());
}
if ($event->getRetry()) {
$context->setInfo('reconnection_time', $state->reconnectionTime = $event->getRetry());
}
yield $event;
}
});
}
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
/**
* Represents a 4xx response.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class ClientException extends \RuntimeException implements ClientExceptionInterface
{
use HttpExceptionTrait;
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
final class EventSourceException extends \RuntimeException implements DecodingExceptionInterface
{
}

View File

@@ -0,0 +1,78 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
trait HttpExceptionTrait
{
private $response;
public function __construct(ResponseInterface $response)
{
$this->response = $response;
$code = $response->getInfo('http_code');
$url = $response->getInfo('url');
$message = sprintf('HTTP %d returned for "%s".', $code, $url);
$httpCodeFound = false;
$isJson = false;
foreach (array_reverse($response->getInfo('response_headers')) as $h) {
if (str_starts_with($h, 'HTTP/')) {
if ($httpCodeFound) {
break;
}
$message = sprintf('%s returned for "%s".', $h, $url);
$httpCodeFound = true;
}
if (0 === stripos($h, 'content-type:')) {
if (preg_match('/\bjson\b/i', $h)) {
$isJson = true;
}
if ($httpCodeFound) {
break;
}
}
}
// Try to guess a better error message using common API error formats
// The MIME type isn't explicitly checked because some formats inherit from others
// Ex: JSON:API follows RFC 7807 semantics, Hydra can be used in any JSON-LD-compatible format
if ($isJson && $body = json_decode($response->getContent(false), true)) {
if (isset($body['hydra:title']) || isset($body['hydra:description'])) {
// see http://www.hydra-cg.com/spec/latest/core/#description-of-http-status-codes-and-errors
$separator = isset($body['hydra:title'], $body['hydra:description']) ? "\n\n" : '';
$message = ($body['hydra:title'] ?? '').$separator.($body['hydra:description'] ?? '');
} elseif ((isset($body['title']) || isset($body['detail']))
&& (\is_scalar($body['title'] ?? '') && \is_scalar($body['detail'] ?? ''))) {
// see RFC 7807 and https://jsonapi.org/format/#error-objects
$separator = isset($body['title'], $body['detail']) ? "\n\n" : '';
$message = ($body['title'] ?? '').$separator.($body['detail'] ?? '');
}
}
parent::__construct($message, $code);
}
public function getResponse(): ResponseInterface
{
return $this->response;
}
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
final class InvalidArgumentException extends \InvalidArgumentException implements TransportExceptionInterface
{
}

View File

@@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
/**
* Thrown by responses' toArray() method when their content cannot be JSON-decoded.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class JsonException extends \JsonException implements DecodingExceptionInterface
{
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
/**
* Represents a 3xx response.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class RedirectionException extends \RuntimeException implements RedirectionExceptionInterface
{
use HttpExceptionTrait;
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
/**
* Represents a 5xx response.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class ServerException extends \RuntimeException implements ServerExceptionInterface
{
use HttpExceptionTrait;
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\TimeoutExceptionInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
final class TimeoutException extends TransportException implements TimeoutExceptionInterface
{
}

View File

@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class TransportException extends \RuntimeException implements TransportExceptionInterface
{
}

View File

@@ -0,0 +1,79 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient;
use Amp\Http\Client\Connection\ConnectionLimitingPool;
use Amp\Promise;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* A factory to instantiate the best possible HTTP client for the runtime.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class HttpClient
{
/**
* @param array $defaultOptions Default request's options
* @param int $maxHostConnections The maximum number of connections to a single host
* @param int $maxPendingPushes The maximum number of pushed responses to accept in the queue
*
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
*/
public static function create(array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 50): HttpClientInterface
{
if ($amp = class_exists(ConnectionLimitingPool::class) && interface_exists(Promise::class)) {
if (!\extension_loaded('curl')) {
return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
}
// Skip curl when HTTP/2 push is unsupported or buggy, see https://bugs.php.net/77535
if (\PHP_VERSION_ID < 70217 || (\PHP_VERSION_ID >= 70300 && \PHP_VERSION_ID < 70304) || !\defined('CURLMOPT_PUSHFUNCTION')) {
return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
}
static $curlVersion = null;
$curlVersion = $curlVersion ?? curl_version();
// HTTP/2 push crashes before curl 7.61
if (0x073D00 > $curlVersion['version_number'] || !(\CURL_VERSION_HTTP2 & $curlVersion['features'])) {
return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
}
}
if (\extension_loaded('curl')) {
if ('\\' !== \DIRECTORY_SEPARATOR || isset($defaultOptions['cafile']) || isset($defaultOptions['capath']) || \ini_get('curl.cainfo') || \ini_get('openssl.cafile') || \ini_get('openssl.capath')) {
return new CurlHttpClient($defaultOptions, $maxHostConnections, $maxPendingPushes);
}
@trigger_error('Configure the "curl.cainfo", "openssl.cafile" or "openssl.capath" php.ini setting to enable the CurlHttpClient', \E_USER_WARNING);
}
if ($amp) {
return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
}
@trigger_error((\extension_loaded('curl') ? 'Upgrade' : 'Install').' the curl extension or run "composer require amphp/http-client:^4.2.1" to perform async HTTP operations, including full HTTP/2 support', \E_USER_NOTICE);
return new NativeHttpClient($defaultOptions, $maxHostConnections);
}
/**
* Creates a client that adds options (e.g. authentication headers) only when the request URL matches the provided base URI.
*/
public static function createForBaseUri(string $baseUri, array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 50): HttpClientInterface
{
$client = self::create([], $maxHostConnections, $maxPendingPushes);
return ScopingHttpClient::forBaseUri($client, $baseUri, $defaultOptions);
}
}

View File

@@ -0,0 +1,739 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\InvalidArgumentException;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\TransportException;
/**
* Provides the common logic from writing HttpClientInterface implementations.
*
* All private methods are static to prevent implementers from creating memory leaks via circular references.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
trait HttpClientTrait
{
private static $CHUNK_SIZE = 16372;
/**
* {@inheritdoc}
*/
public function withOptions(array $options): self
{
$clone = clone $this;
$clone->defaultOptions = self::mergeDefaultOptions($options, $this->defaultOptions);
return $clone;
}
/**
* Validates and normalizes method, URL and options, and merges them with defaults.
*
* @throws InvalidArgumentException When a not-supported option is found
*/
private static function prepareRequest(?string $method, ?string $url, array $options, array $defaultOptions = [], bool $allowExtraOptions = false): array
{
if (null !== $method) {
if (\strlen($method) !== strspn($method, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')) {
throw new InvalidArgumentException(sprintf('Invalid HTTP method "%s", only uppercase letters are accepted.', $method));
}
if (!$method) {
throw new InvalidArgumentException('The HTTP method cannot be empty.');
}
}
$options = self::mergeDefaultOptions($options, $defaultOptions, $allowExtraOptions);
$buffer = $options['buffer'] ?? true;
if ($buffer instanceof \Closure) {
$options['buffer'] = static function (array $headers) use ($buffer) {
if (!\is_bool($buffer = $buffer($headers))) {
if (!\is_array($bufferInfo = @stream_get_meta_data($buffer))) {
throw new \LogicException(sprintf('The closure passed as option "buffer" must return bool or stream resource, got "%s".', get_debug_type($buffer)));
}
if (false === strpbrk($bufferInfo['mode'], 'acew+')) {
throw new \LogicException(sprintf('The stream returned by the closure passed as option "buffer" must be writeable, got mode "%s".', $bufferInfo['mode']));
}
}
return $buffer;
};
} elseif (!\is_bool($buffer)) {
if (!\is_array($bufferInfo = @stream_get_meta_data($buffer))) {
throw new InvalidArgumentException(sprintf('Option "buffer" must be bool, stream resource or Closure, "%s" given.', get_debug_type($buffer)));
}
if (false === strpbrk($bufferInfo['mode'], 'acew+')) {
throw new InvalidArgumentException(sprintf('The stream in option "buffer" must be writeable, mode "%s" given.', $bufferInfo['mode']));
}
}
if (isset($options['json'])) {
if (isset($options['body']) && '' !== $options['body']) {
throw new InvalidArgumentException('Define either the "json" or the "body" option, setting both is not supported.');
}
$options['body'] = self::jsonEncode($options['json']);
unset($options['json']);
if (!isset($options['normalized_headers']['content-type'])) {
$options['normalized_headers']['content-type'] = ['Content-Type: application/json'];
}
}
if (!isset($options['normalized_headers']['accept'])) {
$options['normalized_headers']['accept'] = ['Accept: */*'];
}
if (isset($options['body'])) {
$options['body'] = self::normalizeBody($options['body']);
if (\is_string($options['body'])
&& (string) \strlen($options['body']) !== substr($h = $options['normalized_headers']['content-length'][0] ?? '', 16)
&& ('' !== $h || '' !== $options['body'])
) {
if ('chunked' === substr($options['normalized_headers']['transfer-encoding'][0] ?? '', \strlen('Transfer-Encoding: '))) {
unset($options['normalized_headers']['transfer-encoding']);
$options['body'] = self::dechunk($options['body']);
}
$options['normalized_headers']['content-length'] = [substr_replace($h ?: 'Content-Length: ', \strlen($options['body']), 16)];
}
}
if (isset($options['peer_fingerprint'])) {
$options['peer_fingerprint'] = self::normalizePeerFingerprint($options['peer_fingerprint']);
}
// Validate on_progress
if (isset($options['on_progress']) && !\is_callable($onProgress = $options['on_progress'])) {
throw new InvalidArgumentException(sprintf('Option "on_progress" must be callable, "%s" given.', get_debug_type($onProgress)));
}
if (\is_array($options['auth_basic'] ?? null)) {
$count = \count($options['auth_basic']);
if ($count <= 0 || $count > 2) {
throw new InvalidArgumentException(sprintf('Option "auth_basic" must contain 1 or 2 elements, "%s" given.', $count));
}
$options['auth_basic'] = implode(':', $options['auth_basic']);
}
if (!\is_string($options['auth_basic'] ?? '')) {
throw new InvalidArgumentException(sprintf('Option "auth_basic" must be string or an array, "%s" given.', get_debug_type($options['auth_basic'])));
}
if (isset($options['auth_bearer'])) {
if (!\is_string($options['auth_bearer'])) {
throw new InvalidArgumentException(sprintf('Option "auth_bearer" must be a string, "%s" given.', get_debug_type($options['auth_bearer'])));
}
if (preg_match('{[^\x21-\x7E]}', $options['auth_bearer'])) {
throw new InvalidArgumentException('Invalid character found in option "auth_bearer": '.json_encode($options['auth_bearer']).'.');
}
}
if (isset($options['auth_basic'], $options['auth_bearer'])) {
throw new InvalidArgumentException('Define either the "auth_basic" or the "auth_bearer" option, setting both is not supported.');
}
if (null !== $url) {
// Merge auth with headers
if (($options['auth_basic'] ?? false) && !($options['normalized_headers']['authorization'] ?? false)) {
$options['normalized_headers']['authorization'] = ['Authorization: Basic '.base64_encode($options['auth_basic'])];
}
// Merge bearer with headers
if (($options['auth_bearer'] ?? false) && !($options['normalized_headers']['authorization'] ?? false)) {
$options['normalized_headers']['authorization'] = ['Authorization: Bearer '.$options['auth_bearer']];
}
unset($options['auth_basic'], $options['auth_bearer']);
// Parse base URI
if (\is_string($options['base_uri'])) {
$options['base_uri'] = self::parseUrl($options['base_uri']);
}
// Validate and resolve URL
$url = self::parseUrl($url, $options['query']);
$url = self::resolveUrl($url, $options['base_uri'], $defaultOptions['query'] ?? []);
}
// Finalize normalization of options
$options['http_version'] = (string) ($options['http_version'] ?? '') ?: null;
if (0 > $options['timeout'] = (float) ($options['timeout'] ?? \ini_get('default_socket_timeout'))) {
$options['timeout'] = 172800.0; // 2 days
}
$options['max_duration'] = isset($options['max_duration']) ? (float) $options['max_duration'] : 0;
$options['headers'] = array_merge(...array_values($options['normalized_headers']));
return [$url, $options];
}
/**
* @throws InvalidArgumentException When an invalid option is found
*/
private static function mergeDefaultOptions(array $options, array $defaultOptions, bool $allowExtraOptions = false): array
{
$options['normalized_headers'] = self::normalizeHeaders($options['headers'] ?? []);
if ($defaultOptions['headers'] ?? false) {
$options['normalized_headers'] += self::normalizeHeaders($defaultOptions['headers']);
}
$options['headers'] = array_merge(...array_values($options['normalized_headers']) ?: [[]]);
if ($resolve = $options['resolve'] ?? false) {
$options['resolve'] = [];
foreach ($resolve as $k => $v) {
if ('' === $v = (string) $v) {
$v = null;
} elseif ('[' === $v[0] && ']' === substr($v, -1) && str_contains($v, ':')) {
$v = substr($v, 1, -1);
}
$options['resolve'][substr(self::parseUrl('http://'.$k)['authority'], 2)] = $v;
}
}
// Option "query" is never inherited from defaults
$options['query'] = $options['query'] ?? [];
$options += $defaultOptions;
if (isset(self::$emptyDefaults)) {
foreach (self::$emptyDefaults as $k => $v) {
if (!isset($options[$k])) {
$options[$k] = $v;
}
}
}
if (isset($defaultOptions['extra'])) {
$options['extra'] += $defaultOptions['extra'];
}
if ($resolve = $defaultOptions['resolve'] ?? false) {
foreach ($resolve as $k => $v) {
if ('' === $v = (string) $v) {
$v = null;
} elseif ('[' === $v[0] && ']' === substr($v, -1) && str_contains($v, ':')) {
$v = substr($v, 1, -1);
}
$options['resolve'] += [substr(self::parseUrl('http://'.$k)['authority'], 2) => $v];
}
}
if ($allowExtraOptions || !$defaultOptions) {
return $options;
}
// Look for unsupported options
foreach ($options as $name => $v) {
if (\array_key_exists($name, $defaultOptions) || 'normalized_headers' === $name) {
continue;
}
if ('auth_ntlm' === $name) {
if (!\extension_loaded('curl')) {
$msg = 'try installing the "curl" extension to use "%s" instead.';
} else {
$msg = 'try using "%s" instead.';
}
throw new InvalidArgumentException(sprintf('Option "auth_ntlm" is not supported by "%s", '.$msg, __CLASS__, CurlHttpClient::class));
}
$alternatives = [];
foreach ($defaultOptions as $k => $v) {
if (levenshtein($name, $k) <= \strlen($name) / 3 || str_contains($k, $name)) {
$alternatives[] = $k;
}
}
throw new InvalidArgumentException(sprintf('Unsupported option "%s" passed to "%s", did you mean "%s"?', $name, __CLASS__, implode('", "', $alternatives ?: array_keys($defaultOptions))));
}
return $options;
}
/**
* @return string[][]
*
* @throws InvalidArgumentException When an invalid header is found
*/
private static function normalizeHeaders(array $headers): array
{
$normalizedHeaders = [];
foreach ($headers as $name => $values) {
if (\is_object($values) && method_exists($values, '__toString')) {
$values = (string) $values;
}
if (\is_int($name)) {
if (!\is_string($values)) {
throw new InvalidArgumentException(sprintf('Invalid value for header "%s": expected string, "%s" given.', $name, get_debug_type($values)));
}
[$name, $values] = explode(':', $values, 2);
$values = [ltrim($values)];
} elseif (!is_iterable($values)) {
if (\is_object($values)) {
throw new InvalidArgumentException(sprintf('Invalid value for header "%s": expected string, "%s" given.', $name, get_debug_type($values)));
}
$values = (array) $values;
}
$lcName = strtolower($name);
$normalizedHeaders[$lcName] = [];
foreach ($values as $value) {
$normalizedHeaders[$lcName][] = $value = $name.': '.$value;
if (\strlen($value) !== strcspn($value, "\r\n\0")) {
throw new InvalidArgumentException(sprintf('Invalid header: CR/LF/NUL found in "%s".', $value));
}
}
}
return $normalizedHeaders;
}
/**
* @param array|string|resource|\Traversable|\Closure $body
*
* @return string|resource|\Closure
*
* @throws InvalidArgumentException When an invalid body is passed
*/
private static function normalizeBody($body)
{
if (\is_array($body)) {
array_walk_recursive($body, $caster = static function (&$v) use (&$caster) {
if (\is_object($v)) {
if ($vars = get_object_vars($v)) {
array_walk_recursive($vars, $caster);
$v = $vars;
} elseif (method_exists($v, '__toString')) {
$v = (string) $v;
}
}
});
return http_build_query($body, '', '&');
}
if (\is_string($body)) {
return $body;
}
$generatorToCallable = static function (\Generator $body): \Closure {
return static function () use ($body) {
while ($body->valid()) {
$chunk = $body->current();
$body->next();
if ('' !== $chunk) {
return $chunk;
}
}
return '';
};
};
if ($body instanceof \Generator) {
return $generatorToCallable($body);
}
if ($body instanceof \Traversable) {
return $generatorToCallable((static function ($body) { yield from $body; })($body));
}
if ($body instanceof \Closure) {
$r = new \ReflectionFunction($body);
$body = $r->getClosure();
if ($r->isGenerator()) {
$body = $body(self::$CHUNK_SIZE);
return $generatorToCallable($body);
}
return $body;
}
if (!\is_array(@stream_get_meta_data($body))) {
throw new InvalidArgumentException(sprintf('Option "body" must be string, stream resource, iterable or callable, "%s" given.', get_debug_type($body)));
}
return $body;
}
private static function dechunk(string $body): string
{
$h = fopen('php://temp', 'w+');
stream_filter_append($h, 'dechunk', \STREAM_FILTER_WRITE);
fwrite($h, $body);
$body = stream_get_contents($h, -1, 0);
rewind($h);
ftruncate($h, 0);
if (fwrite($h, '-') && '' !== stream_get_contents($h, -1, 0)) {
throw new TransportException('Request body has broken chunked encoding.');
}
return $body;
}
/**
* @param string|string[] $fingerprint
*
* @throws InvalidArgumentException When an invalid fingerprint is passed
*/
private static function normalizePeerFingerprint($fingerprint): array
{
if (\is_string($fingerprint)) {
switch (\strlen($fingerprint = str_replace(':', '', $fingerprint))) {
case 32: $fingerprint = ['md5' => $fingerprint]; break;
case 40: $fingerprint = ['sha1' => $fingerprint]; break;
case 44: $fingerprint = ['pin-sha256' => [$fingerprint]]; break;
case 64: $fingerprint = ['sha256' => $fingerprint]; break;
default: throw new InvalidArgumentException(sprintf('Cannot auto-detect fingerprint algorithm for "%s".', $fingerprint));
}
} elseif (\is_array($fingerprint)) {
foreach ($fingerprint as $algo => $hash) {
$fingerprint[$algo] = 'pin-sha256' === $algo ? (array) $hash : str_replace(':', '', $hash);
}
} else {
throw new InvalidArgumentException(sprintf('Option "peer_fingerprint" must be string or array, "%s" given.', get_debug_type($fingerprint)));
}
return $fingerprint;
}
/**
* @param mixed $value
*
* @throws InvalidArgumentException When the value cannot be json-encoded
*/
private static function jsonEncode($value, ?int $flags = null, int $maxDepth = 512): string
{
$flags = $flags ?? (\JSON_HEX_TAG | \JSON_HEX_APOS | \JSON_HEX_AMP | \JSON_HEX_QUOT | \JSON_PRESERVE_ZERO_FRACTION);
try {
$value = json_encode($value, $flags | (\PHP_VERSION_ID >= 70300 ? \JSON_THROW_ON_ERROR : 0), $maxDepth);
} catch (\JsonException $e) {
throw new InvalidArgumentException('Invalid value for "json" option: '.$e->getMessage());
}
if (\PHP_VERSION_ID < 70300 && \JSON_ERROR_NONE !== json_last_error() && (false === $value || !($flags & \JSON_PARTIAL_OUTPUT_ON_ERROR))) {
throw new InvalidArgumentException('Invalid value for "json" option: '.json_last_error_msg());
}
return $value;
}
/**
* Resolves a URL against a base URI.
*
* @see https://tools.ietf.org/html/rfc3986#section-5.2.2
*
* @throws InvalidArgumentException When an invalid URL is passed
*/
private static function resolveUrl(array $url, ?array $base, array $queryDefaults = []): array
{
$givenUrl = $url;
if (null !== $base && '' === ($base['scheme'] ?? '').($base['authority'] ?? '')) {
throw new InvalidArgumentException(sprintf('Invalid "base_uri" option: host or scheme is missing in "%s".', implode('', $base)));
}
if (null === $url['scheme'] && (null === $base || null === $base['scheme'])) {
throw new InvalidArgumentException(sprintf('Invalid URL: scheme is missing in "%s". Did you forget to add "http(s)://"?', implode('', $base ?? $url)));
}
if (null === $base && '' === $url['scheme'].$url['authority']) {
throw new InvalidArgumentException(sprintf('Invalid URL: no "base_uri" option was provided and host or scheme is missing in "%s".', implode('', $url)));
}
if (null !== $url['scheme']) {
$url['path'] = self::removeDotSegments($url['path'] ?? '');
} else {
if (null !== $url['authority']) {
$url['path'] = self::removeDotSegments($url['path'] ?? '');
} else {
if (null === $url['path']) {
$url['path'] = $base['path'];
$url['query'] = $url['query'] ?? $base['query'];
} else {
if ('/' !== $url['path'][0]) {
if (null === $base['path']) {
$url['path'] = '/'.$url['path'];
} else {
$segments = explode('/', $base['path']);
array_splice($segments, -1, 1, [$url['path']]);
$url['path'] = implode('/', $segments);
}
}
$url['path'] = self::removeDotSegments($url['path']);
}
$url['authority'] = $base['authority'];
if ($queryDefaults) {
$url['query'] = '?'.self::mergeQueryString(substr($url['query'] ?? '', 1), $queryDefaults, false);
}
}
$url['scheme'] = $base['scheme'];
}
if ('' === ($url['path'] ?? '')) {
$url['path'] = '/';
}
if ('?' === ($url['query'] ?? '')) {
$url['query'] = null;
}
if (null !== $url['scheme'] && null === $url['authority']) {
throw new InvalidArgumentException(\sprintf('Invalid URL: host is missing in "%s".', implode('', $givenUrl)));
}
return $url;
}
/**
* Parses a URL and fixes its encoding if needed.
*
* @throws InvalidArgumentException When an invalid URL is passed
*/
private static function parseUrl(string $url, array $query = [], array $allowedSchemes = ['http' => 80, 'https' => 443]): array
{
$tail = '';
if (false === $parts = parse_url(\strlen($url) !== strcspn($url, '?#') ? $url : $url.$tail = '#')) {
throw new InvalidArgumentException(sprintf('Malformed URL "%s".', $url));
}
if ($query) {
$parts['query'] = self::mergeQueryString($parts['query'] ?? null, $query, true);
}
$scheme = $parts['scheme'] ?? null;
$host = $parts['host'] ?? null;
if (!$scheme && $host && !str_starts_with($url, '//')) {
$parts = parse_url(':/'.$url.$tail);
$parts['path'] = substr($parts['path'], 2);
$scheme = $host = null;
}
$port = $parts['port'] ?? 0;
if (null !== $scheme) {
if (!isset($allowedSchemes[$scheme = strtolower($scheme)])) {
throw new InvalidArgumentException(sprintf('Unsupported scheme in "%s": "%s" expected.', $url, implode('" or "', array_keys($allowedSchemes))));
}
$port = $allowedSchemes[$scheme] === $port ? 0 : $port;
$scheme .= ':';
}
if (null !== $host) {
if (!\defined('INTL_IDNA_VARIANT_UTS46') && preg_match('/[\x80-\xFF]/', $host)) {
throw new InvalidArgumentException(sprintf('Unsupported IDN "%s", try enabling the "intl" PHP extension or running "composer require symfony/polyfill-intl-idn".', $host));
}
$host = \defined('INTL_IDNA_VARIANT_UTS46') ? idn_to_ascii($host, \IDNA_DEFAULT | \IDNA_USE_STD3_RULES | \IDNA_CHECK_BIDI | \IDNA_CHECK_CONTEXTJ | \IDNA_NONTRANSITIONAL_TO_ASCII, \INTL_IDNA_VARIANT_UTS46) ?: strtolower($host) : strtolower($host);
$host .= $port ? ':'.$port : '';
}
foreach (['user', 'pass', 'path', 'query', 'fragment'] as $part) {
if (!isset($parts[$part])) {
continue;
}
if (str_contains($parts[$part], '%')) {
// https://tools.ietf.org/html/rfc3986#section-2.3
$parts[$part] = preg_replace_callback('/%(?:2[DE]|3[0-9]|[46][1-9A-F]|5F|[57][0-9A]|7E)++/i', function ($m) { return rawurldecode($m[0]); }, $parts[$part]);
}
// https://tools.ietf.org/html/rfc3986#section-3.3
$parts[$part] = preg_replace_callback("#[^-A-Za-z0-9._~!$&/'()[\]*+,;=:@{}%]++#", function ($m) { return rawurlencode($m[0]); }, $parts[$part]);
}
return [
'scheme' => $scheme,
'authority' => null !== $host ? '//'.(isset($parts['user']) ? $parts['user'].(isset($parts['pass']) ? ':'.$parts['pass'] : '').'@' : '').$host : null,
'path' => isset($parts['path'][0]) ? $parts['path'] : null,
'query' => isset($parts['query']) ? '?'.$parts['query'] : null,
'fragment' => isset($parts['fragment']) && !$tail ? '#'.$parts['fragment'] : null,
];
}
/**
* Removes dot-segments from a path.
*
* @see https://tools.ietf.org/html/rfc3986#section-5.2.4
*/
private static function removeDotSegments(string $path)
{
$result = '';
while (!\in_array($path, ['', '.', '..'], true)) {
if ('.' === $path[0] && (str_starts_with($path, $p = '../') || str_starts_with($path, $p = './'))) {
$path = substr($path, \strlen($p));
} elseif ('/.' === $path || str_starts_with($path, '/./')) {
$path = substr_replace($path, '/', 0, 3);
} elseif ('/..' === $path || str_starts_with($path, '/../')) {
$i = strrpos($result, '/');
$result = $i ? substr($result, 0, $i) : '';
$path = substr_replace($path, '/', 0, 4);
} else {
$i = strpos($path, '/', 1) ?: \strlen($path);
$result .= substr($path, 0, $i);
$path = substr($path, $i);
}
}
return $result;
}
/**
* Merges and encodes a query array with a query string.
*
* @throws InvalidArgumentException When an invalid query-string value is passed
*/
private static function mergeQueryString(?string $queryString, array $queryArray, bool $replace): ?string
{
if (!$queryArray) {
return $queryString;
}
$query = [];
if (null !== $queryString) {
foreach (explode('&', $queryString) as $v) {
if ('' !== $v) {
$k = urldecode(explode('=', $v, 2)[0]);
$query[$k] = (isset($query[$k]) ? $query[$k].'&' : '').$v;
}
}
}
if ($replace) {
foreach ($queryArray as $k => $v) {
if (null === $v) {
unset($query[$k]);
}
}
}
$queryString = http_build_query($queryArray, '', '&', \PHP_QUERY_RFC3986);
$queryArray = [];
if ($queryString) {
if (str_contains($queryString, '%')) {
// https://tools.ietf.org/html/rfc3986#section-2.3 + some chars not encoded by browsers
$queryString = strtr($queryString, [
'%21' => '!',
'%24' => '$',
'%28' => '(',
'%29' => ')',
'%2A' => '*',
'%2F' => '/',
'%3A' => ':',
'%3B' => ';',
'%40' => '@',
'%5B' => '[',
'%5D' => ']',
]);
}
foreach (explode('&', $queryString) as $v) {
$queryArray[rawurldecode(explode('=', $v, 2)[0])] = $v;
}
}
return implode('&', $replace ? array_replace($query, $queryArray) : ($query + $queryArray));
}
/**
* Loads proxy configuration from the same environment variables as curl when no proxy is explicitly set.
*/
private static function getProxy(?string $proxy, array $url, ?string $noProxy): ?array
{
if (null === $proxy = self::getProxyUrl($proxy, $url)) {
return null;
}
$proxy = (parse_url($proxy) ?: []) + ['scheme' => 'http'];
if (!isset($proxy['host'])) {
throw new TransportException('Invalid HTTP proxy: host is missing.');
}
if ('http' === $proxy['scheme']) {
$proxyUrl = 'tcp://'.$proxy['host'].':'.($proxy['port'] ?? '80');
} elseif ('https' === $proxy['scheme']) {
$proxyUrl = 'ssl://'.$proxy['host'].':'.($proxy['port'] ?? '443');
} else {
throw new TransportException(sprintf('Unsupported proxy scheme "%s": "http" or "https" expected.', $proxy['scheme']));
}
$noProxy = $noProxy ?? $_SERVER['no_proxy'] ?? $_SERVER['NO_PROXY'] ?? '';
$noProxy = $noProxy ? preg_split('/[\s,]+/', $noProxy) : [];
return [
'url' => $proxyUrl,
'auth' => isset($proxy['user']) ? 'Basic '.base64_encode(rawurldecode($proxy['user']).':'.rawurldecode($proxy['pass'] ?? '')) : null,
'no_proxy' => $noProxy,
];
}
private static function getProxyUrl(?string $proxy, array $url): ?string
{
if (null !== $proxy) {
return $proxy;
}
// Ignore HTTP_PROXY except on the CLI to work around httpoxy set of vulnerabilities
$proxy = $_SERVER['http_proxy'] ?? (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) ? $_SERVER['HTTP_PROXY'] ?? null : null) ?? $_SERVER['all_proxy'] ?? $_SERVER['ALL_PROXY'] ?? null;
if ('https:' === $url['scheme']) {
$proxy = $_SERVER['https_proxy'] ?? $_SERVER['HTTPS_PROXY'] ?? $proxy;
}
return $proxy;
}
private static function shouldBuffer(array $headers): bool
{
if (null === $contentType = $headers['content-type'][0] ?? null) {
return false;
}
if (false !== $i = strpos($contentType, ';')) {
$contentType = substr($contentType, 0, $i);
}
return $contentType && preg_match('#^(?:text/|application/(?:.+\+)?(?:json|xml)$)#i', $contentType);
}
}

View File

@@ -0,0 +1,333 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\HttpClientInterface;
/**
* A helper providing autocompletion for available options.
*
* @see HttpClientInterface for a description of each options.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class HttpOptions
{
private $options = [];
public function toArray(): array
{
return $this->options;
}
/**
* @return $this
*/
public function setAuthBasic(string $user, string $password = '')
{
$this->options['auth_basic'] = $user;
if ('' !== $password) {
$this->options['auth_basic'] .= ':'.$password;
}
return $this;
}
/**
* @return $this
*/
public function setAuthBearer(string $token)
{
$this->options['auth_bearer'] = $token;
return $this;
}
/**
* @return $this
*/
public function setQuery(array $query)
{
$this->options['query'] = $query;
return $this;
}
/**
* @return $this
*/
public function setHeaders(iterable $headers)
{
$this->options['headers'] = $headers;
return $this;
}
/**
* @param array|string|resource|\Traversable|\Closure $body
*
* @return $this
*/
public function setBody($body)
{
$this->options['body'] = $body;
return $this;
}
/**
* @param mixed $json
*
* @return $this
*/
public function setJson($json)
{
$this->options['json'] = $json;
return $this;
}
/**
* @return $this
*/
public function setUserData($data)
{
$this->options['user_data'] = $data;
return $this;
}
/**
* @return $this
*/
public function setMaxRedirects(int $max)
{
$this->options['max_redirects'] = $max;
return $this;
}
/**
* @return $this
*/
public function setHttpVersion(string $version)
{
$this->options['http_version'] = $version;
return $this;
}
/**
* @return $this
*/
public function setBaseUri(string $uri)
{
$this->options['base_uri'] = $uri;
return $this;
}
/**
* @return $this
*/
public function buffer(bool $buffer)
{
$this->options['buffer'] = $buffer;
return $this;
}
/**
* @param callable(int, int, array, \Closure|null=):void $callback
*
* @return $this
*/
public function setOnProgress(callable $callback)
{
$this->options['on_progress'] = $callback;
return $this;
}
/**
* @return $this
*/
public function resolve(array $hostIps)
{
$this->options['resolve'] = $hostIps;
return $this;
}
/**
* @return $this
*/
public function setProxy(string $proxy)
{
$this->options['proxy'] = $proxy;
return $this;
}
/**
* @return $this
*/
public function setNoProxy(string $noProxy)
{
$this->options['no_proxy'] = $noProxy;
return $this;
}
/**
* @return $this
*/
public function setTimeout(float $timeout)
{
$this->options['timeout'] = $timeout;
return $this;
}
/**
* @return $this
*/
public function setMaxDuration(float $maxDuration)
{
$this->options['max_duration'] = $maxDuration;
return $this;
}
/**
* @return $this
*/
public function bindTo(string $bindto)
{
$this->options['bindto'] = $bindto;
return $this;
}
/**
* @return $this
*/
public function verifyPeer(bool $verify)
{
$this->options['verify_peer'] = $verify;
return $this;
}
/**
* @return $this
*/
public function verifyHost(bool $verify)
{
$this->options['verify_host'] = $verify;
return $this;
}
/**
* @return $this
*/
public function setCaFile(string $cafile)
{
$this->options['cafile'] = $cafile;
return $this;
}
/**
* @return $this
*/
public function setCaPath(string $capath)
{
$this->options['capath'] = $capath;
return $this;
}
/**
* @return $this
*/
public function setLocalCert(string $cert)
{
$this->options['local_cert'] = $cert;
return $this;
}
/**
* @return $this
*/
public function setLocalPk(string $pk)
{
$this->options['local_pk'] = $pk;
return $this;
}
/**
* @return $this
*/
public function setPassphrase(string $passphrase)
{
$this->options['passphrase'] = $passphrase;
return $this;
}
/**
* @return $this
*/
public function setCiphers(string $ciphers)
{
$this->options['ciphers'] = $ciphers;
return $this;
}
/**
* @param string|array $fingerprint
*
* @return $this
*/
public function setPeerFingerprint($fingerprint)
{
$this->options['peer_fingerprint'] = $fingerprint;
return $this;
}
/**
* @return $this
*/
public function capturePeerCertChain(bool $capture)
{
$this->options['capture_peer_cert_chain'] = $capture;
return $this;
}
/**
* @return $this
*/
public function setExtra(string $name, $value)
{
$this->options['extra'][$name] = $value;
return $this;
}
}

View File

@@ -0,0 +1,276 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient;
use GuzzleHttp\Promise\Promise as GuzzlePromise;
use GuzzleHttp\Promise\RejectedPromise;
use GuzzleHttp\Promise\Utils;
use Http\Client\Exception\NetworkException;
use Http\Client\Exception\RequestException;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient as HttplugInterface;
use Http\Discovery\Exception\NotFoundException;
use Http\Discovery\Psr17FactoryDiscovery;
use Http\Message\RequestFactory;
use Http\Message\StreamFactory;
use Http\Message\UriFactory;
use Http\Promise\Promise;
use KadenceWP\KadenceBlocks\Nyholm\Psr7\Factory\Psr17Factory;
use KadenceWP\KadenceBlocks\Nyholm\Psr7\Request;
use KadenceWP\KadenceBlocks\Nyholm\Psr7\Uri;
use KadenceWP\KadenceBlocks\Psr\Http\Message\RequestFactoryInterface;
use KadenceWP\KadenceBlocks\Psr\Http\Message\RequestInterface;
use KadenceWP\KadenceBlocks\Psr\Http\Message\ResponseFactoryInterface;
use KadenceWP\KadenceBlocks\Psr\Http\Message\ResponseInterface as Psr7ResponseInterface;
use KadenceWP\KadenceBlocks\Psr\Http\Message\StreamFactoryInterface;
use KadenceWP\KadenceBlocks\Psr\Http\Message\StreamInterface;
use KadenceWP\KadenceBlocks\Psr\Http\Message\UriFactoryInterface;
use KadenceWP\KadenceBlocks\Psr\Http\Message\UriInterface;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal\HttplugWaitLoop;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\HttplugPromise;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\HttpClientInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\Service\ResetInterface;
if (!interface_exists(HttplugInterface::class)) {
throw new \LogicException('You cannot use "KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\HttplugClient" as the "php-http/httplug" package is not installed. Try running "composer require php-http/httplug".');
}
if (!interface_exists(RequestFactory::class)) {
throw new \LogicException('You cannot use "KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\HttplugClient" as the "php-http/message-factory" package is not installed. Try running "composer require php-http/message-factory".');
}
/**
* An adapter to turn a Symfony HttpClientInterface into an Httplug client.
*
* Run "composer require nyholm/psr7" to install an efficient implementation of response
* and stream factories with flex-provided autowiring aliases.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class HttplugClient implements HttplugInterface, HttpAsyncClient, RequestFactory, StreamFactory, UriFactory, ResetInterface
{
private $client;
private $responseFactory;
private $streamFactory;
/**
* @var \SplObjectStorage<ResponseInterface, array{RequestInterface, Promise}>|null
*/
private $promisePool;
private $waitLoop;
public function __construct(?HttpClientInterface $client = null, ?ResponseFactoryInterface $responseFactory = null, ?StreamFactoryInterface $streamFactory = null)
{
$this->client = $client ?? HttpClient::create();
$this->responseFactory = $responseFactory;
$this->streamFactory = $streamFactory ?? ($responseFactory instanceof StreamFactoryInterface ? $responseFactory : null);
$this->promisePool = class_exists(Utils::class) ? new \SplObjectStorage() : null;
if (null === $this->responseFactory || null === $this->streamFactory) {
if (!class_exists(Psr17Factory::class) && !class_exists(Psr17FactoryDiscovery::class)) {
throw new \LogicException('You cannot use the "KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\HttplugClient" as no PSR-17 factories have been provided. Try running "composer require nyholm/psr7".');
}
try {
$psr17Factory = class_exists(Psr17Factory::class, false) ? new Psr17Factory() : null;
$this->responseFactory = $this->responseFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findResponseFactory();
$this->streamFactory = $this->streamFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findStreamFactory();
} catch (NotFoundException $e) {
throw new \LogicException('You cannot use the "KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\HttplugClient" as no PSR-17 factories have been found. Try running "composer require nyholm/psr7".', 0, $e);
}
}
$this->waitLoop = new HttplugWaitLoop($this->client, $this->promisePool, $this->responseFactory, $this->streamFactory);
}
/**
* {@inheritdoc}
*/
public function sendRequest(RequestInterface $request): Psr7ResponseInterface
{
try {
return HttplugWaitLoop::createPsr7Response($this->responseFactory, $this->streamFactory, $this->client, $this->sendPsr7Request($request), true);
} catch (TransportExceptionInterface $e) {
throw new NetworkException($e->getMessage(), $request, $e);
}
}
/**
* {@inheritdoc}
*
* @return HttplugPromise
*/
public function sendAsyncRequest(RequestInterface $request): Promise
{
if (!$promisePool = $this->promisePool) {
throw new \LogicException(sprintf('You cannot use "%s()" as the "guzzlehttp/promises" package is not installed. Try running "composer require guzzlehttp/promises".', __METHOD__));
}
try {
$response = $this->sendPsr7Request($request, true);
} catch (NetworkException $e) {
return new HttplugPromise(new RejectedPromise($e));
}
$waitLoop = $this->waitLoop;
$promise = new GuzzlePromise(static function () use ($response, $waitLoop) {
$waitLoop->wait($response);
}, static function () use ($response, $promisePool) {
$response->cancel();
unset($promisePool[$response]);
});
$promisePool[$response] = [$request, $promise];
return new HttplugPromise($promise);
}
/**
* Resolves pending promises that complete before the timeouts are reached.
*
* When $maxDuration is null and $idleTimeout is reached, promises are rejected.
*
* @return int The number of remaining pending promises
*/
public function wait(?float $maxDuration = null, ?float $idleTimeout = null): int
{
return $this->waitLoop->wait(null, $maxDuration, $idleTimeout);
}
/**
* {@inheritdoc}
*/
public function createRequest($method, $uri, array $headers = [], $body = null, $protocolVersion = '1.1'): RequestInterface
{
if ($this->responseFactory instanceof RequestFactoryInterface) {
$request = $this->responseFactory->createRequest($method, $uri);
} elseif (class_exists(Request::class)) {
$request = new Request($method, $uri);
} elseif (class_exists(Psr17FactoryDiscovery::class)) {
$request = Psr17FactoryDiscovery::findRequestFactory()->createRequest($method, $uri);
} else {
throw new \LogicException(sprintf('You cannot use "%s()" as the "nyholm/psr7" package is not installed. Try running "composer require nyholm/psr7".', __METHOD__));
}
$request = $request
->withProtocolVersion($protocolVersion)
->withBody($this->createStream($body))
;
foreach ($headers as $name => $value) {
$request = $request->withAddedHeader($name, $value);
}
return $request;
}
/**
* {@inheritdoc}
*/
public function createStream($body = null): StreamInterface
{
if ($body instanceof StreamInterface) {
return $body;
}
if (\is_string($body ?? '')) {
$stream = $this->streamFactory->createStream($body ?? '');
} elseif (\is_resource($body)) {
$stream = $this->streamFactory->createStreamFromResource($body);
} else {
throw new \InvalidArgumentException(sprintf('"%s()" expects string, resource or StreamInterface, "%s" given.', __METHOD__, get_debug_type($body)));
}
if ($stream->isSeekable()) {
$stream->seek(0);
}
return $stream;
}
/**
* {@inheritdoc}
*/
public function createUri($uri): UriInterface
{
if ($uri instanceof UriInterface) {
return $uri;
}
if ($this->responseFactory instanceof UriFactoryInterface) {
return $this->responseFactory->createUri($uri);
}
if (class_exists(Uri::class)) {
return new Uri($uri);
}
if (class_exists(Psr17FactoryDiscovery::class)) {
return Psr17FactoryDiscovery::findUrlFactory()->createUri($uri);
}
throw new \LogicException(sprintf('You cannot use "%s()" as the "nyholm/psr7" package is not installed. Try running "composer require nyholm/psr7".', __METHOD__));
}
public function __sleep(): array
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
$this->wait();
}
public function reset()
{
if ($this->client instanceof ResetInterface) {
$this->client->reset();
}
}
private function sendPsr7Request(RequestInterface $request, ?bool $buffer = null): ResponseInterface
{
try {
$body = $request->getBody();
if ($body->isSeekable()) {
$body->seek(0);
}
$options = [
'headers' => $request->getHeaders(),
'body' => $body->getContents(),
'buffer' => $buffer,
];
if ('1.0' === $request->getProtocolVersion()) {
$options['http_version'] = '1.0';
}
return $this->client->request($request->getMethod(), (string) $request->getUri(), $options);
} catch (\InvalidArgumentException $e) {
throw new RequestException($e->getMessage(), $request, $e);
} catch (TransportExceptionInterface $e) {
throw new NetworkException($e->getMessage(), $request, $e);
}
}
}

View File

@@ -0,0 +1,142 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal;
use Amp\ByteStream\InputStream;
use Amp\ByteStream\ResourceInputStream;
use Amp\Http\Client\RequestBody;
use Amp\Promise;
use Amp\Success;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\TransportException;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class AmpBody implements RequestBody, InputStream
{
private $body;
private $info;
private $onProgress;
private $offset = 0;
private $length = -1;
private $uploaded;
public function __construct($body, &$info, \Closure $onProgress)
{
$this->body = $body;
$this->info = &$info;
$this->onProgress = $onProgress;
if (\is_resource($body)) {
$this->offset = ftell($body);
$this->length = fstat($body)['size'];
$this->body = new ResourceInputStream($body);
} elseif (\is_string($body)) {
$this->length = \strlen($body);
}
}
public function createBodyStream(): InputStream
{
if (null !== $this->uploaded) {
$this->uploaded = null;
if (\is_string($this->body)) {
$this->offset = 0;
} elseif ($this->body instanceof ResourceInputStream) {
fseek($this->body->getResource(), $this->offset);
}
}
return $this;
}
public function getHeaders(): Promise
{
return new Success([]);
}
public function getBodyLength(): Promise
{
return new Success($this->length - $this->offset);
}
public function read(): Promise
{
$this->info['size_upload'] += $this->uploaded;
$this->uploaded = 0;
($this->onProgress)();
$chunk = $this->doRead();
$chunk->onResolve(function ($e, $data) {
if (null !== $data) {
$this->uploaded = \strlen($data);
} else {
$this->info['upload_content_length'] = $this->info['size_upload'];
}
});
return $chunk;
}
public static function rewind(RequestBody $body): RequestBody
{
if (!$body instanceof self) {
return $body;
}
$body->uploaded = null;
if ($body->body instanceof ResourceInputStream) {
fseek($body->body->getResource(), $body->offset);
return new $body($body->body, $body->info, $body->onProgress);
}
if (\is_string($body->body)) {
$body->offset = 0;
}
return $body;
}
private function doRead(): Promise
{
if ($this->body instanceof ResourceInputStream) {
return $this->body->read();
}
if (null === $this->offset || !$this->length) {
return new Success();
}
if (\is_string($this->body)) {
$this->offset = null;
return new Success($this->body);
}
if ('' === $data = ($this->body)(16372)) {
$this->offset = null;
return new Success();
}
if (!\is_string($data)) {
throw new TransportException(sprintf('Return value of the "body" option callback must be string, "%s" returned.', get_debug_type($data)));
}
return new Success($data);
}
}

View File

@@ -0,0 +1,217 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal;
use Amp\CancellationToken;
use Amp\Deferred;
use Amp\Http\Client\Connection\ConnectionLimitingPool;
use Amp\Http\Client\Connection\DefaultConnectionFactory;
use Amp\Http\Client\InterceptedHttpClient;
use Amp\Http\Client\Interceptor\RetryRequests;
use Amp\Http\Client\PooledHttpClient;
use Amp\Http\Client\Request;
use Amp\Http\Client\Response;
use Amp\Http\Tunnel\Http1TunnelConnector;
use Amp\Http\Tunnel\Https1TunnelConnector;
use Amp\Promise;
use Amp\Socket\Certificate;
use Amp\Socket\ClientTlsContext;
use Amp\Socket\ConnectContext;
use Amp\Socket\Connector;
use Amp\Socket\DnsConnector;
use Amp\Socket\SocketAddress;
use Amp\Success;
use KadenceWP\KadenceBlocks\Psr\Log\LoggerInterface;
/**
* Internal representation of the Amp client's state.
*
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
final class AmpClientState extends ClientState
{
public $dnsCache = [];
public $responseCount = 0;
public $pushedResponses = [];
private $clients = [];
private $clientConfigurator;
private $maxHostConnections;
private $maxPendingPushes;
private $logger;
public function __construct(?callable $clientConfigurator, int $maxHostConnections, int $maxPendingPushes, ?LoggerInterface &$logger)
{
$this->clientConfigurator = $clientConfigurator ?? static function (PooledHttpClient $client) {
return new InterceptedHttpClient($client, new RetryRequests(2));
};
$this->maxHostConnections = $maxHostConnections;
$this->maxPendingPushes = $maxPendingPushes;
$this->logger = &$logger;
}
/**
* @return Promise<Response>
*/
public function request(array $options, Request $request, CancellationToken $cancellation, array &$info, \Closure $onProgress, &$handle): Promise
{
if ($options['proxy']) {
if ($request->hasHeader('proxy-authorization')) {
$options['proxy']['auth'] = $request->getHeader('proxy-authorization');
}
// Matching "no_proxy" should follow the behavior of curl
$host = $request->getUri()->getHost();
foreach ($options['proxy']['no_proxy'] as $rule) {
$dotRule = '.'.ltrim($rule, '.');
if ('*' === $rule || $host === $rule || substr($host, -\strlen($dotRule)) === $dotRule) {
$options['proxy'] = null;
break;
}
}
}
$request = clone $request;
if ($request->hasHeader('proxy-authorization')) {
$request->removeHeader('proxy-authorization');
}
if ($options['capture_peer_cert_chain']) {
$info['peer_certificate_chain'] = [];
}
$request->addEventListener(new AmpListener($info, $options['peer_fingerprint']['pin-sha256'] ?? [], $onProgress, $handle));
$request->setPushHandler(function ($request, $response) use ($options): Promise {
return $this->handlePush($request, $response, $options);
});
($request->hasHeader('content-length') ? new Success((int) $request->getHeader('content-length')) : $request->getBody()->getBodyLength())
->onResolve(static function ($e, $bodySize) use (&$info) {
if (null !== $bodySize && 0 <= $bodySize) {
$info['upload_content_length'] = ((1 + $info['upload_content_length']) ?? 1) - 1 + $bodySize;
}
});
[$client, $connector] = $this->getClient($options);
$response = $client->request($request, $cancellation);
$response->onResolve(static function ($e) use ($connector, &$handle) {
if (null === $e) {
$handle = $connector->handle;
}
});
return $response;
}
private function getClient(array $options): array
{
$options = [
'bindto' => $options['bindto'] ?: '0',
'verify_peer' => $options['verify_peer'],
'capath' => $options['capath'],
'cafile' => $options['cafile'],
'local_cert' => $options['local_cert'],
'local_pk' => $options['local_pk'],
'ciphers' => $options['ciphers'],
'capture_peer_cert_chain' => $options['capture_peer_cert_chain'] || $options['peer_fingerprint'],
'proxy' => $options['proxy'],
];
$key = md5(serialize($options));
if (isset($this->clients[$key])) {
return $this->clients[$key];
}
$context = new ClientTlsContext('');
$options['verify_peer'] || $context = $context->withoutPeerVerification();
$options['cafile'] && $context = $context->withCaFile($options['cafile']);
$options['capath'] && $context = $context->withCaPath($options['capath']);
$options['local_cert'] && $context = $context->withCertificate(new Certificate($options['local_cert'], $options['local_pk']));
$options['ciphers'] && $context = $context->withCiphers($options['ciphers']);
$options['capture_peer_cert_chain'] && $context = $context->withPeerCapturing();
$connector = $handleConnector = new class() implements Connector {
public $connector;
public $uri;
public $handle;
public function connect(string $uri, ?ConnectContext $context = null, ?CancellationToken $token = null): Promise
{
$result = $this->connector->connect($this->uri ?? $uri, $context, $token);
$result->onResolve(function ($e, $socket) {
$this->handle = null !== $socket ? $socket->getResource() : false;
});
return $result;
}
};
$connector->connector = new DnsConnector(new AmpResolver($this->dnsCache));
$context = (new ConnectContext())
->withTcpNoDelay()
->withTlsContext($context);
if ($options['bindto']) {
if (file_exists($options['bindto'])) {
$connector->uri = 'unix://'.$options['bindto'];
} else {
$context = $context->withBindTo($options['bindto']);
}
}
if ($options['proxy']) {
$proxyUrl = parse_url($options['proxy']['url']);
$proxySocket = new SocketAddress($proxyUrl['host'], $proxyUrl['port']);
$proxyHeaders = $options['proxy']['auth'] ? ['Proxy-Authorization' => $options['proxy']['auth']] : [];
if ('ssl' === $proxyUrl['scheme']) {
$connector = new Https1TunnelConnector($proxySocket, $context->getTlsContext(), $proxyHeaders, $connector);
} else {
$connector = new Http1TunnelConnector($proxySocket, $proxyHeaders, $connector);
}
}
$maxHostConnections = 0 < $this->maxHostConnections ? $this->maxHostConnections : \PHP_INT_MAX;
$pool = new DefaultConnectionFactory($connector, $context);
$pool = ConnectionLimitingPool::byAuthority($maxHostConnections, $pool);
return $this->clients[$key] = [($this->clientConfigurator)(new PooledHttpClient($pool)), $handleConnector];
}
private function handlePush(Request $request, Promise $response, array $options): Promise
{
$deferred = new Deferred();
$authority = $request->getUri()->getAuthority();
if ($this->maxPendingPushes <= \count($this->pushedResponses[$authority] ?? [])) {
$fifoUrl = key($this->pushedResponses[$authority]);
unset($this->pushedResponses[$authority][$fifoUrl]);
$this->logger && $this->logger->debug(sprintf('Evicting oldest pushed response: "%s"', $fifoUrl));
}
$url = (string) $request->getUri();
$this->logger && $this->logger->debug(sprintf('Queueing pushed response: "%s"', $url));
$this->pushedResponses[$authority][] = [$url, $deferred, $request, $response, [
'proxy' => $options['proxy'],
'bindto' => $options['bindto'],
'local_cert' => $options['local_cert'],
'local_pk' => $options['local_pk'],
]];
return $deferred->promise();
}
}

View File

@@ -0,0 +1,183 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal;
use Amp\Http\Client\Connection\Stream;
use Amp\Http\Client\EventListener;
use Amp\Http\Client\Request;
use Amp\Promise;
use Amp\Success;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\TransportException;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class AmpListener implements EventListener
{
private $info;
private $pinSha256;
private $onProgress;
private $handle;
public function __construct(array &$info, array $pinSha256, \Closure $onProgress, &$handle)
{
$info += [
'connect_time' => 0.0,
'pretransfer_time' => 0.0,
'starttransfer_time' => 0.0,
'total_time' => 0.0,
'namelookup_time' => 0.0,
'primary_ip' => '',
'primary_port' => 0,
];
$this->info = &$info;
$this->pinSha256 = $pinSha256;
$this->onProgress = $onProgress;
$this->handle = &$handle;
}
public function startRequest(Request $request): Promise
{
$this->info['start_time'] = $this->info['start_time'] ?? microtime(true);
($this->onProgress)();
return new Success();
}
public function startDnsResolution(Request $request): Promise
{
($this->onProgress)();
return new Success();
}
public function startConnectionCreation(Request $request): Promise
{
($this->onProgress)();
return new Success();
}
public function startTlsNegotiation(Request $request): Promise
{
($this->onProgress)();
return new Success();
}
public function startSendingRequest(Request $request, Stream $stream): Promise
{
$host = $stream->getRemoteAddress()->getHost();
$this->info['primary_ip'] = $host;
if (false !== strpos($host, ':')) {
$host = '['.$host.']';
}
$this->info['primary_port'] = $stream->getRemoteAddress()->getPort();
$this->info['pretransfer_time'] = microtime(true) - $this->info['start_time'];
$this->info['debug'] .= sprintf("* Connected to %s (%s) port %d\n", $request->getUri()->getHost(), $host, $this->info['primary_port']);
if ((isset($this->info['peer_certificate_chain']) || $this->pinSha256) && null !== $tlsInfo = $stream->getTlsInfo()) {
foreach ($tlsInfo->getPeerCertificates() as $cert) {
$this->info['peer_certificate_chain'][] = openssl_x509_read($cert->toPem());
}
if ($this->pinSha256) {
$pin = openssl_pkey_get_public($this->info['peer_certificate_chain'][0]);
$pin = openssl_pkey_get_details($pin)['key'];
$pin = \array_slice(explode("\n", $pin), 1, -2);
$pin = base64_decode(implode('', $pin));
$pin = base64_encode(hash('sha256', $pin, true));
if (!\in_array($pin, $this->pinSha256, true)) {
throw new TransportException(sprintf('SSL public key does not match pinned public key for "%s".', $this->info['url']));
}
}
}
($this->onProgress)();
$uri = $request->getUri();
$requestUri = $uri->getPath() ?: '/';
if ('' !== $query = $uri->getQuery()) {
$requestUri .= '?'.$query;
}
if ('CONNECT' === $method = $request->getMethod()) {
$requestUri = $uri->getHost().': '.($uri->getPort() ?? ('https' === $uri->getScheme() ? 443 : 80));
}
$this->info['debug'] .= sprintf("> %s %s HTTP/%s \r\n", $method, $requestUri, $request->getProtocolVersions()[0]);
foreach ($request->getRawHeaders() as [$name, $value]) {
$this->info['debug'] .= $name.': '.$value."\r\n";
}
$this->info['debug'] .= "\r\n";
return new Success();
}
public function completeSendingRequest(Request $request, Stream $stream): Promise
{
($this->onProgress)();
return new Success();
}
public function startReceivingResponse(Request $request, Stream $stream): Promise
{
$this->info['starttransfer_time'] = microtime(true) - $this->info['start_time'];
($this->onProgress)();
return new Success();
}
public function completeReceivingResponse(Request $request, Stream $stream): Promise
{
$this->handle = null;
($this->onProgress)();
return new Success();
}
public function completeDnsResolution(Request $request): Promise
{
$this->info['namelookup_time'] = microtime(true) - $this->info['start_time'];
($this->onProgress)();
return new Success();
}
public function completeConnectionCreation(Request $request): Promise
{
$this->info['connect_time'] = microtime(true) - $this->info['start_time'];
($this->onProgress)();
return new Success();
}
public function completeTlsNegotiation(Request $request): Promise
{
($this->onProgress)();
return new Success();
}
public function abort(Request $request, \Throwable $cause): Promise
{
return new Success();
}
}

View File

@@ -0,0 +1,64 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal;
use Amp\Dns;
use Amp\Dns\Record;
use Amp\Promise;
use Amp\Success;
/**
* Handles local overrides for the DNS resolver.
*
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class AmpResolver implements Dns\Resolver
{
private $dnsMap;
public function __construct(array &$dnsMap)
{
$this->dnsMap = &$dnsMap;
}
public function resolve(string $name, ?int $typeRestriction = null): Promise
{
$recordType = Record::A;
$ip = $this->dnsMap[$name] ?? null;
if (null !== $ip && str_contains($ip, ':')) {
$recordType = Record::AAAA;
}
if (null === $ip || $recordType !== ($typeRestriction ?? $recordType)) {
return Dns\resolver()->resolve($name, $typeRestriction);
}
return new Success([new Record($ip, $recordType, null)]);
}
public function query(string $name, int $type): Promise
{
$recordType = Record::A;
$ip = $this->dnsMap[$name] ?? null;
if (null !== $ip && str_contains($ip, ':')) {
$recordType = Record::AAAA;
}
if (null === $ip || $recordType !== $type) {
return Dns\resolver()->query($name, $type);
}
return new Success([new Record($ip, $recordType, null)]);
}
}

View File

@@ -0,0 +1,40 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
final class Canary
{
private $canceller;
public function __construct(\Closure $canceller)
{
$this->canceller = $canceller;
}
public function cancel()
{
if (($canceller = $this->canceller) instanceof \Closure) {
$this->canceller = null;
$canceller();
}
}
public function __destruct()
{
$this->cancel();
}
}

View File

@@ -0,0 +1,26 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal;
/**
* Internal representation of the client state.
*
* @author Alexander M. Turek <me@derrabus.de>
*
* @internal
*/
class ClientState
{
public $handlesActivity = [];
public $openHandles = [];
public $lastTimeout;
}

View File

@@ -0,0 +1,149 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal;
use KadenceWP\KadenceBlocks\Psr\Log\LoggerInterface;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\CurlResponse;
/**
* Internal representation of the cURL client's state.
*
* @author Alexander M. Turek <me@derrabus.de>
*
* @internal
*/
final class CurlClientState extends ClientState
{
/** @var \CurlMultiHandle|resource|null */
public $handle;
/** @var \CurlShareHandle|resource|null */
public $share;
/** @var PushedResponse[] */
public $pushedResponses = [];
/** @var DnsCache */
public $dnsCache;
/** @var float[] */
public $pauseExpiries = [];
public $execCounter = \PHP_INT_MIN;
/** @var LoggerInterface|null */
public $logger;
public $performing = false;
public static $curlVersion;
public function __construct(int $maxHostConnections, int $maxPendingPushes)
{
self::$curlVersion = self::$curlVersion ?? curl_version();
$this->handle = curl_multi_init();
$this->dnsCache = new DnsCache();
$this->reset();
// Don't enable HTTP/1.1 pipelining: it forces responses to be sent in order
if (\defined('CURLPIPE_MULTIPLEX')) {
curl_multi_setopt($this->handle, \CURLMOPT_PIPELINING, \CURLPIPE_MULTIPLEX);
}
if (\defined('CURLMOPT_MAX_HOST_CONNECTIONS') && 0 < $maxHostConnections) {
$maxHostConnections = curl_multi_setopt($this->handle, \CURLMOPT_MAX_HOST_CONNECTIONS, $maxHostConnections) ? 4294967295 : $maxHostConnections;
}
if (\defined('CURLMOPT_MAXCONNECTS') && 0 < $maxHostConnections) {
curl_multi_setopt($this->handle, \CURLMOPT_MAXCONNECTS, $maxHostConnections);
}
// Skip configuring HTTP/2 push when it's unsupported or buggy, see https://bugs.php.net/77535
if (0 >= $maxPendingPushes || \PHP_VERSION_ID < 70217 || (\PHP_VERSION_ID >= 70300 && \PHP_VERSION_ID < 70304)) {
return;
}
// HTTP/2 push crashes before curl 7.61
if (!\defined('CURLMOPT_PUSHFUNCTION') || 0x073D00 > self::$curlVersion['version_number'] || !(\CURL_VERSION_HTTP2 & self::$curlVersion['features'])) {
return;
}
// Clone to prevent a circular reference
$multi = clone $this;
$multi->handle = null;
$multi->share = null;
$multi->pushedResponses = &$this->pushedResponses;
$multi->logger = &$this->logger;
$multi->handlesActivity = &$this->handlesActivity;
$multi->openHandles = &$this->openHandles;
curl_multi_setopt($this->handle, \CURLMOPT_PUSHFUNCTION, static function ($parent, $pushed, array $requestHeaders) use ($multi, $maxPendingPushes) {
return $multi->handlePush($parent, $pushed, $requestHeaders, $maxPendingPushes);
});
}
public function reset()
{
foreach ($this->pushedResponses as $url => $response) {
$this->logger && $this->logger->debug(sprintf('Unused pushed response: "%s"', $url));
curl_multi_remove_handle($this->handle, $response->handle);
curl_close($response->handle);
}
$this->pushedResponses = [];
$this->dnsCache->evictions = $this->dnsCache->evictions ?: $this->dnsCache->removals;
$this->dnsCache->removals = $this->dnsCache->hostnames = [];
$this->share = curl_share_init();
curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_DNS);
curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_SSL_SESSION);
if (\defined('CURL_LOCK_DATA_CONNECT') && \PHP_VERSION_ID >= 80000) {
curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_CONNECT);
}
}
private function handlePush($parent, $pushed, array $requestHeaders, int $maxPendingPushes): int
{
$headers = [];
$origin = curl_getinfo($parent, \CURLINFO_EFFECTIVE_URL);
foreach ($requestHeaders as $h) {
if (false !== $i = strpos($h, ':', 1)) {
$headers[substr($h, 0, $i)][] = substr($h, 1 + $i);
}
}
if (!isset($headers[':method']) || !isset($headers[':scheme']) || !isset($headers[':authority']) || !isset($headers[':path'])) {
$this->logger && $this->logger->debug(sprintf('Rejecting pushed response from "%s": pushed headers are invalid', $origin));
return \CURL_PUSH_DENY;
}
$url = $headers[':scheme'][0].'://'.$headers[':authority'][0];
// curl before 7.65 doesn't validate the pushed ":authority" header,
// but this is a MUST in the HTTP/2 RFC; let's restrict pushes to the original host,
// ignoring domains mentioned as alt-name in the certificate for now (same as curl).
if (!str_starts_with($origin, $url.'/')) {
$this->logger && $this->logger->debug(sprintf('Rejecting pushed response from "%s": server is not authoritative for "%s"', $origin, $url));
return \CURL_PUSH_DENY;
}
if ($maxPendingPushes <= \count($this->pushedResponses)) {
$fifoUrl = key($this->pushedResponses);
unset($this->pushedResponses[$fifoUrl]);
$this->logger && $this->logger->debug(sprintf('Evicting oldest pushed response: "%s"', $fifoUrl));
}
$url .= $headers[':path'][0];
$this->logger && $this->logger->debug(sprintf('Queueing pushed response: "%s"', $url));
$this->pushedResponses[$url] = new PushedResponse(new CurlResponse($this, $pushed), $headers, $this->openHandles[(int) $parent][1] ?? [], $pushed);
return \CURL_PUSH_OK;
}
}

View File

@@ -0,0 +1,39 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal;
/**
* Cache for resolved DNS queries.
*
* @author Alexander M. Turek <me@derrabus.de>
*
* @internal
*/
final class DnsCache
{
/**
* Resolved hostnames (hostname => IP address).
*
* @var string[]
*/
public $hostnames = [];
/**
* @var string[]
*/
public $removals = [];
/**
* @var string[]
*/
public $evictions = [];
}

View File

@@ -0,0 +1,153 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal;
use Http\Client\Exception\NetworkException;
use Http\Promise\Promise;
use KadenceWP\KadenceBlocks\Psr\Http\Message\RequestInterface as Psr7RequestInterface;
use KadenceWP\KadenceBlocks\Psr\Http\Message\ResponseFactoryInterface;
use KadenceWP\KadenceBlocks\Psr\Http\Message\ResponseInterface as Psr7ResponseInterface;
use KadenceWP\KadenceBlocks\Psr\Http\Message\StreamFactoryInterface;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\StreamableInterface;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\StreamWrapper;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\HttpClientInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
final class HttplugWaitLoop
{
private $client;
private $promisePool;
private $responseFactory;
private $streamFactory;
/**
* @param \SplObjectStorage<ResponseInterface, array{Psr7RequestInterface, Promise}>|null $promisePool
*/
public function __construct(HttpClientInterface $client, ?\SplObjectStorage $promisePool, ResponseFactoryInterface $responseFactory, StreamFactoryInterface $streamFactory)
{
$this->client = $client;
$this->promisePool = $promisePool;
$this->responseFactory = $responseFactory;
$this->streamFactory = $streamFactory;
}
public function wait(?ResponseInterface $pendingResponse, ?float $maxDuration = null, ?float $idleTimeout = null): int
{
if (!$this->promisePool) {
return 0;
}
$guzzleQueue = \GuzzleHttp\Promise\Utils::queue();
if (0.0 === $remainingDuration = $maxDuration) {
$idleTimeout = 0.0;
} elseif (null !== $maxDuration) {
$startTime = microtime(true);
$idleTimeout = max(0.0, min($maxDuration / 5, $idleTimeout ?? $maxDuration));
}
do {
foreach ($this->client->stream($this->promisePool, $idleTimeout) as $response => $chunk) {
try {
if (null !== $maxDuration && $chunk->isTimeout()) {
goto check_duration;
}
if ($chunk->isFirst()) {
// Deactivate throwing on 3/4/5xx
$response->getStatusCode();
}
if (!$chunk->isLast()) {
goto check_duration;
}
if ([, $promise] = $this->promisePool[$response] ?? null) {
unset($this->promisePool[$response]);
$promise->resolve(self::createPsr7Response($this->responseFactory, $this->streamFactory, $this->client, $response, true));
}
} catch (\Exception $e) {
if ([$request, $promise] = $this->promisePool[$response] ?? null) {
unset($this->promisePool[$response]);
if ($e instanceof TransportExceptionInterface) {
$e = new NetworkException($e->getMessage(), $request, $e);
}
$promise->reject($e);
}
}
$guzzleQueue->run();
if ($pendingResponse === $response) {
return $this->promisePool->count();
}
check_duration:
if (null !== $maxDuration && $idleTimeout && $idleTimeout > $remainingDuration = max(0.0, $maxDuration - microtime(true) + $startTime)) {
$idleTimeout = $remainingDuration / 5;
break;
}
}
if (!$count = $this->promisePool->count()) {
return 0;
}
} while (null === $maxDuration || 0 < $remainingDuration);
return $count;
}
public static function createPsr7Response(ResponseFactoryInterface $responseFactory, StreamFactoryInterface $streamFactory, HttpClientInterface $client, ResponseInterface $response, bool $buffer): Psr7ResponseInterface
{
$responseParameters = [$response->getStatusCode()];
foreach ($response->getInfo('response_headers') as $h) {
if (11 <= \strlen($h) && '/' === $h[4] && preg_match('#^HTTP/\d+(?:\.\d+)? (?:\d\d\d) (.+)#', $h, $m)) {
$responseParameters[1] = $m[1];
}
}
$psrResponse = $responseFactory->createResponse(...$responseParameters);
foreach ($response->getHeaders(false) as $name => $values) {
foreach ($values as $value) {
try {
$psrResponse = $psrResponse->withAddedHeader($name, $value);
} catch (\InvalidArgumentException $e) {
// ignore invalid header
}
}
}
if ($response instanceof StreamableInterface) {
$body = $streamFactory->createStreamFromResource($response->toStream(false));
} elseif (!$buffer) {
$body = $streamFactory->createStreamFromResource(StreamWrapper::createResource($response, $client));
} else {
$body = $streamFactory->createStream($response->getContent(false));
}
if ($body->isSeekable()) {
$body->seek(0);
}
return $psrResponse->withBody($body);
}
}

View File

@@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal;
/**
* Internal representation of the native client's state.
*
* @author Alexander M. Turek <me@derrabus.de>
*
* @internal
*/
final class NativeClientState extends ClientState
{
/** @var int */
public $id;
/** @var int */
public $maxHostConnections = \PHP_INT_MAX;
/** @var int */
public $responseCount = 0;
/** @var string[] */
public $dnsCache = [];
/** @var bool */
public $sleep = false;
/** @var int[] */
public $hosts = [];
public function __construct()
{
$this->id = random_int(\PHP_INT_MIN, \PHP_INT_MAX);
}
public function reset()
{
$this->responseCount = 0;
$this->dnsCache = [];
$this->hosts = [];
}
}

View File

@@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\CurlResponse;
/**
* A pushed response with its request headers.
*
* @author Alexander M. Turek <me@derrabus.de>
*
* @internal
*/
final class PushedResponse
{
public $response;
/** @var string[] */
public $requestHeaders;
public $parentOptions = [];
public $handle;
public function __construct(CurlResponse $response, array $requestHeaders, array $parentOptions, $handle)
{
$this->response = $response;
$this->requestHeaders = $requestHeaders;
$this->parentOptions = $parentOptions;
$this->handle = $handle;
}
}

View File

@@ -0,0 +1,19 @@
Copyright (c) 2018-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,124 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\TransportException;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\MockResponse;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\ResponseStream;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\HttpClientInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseStreamInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\Service\ResetInterface;
/**
* A test-friendly HttpClient that doesn't make actual HTTP requests.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class MockHttpClient implements HttpClientInterface, ResetInterface
{
use HttpClientTrait;
private $responseFactory;
private $requestsCount = 0;
private $defaultOptions = [];
/**
* @param callable|callable[]|ResponseInterface|ResponseInterface[]|iterable|null $responseFactory
*/
public function __construct($responseFactory = null, ?string $baseUri = 'https://example.com')
{
$this->setResponseFactory($responseFactory);
$this->defaultOptions['base_uri'] = $baseUri;
}
/**
* @param callable|callable[]|ResponseInterface|ResponseInterface[]|iterable|null $responseFactory
*/
public function setResponseFactory($responseFactory): void
{
if ($responseFactory instanceof ResponseInterface) {
$responseFactory = [$responseFactory];
}
if (!$responseFactory instanceof \Iterator && null !== $responseFactory && !\is_callable($responseFactory)) {
$responseFactory = (static function () use ($responseFactory) {
yield from $responseFactory;
})();
}
$this->responseFactory = $responseFactory;
}
/**
* {@inheritdoc}
*/
public function request(string $method, string $url, array $options = []): ResponseInterface
{
[$url, $options] = $this->prepareRequest($method, $url, $options, $this->defaultOptions, true);
$url = implode('', $url);
if (null === $this->responseFactory) {
$response = new MockResponse();
} elseif (\is_callable($this->responseFactory)) {
$response = ($this->responseFactory)($method, $url, $options);
} elseif (!$this->responseFactory->valid()) {
throw new TransportException('The response factory iterator passed to MockHttpClient is empty.');
} else {
$responseFactory = $this->responseFactory->current();
$response = \is_callable($responseFactory) ? $responseFactory($method, $url, $options) : $responseFactory;
$this->responseFactory->next();
}
++$this->requestsCount;
if (!$response instanceof ResponseInterface) {
throw new TransportException(sprintf('The response factory passed to MockHttpClient must return/yield an instance of ResponseInterface, "%s" given.', \is_object($response) ? \get_class($response) : \gettype($response)));
}
return MockResponse::fromRequest($method, $url, $options, $response);
}
/**
* {@inheritdoc}
*/
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
{
if ($responses instanceof ResponseInterface) {
$responses = [$responses];
} elseif (!is_iterable($responses)) {
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of MockResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
}
return new ResponseStream(MockResponse::stream($responses, $timeout));
}
public function getRequestsCount(): int
{
return $this->requestsCount;
}
/**
* {@inheritdoc}
*/
public function withOptions(array $options): self
{
$clone = clone $this;
$clone->defaultOptions = self::mergeDefaultOptions($options, $this->defaultOptions, true);
return $clone;
}
public function reset()
{
$this->requestsCount = 0;
}
}

View File

@@ -0,0 +1,483 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient;
use KadenceWP\KadenceBlocks\Psr\Log\LoggerAwareInterface;
use KadenceWP\KadenceBlocks\Psr\Log\LoggerAwareTrait;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\InvalidArgumentException;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\TransportException;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal\NativeClientState;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\NativeResponse;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\ResponseStream;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\HttpClientInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseStreamInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\Service\ResetInterface;
/**
* A portable implementation of the HttpClientInterface contracts based on PHP stream wrappers.
*
* PHP stream wrappers are able to fetch response bodies concurrently,
* but each request is opened synchronously.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface
{
use HttpClientTrait;
use LoggerAwareTrait;
private $defaultOptions = self::OPTIONS_DEFAULTS;
private static $emptyDefaults = self::OPTIONS_DEFAULTS;
/** @var NativeClientState */
private $multi;
/**
* @param array $defaultOptions Default request's options
* @param int $maxHostConnections The maximum number of connections to open
*
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
*/
public function __construct(array $defaultOptions = [], int $maxHostConnections = 6)
{
$this->defaultOptions['buffer'] = $this->defaultOptions['buffer'] ?? \Closure::fromCallable([__CLASS__, 'shouldBuffer']);
if ($defaultOptions) {
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
}
$this->multi = new NativeClientState();
$this->multi->maxHostConnections = 0 < $maxHostConnections ? $maxHostConnections : \PHP_INT_MAX;
}
/**
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
*
* {@inheritdoc}
*/
public function request(string $method, string $url, array $options = []): ResponseInterface
{
[$url, $options] = self::prepareRequest($method, $url, $options, $this->defaultOptions);
if ($options['bindto']) {
if (file_exists($options['bindto'])) {
throw new TransportException(__CLASS__.' cannot bind to local Unix sockets, use e.g. CurlHttpClient instead.');
}
if (str_starts_with($options['bindto'], 'if!')) {
throw new TransportException(__CLASS__.' cannot bind to network interfaces, use e.g. CurlHttpClient instead.');
}
if (str_starts_with($options['bindto'], 'host!')) {
$options['bindto'] = substr($options['bindto'], 5);
}
if ((\PHP_VERSION_ID < 80223 || 80300 <= \PHP_VERSION_ID && 80311 < \PHP_VERSION_ID) && '\\' === \DIRECTORY_SEPARATOR && '[' === $options['bindto'][0]) {
$options['bindto'] = preg_replace('{^\[[^\]]++\]}', '[$0]', $options['bindto']);
}
}
$hasContentLength = isset($options['normalized_headers']['content-length']);
$hasBody = '' !== $options['body'] || 'POST' === $method || $hasContentLength;
$options['body'] = self::getBodyAsString($options['body']);
if ('chunked' === substr($options['normalized_headers']['transfer-encoding'][0] ?? '', \strlen('Transfer-Encoding: '))) {
unset($options['normalized_headers']['transfer-encoding']);
$options['headers'] = array_merge(...array_values($options['normalized_headers']));
$options['body'] = self::dechunk($options['body']);
}
if ('' === $options['body'] && $hasBody && !$hasContentLength) {
$options['headers'][] = 'Content-Length: 0';
}
if ($hasBody && !isset($options['normalized_headers']['content-type'])) {
$options['headers'][] = 'Content-Type: application/x-www-form-urlencoded';
}
if (\extension_loaded('zlib') && !isset($options['normalized_headers']['accept-encoding'])) {
// gzip is the most widely available algo, no need to deal with deflate
$options['headers'][] = 'Accept-Encoding: gzip';
}
if ($options['peer_fingerprint']) {
if (isset($options['peer_fingerprint']['pin-sha256']) && 1 === \count($options['peer_fingerprint'])) {
throw new TransportException(__CLASS__.' cannot verify "pin-sha256" fingerprints, please provide a "sha256" one.');
}
unset($options['peer_fingerprint']['pin-sha256']);
}
$info = [
'response_headers' => [],
'url' => $url,
'error' => null,
'canceled' => false,
'http_method' => $method,
'http_code' => 0,
'redirect_count' => 0,
'start_time' => 0.0,
'connect_time' => 0.0,
'redirect_time' => 0.0,
'pretransfer_time' => 0.0,
'starttransfer_time' => 0.0,
'total_time' => 0.0,
'namelookup_time' => 0.0,
'size_upload' => 0,
'size_download' => 0,
'size_body' => \strlen($options['body']),
'primary_ip' => '',
'primary_port' => 'http:' === $url['scheme'] ? 80 : 443,
'debug' => \extension_loaded('curl') ? '' : "* Enable the curl extension for better performance\n",
];
if ($onProgress = $options['on_progress']) {
// Memoize the last progress to ease calling the callback periodically when no network transfer happens
$lastProgress = [0, 0];
$maxDuration = 0 < $options['max_duration'] ? $options['max_duration'] : \INF;
$onProgress = static function (...$progress) use ($onProgress, &$lastProgress, &$info, $maxDuration) {
if ($info['total_time'] >= $maxDuration) {
throw new TransportException(sprintf('Max duration was reached for "%s".', implode('', $info['url'])));
}
$progressInfo = $info;
$progressInfo['url'] = implode('', $info['url']);
unset($progressInfo['size_body']);
if ($progress && -1 === $progress[0]) {
// Response completed
$lastProgress[0] = max($lastProgress);
} else {
$lastProgress = $progress ?: $lastProgress;
}
$onProgress($lastProgress[0], $lastProgress[1], $progressInfo);
};
} elseif (0 < $options['max_duration']) {
$maxDuration = $options['max_duration'];
$onProgress = static function () use (&$info, $maxDuration): void {
if ($info['total_time'] >= $maxDuration) {
throw new TransportException(sprintf('Max duration was reached for "%s".', implode('', $info['url'])));
}
};
}
// Always register a notification callback to compute live stats about the response
$notification = static function (int $code, int $severity, ?string $msg, int $msgCode, int $dlNow, int $dlSize) use ($onProgress, &$info) {
$info['total_time'] = microtime(true) - $info['start_time'];
if (\STREAM_NOTIFY_PROGRESS === $code) {
$info['starttransfer_time'] = $info['starttransfer_time'] ?: $info['total_time'];
$info['size_upload'] += $dlNow ? 0 : $info['size_body'];
$info['size_download'] = $dlNow;
} elseif (\STREAM_NOTIFY_CONNECT === $code) {
$info['connect_time'] = $info['total_time'];
$info['debug'] .= $info['request_header'];
unset($info['request_header']);
} else {
return;
}
if ($onProgress) {
$onProgress($dlNow, $dlSize);
}
};
if ($options['resolve']) {
$this->multi->dnsCache = $options['resolve'] + $this->multi->dnsCache;
}
$this->logger && $this->logger->info(sprintf('Request: "%s %s"', $method, implode('', $url)));
if (!isset($options['normalized_headers']['user-agent'])) {
$options['headers'][] = 'User-Agent: Symfony HttpClient/Native';
}
if (0 < $options['max_duration']) {
$options['timeout'] = min($options['max_duration'], $options['timeout']);
}
$bindto = $options['bindto'];
if (!$bindto && (70322 === \PHP_VERSION_ID || 70410 === \PHP_VERSION_ID)) {
$bindto = '0:0';
}
$context = [
'http' => [
'protocol_version' => min($options['http_version'] ?: '1.1', '1.1'),
'method' => $method,
'content' => $options['body'],
'ignore_errors' => true,
'curl_verify_ssl_peer' => $options['verify_peer'],
'curl_verify_ssl_host' => $options['verify_host'],
'auto_decode' => false, // Disable dechunk filter, it's incompatible with stream_select()
'timeout' => $options['timeout'],
'follow_location' => false, // We follow redirects ourselves - the native logic is too limited
],
'ssl' => array_filter([
'verify_peer' => $options['verify_peer'],
'verify_peer_name' => $options['verify_host'],
'cafile' => $options['cafile'],
'capath' => $options['capath'],
'local_cert' => $options['local_cert'],
'local_pk' => $options['local_pk'],
'passphrase' => $options['passphrase'],
'ciphers' => $options['ciphers'],
'peer_fingerprint' => $options['peer_fingerprint'],
'capture_peer_cert_chain' => $options['capture_peer_cert_chain'],
'allow_self_signed' => (bool) $options['peer_fingerprint'],
'SNI_enabled' => true,
'disable_compression' => true,
], static function ($v) { return null !== $v; }),
'socket' => [
'bindto' => $bindto,
'tcp_nodelay' => true,
],
];
$context = stream_context_create($context, ['notification' => $notification]);
$resolver = static function ($multi) use ($context, $options, $url, &$info, $onProgress) {
[$host, $port] = self::parseHostPort($url, $info);
if (!isset($options['normalized_headers']['host'])) {
$options['headers'][] = 'Host: '.$host.$port;
}
$proxy = self::getProxy($options['proxy'], $url, $options['no_proxy']);
if (!self::configureHeadersAndProxy($context, $host, $options['headers'], $proxy, 'https:' === $url['scheme'])) {
$ip = self::dnsResolve($host, $multi, $info, $onProgress);
$url['authority'] = substr_replace($url['authority'], $ip, -\strlen($host) - \strlen($port), \strlen($host));
}
return [self::createRedirectResolver($options, $host, $proxy, $info, $onProgress), implode('', $url)];
};
return new NativeResponse($this->multi, $context, implode('', $url), $options, $info, $resolver, $onProgress, $this->logger);
}
/**
* {@inheritdoc}
*/
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
{
if ($responses instanceof NativeResponse) {
$responses = [$responses];
} elseif (!is_iterable($responses)) {
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of NativeResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
}
return new ResponseStream(NativeResponse::stream($responses, $timeout));
}
public function reset()
{
$this->multi->reset();
}
private static function getBodyAsString($body): string
{
if (\is_resource($body)) {
return stream_get_contents($body);
}
if (!$body instanceof \Closure) {
return $body;
}
$result = '';
while ('' !== $data = $body(self::$CHUNK_SIZE)) {
if (!\is_string($data)) {
throw new TransportException(sprintf('Return value of the "body" option callback must be string, "%s" returned.', get_debug_type($data)));
}
$result .= $data;
}
return $result;
}
/**
* Extracts the host and the port from the URL.
*/
private static function parseHostPort(array $url, array &$info): array
{
if ($port = parse_url($url['authority'], \PHP_URL_PORT) ?: '') {
$info['primary_port'] = $port;
$port = ':'.$port;
} else {
$info['primary_port'] = 'http:' === $url['scheme'] ? 80 : 443;
}
return [parse_url($url['authority'], \PHP_URL_HOST), $port];
}
/**
* Resolves the IP of the host using the local DNS cache if possible.
*/
private static function dnsResolve($host, NativeClientState $multi, array &$info, ?\Closure $onProgress): string
{
$flag = '' !== $host && '[' === $host[0] && ']' === $host[-1] && str_contains($host, ':') ? \FILTER_FLAG_IPV6 : \FILTER_FLAG_IPV4;
$ip = \FILTER_FLAG_IPV6 === $flag ? substr($host, 1, -1) : $host;
if (filter_var($ip, \FILTER_VALIDATE_IP, $flag)) {
// The host is already an IP address
} elseif (null === $ip = $multi->dnsCache[$host] ?? null) {
$info['debug'] .= "* Hostname was NOT found in DNS cache\n";
$now = microtime(true);
if (!$ip = gethostbynamel($host)) {
throw new TransportException(sprintf('Could not resolve host "%s".', $host));
}
$multi->dnsCache[$host] = $ip = $ip[0];
$info['debug'] .= "* Added {$host}:0:{$ip} to DNS cache\n";
$host = $ip;
} else {
$info['debug'] .= "* Hostname was found in DNS cache\n";
$host = str_contains($ip, ':') ? "[$ip]" : $ip;
}
$info['namelookup_time'] = microtime(true) - ($info['start_time'] ?: $now);
$info['primary_ip'] = $ip;
if ($onProgress) {
// Notify DNS resolution
$onProgress();
}
return $host;
}
/**
* Handles redirects - the native logic is too buggy to be used.
*/
private static function createRedirectResolver(array $options, string $host, ?array $proxy, array &$info, ?\Closure $onProgress): \Closure
{
$redirectHeaders = [];
if (0 < $maxRedirects = $options['max_redirects']) {
$redirectHeaders = ['host' => $host];
$redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
return 0 !== stripos($h, 'Host:');
});
if (isset($options['normalized_headers']['authorization']) || isset($options['normalized_headers']['cookie'])) {
$redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], static function ($h) {
return 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:');
});
}
}
return static function (NativeClientState $multi, ?string $location, $context) use (&$redirectHeaders, $proxy, &$info, $maxRedirects, $onProgress): ?string {
if (null === $location || $info['http_code'] < 300 || 400 <= $info['http_code']) {
$info['redirect_url'] = null;
return null;
}
try {
$url = self::parseUrl($location);
$locationHasHost = isset($url['authority']);
$url = self::resolveUrl($url, $info['url']);
} catch (InvalidArgumentException $e) {
$info['redirect_url'] = null;
return null;
}
$info['redirect_url'] = implode('', $url);
if ($info['redirect_count'] >= $maxRedirects) {
return null;
}
$info['url'] = $url;
++$info['redirect_count'];
$info['redirect_time'] = microtime(true) - $info['start_time'];
// Do like curl and browsers: turn POST to GET on 301, 302 and 303
if (\in_array($info['http_code'], [301, 302, 303], true)) {
$options = stream_context_get_options($context)['http'];
if ('POST' === $options['method'] || 303 === $info['http_code']) {
$info['http_method'] = $options['method'] = 'HEAD' === $options['method'] ? 'HEAD' : 'GET';
$options['content'] = '';
$filterContentHeaders = static function ($h) {
return 0 !== stripos($h, 'Content-Length:') && 0 !== stripos($h, 'Content-Type:') && 0 !== stripos($h, 'Transfer-Encoding:');
};
$options['header'] = array_filter($options['header'], $filterContentHeaders);
$redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], $filterContentHeaders);
$redirectHeaders['with_auth'] = array_filter($redirectHeaders['with_auth'], $filterContentHeaders);
if (\PHP_VERSION_ID >= 80300) {
stream_context_set_options($context, ['http' => $options]);
} else {
stream_context_set_option($context, ['http' => $options]);
}
}
}
[$host, $port] = self::parseHostPort($url, $info);
if ($locationHasHost) {
// Authorization and Cookie headers MUST NOT follow except for the initial host name
$requestHeaders = $redirectHeaders['host'] === $host ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
$requestHeaders[] = 'Host: '.$host.$port;
$dnsResolve = !self::configureHeadersAndProxy($context, $host, $requestHeaders, $proxy, 'https:' === $url['scheme']);
} else {
$dnsResolve = isset(stream_context_get_options($context)['ssl']['peer_name']);
}
if ($dnsResolve) {
$ip = self::dnsResolve($host, $multi, $info, $onProgress);
$url['authority'] = substr_replace($url['authority'], $ip, -\strlen($host) - \strlen($port), \strlen($host));
}
return implode('', $url);
};
}
private static function configureHeadersAndProxy($context, string $host, array $requestHeaders, ?array $proxy, bool $isSsl): bool
{
if (null === $proxy) {
stream_context_set_option($context, 'http', 'header', $requestHeaders);
stream_context_set_option($context, 'ssl', 'peer_name', $host);
return false;
}
// Matching "no_proxy" should follow the behavior of curl
foreach ($proxy['no_proxy'] as $rule) {
$dotRule = '.'.ltrim($rule, '.');
if ('*' === $rule || $host === $rule || str_ends_with($host, $dotRule)) {
stream_context_set_option($context, 'http', 'proxy', null);
stream_context_set_option($context, 'http', 'request_fulluri', false);
stream_context_set_option($context, 'http', 'header', $requestHeaders);
stream_context_set_option($context, 'ssl', 'peer_name', $host);
return false;
}
}
if (null !== $proxy['auth']) {
$requestHeaders[] = 'Proxy-Authorization: '.$proxy['auth'];
}
stream_context_set_option($context, 'http', 'proxy', $proxy['url']);
stream_context_set_option($context, 'http', 'request_fulluri', !$isSsl);
stream_context_set_option($context, 'http', 'header', $requestHeaders);
stream_context_set_option($context, 'ssl', 'peer_name', null);
return true;
}
}

View File

@@ -0,0 +1,268 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient;
use KadenceWP\KadenceBlocks\Psr\Log\LoggerAwareInterface;
use KadenceWP\KadenceBlocks\Psr\Log\LoggerInterface;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\TransportException;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\AsyncContext;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\AsyncResponse;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpFoundation\IpUtils;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ChunkInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\HttpClientInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\Service\ResetInterface;
/**
* Decorator that blocks requests to private networks by default.
*
* @author Hallison Boaventura <hallisonboaventura@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
final class NoPrivateNetworkHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface
{
use HttpClientTrait;
use AsyncDecoratorTrait;
private const PRIVATE_SUBNETS = [
'127.0.0.0/8',
'10.0.0.0/8',
'192.168.0.0/16',
'172.16.0.0/12',
'169.254.0.0/16',
'0.0.0.0/8',
'240.0.0.0/4',
'::1/128',
'fc00::/7',
'fe80::/10',
'::ffff:0:0/96',
'::/128',
];
private $defaultOptions = self::OPTIONS_DEFAULTS;
private $client;
private $subnets;
private $ipFlags;
private $dnsCache;
/**
* @param string|array|null $subnets String or array of subnets using CIDR notation that should be considered private.
* If null is passed, the standard private subnets will be used.
*/
public function __construct(HttpClientInterface $client, $subnets = null)
{
if (!(\is_array($subnets) || \is_string($subnets) || null === $subnets)) {
throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be of the type array, string or null. "%s" given.', __METHOD__, get_debug_type($subnets)));
}
if (!class_exists(IpUtils::class)) {
throw new \LogicException(sprintf('You cannot use "%s" if the HttpFoundation component is not installed. Try running "composer require symfony/http-foundation".', __CLASS__));
}
if (null === $subnets) {
$ipFlags = \FILTER_FLAG_IPV4 | \FILTER_FLAG_IPV6;
} else {
$ipFlags = 0;
foreach ((array) $subnets as $subnet) {
$ipFlags |= str_contains($subnet, ':') ? \FILTER_FLAG_IPV6 : \FILTER_FLAG_IPV4;
}
}
if (!\defined('STREAM_PF_INET6')) {
$ipFlags &= ~\FILTER_FLAG_IPV6;
}
$this->client = $client;
$this->subnets = null !== $subnets ? (array) $subnets : null;
$this->ipFlags = $ipFlags;
$this->dnsCache = new \ArrayObject();
}
/**
* {@inheritdoc}
*/
public function request(string $method, string $url, array $options = []): ResponseInterface
{
[$url, $options] = self::prepareRequest($method, $url, $options, $this->defaultOptions, true);
$redirectHeaders = parse_url($url['authority']);
$host = $redirectHeaders['host'];
$url = implode('', $url);
$dnsCache = $this->dnsCache;
$ip = self::dnsResolve($dnsCache, $host, $this->ipFlags, $options);
self::ipCheck($ip, $this->subnets, $this->ipFlags, $host, $url);
$onProgress = $options['on_progress'] ?? null;
$subnets = $this->subnets;
$ipFlags = $this->ipFlags;
$lastPrimaryIp = '';
$options['on_progress'] = static function (int $dlNow, int $dlSize, array $info) use ($onProgress, $subnets, $ipFlags, &$lastPrimaryIp): void {
if (!\in_array($info['primary_ip'] ?? '', ['', $lastPrimaryIp], true)) {
self::ipCheck($info['primary_ip'], $subnets, $ipFlags, null, $info['url']);
$lastPrimaryIp = $info['primary_ip'];
}
null !== $onProgress && $onProgress($dlNow, $dlSize, $info);
};
if (0 >= $maxRedirects = $options['max_redirects']) {
return new AsyncResponse($this->client, $method, $url, $options);
}
$options['max_redirects'] = 0;
$redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = $options['headers'];
if (isset($options['normalized_headers']['host']) || isset($options['normalized_headers']['authorization']) || isset($options['normalized_headers']['cookie'])) {
$redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], static function ($h) {
return 0 !== stripos($h, 'Host:') && 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:');
});
}
return new AsyncResponse($this->client, $method, $url, $options, static function (ChunkInterface $chunk, AsyncContext $context) use (&$method, &$options, $maxRedirects, &$redirectHeaders, $subnets, $ipFlags, $dnsCache): \Generator {
if (null !== $chunk->getError() || $chunk->isTimeout() || !$chunk->isFirst()) {
yield $chunk;
return;
}
$statusCode = $context->getStatusCode();
if ($statusCode < 300 || 400 <= $statusCode || null === $url = $context->getInfo('redirect_url')) {
$context->passthru();
yield $chunk;
return;
}
$host = parse_url($url, \PHP_URL_HOST);
$ip = self::dnsResolve($dnsCache, $host, $ipFlags, $options);
self::ipCheck($ip, $subnets, $ipFlags, $host, $url);
// Do like curl and browsers: turn POST to GET on 301, 302 and 303
if (303 === $statusCode || 'POST' === $method && \in_array($statusCode, [301, 302], true)) {
$method = 'HEAD' === $method ? 'HEAD' : 'GET';
unset($options['body'], $options['json']);
if (isset($options['normalized_headers']['content-length']) || isset($options['normalized_headers']['content-type']) || isset($options['normalized_headers']['transfer-encoding'])) {
$filterContentHeaders = static function ($h) {
return 0 !== stripos($h, 'Content-Length:') && 0 !== stripos($h, 'Content-Type:') && 0 !== stripos($h, 'Transfer-Encoding:');
};
$options['header'] = array_filter($options['header'], $filterContentHeaders);
$redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], $filterContentHeaders);
$redirectHeaders['with_auth'] = array_filter($redirectHeaders['with_auth'], $filterContentHeaders);
}
}
// Authorization and Cookie headers MUST NOT follow except for the initial host name
$options['headers'] = $redirectHeaders['host'] === $host ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
static $redirectCount = 0;
$context->setInfo('redirect_count', ++$redirectCount);
$context->replaceRequest($method, $url, $options);
if ($redirectCount >= $maxRedirects) {
$context->passthru();
}
});
}
/**
* {@inheritdoc}
*/
public function setLogger(LoggerInterface $logger): void
{
if ($this->client instanceof LoggerAwareInterface) {
$this->client->setLogger($logger);
}
}
/**
* {@inheritdoc}
*/
public function withOptions(array $options): self
{
$clone = clone $this;
$clone->client = $this->client->withOptions($options);
$clone->defaultOptions = self::mergeDefaultOptions($options, $this->defaultOptions);
return $clone;
}
public function reset()
{
$this->dnsCache->exchangeArray([]);
if ($this->client instanceof ResetInterface) {
$this->client->reset();
}
}
private static function dnsResolve(\ArrayObject $dnsCache, string $host, int $ipFlags, array &$options): string
{
if ($ip = filter_var(trim($host, '[]'), \FILTER_VALIDATE_IP) ?: $options['resolve'][$host] ?? false) {
return $ip;
}
if ($dnsCache->offsetExists($host)) {
return $dnsCache[$host];
}
if ((\FILTER_FLAG_IPV4 & $ipFlags) && $ip = gethostbynamel($host)) {
return $options['resolve'][$host] = $dnsCache[$host] = $ip[0];
}
if (!(\FILTER_FLAG_IPV6 & $ipFlags)) {
return $host;
}
if ($ip = dns_get_record($host, \DNS_AAAA)) {
$ip = $ip[0]['ipv6'];
} elseif (extension_loaded('sockets')) {
if (!$info = socket_addrinfo_lookup($host, 0, ['ai_socktype' => \SOCK_STREAM, 'ai_family' => \AF_INET6])) {
return $host;
}
$ip = socket_addrinfo_explain($info[0])['ai_addr']['sin6_addr'];
} elseif ('localhost' === $host || 'localhost.' === $host) {
$ip = '::1';
} else {
return $host;
}
return $options['resolve'][$host] = $dnsCache[$host] = $ip;
}
private static function ipCheck(string $ip, ?array $subnets, int $ipFlags, ?string $host, string $url): void
{
if (null === $subnets) {
// Quick check, but not reliable enough, see https://github.com/php/php-src/issues/16944
$ipFlags |= \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE;
}
if (false !== filter_var($ip, \FILTER_VALIDATE_IP, $ipFlags) && !IpUtils::checkIp($ip, $subnets ?? self::PRIVATE_SUBNETS)) {
return;
}
if (null !== $host) {
$type = 'Host';
} else {
$host = $ip;
$type = 'IP';
}
throw new TransportException($type.\sprintf(' "%s" is blocked for "%s".', $host, $url));
}
}

View File

@@ -0,0 +1,231 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient;
use Http\Discovery\Exception\NotFoundException;
use Http\Discovery\Psr17FactoryDiscovery;
use KadenceWP\KadenceBlocks\Nyholm\Psr7\Factory\Psr17Factory;
use KadenceWP\KadenceBlocks\Nyholm\Psr7\Request;
use KadenceWP\KadenceBlocks\Nyholm\Psr7\Uri;
use KadenceWP\KadenceBlocks\Psr\Http\Client\ClientInterface;
use KadenceWP\KadenceBlocks\Psr\Http\Client\NetworkExceptionInterface;
use KadenceWP\KadenceBlocks\Psr\Http\Client\RequestExceptionInterface;
use KadenceWP\KadenceBlocks\Psr\Http\Message\RequestFactoryInterface;
use KadenceWP\KadenceBlocks\Psr\Http\Message\RequestInterface;
use KadenceWP\KadenceBlocks\Psr\Http\Message\ResponseFactoryInterface;
use KadenceWP\KadenceBlocks\Psr\Http\Message\ResponseInterface;
use KadenceWP\KadenceBlocks\Psr\Http\Message\StreamFactoryInterface;
use KadenceWP\KadenceBlocks\Psr\Http\Message\StreamInterface;
use KadenceWP\KadenceBlocks\Psr\Http\Message\UriFactoryInterface;
use KadenceWP\KadenceBlocks\Psr\Http\Message\UriInterface;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal\HttplugWaitLoop;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\StreamableInterface;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\StreamWrapper;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\HttpClientInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface as HttpClientResponseInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\Service\ResetInterface;
if (!interface_exists(RequestFactoryInterface::class)) {
throw new \LogicException('You cannot use the "KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Psr18Client" as the "psr/http-factory" package is not installed. Try running "composer require nyholm/psr7".');
}
if (!interface_exists(ClientInterface::class)) {
throw new \LogicException('You cannot use the "KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Psr18Client" as the "psr/http-client" package is not installed. Try running "composer require psr/http-client".');
}
/**
* An adapter to turn a Symfony HttpClientInterface into a PSR-18 ClientInterface.
*
* Run "composer require psr/http-client" to install the base ClientInterface. Run
* "composer require nyholm/psr7" to install an efficient implementation of response
* and stream factories with flex-provided autowiring aliases.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class Psr18Client implements ClientInterface, RequestFactoryInterface, StreamFactoryInterface, UriFactoryInterface, ResetInterface
{
private $client;
private $responseFactory;
private $streamFactory;
public function __construct(?HttpClientInterface $client = null, ?ResponseFactoryInterface $responseFactory = null, ?StreamFactoryInterface $streamFactory = null)
{
$this->client = $client ?? HttpClient::create();
$this->responseFactory = $responseFactory;
$this->streamFactory = $streamFactory ?? ($responseFactory instanceof StreamFactoryInterface ? $responseFactory : null);
if (null !== $this->responseFactory && null !== $this->streamFactory) {
return;
}
if (!class_exists(Psr17Factory::class) && !class_exists(Psr17FactoryDiscovery::class)) {
throw new \LogicException('You cannot use the "KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Psr18Client" as no PSR-17 factories have been provided. Try running "composer require nyholm/psr7".');
}
try {
$psr17Factory = class_exists(Psr17Factory::class, false) ? new Psr17Factory() : null;
$this->responseFactory = $this->responseFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findResponseFactory();
$this->streamFactory = $this->streamFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findStreamFactory();
} catch (NotFoundException $e) {
throw new \LogicException('You cannot use the "KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\HttplugClient" as no PSR-17 factories have been found. Try running "composer require nyholm/psr7".', 0, $e);
}
}
/**
* {@inheritdoc}
*/
public function sendRequest(RequestInterface $request): ResponseInterface
{
try {
$body = $request->getBody();
if ($body->isSeekable()) {
$body->seek(0);
}
$options = [
'headers' => $request->getHeaders(),
'body' => $body->getContents(),
];
if ('1.0' === $request->getProtocolVersion()) {
$options['http_version'] = '1.0';
}
$response = $this->client->request($request->getMethod(), (string) $request->getUri(), $options);
return HttplugWaitLoop::createPsr7Response($this->responseFactory, $this->streamFactory, $this->client, $response, false);
} catch (TransportExceptionInterface $e) {
if ($e instanceof \InvalidArgumentException) {
throw new Psr18RequestException($e, $request);
}
throw new Psr18NetworkException($e, $request);
}
}
/**
* {@inheritdoc}
*/
public function createRequest(string $method, $uri): RequestInterface
{
if ($this->responseFactory instanceof RequestFactoryInterface) {
return $this->responseFactory->createRequest($method, $uri);
}
if (class_exists(Request::class)) {
return new Request($method, $uri);
}
if (class_exists(Psr17FactoryDiscovery::class)) {
return Psr17FactoryDiscovery::findRequestFactory()->createRequest($method, $uri);
}
throw new \LogicException(sprintf('You cannot use "%s()" as the "nyholm/psr7" package is not installed. Try running "composer require nyholm/psr7".', __METHOD__));
}
/**
* {@inheritdoc}
*/
public function createStream(string $content = ''): StreamInterface
{
$stream = $this->streamFactory->createStream($content);
if ($stream->isSeekable()) {
$stream->seek(0);
}
return $stream;
}
/**
* {@inheritdoc}
*/
public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface
{
return $this->streamFactory->createStreamFromFile($filename, $mode);
}
/**
* {@inheritdoc}
*/
public function createStreamFromResource($resource): StreamInterface
{
return $this->streamFactory->createStreamFromResource($resource);
}
/**
* {@inheritdoc}
*/
public function createUri(string $uri = ''): UriInterface
{
if ($this->responseFactory instanceof UriFactoryInterface) {
return $this->responseFactory->createUri($uri);
}
if (class_exists(Uri::class)) {
return new Uri($uri);
}
if (class_exists(Psr17FactoryDiscovery::class)) {
return Psr17FactoryDiscovery::findUrlFactory()->createUri($uri);
}
throw new \LogicException(sprintf('You cannot use "%s()" as the "nyholm/psr7" package is not installed. Try running "composer require nyholm/psr7".', __METHOD__));
}
public function reset()
{
if ($this->client instanceof ResetInterface) {
$this->client->reset();
}
}
}
/**
* @internal
*/
class Psr18NetworkException extends \RuntimeException implements NetworkExceptionInterface
{
private $request;
public function __construct(TransportExceptionInterface $e, RequestInterface $request)
{
parent::__construct($e->getMessage(), 0, $e);
$this->request = $request;
}
public function getRequest(): RequestInterface
{
return $this->request;
}
}
/**
* @internal
*/
class Psr18RequestException extends \InvalidArgumentException implements RequestExceptionInterface
{
private $request;
public function __construct(TransportExceptionInterface $e, RequestInterface $request)
{
parent::__construct($e->getMessage(), 0, $e);
$this->request = $request;
}
public function getRequest(): RequestInterface
{
return $this->request;
}
}

View File

@@ -0,0 +1,27 @@
HttpClient component
====================
The HttpClient component provides powerful methods to fetch HTTP resources synchronously or asynchronously.
Sponsor
-------
The Httpclient component for Symfony 5.4/6.0 is [backed][1] by [Klaxoon][2].
Klaxoon is a platform that empowers organizations to run effective and
productive workshops easily in a hybrid environment. Anytime, Anywhere.
Help Symfony by [sponsoring][3] its development!
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/http_client.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)
[1]: https://symfony.com/backers
[2]: https://klaxoon.com
[3]: https://symfony.com/sponsor

View File

@@ -0,0 +1,460 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response;
use Amp\ByteStream\StreamException;
use Amp\CancellationTokenSource;
use Amp\Coroutine;
use Amp\Deferred;
use Amp\Http\Client\HttpException;
use Amp\Http\Client\Request;
use Amp\Http\Client\Response;
use Amp\Loop;
use Amp\Promise;
use Amp\Success;
use KadenceWP\KadenceBlocks\Psr\Log\LoggerInterface;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Chunk\FirstChunk;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Chunk\InformationalChunk;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\InvalidArgumentException;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\TransportException;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\HttpClientTrait;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal\AmpBody;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal\AmpClientState;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal\Canary;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal\ClientState;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
final class AmpResponse implements ResponseInterface, StreamableInterface
{
use CommonResponseTrait;
use TransportResponseTrait;
private static $nextId = 'a';
private $multi;
private $options;
private $onProgress;
private static $delay;
/**
* @internal
*/
public function __construct(AmpClientState $multi, Request $request, array $options, ?LoggerInterface $logger)
{
$this->multi = $multi;
$this->options = &$options;
$this->logger = $logger;
$this->timeout = $options['timeout'];
$this->shouldBuffer = $options['buffer'];
if ($this->inflate = \extension_loaded('zlib') && !$request->hasHeader('accept-encoding')) {
$request->setHeader('Accept-Encoding', 'gzip');
}
$this->initializer = static function (self $response) {
return null !== $response->options;
};
$info = &$this->info;
$headers = &$this->headers;
$canceller = new CancellationTokenSource();
$handle = &$this->handle;
$info['url'] = (string) $request->getUri();
$info['http_method'] = $request->getMethod();
$info['start_time'] = null;
$info['redirect_url'] = null;
$info['redirect_time'] = 0.0;
$info['redirect_count'] = 0;
$info['size_upload'] = 0.0;
$info['size_download'] = 0.0;
$info['upload_content_length'] = -1.0;
$info['download_content_length'] = -1.0;
$info['user_data'] = $options['user_data'];
$info['max_duration'] = $options['max_duration'];
$info['debug'] = '';
$onProgress = $options['on_progress'] ?? static function () {};
$onProgress = $this->onProgress = static function () use (&$info, $onProgress) {
$info['total_time'] = microtime(true) - $info['start_time'];
$onProgress((int) $info['size_download'], ((int) (1 + $info['download_content_length']) ?: 1) - 1, (array) $info);
};
$pauseDeferred = new Deferred();
$pause = new Success();
$throttleWatcher = null;
$this->id = $id = self::$nextId++;
Loop::defer(static function () use ($request, $multi, &$id, &$info, &$headers, $canceller, &$options, $onProgress, &$handle, $logger, &$pause) {
return new Coroutine(self::generateResponse($request, $multi, $id, $info, $headers, $canceller, $options, $onProgress, $handle, $logger, $pause));
});
$info['pause_handler'] = static function (float $duration) use (&$throttleWatcher, &$pauseDeferred, &$pause) {
if (null !== $throttleWatcher) {
Loop::cancel($throttleWatcher);
}
$pause = $pauseDeferred->promise();
if ($duration <= 0) {
$deferred = $pauseDeferred;
$pauseDeferred = new Deferred();
$deferred->resolve();
} else {
$throttleWatcher = Loop::delay(ceil(1000 * $duration), static function () use (&$pauseDeferred) {
$deferred = $pauseDeferred;
$pauseDeferred = new Deferred();
$deferred->resolve();
});
}
};
$multi->lastTimeout = null;
$multi->openHandles[$id] = $id;
++$multi->responseCount;
$this->canary = new Canary(static function () use ($canceller, $multi, $id) {
$canceller->cancel();
unset($multi->openHandles[$id], $multi->handlesActivity[$id]);
});
}
/**
* {@inheritdoc}
*/
public function getInfo(?string $type = null)
{
return null !== $type ? $this->info[$type] ?? null : $this->info;
}
public function __sleep(): array
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
try {
$this->doDestruct();
} finally {
// Clear the DNS cache when all requests completed
if (0 >= --$this->multi->responseCount) {
$this->multi->responseCount = 0;
$this->multi->dnsCache = [];
}
}
}
/**
* {@inheritdoc}
*/
private static function schedule(self $response, array &$runningResponses): void
{
if (isset($runningResponses[0])) {
$runningResponses[0][1][$response->id] = $response;
} else {
$runningResponses[0] = [$response->multi, [$response->id => $response]];
}
if (!isset($response->multi->openHandles[$response->id])) {
$response->multi->handlesActivity[$response->id][] = null;
$response->multi->handlesActivity[$response->id][] = null !== $response->info['error'] ? new TransportException($response->info['error']) : null;
}
}
/**
* {@inheritdoc}
*
* @param AmpClientState $multi
*/
private static function perform(ClientState $multi, ?array &$responses = null): void
{
if ($responses) {
foreach ($responses as $response) {
try {
if ($response->info['start_time']) {
$response->info['total_time'] = microtime(true) - $response->info['start_time'];
($response->onProgress)();
}
} catch (\Throwable $e) {
$multi->handlesActivity[$response->id][] = null;
$multi->handlesActivity[$response->id][] = $e;
}
}
}
}
/**
* {@inheritdoc}
*
* @param AmpClientState $multi
*/
private static function select(ClientState $multi, float $timeout): int
{
$timeout += microtime(true);
self::$delay = Loop::defer(static function () use ($timeout) {
if (0 < $timeout -= microtime(true)) {
self::$delay = Loop::delay(ceil(1000 * $timeout), [Loop::class, 'stop']);
} else {
Loop::stop();
}
});
Loop::run();
return null === self::$delay ? 1 : 0;
}
private static function generateResponse(Request $request, AmpClientState $multi, string $id, array &$info, array &$headers, CancellationTokenSource $canceller, array &$options, \Closure $onProgress, &$handle, ?LoggerInterface $logger, Promise &$pause)
{
$request->setInformationalResponseHandler(static function (Response $response) use ($multi, $id, &$info, &$headers) {
self::addResponseHeaders($response, $info, $headers);
$multi->handlesActivity[$id][] = new InformationalChunk($response->getStatus(), $response->getHeaders());
self::stopLoop();
});
try {
/* @var Response $response */
if (null === $response = yield from self::getPushedResponse($request, $multi, $info, $headers, $options, $logger)) {
$logger && $logger->info(sprintf('Request: "%s %s"', $info['http_method'], $info['url']));
$response = yield from self::followRedirects($request, $multi, $info, $headers, $canceller, $options, $onProgress, $handle, $logger, $pause);
}
$options = null;
$multi->handlesActivity[$id][] = new FirstChunk();
if ('HEAD' === $response->getRequest()->getMethod() || \in_array($info['http_code'], [204, 304], true)) {
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = null;
self::stopLoop();
return;
}
if ($response->hasHeader('content-length')) {
$info['download_content_length'] = (float) $response->getHeader('content-length');
}
$body = $response->getBody();
while (true) {
self::stopLoop();
yield $pause;
if (null === $data = yield $body->read()) {
break;
}
$info['size_download'] += \strlen($data);
$multi->handlesActivity[$id][] = $data;
}
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = null;
} catch (\Throwable $e) {
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = $e;
} finally {
$info['download_content_length'] = $info['size_download'];
}
self::stopLoop();
}
private static function followRedirects(Request $originRequest, AmpClientState $multi, array &$info, array &$headers, CancellationTokenSource $canceller, array $options, \Closure $onProgress, &$handle, ?LoggerInterface $logger, Promise &$pause)
{
yield $pause;
$originRequest->setBody(new AmpBody($options['body'], $info, $onProgress));
$response = yield $multi->request($options, $originRequest, $canceller->getToken(), $info, $onProgress, $handle);
$previousUrl = null;
while (true) {
self::addResponseHeaders($response, $info, $headers);
$status = $response->getStatus();
if (!\in_array($status, [301, 302, 303, 307, 308], true) || null === $location = $response->getHeader('location')) {
return $response;
}
$urlResolver = new class() {
use HttpClientTrait {
parseUrl as public;
resolveUrl as public;
}
};
try {
$previousUrl = $previousUrl ?? $urlResolver::parseUrl($info['url']);
$location = $urlResolver::parseUrl($location);
$location = $urlResolver::resolveUrl($location, $previousUrl);
$info['redirect_url'] = implode('', $location);
} catch (InvalidArgumentException $e) {
return $response;
}
if (0 >= $options['max_redirects'] || $info['redirect_count'] >= $options['max_redirects']) {
return $response;
}
$logger && $logger->info(sprintf('Redirecting: "%s %s"', $status, $info['url']));
try {
// Discard body of redirects
while (null !== yield $response->getBody()->read()) {
}
} catch (HttpException|StreamException $e) {
// Ignore streaming errors on previous responses
}
++$info['redirect_count'];
$info['url'] = $info['redirect_url'];
$info['redirect_url'] = null;
$previousUrl = $location;
$request = new Request($info['url'], $info['http_method']);
$request->setProtocolVersions($originRequest->getProtocolVersions());
$request->setTcpConnectTimeout($originRequest->getTcpConnectTimeout());
$request->setTlsHandshakeTimeout($originRequest->getTlsHandshakeTimeout());
$request->setTransferTimeout($originRequest->getTransferTimeout());
if (\in_array($status, [301, 302, 303], true)) {
$originRequest->removeHeader('transfer-encoding');
$originRequest->removeHeader('content-length');
$originRequest->removeHeader('content-type');
// Do like curl and browsers: turn POST to GET on 301, 302 and 303
if ('POST' === $response->getRequest()->getMethod() || 303 === $status) {
$info['http_method'] = 'HEAD' === $response->getRequest()->getMethod() ? 'HEAD' : 'GET';
$request->setMethod($info['http_method']);
}
} else {
$request->setBody(AmpBody::rewind($response->getRequest()->getBody()));
}
foreach ($originRequest->getRawHeaders() as [$name, $value]) {
$request->addHeader($name, $value);
}
if ($request->getUri()->getAuthority() !== $originRequest->getUri()->getAuthority()) {
$request->removeHeader('authorization');
$request->removeHeader('cookie');
$request->removeHeader('host');
}
yield $pause;
$response = yield $multi->request($options, $request, $canceller->getToken(), $info, $onProgress, $handle);
$info['redirect_time'] = microtime(true) - $info['start_time'];
}
}
private static function addResponseHeaders(Response $response, array &$info, array &$headers): void
{
$info['http_code'] = $response->getStatus();
if ($headers) {
$info['debug'] .= "< \r\n";
$headers = [];
}
$h = sprintf('HTTP/%s %s %s', $response->getProtocolVersion(), $response->getStatus(), $response->getReason());
$info['debug'] .= "< {$h}\r\n";
$info['response_headers'][] = $h;
foreach ($response->getRawHeaders() as [$name, $value]) {
$headers[strtolower($name)][] = $value;
$h = $name.': '.$value;
$info['debug'] .= "< {$h}\r\n";
$info['response_headers'][] = $h;
}
$info['debug'] .= "< \r\n";
}
/**
* Accepts pushed responses only if their headers related to authentication match the request.
*/
private static function getPushedResponse(Request $request, AmpClientState $multi, array &$info, array &$headers, array $options, ?LoggerInterface $logger)
{
if ('' !== $options['body']) {
return null;
}
$authority = $request->getUri()->getAuthority();
foreach ($multi->pushedResponses[$authority] ?? [] as $i => [$pushedUrl, $pushDeferred, $pushedRequest, $pushedResponse, $parentOptions]) {
if ($info['url'] !== $pushedUrl || $info['http_method'] !== $pushedRequest->getMethod()) {
continue;
}
foreach ($parentOptions as $k => $v) {
if ($options[$k] !== $v) {
continue 2;
}
}
foreach (['authorization', 'cookie', 'range', 'proxy-authorization'] as $k) {
if ($pushedRequest->getHeaderArray($k) !== $request->getHeaderArray($k)) {
continue 2;
}
}
$response = yield $pushedResponse;
foreach ($response->getHeaderArray('vary') as $vary) {
foreach (preg_split('/\s*+,\s*+/', $vary) as $v) {
if ('*' === $v || ($pushedRequest->getHeaderArray($v) !== $request->getHeaderArray($v) && 'accept-encoding' !== strtolower($v))) {
$logger && $logger->debug(sprintf('Skipping pushed response: "%s"', $info['url']));
continue 3;
}
}
}
$pushDeferred->resolve();
$logger && $logger->debug(sprintf('Accepting pushed response: "%s %s"', $info['http_method'], $info['url']));
self::addResponseHeaders($response, $info, $headers);
unset($multi->pushedResponses[$authority][$i]);
if (!$multi->pushedResponses[$authority]) {
unset($multi->pushedResponses[$authority]);
}
return $response;
}
}
private static function stopLoop(): void
{
if (null !== self::$delay) {
Loop::cancel(self::$delay);
self::$delay = null;
}
Loop::defer([Loop::class, 'stop']);
}
}

View File

@@ -0,0 +1,195 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Chunk\DataChunk;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Chunk\LastChunk;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\TransportException;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ChunkInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\HttpClientInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface;
/**
* A DTO to work with AsyncResponse.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class AsyncContext
{
private $passthru;
private $client;
private $response;
private $info = [];
private $content;
private $offset;
public function __construct(&$passthru, HttpClientInterface $client, ResponseInterface &$response, array &$info, $content, int $offset)
{
$this->passthru = &$passthru;
$this->client = $client;
$this->response = &$response;
$this->info = &$info;
$this->content = $content;
$this->offset = $offset;
}
/**
* Returns the HTTP status without consuming the response.
*/
public function getStatusCode(): int
{
return $this->response->getInfo('http_code');
}
/**
* Returns the headers without consuming the response.
*/
public function getHeaders(): array
{
$headers = [];
foreach ($this->response->getInfo('response_headers') as $h) {
if (11 <= \strlen($h) && '/' === $h[4] && preg_match('#^HTTP/\d+(?:\.\d+)? ([123456789]\d\d)(?: |$)#', $h, $m)) {
$headers = [];
} elseif (2 === \count($m = explode(':', $h, 2))) {
$headers[strtolower($m[0])][] = ltrim($m[1]);
}
}
return $headers;
}
/**
* @return resource|null The PHP stream resource where the content is buffered, if it is
*/
public function getContent()
{
return $this->content;
}
/**
* Creates a new chunk of content.
*/
public function createChunk(string $data): ChunkInterface
{
return new DataChunk($this->offset, $data);
}
/**
* Pauses the request for the given number of seconds.
*/
public function pause(float $duration): void
{
if (\is_callable($pause = $this->response->getInfo('pause_handler'))) {
$pause($duration);
} elseif (0 < $duration) {
usleep((int) (1E6 * $duration));
}
}
/**
* Cancels the request and returns the last chunk to yield.
*/
public function cancel(): ChunkInterface
{
$this->info['canceled'] = true;
$this->info['error'] = 'Response has been canceled.';
$this->response->cancel();
return new LastChunk();
}
/**
* Returns the current info of the response.
*/
public function getInfo(?string $type = null)
{
if (null !== $type) {
return $this->info[$type] ?? $this->response->getInfo($type);
}
return $this->info + $this->response->getInfo();
}
/**
* Attaches an info to the response.
*
* @return $this
*/
public function setInfo(string $type, $value): self
{
if ('canceled' === $type && $value !== $this->info['canceled']) {
throw new \LogicException('You cannot set the "canceled" info directly.');
}
if (null === $value) {
unset($this->info[$type]);
} else {
$this->info[$type] = $value;
}
return $this;
}
/**
* Returns the currently processed response.
*/
public function getResponse(): ResponseInterface
{
return $this->response;
}
/**
* Replaces the currently processed response by doing a new request.
*/
public function replaceRequest(string $method, string $url, array $options = []): ResponseInterface
{
$this->info['previous_info'][] = $info = $this->response->getInfo();
if (null !== $onProgress = $options['on_progress'] ?? null) {
$thisInfo = &$this->info;
$options['on_progress'] = static function (int $dlNow, int $dlSize, array $info) use (&$thisInfo, $onProgress) {
$onProgress($dlNow, $dlSize, $thisInfo + $info);
};
}
if (0 < ($info['max_duration'] ?? 0) && 0 < ($info['total_time'] ?? 0)) {
if (0 >= $options['max_duration'] = $info['max_duration'] - $info['total_time']) {
throw new TransportException(sprintf('Max duration was reached for "%s".', $info['url']));
}
}
return $this->response = $this->client->request($method, $url, ['buffer' => false] + $options);
}
/**
* Replaces the currently processed response by another one.
*/
public function replaceResponse(ResponseInterface $response): ResponseInterface
{
$this->info['previous_info'][] = $this->response->getInfo();
return $this->response = $response;
}
/**
* Replaces or removes the chunk filter iterator.
*
* @param ?callable(ChunkInterface, self): ?\Iterator $passthru
*/
public function passthru(?callable $passthru = null): void
{
$this->passthru = $passthru ?? static function ($chunk, $context) {
$context->passthru = null;
yield $chunk;
};
}
}

View File

@@ -0,0 +1,486 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Chunk\ErrorChunk;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Chunk\FirstChunk;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Chunk\LastChunk;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\TransportException;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ChunkInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\HttpClientInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface;
/**
* Provides a single extension point to process a response's content stream.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class AsyncResponse implements ResponseInterface, StreamableInterface
{
use CommonResponseTrait;
private const FIRST_CHUNK_YIELDED = 1;
private const LAST_CHUNK_YIELDED = 2;
private $client;
private $response;
private $info = ['canceled' => false];
private $passthru;
private $stream;
private $yieldedState;
/**
* @param ?callable(ChunkInterface, AsyncContext): ?\Iterator $passthru
*/
public function __construct(HttpClientInterface $client, string $method, string $url, array $options, ?callable $passthru = null)
{
$this->client = $client;
$this->shouldBuffer = $options['buffer'] ?? true;
if (null !== $onProgress = $options['on_progress'] ?? null) {
$thisInfo = &$this->info;
$options['on_progress'] = static function (int $dlNow, int $dlSize, array $info) use (&$thisInfo, $onProgress) {
$onProgress($dlNow, $dlSize, $thisInfo + $info);
};
}
$this->response = $client->request($method, $url, ['buffer' => false] + $options);
$this->passthru = $passthru;
$this->initializer = static function (self $response, ?float $timeout = null) {
if (null === $response->shouldBuffer) {
return false;
}
while (true) {
foreach (self::stream([$response], $timeout) as $chunk) {
if ($chunk->isTimeout() && $response->passthru) {
// Timeouts thrown during initialization are transport errors
foreach (self::passthru($response->client, $response, new ErrorChunk($response->offset, new TransportException($chunk->getError()))) as $chunk) {
if ($chunk->isFirst()) {
return false;
}
}
continue 2;
}
if ($chunk->isFirst()) {
return false;
}
}
return false;
}
};
if (\array_key_exists('user_data', $options)) {
$this->info['user_data'] = $options['user_data'];
}
if (\array_key_exists('max_duration', $options)) {
$this->info['max_duration'] = $options['max_duration'];
}
}
public function getStatusCode(): int
{
if ($this->initializer) {
self::initialize($this);
}
return $this->response->getStatusCode();
}
public function getHeaders(bool $throw = true): array
{
if ($this->initializer) {
self::initialize($this);
}
$headers = $this->response->getHeaders(false);
if ($throw) {
$this->checkStatusCode();
}
return $headers;
}
public function getInfo(?string $type = null)
{
if ('debug' === ($type ?? 'debug')) {
$debug = implode('', array_column($this->info['previous_info'] ?? [], 'debug'));
$debug .= $this->response->getInfo('debug');
if ('debug' === $type) {
return $debug;
}
}
if (null !== $type) {
return $this->info[$type] ?? $this->response->getInfo($type);
}
return array_merge($this->info + $this->response->getInfo(), ['debug' => $debug]);
}
public function toStream(bool $throw = true)
{
if ($throw) {
// Ensure headers arrived
$this->getHeaders(true);
}
$handle = function () {
$stream = $this->response instanceof StreamableInterface ? $this->response->toStream(false) : StreamWrapper::createResource($this->response);
return stream_get_meta_data($stream)['wrapper_data']->stream_cast(\STREAM_CAST_FOR_SELECT);
};
$stream = StreamWrapper::createResource($this);
stream_get_meta_data($stream)['wrapper_data']
->bindHandles($handle, $this->content);
return $stream;
}
public function cancel(): void
{
if ($this->info['canceled']) {
return;
}
$this->info['canceled'] = true;
$this->info['error'] = 'Response has been canceled.';
$this->close();
$client = $this->client;
$this->client = null;
if (!$this->passthru) {
return;
}
try {
foreach (self::passthru($client, $this, new LastChunk()) as $chunk) {
// no-op
}
$this->passthru = null;
} catch (ExceptionInterface $e) {
// ignore any errors when canceling
}
}
public function __destruct()
{
$httpException = null;
if ($this->initializer && null === $this->getInfo('error')) {
try {
self::initialize($this, -0.0);
$this->getHeaders(true);
} catch (HttpExceptionInterface $httpException) {
// no-op
}
}
if ($this->passthru && null === $this->getInfo('error')) {
$this->info['canceled'] = true;
try {
foreach (self::passthru($this->client, $this, new LastChunk()) as $chunk) {
// no-op
}
} catch (ExceptionInterface $e) {
// ignore any errors when destructing
}
}
if (null !== $httpException) {
throw $httpException;
}
}
/**
* @internal
*/
public static function stream(iterable $responses, ?float $timeout = null, ?string $class = null): \Generator
{
while ($responses) {
$wrappedResponses = [];
$asyncMap = new \SplObjectStorage();
$client = null;
foreach ($responses as $r) {
if (!$r instanceof self) {
throw new \TypeError(sprintf('"%s::stream()" expects parameter 1 to be an iterable of AsyncResponse objects, "%s" given.', $class ?? static::class, get_debug_type($r)));
}
if (null !== $e = $r->info['error'] ?? null) {
yield $r => $chunk = new ErrorChunk($r->offset, new TransportException($e));
$chunk->didThrow() ?: $chunk->getContent();
continue;
}
if (null === $client) {
$client = $r->client;
} elseif ($r->client !== $client) {
throw new TransportException('Cannot stream AsyncResponse objects with many clients.');
}
$asyncMap[$r->response] = $r;
$wrappedResponses[] = $r->response;
if ($r->stream) {
yield from self::passthruStream($response = $r->response, $r, new FirstChunk(), $asyncMap);
if (!isset($asyncMap[$response])) {
array_pop($wrappedResponses);
}
if ($r->response !== $response && !isset($asyncMap[$r->response])) {
$asyncMap[$r->response] = $r;
$wrappedResponses[] = $r->response;
}
}
}
if (!$client || !$wrappedResponses) {
return;
}
$chunk = null;
foreach ($client->stream($wrappedResponses, $timeout) as $response => $chunk) {
$r = $asyncMap[$response];
if (null === $chunk->getError()) {
if ($chunk->isFirst()) {
// Ensure no exception is thrown on destruct for the wrapped response
$r->response->getStatusCode();
} elseif (0 === $r->offset && null === $r->content && $chunk->isLast()) {
$r->content = fopen('php://memory', 'w+');
}
}
if (!$r->passthru) {
if (null !== $chunk->getError() || $chunk->isLast()) {
unset($asyncMap[$response]);
} elseif (null !== $r->content && '' !== ($content = $chunk->getContent()) && \strlen($content) !== fwrite($r->content, $content)) {
$chunk = new ErrorChunk($r->offset, new TransportException(sprintf('Failed writing %d bytes to the response buffer.', \strlen($content))));
$r->info['error'] = $chunk->getError();
$r->response->cancel();
}
yield $r => $chunk;
continue;
}
if (null !== $chunk->getError()) {
// no-op
} elseif ($chunk->isFirst()) {
$r->yieldedState = self::FIRST_CHUNK_YIELDED;
} elseif (self::FIRST_CHUNK_YIELDED !== $r->yieldedState && null === $chunk->getInformationalStatus()) {
throw new \LogicException(sprintf('Instance of "%s" is already consumed and cannot be managed by "%s". A decorated client should not call any of the response\'s methods in its "request()" method.', get_debug_type($response), $class ?? static::class));
}
foreach (self::passthru($r->client, $r, $chunk, $asyncMap) as $chunk) {
yield $r => $chunk;
}
if ($r->response !== $response && isset($asyncMap[$response])) {
break;
}
}
if (null === $chunk) {
throw new \LogicException(\sprintf('"%s" is not compliant with HttpClientInterface: its "stream()" method didn\'t yield any chunks when it should have.', get_debug_type($client)));
}
if (null === $chunk->getError() && $chunk->isLast()) {
$r->yieldedState = self::LAST_CHUNK_YIELDED;
}
if (null === $chunk->getError() && self::LAST_CHUNK_YIELDED !== $r->yieldedState && $r->response === $response && null !== $r->client) {
throw new \LogicException('A chunk passthru must yield an "isLast()" chunk before ending a stream.');
}
$responses = [];
foreach ($asyncMap as $response) {
$r = $asyncMap[$response];
if (null !== $r->client) {
$responses[] = $asyncMap[$response];
}
}
}
}
/**
* @param \SplObjectStorage<ResponseInterface, AsyncResponse>|null $asyncMap
*/
private static function passthru(HttpClientInterface $client, self $r, ChunkInterface $chunk, ?\SplObjectStorage $asyncMap = null): \Generator
{
$r->stream = null;
$response = $r->response;
$context = new AsyncContext($r->passthru, $client, $r->response, $r->info, $r->content, $r->offset);
if (null === $stream = ($r->passthru)($chunk, $context)) {
if ($r->response === $response && (null !== $chunk->getError() || $chunk->isLast())) {
throw new \LogicException('A chunk passthru cannot swallow the last chunk.');
}
return;
}
if (!$stream instanceof \Iterator) {
throw new \LogicException(sprintf('A chunk passthru must return an "Iterator", "%s" returned.', get_debug_type($stream)));
}
$r->stream = $stream;
yield from self::passthruStream($response, $r, null, $asyncMap);
}
/**
* @param \SplObjectStorage<ResponseInterface, AsyncResponse>|null $asyncMap
*/
private static function passthruStream(ResponseInterface $response, self $r, ?ChunkInterface $chunk, ?\SplObjectStorage $asyncMap): \Generator
{
while (true) {
try {
if (null !== $chunk && $r->stream) {
$r->stream->next();
}
if (!$r->stream || !$r->stream->valid() || !$r->stream) {
$r->stream = null;
break;
}
} catch (\Throwable $e) {
unset($asyncMap[$response]);
$r->stream = null;
$r->info['error'] = $e->getMessage();
$r->response->cancel();
yield $r => $chunk = new ErrorChunk($r->offset, $e);
$chunk->didThrow() ?: $chunk->getContent();
break;
}
$chunk = $r->stream->current();
if (!$chunk instanceof ChunkInterface) {
throw new \LogicException(sprintf('A chunk passthru must yield instances of "%s", "%s" yielded.', ChunkInterface::class, get_debug_type($chunk)));
}
if (null !== $chunk->getError()) {
// no-op
} elseif ($chunk->isFirst()) {
$e = $r->openBuffer();
yield $r => $chunk;
if ($r->initializer && null === $r->getInfo('error')) {
// Ensure the HTTP status code is always checked
$r->getHeaders(true);
}
if (null === $e) {
continue;
}
$r->response->cancel();
$chunk = new ErrorChunk($r->offset, $e);
} elseif ('' !== $content = $chunk->getContent()) {
if (null !== $r->shouldBuffer) {
throw new \LogicException('A chunk passthru must yield an "isFirst()" chunk before any content chunk.');
}
if (null !== $r->content && \strlen($content) !== fwrite($r->content, $content)) {
$chunk = new ErrorChunk($r->offset, new TransportException(sprintf('Failed writing %d bytes to the response buffer.', \strlen($content))));
$r->info['error'] = $chunk->getError();
$r->response->cancel();
}
}
if (null !== $chunk->getError() || $chunk->isLast()) {
$stream = $r->stream;
$r->stream = null;
unset($asyncMap[$response]);
}
if (null === $chunk->getError()) {
$r->offset += \strlen($content);
yield $r => $chunk;
if (!$chunk->isLast()) {
continue;
}
$stream->next();
if ($stream->valid()) {
throw new \LogicException('A chunk passthru cannot yield after an "isLast()" chunk.');
}
$r->passthru = null;
} else {
if ($chunk instanceof ErrorChunk) {
$chunk->didThrow(false);
} else {
try {
$chunk = new ErrorChunk($chunk->getOffset(), !$chunk->isTimeout() ?: $chunk->getError());
} catch (TransportExceptionInterface $e) {
$chunk = new ErrorChunk($chunk->getOffset(), $e);
}
}
yield $r => $chunk;
$chunk->didThrow() ?: $chunk->getContent();
}
break;
}
}
private function openBuffer(): ?\Throwable
{
if (null === $shouldBuffer = $this->shouldBuffer) {
throw new \LogicException('A chunk passthru cannot yield more than one "isFirst()" chunk.');
}
$e = $this->shouldBuffer = null;
if ($shouldBuffer instanceof \Closure) {
try {
$shouldBuffer = $shouldBuffer($this->getHeaders(false));
if (null !== $e = $this->response->getInfo('error')) {
throw new TransportException($e);
}
} catch (\Throwable $e) {
$this->info['error'] = $e->getMessage();
$this->response->cancel();
}
}
if (true === $shouldBuffer) {
$this->content = fopen('php://temp', 'w+');
} elseif (\is_resource($shouldBuffer)) {
$this->content = $shouldBuffer;
}
return $e;
}
private function close(): void
{
$this->response->cancel();
}
}

View File

@@ -0,0 +1,185 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\ClientException;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\JsonException;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\RedirectionException;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\ServerException;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\TransportException;
/**
* Implements common logic for response classes.
*
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
trait CommonResponseTrait
{
/**
* @var callable|null A callback that tells whether we're waiting for response headers
*/
private $initializer;
private $shouldBuffer;
private $content;
private $offset = 0;
private $jsonData;
/**
* {@inheritdoc}
*/
public function getContent(bool $throw = true): string
{
if ($this->initializer) {
self::initialize($this);
}
if ($throw) {
$this->checkStatusCode();
}
if (null === $this->content) {
$content = null;
foreach (self::stream([$this]) as $chunk) {
if (!$chunk->isLast()) {
$content .= $chunk->getContent();
}
}
if (null !== $content) {
return $content;
}
if (null === $this->content) {
throw new TransportException('Cannot get the content of the response twice: buffering is disabled.');
}
} else {
foreach (self::stream([$this]) as $chunk) {
// Chunks are buffered in $this->content already
}
}
rewind($this->content);
return stream_get_contents($this->content);
}
/**
* {@inheritdoc}
*/
public function toArray(bool $throw = true): array
{
if ('' === $content = $this->getContent($throw)) {
throw new JsonException('Response body is empty.');
}
if (null !== $this->jsonData) {
return $this->jsonData;
}
try {
$content = json_decode($content, true, 512, \JSON_BIGINT_AS_STRING | (\PHP_VERSION_ID >= 70300 ? \JSON_THROW_ON_ERROR : 0));
} catch (\JsonException $e) {
throw new JsonException($e->getMessage().sprintf(' for "%s".', $this->getInfo('url')), $e->getCode());
}
if (\PHP_VERSION_ID < 70300 && \JSON_ERROR_NONE !== json_last_error()) {
throw new JsonException(json_last_error_msg().sprintf(' for "%s".', $this->getInfo('url')), json_last_error());
}
if (!\is_array($content)) {
throw new JsonException(sprintf('JSON content was expected to decode to an array, "%s" returned for "%s".', get_debug_type($content), $this->getInfo('url')));
}
if (null !== $this->content) {
// Option "buffer" is true
return $this->jsonData = $content;
}
return $content;
}
/**
* {@inheritdoc}
*/
public function toStream(bool $throw = true)
{
if ($throw) {
// Ensure headers arrived
$this->getHeaders($throw);
}
$stream = StreamWrapper::createResource($this);
stream_get_meta_data($stream)['wrapper_data']
->bindHandles($this->handle, $this->content);
return $stream;
}
public function __sleep(): array
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
/**
* Closes the response and all its network handles.
*/
abstract protected function close(): void;
private static function initialize(self $response): void
{
if (null !== $response->getInfo('error')) {
throw new TransportException($response->getInfo('error'));
}
try {
if (($response->initializer)($response, -0.0)) {
foreach (self::stream([$response], -0.0) as $chunk) {
if ($chunk->isFirst()) {
break;
}
}
}
} catch (\Throwable $e) {
// Persist timeouts thrown during initialization
$response->info['error'] = $e->getMessage();
$response->close();
throw $e;
}
$response->initializer = null;
}
private function checkStatusCode()
{
$code = $this->getInfo('http_code');
if (500 <= $code) {
throw new ServerException($this);
}
if (400 <= $code) {
throw new ClientException($this);
}
if (300 <= $code) {
throw new RedirectionException($this);
}
}
}

View File

@@ -0,0 +1,463 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response;
use KadenceWP\KadenceBlocks\Psr\Log\LoggerInterface;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Chunk\FirstChunk;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Chunk\InformationalChunk;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\TransportException;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal\Canary;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal\ClientState;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal\CurlClientState;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
final class CurlResponse implements ResponseInterface, StreamableInterface
{
use CommonResponseTrait {
getContent as private doGetContent;
}
use TransportResponseTrait;
private $multi;
private $debugBuffer;
/**
* @param \CurlHandle|resource|string $ch
*
* @internal
*/
public function __construct(CurlClientState $multi, $ch, ?array $options = null, ?LoggerInterface $logger = null, string $method = 'GET', ?callable $resolveRedirect = null, ?int $curlVersion = null)
{
$this->multi = $multi;
if (\is_resource($ch) || $ch instanceof \CurlHandle) {
$this->handle = $ch;
$this->debugBuffer = fopen('php://temp', 'w+');
if (0x074000 === $curlVersion) {
fwrite($this->debugBuffer, 'Due to a bug in curl 7.64.0, the debug log is disabled; use another version to work around the issue.');
} else {
curl_setopt($ch, \CURLOPT_VERBOSE, true);
curl_setopt($ch, \CURLOPT_STDERR, $this->debugBuffer);
}
} else {
$this->info['url'] = $ch;
$ch = $this->handle;
}
$this->id = $id = (int) $ch;
$this->logger = $logger;
$this->shouldBuffer = $options['buffer'] ?? true;
$this->timeout = $options['timeout'] ?? null;
$this->info['http_method'] = $method;
$this->info['user_data'] = $options['user_data'] ?? null;
$this->info['max_duration'] = $options['max_duration'] ?? null;
$this->info['start_time'] = $this->info['start_time'] ?? microtime(true);
$info = &$this->info;
$headers = &$this->headers;
$debugBuffer = $this->debugBuffer;
if (!$info['response_headers']) {
// Used to keep track of what we're waiting for
curl_setopt($ch, \CURLOPT_PRIVATE, \in_array($method, ['GET', 'HEAD', 'OPTIONS', 'TRACE'], true) && 1.0 < (float) ($options['http_version'] ?? 1.1) ? 'H2' : 'H0'); // H = headers + retry counter
}
curl_setopt($ch, \CURLOPT_HEADERFUNCTION, static function ($ch, string $data) use (&$info, &$headers, $options, $multi, $id, &$location, $resolveRedirect, $logger): int {
return self::parseHeaderLine($ch, $data, $info, $headers, $options, $multi, $id, $location, $resolveRedirect, $logger);
});
if (null === $options) {
// Pushed response: buffer until requested
curl_setopt($ch, \CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use ($multi, $id): int {
$multi->handlesActivity[$id][] = $data;
curl_pause($ch, \CURLPAUSE_RECV);
return \strlen($data);
});
return;
}
$execCounter = $multi->execCounter;
$this->info['pause_handler'] = static function (float $duration) use ($ch, $multi, $execCounter) {
if (0 < $duration) {
if ($execCounter === $multi->execCounter) {
curl_multi_remove_handle($multi->handle, $ch);
}
$lastExpiry = end($multi->pauseExpiries);
$multi->pauseExpiries[(int) $ch] = $duration += microtime(true);
if (false !== $lastExpiry && $lastExpiry > $duration) {
asort($multi->pauseExpiries);
}
curl_pause($ch, \CURLPAUSE_ALL);
} else {
unset($multi->pauseExpiries[(int) $ch]);
curl_pause($ch, \CURLPAUSE_CONT);
curl_multi_add_handle($multi->handle, $ch);
}
};
$this->inflate = !isset($options['normalized_headers']['accept-encoding']);
curl_pause($ch, \CURLPAUSE_CONT);
if ($onProgress = $options['on_progress']) {
$url = isset($info['url']) ? ['url' => $info['url']] : [];
curl_setopt($ch, \CURLOPT_NOPROGRESS, false);
curl_setopt($ch, \CURLOPT_PROGRESSFUNCTION, static function ($ch, $dlSize, $dlNow) use ($onProgress, &$info, $url, $multi, $debugBuffer) {
try {
rewind($debugBuffer);
$debug = ['debug' => stream_get_contents($debugBuffer)];
$onProgress($dlNow, $dlSize, $url + curl_getinfo($ch) + $info + $debug);
} catch (\Throwable $e) {
$multi->handlesActivity[(int) $ch][] = null;
$multi->handlesActivity[(int) $ch][] = $e;
return 1; // Abort the request
}
return null;
});
}
curl_setopt($ch, \CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use ($multi, $id): int {
if ('H' === (curl_getinfo($ch, \CURLINFO_PRIVATE)[0] ?? null)) {
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = new TransportException(sprintf('Unsupported protocol for "%s"', curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL)));
return 0;
}
curl_setopt($ch, \CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use ($multi, $id): int {
$multi->handlesActivity[$id][] = $data;
return \strlen($data);
});
$multi->handlesActivity[$id][] = $data;
return \strlen($data);
});
$this->initializer = static function (self $response) {
$waitFor = curl_getinfo($ch = $response->handle, \CURLINFO_PRIVATE);
return 'H' === $waitFor[0];
};
// Schedule the request in a non-blocking way
$multi->lastTimeout = null;
$multi->openHandles[$id] = [$ch, $options];
curl_multi_add_handle($multi->handle, $ch);
$this->canary = new Canary(static function () use ($ch, $multi, $id) {
unset($multi->pauseExpiries[$id], $multi->openHandles[$id], $multi->handlesActivity[$id]);
curl_setopt($ch, \CURLOPT_PRIVATE, '_0');
if ($multi->performing) {
return;
}
curl_multi_remove_handle($multi->handle, $ch);
curl_setopt_array($ch, [
\CURLOPT_NOPROGRESS => true,
\CURLOPT_PROGRESSFUNCTION => null,
\CURLOPT_HEADERFUNCTION => null,
\CURLOPT_WRITEFUNCTION => null,
\CURLOPT_READFUNCTION => null,
\CURLOPT_INFILE => null,
]);
if (!$multi->openHandles) {
// Schedule DNS cache eviction for the next request
$multi->dnsCache->evictions = $multi->dnsCache->evictions ?: $multi->dnsCache->removals;
$multi->dnsCache->removals = $multi->dnsCache->hostnames = [];
}
});
}
/**
* {@inheritdoc}
*/
public function getInfo(?string $type = null)
{
if (!$info = $this->finalInfo) {
$info = array_merge($this->info, curl_getinfo($this->handle));
$info['url'] = $this->info['url'] ?? $info['url'];
$info['redirect_url'] = $this->info['redirect_url'] ?? null;
// workaround curl not subtracting the time offset for pushed responses
if (isset($this->info['url']) && $info['start_time'] / 1000 < $info['total_time']) {
$info['total_time'] -= $info['starttransfer_time'] ?: $info['total_time'];
$info['starttransfer_time'] = 0.0;
}
rewind($this->debugBuffer);
$info['debug'] = stream_get_contents($this->debugBuffer);
$waitFor = curl_getinfo($this->handle, \CURLINFO_PRIVATE);
if ('H' !== $waitFor[0] && 'C' !== $waitFor[0]) {
curl_setopt($this->handle, \CURLOPT_VERBOSE, false);
rewind($this->debugBuffer);
ftruncate($this->debugBuffer, 0);
$this->finalInfo = $info;
}
}
return null !== $type ? $info[$type] ?? null : $info;
}
/**
* {@inheritdoc}
*/
public function getContent(bool $throw = true): string
{
$performing = $this->multi->performing;
$this->multi->performing = $performing || '_0' === curl_getinfo($this->handle, \CURLINFO_PRIVATE);
try {
return $this->doGetContent($throw);
} finally {
$this->multi->performing = $performing;
}
}
public function __destruct()
{
try {
if (null === $this->timeout) {
return; // Unused pushed response
}
$this->doDestruct();
} finally {
if (\is_resource($this->handle) || $this->handle instanceof \CurlHandle) {
curl_setopt($this->handle, \CURLOPT_VERBOSE, false);
}
}
}
/**
* {@inheritdoc}
*/
private static function schedule(self $response, array &$runningResponses): void
{
if (isset($runningResponses[$i = (int) $response->multi->handle])) {
$runningResponses[$i][1][$response->id] = $response;
} else {
$runningResponses[$i] = [$response->multi, [$response->id => $response]];
}
if ('_0' === curl_getinfo($ch = $response->handle, \CURLINFO_PRIVATE)) {
// Response already completed
$response->multi->handlesActivity[$response->id][] = null;
$response->multi->handlesActivity[$response->id][] = null !== $response->info['error'] ? new TransportException($response->info['error']) : null;
}
}
/**
* {@inheritdoc}
*
* @param CurlClientState $multi
*/
private static function perform(ClientState $multi, ?array &$responses = null): void
{
if ($multi->performing) {
if ($responses) {
$response = current($responses);
$multi->handlesActivity[(int) $response->handle][] = null;
$multi->handlesActivity[(int) $response->handle][] = new TransportException(sprintf('Userland callback cannot use the client nor the response while processing "%s".', curl_getinfo($response->handle, \CURLINFO_EFFECTIVE_URL)));
}
return;
}
try {
$multi->performing = true;
++$multi->execCounter;
$active = 0;
while (\CURLM_CALL_MULTI_PERFORM === ($err = curl_multi_exec($multi->handle, $active))) {
}
if (\CURLM_OK !== $err) {
throw new TransportException(curl_multi_strerror($err));
}
while ($info = curl_multi_info_read($multi->handle)) {
if (\CURLMSG_DONE !== $info['msg']) {
continue;
}
$result = $info['result'];
$id = (int) $ch = $info['handle'];
$waitFor = @curl_getinfo($ch, \CURLINFO_PRIVATE) ?: '_0';
if (\in_array($result, [\CURLE_SEND_ERROR, \CURLE_RECV_ERROR, /* CURLE_HTTP2 */ 16, /* CURLE_HTTP2_STREAM */ 92], true) && $waitFor[1] && 'C' !== $waitFor[0]) {
curl_multi_remove_handle($multi->handle, $ch);
$waitFor[1] = (string) ((int) $waitFor[1] - 1); // decrement the retry counter
curl_setopt($ch, \CURLOPT_PRIVATE, $waitFor);
curl_setopt($ch, \CURLOPT_FORBID_REUSE, true);
if (0 === curl_multi_add_handle($multi->handle, $ch)) {
continue;
}
}
if (\CURLE_RECV_ERROR === $result && 'H' === $waitFor[0] && 400 <= ($responses[(int) $ch]->info['http_code'] ?? 0)) {
$multi->handlesActivity[$id][] = new FirstChunk();
}
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = \in_array($result, [\CURLE_OK, \CURLE_TOO_MANY_REDIRECTS], true) || '_0' === $waitFor || curl_getinfo($ch, \CURLINFO_SIZE_DOWNLOAD) === curl_getinfo($ch, \CURLINFO_CONTENT_LENGTH_DOWNLOAD) || (curl_error($ch) === 'OpenSSL SSL_read: SSL_ERROR_SYSCALL, errno 0' && -1.0 === curl_getinfo($ch, \CURLINFO_CONTENT_LENGTH_DOWNLOAD) && \in_array('close', array_map('strtolower', $responses[$id]->headers['connection']), true)) ? null : new TransportException(ucfirst(curl_error($ch) ?: curl_strerror($result)).sprintf(' for "%s".', curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL)));
}
} finally {
$multi->performing = false;
}
}
/**
* {@inheritdoc}
*
* @param CurlClientState $multi
*/
private static function select(ClientState $multi, float $timeout): int
{
if (\PHP_VERSION_ID < 70211) {
// workaround https://bugs.php.net/76480
$timeout = min($timeout, 0.01);
}
if ($multi->pauseExpiries) {
$now = microtime(true);
foreach ($multi->pauseExpiries as $id => $pauseExpiry) {
if ($now < $pauseExpiry) {
$timeout = min($timeout, $pauseExpiry - $now);
break;
}
unset($multi->pauseExpiries[$id]);
curl_pause($multi->openHandles[$id][0], \CURLPAUSE_CONT);
curl_multi_add_handle($multi->handle, $multi->openHandles[$id][0]);
}
}
if (0 !== $selected = curl_multi_select($multi->handle, $timeout)) {
return $selected;
}
if ($multi->pauseExpiries && 0 < $timeout -= microtime(true) - $now) {
usleep((int) (1E6 * $timeout));
}
return 0;
}
/**
* Parses header lines as curl yields them to us.
*/
private static function parseHeaderLine($ch, string $data, array &$info, array &$headers, ?array $options, CurlClientState $multi, int $id, ?string &$location, ?callable $resolveRedirect, ?LoggerInterface $logger): int
{
if (!str_ends_with($data, "\r\n")) {
return 0;
}
$waitFor = @curl_getinfo($ch, \CURLINFO_PRIVATE) ?: '_0';
if ('H' !== $waitFor[0]) {
return \strlen($data); // Ignore HTTP trailers
}
$statusCode = curl_getinfo($ch, \CURLINFO_RESPONSE_CODE);
if ($statusCode !== $info['http_code'] && !preg_match("#^HTTP/\d+(?:\.\d+)? {$statusCode}(?: |\r\n$)#", $data)) {
return \strlen($data); // Ignore headers from responses to CONNECT requests
}
if ("\r\n" !== $data) {
// Regular header line: add it to the list
self::addResponseHeaders([substr($data, 0, -2)], $info, $headers);
if (!str_starts_with($data, 'HTTP/')) {
if (0 === stripos($data, 'Location:')) {
$location = trim(substr($data, 9, -2));
}
return \strlen($data);
}
if (\function_exists('openssl_x509_read') && $certinfo = curl_getinfo($ch, \CURLINFO_CERTINFO)) {
$info['peer_certificate_chain'] = array_map('openssl_x509_read', array_column($certinfo, 'Cert'));
}
if (300 <= $info['http_code'] && $info['http_code'] < 400) {
if (curl_getinfo($ch, \CURLINFO_REDIRECT_COUNT) === $options['max_redirects']) {
curl_setopt($ch, \CURLOPT_FOLLOWLOCATION, false);
} elseif (303 === $info['http_code'] || ('POST' === $info['http_method'] && \in_array($info['http_code'], [301, 302], true))) {
curl_setopt($ch, \CURLOPT_POSTFIELDS, '');
}
}
return \strlen($data);
}
// End of headers: handle informational responses, redirects, etc.
if (200 > $statusCode) {
$multi->handlesActivity[$id][] = new InformationalChunk($statusCode, $headers);
$location = null;
return \strlen($data);
}
$info['redirect_url'] = null;
if (300 <= $statusCode && $statusCode < 400 && null !== $location) {
if ($noContent = 303 === $statusCode || ('POST' === $info['http_method'] && \in_array($statusCode, [301, 302], true))) {
$info['http_method'] = 'HEAD' === $info['http_method'] ? 'HEAD' : 'GET';
curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, $info['http_method']);
}
if (null === $info['redirect_url'] = $resolveRedirect($ch, $location, $noContent)) {
$options['max_redirects'] = curl_getinfo($ch, \CURLINFO_REDIRECT_COUNT);
curl_setopt($ch, \CURLOPT_FOLLOWLOCATION, false);
curl_setopt($ch, \CURLOPT_MAXREDIRS, $options['max_redirects']);
}
}
if (401 === $statusCode && isset($options['auth_ntlm']) && 0 === strncasecmp($headers['www-authenticate'][0] ?? '', 'NTLM ', 5)) {
// Continue with NTLM auth
} elseif ($statusCode < 300 || 400 <= $statusCode || null === $location || curl_getinfo($ch, \CURLINFO_REDIRECT_COUNT) === $options['max_redirects']) {
// Headers and redirects completed, time to get the response's content
$multi->handlesActivity[$id][] = new FirstChunk();
if ('HEAD' === $info['http_method'] || \in_array($statusCode, [204, 304], true)) {
$waitFor = '_0'; // no content expected
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = null;
} else {
$waitFor[0] = 'C'; // C = content
}
curl_setopt($ch, \CURLOPT_PRIVATE, $waitFor);
} elseif (null !== $info['redirect_url'] && $logger) {
$logger->info(sprintf('Redirecting: "%s %s"', $info['http_code'], $info['redirect_url']));
}
$location = null;
return \strlen($data);
}
}

View File

@@ -0,0 +1,80 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response;
use GuzzleHttp\Promise\Create;
use GuzzleHttp\Promise\PromiseInterface as GuzzlePromiseInterface;
use Http\Promise\Promise as HttplugPromiseInterface;
use KadenceWP\KadenceBlocks\Psr\Http\Message\ResponseInterface as Psr7ResponseInterface;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*
* @internal
*/
final class HttplugPromise implements HttplugPromiseInterface
{
private $promise;
public function __construct(GuzzlePromiseInterface $promise)
{
$this->promise = $promise;
}
public function then(?callable $onFulfilled = null, ?callable $onRejected = null): self
{
return new self($this->promise->then(
$this->wrapThenCallback($onFulfilled),
$this->wrapThenCallback($onRejected)
));
}
public function cancel(): void
{
$this->promise->cancel();
}
/**
* {@inheritdoc}
*/
public function getState(): string
{
return $this->promise->getState();
}
/**
* {@inheritdoc}
*
* @return Psr7ResponseInterface|mixed
*/
public function wait($unwrap = true)
{
$result = $this->promise->wait($unwrap);
while ($result instanceof HttplugPromiseInterface || $result instanceof GuzzlePromiseInterface) {
$result = $result->wait($unwrap);
}
return $result;
}
private function wrapThenCallback(?callable $callback): ?callable
{
if (null === $callback) {
return null;
}
return static function ($value) use ($callback) {
return Create::promiseFor($callback($value));
};
}
}

View File

@@ -0,0 +1,343 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Chunk\ErrorChunk;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Chunk\FirstChunk;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\InvalidArgumentException;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\TransportException;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal\ClientState;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface;
/**
* A test-friendly response.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class MockResponse implements ResponseInterface, StreamableInterface
{
use CommonResponseTrait;
use TransportResponseTrait {
doDestruct as public __destruct;
}
private $body;
private $requestOptions = [];
private $requestUrl;
private $requestMethod;
private static $mainMulti;
private static $idSequence = 0;
/**
* @param string|string[]|iterable $body The response body as a string or an iterable of strings,
* yielding an empty string simulates an idle timeout,
* throwing an exception yields an ErrorChunk
*
* @see ResponseInterface::getInfo() for possible info, e.g. "response_headers"
*/
public function __construct($body = '', array $info = [])
{
$this->body = is_iterable($body) ? $body : (string) $body;
$this->info = $info + ['http_code' => 200] + $this->info;
if (!isset($info['response_headers'])) {
return;
}
$responseHeaders = [];
foreach ($info['response_headers'] as $k => $v) {
foreach ((array) $v as $v) {
$responseHeaders[] = (\is_string($k) ? $k.': ' : '').$v;
}
}
$this->info['response_headers'] = [];
self::addResponseHeaders($responseHeaders, $this->info, $this->headers);
}
/**
* Returns the options used when doing the request.
*/
public function getRequestOptions(): array
{
return $this->requestOptions;
}
/**
* Returns the URL used when doing the request.
*/
public function getRequestUrl(): string
{
return $this->requestUrl;
}
/**
* Returns the method used when doing the request.
*/
public function getRequestMethod(): string
{
return $this->requestMethod;
}
/**
* {@inheritdoc}
*/
public function getInfo(?string $type = null)
{
return null !== $type ? $this->info[$type] ?? null : $this->info;
}
/**
* {@inheritdoc}
*/
public function cancel(): void
{
$this->info['canceled'] = true;
$this->info['error'] = 'Response has been canceled.';
try {
$this->body = null;
} catch (TransportException $e) {
// ignore errors when canceling
}
$onProgress = $this->requestOptions['on_progress'] ?? static function () {};
$dlSize = isset($this->headers['content-encoding']) || 'HEAD' === ($this->info['http_method'] ?? null) || \in_array($this->info['http_code'], [204, 304], true) ? 0 : (int) ($this->headers['content-length'][0] ?? 0);
$onProgress($this->offset, $dlSize, $this->info);
}
/**
* {@inheritdoc}
*/
protected function close(): void
{
$this->inflate = null;
$this->body = [];
}
/**
* @internal
*/
public static function fromRequest(string $method, string $url, array $options, ResponseInterface $mock): self
{
$response = new self([]);
$response->requestOptions = $options;
$response->id = ++self::$idSequence;
$response->shouldBuffer = $options['buffer'] ?? true;
$response->initializer = static function (self $response) {
return \is_array($response->body[0] ?? null);
};
$response->info['redirect_count'] = 0;
$response->info['redirect_url'] = null;
$response->info['start_time'] = microtime(true);
$response->info['http_method'] = $method;
$response->info['http_code'] = 0;
$response->info['user_data'] = $options['user_data'] ?? null;
$response->info['max_duration'] = $options['max_duration'] ?? null;
$response->info['url'] = $url;
if ($mock instanceof self) {
$mock->requestOptions = $response->requestOptions;
$mock->requestMethod = $method;
$mock->requestUrl = $url;
}
self::writeRequest($response, $options, $mock);
$response->body[] = [$options, $mock];
return $response;
}
/**
* {@inheritdoc}
*/
protected static function schedule(self $response, array &$runningResponses): void
{
if (!$response->id) {
throw new InvalidArgumentException('MockResponse instances must be issued by MockHttpClient before processing.');
}
$multi = self::$mainMulti ?? self::$mainMulti = new ClientState();
if (!isset($runningResponses[0])) {
$runningResponses[0] = [$multi, []];
}
$runningResponses[0][1][$response->id] = $response;
}
/**
* {@inheritdoc}
*/
protected static function perform(ClientState $multi, array &$responses): void
{
foreach ($responses as $response) {
$id = $response->id;
if (null === $response->body) {
// Canceled response
$response->body = [];
} elseif ([] === $response->body) {
// Error chunk
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = null !== $response->info['error'] ? new TransportException($response->info['error']) : null;
} elseif (null === $chunk = array_shift($response->body)) {
// Last chunk
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = array_shift($response->body);
} elseif (\is_array($chunk)) {
// First chunk
try {
$offset = 0;
$chunk[1]->getStatusCode();
$chunk[1]->getHeaders(false);
self::readResponse($response, $chunk[0], $chunk[1], $offset);
$multi->handlesActivity[$id][] = new FirstChunk();
$buffer = $response->requestOptions['buffer'] ?? null;
if ($buffer instanceof \Closure && $response->content = $buffer($response->headers) ?: null) {
$response->content = \is_resource($response->content) ? $response->content : fopen('php://temp', 'w+');
}
} catch (\Throwable $e) {
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = $e;
}
} elseif ($chunk instanceof \Throwable) {
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = $chunk;
} else {
// Data or timeout chunk
$multi->handlesActivity[$id][] = $chunk;
}
}
}
/**
* {@inheritdoc}
*/
protected static function select(ClientState $multi, float $timeout): int
{
return 42;
}
/**
* Simulates sending the request.
*/
private static function writeRequest(self $response, array $options, ResponseInterface $mock)
{
$onProgress = $options['on_progress'] ?? static function () {};
$response->info += $mock->getInfo() ?: [];
// simulate "size_upload" if it is set
if (isset($response->info['size_upload'])) {
$response->info['size_upload'] = 0.0;
}
// simulate "total_time" if it is not set
if (!isset($response->info['total_time'])) {
$response->info['total_time'] = microtime(true) - $response->info['start_time'];
}
// "notify" DNS resolution
$onProgress(0, 0, $response->info);
// consume the request body
if (\is_resource($body = $options['body'] ?? '')) {
$data = stream_get_contents($body);
if (isset($response->info['size_upload'])) {
$response->info['size_upload'] += \strlen($data);
}
} elseif ($body instanceof \Closure) {
while ('' !== $data = $body(16372)) {
if (!\is_string($data)) {
throw new TransportException(sprintf('Return value of the "body" option callback must be string, "%s" returned.', get_debug_type($data)));
}
// "notify" upload progress
if (isset($response->info['size_upload'])) {
$response->info['size_upload'] += \strlen($data);
}
$onProgress(0, 0, $response->info);
}
}
}
/**
* Simulates reading the response.
*/
private static function readResponse(self $response, array $options, ResponseInterface $mock, int &$offset)
{
$onProgress = $options['on_progress'] ?? static function () {};
// populate info related to headers
$info = $mock->getInfo() ?: [];
$response->info['http_code'] = ($info['http_code'] ?? 0) ?: $mock->getStatusCode() ?: 200;
$response->addResponseHeaders($info['response_headers'] ?? [], $response->info, $response->headers);
$dlSize = isset($response->headers['content-encoding']) || 'HEAD' === $response->info['http_method'] || \in_array($response->info['http_code'], [204, 304], true) ? 0 : (int) ($response->headers['content-length'][0] ?? 0);
$response->info = [
'start_time' => $response->info['start_time'],
'user_data' => $response->info['user_data'],
'max_duration' => $response->info['max_duration'],
'http_code' => $response->info['http_code'],
] + $info + $response->info;
if (null !== $response->info['error']) {
throw new TransportException($response->info['error']);
}
if (!isset($response->info['total_time'])) {
$response->info['total_time'] = microtime(true) - $response->info['start_time'];
}
// "notify" headers arrival
$onProgress(0, $dlSize, $response->info);
// cast response body to activity list
$body = $mock instanceof self ? $mock->body : $mock->getContent(false);
if (!\is_string($body)) {
try {
foreach ($body as $chunk) {
if ('' === $chunk = (string) $chunk) {
// simulate an idle timeout
$response->body[] = new ErrorChunk($offset, sprintf('Idle timeout reached for "%s".', $response->info['url']));
} else {
$response->body[] = $chunk;
$offset += \strlen($chunk);
// "notify" download progress
$onProgress($offset, $dlSize, $response->info);
}
}
} catch (\Throwable $e) {
$response->body[] = $e;
}
} elseif ('' !== $body) {
$response->body[] = $body;
$offset = \strlen($body);
}
if (!isset($response->info['total_time'])) {
$response->info['total_time'] = microtime(true) - $response->info['start_time'];
}
// "notify" completion
$onProgress($offset, $dlSize, $response->info);
if ($dlSize && $offset !== $dlSize) {
throw new TransportException(sprintf('Transfer closed with %d bytes remaining to read.', $dlSize - $offset));
}
}
}

View File

@@ -0,0 +1,376 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response;
use KadenceWP\KadenceBlocks\Psr\Log\LoggerInterface;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Chunk\FirstChunk;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\TransportException;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal\Canary;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal\ClientState;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal\NativeClientState;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
final class NativeResponse implements ResponseInterface, StreamableInterface
{
use CommonResponseTrait;
use TransportResponseTrait;
private $context;
private $url;
private $resolver;
private $onProgress;
private $remaining;
private $buffer;
private $multi;
private $pauseExpiry = 0;
/**
* @internal
*/
public function __construct(NativeClientState $multi, $context, string $url, array $options, array &$info, callable $resolver, ?callable $onProgress, ?LoggerInterface $logger)
{
$this->multi = $multi;
$this->id = $id = (int) $context;
$this->context = $context;
$this->url = $url;
$this->logger = $logger;
$this->timeout = $options['timeout'];
$this->info = &$info;
$this->resolver = $resolver;
$this->onProgress = $onProgress;
$this->inflate = !isset($options['normalized_headers']['accept-encoding']);
$this->shouldBuffer = $options['buffer'] ?? true;
// Temporary resource to dechunk the response stream
$this->buffer = fopen('php://temp', 'w+');
$info['user_data'] = $options['user_data'];
$info['max_duration'] = $options['max_duration'];
++$multi->responseCount;
$this->initializer = static function (self $response) {
return null === $response->remaining;
};
$pauseExpiry = &$this->pauseExpiry;
$info['pause_handler'] = static function (float $duration) use (&$pauseExpiry) {
$pauseExpiry = 0 < $duration ? microtime(true) + $duration : 0;
};
$this->canary = new Canary(static function () use ($multi, $id) {
if (null !== ($host = $multi->openHandles[$id][6] ?? null) && 0 >= --$multi->hosts[$host]) {
unset($multi->hosts[$host]);
}
unset($multi->openHandles[$id], $multi->handlesActivity[$id]);
});
}
/**
* {@inheritdoc}
*/
public function getInfo(?string $type = null)
{
if (!$info = $this->finalInfo) {
$info = $this->info;
$info['url'] = implode('', $info['url']);
unset($info['size_body'], $info['request_header']);
if (null === $this->buffer) {
$this->finalInfo = $info;
}
}
return null !== $type ? $info[$type] ?? null : $info;
}
public function __destruct()
{
try {
$this->doDestruct();
} finally {
// Clear the DNS cache when all requests completed
if (0 >= --$this->multi->responseCount) {
$this->multi->responseCount = 0;
$this->multi->dnsCache = [];
}
}
}
private function open(): void
{
$url = $this->url;
set_error_handler(function ($type, $msg) use (&$url) {
if (\E_NOTICE !== $type || 'fopen(): Content-type not specified assuming application/x-www-form-urlencoded' !== $msg) {
throw new TransportException($msg);
}
$this->logger && $this->logger->info(sprintf('%s for "%s".', $msg, $url ?? $this->url));
});
try {
$this->info['start_time'] = microtime(true);
[$resolver, $url] = ($this->resolver)($this->multi);
while (true) {
$context = stream_context_get_options($this->context);
if ($proxy = $context['http']['proxy'] ?? null) {
$this->info['debug'] .= "* Establish HTTP proxy tunnel to {$proxy}\n";
$this->info['request_header'] = $url;
} else {
$this->info['debug'] .= "* Trying {$this->info['primary_ip']}...\n";
$this->info['request_header'] = $this->info['url']['path'].$this->info['url']['query'];
}
$this->info['request_header'] = sprintf("> %s %s HTTP/%s \r\n", $context['http']['method'], $this->info['request_header'], $context['http']['protocol_version']);
$this->info['request_header'] .= implode("\r\n", $context['http']['header'])."\r\n\r\n";
if (\array_key_exists('peer_name', $context['ssl']) && null === $context['ssl']['peer_name']) {
unset($context['ssl']['peer_name']);
$this->context = stream_context_create([], ['options' => $context] + stream_context_get_params($this->context));
}
// Send request and follow redirects when needed
$this->handle = $h = fopen($url, 'r', false, $this->context);
self::addResponseHeaders(stream_get_meta_data($h)['wrapper_data'], $this->info, $this->headers, $this->info['debug']);
$url = $resolver($this->multi, $this->headers['location'][0] ?? null, $this->context);
if (null === $url) {
break;
}
$this->logger && $this->logger->info(sprintf('Redirecting: "%s %s"', $this->info['http_code'], $url ?? $this->url));
}
} catch (\Throwable $e) {
$this->close();
$this->multi->handlesActivity[$this->id][] = null;
$this->multi->handlesActivity[$this->id][] = $e;
return;
} finally {
$this->info['pretransfer_time'] = $this->info['total_time'] = microtime(true) - $this->info['start_time'];
restore_error_handler();
}
if (isset($context['ssl']['capture_peer_cert_chain']) && isset(($context = stream_context_get_options($this->context))['ssl']['peer_certificate_chain'])) {
$this->info['peer_certificate_chain'] = $context['ssl']['peer_certificate_chain'];
}
stream_set_blocking($h, false);
$this->context = $this->resolver = null;
// Create dechunk buffers
if (isset($this->headers['content-length'])) {
$this->remaining = (int) $this->headers['content-length'][0];
} elseif ('chunked' === ($this->headers['transfer-encoding'][0] ?? null)) {
stream_filter_append($this->buffer, 'dechunk', \STREAM_FILTER_WRITE);
$this->remaining = -1;
} else {
$this->remaining = -2;
}
$this->multi->handlesActivity[$this->id] = [new FirstChunk()];
if ('HEAD' === $context['http']['method'] || \in_array($this->info['http_code'], [204, 304], true)) {
$this->multi->handlesActivity[$this->id][] = null;
$this->multi->handlesActivity[$this->id][] = null;
return;
}
$host = parse_url($this->info['redirect_url'] ?? $this->url, \PHP_URL_HOST);
$this->multi->lastTimeout = null;
$this->multi->openHandles[$this->id] = [&$this->pauseExpiry, $h, $this->buffer, $this->onProgress, &$this->remaining, &$this->info, $host];
$this->multi->hosts[$host] = 1 + ($this->multi->hosts[$host] ?? 0);
}
/**
* {@inheritdoc}
*/
private function close(): void
{
$this->canary->cancel();
$this->handle = $this->buffer = $this->inflate = $this->onProgress = null;
}
/**
* {@inheritdoc}
*/
private static function schedule(self $response, array &$runningResponses): void
{
if (!isset($runningResponses[$i = $response->multi->id])) {
$runningResponses[$i] = [$response->multi, []];
}
$runningResponses[$i][1][$response->id] = $response;
if (null === $response->buffer) {
// Response already completed
$response->multi->handlesActivity[$response->id][] = null;
$response->multi->handlesActivity[$response->id][] = null !== $response->info['error'] ? new TransportException($response->info['error']) : null;
}
}
/**
* {@inheritdoc}
*
* @param NativeClientState $multi
*/
private static function perform(ClientState $multi, ?array &$responses = null): void
{
foreach ($multi->openHandles as $i => [$pauseExpiry, $h, $buffer, $onProgress]) {
if ($pauseExpiry) {
if (microtime(true) < $pauseExpiry) {
continue;
}
$multi->openHandles[$i][0] = 0;
}
$hasActivity = false;
$remaining = &$multi->openHandles[$i][4];
$info = &$multi->openHandles[$i][5];
$e = null;
// Read incoming buffer and write it to the dechunk one
try {
if ($remaining && '' !== $data = (string) fread($h, 0 > $remaining ? 16372 : $remaining)) {
fwrite($buffer, $data);
$hasActivity = true;
$multi->sleep = false;
if (-1 !== $remaining) {
$remaining -= \strlen($data);
}
}
} catch (\Throwable $e) {
$hasActivity = $onProgress = false;
}
if (!$hasActivity) {
if ($onProgress) {
try {
// Notify the progress callback so that it can e.g. cancel
// the request if the stream is inactive for too long
$info['total_time'] = microtime(true) - $info['start_time'];
$onProgress();
} catch (\Throwable $e) {
// no-op
}
}
} elseif ('' !== $data = stream_get_contents($buffer, -1, 0)) {
rewind($buffer);
ftruncate($buffer, 0);
if (null === $e) {
$multi->handlesActivity[$i][] = $data;
}
}
if (null !== $e || !$remaining || feof($h)) {
// Stream completed
$info['total_time'] = microtime(true) - $info['start_time'];
$info['starttransfer_time'] = $info['starttransfer_time'] ?: $info['total_time'];
if ($onProgress) {
try {
$onProgress(-1);
} catch (\Throwable $e) {
// no-op
}
}
if (null === $e) {
if (0 < $remaining) {
$e = new TransportException(sprintf('Transfer closed with %s bytes remaining to read.', $remaining));
} elseif (-1 === $remaining && fwrite($buffer, '-') && '' !== stream_get_contents($buffer, -1, 0)) {
$e = new TransportException('Transfer closed with outstanding data remaining from chunked response.');
}
}
$multi->handlesActivity[$i][] = null;
$multi->handlesActivity[$i][] = $e;
if (null !== ($host = $multi->openHandles[$i][6] ?? null) && 0 >= --$multi->hosts[$host]) {
unset($multi->hosts[$host]);
}
unset($multi->openHandles[$i]);
$multi->sleep = false;
}
}
if (null === $responses) {
return;
}
$maxHosts = $multi->maxHostConnections;
foreach ($responses as $i => $response) {
if (null !== $response->remaining || null === $response->buffer) {
continue;
}
if ($response->pauseExpiry && microtime(true) < $response->pauseExpiry) {
// Create empty open handles to tell we still have pending requests
$multi->openHandles[$i] = [\INF, null, null, null];
} elseif ($maxHosts && $maxHosts > ($multi->hosts[parse_url($response->url, \PHP_URL_HOST)] ?? 0)) {
// Open the next pending request - this is a blocking operation so we do only one of them
$response->open();
$multi->sleep = false;
self::perform($multi);
$maxHosts = 0;
}
}
}
/**
* {@inheritdoc}
*
* @param NativeClientState $multi
*/
private static function select(ClientState $multi, float $timeout): int
{
if (!$multi->sleep = !$multi->sleep) {
return -1;
}
$_ = $handles = [];
$now = null;
foreach ($multi->openHandles as [$pauseExpiry, $h]) {
if (null === $h) {
continue;
}
if ($pauseExpiry && ($now ?? $now = microtime(true)) < $pauseExpiry) {
$timeout = min($timeout, $pauseExpiry - $now);
continue;
}
$handles[] = $h;
}
if (!$handles) {
usleep((int) (1E6 * $timeout));
return 0;
}
return stream_select($handles, $_, $_, (int) $timeout, (int) (1E6 * ($timeout - (int) $timeout)));
}
}

View File

@@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ChunkInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseStreamInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
final class ResponseStream implements ResponseStreamInterface
{
private $generator;
public function __construct(\Generator $generator)
{
$this->generator = $generator;
}
public function key(): ResponseInterface
{
return $this->generator->key();
}
public function current(): ChunkInterface
{
return $this->generator->current();
}
public function next(): void
{
$this->generator->next();
}
public function rewind(): void
{
$this->generator->rewind();
}
public function valid(): bool
{
return $this->generator->valid();
}
}

View File

@@ -0,0 +1,313 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\HttpClientInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface;
/**
* Allows turning ResponseInterface instances to PHP streams.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class StreamWrapper
{
/** @var resource|null */
public $context;
/** @var HttpClientInterface */
private $client;
/** @var ResponseInterface */
private $response;
/** @var resource|string|null */
private $content;
/** @var resource|null */
private $handle;
private $blocking = true;
private $timeout;
private $eof = false;
private $offset = 0;
/**
* Creates a PHP stream resource from a ResponseInterface.
*
* @return resource
*/
public static function createResource(ResponseInterface $response, ?HttpClientInterface $client = null)
{
if ($response instanceof StreamableInterface) {
$stack = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS, 2);
if ($response !== ($stack[1]['object'] ?? null)) {
return $response->toStream(false);
}
}
if (null === $client && !method_exists($response, 'stream')) {
throw new \InvalidArgumentException(sprintf('Providing a client to "%s()" is required when the response doesn\'t have any "stream()" method.', __CLASS__));
}
static $registered = false;
if (!$registered = $registered || stream_wrapper_register(strtr(__CLASS__, '\\', '-'), __CLASS__)) {
throw new \RuntimeException(error_get_last()['message'] ?? 'Registering the "symfony" stream wrapper failed.');
}
$context = [
'client' => $client ?? $response,
'response' => $response,
];
return fopen(strtr(__CLASS__, '\\', '-').'://'.$response->getInfo('url'), 'r', false, stream_context_create(['symfony' => $context]));
}
public function getResponse(): ResponseInterface
{
return $this->response;
}
/**
* @param resource|callable|null $handle The resource handle that should be monitored when
* stream_select() is used on the created stream
* @param resource|null $content The seekable resource where the response body is buffered
*/
public function bindHandles(&$handle, &$content): void
{
$this->handle = &$handle;
$this->content = &$content;
$this->offset = null;
}
public function stream_open(string $path, string $mode, int $options): bool
{
if ('r' !== $mode) {
if ($options & \STREAM_REPORT_ERRORS) {
trigger_error(sprintf('Invalid mode "%s": only "r" is supported.', $mode), \E_USER_WARNING);
}
return false;
}
$context = stream_context_get_options($this->context)['symfony'] ?? null;
$this->client = $context['client'] ?? null;
$this->response = $context['response'] ?? null;
$this->context = null;
if (null !== $this->client && null !== $this->response) {
return true;
}
if ($options & \STREAM_REPORT_ERRORS) {
trigger_error('Missing options "client" or "response" in "symfony" stream context.', \E_USER_WARNING);
}
return false;
}
public function stream_read(int $count)
{
if (\is_resource($this->content)) {
// Empty the internal activity list
foreach ($this->client->stream([$this->response], 0) as $chunk) {
try {
if (!$chunk->isTimeout() && $chunk->isFirst()) {
$this->response->getStatusCode(); // ignore 3/4/5xx
}
} catch (ExceptionInterface $e) {
trigger_error($e->getMessage(), \E_USER_WARNING);
return false;
}
}
if (0 !== fseek($this->content, $this->offset ?? 0)) {
return false;
}
if ('' !== $data = fread($this->content, $count)) {
fseek($this->content, 0, \SEEK_END);
$this->offset += \strlen($data);
return $data;
}
}
if (\is_string($this->content)) {
if (\strlen($this->content) <= $count) {
$data = $this->content;
$this->content = null;
} else {
$data = substr($this->content, 0, $count);
$this->content = substr($this->content, $count);
}
$this->offset += \strlen($data);
return $data;
}
foreach ($this->client->stream([$this->response], $this->blocking ? $this->timeout : 0) as $chunk) {
try {
$this->eof = true;
$this->eof = !$chunk->isTimeout();
if (!$this->eof && !$this->blocking) {
return '';
}
$this->eof = $chunk->isLast();
if ($chunk->isFirst()) {
$this->response->getStatusCode(); // ignore 3/4/5xx
}
if ('' !== $data = $chunk->getContent()) {
if (\strlen($data) > $count) {
if (null === $this->content) {
$this->content = substr($data, $count);
}
$data = substr($data, 0, $count);
}
$this->offset += \strlen($data);
return $data;
}
} catch (ExceptionInterface $e) {
trigger_error($e->getMessage(), \E_USER_WARNING);
return false;
}
}
return '';
}
public function stream_set_option(int $option, int $arg1, ?int $arg2): bool
{
if (\STREAM_OPTION_BLOCKING === $option) {
$this->blocking = (bool) $arg1;
} elseif (\STREAM_OPTION_READ_TIMEOUT === $option) {
$this->timeout = $arg1 + $arg2 / 1e6;
} else {
return false;
}
return true;
}
public function stream_tell(): int
{
return $this->offset ?? 0;
}
public function stream_eof(): bool
{
return $this->eof && !\is_string($this->content);
}
public function stream_seek(int $offset, int $whence = \SEEK_SET): bool
{
if (null === $this->content && null === $this->offset) {
$this->response->getStatusCode();
$this->offset = 0;
}
if (!\is_resource($this->content) || 0 !== fseek($this->content, 0, \SEEK_END)) {
return false;
}
$size = ftell($this->content);
if (\SEEK_CUR === $whence) {
$offset += $this->offset ?? 0;
}
if (\SEEK_END === $whence || $size < $offset) {
foreach ($this->client->stream([$this->response]) as $chunk) {
try {
if ($chunk->isFirst()) {
$this->response->getStatusCode(); // ignore 3/4/5xx
}
// Chunks are buffered in $this->content already
$size += \strlen($chunk->getContent());
if (\SEEK_END !== $whence && $offset <= $size) {
break;
}
} catch (ExceptionInterface $e) {
trigger_error($e->getMessage(), \E_USER_WARNING);
return false;
}
}
if (\SEEK_END === $whence) {
$offset += $size;
}
}
if (0 <= $offset && $offset <= $size) {
$this->eof = false;
$this->offset = $offset;
return true;
}
return false;
}
public function stream_cast(int $castAs)
{
if (\STREAM_CAST_FOR_SELECT === $castAs) {
$this->response->getHeaders(false);
return (\is_callable($this->handle) ? ($this->handle)() : $this->handle) ?? false;
}
return false;
}
public function stream_stat(): array
{
try {
$headers = $this->response->getHeaders(false);
} catch (ExceptionInterface $e) {
trigger_error($e->getMessage(), \E_USER_WARNING);
$headers = [];
}
return [
'dev' => 0,
'ino' => 0,
'mode' => 33060,
'nlink' => 0,
'uid' => 0,
'gid' => 0,
'rdev' => 0,
'size' => (int) ($headers['content-length'][0] ?? -1),
'atime' => 0,
'mtime' => strtotime($headers['last-modified'][0] ?? '') ?: 0,
'ctime' => 0,
'blksize' => 0,
'blocks' => 0,
];
}
private function __construct()
{
}
}

View File

@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
interface StreamableInterface
{
/**
* Casts the response to a PHP stream resource.
*
* @return resource
*
* @throws TransportExceptionInterface When a network error occurs
* @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
* @throws ClientExceptionInterface On a 4xx when $throw is true
* @throws ServerExceptionInterface On a 5xx when $throw is true
*/
public function toStream(bool $throw = true);
}

View File

@@ -0,0 +1,221 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Chunk\ErrorChunk;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\ClientException;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\RedirectionException;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\ServerException;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\TraceableHttpClient;
use Symfony\Component\Stopwatch\StopwatchEvent;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\HttpClientInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class TraceableResponse implements ResponseInterface, StreamableInterface
{
private $client;
private $response;
private $content;
private $event;
public function __construct(HttpClientInterface $client, ResponseInterface $response, &$content, ?StopwatchEvent $event = null)
{
$this->client = $client;
$this->response = $response;
$this->content = &$content;
$this->event = $event;
}
public function __sleep(): array
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
try {
if (method_exists($this->response, '__destruct')) {
$this->response->__destruct();
}
} finally {
if ($this->event && $this->event->isStarted()) {
$this->event->stop();
}
}
}
public function getStatusCode(): int
{
try {
return $this->response->getStatusCode();
} finally {
if ($this->event && $this->event->isStarted()) {
$this->event->lap();
}
}
}
public function getHeaders(bool $throw = true): array
{
try {
return $this->response->getHeaders($throw);
} finally {
if ($this->event && $this->event->isStarted()) {
$this->event->lap();
}
}
}
public function getContent(bool $throw = true): string
{
try {
if (false === $this->content) {
return $this->response->getContent($throw);
}
return $this->content = $this->response->getContent(false);
} finally {
if ($this->event && $this->event->isStarted()) {
$this->event->stop();
}
if ($throw) {
$this->checkStatusCode($this->response->getStatusCode());
}
}
}
public function toArray(bool $throw = true): array
{
try {
if (false === $this->content) {
return $this->response->toArray($throw);
}
return $this->content = $this->response->toArray(false);
} finally {
if ($this->event && $this->event->isStarted()) {
$this->event->stop();
}
if ($throw) {
$this->checkStatusCode($this->response->getStatusCode());
}
}
}
public function cancel(): void
{
$this->response->cancel();
if ($this->event && $this->event->isStarted()) {
$this->event->stop();
}
}
public function getInfo(?string $type = null)
{
return $this->response->getInfo($type);
}
/**
* Casts the response to a PHP stream resource.
*
* @return resource
*
* @throws TransportExceptionInterface When a network error occurs
* @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
* @throws ClientExceptionInterface On a 4xx when $throw is true
* @throws ServerExceptionInterface On a 5xx when $throw is true
*/
public function toStream(bool $throw = true)
{
if ($throw) {
// Ensure headers arrived
$this->response->getHeaders(true);
}
if ($this->response instanceof StreamableInterface) {
return $this->response->toStream(false);
}
return StreamWrapper::createResource($this->response, $this->client);
}
/**
* @internal
*/
public static function stream(HttpClientInterface $client, iterable $responses, ?float $timeout): \Generator
{
$wrappedResponses = [];
$traceableMap = new \SplObjectStorage();
foreach ($responses as $r) {
if (!$r instanceof self) {
throw new \TypeError(sprintf('"%s::stream()" expects parameter 1 to be an iterable of TraceableResponse objects, "%s" given.', TraceableHttpClient::class, get_debug_type($r)));
}
$traceableMap[$r->response] = $r;
$wrappedResponses[] = $r->response;
if ($r->event && !$r->event->isStarted()) {
$r->event->start();
}
}
foreach ($client->stream($wrappedResponses, $timeout) as $r => $chunk) {
if ($traceableMap[$r]->event && $traceableMap[$r]->event->isStarted()) {
try {
if ($chunk->isTimeout() || !$chunk->isLast()) {
$traceableMap[$r]->event->lap();
} else {
$traceableMap[$r]->event->stop();
}
} catch (TransportExceptionInterface $e) {
$traceableMap[$r]->event->stop();
if ($chunk instanceof ErrorChunk) {
$chunk->didThrow(false);
} else {
$chunk = new ErrorChunk($chunk->getOffset(), $e);
}
}
}
yield $traceableMap[$r] => $chunk;
}
}
private function checkStatusCode(int $code)
{
if (500 <= $code) {
throw new ServerException($this);
}
if (400 <= $code) {
throw new ClientException($this);
}
if (300 <= $code) {
throw new RedirectionException($this);
}
}
}

View File

@@ -0,0 +1,312 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Chunk\DataChunk;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Chunk\ErrorChunk;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Chunk\FirstChunk;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Chunk\LastChunk;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\TransportException;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Internal\ClientState;
/**
* Implements common logic for transport-level response classes.
*
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
trait TransportResponseTrait
{
private $canary;
private $headers = [];
private $info = [
'response_headers' => [],
'http_code' => 0,
'error' => null,
'canceled' => false,
];
/** @var object|resource */
private $handle;
private $id;
private $timeout = 0;
private $inflate;
private $finalInfo;
private $logger;
/**
* {@inheritdoc}
*/
public function getStatusCode(): int
{
if ($this->initializer) {
self::initialize($this);
}
return $this->info['http_code'];
}
/**
* {@inheritdoc}
*/
public function getHeaders(bool $throw = true): array
{
if ($this->initializer) {
self::initialize($this);
}
if ($throw) {
$this->checkStatusCode();
}
return $this->headers;
}
/**
* {@inheritdoc}
*/
public function cancel(): void
{
$this->info['canceled'] = true;
$this->info['error'] = 'Response has been canceled.';
$this->close();
}
/**
* Closes the response and all its network handles.
*/
protected function close(): void
{
$this->canary->cancel();
$this->inflate = null;
}
/**
* Adds pending responses to the activity list.
*/
abstract protected static function schedule(self $response, array &$runningResponses): void;
/**
* Performs all pending non-blocking operations.
*/
abstract protected static function perform(ClientState $multi, array &$responses): void;
/**
* Waits for network activity.
*/
abstract protected static function select(ClientState $multi, float $timeout): int;
private static function addResponseHeaders(array $responseHeaders, array &$info, array &$headers, string &$debug = ''): void
{
foreach ($responseHeaders as $h) {
if (11 <= \strlen($h) && '/' === $h[4] && preg_match('#^HTTP/\d+(?:\.\d+)? (\d\d\d)(?: |$)#', $h, $m)) {
if ($headers) {
$debug .= "< \r\n";
$headers = [];
}
$info['http_code'] = (int) $m[1];
} elseif (2 === \count($m = explode(':', $h, 2))) {
$headers[strtolower($m[0])][] = ltrim($m[1]);
}
$debug .= "< {$h}\r\n";
$info['response_headers'][] = $h;
}
$debug .= "< \r\n";
}
/**
* Ensures the request is always sent and that the response code was checked.
*/
private function doDestruct()
{
$this->shouldBuffer = true;
if ($this->initializer && null === $this->info['error']) {
self::initialize($this);
$this->checkStatusCode();
}
}
/**
* Implements an event loop based on a buffer activity queue.
*
* @param iterable<array-key, self> $responses
*
* @internal
*/
public static function stream(iterable $responses, ?float $timeout = null): \Generator
{
$runningResponses = [];
foreach ($responses as $response) {
self::schedule($response, $runningResponses);
}
$lastActivity = microtime(true);
$elapsedTimeout = 0;
if ($fromLastTimeout = 0.0 === $timeout && '-0' === (string) $timeout) {
$timeout = null;
} elseif ($fromLastTimeout = 0 > $timeout) {
$timeout = -$timeout;
}
while (true) {
$hasActivity = false;
$timeoutMax = 0;
$timeoutMin = $timeout ?? \INF;
/** @var ClientState $multi */
foreach ($runningResponses as $i => [$multi]) {
$responses = &$runningResponses[$i][1];
self::perform($multi, $responses);
foreach ($responses as $j => $response) {
$timeoutMax = $timeout ?? max($timeoutMax, $response->timeout);
$timeoutMin = min($timeoutMin, $response->timeout, 1);
$chunk = false;
if ($fromLastTimeout && null !== $multi->lastTimeout) {
$elapsedTimeout = microtime(true) - $multi->lastTimeout;
}
if (isset($multi->handlesActivity[$j])) {
$multi->lastTimeout = null;
} elseif (!isset($multi->openHandles[$j])) {
unset($responses[$j]);
continue;
} elseif ($elapsedTimeout >= $timeoutMax) {
$multi->handlesActivity[$j] = [new ErrorChunk($response->offset, sprintf('Idle timeout reached for "%s".', $response->getInfo('url')))];
$multi->lastTimeout ?? $multi->lastTimeout = $lastActivity;
} else {
continue;
}
while ($multi->handlesActivity[$j] ?? false) {
$hasActivity = true;
$elapsedTimeout = 0;
if (\is_string($chunk = array_shift($multi->handlesActivity[$j]))) {
if (null !== $response->inflate && false === $chunk = @inflate_add($response->inflate, $chunk)) {
$multi->handlesActivity[$j] = [null, new TransportException(sprintf('Error while processing content unencoding for "%s".', $response->getInfo('url')))];
continue;
}
if ('' !== $chunk && null !== $response->content && \strlen($chunk) !== fwrite($response->content, $chunk)) {
$multi->handlesActivity[$j] = [null, new TransportException(sprintf('Failed writing %d bytes to the response buffer.', \strlen($chunk)))];
continue;
}
$chunkLen = \strlen($chunk);
$chunk = new DataChunk($response->offset, $chunk);
$response->offset += $chunkLen;
} elseif (null === $chunk) {
$e = $multi->handlesActivity[$j][0];
unset($responses[$j], $multi->handlesActivity[$j]);
$response->close();
if (null !== $e) {
$response->info['error'] = $e->getMessage();
if ($e instanceof \Error) {
throw $e;
}
$chunk = new ErrorChunk($response->offset, $e);
} else {
if (0 === $response->offset && null === $response->content) {
$response->content = fopen('php://memory', 'w+');
}
$chunk = new LastChunk($response->offset);
}
} elseif ($chunk instanceof ErrorChunk) {
unset($responses[$j]);
$elapsedTimeout = $timeoutMax;
} elseif ($chunk instanceof FirstChunk) {
if ($response->logger) {
$info = $response->getInfo();
$response->logger->info(sprintf('Response: "%s %s"', $info['http_code'], $info['url']));
}
$response->inflate = \extension_loaded('zlib') && $response->inflate && 'gzip' === ($response->headers['content-encoding'][0] ?? null) ? inflate_init(\ZLIB_ENCODING_GZIP) : null;
if ($response->shouldBuffer instanceof \Closure) {
try {
$response->shouldBuffer = ($response->shouldBuffer)($response->headers);
if (null !== $response->info['error']) {
throw new TransportException($response->info['error']);
}
} catch (\Throwable $e) {
$response->close();
$multi->handlesActivity[$j] = [null, $e];
}
}
if (true === $response->shouldBuffer) {
$response->content = fopen('php://temp', 'w+');
} elseif (\is_resource($response->shouldBuffer)) {
$response->content = $response->shouldBuffer;
}
$response->shouldBuffer = null;
yield $response => $chunk;
if ($response->initializer && null === $response->info['error']) {
// Ensure the HTTP status code is always checked
$response->getHeaders(true);
}
continue;
}
yield $response => $chunk;
}
unset($multi->handlesActivity[$j]);
if ($chunk instanceof ErrorChunk && !$chunk->didThrow()) {
// Ensure transport exceptions are always thrown
$chunk->getContent();
}
}
if (!$responses) {
unset($runningResponses[$i]);
}
// Prevent memory leaks
$multi->handlesActivity = $multi->handlesActivity ?: [];
$multi->openHandles = $multi->openHandles ?: [];
}
if (!$runningResponses) {
break;
}
if ($hasActivity) {
$lastActivity = microtime(true);
continue;
}
if (-1 === self::select($multi, min($timeoutMin, $timeoutMax - $elapsedTimeout))) {
usleep((int) min(500, 1E6 * $timeoutMin));
}
$elapsedTimeout = microtime(true) - $lastActivity;
}
}
}

View File

@@ -0,0 +1,115 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Retry;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\InvalidArgumentException;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\AsyncContext;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* Decides to retry the request when HTTP status codes belong to the given list of codes.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class GenericRetryStrategy implements RetryStrategyInterface
{
public const IDEMPOTENT_METHODS = ['GET', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'TRACE'];
public const DEFAULT_RETRY_STATUS_CODES = [
0 => self::IDEMPOTENT_METHODS, // for transport exceptions
423,
425,
429,
500 => self::IDEMPOTENT_METHODS,
502,
503,
504 => self::IDEMPOTENT_METHODS,
507 => self::IDEMPOTENT_METHODS,
510 => self::IDEMPOTENT_METHODS,
];
private $statusCodes;
private $delayMs;
private $multiplier;
private $maxDelayMs;
private $jitter;
/**
* @param array $statusCodes List of HTTP status codes that trigger a retry
* @param int $delayMs Amount of time to delay (or the initial value when multiplier is used)
* @param float $multiplier Multiplier to apply to the delay each time a retry occurs
* @param int $maxDelayMs Maximum delay to allow (0 means no maximum)
* @param float $jitter Probability of randomness int delay (0 = none, 1 = 100% random)
*/
public function __construct(array $statusCodes = self::DEFAULT_RETRY_STATUS_CODES, int $delayMs = 1000, float $multiplier = 2.0, int $maxDelayMs = 0, float $jitter = 0.1)
{
$this->statusCodes = $statusCodes;
if ($delayMs < 0) {
throw new InvalidArgumentException(sprintf('Delay must be greater than or equal to zero: "%s" given.', $delayMs));
}
$this->delayMs = $delayMs;
if ($multiplier < 1) {
throw new InvalidArgumentException(sprintf('Multiplier must be greater than or equal to one: "%s" given.', $multiplier));
}
$this->multiplier = $multiplier;
if ($maxDelayMs < 0) {
throw new InvalidArgumentException(sprintf('Max delay must be greater than or equal to zero: "%s" given.', $maxDelayMs));
}
$this->maxDelayMs = $maxDelayMs;
if ($jitter < 0 || $jitter > 1) {
throw new InvalidArgumentException(sprintf('Jitter must be between 0 and 1: "%s" given.', $jitter));
}
$this->jitter = $jitter;
}
public function shouldRetry(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): ?bool
{
$statusCode = $context->getStatusCode();
if (\in_array($statusCode, $this->statusCodes, true)) {
return true;
}
if (isset($this->statusCodes[$statusCode]) && \is_array($this->statusCodes[$statusCode])) {
return \in_array($context->getInfo('http_method'), $this->statusCodes[$statusCode], true);
}
if (null === $exception) {
return false;
}
if (\in_array(0, $this->statusCodes, true)) {
return true;
}
if (isset($this->statusCodes[0]) && \is_array($this->statusCodes[0])) {
return \in_array($context->getInfo('http_method'), $this->statusCodes[0], true);
}
return false;
}
public function getDelay(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): int
{
$delay = $this->delayMs * $this->multiplier ** $context->getInfo('retry_count');
if ($this->jitter > 0) {
$randomness = (int) ($delay * $this->jitter);
$delay = $delay + random_int(-$randomness, +$randomness);
}
if ($delay > $this->maxDelayMs && 0 !== $this->maxDelayMs) {
return $this->maxDelayMs;
}
return (int) $delay;
}
}

View File

@@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Retry;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\AsyncContext;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
interface RetryStrategyInterface
{
/**
* Returns whether the request should be retried.
*
* @param ?string $responseContent Null is passed when the body did not arrive yet
*
* @return bool|null Returns null to signal that the body is required to take a decision
*/
public function shouldRetry(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): ?bool;
/**
* Returns the time to wait in milliseconds.
*/
public function getDelay(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): int;
}

View File

@@ -0,0 +1,171 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient;
use KadenceWP\KadenceBlocks\Psr\Log\LoggerInterface;
use KadenceWP\KadenceBlocks\Psr\Log\NullLogger;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\AsyncContext;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\AsyncResponse;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Retry\GenericRetryStrategy;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Retry\RetryStrategyInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ChunkInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\HttpClientInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\Service\ResetInterface;
/**
* Automatically retries failing HTTP requests.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
class RetryableHttpClient implements HttpClientInterface, ResetInterface
{
use AsyncDecoratorTrait;
private $strategy;
private $maxRetries;
private $logger;
/**
* @param int $maxRetries The maximum number of times to retry
*/
public function __construct(HttpClientInterface $client, ?RetryStrategyInterface $strategy = null, int $maxRetries = 3, ?LoggerInterface $logger = null)
{
$this->client = $client;
$this->strategy = $strategy ?? new GenericRetryStrategy();
$this->maxRetries = $maxRetries;
$this->logger = $logger ?? new NullLogger();
}
public function request(string $method, string $url, array $options = []): ResponseInterface
{
if ($this->maxRetries <= 0) {
return new AsyncResponse($this->client, $method, $url, $options);
}
$retryCount = 0;
$content = '';
$firstChunk = null;
return new AsyncResponse($this->client, $method, $url, $options, function (ChunkInterface $chunk, AsyncContext $context) use ($method, $url, $options, &$retryCount, &$content, &$firstChunk) {
$exception = null;
try {
if ($context->getInfo('canceled') || $chunk->isTimeout() || null !== $chunk->getInformationalStatus()) {
yield $chunk;
return;
}
} catch (TransportExceptionInterface $exception) {
// catch TransportExceptionInterface to send it to the strategy
}
if (null !== $exception) {
// always retry request that fail to resolve DNS
if ('' !== $context->getInfo('primary_ip')) {
$shouldRetry = $this->strategy->shouldRetry($context, null, $exception);
if (null === $shouldRetry) {
throw new \LogicException(sprintf('The "%s::shouldRetry()" method must not return null when called with an exception.', \get_class($this->strategy)));
}
if (false === $shouldRetry) {
yield from $this->passthru($context, $firstChunk, $content, $chunk);
return;
}
}
} elseif ($chunk->isFirst()) {
if (false === $shouldRetry = $this->strategy->shouldRetry($context, null, null)) {
yield from $this->passthru($context, $firstChunk, $content, $chunk);
return;
}
// Body is needed to decide
if (null === $shouldRetry) {
$firstChunk = $chunk;
$content = '';
return;
}
} else {
if (!$chunk->isLast()) {
$content .= $chunk->getContent();
return;
}
if (null === $shouldRetry = $this->strategy->shouldRetry($context, $content, null)) {
throw new \LogicException(sprintf('The "%s::shouldRetry()" method must not return null when called with a body.', \get_class($this->strategy)));
}
if (false === $shouldRetry) {
yield from $this->passthru($context, $firstChunk, $content, $chunk);
return;
}
}
$context->getResponse()->cancel();
$delay = $this->getDelayFromHeader($context->getHeaders()) ?? $this->strategy->getDelay($context, !$exception && $chunk->isLast() ? $content : null, $exception);
++$retryCount;
$content = '';
$firstChunk = null;
$this->logger->info('Try #{count} after {delay}ms'.($exception ? ': '.$exception->getMessage() : ', status code: '.$context->getStatusCode()), [
'count' => $retryCount,
'delay' => $delay,
]);
$context->setInfo('retry_count', $retryCount);
$context->replaceRequest($method, $url, $options);
$context->pause($delay / 1000);
if ($retryCount >= $this->maxRetries) {
$context->passthru();
}
});
}
private function getDelayFromHeader(array $headers): ?int
{
if (null !== $after = $headers['retry-after'][0] ?? null) {
if (is_numeric($after)) {
return (int) ($after * 1000);
}
if (false !== $time = strtotime($after)) {
return max(0, $time - time()) * 1000;
}
}
return null;
}
private function passthru(AsyncContext $context, ?ChunkInterface $firstChunk, string &$content, ChunkInterface $lastChunk): \Generator
{
$context->passthru();
if (null !== $firstChunk) {
yield $firstChunk;
}
if ('' !== $content) {
$chunk = $context->createChunk($content);
$content = '';
yield $chunk;
}
yield $lastChunk;
}
}

View File

@@ -0,0 +1,131 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient;
use KadenceWP\KadenceBlocks\Psr\Log\LoggerAwareInterface;
use KadenceWP\KadenceBlocks\Psr\Log\LoggerInterface;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Exception\InvalidArgumentException;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\HttpClientInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseStreamInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\Service\ResetInterface;
/**
* Auto-configure the default options based on the requested URL.
*
* @author Anthony Martin <anthony.martin@sensiolabs.com>
*/
class ScopingHttpClient implements HttpClientInterface, ResetInterface, LoggerAwareInterface
{
use HttpClientTrait;
private $client;
private $defaultOptionsByRegexp;
private $defaultRegexp;
public function __construct(HttpClientInterface $client, array $defaultOptionsByRegexp, ?string $defaultRegexp = null)
{
$this->client = $client;
$this->defaultOptionsByRegexp = $defaultOptionsByRegexp;
$this->defaultRegexp = $defaultRegexp;
if (null !== $defaultRegexp && !isset($defaultOptionsByRegexp[$defaultRegexp])) {
throw new InvalidArgumentException(sprintf('No options are mapped to the provided "%s" default regexp.', $defaultRegexp));
}
}
public static function forBaseUri(HttpClientInterface $client, string $baseUri, array $defaultOptions = [], ?string $regexp = null): self
{
if (null === $regexp) {
$regexp = preg_quote(implode('', self::resolveUrl(self::parseUrl('.'), self::parseUrl($baseUri))));
}
$defaultOptions['base_uri'] = $baseUri;
return new self($client, [$regexp => $defaultOptions], $regexp);
}
/**
* {@inheritdoc}
*/
public function request(string $method, string $url, array $options = []): ResponseInterface
{
$e = null;
$url = self::parseUrl($url, $options['query'] ?? []);
if (\is_string($options['base_uri'] ?? null)) {
$options['base_uri'] = self::parseUrl($options['base_uri']);
}
try {
$url = implode('', self::resolveUrl($url, $options['base_uri'] ?? null));
} catch (InvalidArgumentException $e) {
if (null === $this->defaultRegexp) {
throw $e;
}
$defaultOptions = $this->defaultOptionsByRegexp[$this->defaultRegexp];
$options = self::mergeDefaultOptions($options, $defaultOptions, true);
if (\is_string($options['base_uri'] ?? null)) {
$options['base_uri'] = self::parseUrl($options['base_uri']);
}
$url = implode('', self::resolveUrl($url, $options['base_uri'] ?? null, $defaultOptions['query'] ?? []));
}
foreach ($this->defaultOptionsByRegexp as $regexp => $defaultOptions) {
if (preg_match("{{$regexp}}A", $url)) {
if (null === $e || $regexp !== $this->defaultRegexp) {
$options = self::mergeDefaultOptions($options, $defaultOptions, true);
}
break;
}
}
return $this->client->request($method, $url, $options);
}
/**
* {@inheritdoc}
*/
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
{
return $this->client->stream($responses, $timeout);
}
public function reset()
{
if ($this->client instanceof ResetInterface) {
$this->client->reset();
}
}
/**
* {@inheritdoc}
*/
public function setLogger(LoggerInterface $logger): void
{
if ($this->client instanceof LoggerAwareInterface) {
$this->client->setLogger($logger);
}
}
/**
* {@inheritdoc}
*/
public function withOptions(array $options): self
{
$clone = clone $this;
$clone->client = $this->client->withOptions($options);
return $clone;
}
}

View File

@@ -0,0 +1,120 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpClient;
use KadenceWP\KadenceBlocks\Psr\Log\LoggerAwareInterface;
use KadenceWP\KadenceBlocks\Psr\Log\LoggerInterface;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\ResponseStream;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpClient\Response\TraceableResponse;
use Symfony\Component\Stopwatch\Stopwatch;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\HttpClientInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\HttpClient\ResponseStreamInterface;
use KadenceWP\KadenceBlocks\Symfony\Contracts\Service\ResetInterface;
/**
* @author Jérémy Romey <jeremy@free-agent.fr>
*/
final class TraceableHttpClient implements HttpClientInterface, ResetInterface, LoggerAwareInterface
{
private $client;
private $stopwatch;
private $tracedRequests;
public function __construct(HttpClientInterface $client, ?Stopwatch $stopwatch = null)
{
$this->client = $client;
$this->stopwatch = $stopwatch;
$this->tracedRequests = new \ArrayObject();
}
/**
* {@inheritdoc}
*/
public function request(string $method, string $url, array $options = []): ResponseInterface
{
$content = null;
$traceInfo = [];
$this->tracedRequests[] = [
'method' => $method,
'url' => $url,
'options' => $options,
'info' => &$traceInfo,
'content' => &$content,
];
$onProgress = $options['on_progress'] ?? null;
if (false === ($options['extra']['trace_content'] ?? true)) {
unset($content);
$content = false;
}
$options['on_progress'] = function (int $dlNow, int $dlSize, array $info) use (&$traceInfo, $onProgress) {
$traceInfo = $info;
if (null !== $onProgress) {
$onProgress($dlNow, $dlSize, $info);
}
};
return new TraceableResponse($this->client, $this->client->request($method, $url, $options), $content, null === $this->stopwatch ? null : $this->stopwatch->start("$method $url", 'http_client'));
}
/**
* {@inheritdoc}
*/
public function stream($responses, ?float $timeout = null): ResponseStreamInterface
{
if ($responses instanceof TraceableResponse) {
$responses = [$responses];
} elseif (!is_iterable($responses)) {
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of TraceableResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
}
return new ResponseStream(TraceableResponse::stream($this->client, $responses, $timeout));
}
public function getTracedRequests(): array
{
return $this->tracedRequests->getArrayCopy();
}
public function reset()
{
if ($this->client instanceof ResetInterface) {
$this->client->reset();
}
$this->tracedRequests->exchangeArray([]);
}
/**
* {@inheritdoc}
*/
public function setLogger(LoggerInterface $logger): void
{
if ($this->client instanceof LoggerAwareInterface) {
$this->client->setLogger($logger);
}
}
/**
* {@inheritdoc}
*/
public function withOptions(array $options): self
{
$clone = clone $this;
$clone->client = $this->client->withOptions($options);
return $clone;
}
}

View File

@@ -0,0 +1,168 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpFoundation;
// Help opcache.preload discover always-needed symbols
class_exists(AcceptHeaderItem::class);
/**
* Represents an Accept-* header.
*
* An accept header is compound with a list of items,
* sorted by descending quality.
*
* @author Jean-François Simon <contact@jfsimon.fr>
*/
class AcceptHeader
{
/**
* @var AcceptHeaderItem[]
*/
private $items = [];
/**
* @var bool
*/
private $sorted = true;
/**
* @param AcceptHeaderItem[] $items
*/
public function __construct(array $items)
{
foreach ($items as $item) {
$this->add($item);
}
}
/**
* Builds an AcceptHeader instance from a string.
*
* @return self
*/
public static function fromString(?string $headerValue)
{
$index = 0;
$parts = HeaderUtils::split($headerValue ?? '', ',;=');
return new self(array_map(function ($subParts) use (&$index) {
$part = array_shift($subParts);
$attributes = HeaderUtils::combine($subParts);
$item = new AcceptHeaderItem($part[0], $attributes);
$item->setIndex($index++);
return $item;
}, $parts));
}
/**
* Returns header value's string representation.
*
* @return string
*/
public function __toString()
{
return implode(',', $this->items);
}
/**
* Tests if header has given value.
*
* @return bool
*/
public function has(string $value)
{
return isset($this->items[$value]);
}
/**
* Returns given value's item, if exists.
*
* @return AcceptHeaderItem|null
*/
public function get(string $value)
{
return $this->items[$value] ?? $this->items[explode('/', $value)[0].'/*'] ?? $this->items['*/*'] ?? $this->items['*'] ?? null;
}
/**
* Adds an item.
*
* @return $this
*/
public function add(AcceptHeaderItem $item)
{
$this->items[$item->getValue()] = $item;
$this->sorted = false;
return $this;
}
/**
* Returns all items.
*
* @return AcceptHeaderItem[]
*/
public function all()
{
$this->sort();
return $this->items;
}
/**
* Filters items on their value using given regex.
*
* @return self
*/
public function filter(string $pattern)
{
return new self(array_filter($this->items, function (AcceptHeaderItem $item) use ($pattern) {
return preg_match($pattern, $item->getValue());
}));
}
/**
* Returns first item.
*
* @return AcceptHeaderItem|null
*/
public function first()
{
$this->sort();
return !empty($this->items) ? reset($this->items) : null;
}
/**
* Sorts items by descending quality.
*/
private function sort(): void
{
if (!$this->sorted) {
uasort($this->items, function (AcceptHeaderItem $a, AcceptHeaderItem $b) {
$qA = $a->getQuality();
$qB = $b->getQuality();
if ($qA === $qB) {
return $a->getIndex() > $b->getIndex() ? 1 : -1;
}
return $qA > $qB ? -1 : 1;
});
$this->sorted = true;
}
}
}

View File

@@ -0,0 +1,177 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpFoundation;
/**
* Represents an Accept-* header item.
*
* @author Jean-François Simon <contact@jfsimon.fr>
*/
class AcceptHeaderItem
{
private $value;
private $quality = 1.0;
private $index = 0;
private $attributes = [];
public function __construct(string $value, array $attributes = [])
{
$this->value = $value;
foreach ($attributes as $name => $value) {
$this->setAttribute($name, $value);
}
}
/**
* Builds an AcceptHeaderInstance instance from a string.
*
* @return self
*/
public static function fromString(?string $itemValue)
{
$parts = HeaderUtils::split($itemValue ?? '', ';=');
$part = array_shift($parts);
$attributes = HeaderUtils::combine($parts);
return new self($part[0], $attributes);
}
/**
* Returns header value's string representation.
*
* @return string
*/
public function __toString()
{
$string = $this->value.($this->quality < 1 ? ';q='.$this->quality : '');
if (\count($this->attributes) > 0) {
$string .= '; '.HeaderUtils::toString($this->attributes, ';');
}
return $string;
}
/**
* Set the item value.
*
* @return $this
*/
public function setValue(string $value)
{
$this->value = $value;
return $this;
}
/**
* Returns the item value.
*
* @return string
*/
public function getValue()
{
return $this->value;
}
/**
* Set the item quality.
*
* @return $this
*/
public function setQuality(float $quality)
{
$this->quality = $quality;
return $this;
}
/**
* Returns the item quality.
*
* @return float
*/
public function getQuality()
{
return $this->quality;
}
/**
* Set the item index.
*
* @return $this
*/
public function setIndex(int $index)
{
$this->index = $index;
return $this;
}
/**
* Returns the item index.
*
* @return int
*/
public function getIndex()
{
return $this->index;
}
/**
* Tests if an attribute exists.
*
* @return bool
*/
public function hasAttribute(string $name)
{
return isset($this->attributes[$name]);
}
/**
* Returns an attribute by its name.
*
* @param mixed $default
*
* @return mixed
*/
public function getAttribute(string $name, $default = null)
{
return $this->attributes[$name] ?? $default;
}
/**
* Returns all attributes.
*
* @return array
*/
public function getAttributes()
{
return $this->attributes;
}
/**
* Set an attribute.
*
* @return $this
*/
public function setAttribute(string $name, string $value)
{
if ('q' === $name) {
$this->quality = (float) $value;
} else {
$this->attributes[$name] = $value;
}
return $this;
}
}

View File

@@ -0,0 +1,418 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace KadenceWP\KadenceBlocks\Symfony\Component\HttpFoundation;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpFoundation\File\Exception\FileException;
use KadenceWP\KadenceBlocks\Symfony\Component\HttpFoundation\File\File;
/**
* BinaryFileResponse represents an HTTP response delivering a file.
*
* @author Niklas Fiekas <niklas.fiekas@tu-clausthal.de>
* @author stealth35 <stealth35-php@live.fr>
* @author Igor Wiedler <igor@wiedler.ch>
* @author Jordan Alliot <jordan.alliot@gmail.com>
* @author Sergey Linnik <linniksa@gmail.com>
*/
class BinaryFileResponse extends Response
{
protected static $trustXSendfileTypeHeader = false;
/**
* @var File
*/
protected $file;
protected $offset = 0;
protected $maxlen = -1;
protected $deleteFileAfterSend = false;
protected $chunkSize = 16 * 1024;
/**
* @param \SplFileInfo|string $file The file to stream
* @param int $status The response status code
* @param array $headers An array of response headers
* @param bool $public Files are public by default
* @param string|null $contentDisposition The type of Content-Disposition to set automatically with the filename
* @param bool $autoEtag Whether the ETag header should be automatically set
* @param bool $autoLastModified Whether the Last-Modified header should be automatically set
*/
public function __construct($file, int $status = 200, array $headers = [], bool $public = true, ?string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true)
{
parent::__construct(null, $status, $headers);
$this->setFile($file, $contentDisposition, $autoEtag, $autoLastModified);
if ($public) {
$this->setPublic();
}
}
/**
* @param \SplFileInfo|string $file The file to stream
* @param int $status The response status code
* @param array $headers An array of response headers
* @param bool $public Files are public by default
* @param string|null $contentDisposition The type of Content-Disposition to set automatically with the filename
* @param bool $autoEtag Whether the ETag header should be automatically set
* @param bool $autoLastModified Whether the Last-Modified header should be automatically set
*
* @return static
*
* @deprecated since Symfony 5.2, use __construct() instead.
*/
public static function create($file = null, int $status = 200, array $headers = [], bool $public = true, ?string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true)
{
kadence_blocks_trigger_deprecation('symfony/http-foundation', '5.2', 'The "%s()" method is deprecated, use "new %s()" instead.', __METHOD__, static::class);
return new static($file, $status, $headers, $public, $contentDisposition, $autoEtag, $autoLastModified);
}
/**
* Sets the file to stream.
*
* @param \SplFileInfo|string $file The file to stream
*
* @return $this
*
* @throws FileException
*/
public function setFile($file, ?string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true)
{
if (!$file instanceof File) {
if ($file instanceof \SplFileInfo) {
$file = new File($file->getPathname());
} else {
$file = new File((string) $file);
}
}
if (!$file->isReadable()) {
throw new FileException('File must be readable.');
}
$this->file = $file;
if ($autoEtag) {
$this->setAutoEtag();
}
if ($autoLastModified) {
$this->setAutoLastModified();
}
if ($contentDisposition) {
$this->setContentDisposition($contentDisposition);
}
return $this;
}
/**
* Gets the file.
*
* @return File
*/
public function getFile()
{
return $this->file;
}
/**
* Sets the response stream chunk size.
*
* @return $this
*/
public function setChunkSize(int $chunkSize): self
{
if ($chunkSize < 1 || $chunkSize > \PHP_INT_MAX) {
throw new \LogicException('The chunk size of a BinaryFileResponse cannot be less than 1 or greater than PHP_INT_MAX.');
}
$this->chunkSize = $chunkSize;
return $this;
}
/**
* Automatically sets the Last-Modified header according the file modification date.
*
* @return $this
*/
public function setAutoLastModified()
{
$this->setLastModified(\DateTime::createFromFormat('U', $this->file->getMTime()));
return $this;
}
/**
* Automatically sets the ETag header according to the checksum of the file.
*
* @return $this
*/
public function setAutoEtag()
{
$this->setEtag(base64_encode(hash_file('sha256', $this->file->getPathname(), true)));
return $this;
}
/**
* Sets the Content-Disposition header with the given filename.
*
* @param string $disposition ResponseHeaderBag::DISPOSITION_INLINE or ResponseHeaderBag::DISPOSITION_ATTACHMENT
* @param string $filename Optionally use this UTF-8 encoded filename instead of the real name of the file
* @param string $filenameFallback A fallback filename, containing only ASCII characters. Defaults to an automatically encoded filename
*
* @return $this
*/
public function setContentDisposition(string $disposition, string $filename = '', string $filenameFallback = '')
{
if ('' === $filename) {
$filename = $this->file->getFilename();
}
if ('' === $filenameFallback && (!preg_match('/^[\x20-\x7e]*$/', $filename) || str_contains($filename, '%'))) {
$encoding = mb_detect_encoding($filename, null, true) ?: '8bit';
for ($i = 0, $filenameLength = mb_strlen($filename, $encoding); $i < $filenameLength; ++$i) {
$char = mb_substr($filename, $i, 1, $encoding);
if ('%' === $char || \ord($char) < 32 || \ord($char) > 126) {
$filenameFallback .= '_';
} else {
$filenameFallback .= $char;
}
}
}
$dispositionHeader = $this->headers->makeDisposition($disposition, $filename, $filenameFallback);
$this->headers->set('Content-Disposition', $dispositionHeader);
return $this;
}
/**
* {@inheritdoc}
*/
public function prepare(Request $request)
{
if ($this->isInformational() || $this->isEmpty()) {
parent::prepare($request);
$this->maxlen = 0;
return $this;
}
if (!$this->headers->has('Content-Type')) {
$this->headers->set('Content-Type', $this->file->getMimeType() ?: 'application/octet-stream');
}
parent::prepare($request);
$this->offset = 0;
$this->maxlen = -1;
if (false === $fileSize = $this->file->getSize()) {
return $this;
}
$this->headers->remove('Transfer-Encoding');
$this->headers->set('Content-Length', $fileSize);
if (!$this->headers->has('Accept-Ranges')) {
// Only accept ranges on safe HTTP methods
$this->headers->set('Accept-Ranges', $request->isMethodSafe() ? 'bytes' : 'none');
}
if (self::$trustXSendfileTypeHeader && $request->headers->has('X-Sendfile-Type')) {
// Use X-Sendfile, do not send any content.
$type = $request->headers->get('X-Sendfile-Type');
$path = $this->file->getRealPath();
// Fall back to scheme://path for stream wrapped locations.
if (false === $path) {
$path = $this->file->getPathname();
}
if ('x-accel-redirect' === strtolower($type)) {
// Do X-Accel-Mapping substitutions.
// @link https://github.com/rack/rack/blob/main/lib/rack/sendfile.rb
// @link https://mattbrictson.com/blog/accelerated-rails-downloads
if (!$request->headers->has('X-Accel-Mapping')) {
throw new \LogicException('The "X-Accel-Mapping" header must be set when "X-Sendfile-Type" is set to "X-Accel-Redirect".');
}
$parts = HeaderUtils::split($request->headers->get('X-Accel-Mapping'), ',=');
foreach ($parts as $part) {
[$pathPrefix, $location] = $part;
if (substr($path, 0, \strlen($pathPrefix)) === $pathPrefix) {
$path = $location.substr($path, \strlen($pathPrefix));
// Only set X-Accel-Redirect header if a valid URI can be produced
// as nginx does not serve arbitrary file paths.
$this->headers->set($type, $path);
$this->maxlen = 0;
break;
}
}
} else {
$this->headers->set($type, $path);
$this->maxlen = 0;
}
} elseif ($request->headers->has('Range') && $request->isMethod('GET')) {
// Process the range headers.
if (!$request->headers->has('If-Range') || $this->hasValidIfRangeHeader($request->headers->get('If-Range'))) {
$range = $request->headers->get('Range');
if (str_starts_with($range, 'bytes=')) {
[$start, $end] = explode('-', substr($range, 6), 2) + [1 => 0];
$end = ('' === $end) ? $fileSize - 1 : (int) $end;
if ('' === $start) {
$start = $fileSize - $end;
$end = $fileSize - 1;
} else {
$start = (int) $start;
}
if ($start <= $end) {
$end = min($end, $fileSize - 1);
if ($start < 0 || $start > $end) {
$this->setStatusCode(416);
$this->headers->set('Content-Range', sprintf('bytes */%s', $fileSize));
} elseif ($end - $start < $fileSize - 1) {
$this->maxlen = $end < $fileSize ? $end - $start + 1 : -1;
$this->offset = $start;
$this->setStatusCode(206);
$this->headers->set('Content-Range', sprintf('bytes %s-%s/%s', $start, $end, $fileSize));
$this->headers->set('Content-Length', $end - $start + 1);
}
}
}
}
}
if ($request->isMethod('HEAD')) {
$this->maxlen = 0;
}
return $this;
}
private function hasValidIfRangeHeader(?string $header): bool
{
if ($this->getEtag() === $header) {
return true;
}
if (null === $lastModified = $this->getLastModified()) {
return false;
}
return $lastModified->format('D, d M Y H:i:s').' GMT' === $header;
}
/**
* {@inheritdoc}
*/
public function sendContent()
{
try {
if (!$this->isSuccessful()) {
return parent::sendContent();
}
if (0 === $this->maxlen) {
return $this;
}
$out = fopen('php://output', 'w');
$file = fopen($this->file->getPathname(), 'r');
ignore_user_abort(true);
if (0 !== $this->offset) {
fseek($file, $this->offset);
}
$length = $this->maxlen;
while ($length && !feof($file)) {
$read = $length > $this->chunkSize || 0 > $length ? $this->chunkSize : $length;
if (false === $data = fread($file, $read)) {
break;
}
while ('' !== $data) {
$read = fwrite($out, $data);
if (false === $read || connection_aborted()) {
break 2;
}
if (0 < $length) {
$length -= $read;
}
$data = substr($data, $read);
}
}
fclose($out);
fclose($file);
} finally {
if ($this->deleteFileAfterSend && is_file($this->file->getPathname())) {
unlink($this->file->getPathname());
}
}
return $this;
}
/**
* {@inheritdoc}
*
* @throws \LogicException when the content is not null
*/
public function setContent(?string $content)
{
if (null !== $content) {
throw new \LogicException('The content cannot be set on a BinaryFileResponse instance.');
}
return $this;
}
/**
* {@inheritdoc}
*/
public function getContent()
{
return false;
}
/**
* Trust X-Sendfile-Type header.
*/
public static function trustXSendfileTypeHeader()
{
self::$trustXSendfileTypeHeader = true;
}
/**
* If this is set to true, the file will be unlinked after the request is sent
* Note: If the X-Sendfile header is used, the deleteFileAfterSend setting will not be used.
*
* @return $this
*/
public function deleteFileAfterSend(bool $shouldDelete = true)
{
$this->deleteFileAfterSend = $shouldDelete;
return $this;
}
}

View File

@@ -0,0 +1,296 @@
CHANGELOG
=========
5.4
---
* Deprecate passing `null` as `$requestIp` to `IpUtils::__checkIp()`, `IpUtils::__checkIp4()` or `IpUtils::__checkIp6()`, pass an empty string instead.
* Add the `litespeed_finish_request` method to work with Litespeed
* Deprecate `upload_progress.*` and `url_rewriter.tags` session options
* Allow setting session options via DSN
5.3
---
* Add the `SessionFactory`, `NativeSessionStorageFactory`, `PhpBridgeSessionStorageFactory` and `MockFileSessionStorageFactory` classes
* Calling `Request::getSession()` when there is no available session throws a `SessionNotFoundException`
* Add the `RequestStack::getSession` method
* Deprecate the `NamespacedAttributeBag` class
* Add `ResponseFormatSame` PHPUnit constraint
* Deprecate the `RequestStack::getMasterRequest()` method and add `getMainRequest()` as replacement
5.2.0
-----
* added support for `X-Forwarded-Prefix` header
* added `HeaderUtils::parseQuery()`: it does the same as `parse_str()` but preserves dots in variable names
* added `File::getContent()`
* added ability to use comma separated ip addresses for `RequestMatcher::matchIps()`
* added `Request::toArray()` to parse a JSON request body to an array
* added `RateLimiter\RequestRateLimiterInterface` and `RateLimiter\AbstractRequestRateLimiter`
* deprecated not passing a `Closure` together with `FILTER_CALLBACK` to `ParameterBag::filter()`; wrap your filter in a closure instead.
* Deprecated the `Request::HEADER_X_FORWARDED_ALL` constant, use either `HEADER_X_FORWARDED_FOR | HEADER_X_FORWARDED_HOST | HEADER_X_FORWARDED_PORT | HEADER_X_FORWARDED_PROTO` or `HEADER_X_FORWARDED_AWS_ELB` or `HEADER_X_FORWARDED_TRAEFIK` constants instead.
* Deprecated `BinaryFileResponse::create()`, use `__construct()` instead
5.1.0
-----
* added `Cookie::withValue`, `Cookie::withDomain`, `Cookie::withExpires`,
`Cookie::withPath`, `Cookie::withSecure`, `Cookie::withHttpOnly`,
`Cookie::withRaw`, `Cookie::withSameSite`
* Deprecate `Response::create()`, `JsonResponse::create()`,
`RedirectResponse::create()`, and `StreamedResponse::create()` methods (use
`__construct()` instead)
* added `Request::preferSafeContent()` and `Response::setContentSafe()` to handle "safe" HTTP preference
according to [RFC 8674](https://tools.ietf.org/html/rfc8674)
* made the Mime component an optional dependency
* added `MarshallingSessionHandler`, `IdentityMarshaller`
* made `Session` accept a callback to report when the session is being used
* Add support for all core cache control directives
* Added `Symfony\Component\HttpFoundation\InputBag`
* Deprecated retrieving non-string values using `InputBag::get()`, use `InputBag::all()` if you need access to the collection of values
5.0.0
-----
* made `Cookie` auto-secure and lax by default
* removed classes in the `MimeType` namespace, use the Symfony Mime component instead
* removed method `UploadedFile::getClientSize()` and the related constructor argument
* made `Request::getSession()` throw if the session has not been set before
* removed `Response::HTTP_RESERVED_FOR_WEBDAV_ADVANCED_COLLECTIONS_EXPIRED_PROPOSAL`
* passing a null url when instantiating a `RedirectResponse` is not allowed
4.4.0
-----
* passing arguments to `Request::isMethodSafe()` is deprecated.
* `ApacheRequest` is deprecated, use the `Request` class instead.
* passing a third argument to `HeaderBag::get()` is deprecated, use method `all()` instead
* [BC BREAK] `PdoSessionHandler` with MySQL changed the type of the lifetime column,
make sure to run `ALTER TABLE sessions MODIFY sess_lifetime INTEGER UNSIGNED NOT NULL` to
update your database.
* `PdoSessionHandler` now precalculates the expiry timestamp in the lifetime column,
make sure to run `CREATE INDEX EXPIRY ON sessions (sess_lifetime)` to update your database
to speed up garbage collection of expired sessions.
* added `SessionHandlerFactory` to create session handlers with a DSN
* added `IpUtils::anonymize()` to help with GDPR compliance.
4.3.0
-----
* added PHPUnit constraints: `RequestAttributeValueSame`, `ResponseCookieValueSame`, `ResponseHasCookie`,
`ResponseHasHeader`, `ResponseHeaderSame`, `ResponseIsRedirected`, `ResponseIsSuccessful`, and `ResponseStatusCodeSame`
* deprecated `MimeTypeGuesserInterface` and `ExtensionGuesserInterface` in favor of `Symfony\Component\Mime\MimeTypesInterface`.
* deprecated `MimeType` and `MimeTypeExtensionGuesser` in favor of `Symfony\Component\Mime\MimeTypes`.
* deprecated `FileBinaryMimeTypeGuesser` in favor of `Symfony\Component\Mime\FileBinaryMimeTypeGuesser`.
* deprecated `FileinfoMimeTypeGuesser` in favor of `Symfony\Component\Mime\FileinfoMimeTypeGuesser`.
* added `UrlHelper` that allows to get an absolute URL and a relative path for a given path
4.2.0
-----
* the default value of the "$secure" and "$samesite" arguments of Cookie's constructor
will respectively change from "false" to "null" and from "null" to "lax" in Symfony
5.0, you should define their values explicitly or use "Cookie::create()" instead.
* added `matchPort()` in RequestMatcher
4.1.3
-----
* [BC BREAK] Support for the IIS-only `X_ORIGINAL_URL` and `X_REWRITE_URL`
HTTP headers has been dropped for security reasons.
4.1.0
-----
* Query string normalization uses `parse_str()` instead of custom parsing logic.
* Passing the file size to the constructor of the `UploadedFile` class is deprecated.
* The `getClientSize()` method of the `UploadedFile` class is deprecated. Use `getSize()` instead.
* added `RedisSessionHandler` to use Redis as a session storage
* The `get()` method of the `AcceptHeader` class now takes into account the
`*` and `*/*` default values (if they are present in the Accept HTTP header)
when looking for items.
* deprecated `Request::getSession()` when no session has been set. Use `Request::hasSession()` instead.
* added `CannotWriteFileException`, `ExtensionFileException`, `FormSizeFileException`,
`IniSizeFileException`, `NoFileException`, `NoTmpDirFileException`, `PartialFileException` to
handle failed `UploadedFile`.
* added `MigratingSessionHandler` for migrating between two session handlers without losing sessions
* added `HeaderUtils`.
4.0.0
-----
* the `Request::setTrustedHeaderName()` and `Request::getTrustedHeaderName()`
methods have been removed
* the `Request::HEADER_CLIENT_IP` constant has been removed, use
`Request::HEADER_X_FORWARDED_FOR` instead
* the `Request::HEADER_CLIENT_HOST` constant has been removed, use
`Request::HEADER_X_FORWARDED_HOST` instead
* the `Request::HEADER_CLIENT_PROTO` constant has been removed, use
`Request::HEADER_X_FORWARDED_PROTO` instead
* the `Request::HEADER_CLIENT_PORT` constant has been removed, use
`Request::HEADER_X_FORWARDED_PORT` instead
* checking for cacheable HTTP methods using the `Request::isMethodSafe()`
method (by not passing `false` as its argument) is not supported anymore and
throws a `\BadMethodCallException`
* the `WriteCheckSessionHandler`, `NativeSessionHandler` and `NativeProxy` classes have been removed
* setting session save handlers that do not implement `\SessionHandlerInterface` in
`NativeSessionStorage::setSaveHandler()` is not supported anymore and throws a
`\TypeError`
3.4.0
-----
* implemented PHP 7.0's `SessionUpdateTimestampHandlerInterface` with a new
`AbstractSessionHandler` base class and a new `StrictSessionHandler` wrapper
* deprecated the `WriteCheckSessionHandler`, `NativeSessionHandler` and `NativeProxy` classes
* deprecated setting session save handlers that do not implement `\SessionHandlerInterface` in `NativeSessionStorage::setSaveHandler()`
* deprecated using `MongoDbSessionHandler` with the legacy mongo extension; use it with the mongodb/mongodb package and ext-mongodb instead
* deprecated `MemcacheSessionHandler`; use `MemcachedSessionHandler` instead
3.3.0
-----
* the `Request::setTrustedProxies()` method takes a new `$trustedHeaderSet` argument,
see https://symfony.com/doc/current/deployment/proxies.html for more info,
* deprecated the `Request::setTrustedHeaderName()` and `Request::getTrustedHeaderName()` methods,
* added `File\Stream`, to be passed to `BinaryFileResponse` when the size of the served file is unknown,
disabling `Range` and `Content-Length` handling, switching to chunked encoding instead
* added the `Cookie::fromString()` method that allows to create a cookie from a
raw header string
3.1.0
-----
* Added support for creating `JsonResponse` with a string of JSON data
3.0.0
-----
* The precedence of parameters returned from `Request::get()` changed from "GET, PATH, BODY" to "PATH, GET, BODY"
2.8.0
-----
* Finding deep items in `ParameterBag::get()` is deprecated since version 2.8 and
will be removed in 3.0.
2.6.0
-----
* PdoSessionHandler changes
- implemented different session locking strategies to prevent loss of data by concurrent access to the same session
- [BC BREAK] save session data in a binary column without base64_encode
- [BC BREAK] added lifetime column to the session table which allows to have different lifetimes for each session
- implemented lazy connections that are only opened when a session is used by either passing a dsn string
explicitly or falling back to session.save_path ini setting
- added a createTable method that initializes a correctly defined table depending on the database vendor
2.5.0
-----
* added `JsonResponse::setEncodingOptions()` & `JsonResponse::getEncodingOptions()` for easier manipulation
of the options used while encoding data to JSON format.
2.4.0
-----
* added RequestStack
* added Request::getEncodings()
* added accessors methods to session handlers
2.3.0
-----
* added support for ranges of IPs in trusted proxies
* `UploadedFile::isValid` now returns false if the file was not uploaded via HTTP (in a non-test mode)
* Improved error-handling of `\Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler`
to ensure the supplied PDO handler throws Exceptions on error (as the class expects). Added related test cases
to verify that Exceptions are properly thrown when the PDO queries fail.
2.2.0
-----
* fixed the Request::create() precedence (URI information always take precedence now)
* added Request::getTrustedProxies()
* deprecated Request::isProxyTrusted()
* [BC BREAK] JsonResponse does not turn a top level empty array to an object anymore, use an ArrayObject to enforce objects
* added a IpUtils class to check if an IP belongs to a CIDR
* added Request::getRealMethod() to get the "real" HTTP method (getMethod() returns the "intended" HTTP method)
* disabled _method request parameter support by default (call Request::enableHttpMethodParameterOverride() to
enable it, and Request::getHttpMethodParameterOverride() to check if it is supported)
* Request::splitHttpAcceptHeader() method is deprecated and will be removed in 2.3
* Deprecated Flashbag::count() and \Countable interface, will be removed in 2.3
2.1.0
-----
* added Request::getSchemeAndHttpHost() and Request::getUserInfo()
* added a fluent interface to the Response class
* added Request::isProxyTrusted()
* added JsonResponse
* added a getTargetUrl method to RedirectResponse
* added support for streamed responses
* made Response::prepare() method the place to enforce HTTP specification
* [BC BREAK] moved management of the locale from the Session class to the Request class
* added a generic access to the PHP built-in filter mechanism: ParameterBag::filter()
* made FileBinaryMimeTypeGuesser command configurable
* added Request::getUser() and Request::getPassword()
* added support for the PATCH method in Request
* removed the ContentTypeMimeTypeGuesser class as it is deprecated and never used on PHP 5.3
* added ResponseHeaderBag::makeDisposition() (implements RFC 6266)
* made mimetype to extension conversion configurable
* [BC BREAK] Moved all session related classes and interfaces into own namespace, as
`Symfony\Component\HttpFoundation\Session` and renamed classes accordingly.
Session handlers are located in the subnamespace `Symfony\Component\HttpFoundation\Session\Handler`.
* SessionHandlers must implement `\SessionHandlerInterface` or extend from the
`Symfony\Component\HttpFoundation\Storage\Handler\NativeSessionHandler` base class.
* Added internal storage driver proxy mechanism for forward compatibility with
PHP 5.4 `\SessionHandler` class.
* Added session handlers for custom Memcache, Memcached and Null session save handlers.
* [BC BREAK] Removed `NativeSessionStorage` and replaced with `NativeFileSessionHandler`.
* [BC BREAK] `SessionStorageInterface` methods removed: `write()`, `read()` and
`remove()`. Added `getBag()`, `registerBag()`. The `NativeSessionStorage` class
is a mediator for the session storage internals including the session handlers
which do the real work of participating in the internal PHP session workflow.
* [BC BREAK] Introduced mock implementations of `SessionStorage` to enable unit
and functional testing without starting real PHP sessions. Removed
`ArraySessionStorage`, and replaced with `MockArraySessionStorage` for unit
tests; removed `FilesystemSessionStorage`, and replaced with`MockFileSessionStorage`
for functional tests. These do not interact with global session ini
configuration values, session functions or `$_SESSION` superglobal. This means
they can be configured directly allowing multiple instances to work without
conflicting in the same PHP process.
* [BC BREAK] Removed the `close()` method from the `Session` class, as this is
now redundant.
* Deprecated the following methods from the Session class: `setFlash()`, `setFlashes()`
`getFlash()`, `hasFlash()`, and `removeFlash()`. Use `getFlashBag()` instead
which returns a `FlashBagInterface`.
* `Session->clear()` now only clears session attributes as before it cleared
flash messages and attributes. `Session->getFlashBag()->all()` clears flashes now.
* Session data is now managed by `SessionBagInterface` to better encapsulate
session data.
* Refactored session attribute and flash messages system to their own
`SessionBagInterface` implementations.
* Added `FlashBag`. Flashes expire when retrieved by `get()` or `all()`. This
implementation is ESI compatible.
* Added `AutoExpireFlashBag` (default) to replicate Symfony 2.0.x auto expire
behavior of messages auto expiring after one page page load. Messages must
be retrieved by `get()` or `all()`.
* Added `Symfony\Component\HttpFoundation\Attribute\AttributeBag` to replicate
attributes storage behavior from 2.0.x (default).
* Added `Symfony\Component\HttpFoundation\Attribute\NamespacedAttributeBag` for
namespace session attributes.
* Flash API can stores messages in an array so there may be multiple messages
per flash type. The old `Session` class API remains without BC break as it
will allow single messages as before.
* Added basic session meta-data to the session to record session create time,
last updated time, and the lifetime of the session cookie that was provided
to the client.
* Request::getClientIp() method doesn't take a parameter anymore but bases
itself on the trustProxy parameter.
* Added isMethod() to Request object.
* [BC BREAK] The methods `getPathInfo()`, `getBaseUrl()` and `getBasePath()` of
a `Request` now all return a raw value (vs a urldecoded value before). Any call
to one of these methods must be checked and wrapped in a `rawurldecode()` if
needed.

Some files were not shown because too many files have changed in this diff Show More