From f21ccd78d00cb6881a6342dc246d8f62bdd43fec Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 27 Aug 2021 04:24:41 +0200 Subject: [PATCH 1/5] External libraries: upgrade to Requests 2.0.5 --- src/wp-includes/Requests/Auth.php | 25 +- src/wp-includes/Requests/Auth/Basic.php | 43 +- src/wp-includes/Requests/Autoload.php | 187 +++ src/wp-includes/Requests/Capability.php | 36 + src/wp-includes/Requests/Cookie.php | 217 ++-- src/wp-includes/Requests/Cookie/Jar.php | 125 +- src/wp-includes/Requests/Exception.php | 12 +- .../Requests/Exception/ArgumentCount.php | 47 + .../Requests/Exception/HTTP/429.php | 29 - .../Requests/Exception/{HTTP.php => Http.php} | 21 +- .../{HTTP/304.php => Http/Status304.php} | 10 +- .../{HTTP/305.php => Http/Status305.php} | 10 +- .../{HTTP/306.php => Http/Status306.php} | 10 +- .../{HTTP/400.php => Http/Status400.php} | 10 +- .../{HTTP/401.php => Http/Status401.php} | 10 +- .../{HTTP/402.php => Http/Status402.php} | 10 +- .../{HTTP/403.php => Http/Status403.php} | 10 +- .../{HTTP/404.php => Http/Status404.php} | 10 +- .../{HTTP/405.php => Http/Status405.php} | 10 +- .../{HTTP/406.php => Http/Status406.php} | 10 +- .../{HTTP/407.php => Http/Status407.php} | 10 +- .../{HTTP/408.php => Http/Status408.php} | 10 +- .../{HTTP/409.php => Http/Status409.php} | 10 +- .../{HTTP/410.php => Http/Status410.php} | 10 +- .../{HTTP/411.php => Http/Status411.php} | 10 +- .../{HTTP/412.php => Http/Status412.php} | 10 +- .../{HTTP/413.php => Http/Status413.php} | 10 +- .../{HTTP/414.php => Http/Status414.php} | 10 +- .../{HTTP/415.php => Http/Status415.php} | 10 +- .../{HTTP/416.php => Http/Status416.php} | 10 +- .../{HTTP/417.php => Http/Status417.php} | 10 +- .../{HTTP/418.php => Http/Status418.php} | 16 +- .../{HTTP/428.php => Http/Status428.php} | 16 +- .../Requests/Exception/Http/Status429.php | 35 + .../{HTTP/431.php => Http/Status431.php} | 16 +- .../{HTTP/500.php => Http/Status500.php} | 10 +- .../{HTTP/501.php => Http/Status501.php} | 10 +- .../{HTTP/502.php => Http/Status502.php} | 10 +- .../{HTTP/503.php => Http/Status503.php} | 10 +- .../{HTTP/504.php => Http/Status504.php} | 10 +- .../{HTTP/505.php => Http/Status505.php} | 10 +- .../{HTTP/511.php => Http/Status511.php} | 16 +- .../Unknown.php => Http/StatusUnknown.php} | 17 +- .../Requests/Exception/InvalidArgument.php | 41 + .../Requests/Exception/Transport.php | 16 +- .../Transport/{cURL.php => Curl.php} | 30 +- .../Requests/{Hooker.php => HookManager.php} | 14 +- src/wp-includes/Requests/Hooks.php | 71 +- .../{IDNAEncoder.php => IdnaEncoder.php} | 205 +-- .../Requests/{IPv6.php => Ipv6.php} | 69 +- src/wp-includes/Requests/{IRI.php => Iri.php} | 105 +- src/wp-includes/Requests/Port.php | 75 ++ src/wp-includes/Requests/Proxy.php | 29 +- .../Requests/Proxy/{HTTP.php => Http.php} | 67 +- src/wp-includes/Requests/Requests.php | 1095 +++++++++++++++++ src/wp-includes/Requests/Response.php | 91 +- src/wp-includes/Requests/Response/Headers.php | 80 +- src/wp-includes/Requests/Session.php | 148 ++- src/wp-includes/Requests/{SSL.php => Ssl.php} | 78 +- src/wp-includes/Requests/Transport.php | 30 +- .../Requests/Transport/{cURL.php => Curl.php} | 223 ++-- .../{fsockopen.php => Fsockopen.php} | 218 ++-- .../Utility/CaseInsensitiveDictionary.php | 86 +- .../Requests/Utility/FilteredIterator.php | 55 +- .../Requests/Utility/InputValidator.php | 109 ++ src/wp-includes/class-requests.php | 997 +-------------- 66 files changed, 3178 insertions(+), 1882 deletions(-) create mode 100644 src/wp-includes/Requests/Autoload.php create mode 100644 src/wp-includes/Requests/Capability.php create mode 100644 src/wp-includes/Requests/Exception/ArgumentCount.php delete mode 100644 src/wp-includes/Requests/Exception/HTTP/429.php rename src/wp-includes/Requests/Exception/{HTTP.php => Http.php} (74%) rename src/wp-includes/Requests/Exception/{HTTP/304.php => Http/Status304.php} (61%) rename src/wp-includes/Requests/Exception/{HTTP/305.php => Http/Status305.php} (60%) rename src/wp-includes/Requests/Exception/{HTTP/306.php => Http/Status306.php} (61%) rename src/wp-includes/Requests/Exception/{HTTP/400.php => Http/Status400.php} (60%) rename src/wp-includes/Requests/Exception/{HTTP/401.php => Http/Status401.php} (61%) rename src/wp-includes/Requests/Exception/{HTTP/402.php => Http/Status402.php} (62%) rename src/wp-includes/Requests/Exception/{HTTP/403.php => Http/Status403.php} (60%) rename src/wp-includes/Requests/Exception/{HTTP/404.php => Http/Status404.php} (60%) rename src/wp-includes/Requests/Exception/{HTTP/405.php => Http/Status405.php} (62%) rename src/wp-includes/Requests/Exception/{HTTP/406.php => Http/Status406.php} (61%) rename src/wp-includes/Requests/Exception/{HTTP/407.php => Http/Status407.php} (64%) rename src/wp-includes/Requests/Exception/{HTTP/408.php => Http/Status408.php} (61%) rename src/wp-includes/Requests/Exception/{HTTP/409.php => Http/Status409.php} (60%) rename src/wp-includes/Requests/Exception/{HTTP/410.php => Http/Status410.php} (58%) rename src/wp-includes/Requests/Exception/{HTTP/411.php => Http/Status411.php} (61%) rename src/wp-includes/Requests/Exception/{HTTP/412.php => Http/Status412.php} (62%) rename src/wp-includes/Requests/Exception/{HTTP/413.php => Http/Status413.php} (63%) rename src/wp-includes/Requests/Exception/{HTTP/414.php => Http/Status414.php} (63%) rename src/wp-includes/Requests/Exception/{HTTP/415.php => Http/Status415.php} (63%) rename src/wp-includes/Requests/Exception/{HTTP/416.php => Http/Status416.php} (65%) rename src/wp-includes/Requests/Exception/{HTTP/417.php => Http/Status417.php} (62%) rename src/wp-includes/Requests/Exception/{HTTP/418.php => Http/Status418.php} (50%) rename src/wp-includes/Requests/Exception/{HTTP/428.php => Http/Status428.php} (52%) create mode 100644 src/wp-includes/Requests/Exception/Http/Status429.php rename src/wp-includes/Requests/Exception/{HTTP/431.php => Http/Status431.php} (55%) rename src/wp-includes/Requests/Exception/{HTTP/500.php => Http/Status500.php} (63%) rename src/wp-includes/Requests/Exception/{HTTP/501.php => Http/Status501.php} (61%) rename src/wp-includes/Requests/Exception/{HTTP/502.php => Http/Status502.php} (60%) rename src/wp-includes/Requests/Exception/{HTTP/503.php => Http/Status503.php} (62%) rename src/wp-includes/Requests/Exception/{HTTP/504.php => Http/Status504.php} (61%) rename src/wp-includes/Requests/Exception/{HTTP/505.php => Http/Status505.php} (64%) rename src/wp-includes/Requests/Exception/{HTTP/511.php => Http/Status511.php} (55%) rename src/wp-includes/Requests/Exception/{HTTP/Unknown.php => Http/StatusUnknown.php} (61%) create mode 100644 src/wp-includes/Requests/Exception/InvalidArgument.php rename src/wp-includes/Requests/Exception/Transport/{cURL.php => Curl.php} (56%) rename src/wp-includes/Requests/{Hooker.php => HookManager.php} (66%) rename src/wp-includes/Requests/{IDNAEncoder.php => IdnaEncoder.php} (60%) rename src/wp-includes/Requests/{IPv6.php => Ipv6.php} (79%) rename src/wp-includes/Requests/{IRI.php => Iri.php} (91%) create mode 100644 src/wp-includes/Requests/Port.php rename src/wp-includes/Requests/Proxy/{HTTP.php => Http.php} (56%) create mode 100644 src/wp-includes/Requests/Requests.php rename src/wp-includes/Requests/{SSL.php => Ssl.php} (55%) rename src/wp-includes/Requests/Transport/{cURL.php => Curl.php} (69%) rename src/wp-includes/Requests/Transport/{fsockopen.php => Fsockopen.php} (59%) create mode 100644 src/wp-includes/Requests/Utility/InputValidator.php diff --git a/src/wp-includes/Requests/Auth.php b/src/wp-includes/Requests/Auth.php index 914c7449ca601..63ebb0833377e 100644 --- a/src/wp-includes/Requests/Auth.php +++ b/src/wp-includes/Requests/Auth.php @@ -2,10 +2,13 @@ /** * Authentication provider interface * - * @package Requests - * @subpackage Authentication + * @package Requests\Authentication */ +namespace WpOrg\Requests; + +use WpOrg\Requests\Hooks; + /** * Authentication provider interface * @@ -14,20 +17,20 @@ * Parameters should be passed via the constructor where possible, as this * makes it much easier for users to use your provider. * - * @see Requests_Hooks - * @package Requests - * @subpackage Authentication + * @see \WpOrg\Requests\Hooks + * + * @package Requests\Authentication */ -interface Requests_Auth { +interface Auth { /** * Register hooks as needed * - * This method is called in {@see Requests::request} when the user has set - * an instance as the 'auth' option. Use this callback to register all the + * This method is called in {@see \WpOrg\Requests\Requests::request()} when the user + * has set an instance as the 'auth' option. Use this callback to register all the * hooks you'll need. * - * @see Requests_Hooks::register - * @param Requests_Hooks $hooks Hook system + * @see \WpOrg\Requests\Hooks::register() + * @param \WpOrg\Requests\Hooks $hooks Hook system */ - public function register(Requests_Hooks $hooks); + public function register(Hooks $hooks); } diff --git a/src/wp-includes/Requests/Auth/Basic.php b/src/wp-includes/Requests/Auth/Basic.php index 68d15bd5db2a6..8ea0eb5538ce9 100644 --- a/src/wp-includes/Requests/Auth/Basic.php +++ b/src/wp-includes/Requests/Auth/Basic.php @@ -2,20 +2,25 @@ /** * Basic Authentication provider * - * @package Requests - * @subpackage Authentication + * @package Requests\Authentication */ +namespace WpOrg\Requests\Auth; + +use WpOrg\Requests\Auth; +use WpOrg\Requests\Exception\ArgumentCount; +use WpOrg\Requests\Exception\InvalidArgument; +use WpOrg\Requests\Hooks; + /** * Basic Authentication provider * * Provides a handler for Basic HTTP authentication via the Authorization * header. * - * @package Requests - * @subpackage Authentication + * @package Requests\Authentication */ -class Requests_Auth_Basic implements Requests_Auth { +class Basic implements Auth { /** * Username * @@ -33,35 +38,45 @@ class Requests_Auth_Basic implements Requests_Auth { /** * Constructor * - * @throws Requests_Exception On incorrect number of arguments (`authbasicbadargs`) + * @since 2.0 Throws an `InvalidArgument` exception. + * @since 2.0 Throws an `ArgumentCount` exception instead of the Requests base `Exception. + * * @param array|null $args Array of user and password. Must have exactly two elements + * + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not an array or null. + * @throws \WpOrg\Requests\Exception\ArgumentCount On incorrect number of array elements (`authbasicbadargs`). */ public function __construct($args = null) { if (is_array($args)) { if (count($args) !== 2) { - throw new Requests_Exception('Invalid number of arguments', 'authbasicbadargs'); + throw ArgumentCount::create('an array with exactly two elements', count($args), 'authbasicbadargs'); } list($this->user, $this->pass) = $args; + return; + } + + if ($args !== null) { + throw InvalidArgument::create(1, '$args', 'array|null', gettype($args)); } } /** * Register the necessary callbacks * - * @see curl_before_send - * @see fsockopen_header - * @param Requests_Hooks $hooks Hook system + * @see \WpOrg\Requests\Auth\Basic::curl_before_send() + * @see \WpOrg\Requests\Auth\Basic::fsockopen_header() + * @param \WpOrg\Requests\Hooks $hooks Hook system */ - public function register(Requests_Hooks $hooks) { - $hooks->register('curl.before_send', array($this, 'curl_before_send')); - $hooks->register('fsockopen.after_headers', array($this, 'fsockopen_header')); + public function register(Hooks $hooks) { + $hooks->register('curl.before_send', [$this, 'curl_before_send']); + $hooks->register('fsockopen.after_headers', [$this, 'fsockopen_header']); } /** * Set cURL parameters before the data is sent * - * @param resource $handle cURL resource + * @param resource|\CurlHandle $handle cURL handle */ public function curl_before_send(&$handle) { curl_setopt($handle, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); diff --git a/src/wp-includes/Requests/Autoload.php b/src/wp-includes/Requests/Autoload.php new file mode 100644 index 0000000000000..26dd280ee8ab0 --- /dev/null +++ b/src/wp-includes/Requests/Autoload.php @@ -0,0 +1,187 @@ + '\WpOrg\Requests\Auth', + 'requests_hooker' => '\WpOrg\Requests\HookManager', + 'requests_proxy' => '\WpOrg\Requests\Proxy', + 'requests_transport' => '\WpOrg\Requests\Transport', + + // Classes. + 'requests_cookie' => '\WpOrg\Requests\Cookie', + 'requests_exception' => '\WpOrg\Requests\Exception', + 'requests_hooks' => '\WpOrg\Requests\Hooks', + 'requests_idnaencoder' => '\WpOrg\Requests\IdnaEncoder', + 'requests_ipv6' => '\WpOrg\Requests\Ipv6', + 'requests_iri' => '\WpOrg\Requests\Iri', + 'requests_response' => '\WpOrg\Requests\Response', + 'requests_session' => '\WpOrg\Requests\Session', + 'requests_ssl' => '\WpOrg\Requests\Ssl', + 'requests_auth_basic' => '\WpOrg\Requests\Auth\Basic', + 'requests_cookie_jar' => '\WpOrg\Requests\Cookie\Jar', + 'requests_proxy_http' => '\WpOrg\Requests\Proxy\Http', + 'requests_response_headers' => '\WpOrg\Requests\Response\Headers', + 'requests_transport_curl' => '\WpOrg\Requests\Transport\Curl', + 'requests_transport_fsockopen' => '\WpOrg\Requests\Transport\Fsockopen', + 'requests_utility_caseinsensitivedictionary' => '\WpOrg\Requests\Utility\CaseInsensitiveDictionary', + 'requests_utility_filterediterator' => '\WpOrg\Requests\Utility\FilteredIterator', + 'requests_exception_http' => '\WpOrg\Requests\Exception\Http', + 'requests_exception_transport' => '\WpOrg\Requests\Exception\Transport', + 'requests_exception_transport_curl' => '\WpOrg\Requests\Exception\Transport\Curl', + 'requests_exception_http_304' => '\WpOrg\Requests\Exception\Http\Status304', + 'requests_exception_http_305' => '\WpOrg\Requests\Exception\Http\Status305', + 'requests_exception_http_306' => '\WpOrg\Requests\Exception\Http\Status306', + 'requests_exception_http_400' => '\WpOrg\Requests\Exception\Http\Status400', + 'requests_exception_http_401' => '\WpOrg\Requests\Exception\Http\Status401', + 'requests_exception_http_402' => '\WpOrg\Requests\Exception\Http\Status402', + 'requests_exception_http_403' => '\WpOrg\Requests\Exception\Http\Status403', + 'requests_exception_http_404' => '\WpOrg\Requests\Exception\Http\Status404', + 'requests_exception_http_405' => '\WpOrg\Requests\Exception\Http\Status405', + 'requests_exception_http_406' => '\WpOrg\Requests\Exception\Http\Status406', + 'requests_exception_http_407' => '\WpOrg\Requests\Exception\Http\Status407', + 'requests_exception_http_408' => '\WpOrg\Requests\Exception\Http\Status408', + 'requests_exception_http_409' => '\WpOrg\Requests\Exception\Http\Status409', + 'requests_exception_http_410' => '\WpOrg\Requests\Exception\Http\Status410', + 'requests_exception_http_411' => '\WpOrg\Requests\Exception\Http\Status411', + 'requests_exception_http_412' => '\WpOrg\Requests\Exception\Http\Status412', + 'requests_exception_http_413' => '\WpOrg\Requests\Exception\Http\Status413', + 'requests_exception_http_414' => '\WpOrg\Requests\Exception\Http\Status414', + 'requests_exception_http_415' => '\WpOrg\Requests\Exception\Http\Status415', + 'requests_exception_http_416' => '\WpOrg\Requests\Exception\Http\Status416', + 'requests_exception_http_417' => '\WpOrg\Requests\Exception\Http\Status417', + 'requests_exception_http_418' => '\WpOrg\Requests\Exception\Http\Status418', + 'requests_exception_http_428' => '\WpOrg\Requests\Exception\Http\Status428', + 'requests_exception_http_429' => '\WpOrg\Requests\Exception\Http\Status429', + 'requests_exception_http_431' => '\WpOrg\Requests\Exception\Http\Status431', + 'requests_exception_http_500' => '\WpOrg\Requests\Exception\Http\Status500', + 'requests_exception_http_501' => '\WpOrg\Requests\Exception\Http\Status501', + 'requests_exception_http_502' => '\WpOrg\Requests\Exception\Http\Status502', + 'requests_exception_http_503' => '\WpOrg\Requests\Exception\Http\Status503', + 'requests_exception_http_504' => '\WpOrg\Requests\Exception\Http\Status504', + 'requests_exception_http_505' => '\WpOrg\Requests\Exception\Http\Status505', + 'requests_exception_http_511' => '\WpOrg\Requests\Exception\Http\Status511', + 'requests_exception_http_unknown' => '\WpOrg\Requests\Exception\Http\StatusUnknown', + ]; + + /** + * Register the autoloader. + * + * Note: the autoloader is *prepended* in the autoload queue. + * This is done to ensure that the Requests 2.0 autoloader takes precedence + * over a potentially (dependency-registered) Requests 1.x autoloader. + * + * @internal This method contains a safeguard against the autoloader being + * registered multiple times. This safeguard uses a global constant to + * (hopefully/in most cases) still function correctly, even if the + * class would be renamed. + * + * @return void + */ + public static function register() { + if (defined('REQUESTS_AUTOLOAD_REGISTERED') === false) { + spl_autoload_register([self::class, 'load'], true); + define('REQUESTS_AUTOLOAD_REGISTERED', true); + } + } + + /** + * Autoloader. + * + * @param string $class_name Name of the class name to load. + * + * @return bool Whether a class was loaded or not. + */ + public static function load($class_name) { + // Check that the class starts with "Requests" (PSR-0) or "WpOrg\Requests" (PSR-4). + $psr_4_prefix_pos = strpos($class_name, 'WpOrg\\Requests\\'); + + if (stripos($class_name, 'Requests') !== 0 && $psr_4_prefix_pos !== 0) { + return false; + } + + $class_lower = strtolower($class_name); + + if ($class_lower === 'requests') { + // Reference to the original PSR-0 Requests class. + $file = dirname(__DIR__) . '/library/Requests.php'; + } elseif ($psr_4_prefix_pos === 0) { + // PSR-4 classname. + $file = __DIR__ . '/' . strtr(substr($class_name, 15), '\\', '/') . '.php'; + } + + if (isset($file) && file_exists($file)) { + include $file; + return true; + } + + /* + * Okay, so the class starts with "Requests", but we couldn't find the file. + * If this is one of the deprecated/renamed PSR-0 classes being requested, + * let's alias it to the new name and throw a deprecation notice. + */ + if (isset(self::$deprecated_classes[$class_lower])) { + /* + * Integrators who cannot yet upgrade to the PSR-4 class names can silence deprecations + * by defining a `REQUESTS_SILENCE_PSR0_DEPRECATIONS` constant and setting it to `true`. + * The constant needs to be defined before the first deprecated class is requested + * via this autoloader. + */ + if (!defined('REQUESTS_SILENCE_PSR0_DEPRECATIONS') || REQUESTS_SILENCE_PSR0_DEPRECATIONS !== true) { + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error + trigger_error( + 'The PSR-0 `Requests_...` class names in the Request library are deprecated.' + . ' Switch to the PSR-4 `WpOrg\Requests\...` class names at your earliest convenience.', + E_USER_DEPRECATED + ); + + // Prevent the deprecation notice from being thrown twice. + if (!defined('REQUESTS_SILENCE_PSR0_DEPRECATIONS')) { + define('REQUESTS_SILENCE_PSR0_DEPRECATIONS', true); + } + } + + // Create an alias and let the autoloader recursively kick in to load the PSR-4 class. + return class_alias(self::$deprecated_classes[$class_lower], $class_name, true); + } + + return false; + } + } +} diff --git a/src/wp-includes/Requests/Capability.php b/src/wp-includes/Requests/Capability.php new file mode 100644 index 0000000000000..87b8340a34e92 --- /dev/null +++ b/src/wp-includes/Requests/Capability.php @@ -0,0 +1,36 @@ + + */ + const ALL = [ + self::SSL, + ]; +} diff --git a/src/wp-includes/Requests/Cookie.php b/src/wp-includes/Requests/Cookie.php index 7dd5b6822d347..ccbbc73dbc1c1 100644 --- a/src/wp-includes/Requests/Cookie.php +++ b/src/wp-includes/Requests/Cookie.php @@ -2,17 +2,23 @@ /** * Cookie storage object * - * @package Requests - * @subpackage Cookies + * @package Requests\Cookies */ +namespace WpOrg\Requests; + +use WpOrg\Requests\Exception\InvalidArgument; +use WpOrg\Requests\Iri; +use WpOrg\Requests\Response\Headers; +use WpOrg\Requests\Utility\CaseInsensitiveDictionary; +use WpOrg\Requests\Utility\InputValidator; + /** * Cookie storage object * - * @package Requests - * @subpackage Cookies + * @package Requests\Cookies */ -class Requests_Cookie { +class Cookie { /** * Cookie name. * @@ -33,9 +39,9 @@ class Requests_Cookie { * Valid keys are (currently) path, domain, expires, max-age, secure and * httponly. * - * @var Requests_Utility_CaseInsensitiveDictionary|array Array-like object + * @var \WpOrg\Requests\Utility\CaseInsensitiveDictionary|array Array-like object */ - public $attributes = array(); + public $attributes = []; /** * Cookie flags @@ -45,7 +51,7 @@ class Requests_Cookie { * * @var array */ - public $flags = array(); + public $flags = []; /** * Reference time for relative calculations @@ -62,18 +68,46 @@ class Requests_Cookie { * * @param string $name * @param string $value - * @param array|Requests_Utility_CaseInsensitiveDictionary $attributes Associative array of attribute data + * @param array|\WpOrg\Requests\Utility\CaseInsensitiveDictionary $attributes Associative array of attribute data + * @param array $flags + * @param int|null $reference_time + * + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $name argument is not a string. + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $value argument is not a string. + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $attributes argument is not an array or iterable object with array access. + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $flags argument is not an array. + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $reference_time argument is not an integer or null. */ - public function __construct($name, $value, $attributes = array(), $flags = array(), $reference_time = null) { + public function __construct($name, $value, $attributes = [], $flags = [], $reference_time = null) { + if (is_string($name) === false) { + throw InvalidArgument::create(1, '$name', 'string', gettype($name)); + } + + if (is_string($value) === false) { + throw InvalidArgument::create(2, '$value', 'string', gettype($value)); + } + + if (InputValidator::has_array_access($attributes) === false || InputValidator::is_iterable($attributes) === false) { + throw InvalidArgument::create(3, '$attributes', 'array|ArrayAccess&Traversable', gettype($attributes)); + } + + if (is_array($flags) === false) { + throw InvalidArgument::create(4, '$flags', 'array', gettype($flags)); + } + + if ($reference_time !== null && is_int($reference_time) === false) { + throw InvalidArgument::create(5, '$reference_time', 'integer|null', gettype($reference_time)); + } + $this->name = $name; $this->value = $value; $this->attributes = $attributes; - $default_flags = array( + $default_flags = [ 'creation' => time(), 'last-access' => time(), 'persistent' => false, 'host-only' => true, - ); + ]; $this->flags = array_merge($default_flags, $flags); $this->reference_time = time(); @@ -84,6 +118,15 @@ public function __construct($name, $value, $attributes = array(), $flags = array $this->normalize(); } + /** + * Get the cookie value + * + * Attributes and other data can be accessed via methods. + */ + public function __toString() { + return $this->value; + } + /** * Check if a cookie is expired. * @@ -113,10 +156,10 @@ public function is_expired() { /** * Check if a cookie is valid for a given URI * - * @param Requests_IRI $uri URI to check + * @param \WpOrg\Requests\Iri $uri URI to check * @return boolean Whether the cookie is valid for the given URI */ - public function uri_matches(Requests_IRI $uri) { + public function uri_matches(Iri $uri) { if (!$this->domain_matches($uri->host)) { return false; } @@ -131,19 +174,23 @@ public function uri_matches(Requests_IRI $uri) { /** * Check if a cookie is valid for a given domain * - * @param string $string Domain to check + * @param string $domain Domain to check * @return boolean Whether the cookie is valid for the given domain */ - public function domain_matches($string) { + public function domain_matches($domain) { + if (is_string($domain) === false) { + return false; + } + if (!isset($this->attributes['domain'])) { // Cookies created manually; cookies created by Requests will set // the domain to the requested domain return true; } - $domain_string = $this->attributes['domain']; - if ($domain_string === $string) { - // The domain string and the string are identical. + $cookie_domain = $this->attributes['domain']; + if ($cookie_domain === $domain) { + // The cookie domain and the passed domain are identical. return true; } @@ -153,26 +200,26 @@ public function domain_matches($string) { return false; } - if (strlen($string) <= strlen($domain_string)) { - // For obvious reasons, the string cannot be a suffix if the domain - // is shorter than the domain string + if (strlen($domain) <= strlen($cookie_domain)) { + // For obvious reasons, the cookie domain cannot be a suffix if the passed domain + // is shorter than the cookie domain return false; } - if (substr($string, -1 * strlen($domain_string)) !== $domain_string) { - // The domain string should be a suffix of the string. + if (substr($domain, -1 * strlen($cookie_domain)) !== $cookie_domain) { + // The cookie domain should be a suffix of the passed domain. return false; } - $prefix = substr($string, 0, strlen($string) - strlen($domain_string)); + $prefix = substr($domain, 0, strlen($domain) - strlen($cookie_domain)); if (substr($prefix, -1) !== '.') { - // The last character of the string that is not included in the + // The last character of the passed domain that is not included in the // domain string should be a %x2E (".") character. return false; } - // The string should be a host name (i.e., not an IP address). - return !preg_match('#^(.+\.)\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$#', $string); + // The passed domain should be a host name (i.e., not an IP address). + return !preg_match('#^(.+\.)\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$#', $domain); } /** @@ -195,6 +242,10 @@ public function path_matches($request_path) { return true; } + if (is_scalar($request_path) === false) { + return false; + } + $cookie_path = $this->attributes['path']; if ($cookie_path === $request_path) { @@ -280,8 +331,7 @@ protected function normalize_attribute($name, $value) { $delta_seconds = (int) $value; if ($delta_seconds <= 0) { $expiry_time = 0; - } - else { + } else { $expiry_time = $this->reference_time + $delta_seconds; } @@ -316,17 +366,6 @@ public function format_for_header() { return sprintf('%s=%s', $this->name, $this->value); } - /** - * Format a cookie for a Cookie header - * - * @codeCoverageIgnore - * @deprecated Use {@see Requests_Cookie::format_for_header} - * @return string - */ - public function formatForHeader() { - return $this->format_for_header(); - } - /** * Format a cookie for a Set-Cookie header * @@ -338,40 +377,20 @@ public function formatForHeader() { public function format_for_set_cookie() { $header_value = $this->format_for_header(); if (!empty($this->attributes)) { - $parts = array(); + $parts = []; foreach ($this->attributes as $key => $value) { // Ignore non-associative attributes if (is_numeric($key)) { $parts[] = $value; - } - else { + } else { $parts[] = sprintf('%s=%s', $key, $value); } } $header_value .= '; ' . implode('; ', $parts); } - return $header_value; - } - /** - * Format a cookie for a Set-Cookie header - * - * @codeCoverageIgnore - * @deprecated Use {@see Requests_Cookie::format_for_set_cookie} - * @return string - */ - public function formatForSetCookie() { - return $this->format_for_set_cookie(); - } - - /** - * Get the cookie value - * - * Attributes and other data can be accessed via methods. - */ - public function __toString() { - return $this->value; + return $header_value; } /** @@ -381,17 +400,29 @@ public function __toString() { * is an intentional deviation from RFC 2109 and RFC 2616. RFC 6265 * specifies some of this handling, but not in a thorough manner. * - * @param string Cookie header value (from a Set-Cookie header) - * @return Requests_Cookie Parsed cookie object + * @param string $cookie_header Cookie header value (from a Set-Cookie header) + * @param string $name + * @param int|null $reference_time + * @return \WpOrg\Requests\Cookie Parsed cookie object + * + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $cookie_header argument is not a string. + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $name argument is not a string. */ - public static function parse($string, $name = '', $reference_time = null) { - $parts = explode(';', $string); + public static function parse($cookie_header, $name = '', $reference_time = null) { + if (is_string($cookie_header) === false) { + throw InvalidArgument::create(1, '$cookie_header', 'string', gettype($cookie_header)); + } + + if (is_string($name) === false) { + throw InvalidArgument::create(2, '$name', 'string', gettype($name)); + } + + $parts = explode(';', $cookie_header); $kvparts = array_shift($parts); if (!empty($name)) { - $value = $string; - } - elseif (strpos($kvparts, '=') === false) { + $value = $cookie_header; + } elseif (strpos($kvparts, '=') === false) { // Some sites might only have a value without the equals separator. // Deviate from RFC 6265 and pretend it was actually a blank name // (`=foo`) @@ -399,23 +430,22 @@ public static function parse($string, $name = '', $reference_time = null) { // https://bugzilla.mozilla.org/show_bug.cgi?id=169091 $name = ''; $value = $kvparts; - } - else { + } else { list($name, $value) = explode('=', $kvparts, 2); } + $name = trim($name); $value = trim($value); - // Attribute key are handled case-insensitively - $attributes = new Requests_Utility_CaseInsensitiveDictionary(); + // Attribute keys are handled case-insensitively + $attributes = new CaseInsensitiveDictionary(); if (!empty($parts)) { foreach ($parts as $part) { if (strpos($part, '=') === false) { $part_key = $part; $part_value = true; - } - else { + } else { list($part_key, $part_value) = explode('=', $part, 2); $part_value = trim($part_value); } @@ -425,24 +455,24 @@ public static function parse($string, $name = '', $reference_time = null) { } } - return new Requests_Cookie($name, $value, $attributes, array(), $reference_time); + return new static($name, $value, $attributes, [], $reference_time); } /** * Parse all Set-Cookie headers from request headers * - * @param Requests_Response_Headers $headers Headers to parse from - * @param Requests_IRI|null $origin URI for comparing cookie origins + * @param \WpOrg\Requests\Response\Headers $headers Headers to parse from + * @param \WpOrg\Requests\Iri|null $origin URI for comparing cookie origins * @param int|null $time Reference time for expiration calculation * @return array */ - public static function parse_from_headers(Requests_Response_Headers $headers, Requests_IRI $origin = null, $time = null) { + public static function parse_from_headers(Headers $headers, Iri $origin = null, $time = null) { $cookie_headers = $headers->getValues('Set-Cookie'); if (empty($cookie_headers)) { - return array(); + return []; } - $cookies = array(); + $cookies = []; foreach ($cookie_headers as $header) { $parsed = self::parse($header, '', $time); @@ -450,8 +480,7 @@ public static function parse_from_headers(Requests_Response_Headers $headers, Re if (empty($parsed->attributes['domain']) && !empty($origin)) { $parsed->attributes['domain'] = $origin->host; $parsed->flags['host-only'] = true; - } - else { + } else { $parsed->flags['host-only'] = false; } @@ -465,19 +494,18 @@ public static function parse_from_headers(Requests_Response_Headers $headers, Re // the uri-path is not a %x2F ("/") character, output // %x2F ("/") and skip the remaining steps. $path = '/'; - } - elseif (substr_count($path, '/') === 1) { + } elseif (substr_count($path, '/') === 1) { // If the uri-path contains no more than one %x2F ("/") // character, output %x2F ("/") and skip the remaining // step. $path = '/'; - } - else { + } else { // Output the characters of the uri-path from the first // character up to, but not including, the right-most // %x2F ("/"). $path = substr($path, 0, strrpos($path, '/')); } + $parsed->attributes['path'] = $path; } @@ -491,15 +519,4 @@ public static function parse_from_headers(Requests_Response_Headers $headers, Re return $cookies; } - - /** - * Parse all Set-Cookie headers from request headers - * - * @codeCoverageIgnore - * @deprecated Use {@see Requests_Cookie::parse_from_headers} - * @return array - */ - public static function parseFromHeaders(Requests_Response_Headers $headers) { - return self::parse_from_headers($headers); - } } diff --git a/src/wp-includes/Requests/Cookie/Jar.php b/src/wp-includes/Requests/Cookie/Jar.php index a816f90a0ae7f..dfbb8b739bd99 100644 --- a/src/wp-includes/Requests/Cookie/Jar.php +++ b/src/wp-includes/Requests/Cookie/Jar.php @@ -2,112 +2,123 @@ /** * Cookie holder object * - * @package Requests - * @subpackage Cookies + * @package Requests\Cookies */ +namespace WpOrg\Requests\Cookie; + +use ArrayAccess; +use ArrayIterator; +use IteratorAggregate; +use ReturnTypeWillChange; +use WpOrg\Requests\Cookie; +use WpOrg\Requests\Exception; +use WpOrg\Requests\Exception\InvalidArgument; +use WpOrg\Requests\HookManager; +use WpOrg\Requests\Iri; +use WpOrg\Requests\Response; + /** * Cookie holder object * - * @package Requests - * @subpackage Cookies + * @package Requests\Cookies */ -class Requests_Cookie_Jar implements ArrayAccess, IteratorAggregate { +class Jar implements ArrayAccess, IteratorAggregate { /** * Actual item data * * @var array */ - protected $cookies = array(); + protected $cookies = []; /** * Create a new jar * * @param array $cookies Existing cookie values + * + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not an array. */ - public function __construct($cookies = array()) { + public function __construct($cookies = []) { + if (is_array($cookies) === false) { + throw InvalidArgument::create(1, '$cookies', 'array', gettype($cookies)); + } + $this->cookies = $cookies; } /** - * Normalise cookie data into a Requests_Cookie + * Normalise cookie data into a \WpOrg\Requests\Cookie * - * @param string|Requests_Cookie $cookie - * @return Requests_Cookie + * @param string|\WpOrg\Requests\Cookie $cookie + * @return \WpOrg\Requests\Cookie */ - public function normalize_cookie($cookie, $key = null) { - if ($cookie instanceof Requests_Cookie) { + public function normalize_cookie($cookie, $key = '') { + if ($cookie instanceof Cookie) { return $cookie; } - return Requests_Cookie::parse($cookie, $key); - } - - /** - * Normalise cookie data into a Requests_Cookie - * - * @codeCoverageIgnore - * @deprecated Use {@see Requests_Cookie_Jar::normalize_cookie} - * @return Requests_Cookie - */ - public function normalizeCookie($cookie, $key = null) { - return $this->normalize_cookie($cookie, $key); + return Cookie::parse($cookie, $key); } /** * Check if the given item exists * - * @param string $key Item key + * @param string $offset Item key * @return boolean Does the item exist? */ - public function offsetExists($key) { - return isset($this->cookies[$key]); + #[ReturnTypeWillChange] + public function offsetExists($offset) { + return isset($this->cookies[$offset]); } /** * Get the value for the item * - * @param string $key Item key + * @param string $offset Item key * @return string|null Item value (null if offsetExists is false) */ - public function offsetGet($key) { - if (!isset($this->cookies[$key])) { + #[ReturnTypeWillChange] + public function offsetGet($offset) { + if (!isset($this->cookies[$offset])) { return null; } - return $this->cookies[$key]; + return $this->cookies[$offset]; } /** * Set the given item * - * @throws Requests_Exception On attempting to use dictionary as list (`invalidset`) - * - * @param string $key Item name + * @param string $offset Item name * @param string $value Item value + * + * @throws \WpOrg\Requests\Exception On attempting to use dictionary as list (`invalidset`) */ - public function offsetSet($key, $value) { - if ($key === null) { - throw new Requests_Exception('Object is a dictionary, not a list', 'invalidset'); + #[ReturnTypeWillChange] + public function offsetSet($offset, $value) { + if ($offset === null) { + throw new Exception('Object is a dictionary, not a list', 'invalidset'); } - $this->cookies[$key] = $value; + $this->cookies[$offset] = $value; } /** * Unset the given header * - * @param string $key + * @param string $offset */ - public function offsetUnset($key) { - unset($this->cookies[$key]); + #[ReturnTypeWillChange] + public function offsetUnset($offset) { + unset($this->cookies[$offset]); } /** * Get an iterator for the data * - * @return ArrayIterator + * @return \ArrayIterator */ + #[ReturnTypeWillChange] public function getIterator() { return new ArrayIterator($this->cookies); } @@ -115,11 +126,11 @@ public function getIterator() { /** * Register the cookie handler with the request's hooking system * - * @param Requests_Hooker $hooks Hooking system + * @param \WpOrg\Requests\HookManager $hooks Hooking system */ - public function register(Requests_Hooker $hooks) { - $hooks->register('requests.before_request', array($this, 'before_request')); - $hooks->register('requests.before_redirect_check', array($this, 'before_redirect_check')); + public function register(HookManager $hooks) { + $hooks->register('requests.before_request', [$this, 'before_request']); + $hooks->register('requests.before_redirect_check', [$this, 'before_redirect_check']); } /** @@ -134,12 +145,12 @@ public function register(Requests_Hooker $hooks) { * @param array $options */ public function before_request($url, &$headers, &$data, &$type, &$options) { - if (!$url instanceof Requests_IRI) { - $url = new Requests_IRI($url); + if (!$url instanceof Iri) { + $url = new Iri($url); } if (!empty($this->cookies)) { - $cookies = array(); + $cookies = []; foreach ($this->cookies as $key => $cookie) { $cookie = $this->normalize_cookie($cookie, $key); @@ -160,16 +171,16 @@ public function before_request($url, &$headers, &$data, &$type, &$options) { /** * Parse all cookies from a response and attach them to the response * - * @var Requests_Response $response + * @param \WpOrg\Requests\Response $response */ - public function before_redirect_check(Requests_Response $return) { - $url = $return->url; - if (!$url instanceof Requests_IRI) { - $url = new Requests_IRI($url); + public function before_redirect_check(Response $response) { + $url = $response->url; + if (!$url instanceof Iri) { + $url = new Iri($url); } - $cookies = Requests_Cookie::parse_from_headers($return->headers, $url); - $this->cookies = array_merge($this->cookies, $cookies); - $return->cookies = $this; + $cookies = Cookie::parse_from_headers($response->headers, $url); + $this->cookies = array_merge($this->cookies, $cookies); + $response->cookies = $this; } } diff --git a/src/wp-includes/Requests/Exception.php b/src/wp-includes/Requests/Exception.php index 1080efd971e7a..b67d1b1a40c70 100644 --- a/src/wp-includes/Requests/Exception.php +++ b/src/wp-includes/Requests/Exception.php @@ -2,15 +2,19 @@ /** * Exception for HTTP requests * - * @package Requests + * @package Requests\Exceptions */ +namespace WpOrg\Requests; + +use Exception as PHPException; + /** * Exception for HTTP requests * - * @package Requests + * @package Requests\Exceptions */ -class Requests_Exception extends Exception { +class Exception extends PHPException { /** * Type of exception * @@ -41,7 +45,7 @@ public function __construct($message, $type, $data = null, $code = 0) { } /** - * Like {@see getCode()}, but a string code. + * Like {@see \Exception::getCode()}, but a string code. * * @codeCoverageIgnore * @return string diff --git a/src/wp-includes/Requests/Exception/ArgumentCount.php b/src/wp-includes/Requests/Exception/ArgumentCount.php new file mode 100644 index 0000000000000..b5773ddf3345e --- /dev/null +++ b/src/wp-includes/Requests/Exception/ArgumentCount.php @@ -0,0 +1,47 @@ +reason; @@ -58,14 +65,14 @@ public function getReason() { */ public static function get_class($code) { if (!$code) { - return 'Requests_Exception_HTTP_Unknown'; + return StatusUnknown::class; } - $class = sprintf('Requests_Exception_HTTP_%d', $code); + $class = sprintf('\WpOrg\Requests\Exception\Http\Status%d', $code); if (class_exists($class)) { return $class; } - return 'Requests_Exception_HTTP_Unknown'; + return StatusUnknown::class; } } diff --git a/src/wp-includes/Requests/Exception/HTTP/304.php b/src/wp-includes/Requests/Exception/Http/Status304.php similarity index 61% rename from src/wp-includes/Requests/Exception/HTTP/304.php rename to src/wp-includes/Requests/Exception/Http/Status304.php index 2636ba4517d19..d510ae7d4ebbc 100644 --- a/src/wp-includes/Requests/Exception/HTTP/304.php +++ b/src/wp-includes/Requests/Exception/Http/Status304.php @@ -2,15 +2,19 @@ /** * Exception for 304 Not Modified responses * - * @package Requests + * @package Requests\Exceptions */ +namespace WpOrg\Requests\Exception\Http; + +use WpOrg\Requests\Exception\Http; + /** * Exception for 304 Not Modified responses * - * @package Requests + * @package Requests\Exceptions */ -class Requests_Exception_HTTP_304 extends Requests_Exception_HTTP { +final class Status304 extends Http { /** * HTTP status code * diff --git a/src/wp-includes/Requests/Exception/HTTP/305.php b/src/wp-includes/Requests/Exception/Http/Status305.php similarity index 60% rename from src/wp-includes/Requests/Exception/HTTP/305.php rename to src/wp-includes/Requests/Exception/Http/Status305.php index 37d115a81a3ff..8be63c3f21ee3 100644 --- a/src/wp-includes/Requests/Exception/HTTP/305.php +++ b/src/wp-includes/Requests/Exception/Http/Status305.php @@ -2,15 +2,19 @@ /** * Exception for 305 Use Proxy responses * - * @package Requests + * @package Requests\Exceptions */ +namespace WpOrg\Requests\Exception\Http; + +use WpOrg\Requests\Exception\Http; + /** * Exception for 305 Use Proxy responses * - * @package Requests + * @package Requests\Exceptions */ -class Requests_Exception_HTTP_305 extends Requests_Exception_HTTP { +final class Status305 extends Http { /** * HTTP status code * diff --git a/src/wp-includes/Requests/Exception/HTTP/306.php b/src/wp-includes/Requests/Exception/Http/Status306.php similarity index 61% rename from src/wp-includes/Requests/Exception/HTTP/306.php rename to src/wp-includes/Requests/Exception/Http/Status306.php index 743a4ed1d0ea7..2f3534ab9da87 100644 --- a/src/wp-includes/Requests/Exception/HTTP/306.php +++ b/src/wp-includes/Requests/Exception/Http/Status306.php @@ -2,15 +2,19 @@ /** * Exception for 306 Switch Proxy responses * - * @package Requests + * @package Requests\Exceptions */ +namespace WpOrg\Requests\Exception\Http; + +use WpOrg\Requests\Exception\Http; + /** * Exception for 306 Switch Proxy responses * - * @package Requests + * @package Requests\Exceptions */ -class Requests_Exception_HTTP_306 extends Requests_Exception_HTTP { +final class Status306 extends Http { /** * HTTP status code * diff --git a/src/wp-includes/Requests/Exception/HTTP/400.php b/src/wp-includes/Requests/Exception/Http/Status400.php similarity index 60% rename from src/wp-includes/Requests/Exception/HTTP/400.php rename to src/wp-includes/Requests/Exception/Http/Status400.php index 5bd5428c0fd66..4e39623578d63 100644 --- a/src/wp-includes/Requests/Exception/HTTP/400.php +++ b/src/wp-includes/Requests/Exception/Http/Status400.php @@ -2,15 +2,19 @@ /** * Exception for 400 Bad Request responses * - * @package Requests + * @package Requests\Exceptions */ +namespace WpOrg\Requests\Exception\Http; + +use WpOrg\Requests\Exception\Http; + /** * Exception for 400 Bad Request responses * - * @package Requests + * @package Requests\Exceptions */ -class Requests_Exception_HTTP_400 extends Requests_Exception_HTTP { +final class Status400 extends Http { /** * HTTP status code * diff --git a/src/wp-includes/Requests/Exception/HTTP/401.php b/src/wp-includes/Requests/Exception/Http/Status401.php similarity index 61% rename from src/wp-includes/Requests/Exception/HTTP/401.php rename to src/wp-includes/Requests/Exception/Http/Status401.php index 62a283ab09131..2a76429ce7e3b 100644 --- a/src/wp-includes/Requests/Exception/HTTP/401.php +++ b/src/wp-includes/Requests/Exception/Http/Status401.php @@ -2,15 +2,19 @@ /** * Exception for 401 Unauthorized responses * - * @package Requests + * @package Requests\Exceptions */ +namespace WpOrg\Requests\Exception\Http; + +use WpOrg\Requests\Exception\Http; + /** * Exception for 401 Unauthorized responses * - * @package Requests + * @package Requests\Exceptions */ -class Requests_Exception_HTTP_401 extends Requests_Exception_HTTP { +final class Status401 extends Http { /** * HTTP status code * diff --git a/src/wp-includes/Requests/Exception/HTTP/402.php b/src/wp-includes/Requests/Exception/Http/Status402.php similarity index 62% rename from src/wp-includes/Requests/Exception/HTTP/402.php rename to src/wp-includes/Requests/Exception/Http/Status402.php index f287fd4b226d6..09d42879d5fe9 100644 --- a/src/wp-includes/Requests/Exception/HTTP/402.php +++ b/src/wp-includes/Requests/Exception/Http/Status402.php @@ -2,15 +2,19 @@ /** * Exception for 402 Payment Required responses * - * @package Requests + * @package Requests\Exceptions */ +namespace WpOrg\Requests\Exception\Http; + +use WpOrg\Requests\Exception\Http; + /** * Exception for 402 Payment Required responses * - * @package Requests + * @package Requests\Exceptions */ -class Requests_Exception_HTTP_402 extends Requests_Exception_HTTP { +final class Status402 extends Http { /** * HTTP status code * diff --git a/src/wp-includes/Requests/Exception/HTTP/403.php b/src/wp-includes/Requests/Exception/Http/Status403.php similarity index 60% rename from src/wp-includes/Requests/Exception/HTTP/403.php rename to src/wp-includes/Requests/Exception/Http/Status403.php index 2ae1c44459cf2..0b1fc3963dd0b 100644 --- a/src/wp-includes/Requests/Exception/HTTP/403.php +++ b/src/wp-includes/Requests/Exception/Http/Status403.php @@ -2,15 +2,19 @@ /** * Exception for 403 Forbidden responses * - * @package Requests + * @package Requests\Exceptions */ +namespace WpOrg\Requests\Exception\Http; + +use WpOrg\Requests\Exception\Http; + /** * Exception for 403 Forbidden responses * - * @package Requests + * @package Requests\Exceptions */ -class Requests_Exception_HTTP_403 extends Requests_Exception_HTTP { +final class Status403 extends Http { /** * HTTP status code * diff --git a/src/wp-includes/Requests/Exception/HTTP/404.php b/src/wp-includes/Requests/Exception/Http/Status404.php similarity index 60% rename from src/wp-includes/Requests/Exception/HTTP/404.php rename to src/wp-includes/Requests/Exception/Http/Status404.php index e6e28672c3494..ef39a853e3d95 100644 --- a/src/wp-includes/Requests/Exception/HTTP/404.php +++ b/src/wp-includes/Requests/Exception/Http/Status404.php @@ -2,15 +2,19 @@ /** * Exception for 404 Not Found responses * - * @package Requests + * @package Requests\Exceptions */ +namespace WpOrg\Requests\Exception\Http; + +use WpOrg\Requests\Exception\Http; + /** * Exception for 404 Not Found responses * - * @package Requests + * @package Requests\Exceptions */ -class Requests_Exception_HTTP_404 extends Requests_Exception_HTTP { +final class Status404 extends Http { /** * HTTP status code * diff --git a/src/wp-includes/Requests/Exception/HTTP/405.php b/src/wp-includes/Requests/Exception/Http/Status405.php similarity index 62% rename from src/wp-includes/Requests/Exception/HTTP/405.php rename to src/wp-includes/Requests/Exception/Http/Status405.php index 0461e6108cbd8..666fbfe7c70ef 100644 --- a/src/wp-includes/Requests/Exception/HTTP/405.php +++ b/src/wp-includes/Requests/Exception/Http/Status405.php @@ -2,15 +2,19 @@ /** * Exception for 405 Method Not Allowed responses * - * @package Requests + * @package Requests\Exceptions */ +namespace WpOrg\Requests\Exception\Http; + +use WpOrg\Requests\Exception\Http; + /** * Exception for 405 Method Not Allowed responses * - * @package Requests + * @package Requests\Exceptions */ -class Requests_Exception_HTTP_405 extends Requests_Exception_HTTP { +final class Status405 extends Http { /** * HTTP status code * diff --git a/src/wp-includes/Requests/Exception/HTTP/406.php b/src/wp-includes/Requests/Exception/Http/Status406.php similarity index 61% rename from src/wp-includes/Requests/Exception/HTTP/406.php rename to src/wp-includes/Requests/Exception/Http/Status406.php index 980ef0efc56b5..37952f80a2e3f 100644 --- a/src/wp-includes/Requests/Exception/HTTP/406.php +++ b/src/wp-includes/Requests/Exception/Http/Status406.php @@ -2,15 +2,19 @@ /** * Exception for 406 Not Acceptable responses * - * @package Requests + * @package Requests\Exceptions */ +namespace WpOrg\Requests\Exception\Http; + +use WpOrg\Requests\Exception\Http; + /** * Exception for 406 Not Acceptable responses * - * @package Requests + * @package Requests\Exceptions */ -class Requests_Exception_HTTP_406 extends Requests_Exception_HTTP { +final class Status406 extends Http { /** * HTTP status code * diff --git a/src/wp-includes/Requests/Exception/HTTP/407.php b/src/wp-includes/Requests/Exception/Http/Status407.php similarity index 64% rename from src/wp-includes/Requests/Exception/HTTP/407.php rename to src/wp-includes/Requests/Exception/Http/Status407.php index d08c6af7cc72f..d8796f9b5495b 100644 --- a/src/wp-includes/Requests/Exception/HTTP/407.php +++ b/src/wp-includes/Requests/Exception/Http/Status407.php @@ -2,15 +2,19 @@ /** * Exception for 407 Proxy Authentication Required responses * - * @package Requests + * @package Requests\Exceptions */ +namespace WpOrg\Requests\Exception\Http; + +use WpOrg\Requests\Exception\Http; + /** * Exception for 407 Proxy Authentication Required responses * - * @package Requests + * @package Requests\Exceptions */ -class Requests_Exception_HTTP_407 extends Requests_Exception_HTTP { +final class Status407 extends Http { /** * HTTP status code * diff --git a/src/wp-includes/Requests/Exception/HTTP/408.php b/src/wp-includes/Requests/Exception/Http/Status408.php similarity index 61% rename from src/wp-includes/Requests/Exception/HTTP/408.php rename to src/wp-includes/Requests/Exception/Http/Status408.php index db15bc29105e8..6718d064a11da 100644 --- a/src/wp-includes/Requests/Exception/HTTP/408.php +++ b/src/wp-includes/Requests/Exception/Http/Status408.php @@ -2,15 +2,19 @@ /** * Exception for 408 Request Timeout responses * - * @package Requests + * @package Requests\Exceptions */ +namespace WpOrg\Requests\Exception\Http; + +use WpOrg\Requests\Exception\Http; + /** * Exception for 408 Request Timeout responses * - * @package Requests + * @package Requests\Exceptions */ -class Requests_Exception_HTTP_408 extends Requests_Exception_HTTP { +final class Status408 extends Http { /** * HTTP status code * diff --git a/src/wp-includes/Requests/Exception/HTTP/409.php b/src/wp-includes/Requests/Exception/Http/Status409.php similarity index 60% rename from src/wp-includes/Requests/Exception/HTTP/409.php rename to src/wp-includes/Requests/Exception/Http/Status409.php index 83002f9361193..711a5c43f7608 100644 --- a/src/wp-includes/Requests/Exception/HTTP/409.php +++ b/src/wp-includes/Requests/Exception/Http/Status409.php @@ -2,15 +2,19 @@ /** * Exception for 409 Conflict responses * - * @package Requests + * @package Requests\Exceptions */ +namespace WpOrg\Requests\Exception\Http; + +use WpOrg\Requests\Exception\Http; + /** * Exception for 409 Conflict responses * - * @package Requests + * @package Requests\Exceptions */ -class Requests_Exception_HTTP_409 extends Requests_Exception_HTTP { +final class Status409 extends Http { /** * HTTP status code * diff --git a/src/wp-includes/Requests/Exception/HTTP/410.php b/src/wp-includes/Requests/Exception/Http/Status410.php similarity index 58% rename from src/wp-includes/Requests/Exception/HTTP/410.php rename to src/wp-includes/Requests/Exception/Http/Status410.php index 5bf7aa6d4698b..127443b7815d8 100644 --- a/src/wp-includes/Requests/Exception/HTTP/410.php +++ b/src/wp-includes/Requests/Exception/Http/Status410.php @@ -2,15 +2,19 @@ /** * Exception for 410 Gone responses * - * @package Requests + * @package Requests\Exceptions */ +namespace WpOrg\Requests\Exception\Http; + +use WpOrg\Requests\Exception\Http; + /** * Exception for 410 Gone responses * - * @package Requests + * @package Requests\Exceptions */ -class Requests_Exception_HTTP_410 extends Requests_Exception_HTTP { +final class Status410 extends Http { /** * HTTP status code * diff --git a/src/wp-includes/Requests/Exception/HTTP/411.php b/src/wp-includes/Requests/Exception/Http/Status411.php similarity index 61% rename from src/wp-includes/Requests/Exception/HTTP/411.php rename to src/wp-includes/Requests/Exception/Http/Status411.php index 25517b4f3695d..e70e63c6490b9 100644 --- a/src/wp-includes/Requests/Exception/HTTP/411.php +++ b/src/wp-includes/Requests/Exception/Http/Status411.php @@ -2,15 +2,19 @@ /** * Exception for 411 Length Required responses * - * @package Requests + * @package Requests\Exceptions */ +namespace WpOrg\Requests\Exception\Http; + +use WpOrg\Requests\Exception\Http; + /** * Exception for 411 Length Required responses * - * @package Requests + * @package Requests\Exceptions */ -class Requests_Exception_HTTP_411 extends Requests_Exception_HTTP { +final class Status411 extends Http { /** * HTTP status code * diff --git a/src/wp-includes/Requests/Exception/HTTP/412.php b/src/wp-includes/Requests/Exception/Http/Status412.php similarity index 62% rename from src/wp-includes/Requests/Exception/HTTP/412.php rename to src/wp-includes/Requests/Exception/Http/Status412.php index e89533a7efa32..4a8b9185b27be 100644 --- a/src/wp-includes/Requests/Exception/HTTP/412.php +++ b/src/wp-includes/Requests/Exception/Http/Status412.php @@ -2,15 +2,19 @@ /** * Exception for 412 Precondition Failed responses * - * @package Requests + * @package Requests\Exceptions */ +namespace WpOrg\Requests\Exception\Http; + +use WpOrg\Requests\Exception\Http; + /** * Exception for 412 Precondition Failed responses * - * @package Requests + * @package Requests\Exceptions */ -class Requests_Exception_HTTP_412 extends Requests_Exception_HTTP { +final class Status412 extends Http { /** * HTTP status code * diff --git a/src/wp-includes/Requests/Exception/HTTP/413.php b/src/wp-includes/Requests/Exception/Http/Status413.php similarity index 63% rename from src/wp-includes/Requests/Exception/HTTP/413.php rename to src/wp-includes/Requests/Exception/Http/Status413.php index a7b38fce11c64..96a96fb163c66 100644 --- a/src/wp-includes/Requests/Exception/HTTP/413.php +++ b/src/wp-includes/Requests/Exception/Http/Status413.php @@ -2,15 +2,19 @@ /** * Exception for 413 Request Entity Too Large responses * - * @package Requests + * @package Requests\Exceptions */ +namespace WpOrg\Requests\Exception\Http; + +use WpOrg\Requests\Exception\Http; + /** * Exception for 413 Request Entity Too Large responses * - * @package Requests + * @package Requests\Exceptions */ -class Requests_Exception_HTTP_413 extends Requests_Exception_HTTP { +final class Status413 extends Http { /** * HTTP status code * diff --git a/src/wp-includes/Requests/Exception/HTTP/414.php b/src/wp-includes/Requests/Exception/Http/Status414.php similarity index 63% rename from src/wp-includes/Requests/Exception/HTTP/414.php rename to src/wp-includes/Requests/Exception/Http/Status414.php index 54c8b8c9aecf5..b65ec937af1ff 100644 --- a/src/wp-includes/Requests/Exception/HTTP/414.php +++ b/src/wp-includes/Requests/Exception/Http/Status414.php @@ -2,15 +2,19 @@ /** * Exception for 414 Request-URI Too Large responses * - * @package Requests + * @package Requests\Exceptions */ +namespace WpOrg\Requests\Exception\Http; + +use WpOrg\Requests\Exception\Http; + /** * Exception for 414 Request-URI Too Large responses * - * @package Requests + * @package Requests\Exceptions */ -class Requests_Exception_HTTP_414 extends Requests_Exception_HTTP { +final class Status414 extends Http { /** * HTTP status code * diff --git a/src/wp-includes/Requests/Exception/HTTP/415.php b/src/wp-includes/Requests/Exception/Http/Status415.php similarity index 63% rename from src/wp-includes/Requests/Exception/HTTP/415.php rename to src/wp-includes/Requests/Exception/Http/Status415.php index 6b5f0785b8326..cb45655e97436 100644 --- a/src/wp-includes/Requests/Exception/HTTP/415.php +++ b/src/wp-includes/Requests/Exception/Http/Status415.php @@ -2,15 +2,19 @@ /** * Exception for 415 Unsupported Media Type responses * - * @package Requests + * @package Requests\Exceptions */ +namespace WpOrg\Requests\Exception\Http; + +use WpOrg\Requests\Exception\Http; + /** * Exception for 415 Unsupported Media Type responses * - * @package Requests + * @package Requests\Exceptions */ -class Requests_Exception_HTTP_415 extends Requests_Exception_HTTP { +final class Status415 extends Http { /** * HTTP status code * diff --git a/src/wp-includes/Requests/Exception/HTTP/416.php b/src/wp-includes/Requests/Exception/Http/Status416.php similarity index 65% rename from src/wp-includes/Requests/Exception/HTTP/416.php rename to src/wp-includes/Requests/Exception/Http/Status416.php index 48a4ecdb8f0dd..c3661a193cfe6 100644 --- a/src/wp-includes/Requests/Exception/HTTP/416.php +++ b/src/wp-includes/Requests/Exception/Http/Status416.php @@ -2,15 +2,19 @@ /** * Exception for 416 Requested Range Not Satisfiable responses * - * @package Requests + * @package Requests\Exceptions */ +namespace WpOrg\Requests\Exception\Http; + +use WpOrg\Requests\Exception\Http; + /** * Exception for 416 Requested Range Not Satisfiable responses * - * @package Requests + * @package Requests\Exceptions */ -class Requests_Exception_HTTP_416 extends Requests_Exception_HTTP { +final class Status416 extends Http { /** * HTTP status code * diff --git a/src/wp-includes/Requests/Exception/HTTP/417.php b/src/wp-includes/Requests/Exception/Http/Status417.php similarity index 62% rename from src/wp-includes/Requests/Exception/HTTP/417.php rename to src/wp-includes/Requests/Exception/Http/Status417.php index 81570330aa758..4adba9a23efb7 100644 --- a/src/wp-includes/Requests/Exception/HTTP/417.php +++ b/src/wp-includes/Requests/Exception/Http/Status417.php @@ -2,15 +2,19 @@ /** * Exception for 417 Expectation Failed responses * - * @package Requests + * @package Requests\Exceptions */ +namespace WpOrg\Requests\Exception\Http; + +use WpOrg\Requests\Exception\Http; + /** * Exception for 417 Expectation Failed responses * - * @package Requests + * @package Requests\Exceptions */ -class Requests_Exception_HTTP_417 extends Requests_Exception_HTTP { +final class Status417 extends Http { /** * HTTP status code * diff --git a/src/wp-includes/Requests/Exception/HTTP/418.php b/src/wp-includes/Requests/Exception/Http/Status418.php similarity index 50% rename from src/wp-includes/Requests/Exception/HTTP/418.php rename to src/wp-includes/Requests/Exception/Http/Status418.php index 0fcc87d614678..5bcb2b84a959a 100644 --- a/src/wp-includes/Requests/Exception/HTTP/418.php +++ b/src/wp-includes/Requests/Exception/Http/Status418.php @@ -2,17 +2,23 @@ /** * Exception for 418 I'm A Teapot responses * - * @see https://tools.ietf.org/html/rfc2324 - * @package Requests + * @link https://tools.ietf.org/html/rfc2324 + * + * @package Requests\Exceptions */ +namespace WpOrg\Requests\Exception\Http; + +use WpOrg\Requests\Exception\Http; + /** * Exception for 418 I'm A Teapot responses * - * @see https://tools.ietf.org/html/rfc2324 - * @package Requests + * @link https://tools.ietf.org/html/rfc2324 + * + * @package Requests\Exceptions */ -class Requests_Exception_HTTP_418 extends Requests_Exception_HTTP { +final class Status418 extends Http { /** * HTTP status code * diff --git a/src/wp-includes/Requests/Exception/HTTP/428.php b/src/wp-includes/Requests/Exception/Http/Status428.php similarity index 52% rename from src/wp-includes/Requests/Exception/HTTP/428.php rename to src/wp-includes/Requests/Exception/Http/Status428.php index 799ddffd548d1..4d7ea51f5c9c7 100644 --- a/src/wp-includes/Requests/Exception/HTTP/428.php +++ b/src/wp-includes/Requests/Exception/Http/Status428.php @@ -2,17 +2,23 @@ /** * Exception for 428 Precondition Required responses * - * @see https://tools.ietf.org/html/rfc6585 - * @package Requests + * @link https://tools.ietf.org/html/rfc6585 + * + * @package Requests\Exceptions */ +namespace WpOrg\Requests\Exception\Http; + +use WpOrg\Requests\Exception\Http; + /** * Exception for 428 Precondition Required responses * - * @see https://tools.ietf.org/html/rfc6585 - * @package Requests + * @link https://tools.ietf.org/html/rfc6585 + * + * @package Requests\Exceptions */ -class Requests_Exception_HTTP_428 extends Requests_Exception_HTTP { +final class Status428 extends Http { /** * HTTP status code * diff --git a/src/wp-includes/Requests/Exception/Http/Status429.php b/src/wp-includes/Requests/Exception/Http/Status429.php new file mode 100644 index 0000000000000..2018196c7bea1 --- /dev/null +++ b/src/wp-includes/Requests/Exception/Http/Status429.php @@ -0,0 +1,35 @@ +code = $data->status_code; + if ($data instanceof Response) { + $this->code = (int) $data->status_code; } parent::__construct($reason, $data); diff --git a/src/wp-includes/Requests/Exception/InvalidArgument.php b/src/wp-includes/Requests/Exception/InvalidArgument.php new file mode 100644 index 0000000000000..0ab7332f4f66a --- /dev/null +++ b/src/wp-includes/Requests/Exception/InvalidArgument.php @@ -0,0 +1,41 @@ +type = $type; } if ($code !== null) { - $this->code = $code; + $this->code = (int) $code; } if ($message !== null) { @@ -47,7 +69,9 @@ public function __construct($message, $type, $data = null, $code = 0) { } /** - * Get the error message + * Get the error message. + * + * @return string */ public function getReason() { return $this->reason; diff --git a/src/wp-includes/Requests/Hooker.php b/src/wp-includes/Requests/HookManager.php similarity index 66% rename from src/wp-includes/Requests/Hooker.php rename to src/wp-includes/Requests/HookManager.php index c852931f1a859..f2920170b9e6b 100644 --- a/src/wp-includes/Requests/Hooker.php +++ b/src/wp-includes/Requests/HookManager.php @@ -2,22 +2,22 @@ /** * Event dispatcher * - * @package Requests - * @subpackage Utilities + * @package Requests\EventDispatcher */ +namespace WpOrg\Requests; + /** * Event dispatcher * - * @package Requests - * @subpackage Utilities + * @package Requests\EventDispatcher */ -interface Requests_Hooker { +interface HookManager { /** * Register a callback for a hook * * @param string $hook Hook name - * @param callback $callback Function/method to call on event + * @param callable $callback Function/method to call on event * @param int $priority Priority number. <0 is executed earlier, >0 is executed later */ public function register($hook, $callback, $priority = 0); @@ -29,5 +29,5 @@ public function register($hook, $callback, $priority = 0); * @param array $parameters Parameters to pass to callbacks * @return boolean Successfulness */ - public function dispatch($hook, $parameters = array()); + public function dispatch($hook, $parameters = []); } diff --git a/src/wp-includes/Requests/Hooks.php b/src/wp-includes/Requests/Hooks.php index f857902cedd1c..74fba0b3e135a 100644 --- a/src/wp-includes/Requests/Hooks.php +++ b/src/wp-includes/Requests/Hooks.php @@ -2,44 +2,57 @@ /** * Handles adding and dispatching events * - * @package Requests - * @subpackage Utilities + * @package Requests\EventDispatcher */ +namespace WpOrg\Requests; + +use WpOrg\Requests\Exception\InvalidArgument; +use WpOrg\Requests\HookManager; +use WpOrg\Requests\Utility\InputValidator; + /** * Handles adding and dispatching events * - * @package Requests - * @subpackage Utilities + * @package Requests\EventDispatcher */ -class Requests_Hooks implements Requests_Hooker { +class Hooks implements HookManager { /** * Registered callbacks for each hook * * @var array */ - protected $hooks = array(); - - /** - * Constructor - */ - public function __construct() { - // pass - } + protected $hooks = []; /** * Register a callback for a hook * * @param string $hook Hook name - * @param callback $callback Function/method to call on event + * @param callable $callback Function/method to call on event * @param int $priority Priority number. <0 is executed earlier, >0 is executed later + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $hook argument is not a string. + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $callback argument is not callable. + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $priority argument is not an integer. */ public function register($hook, $callback, $priority = 0) { - if (!isset($this->hooks[$hook])) { - $this->hooks[$hook] = array(); + if (is_string($hook) === false) { + throw InvalidArgument::create(1, '$hook', 'string', gettype($hook)); + } + + if (is_callable($callback) === false) { + throw InvalidArgument::create(2, '$callback', 'callable', gettype($callback)); + } + + if (InputValidator::is_numeric_array_key($priority) === false) { + throw InvalidArgument::create(3, '$priority', 'integer', gettype($priority)); } - if (!isset($this->hooks[$hook][$priority])) { - $this->hooks[$hook][$priority] = array(); + + if (!isset($this->hooks[$hook])) { + $this->hooks[$hook] = [ + $priority => [], + ]; + } elseif (!isset($this->hooks[$hook][$priority])) { + $this->hooks[$hook][$priority] = []; } $this->hooks[$hook][$priority][] = $callback; @@ -51,15 +64,33 @@ public function register($hook, $callback, $priority = 0) { * @param string $hook Hook name * @param array $parameters Parameters to pass to callbacks * @return boolean Successfulness + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $hook argument is not a string. + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $parameters argument is not an array. */ - public function dispatch($hook, $parameters = array()) { + public function dispatch($hook, $parameters = []) { + if (is_string($hook) === false) { + throw InvalidArgument::create(1, '$hook', 'string', gettype($hook)); + } + + // Check strictly against array, as Array* objects don't work in combination with `call_user_func_array()`. + if (is_array($parameters) === false) { + throw InvalidArgument::create(2, '$parameters', 'array', gettype($parameters)); + } + if (empty($this->hooks[$hook])) { return false; } + if (!empty($parameters)) { + // Strip potential keys from the array to prevent them being interpreted as parameter names in PHP 8.0. + $parameters = array_values($parameters); + } + + ksort($this->hooks[$hook]); + foreach ($this->hooks[$hook] as $priority => $hooked) { foreach ($hooked as $callback) { - call_user_func_array($callback, $parameters); + $callback(...$parameters); } } diff --git a/src/wp-includes/Requests/IDNAEncoder.php b/src/wp-includes/Requests/IdnaEncoder.php similarity index 60% rename from src/wp-includes/Requests/IDNAEncoder.php rename to src/wp-includes/Requests/IdnaEncoder.php index 881142935b48c..094fff3d5231a 100644 --- a/src/wp-includes/Requests/IDNAEncoder.php +++ b/src/wp-includes/Requests/IdnaEncoder.php @@ -1,28 +1,45 @@ 0) { if ($position + $length > $strlen) { - throw new Requests_Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $character); + throw new Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $character); } + for ($position++; $remaining > 0; $position++) { $value = ord($input[$position]); // If it is invalid, count the sequence as invalid and reprocess the current byte: if (($value & 0xC0) !== 0x80) { - throw new Requests_Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $character); + throw new Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $character); } --$remaining; $character |= ($value & 0x3F) << ($remaining * 6); } + $position--; } @@ -208,7 +231,7 @@ protected static function utf8_to_codepoints($input) { || $character > 0xEFFFD ) ) { - throw new Requests_Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $character); + throw new Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $character); } $codepoints[] = $character; @@ -221,10 +244,11 @@ protected static function utf8_to_codepoints($input) { * RFC3492-compliant encoder * * @internal Pseudo-code from Section 6.3 is commented with "#" next to relevant code - * @throws Requests_Exception On character outside of the domain (never happens with Punycode) (`idna.character_outside_domain`) * * @param string $input UTF-8 encoded string to encode * @return string Punycode-encoded string + * + * @throws \WpOrg\Requests\Exception On character outside of the domain (never happens with Punycode) (`idna.character_outside_domain`) */ public static function punycode_encode($input) { $output = ''; @@ -239,7 +263,7 @@ public static function punycode_encode($input) { $b = 0; // see loop // copy them to the output in order $codepoints = self::utf8_to_codepoints($input); - $extended = array(); + $extended = []; foreach ($codepoints as $char) { if ($char < 128) { @@ -247,18 +271,18 @@ public static function punycode_encode($input) { // TODO: this should also check if it's valid for a URL $output .= chr($char); $h++; - } - // Check if the character is non-ASCII, but below initial n - // This never occurs for Punycode, so ignore in coverage - // @codeCoverageIgnoreStart - elseif ($char < $n) { - throw new Requests_Exception('Invalid character', 'idna.character_outside_domain', $char); - } - // @codeCoverageIgnoreEnd - else { + + // Check if the character is non-ASCII, but below initial n + // This never occurs for Punycode, so ignore in coverage + // @codeCoverageIgnoreStart + } elseif ($char < $n) { + throw new Exception('Invalid character', 'idna.character_outside_domain', $char); + // @codeCoverageIgnoreEnd + } else { $extended[$char] = true; } } + $extended = array_keys($extended); sort($extended); $b = $h; @@ -266,6 +290,7 @@ public static function punycode_encode($input) { if (strlen($output) > 0) { $output .= '-'; } + // {if the input contains a non-basic code point < n then fail} // while h < length(input) do begin $codepointcount = count($codepoints); @@ -283,9 +308,7 @@ public static function punycode_encode($input) { // if c < n then increment delta, fail on overflow if ($c < $n) { $delta++; - } - // if c == n then begin - elseif ($c === $n) { + } elseif ($c === $n) { // if c == n then begin // let q = delta $q = $delta; // for k = base to infinity in steps of base do begin @@ -294,17 +317,17 @@ public static function punycode_encode($input) { // tmax if k >= bias + tmax, or k - bias otherwise if ($k <= ($bias + self::BOOTSTRAP_TMIN)) { $t = self::BOOTSTRAP_TMIN; - } - elseif ($k >= ($bias + self::BOOTSTRAP_TMAX)) { + } elseif ($k >= ($bias + self::BOOTSTRAP_TMAX)) { $t = self::BOOTSTRAP_TMAX; - } - else { + } else { $t = $k - $bias; } + // if q < t then break if ($q < $t) { break; } + // output the code point for digit t + ((q - t) mod (base - t)) $digit = $t + (($q - $t) % (self::BOOTSTRAP_BASE - $t)); $output .= self::digit_to_char($digit); @@ -332,18 +355,20 @@ public static function punycode_encode($input) { /** * Convert a digit to its respective character * - * @see https://tools.ietf.org/html/rfc3492#section-5 - * @throws Requests_Exception On invalid digit (`idna.invalid_digit`) + * @link https://tools.ietf.org/html/rfc3492#section-5 * * @param int $digit Digit in the range 0-35 * @return string Single character corresponding to digit + * + * @throws \WpOrg\Requests\Exception On invalid digit (`idna.invalid_digit`) */ protected static function digit_to_char($digit) { // @codeCoverageIgnoreStart // As far as I know, this never happens, but still good to be sure. if ($digit < 0 || $digit > 35) { - throw new Requests_Exception(sprintf('Invalid digit %d', $digit), 'idna.invalid_digit', $digit); + throw new Exception(sprintf('Invalid digit %d', $digit), 'idna.invalid_digit', $digit); } + // @codeCoverageIgnoreEnd $digits = 'abcdefghijklmnopqrstuvwxyz0123456789'; return substr($digits, $digit, 1); @@ -352,7 +377,7 @@ protected static function digit_to_char($digit) { /** * Adapt the bias * - * @see https://tools.ietf.org/html/rfc3492#section-6.1 + * @link https://tools.ietf.org/html/rfc3492#section-6.1 * @param int $delta * @param int $numpoints * @param bool $firsttime @@ -364,11 +389,11 @@ protected static function adapt($delta, $numpoints, $firsttime) { // if firsttime then let delta = delta div damp if ($firsttime) { $delta = floor($delta / self::BOOTSTRAP_DAMP); - } - // else let delta = delta div 2 - else { + } else { + // else let delta = delta div 2 $delta = floor($delta / 2); } + // let delta = delta + (delta div numpoints) $delta += floor($delta / $numpoints); // let k = 0 diff --git a/src/wp-includes/Requests/IPv6.php b/src/wp-includes/Requests/Ipv6.php similarity index 79% rename from src/wp-includes/Requests/IPv6.php rename to src/wp-includes/Requests/Ipv6.php index ba88786be61b8..a90ab8a831b6f 100644 --- a/src/wp-includes/Requests/IPv6.php +++ b/src/wp-includes/Requests/Ipv6.php @@ -2,20 +2,23 @@ /** * Class to validate and to work with IPv6 addresses * - * @package Requests - * @subpackage Utilities + * @package Requests\Utilities */ +namespace WpOrg\Requests; + +use WpOrg\Requests\Exception\InvalidArgument; +use WpOrg\Requests\Utility\InputValidator; + /** * Class to validate and to work with IPv6 addresses * * This was originally based on the PEAR class of the same name, but has been * entirely rewritten. * - * @package Requests - * @subpackage Utilities + * @package Requests\Utilities */ -class Requests_IPv6 { +final class Ipv6 { /** * Uncompresses an IPv6 address * @@ -30,11 +33,20 @@ class Requests_IPv6 { * @author elfrink at introweb dot nl * @author Josh Peck * @copyright 2003-2005 The PHP Group - * @license http://www.opensource.org/licenses/bsd-license.php - * @param string $ip An IPv6 address + * @license https://opensource.org/licenses/bsd-license.php + * + * @param string|Stringable $ip An IPv6 address * @return string The uncompressed IPv6 address + * + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string or a stringable object. */ public static function uncompress($ip) { + if (InputValidator::is_string_or_stringable($ip) === false) { + throw InvalidArgument::create(1, '$ip', 'string|Stringable', gettype($ip)); + } + + $ip = (string) $ip; + if (substr_count($ip, '::') !== 1) { return $ip; } @@ -46,25 +58,24 @@ public static function uncompress($ip) { if (strpos($ip2, '.') !== false) { $c2++; } - // :: + if ($c1 === -1 && $c2 === -1) { + // :: $ip = '0:0:0:0:0:0:0:0'; - } - // ::xxx - elseif ($c1 === -1) { + } elseif ($c1 === -1) { + // ::xxx $fill = str_repeat('0:', 7 - $c2); $ip = str_replace('::', $fill, $ip); - } - // xxx:: - elseif ($c2 === -1) { + } elseif ($c2 === -1) { + // xxx:: $fill = str_repeat(':0', 7 - $c1); $ip = str_replace('::', $fill, $ip); - } - // xxx::xxx - else { + } else { + // xxx::xxx $fill = ':' . str_repeat('0:', 6 - $c2 - $c1); $ip = str_replace('::', $fill, $ip); } + return $ip; } @@ -78,12 +89,14 @@ public static function uncompress($ip) { * Example: FF01:0:0:0:0:0:0:101 -> FF01::101 * 0:0:0:0:0:0:0:1 -> ::1 * - * @see uncompress() + * @see \WpOrg\Requests\Ipv6::uncompress() + * * @param string $ip An IPv6 address * @return string The compressed IPv6 address */ public static function compress($ip) { - // Prepare the IP to be compressed + // Prepare the IP to be compressed. + // Note: Input validation is handled in the `uncompress()` method, which is the first call made in this method. $ip = self::uncompress($ip); $ip_parts = self::split_v6_v4($ip); @@ -106,8 +119,7 @@ public static function compress($ip) { if ($ip_parts[1] !== '') { return implode(':', $ip_parts); - } - else { + } else { return $ip_parts[0]; } } @@ -124,15 +136,14 @@ public static function compress($ip) { * @param string $ip An IPv6 address * @return string[] [0] contains the IPv6 represented part, and [1] the IPv4 represented part */ - protected static function split_v6_v4($ip) { + private static function split_v6_v4($ip) { if (strpos($ip, '.') !== false) { $pos = strrpos($ip, ':'); $ipv6_part = substr($ip, 0, $pos); $ipv4_part = substr($ip, $pos + 1); - return array($ipv6_part, $ipv4_part); - } - else { - return array($ip, ''); + return [$ipv6_part, $ipv4_part]; + } else { + return [$ip, '']; } } @@ -145,6 +156,7 @@ protected static function split_v6_v4($ip) { * @return bool true if $ip is a valid IPv6 address */ public static function check_ipv6($ip) { + // Note: Input validation is handled in the `uncompress()` method, which is the first call made in this method. $ip = self::uncompress($ip); list($ipv6, $ipv4) = self::split_v6_v4($ip); $ipv6 = explode(':', $ipv6); @@ -173,6 +185,7 @@ public static function check_ipv6($ip) { return false; } } + if (count($ipv4) === 4) { foreach ($ipv4 as $ipv4_part) { $value = (int) $ipv4_part; @@ -181,9 +194,9 @@ public static function check_ipv6($ip) { } } } + return true; - } - else { + } else { return false; } } diff --git a/src/wp-includes/Requests/IRI.php b/src/wp-includes/Requests/Iri.php similarity index 91% rename from src/wp-includes/Requests/IRI.php rename to src/wp-includes/Requests/Iri.php index 5d80e495724bd..244578d3448cf 100644 --- a/src/wp-includes/Requests/IRI.php +++ b/src/wp-includes/Requests/Iri.php @@ -2,10 +2,17 @@ /** * IRI parser/serialiser/normaliser * - * @package Requests - * @subpackage Utilities + * @package Requests\Utilities */ +namespace WpOrg\Requests; + +use WpOrg\Requests\Exception; +use WpOrg\Requests\Exception\InvalidArgument; +use WpOrg\Requests\Ipv6; +use WpOrg\Requests\Port; +use WpOrg\Requests\Utility\InputValidator; + /** * IRI parser/serialiser/normaliser * @@ -38,16 +45,15 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * - * @package Requests - * @subpackage Utilities + * @package Requests\Utilities * @author Geoffrey Sneddon * @author Steve Minutillo * @copyright 2007-2009 Geoffrey Sneddon and Steve Minutillo - * @license http://www.opensource.org/licenses/bsd-license.php + * @license https://opensource.org/licenses/bsd-license.php * @link http://hg.gsnedders.com/iri/ * * @property string $iri IRI we're working with - * @property-read string $uri IRI in URI form, {@see to_uri} + * @property-read string $uri IRI in URI form, {@see \WpOrg\Requests\Iri::to_uri()} * @property string $scheme Scheme part of the IRI * @property string $authority Authority part, formatted for a URI (userinfo + host + port) * @property string $iauthority Authority part of the IRI (userinfo + host + port) @@ -63,7 +69,7 @@ * @property string $fragment Fragment, formatted for a URI (after '#') * @property string $ifragment Fragment part of the IRI (after '#') */ -class Requests_IRI { +class Iri { /** * Scheme * @@ -123,19 +129,19 @@ class Requests_IRI { */ protected $normalization = array( 'acap' => array( - 'port' => 674 + 'port' => Port::ACAP, ), 'dict' => array( - 'port' => 2628 + 'port' => Port::DICT, ), 'file' => array( - 'ihost' => 'localhost' + 'ihost' => 'localhost', ), 'http' => array( - 'port' => 80, + 'port' => Port::HTTP, ), 'https' => array( - 'port' => 443, + 'port' => Port::HTTPS, ), ); @@ -240,9 +246,15 @@ public function __unset($name) { /** * Create a new IRI object, from a specified string * - * @param string|null $iri + * @param string|Stringable|null $iri + * + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $iri argument is not a string, Stringable or null. */ public function __construct($iri = null) { + if ($iri !== null && InputValidator::is_string_or_stringable($iri) === false) { + throw InvalidArgument::create(1, '$iri', 'string|Stringable|null', gettype($iri)); + } + $this->set_iri($iri); } @@ -251,13 +263,13 @@ public function __construct($iri = null) { * * Returns false if $base is not absolute, otherwise an IRI. * - * @param Requests_IRI|string $base (Absolute) Base IRI - * @param Requests_IRI|string $relative Relative IRI - * @return Requests_IRI|false + * @param \WpOrg\Requests\Iri|string $base (Absolute) Base IRI + * @param \WpOrg\Requests\Iri|string $relative Relative IRI + * @return \WpOrg\Requests\Iri|false */ public static function absolutize($base, $relative) { - if (!($relative instanceof Requests_IRI)) { - $relative = new Requests_IRI($relative); + if (!($relative instanceof self)) { + $relative = new self($relative); } if (!$relative->is_valid()) { return false; @@ -266,8 +278,8 @@ public static function absolutize($base, $relative) { return clone $relative; } - if (!($base instanceof Requests_IRI)) { - $base = new Requests_IRI($base); + if (!($base instanceof self)) { + $base = new self($base); } if ($base->scheme === null || !$base->is_valid()) { return false; @@ -279,7 +291,7 @@ public static function absolutize($base, $relative) { $target->scheme = $base->scheme; } else { - $target = new Requests_IRI; + $target = new self; $target->scheme = $base->scheme; $target->iuserinfo = $base->iuserinfo; $target->ihost = $base->ihost; @@ -330,7 +342,7 @@ protected function parse_iri($iri) { $iri = trim($iri, "\x20\x09\x0A\x0C\x0D"); $has_match = preg_match('/^((?P[^:\/?#]+):)?(\/\/(?P[^\/?#]*))?(?P[^?#]*)(\?(?P[^#]*))?(#(?P.*))?$/', $iri, $match); if (!$has_match) { - throw new Requests_Exception('Cannot parse supplied IRI', 'iri.cannot_parse', $iri); + throw new Exception('Cannot parse supplied IRI', 'iri.cannot_parse', $iri); } if ($match[1] === '') { @@ -413,18 +425,18 @@ protected function remove_dot_segments($input) { /** * Replace invalid character with percent encoding * - * @param string $string Input string + * @param string $text Input string * @param string $extra_chars Valid characters not in iunreserved or * iprivate (this is ASCII-only) * @param bool $iprivate Allow iprivate * @return string */ - protected function replace_invalid_with_pct_encoding($string, $extra_chars, $iprivate = false) { + protected function replace_invalid_with_pct_encoding($text, $extra_chars, $iprivate = false) { // Normalize as many pct-encoded sections as possible - $string = preg_replace_callback('/(?:%[A-Fa-f0-9]{2})+/', array($this, 'remove_iunreserved_percent_encoded'), $string); + $text = preg_replace_callback('/(?:%[A-Fa-f0-9]{2})+/', array($this, 'remove_iunreserved_percent_encoded'), $text); // Replace invalid percent characters - $string = preg_replace('/%(?![A-Fa-f0-9]{2})/', '%25', $string); + $text = preg_replace('/%(?![A-Fa-f0-9]{2})/', '%25', $text); // Add unreserved and % to $extra_chars (the latter is safe because all // pct-encoded sections are now valid). @@ -432,9 +444,9 @@ protected function replace_invalid_with_pct_encoding($string, $extra_chars, $ipr // Now replace any bytes that aren't allowed with their pct-encoded versions $position = 0; - $strlen = strlen($string); - while (($position += strspn($string, $extra_chars, $position)) < $strlen) { - $value = ord($string[$position]); + $strlen = strlen($text); + while (($position += strspn($text, $extra_chars, $position)) < $strlen) { + $value = ord($text[$position]); // Start position $start = $position; @@ -471,7 +483,7 @@ protected function replace_invalid_with_pct_encoding($string, $extra_chars, $ipr if ($remaining) { if ($position + $length <= $strlen) { for ($position++; $remaining; $position++) { - $value = ord($string[$position]); + $value = ord($text[$position]); // Check that the byte is valid, then add it to the character: if (($value & 0xC0) === 0x80) { @@ -522,7 +534,7 @@ protected function replace_invalid_with_pct_encoding($string, $extra_chars, $ipr } for ($j = $start; $j <= $position; $j++) { - $string = substr_replace($string, sprintf('%%%02X', ord($string[$j])), $j, 1); + $text = substr_replace($text, sprintf('%%%02X', ord($text[$j])), $j, 1); $j += 2; $position += 2; $strlen += 2; @@ -530,7 +542,7 @@ protected function replace_invalid_with_pct_encoding($string, $extra_chars, $ipr } } - return $string; + return $text; } /** @@ -539,13 +551,13 @@ protected function replace_invalid_with_pct_encoding($string, $extra_chars, $ipr * Removes sequences of percent encoded bytes that represent UTF-8 * encoded characters in iunreserved * - * @param array $match PCRE match + * @param array $regex_match PCRE match * @return string Replacement */ - protected function remove_iunreserved_percent_encoded($match) { + protected function remove_iunreserved_percent_encoded($regex_match) { // As we just have valid percent encoded sequences we can just explode // and ignore the first member of the returned array (an empty string). - $bytes = explode('%', $match[0]); + $bytes = explode('%', $regex_match[0]); // Initialize the new string (this is what will be returned) and that // there are no bytes remaining in the current sequence (unsurprising @@ -721,6 +733,9 @@ protected function set_iri($iri) { if ($iri === null) { return true; } + + $iri = (string) $iri; + if (isset($cache[$iri])) { list($this->scheme, $this->iuserinfo, @@ -733,7 +748,7 @@ protected function set_iri($iri) { return $return; } - $parsed = $this->parse_iri((string) $iri); + $parsed = $this->parse_iri($iri); $return = $this->set_scheme($parsed['scheme']) && $this->set_authority($parsed['authority']) @@ -863,8 +878,8 @@ protected function set_host($ihost) { return true; } if (substr($ihost, 0, 1) === '[' && substr($ihost, -1) === ']') { - if (Requests_IPv6::check_ipv6(substr($ihost, 1, -1))) { - $this->ihost = '[' . Requests_IPv6::compress(substr($ihost, 1, -1)) . ']'; + if (Ipv6::check_ipv6(substr($ihost, 1, -1))) { + $this->ihost = '[' . Ipv6::compress(substr($ihost, 1, -1)) . ']'; } else { $this->ihost = null; @@ -985,11 +1000,11 @@ protected function set_fragment($ifragment) { /** * Convert an IRI to a URI (or parts thereof) * - * @param string|bool IRI to convert (or false from {@see get_iri}) + * @param string|bool $iri IRI to convert (or false from {@see \WpOrg\Requests\Iri::get_iri()}) * @return string|false URI if IRI is valid, false otherwise. */ - protected function to_uri($string) { - if (!is_string($string)) { + protected function to_uri($iri) { + if (!is_string($iri)) { return false; } @@ -999,14 +1014,14 @@ protected function to_uri($string) { } $position = 0; - $strlen = strlen($string); - while (($position += strcspn($string, $non_ascii, $position)) < $strlen) { - $string = substr_replace($string, sprintf('%%%02X', ord($string[$position])), $position, 1); + $strlen = strlen($iri); + while (($position += strcspn($iri, $non_ascii, $position)) < $strlen) { + $iri = substr_replace($iri, sprintf('%%%02X', ord($iri[$position])), $position, 1); $position += 3; $strlen += 2; } - return $string; + return $iri; } /** diff --git a/src/wp-includes/Requests/Port.php b/src/wp-includes/Requests/Port.php new file mode 100644 index 0000000000000..55454093855ee --- /dev/null +++ b/src/wp-includes/Requests/Port.php @@ -0,0 +1,75 @@ +proxy = $args; - } - elseif (is_array($args)) { + } elseif (is_array($args)) { if (count($args) === 1) { list($this->proxy) = $args; - } - elseif (count($args) === 3) { + } elseif (count($args) === 3) { list($this->proxy, $this->user, $this->pass) = $args; $this->use_authentication = true; + } else { + throw ArgumentCount::create( + 'an array with exactly one element or exactly three elements', + count($args), + 'proxyhttpbadargs' + ); } - else { - throw new Requests_Exception('Invalid number of arguments', 'proxyhttpbadargs'); - } + } elseif ($args !== null) { + throw InvalidArgument::create(1, '$args', 'array|string|null', gettype($args)); } } @@ -76,19 +89,19 @@ public function __construct($args = null) { * Register the necessary callbacks * * @since 1.6 - * @see curl_before_send - * @see fsockopen_remote_socket - * @see fsockopen_remote_host_path - * @see fsockopen_header - * @param Requests_Hooks $hooks Hook system + * @see \WpOrg\Requests\Proxy\Http::curl_before_send() + * @see \WpOrg\Requests\Proxy\Http::fsockopen_remote_socket() + * @see \WpOrg\Requests\Proxy\Http::fsockopen_remote_host_path() + * @see \WpOrg\Requests\Proxy\Http::fsockopen_header() + * @param \WpOrg\Requests\Hooks $hooks Hook system */ - public function register(Requests_Hooks $hooks) { - $hooks->register('curl.before_send', array($this, 'curl_before_send')); + public function register(Hooks $hooks) { + $hooks->register('curl.before_send', [$this, 'curl_before_send']); - $hooks->register('fsockopen.remote_socket', array($this, 'fsockopen_remote_socket')); - $hooks->register('fsockopen.remote_host_path', array($this, 'fsockopen_remote_host_path')); + $hooks->register('fsockopen.remote_socket', [$this, 'fsockopen_remote_socket']); + $hooks->register('fsockopen.remote_host_path', [$this, 'fsockopen_remote_host_path']); if ($this->use_authentication) { - $hooks->register('fsockopen.after_headers', array($this, 'fsockopen_header')); + $hooks->register('fsockopen.after_headers', [$this, 'fsockopen_header']); } } @@ -96,7 +109,7 @@ public function register(Requests_Hooks $hooks) { * Set cURL parameters before the data is sent * * @since 1.6 - * @param resource $handle cURL resource + * @param resource|\CurlHandle $handle cURL handle */ public function curl_before_send(&$handle) { curl_setopt($handle, CURLOPT_PROXYTYPE, CURLPROXY_HTTP); diff --git a/src/wp-includes/Requests/Requests.php b/src/wp-includes/Requests/Requests.php new file mode 100644 index 0000000000000..a8d9d7e5391ac --- /dev/null +++ b/src/wp-includes/Requests/Requests.php @@ -0,0 +1,1095 @@ + 10, + 'connect_timeout' => 10, + 'useragent' => 'php-requests/' . self::VERSION, + 'protocol_version' => 1.1, + 'redirected' => 0, + 'redirects' => 10, + 'follow_redirects' => true, + 'blocking' => true, + 'type' => self::GET, + 'filename' => false, + 'auth' => false, + 'proxy' => false, + 'cookies' => false, + 'max_bytes' => false, + 'idn' => true, + 'hooks' => null, + 'transport' => null, + 'verify' => null, + 'verifyname' => true, + ]; + + /** + * Default supported Transport classes. + * + * @since 2.0.0 + * + * @var array + */ + const DEFAULT_TRANSPORTS = [ + Curl::class => Curl::class, + Fsockopen::class => Fsockopen::class, + ]; + + /** + * Current version of Requests + * + * @var string + */ + const VERSION = '2.0.5'; + + /** + * Selected transport name + * + * Use {@see \WpOrg\Requests\Requests::get_transport()} instead + * + * @var array + */ + public static $transport = []; + + /** + * Registered transport classes + * + * @var array + */ + protected static $transports = []; + + /** + * Default certificate path. + * + * @see \WpOrg\Requests\Requests::get_certificate_path() + * @see \WpOrg\Requests\Requests::set_certificate_path() + * + * @var string + */ + protected static $certificate_path = __DIR__ . '/../certificates/cacert.pem'; + + /** + * All (known) valid deflate, gzip header magic markers. + * + * These markers relate to different compression levels. + * + * @link https://stackoverflow.com/a/43170354/482864 Marker source. + * + * @since 2.0.0 + * + * @var array + */ + private static $magic_compression_headers = [ + "\x1f\x8b" => true, // Gzip marker. + "\x78\x01" => true, // Zlib marker - level 1. + "\x78\x5e" => true, // Zlib marker - level 2 to 5. + "\x78\x9c" => true, // Zlib marker - level 6. + "\x78\xda" => true, // Zlib marker - level 7 to 9. + ]; + + /** + * This is a static class, do not instantiate it + * + * @codeCoverageIgnore + */ + private function __construct() {} + + /** + * Register a transport + * + * @param string $transport Transport class to add, must support the \WpOrg\Requests\Transport interface + */ + public static function add_transport($transport) { + if (empty(self::$transports)) { + self::$transports = self::DEFAULT_TRANSPORTS; + } + + self::$transports[$transport] = $transport; + } + + /** + * Get the fully qualified class name (FQCN) for a working transport. + * + * @param array $capabilities Optional. Associative array of capabilities to test against, i.e. `['' => true]`. + * @return string FQCN of the transport to use, or an empty string if no transport was + * found which provided the requested capabilities. + */ + protected static function get_transport_class(array $capabilities = []) { + // Caching code, don't bother testing coverage. + // @codeCoverageIgnoreStart + // Array of capabilities as a string to be used as an array key. + ksort($capabilities); + $cap_string = serialize($capabilities); + + // Don't search for a transport if it's already been done for these $capabilities. + if (isset(self::$transport[$cap_string])) { + return self::$transport[$cap_string]; + } + + // Ensure we will not run this same check again later on. + self::$transport[$cap_string] = ''; + // @codeCoverageIgnoreEnd + + if (empty(self::$transports)) { + self::$transports = self::DEFAULT_TRANSPORTS; + } + + // Find us a working transport. + foreach (self::$transports as $class) { + if (!class_exists($class)) { + continue; + } + + $result = $class::test($capabilities); + if ($result === true) { + self::$transport[$cap_string] = $class; + break; + } + } + + return self::$transport[$cap_string]; + } + + /** + * Get a working transport. + * + * @param array $capabilities Optional. Associative array of capabilities to test against, i.e. `['' => true]`. + * @return \WpOrg\Requests\Transport + * @throws \WpOrg\Requests\Exception If no valid transport is found (`notransport`). + */ + protected static function get_transport(array $capabilities = []) { + $class = self::get_transport_class($capabilities); + + if ($class === '') { + throw new Exception('No working transports found', 'notransport', self::$transports); + } + + return new $class(); + } + + /** + * Checks to see if we have a transport for the capabilities requested. + * + * Supported capabilities can be found in the {@see \WpOrg\Requests\Capability} + * interface as constants. + * + * Example usage: + * `Requests::has_capabilities([Capability::SSL => true])`. + * + * @param array $capabilities Optional. Associative array of capabilities to test against, i.e. `['' => true]`. + * @return bool Whether the transport has the requested capabilities. + */ + public static function has_capabilities(array $capabilities = []) { + return self::get_transport_class($capabilities) !== ''; + } + + /**#@+ + * @see \WpOrg\Requests\Requests::request() + * @param string $url + * @param array $headers + * @param array $options + * @return \WpOrg\Requests\Response + */ + /** + * Send a GET request + */ + public static function get($url, $headers = [], $options = []) { + return self::request($url, $headers, null, self::GET, $options); + } + + /** + * Send a HEAD request + */ + public static function head($url, $headers = [], $options = []) { + return self::request($url, $headers, null, self::HEAD, $options); + } + + /** + * Send a DELETE request + */ + public static function delete($url, $headers = [], $options = []) { + return self::request($url, $headers, null, self::DELETE, $options); + } + + /** + * Send a TRACE request + */ + public static function trace($url, $headers = [], $options = []) { + return self::request($url, $headers, null, self::TRACE, $options); + } + /**#@-*/ + + /**#@+ + * @see \WpOrg\Requests\Requests::request() + * @param string $url + * @param array $headers + * @param array $data + * @param array $options + * @return \WpOrg\Requests\Response + */ + /** + * Send a POST request + */ + public static function post($url, $headers = [], $data = [], $options = []) { + return self::request($url, $headers, $data, self::POST, $options); + } + /** + * Send a PUT request + */ + public static function put($url, $headers = [], $data = [], $options = []) { + return self::request($url, $headers, $data, self::PUT, $options); + } + + /** + * Send an OPTIONS request + */ + public static function options($url, $headers = [], $data = [], $options = []) { + return self::request($url, $headers, $data, self::OPTIONS, $options); + } + + /** + * Send a PATCH request + * + * Note: Unlike {@see \WpOrg\Requests\Requests::post()} and {@see \WpOrg\Requests\Requests::put()}, + * `$headers` is required, as the specification recommends that should send an ETag + * + * @link https://tools.ietf.org/html/rfc5789 + */ + public static function patch($url, $headers, $data = [], $options = []) { + return self::request($url, $headers, $data, self::PATCH, $options); + } + /**#@-*/ + + /** + * Main interface for HTTP requests + * + * This method initiates a request and sends it via a transport before + * parsing. + * + * The `$options` parameter takes an associative array with the following + * options: + * + * - `timeout`: How long should we wait for a response? + * Note: for cURL, a minimum of 1 second applies, as DNS resolution + * operates at second-resolution only. + * (float, seconds with a millisecond precision, default: 10, example: 0.01) + * - `connect_timeout`: How long should we wait while trying to connect? + * (float, seconds with a millisecond precision, default: 10, example: 0.01) + * - `useragent`: Useragent to send to the server + * (string, default: php-requests/$version) + * - `follow_redirects`: Should we follow 3xx redirects? + * (boolean, default: true) + * - `redirects`: How many times should we redirect before erroring? + * (integer, default: 10) + * - `blocking`: Should we block processing on this request? + * (boolean, default: true) + * - `filename`: File to stream the body to instead. + * (string|boolean, default: false) + * - `auth`: Authentication handler or array of user/password details to use + * for Basic authentication + * (\WpOrg\Requests\Auth|array|boolean, default: false) + * - `proxy`: Proxy details to use for proxy by-passing and authentication + * (\WpOrg\Requests\Proxy|array|string|boolean, default: false) + * - `max_bytes`: Limit for the response body size. + * (integer|boolean, default: false) + * - `idn`: Enable IDN parsing + * (boolean, default: true) + * - `transport`: Custom transport. Either a class name, or a + * transport object. Defaults to the first working transport from + * {@see \WpOrg\Requests\Requests::getTransport()} + * (string|\WpOrg\Requests\Transport, default: {@see \WpOrg\Requests\Requests::getTransport()}) + * - `hooks`: Hooks handler. + * (\WpOrg\Requests\HookManager, default: new WpOrg\Requests\Hooks()) + * - `verify`: Should we verify SSL certificates? Allows passing in a custom + * certificate file as a string. (Using true uses the system-wide root + * certificate store instead, but this may have different behaviour + * across transports.) + * (string|boolean, default: certificates/cacert.pem) + * - `verifyname`: Should we verify the common name in the SSL certificate? + * (boolean, default: true) + * - `data_format`: How should we send the `$data` parameter? + * (string, one of 'query' or 'body', default: 'query' for + * HEAD/GET/DELETE, 'body' for POST/PUT/OPTIONS/PATCH) + * + * @param string|Stringable $url URL to request + * @param array $headers Extra headers to send with the request + * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests + * @param string $type HTTP request type (use Requests constants) + * @param array $options Options for the request (see description for more information) + * @return \WpOrg\Requests\Response + * + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string or Stringable. + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $type argument is not a string. + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. + * @throws \WpOrg\Requests\Exception On invalid URLs (`nonhttp`) + */ + public static function request($url, $headers = [], $data = [], $type = self::GET, $options = []) { + if (InputValidator::is_string_or_stringable($url) === false) { + throw InvalidArgument::create(1, '$url', 'string|Stringable', gettype($url)); + } + + if (is_string($type) === false) { + throw InvalidArgument::create(4, '$type', 'string', gettype($type)); + } + + if (is_array($options) === false) { + throw InvalidArgument::create(5, '$options', 'array', gettype($options)); + } + + if (empty($options['type'])) { + $options['type'] = $type; + } + + $options = array_merge(self::get_default_options(), $options); + + self::set_defaults($url, $headers, $data, $type, $options); + + $options['hooks']->dispatch('requests.before_request', [&$url, &$headers, &$data, &$type, &$options]); + + if (!empty($options['transport'])) { + $transport = $options['transport']; + + if (is_string($options['transport'])) { + $transport = new $transport(); + } + } else { + $need_ssl = (stripos($url, 'https://') === 0); + $capabilities = [Capability::SSL => $need_ssl]; + $transport = self::get_transport($capabilities); + } + + $response = $transport->request($url, $headers, $data, $options); + + $options['hooks']->dispatch('requests.before_parse', [&$response, $url, $headers, $data, $type, $options]); + + return self::parse_response($response, $url, $headers, $data, $options); + } + + /** + * Send multiple HTTP requests simultaneously + * + * The `$requests` parameter takes an associative or indexed array of + * request fields. The key of each request can be used to match up the + * request with the returned data, or with the request passed into your + * `multiple.request.complete` callback. + * + * The request fields value is an associative array with the following keys: + * + * - `url`: Request URL Same as the `$url` parameter to + * {@see \WpOrg\Requests\Requests::request()} + * (string, required) + * - `headers`: Associative array of header fields. Same as the `$headers` + * parameter to {@see \WpOrg\Requests\Requests::request()} + * (array, default: `array()`) + * - `data`: Associative array of data fields or a string. Same as the + * `$data` parameter to {@see \WpOrg\Requests\Requests::request()} + * (array|string, default: `array()`) + * - `type`: HTTP request type (use \WpOrg\Requests\Requests constants). Same as the `$type` + * parameter to {@see \WpOrg\Requests\Requests::request()} + * (string, default: `\WpOrg\Requests\Requests::GET`) + * - `cookies`: Associative array of cookie name to value, or cookie jar. + * (array|\WpOrg\Requests\Cookie\Jar) + * + * If the `$options` parameter is specified, individual requests will + * inherit options from it. This can be used to use a single hooking system, + * or set all the types to `\WpOrg\Requests\Requests::POST`, for example. + * + * In addition, the `$options` parameter takes the following global options: + * + * - `complete`: A callback for when a request is complete. Takes two + * parameters, a \WpOrg\Requests\Response/\WpOrg\Requests\Exception reference, and the + * ID from the request array (Note: this can also be overridden on a + * per-request basis, although that's a little silly) + * (callback) + * + * @param array $requests Requests data (see description for more information) + * @param array $options Global and default options (see {@see \WpOrg\Requests\Requests::request()}) + * @return array Responses (either \WpOrg\Requests\Response or a \WpOrg\Requests\Exception object) + * + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $requests argument is not an array or iterable object with array access. + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. + */ + public static function request_multiple($requests, $options = []) { + if (InputValidator::has_array_access($requests) === false || InputValidator::is_iterable($requests) === false) { + throw InvalidArgument::create(1, '$requests', 'array|ArrayAccess&Traversable', gettype($requests)); + } + + if (is_array($options) === false) { + throw InvalidArgument::create(2, '$options', 'array', gettype($options)); + } + + $options = array_merge(self::get_default_options(true), $options); + + if (!empty($options['hooks'])) { + $options['hooks']->register('transport.internal.parse_response', [static::class, 'parse_multiple']); + if (!empty($options['complete'])) { + $options['hooks']->register('multiple.request.complete', $options['complete']); + } + } + + foreach ($requests as $id => &$request) { + if (!isset($request['headers'])) { + $request['headers'] = []; + } + + if (!isset($request['data'])) { + $request['data'] = []; + } + + if (!isset($request['type'])) { + $request['type'] = self::GET; + } + + if (!isset($request['options'])) { + $request['options'] = $options; + $request['options']['type'] = $request['type']; + } else { + if (empty($request['options']['type'])) { + $request['options']['type'] = $request['type']; + } + + $request['options'] = array_merge($options, $request['options']); + } + + self::set_defaults($request['url'], $request['headers'], $request['data'], $request['type'], $request['options']); + + // Ensure we only hook in once + if ($request['options']['hooks'] !== $options['hooks']) { + $request['options']['hooks']->register('transport.internal.parse_response', [static::class, 'parse_multiple']); + if (!empty($request['options']['complete'])) { + $request['options']['hooks']->register('multiple.request.complete', $request['options']['complete']); + } + } + } + + unset($request); + + if (!empty($options['transport'])) { + $transport = $options['transport']; + + if (is_string($options['transport'])) { + $transport = new $transport(); + } + } else { + $transport = self::get_transport(); + } + + $responses = $transport->request_multiple($requests, $options); + + foreach ($responses as $id => &$response) { + // If our hook got messed with somehow, ensure we end up with the + // correct response + if (is_string($response)) { + $request = $requests[$id]; + self::parse_multiple($response, $request); + $request['options']['hooks']->dispatch('multiple.request.complete', [&$response, $id]); + } + } + + return $responses; + } + + /** + * Get the default options + * + * @see \WpOrg\Requests\Requests::request() for values returned by this method + * @param boolean $multirequest Is this a multirequest? + * @return array Default option values + */ + protected static function get_default_options($multirequest = false) { + $defaults = static::OPTION_DEFAULTS; + $defaults['verify'] = self::$certificate_path; + + if ($multirequest !== false) { + $defaults['complete'] = null; + } + + return $defaults; + } + + /** + * Get default certificate path. + * + * @return string Default certificate path. + */ + public static function get_certificate_path() { + return self::$certificate_path; + } + + /** + * Set default certificate path. + * + * @param string|Stringable|bool $path Certificate path, pointing to a PEM file. + * + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string, Stringable or boolean. + */ + public static function set_certificate_path($path) { + if (InputValidator::is_string_or_stringable($path) === false && is_bool($path) === false) { + throw InvalidArgument::create(1, '$path', 'string|Stringable|bool', gettype($path)); + } + + self::$certificate_path = $path; + } + + /** + * Set the default values + * + * @param string $url URL to request + * @param array $headers Extra headers to send with the request + * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests + * @param string $type HTTP request type + * @param array $options Options for the request + * @return void $options is updated with the results + * + * @throws \WpOrg\Requests\Exception When the $url is not an http(s) URL. + */ + protected static function set_defaults(&$url, &$headers, &$data, &$type, &$options) { + if (!preg_match('/^http(s)?:\/\//i', $url, $matches)) { + throw new Exception('Only HTTP(S) requests are handled.', 'nonhttp', $url); + } + + if (empty($options['hooks'])) { + $options['hooks'] = new Hooks(); + } + + if (is_array($options['auth'])) { + $options['auth'] = new Basic($options['auth']); + } + + if ($options['auth'] !== false) { + $options['auth']->register($options['hooks']); + } + + if (is_string($options['proxy']) || is_array($options['proxy'])) { + $options['proxy'] = new Http($options['proxy']); + } + + if ($options['proxy'] !== false) { + $options['proxy']->register($options['hooks']); + } + + if (is_array($options['cookies'])) { + $options['cookies'] = new Jar($options['cookies']); + } elseif (empty($options['cookies'])) { + $options['cookies'] = new Jar(); + } + + if ($options['cookies'] !== false) { + $options['cookies']->register($options['hooks']); + } + + if ($options['idn'] !== false) { + $iri = new Iri($url); + $iri->host = IdnaEncoder::encode($iri->ihost); + $url = $iri->uri; + } + + // Massage the type to ensure we support it. + $type = strtoupper($type); + + if (!isset($options['data_format'])) { + if (in_array($type, [self::HEAD, self::GET, self::DELETE], true)) { + $options['data_format'] = 'query'; + } else { + $options['data_format'] = 'body'; + } + } + } + + /** + * HTTP response parser + * + * @param string $headers Full response text including headers and body + * @param string $url Original request URL + * @param array $req_headers Original $headers array passed to {@link request()}, in case we need to follow redirects + * @param array $req_data Original $data array passed to {@link request()}, in case we need to follow redirects + * @param array $options Original $options array passed to {@link request()}, in case we need to follow redirects + * @return \WpOrg\Requests\Response + * + * @throws \WpOrg\Requests\Exception On missing head/body separator (`requests.no_crlf_separator`) + * @throws \WpOrg\Requests\Exception On missing head/body separator (`noversion`) + * @throws \WpOrg\Requests\Exception On missing head/body separator (`toomanyredirects`) + */ + protected static function parse_response($headers, $url, $req_headers, $req_data, $options) { + $return = new Response(); + if (!$options['blocking']) { + return $return; + } + + $return->raw = $headers; + $return->url = (string) $url; + $return->body = ''; + + if (!$options['filename']) { + $pos = strpos($headers, "\r\n\r\n"); + if ($pos === false) { + // Crap! + throw new Exception('Missing header/body separator', 'requests.no_crlf_separator'); + } + + $headers = substr($return->raw, 0, $pos); + // Headers will always be separated from the body by two new lines - `\n\r\n\r`. + $body = substr($return->raw, $pos + 4); + if (!empty($body)) { + $return->body = $body; + } + } + + // Pretend CRLF = LF for compatibility (RFC 2616, section 19.3) + $headers = str_replace("\r\n", "\n", $headers); + // Unfold headers (replace [CRLF] 1*( SP | HT ) with SP) as per RFC 2616 (section 2.2) + $headers = preg_replace('/\n[ \t]/', ' ', $headers); + $headers = explode("\n", $headers); + preg_match('#^HTTP/(1\.\d)[ \t]+(\d+)#i', array_shift($headers), $matches); + if (empty($matches)) { + throw new Exception('Response could not be parsed', 'noversion', $headers); + } + + $return->protocol_version = (float) $matches[1]; + $return->status_code = (int) $matches[2]; + if ($return->status_code >= 200 && $return->status_code < 300) { + $return->success = true; + } + + foreach ($headers as $header) { + list($key, $value) = explode(':', $header, 2); + $value = trim($value); + preg_replace('#(\s+)#i', ' ', $value); + $return->headers[$key] = $value; + } + + if (isset($return->headers['transfer-encoding'])) { + $return->body = self::decode_chunked($return->body); + unset($return->headers['transfer-encoding']); + } + + if (isset($return->headers['content-encoding'])) { + $return->body = self::decompress($return->body); + } + + //fsockopen and cURL compatibility + if (isset($return->headers['connection'])) { + unset($return->headers['connection']); + } + + $options['hooks']->dispatch('requests.before_redirect_check', [&$return, $req_headers, $req_data, $options]); + + if ($return->is_redirect() && $options['follow_redirects'] === true) { + if (isset($return->headers['location']) && $options['redirected'] < $options['redirects']) { + if ($return->status_code === 303) { + $options['type'] = self::GET; + } + + $options['redirected']++; + $location = $return->headers['location']; + if (strpos($location, 'http://') !== 0 && strpos($location, 'https://') !== 0) { + // relative redirect, for compatibility make it absolute + $location = Iri::absolutize($url, $location); + $location = $location->uri; + } + + $hook_args = [ + &$location, + &$req_headers, + &$req_data, + &$options, + $return, + ]; + $options['hooks']->dispatch('requests.before_redirect', $hook_args); + $redirected = self::request($location, $req_headers, $req_data, $options['type'], $options); + $redirected->history[] = $return; + return $redirected; + } elseif ($options['redirected'] >= $options['redirects']) { + throw new Exception('Too many redirects', 'toomanyredirects', $return); + } + } + + $return->redirects = $options['redirected']; + + $options['hooks']->dispatch('requests.after_request', [&$return, $req_headers, $req_data, $options]); + return $return; + } + + /** + * Callback for `transport.internal.parse_response` + * + * Internal use only. Converts a raw HTTP response to a \WpOrg\Requests\Response + * while still executing a multiple request. + * + * @param string $response Full response text including headers and body (will be overwritten with Response instance) + * @param array $request Request data as passed into {@see \WpOrg\Requests\Requests::request_multiple()} + * @return void `$response` is either set to a \WpOrg\Requests\Response instance, or a \WpOrg\Requests\Exception object + */ + public static function parse_multiple(&$response, $request) { + try { + $url = $request['url']; + $headers = $request['headers']; + $data = $request['data']; + $options = $request['options']; + $response = self::parse_response($response, $url, $headers, $data, $options); + } catch (Exception $e) { + $response = $e; + } + } + + /** + * Decoded a chunked body as per RFC 2616 + * + * @link https://tools.ietf.org/html/rfc2616#section-3.6.1 + * @param string $data Chunked body + * @return string Decoded body + */ + protected static function decode_chunked($data) { + if (!preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', trim($data))) { + return $data; + } + + $decoded = ''; + $encoded = $data; + + while (true) { + $is_chunked = (bool) preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', $encoded, $matches); + if (!$is_chunked) { + // Looks like it's not chunked after all + return $data; + } + + $length = hexdec(trim($matches[1])); + if ($length === 0) { + // Ignore trailer headers + return $decoded; + } + + $chunk_length = strlen($matches[0]); + $decoded .= substr($encoded, $chunk_length, $length); + $encoded = substr($encoded, $chunk_length + $length + 2); + + if (trim($encoded) === '0' || empty($encoded)) { + return $decoded; + } + } + + // We'll never actually get down here + // @codeCoverageIgnoreStart + } + // @codeCoverageIgnoreEnd + + /** + * Convert a key => value array to a 'key: value' array for headers + * + * @param iterable $dictionary Dictionary of header values + * @return array List of headers + * + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not iterable. + */ + public static function flatten($dictionary) { + if (InputValidator::is_iterable($dictionary) === false) { + throw InvalidArgument::create(1, '$dictionary', 'iterable', gettype($dictionary)); + } + + $return = []; + foreach ($dictionary as $key => $value) { + $return[] = sprintf('%s: %s', $key, $value); + } + + return $return; + } + + /** + * Decompress an encoded body + * + * Implements gzip, compress and deflate. Guesses which it is by attempting + * to decode. + * + * @param string $data Compressed data in one of the above formats + * @return string Decompressed string + * + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string. + */ + public static function decompress($data) { + if (is_string($data) === false) { + throw InvalidArgument::create(1, '$data', 'string', gettype($data)); + } + + if (trim($data) === '') { + // Empty body does not need further processing. + return $data; + } + + $marker = substr($data, 0, 2); + if (!isset(self::$magic_compression_headers[$marker])) { + // Not actually compressed. Probably cURL ruining this for us. + return $data; + } + + if (function_exists('gzdecode')) { + $decoded = @gzdecode($data); + if ($decoded !== false) { + return $decoded; + } + } + + if (function_exists('gzinflate')) { + $decoded = @gzinflate($data); + if ($decoded !== false) { + return $decoded; + } + } + + $decoded = self::compatible_gzinflate($data); + if ($decoded !== false) { + return $decoded; + } + + if (function_exists('gzuncompress')) { + $decoded = @gzuncompress($data); + if ($decoded !== false) { + return $decoded; + } + } + + return $data; + } + + /** + * Decompression of deflated string while staying compatible with the majority of servers. + * + * Certain Servers will return deflated data with headers which PHP's gzinflate() + * function cannot handle out of the box. The following function has been created from + * various snippets on the gzinflate() PHP documentation. + * + * Warning: Magic numbers within. Due to the potential different formats that the compressed + * data may be returned in, some "magic offsets" are needed to ensure proper decompression + * takes place. For a simple progmatic way to determine the magic offset in use, see: + * https://core.trac.wordpress.org/ticket/18273 + * + * @since 1.6.0 + * @link https://core.trac.wordpress.org/ticket/18273 + * @link https://www.php.net/gzinflate#70875 + * @link https://www.php.net/gzinflate#77336 + * + * @param string $gz_data String to decompress. + * @return string|bool False on failure. + * + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string. + */ + public static function compatible_gzinflate($gz_data) { + if (is_string($gz_data) === false) { + throw InvalidArgument::create(1, '$gz_data', 'string', gettype($gz_data)); + } + + if (trim($gz_data) === '') { + return false; + } + + // Compressed data might contain a full zlib header, if so strip it for + // gzinflate() + if (substr($gz_data, 0, 3) === "\x1f\x8b\x08") { + $i = 10; + $flg = ord(substr($gz_data, 3, 1)); + if ($flg > 0) { + if ($flg & 4) { + list($xlen) = unpack('v', substr($gz_data, $i, 2)); + $i += 2 + $xlen; + } + + if ($flg & 8) { + $i = strpos($gz_data, "\0", $i) + 1; + } + + if ($flg & 16) { + $i = strpos($gz_data, "\0", $i) + 1; + } + + if ($flg & 2) { + $i += 2; + } + } + + $decompressed = self::compatible_gzinflate(substr($gz_data, $i)); + if ($decompressed !== false) { + return $decompressed; + } + } + + // If the data is Huffman Encoded, we must first strip the leading 2 + // byte Huffman marker for gzinflate() + // The response is Huffman coded by many compressors such as + // java.util.zip.Deflater, Ruby's Zlib::Deflate, and .NET's + // System.IO.Compression.DeflateStream. + // + // See https://decompres.blogspot.com/ for a quick explanation of this + // data type + $huffman_encoded = false; + + // low nibble of first byte should be 0x08 + list(, $first_nibble) = unpack('h', $gz_data); + + // First 2 bytes should be divisible by 0x1F + list(, $first_two_bytes) = unpack('n', $gz_data); + + if ($first_nibble === 0x08 && ($first_two_bytes % 0x1F) === 0) { + $huffman_encoded = true; + } + + if ($huffman_encoded) { + $decompressed = @gzinflate(substr($gz_data, 2)); + if ($decompressed !== false) { + return $decompressed; + } + } + + if (substr($gz_data, 0, 4) === "\x50\x4b\x03\x04") { + // ZIP file format header + // Offset 6: 2 bytes, General-purpose field + // Offset 26: 2 bytes, filename length + // Offset 28: 2 bytes, optional field length + // Offset 30: Filename field, followed by optional field, followed + // immediately by data + list(, $general_purpose_flag) = unpack('v', substr($gz_data, 6, 2)); + + // If the file has been compressed on the fly, 0x08 bit is set of + // the general purpose field. We can use this to differentiate + // between a compressed document, and a ZIP file + $zip_compressed_on_the_fly = ((0x08 & $general_purpose_flag) === 0x08); + + if (!$zip_compressed_on_the_fly) { + // Don't attempt to decode a compressed zip file + return $gz_data; + } + + // Determine the first byte of data, based on the above ZIP header + // offsets: + $first_file_start = array_sum(unpack('v2', substr($gz_data, 26, 4))); + $decompressed = @gzinflate(substr($gz_data, 30 + $first_file_start)); + if ($decompressed !== false) { + return $decompressed; + } + + return false; + } + + // Finally fall back to straight gzinflate + $decompressed = @gzinflate($gz_data); + if ($decompressed !== false) { + return $decompressed; + } + + // Fallback for all above failing, not expected, but included for + // debugging and preventing regressions and to track stats + $decompressed = @gzinflate(substr($gz_data, 2)); + if ($decompressed !== false) { + return $decompressed; + } + + return false; + } +} diff --git a/src/wp-includes/Requests/Response.php b/src/wp-includes/Requests/Response.php index 20861dfb82052..8964521a81f8b 100644 --- a/src/wp-includes/Requests/Response.php +++ b/src/wp-includes/Requests/Response.php @@ -2,24 +2,26 @@ /** * HTTP response class * - * Contains a response from Requests::request() + * Contains a response from \WpOrg\Requests\Requests::request() + * * @package Requests */ +namespace WpOrg\Requests; + +use WpOrg\Requests\Cookie\Jar; +use WpOrg\Requests\Exception; +use WpOrg\Requests\Exception\Http; +use WpOrg\Requests\Response\Headers; + /** * HTTP response class * - * Contains a response from Requests::request() + * Contains a response from \WpOrg\Requests\Requests::request() + * * @package Requests */ -class Requests_Response { - /** - * Constructor - */ - public function __construct() { - $this->headers = new Requests_Response_Headers(); - $this->cookies = new Requests_Cookie_Jar(); - } +class Response { /** * Response body @@ -38,9 +40,9 @@ public function __construct() { /** * Headers, as an associative array * - * @var Requests_Response_Headers Array-like object representing headers + * @var \WpOrg\Requests\Response\Headers Array-like object representing headers */ - public $headers = array(); + public $headers = []; /** * Status code, false if non-blocking @@ -80,16 +82,24 @@ public function __construct() { /** * Previous requests (from redirects) * - * @var array Array of Requests_Response objects + * @var array Array of \WpOrg\Requests\Response objects */ - public $history = array(); + public $history = []; /** * Cookies from the request * - * @var Requests_Cookie_Jar Array-like object representing a cookie jar + * @var \WpOrg\Requests\Cookie\Jar Array-like object representing a cookie jar */ - public $cookies = array(); + public $cookies = []; + + /** + * Constructor + */ + public function __construct() { + $this->headers = new Headers(); + $this->cookies = new Jar(); + } /** * Is the response a redirect? @@ -98,25 +108,58 @@ public function __construct() { */ public function is_redirect() { $code = $this->status_code; - return in_array($code, array(300, 301, 302, 303, 307), true) || $code > 307 && $code < 400; + return in_array($code, [300, 301, 302, 303, 307], true) || $code > 307 && $code < 400; } /** * Throws an exception if the request was not successful * - * @throws Requests_Exception If `$allow_redirects` is false, and code is 3xx (`response.no_redirects`) - * @throws Requests_Exception_HTTP On non-successful status code. Exception class corresponds to code (e.g. {@see Requests_Exception_HTTP_404}) * @param boolean $allow_redirects Set to false to throw on a 3xx as well + * + * @throws \WpOrg\Requests\Exception If `$allow_redirects` is false, and code is 3xx (`response.no_redirects`) + * @throws \WpOrg\Requests\Exception\Http On non-successful status code. Exception class corresponds to "Status" + code (e.g. {@see \WpOrg\Requests\Exception\Http\Status404}) */ public function throw_for_status($allow_redirects = true) { if ($this->is_redirect()) { - if (!$allow_redirects) { - throw new Requests_Exception('Redirection not allowed', 'response.no_redirects', $this); + if ($allow_redirects !== true) { + throw new Exception('Redirection not allowed', 'response.no_redirects', $this); } - } - elseif (!$this->success) { - $exception = Requests_Exception_HTTP::get_class($this->status_code); + } elseif (!$this->success) { + $exception = Http::get_class($this->status_code); throw new $exception(null, $this); } } + + /** + * JSON decode the response body. + * + * The method parameters are the same as those for the PHP native `json_decode()` function. + * + * @link https://php.net/json-decode + * + * @param ?bool $associative Optional. When `true`, JSON objects will be returned as associative arrays; + * When `false`, JSON objects will be returned as objects. + * When `null`, JSON objects will be returned as associative arrays + * or objects depending on whether `JSON_OBJECT_AS_ARRAY` is set in the flags. + * Defaults to `true` (in contrast to the PHP native default of `null`). + * @param int $depth Optional. Maximum nesting depth of the structure being decoded. + * Defaults to `512`. + * @param int $options Optional. Bitmask of JSON_BIGINT_AS_STRING, JSON_INVALID_UTF8_IGNORE, + * JSON_INVALID_UTF8_SUBSTITUTE, JSON_OBJECT_AS_ARRAY, JSON_THROW_ON_ERROR. + * Defaults to `0` (no options set). + * + * @return array + * + * @throws \WpOrg\Requests\Exception If `$this->body` is not valid json. + */ + public function decode_body($associative = true, $depth = 512, $options = 0) { + $data = json_decode($this->body, $associative, $depth, $options); + + if (json_last_error() !== JSON_ERROR_NONE) { + $last_error = json_last_error_msg(); + throw new Exception('Unable to parse JSON data: ' . $last_error, 'response.invalid', $this); + } + + return $data; + } } diff --git a/src/wp-includes/Requests/Response/Headers.php b/src/wp-includes/Requests/Response/Headers.php index 12db128ae5df8..eb4f68736b8fd 100644 --- a/src/wp-includes/Requests/Response/Headers.php +++ b/src/wp-includes/Requests/Response/Headers.php @@ -5,68 +5,86 @@ * @package Requests */ +namespace WpOrg\Requests\Response; + +use WpOrg\Requests\Exception; +use WpOrg\Requests\Exception\InvalidArgument; +use WpOrg\Requests\Utility\CaseInsensitiveDictionary; +use WpOrg\Requests\Utility\FilteredIterator; + /** * Case-insensitive dictionary, suitable for HTTP headers * * @package Requests */ -class Requests_Response_Headers extends Requests_Utility_CaseInsensitiveDictionary { +class Headers extends CaseInsensitiveDictionary { /** * Get the given header * - * Unlike {@see self::getValues()}, this returns a string. If there are + * Unlike {@see \WpOrg\Requests\Response\Headers::getValues()}, this returns a string. If there are * multiple values, it concatenates them with a comma as per RFC2616. * * Avoid using this where commas may be used unquoted in values, such as * Set-Cookie headers. * - * @param string $key + * @param string $offset * @return string|null Header value */ - public function offsetGet($key) { - $key = strtolower($key); - if (!isset($this->data[$key])) { + public function offsetGet($offset) { + if (is_string($offset)) { + $offset = strtolower($offset); + } + + if (!isset($this->data[$offset])) { return null; } - return $this->flatten($this->data[$key]); + return $this->flatten($this->data[$offset]); } /** * Set the given item * - * @throws Requests_Exception On attempting to use dictionary as list (`invalidset`) - * - * @param string $key Item name + * @param string $offset Item name * @param string $value Item value + * + * @throws \WpOrg\Requests\Exception On attempting to use dictionary as list (`invalidset`) */ - public function offsetSet($key, $value) { - if ($key === null) { - throw new Requests_Exception('Object is a dictionary, not a list', 'invalidset'); + public function offsetSet($offset, $value) { + if ($offset === null) { + throw new Exception('Object is a dictionary, not a list', 'invalidset'); } - $key = strtolower($key); + if (is_string($offset)) { + $offset = strtolower($offset); + } - if (!isset($this->data[$key])) { - $this->data[$key] = array(); + if (!isset($this->data[$offset])) { + $this->data[$offset] = []; } - $this->data[$key][] = $value; + $this->data[$offset][] = $value; } /** * Get all values for a given header * - * @param string $key + * @param string $offset * @return array|null Header values + * + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not valid as an array key. */ - public function getValues($key) { - $key = strtolower($key); - if (!isset($this->data[$key])) { + public function getValues($offset) { + if (!is_string($offset) && !is_int($offset)) { + throw InvalidArgument::create(1, '$offset', 'string|int', gettype($offset)); + } + + $offset = strtolower($offset); + if (!isset($this->data[$offset])) { return null; } - return $this->data[$key]; + return $this->data[$offset]; } /** @@ -77,22 +95,30 @@ public function getValues($key) { * * @param string|array $value Value to flatten * @return string Flattened value + * + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string or an array. */ public function flatten($value) { + if (is_string($value)) { + return $value; + } + if (is_array($value)) { - $value = implode(',', $value); + return implode(',', $value); } - return $value; + throw InvalidArgument::create(1, '$value', 'string|array', gettype($value)); } /** * Get an iterator for the data * - * Converts the internal - * @return ArrayIterator + * Converts the internally stored values to a comma-separated string if there is more + * than one value for a key. + * + * @return \ArrayIterator */ public function getIterator() { - return new Requests_Utility_FilteredIterator($this->data, array($this, 'flatten')); + return new FilteredIterator($this->data, [$this, 'flatten']); } } diff --git a/src/wp-includes/Requests/Session.php b/src/wp-includes/Requests/Session.php index b2e10991d6cc4..000d2526d40de 100644 --- a/src/wp-includes/Requests/Session.php +++ b/src/wp-includes/Requests/Session.php @@ -2,10 +2,17 @@ /** * Session handler for persistent requests and default parameters * - * @package Requests - * @subpackage Session Handler + * @package Requests\SessionHandler */ +namespace WpOrg\Requests; + +use WpOrg\Requests\Cookie\Jar; +use WpOrg\Requests\Exception\InvalidArgument; +use WpOrg\Requests\Iri; +use WpOrg\Requests\Requests; +use WpOrg\Requests\Utility\InputValidator; + /** * Session handler for persistent requests and default parameters * @@ -14,10 +21,9 @@ * with all subrequests resolved from this. Base options can be set (including * a shared cookie jar), then overridden for individual requests. * - * @package Requests - * @subpackage Session Handler + * @package Requests\SessionHandler */ -class Requests_Session { +class Session { /** * Base URL for requests * @@ -32,7 +38,7 @@ class Requests_Session { * * @var array */ - public $headers = array(); + public $headers = []; /** * Base data for requests @@ -42,7 +48,7 @@ class Requests_Session { * * @var array */ - public $data = array(); + public $data = []; /** * Base options for requests @@ -55,36 +61,57 @@ class Requests_Session { * * @var array */ - public $options = array(); + public $options = []; /** * Create a new session * - * @param string|null $url Base URL for requests + * @param string|Stringable|null $url Base URL for requests * @param array $headers Default headers for requests * @param array $data Default data for requests * @param array $options Default options for requests + * + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string, Stringable or null. + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $headers argument is not an array. + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $data argument is not an array. + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. */ - public function __construct($url = null, $headers = array(), $data = array(), $options = array()) { + public function __construct($url = null, $headers = [], $data = [], $options = []) { + if ($url !== null && InputValidator::is_string_or_stringable($url) === false) { + throw InvalidArgument::create(1, '$url', 'string|Stringable|null', gettype($url)); + } + + if (is_array($headers) === false) { + throw InvalidArgument::create(2, '$headers', 'array', gettype($headers)); + } + + if (is_array($data) === false) { + throw InvalidArgument::create(3, '$data', 'array', gettype($data)); + } + + if (is_array($options) === false) { + throw InvalidArgument::create(4, '$options', 'array', gettype($options)); + } + $this->url = $url; $this->headers = $headers; $this->data = $data; $this->options = $options; if (empty($this->options['cookies'])) { - $this->options['cookies'] = new Requests_Cookie_Jar(); + $this->options['cookies'] = new Jar(); } } /** * Get a property's value * - * @param string $key Property key + * @param string $name Property name. * @return mixed|null Property value, null if none found */ - public function __get($key) { - if (isset($this->options[$key])) { - return $this->options[$key]; + public function __get($name) { + if (isset($this->options[$name])) { + return $this->options[$name]; } return null; @@ -93,93 +120,91 @@ public function __get($key) { /** * Set a property's value * - * @param string $key Property key + * @param string $name Property name. * @param mixed $value Property value */ - public function __set($key, $value) { - $this->options[$key] = $value; + public function __set($name, $value) { + $this->options[$name] = $value; } /** * Remove a property's value * - * @param string $key Property key + * @param string $name Property name. */ - public function __isset($key) { - return isset($this->options[$key]); + public function __isset($name) { + return isset($this->options[$name]); } /** * Remove a property's value * - * @param string $key Property key + * @param string $name Property name. */ - public function __unset($key) { - if (isset($this->options[$key])) { - unset($this->options[$key]); - } + public function __unset($name) { + unset($this->options[$name]); } /**#@+ - * @see request() + * @see \WpOrg\Requests\Session::request() * @param string $url * @param array $headers * @param array $options - * @return Requests_Response + * @return \WpOrg\Requests\Response */ /** * Send a GET request */ - public function get($url, $headers = array(), $options = array()) { + public function get($url, $headers = [], $options = []) { return $this->request($url, $headers, null, Requests::GET, $options); } /** * Send a HEAD request */ - public function head($url, $headers = array(), $options = array()) { + public function head($url, $headers = [], $options = []) { return $this->request($url, $headers, null, Requests::HEAD, $options); } /** * Send a DELETE request */ - public function delete($url, $headers = array(), $options = array()) { + public function delete($url, $headers = [], $options = []) { return $this->request($url, $headers, null, Requests::DELETE, $options); } /**#@-*/ /**#@+ - * @see request() + * @see \WpOrg\Requests\Session::request() * @param string $url * @param array $headers * @param array $data * @param array $options - * @return Requests_Response + * @return \WpOrg\Requests\Response */ /** * Send a POST request */ - public function post($url, $headers = array(), $data = array(), $options = array()) { + public function post($url, $headers = [], $data = [], $options = []) { return $this->request($url, $headers, $data, Requests::POST, $options); } /** * Send a PUT request */ - public function put($url, $headers = array(), $data = array(), $options = array()) { + public function put($url, $headers = [], $data = [], $options = []) { return $this->request($url, $headers, $data, Requests::PUT, $options); } /** * Send a PATCH request * - * Note: Unlike {@see post} and {@see put}, `$headers` is required, as the - * specification recommends that should send an ETag + * Note: Unlike {@see \WpOrg\Requests\Session::post()} and {@see \WpOrg\Requests\Session::put()}, + * `$headers` is required, as the specification recommends that should send an ETag * * @link https://tools.ietf.org/html/rfc5789 */ - public function patch($url, $headers, $data = array(), $options = array()) { + public function patch($url, $headers, $data = [], $options = []) { return $this->request($url, $headers, $data, Requests::PATCH, $options); } /**#@-*/ @@ -190,18 +215,18 @@ public function patch($url, $headers, $data = array(), $options = array()) { * This method initiates a request and sends it via a transport before * parsing. * - * @see Requests::request() - * - * @throws Requests_Exception On invalid URLs (`nonhttp`) + * @see \WpOrg\Requests\Requests::request() * * @param string $url URL to request * @param array $headers Extra headers to send with the request * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests - * @param string $type HTTP request type (use Requests constants) - * @param array $options Options for the request (see {@see Requests::request}) - * @return Requests_Response + * @param string $type HTTP request type (use \WpOrg\Requests\Requests constants) + * @param array $options Options for the request (see {@see \WpOrg\Requests\Requests::request()}) + * @return \WpOrg\Requests\Response + * + * @throws \WpOrg\Requests\Exception On invalid URLs (`nonhttp`) */ - public function request($url, $headers = array(), $data = array(), $type = Requests::GET, $options = array()) { + public function request($url, $headers = [], $data = [], $type = Requests::GET, $options = []) { $request = $this->merge_request(compact('url', 'headers', 'data', 'options')); return Requests::request($request['url'], $request['headers'], $request['data'], $type, $request['options']); @@ -210,13 +235,24 @@ public function request($url, $headers = array(), $data = array(), $type = Reque /** * Send multiple HTTP requests simultaneously * - * @see Requests::request_multiple() + * @see \WpOrg\Requests\Requests::request_multiple() * - * @param array $requests Requests data (see {@see Requests::request_multiple}) - * @param array $options Global and default options (see {@see Requests::request}) - * @return array Responses (either Requests_Response or a Requests_Exception object) + * @param array $requests Requests data (see {@see \WpOrg\Requests\Requests::request_multiple()}) + * @param array $options Global and default options (see {@see \WpOrg\Requests\Requests::request()}) + * @return array Responses (either \WpOrg\Requests\Response or a \WpOrg\Requests\Exception object) + * + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $requests argument is not an array or iterable object with array access. + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. */ - public function request_multiple($requests, $options = array()) { + public function request_multiple($requests, $options = []) { + if (InputValidator::has_array_access($requests) === false || InputValidator::is_iterable($requests) === false) { + throw InvalidArgument::create(1, '$requests', 'array|ArrayAccess&Traversable', gettype($requests)); + } + + if (is_array($options) === false) { + throw InvalidArgument::create(2, '$options', 'array', gettype($options)); + } + foreach ($requests as $key => $request) { $requests[$key] = $this->merge_request($request, false); } @@ -232,31 +268,31 @@ public function request_multiple($requests, $options = array()) { /** * Merge a request's data with the default data * - * @param array $request Request data (same form as {@see request_multiple}) + * @param array $request Request data (same form as {@see \WpOrg\Requests\Session::request_multiple()}) * @param boolean $merge_options Should we merge options as well? * @return array Request data */ protected function merge_request($request, $merge_options = true) { if ($this->url !== null) { - $request['url'] = Requests_IRI::absolutize($this->url, $request['url']); + $request['url'] = Iri::absolutize($this->url, $request['url']); $request['url'] = $request['url']->uri; } if (empty($request['headers'])) { - $request['headers'] = array(); + $request['headers'] = []; } + $request['headers'] = array_merge($this->headers, $request['headers']); if (empty($request['data'])) { if (is_array($this->data)) { $request['data'] = $this->data; } - } - elseif (is_array($request['data']) && is_array($this->data)) { + } elseif (is_array($request['data']) && is_array($this->data)) { $request['data'] = array_merge($this->data, $request['data']); } - if ($merge_options !== false) { + if ($merge_options === true) { $request['options'] = array_merge($this->options, $request['options']); // Disallow forcing the type, as that's a per request setting diff --git a/src/wp-includes/Requests/SSL.php b/src/wp-includes/Requests/Ssl.php similarity index 55% rename from src/wp-includes/Requests/SSL.php rename to src/wp-includes/Requests/Ssl.php index f7ecf3fbdba9e..99da11d8f7148 100644 --- a/src/wp-includes/Requests/SSL.php +++ b/src/wp-includes/Requests/Ssl.php @@ -2,37 +2,49 @@ /** * SSL utilities for Requests * - * @package Requests - * @subpackage Utilities + * @package Requests\Utilities */ +namespace WpOrg\Requests; + +use WpOrg\Requests\Exception\InvalidArgument; +use WpOrg\Requests\Utility\InputValidator; + /** * SSL utilities for Requests * * Collection of utilities for working with and verifying SSL certificates. * - * @package Requests - * @subpackage Utilities + * @package Requests\Utilities */ -class Requests_SSL { +final class Ssl { /** * Verify the certificate against common name and subject alternative names * * Unfortunately, PHP doesn't check the certificate against the alternative * names, leading things like 'https://www.github.com/' to be invalid. * - * @see https://tools.ietf.org/html/rfc2818#section-3.1 RFC2818, Section 3.1 + * @link https://tools.ietf.org/html/rfc2818#section-3.1 RFC2818, Section 3.1 * - * @throws Requests_Exception On not obtaining a match for the host (`fsockopen.ssl.no_match`) - * @param string $host Host name to verify against + * @param string|Stringable $host Host name to verify against * @param array $cert Certificate data from openssl_x509_parse() * @return bool + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $host argument is not a string or a stringable object. + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $cert argument is not an array or array accessible. */ public static function verify_certificate($host, $cert) { + if (InputValidator::is_string_or_stringable($host) === false) { + throw InvalidArgument::create(1, '$host', 'string|Stringable', gettype($host)); + } + + if (InputValidator::has_array_access($cert) === false) { + throw InvalidArgument::create(2, '$cert', 'array|ArrayAccess', gettype($cert)); + } + $has_dns_alt = false; // Check the subjectAltName - if (!empty($cert['extensions']) && !empty($cert['extensions']['subjectAltName'])) { + if (!empty($cert['extensions']['subjectAltName'])) { $altnames = explode(',', $cert['extensions']['subjectAltName']); foreach ($altnames as $altname) { $altname = trim($altname); @@ -50,15 +62,17 @@ public static function verify_certificate($host, $cert) { return true; } } + + if ($has_dns_alt === true) { + return false; + } } // Fall back to checking the common name if we didn't get any dNSName // alt names, as per RFC2818 - if (!$has_dns_alt && !empty($cert['subject']['CN'])) { + if (!empty($cert['subject']['CN'])) { // Check for a match - if (self::match_domain($host, $cert['subject']['CN']) === true) { - return true; - } + return (self::match_domain($host, $cert['subject']['CN']) === true); } return false; @@ -77,11 +91,29 @@ public static function verify_certificate($host, $cert) { * character to be the full first component; that is, with the exclusion of * the third rule. * - * @param string $reference Reference dNSName + * @param string|Stringable $reference Reference dNSName * @return boolean Is the name valid? + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string or a stringable object. */ public static function verify_reference_name($reference) { + if (InputValidator::is_string_or_stringable($reference) === false) { + throw InvalidArgument::create(1, '$reference', 'string|Stringable', gettype($reference)); + } + + if ($reference === '') { + return false; + } + + if (preg_match('`\s`', $reference) > 0) { + // Whitespace detected. This can never be a dNSName. + return false; + } + $parts = explode('.', $reference); + if ($parts !== array_filter($parts)) { + // DNSName cannot contain two dots next to each other. + return false; + } // Check the first part of the name $first = array_shift($parts); @@ -112,29 +144,35 @@ public static function verify_reference_name($reference) { /** * Match a hostname against a dNSName reference * - * @param string $host Requested host - * @param string $reference dNSName to match against + * @param string|Stringable $host Requested host + * @param string|Stringable $reference dNSName to match against * @return boolean Does the domain match? + * @throws \WpOrg\Requests\Exception\InvalidArgument When either of the passed arguments is not a string or a stringable object. */ public static function match_domain($host, $reference) { + if (InputValidator::is_string_or_stringable($host) === false) { + throw InvalidArgument::create(1, '$host', 'string|Stringable', gettype($host)); + } + // Check if the reference is blocklisted first if (self::verify_reference_name($reference) !== true) { return false; } // Check for a direct match - if ($host === $reference) { + if ((string) $host === (string) $reference) { return true; } // Calculate the valid wildcard match if the host is not an IP address - // Also validates that the host has 3 parts or more, as per Firefox's - // ruleset. + // Also validates that the host has 3 parts or more, as per Firefox's ruleset, + // as a wildcard reference is only allowed with 3 parts or more, so the + // comparison will never match if host doesn't contain 3 parts or more as well. if (ip2long($host) === false) { $parts = explode('.', $host); $parts[0] = '*'; $wildcard = implode('.', $parts); - if ($wildcard === $reference) { + if ($wildcard === (string) $reference) { return true; } } diff --git a/src/wp-includes/Requests/Transport.php b/src/wp-includes/Requests/Transport.php index 5146c01d87faf..f2e1c6ed737dc 100644 --- a/src/wp-includes/Requests/Transport.php +++ b/src/wp-includes/Requests/Transport.php @@ -2,40 +2,44 @@ /** * Base HTTP transport * - * @package Requests - * @subpackage Transport + * @package Requests\Transport */ +namespace WpOrg\Requests; + /** * Base HTTP transport * - * @package Requests - * @subpackage Transport + * @package Requests\Transport */ -interface Requests_Transport { +interface Transport { /** * Perform a request * * @param string $url URL to request * @param array $headers Associative array of request headers * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD - * @param array $options Request options, see {@see Requests::response()} for documentation + * @param array $options Request options, see {@see \WpOrg\Requests\Requests::response()} for documentation * @return string Raw HTTP result */ - public function request($url, $headers = array(), $data = array(), $options = array()); + public function request($url, $headers = [], $data = [], $options = []); /** * Send multiple requests simultaneously * - * @param array $requests Request data (array of 'url', 'headers', 'data', 'options') as per {@see Requests_Transport::request} - * @param array $options Global options, see {@see Requests::response()} for documentation - * @return array Array of Requests_Response objects (may contain Requests_Exception or string responses as well) + * @param array $requests Request data (array of 'url', 'headers', 'data', 'options') as per {@see \WpOrg\Requests\Transport::request()} + * @param array $options Global options, see {@see \WpOrg\Requests\Requests::response()} for documentation + * @return array Array of \WpOrg\Requests\Response objects (may contain \WpOrg\Requests\Exception or string responses as well) */ public function request_multiple($requests, $options); /** - * Self-test whether the transport can be used - * @return bool + * Self-test whether the transport can be used. + * + * The available capabilities to test for can be found in {@see \WpOrg\Requests\Capability}. + * + * @param array $capabilities Optional. Associative array of capabilities to test against, i.e. `['' => true]`. + * @return bool Whether the transport can be used. */ - public static function test(); + public static function test($capabilities = []); } diff --git a/src/wp-includes/Requests/Transport/cURL.php b/src/wp-includes/Requests/Transport/Curl.php similarity index 69% rename from src/wp-includes/Requests/Transport/cURL.php rename to src/wp-includes/Requests/Transport/Curl.php index 01bcf3b719f1c..8b0a13080e430 100644 --- a/src/wp-includes/Requests/Transport/cURL.php +++ b/src/wp-includes/Requests/Transport/Curl.php @@ -2,17 +2,27 @@ /** * cURL HTTP transport * - * @package Requests - * @subpackage Transport + * @package Requests\Transport */ +namespace WpOrg\Requests\Transport; + +use RecursiveArrayIterator; +use RecursiveIteratorIterator; +use WpOrg\Requests\Capability; +use WpOrg\Requests\Exception; +use WpOrg\Requests\Exception\InvalidArgument; +use WpOrg\Requests\Exception\Transport\Curl as CurlException; +use WpOrg\Requests\Requests; +use WpOrg\Requests\Transport; +use WpOrg\Requests\Utility\InputValidator; + /** * cURL HTTP transport * - * @package Requests - * @subpackage Transport + * @package Requests\Transport */ -class Requests_Transport_cURL implements Requests_Transport { +final class Curl implements Transport { const CURL_7_10_5 = 0x070A05; const CURL_7_16_2 = 0x071002; @@ -33,7 +43,7 @@ class Requests_Transport_cURL implements Requests_Transport { /** * Information on the current request * - * @var array cURL information array, see {@see https://secure.php.net/curl_getinfo} + * @var array cURL information array, see {@link https://www.php.net/curl_getinfo} */ public $info; @@ -47,44 +57,44 @@ class Requests_Transport_cURL implements Requests_Transport { /** * cURL handle * - * @var resource + * @var resource|\CurlHandle Resource in PHP < 8.0, Instance of CurlHandle in PHP >= 8.0. */ - protected $handle; + private $handle; /** * Hook dispatcher instance * - * @var Requests_Hooks + * @var \WpOrg\Requests\Hooks */ - protected $hooks; + private $hooks; /** * Have we finished the headers yet? * * @var boolean */ - protected $done_headers = false; + private $done_headers = false; /** * If streaming to a file, keep the file pointer * * @var resource */ - protected $stream_handle; + private $stream_handle; /** * How many bytes are in the response body? * * @var int */ - protected $response_bytes; + private $response_bytes; /** * What's the maximum number of bytes we should keep? * * @var int|bool Byte count, or false if no limit. */ - protected $response_byte_limit; + private $response_byte_limit; /** * Constructor @@ -99,10 +109,12 @@ public function __construct() { if ($this->version >= self::CURL_7_10_5) { curl_setopt($this->handle, CURLOPT_ENCODING, ''); } + if (defined('CURLOPT_PROTOCOLS')) { // phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_protocolsFound curl_setopt($this->handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); } + if (defined('CURLOPT_REDIR_PROTOCOLS')) { // phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_redir_protocolsFound curl_setopt($this->handle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); @@ -121,23 +133,52 @@ public function __destruct() { /** * Perform a request * - * @throws Requests_Exception On a cURL error (`curlerror`) - * - * @param string $url URL to request + * @param string|Stringable $url URL to request * @param array $headers Associative array of request headers * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD - * @param array $options Request options, see {@see Requests::response()} for documentation + * @param array $options Request options, see {@see \WpOrg\Requests\Requests::response()} for documentation * @return string Raw HTTP result + * + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string or Stringable. + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $headers argument is not an array. + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $data parameter is not an array or string. + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. + * @throws \WpOrg\Requests\Exception On a cURL error (`curlerror`) */ - public function request($url, $headers = array(), $data = array(), $options = array()) { + public function request($url, $headers = [], $data = [], $options = []) { + if (InputValidator::is_string_or_stringable($url) === false) { + throw InvalidArgument::create(1, '$url', 'string|Stringable', gettype($url)); + } + + if (is_array($headers) === false) { + throw InvalidArgument::create(2, '$headers', 'array', gettype($headers)); + } + + if (!is_array($data) && !is_string($data)) { + if ($data === null) { + $data = ''; + } else { + throw InvalidArgument::create(3, '$data', 'array|string', gettype($data)); + } + } + + if (is_array($options) === false) { + throw InvalidArgument::create(4, '$options', 'array', gettype($options)); + } + $this->hooks = $options['hooks']; $this->setup_handle($url, $headers, $data, $options); - $options['hooks']->dispatch('curl.before_send', array(&$this->handle)); + $options['hooks']->dispatch('curl.before_send', [&$this->handle]); if ($options['filename'] !== false) { - $this->stream_handle = fopen($options['filename'], 'wb'); + // phpcs:ignore WordPress.PHP.NoSilencedErrors -- Silenced the PHP native warning in favour of throwing an exception. + $this->stream_handle = @fopen($options['filename'], 'wb'); + if ($this->stream_handle === false) { + $error = error_get_last(); + throw new Exception($error['message'], 'fopen'); + } } $this->response_data = ''; @@ -151,8 +192,7 @@ public function request($url, $headers = array(), $data = array(), $options = ar if ($options['verify'] === false) { curl_setopt($this->handle, CURLOPT_SSL_VERIFYHOST, 0); curl_setopt($this->handle, CURLOPT_SSL_VERIFYPEER, 0); - } - elseif (is_string($options['verify'])) { + } elseif (is_string($options['verify'])) { curl_setopt($this->handle, CURLOPT_CAINFO, $options['verify']); } } @@ -164,9 +204,9 @@ public function request($url, $headers = array(), $data = array(), $options = ar curl_exec($this->handle); $response = $this->response_data; - $options['hooks']->dispatch('curl.after_send', array()); + $options['hooks']->dispatch('curl.after_send', []); - if (curl_errno($this->handle) === 23 || curl_errno($this->handle) === 61) { + if (curl_errno($this->handle) === CURLE_WRITE_ERROR || curl_errno($this->handle) === CURLE_BAD_CONTENT_ENCODING) { // Reset encoding and try again curl_setopt($this->handle, CURLOPT_ENCODING, 'none'); @@ -179,7 +219,7 @@ public function request($url, $headers = array(), $data = array(), $options = ar $this->process_response($response, $options); // Need to remove the $this reference from the curl handle. - // Otherwise Requests_Transport_cURL wont be garbage collected and the curl_close() will never be called. + // Otherwise \WpOrg\Requests\Transport\Curl won't be garbage collected and the curl_close() will never be called. curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, null); curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, null); @@ -191,41 +231,51 @@ public function request($url, $headers = array(), $data = array(), $options = ar * * @param array $requests Request data * @param array $options Global options - * @return array Array of Requests_Response objects (may contain Requests_Exception or string responses as well) + * @return array Array of \WpOrg\Requests\Response objects (may contain \WpOrg\Requests\Exception or string responses as well) + * + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $requests argument is not an array or iterable object with array access. + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. */ public function request_multiple($requests, $options) { // If you're not requesting, we can't get any responses ¯\_(ツ)_/¯ if (empty($requests)) { - return array(); + return []; + } + + if (InputValidator::has_array_access($requests) === false || InputValidator::is_iterable($requests) === false) { + throw InvalidArgument::create(1, '$requests', 'array|ArrayAccess&Traversable', gettype($requests)); + } + + if (is_array($options) === false) { + throw InvalidArgument::create(2, '$options', 'array', gettype($options)); } $multihandle = curl_multi_init(); - $subrequests = array(); - $subhandles = array(); + $subrequests = []; + $subhandles = []; $class = get_class($this); foreach ($requests as $id => $request) { $subrequests[$id] = new $class(); $subhandles[$id] = $subrequests[$id]->get_subrequest_handle($request['url'], $request['headers'], $request['data'], $request['options']); - $request['options']['hooks']->dispatch('curl.before_multi_add', array(&$subhandles[$id])); + $request['options']['hooks']->dispatch('curl.before_multi_add', [&$subhandles[$id]]); curl_multi_add_handle($multihandle, $subhandles[$id]); } $completed = 0; - $responses = array(); + $responses = []; $subrequestcount = count($subrequests); - $request['options']['hooks']->dispatch('curl.before_multi_exec', array(&$multihandle)); + $request['options']['hooks']->dispatch('curl.before_multi_exec', [&$multihandle]); do { $active = 0; do { $status = curl_multi_exec($multihandle, $active); - } - while ($status === CURLM_CALL_MULTI_PERFORM); + } while ($status === CURLM_CALL_MULTI_PERFORM); - $to_process = array(); + $to_process = []; // Read the information as needed while ($done = curl_multi_info_read($multihandle)) { @@ -241,33 +291,32 @@ public function request_multiple($requests, $options) { if ($done['result'] !== CURLE_OK) { //get error string for handle. $reason = curl_error($done['handle']); - $exception = new Requests_Exception_Transport_cURL( + $exception = new CurlException( $reason, - Requests_Exception_Transport_cURL::EASY, + CurlException::EASY, $done['handle'], $done['result'] ); $responses[$key] = $exception; - $options['hooks']->dispatch('transport.internal.parse_error', array(&$responses[$key], $requests[$key])); - } - else { + $options['hooks']->dispatch('transport.internal.parse_error', [&$responses[$key], $requests[$key]]); + } else { $responses[$key] = $subrequests[$key]->process_response($subrequests[$key]->response_data, $options); - $options['hooks']->dispatch('transport.internal.parse_response', array(&$responses[$key], $requests[$key])); + $options['hooks']->dispatch('transport.internal.parse_response', [&$responses[$key], $requests[$key]]); } curl_multi_remove_handle($multihandle, $done['handle']); curl_close($done['handle']); if (!is_string($responses[$key])) { - $options['hooks']->dispatch('multiple.request.complete', array(&$responses[$key], $key)); + $options['hooks']->dispatch('multiple.request.complete', [&$responses[$key], $key]); } + $completed++; } - } - while ($active || $completed < $subrequestcount); + } while ($active || $completed < $subrequestcount); - $request['options']['hooks']->dispatch('curl.after_multi_exec', array(&$multihandle)); + $request['options']['hooks']->dispatch('curl.after_multi_exec', [&$multihandle]); curl_multi_close($multihandle); @@ -280,8 +329,8 @@ public function request_multiple($requests, $options) { * @param string $url URL to request * @param array $headers Associative array of request headers * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD - * @param array $options Request options, see {@see Requests::response()} for documentation - * @return resource Subrequest's cURL handle + * @param array $options Request options, see {@see \WpOrg\Requests\Requests::response()} for documentation + * @return resource|\CurlHandle Subrequest's cURL handle */ public function &get_subrequest_handle($url, $headers, $data, $options) { $this->setup_handle($url, $headers, $data, $options); @@ -296,6 +345,7 @@ public function &get_subrequest_handle($url, $headers, $data, $options) { if ($options['max_bytes'] !== false) { $this->response_byte_limit = $options['max_bytes']; } + $this->hooks = $options['hooks']; return $this->handle; @@ -307,10 +357,10 @@ public function &get_subrequest_handle($url, $headers, $data, $options) { * @param string $url URL to request * @param array $headers Associative array of request headers * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD - * @param array $options Request options, see {@see Requests::response()} for documentation + * @param array $options Request options, see {@see \WpOrg\Requests\Requests::response()} for documentation */ - protected function setup_handle($url, $headers, $data, $options) { - $options['hooks']->dispatch('curl.before_request', array(&$this->handle)); + private function setup_handle($url, $headers, $data, $options) { + $options['hooks']->dispatch('curl.before_request', [&$this->handle]); // Force closing the connection for old versions of cURL (<7.22). if (!isset($headers['Connection'])) { @@ -340,9 +390,8 @@ protected function setup_handle($url, $headers, $data, $options) { if ($data_format === 'query') { $url = self::format_get($url, $data); $data = ''; - } - elseif (!is_string($data)) { - $data = http_build_query($data, null, '&'); + } elseif (!is_string($data)) { + $data = http_build_query($data, '', '&'); } } @@ -379,35 +428,33 @@ protected function setup_handle($url, $headers, $data, $options) { if (is_int($timeout) || $this->version < self::CURL_7_16_2) { curl_setopt($this->handle, CURLOPT_TIMEOUT, ceil($timeout)); - } - else { + } else { // phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_timeout_msFound curl_setopt($this->handle, CURLOPT_TIMEOUT_MS, round($timeout * 1000)); } if (is_int($options['connect_timeout']) || $this->version < self::CURL_7_16_2) { curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT, ceil($options['connect_timeout'])); - } - else { + } else { // phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_connecttimeout_msFound curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT_MS, round($options['connect_timeout'] * 1000)); } + curl_setopt($this->handle, CURLOPT_URL, $url); - curl_setopt($this->handle, CURLOPT_REFERER, $url); curl_setopt($this->handle, CURLOPT_USERAGENT, $options['useragent']); if (!empty($headers)) { curl_setopt($this->handle, CURLOPT_HTTPHEADER, $headers); } + if ($options['protocol_version'] === 1.1) { curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); - } - else { + } else { curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); } if ($options['blocking'] === true) { - curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, array($this, 'stream_headers')); - curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, array($this, 'stream_body')); + curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, [$this, 'stream_headers']); + curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, [$this, 'stream_body']); curl_setopt($this->handle, CURLOPT_BUFFERSIZE, Requests::BUFFER_SIZE); } } @@ -418,19 +465,19 @@ protected function setup_handle($url, $headers, $data, $options) { * @param string $response Response data from the body * @param array $options Request options * @return string|false HTTP response data including headers. False if non-blocking. - * @throws Requests_Exception + * @throws \WpOrg\Requests\Exception */ public function process_response($response, $options) { if ($options['blocking'] === false) { $fake_headers = ''; - $options['hooks']->dispatch('curl.after_request', array(&$fake_headers)); + $options['hooks']->dispatch('curl.after_request', [&$fake_headers]); return false; } + if ($options['filename'] !== false && $this->stream_handle) { fclose($this->stream_handle); $this->headers = trim($this->headers); - } - else { + } else { $this->headers .= $response; } @@ -440,18 +487,19 @@ public function process_response($response, $options) { curl_errno($this->handle), curl_error($this->handle) ); - throw new Requests_Exception($error, 'curlerror', $this->handle); + throw new Exception($error, 'curlerror', $this->handle); } + $this->info = curl_getinfo($this->handle); - $options['hooks']->dispatch('curl.after_request', array(&$this->headers, &$this->info)); + $options['hooks']->dispatch('curl.after_request', [&$this->headers, &$this->info]); return $this->headers; } /** * Collect the headers as they are received * - * @param resource $handle cURL resource + * @param resource|\CurlHandle $handle cURL handle * @param string $headers Header string * @return integer Length of provided header */ @@ -463,11 +511,13 @@ public function stream_headers($handle, $headers) { $this->headers = ''; $this->done_headers = false; } + $this->headers .= $headers; if ($headers === "\r\n") { $this->done_headers = true; } + return strlen($headers); } @@ -476,12 +526,12 @@ public function stream_headers($handle, $headers) { * * @since 1.6.1 * - * @param resource $handle cURL resource + * @param resource|\CurlHandle $handle cURL handle * @param string $data Body data * @return integer Length of provided data */ public function stream_body($handle, $data) { - $this->hooks->dispatch('request.progress', array($data, $this->response_bytes, $this->response_byte_limit)); + $this->hooks->dispatch('request.progress', [$data, $this->response_bytes, $this->response_byte_limit]); $data_length = strlen($data); // Are we limiting the response size? @@ -500,8 +550,7 @@ public function stream_body($handle, $data) { if ($this->stream_handle) { fwrite($this->stream_handle, $data); - } - else { + } else { $this->response_data .= $data; } @@ -513,46 +562,48 @@ public function stream_body($handle, $data) { * Format a URL given GET data * * @param string $url - * @param array|object $data Data to build query using, see {@see https://secure.php.net/http_build_query} + * @param array|object $data Data to build query using, see {@link https://www.php.net/http_build_query} * @return string URL with data */ - protected static function format_get($url, $data) { + private static function format_get($url, $data) { if (!empty($data)) { $query = ''; $url_parts = parse_url($url); if (empty($url_parts['query'])) { $url_parts['query'] = ''; - } - else { + } else { $query = $url_parts['query']; } - $query .= '&' . http_build_query($data, null, '&'); + $query .= '&' . http_build_query($data, '', '&'); $query = trim($query, '&'); if (empty($url_parts['query'])) { $url .= '?' . $query; - } - else { + } else { $url = str_replace($url_parts['query'], $query, $url); } } + return $url; } /** - * Whether this transport is valid + * Self-test whether the transport can be used. + * + * The available capabilities to test for can be found in {@see \WpOrg\Requests\Capability}. * * @codeCoverageIgnore - * @return boolean True if the transport is valid, false otherwise. + * @param array $capabilities Optional. Associative array of capabilities to test against, i.e. `['' => true]`. + * @return bool Whether the transport can be used. */ - public static function test($capabilities = array()) { + public static function test($capabilities = []) { if (!function_exists('curl_init') || !function_exists('curl_exec')) { return false; } // If needed, check that our installed curl version supports SSL - if (isset($capabilities['ssl']) && $capabilities['ssl']) { + if (isset($capabilities[Capability::SSL]) && $capabilities[Capability::SSL]) { $curl_version = curl_version(); if (!(CURL_VERSION_SSL & $curl_version['features'])) { return false; @@ -568,7 +619,7 @@ public static function test($capabilities = array()) { * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD. * @return string The "Expect" header. */ - protected function get_expect_header($data) { + private function get_expect_header($data) { if (!is_array($data)) { return strlen((string) $data) >= 1048576 ? '100-Continue' : ''; } diff --git a/src/wp-includes/Requests/Transport/fsockopen.php b/src/wp-includes/Requests/Transport/Fsockopen.php similarity index 59% rename from src/wp-includes/Requests/Transport/fsockopen.php rename to src/wp-includes/Requests/Transport/Fsockopen.php index 56f94c99f2390..c3bd4a63d5dbc 100644 --- a/src/wp-includes/Requests/Transport/fsockopen.php +++ b/src/wp-includes/Requests/Transport/Fsockopen.php @@ -2,17 +2,27 @@ /** * fsockopen HTTP transport * - * @package Requests - * @subpackage Transport + * @package Requests\Transport */ +namespace WpOrg\Requests\Transport; + +use WpOrg\Requests\Capability; +use WpOrg\Requests\Exception; +use WpOrg\Requests\Exception\InvalidArgument; +use WpOrg\Requests\Port; +use WpOrg\Requests\Requests; +use WpOrg\Requests\Ssl; +use WpOrg\Requests\Transport; +use WpOrg\Requests\Utility\CaseInsensitiveDictionary; +use WpOrg\Requests\Utility\InputValidator; + /** * fsockopen HTTP transport * - * @package Requests - * @subpackage Transport + * @package Requests\Transport */ -class Requests_Transport_fsockopen implements Requests_Transport { +final class Fsockopen implements Transport { /** * Second to microsecond conversion * @@ -30,7 +40,7 @@ class Requests_Transport_fsockopen implements Requests_Transport { /** * Stream metadata * - * @var array Associative array of properties, see {@see https://secure.php.net/stream_get_meta_data} + * @var array Associative array of properties, see {@link https://www.php.net/stream_get_meta_data} */ public $info; @@ -39,45 +49,70 @@ class Requests_Transport_fsockopen implements Requests_Transport { * * @var int|bool Byte count, or false if no limit. */ - protected $max_bytes = false; + private $max_bytes = false; - protected $connect_error = ''; + private $connect_error = ''; /** * Perform a request * - * @throws Requests_Exception On failure to connect to socket (`fsockopenerror`) - * @throws Requests_Exception On socket timeout (`timeout`) - * - * @param string $url URL to request + * @param string|Stringable $url URL to request * @param array $headers Associative array of request headers * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD - * @param array $options Request options, see {@see Requests::response()} for documentation + * @param array $options Request options, see {@see \WpOrg\Requests\Requests::response()} for documentation * @return string Raw HTTP result + * + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string or Stringable. + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $headers argument is not an array. + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $data parameter is not an array or string. + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. + * @throws \WpOrg\Requests\Exception On failure to connect to socket (`fsockopenerror`) + * @throws \WpOrg\Requests\Exception On socket timeout (`timeout`) */ - public function request($url, $headers = array(), $data = array(), $options = array()) { + public function request($url, $headers = [], $data = [], $options = []) { + if (InputValidator::is_string_or_stringable($url) === false) { + throw InvalidArgument::create(1, '$url', 'string|Stringable', gettype($url)); + } + + if (is_array($headers) === false) { + throw InvalidArgument::create(2, '$headers', 'array', gettype($headers)); + } + + if (!is_array($data) && !is_string($data)) { + if ($data === null) { + $data = ''; + } else { + throw InvalidArgument::create(3, '$data', 'array|string', gettype($data)); + } + } + + if (is_array($options) === false) { + throw InvalidArgument::create(4, '$options', 'array', gettype($options)); + } + $options['hooks']->dispatch('fsockopen.before_request'); $url_parts = parse_url($url); if (empty($url_parts)) { - throw new Requests_Exception('Invalid URL.', 'invalidurl', $url); + throw new Exception('Invalid URL.', 'invalidurl', $url); } + $host = $url_parts['host']; $context = stream_context_create(); $verifyname = false; - $case_insensitive_headers = new Requests_Utility_CaseInsensitiveDictionary($headers); + $case_insensitive_headers = new CaseInsensitiveDictionary($headers); // HTTPS support if (isset($url_parts['scheme']) && strtolower($url_parts['scheme']) === 'https') { $remote_socket = 'ssl://' . $host; if (!isset($url_parts['port'])) { - $url_parts['port'] = 443; + $url_parts['port'] = Port::HTTPS; } - $context_options = array( + $context_options = [ 'verify_peer' => true, 'capture_peer_cert' => true, - ); + ]; $verifyname = true; // SNI, if enabled (OpenSSL >=0.9.8j) @@ -94,8 +129,7 @@ public function request($url, $headers = array(), $data = array(), $options = ar $context_options['verify_peer'] = false; $context_options['verify_peer_name'] = false; $verifyname = false; - } - elseif (is_string($options['verify'])) { + } elseif (is_string($options['verify'])) { $context_options['cafile'] = $options['verify']; } } @@ -105,39 +139,39 @@ public function request($url, $headers = array(), $data = array(), $options = ar $verifyname = false; } - stream_context_set_option($context, array('ssl' => $context_options)); - } - else { + stream_context_set_option($context, ['ssl' => $context_options]); + } else { $remote_socket = 'tcp://' . $host; } $this->max_bytes = $options['max_bytes']; if (!isset($url_parts['port'])) { - $url_parts['port'] = 80; + $url_parts['port'] = Port::HTTP; } + $remote_socket .= ':' . $url_parts['port']; // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler - set_error_handler(array($this, 'connect_error_handler'), E_WARNING | E_NOTICE); + set_error_handler([$this, 'connect_error_handler'], E_WARNING | E_NOTICE); - $options['hooks']->dispatch('fsockopen.remote_socket', array(&$remote_socket)); + $options['hooks']->dispatch('fsockopen.remote_socket', [&$remote_socket]); $socket = stream_socket_client($remote_socket, $errno, $errstr, ceil($options['connect_timeout']), STREAM_CLIENT_CONNECT, $context); restore_error_handler(); if ($verifyname && !$this->verify_certificate_from_context($host, $context)) { - throw new Requests_Exception('SSL certificate did not match the requested domain name', 'ssl.no_match'); + throw new Exception('SSL certificate did not match the requested domain name', 'ssl.no_match'); } if (!$socket) { if ($errno === 0) { // Connection issue - throw new Requests_Exception(rtrim($this->connect_error), 'fsockopen.connect_error'); + throw new Exception(rtrim($this->connect_error), 'fsockopen.connect_error'); } - throw new Requests_Exception($errstr, 'fsockopenerror', null, $errno); + throw new Exception($errstr, 'fsockopenerror', null, $errno); } $data_format = $options['data_format']; @@ -145,12 +179,11 @@ public function request($url, $headers = array(), $data = array(), $options = ar if ($data_format === 'query') { $path = self::format_get($url_parts, $data); $data = ''; - } - else { - $path = self::format_get($url_parts, array()); + } else { + $path = self::format_get($url_parts, []); } - $options['hooks']->dispatch('fsockopen.remote_host_path', array(&$path, $url)); + $options['hooks']->dispatch('fsockopen.remote_host_path', [&$path, $url]); $request_body = ''; $out = sprintf("%s %s HTTP/%.1F\r\n", $options['type'], $path, $options['protocol_version']); @@ -158,8 +191,7 @@ public function request($url, $headers = array(), $data = array(), $options = ar if ($options['type'] !== Requests::TRACE) { if (is_array($data)) { $request_body = http_build_query($data, '', '&'); - } - else { + } else { $request_body = $data; } @@ -177,11 +209,13 @@ public function request($url, $headers = array(), $data = array(), $options = ar } if (!isset($case_insensitive_headers['Host'])) { - $out .= sprintf('Host: %s', $url_parts['host']); + $out .= sprintf('Host: %s', $url_parts['host']); + $scheme_lower = strtolower($url_parts['scheme']); - if ((strtolower($url_parts['scheme']) === 'http' && $url_parts['port'] !== 80) || (strtolower($url_parts['scheme']) === 'https' && $url_parts['port'] !== 443)) { + if (($scheme_lower === 'http' && $url_parts['port'] !== Port::HTTP) || ($scheme_lower === 'https' && $url_parts['port'] !== Port::HTTPS)) { $out .= ':' . $url_parts['port']; } + $out .= "\r\n"; } @@ -200,7 +234,7 @@ public function request($url, $headers = array(), $data = array(), $options = ar $out .= implode("\r\n", $headers) . "\r\n"; } - $options['hooks']->dispatch('fsockopen.after_headers', array(&$out)); + $options['hooks']->dispatch('fsockopen.after_headers', [&$out]); if (substr($out, -2) !== "\r\n") { $out .= "\r\n"; @@ -212,25 +246,25 @@ public function request($url, $headers = array(), $data = array(), $options = ar $out .= "\r\n" . $request_body; - $options['hooks']->dispatch('fsockopen.before_send', array(&$out)); + $options['hooks']->dispatch('fsockopen.before_send', [&$out]); fwrite($socket, $out); - $options['hooks']->dispatch('fsockopen.after_send', array($out)); + $options['hooks']->dispatch('fsockopen.after_send', [$out]); if (!$options['blocking']) { fclose($socket); $fake_headers = ''; - $options['hooks']->dispatch('fsockopen.after_request', array(&$fake_headers)); + $options['hooks']->dispatch('fsockopen.after_request', [&$fake_headers]); return ''; } $timeout_sec = (int) floor($options['timeout']); if ($timeout_sec === $options['timeout']) { $timeout_msec = 0; - } - else { + } else { $timeout_msec = self::SECOND_IN_MICROSECONDS * $options['timeout'] % self::SECOND_IN_MICROSECONDS; } + stream_set_timeout($socket, $timeout_sec, $timeout_msec); $response = ''; @@ -241,13 +275,18 @@ public function request($url, $headers = array(), $data = array(), $options = ar $doingbody = false; $download = false; if ($options['filename']) { - $download = fopen($options['filename'], 'wb'); + // phpcs:ignore WordPress.PHP.NoSilencedErrors -- Silenced the PHP native warning in favour of throwing an exception. + $download = @fopen($options['filename'], 'wb'); + if ($download === false) { + $error = error_get_last(); + throw new Exception($error['message'], 'fopen'); + } } while (!feof($socket)) { $this->info = stream_get_meta_data($socket); if ($this->info['timed_out']) { - throw new Requests_Exception('fsocket timed out', 'timeout'); + throw new Exception('fsocket timed out', 'timeout'); } $block = fread($socket, Requests::BUFFER_SIZE); @@ -261,13 +300,14 @@ public function request($url, $headers = array(), $data = array(), $options = ar // Are we in body mode now? if ($doingbody) { - $options['hooks']->dispatch('request.progress', array($block, $size, $this->max_bytes)); + $options['hooks']->dispatch('request.progress', [$block, $size, $this->max_bytes]); $data_length = strlen($block); if ($this->max_bytes) { // Have we already hit a limit? if ($size === $this->max_bytes) { continue; } + if (($size + $data_length) > $this->max_bytes) { // Limit the length $limited_length = ($this->max_bytes - $size); @@ -278,49 +318,64 @@ public function request($url, $headers = array(), $data = array(), $options = ar $size += strlen($block); if ($download) { fwrite($download, $block); - } - else { + } else { $body .= $block; } } } + $this->headers = $headers; if ($download) { fclose($download); - } - else { + } else { $this->headers .= "\r\n\r\n" . $body; } + fclose($socket); - $options['hooks']->dispatch('fsockopen.after_request', array(&$this->headers, &$this->info)); + $options['hooks']->dispatch('fsockopen.after_request', [&$this->headers, &$this->info]); return $this->headers; } /** * Send multiple requests simultaneously * - * @param array $requests Request data (array of 'url', 'headers', 'data', 'options') as per {@see Requests_Transport::request} - * @param array $options Global options, see {@see Requests::response()} for documentation - * @return array Array of Requests_Response objects (may contain Requests_Exception or string responses as well) + * @param array $requests Request data (array of 'url', 'headers', 'data', 'options') as per {@see \WpOrg\Requests\Transport::request()} + * @param array $options Global options, see {@see \WpOrg\Requests\Requests::response()} for documentation + * @return array Array of \WpOrg\Requests\Response objects (may contain \WpOrg\Requests\Exception or string responses as well) + * + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $requests argument is not an array or iterable object with array access. + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array. */ public function request_multiple($requests, $options) { - $responses = array(); + // If you're not requesting, we can't get any responses ¯\_(ツ)_/¯ + if (empty($requests)) { + return []; + } + + if (InputValidator::has_array_access($requests) === false || InputValidator::is_iterable($requests) === false) { + throw InvalidArgument::create(1, '$requests', 'array|ArrayAccess&Traversable', gettype($requests)); + } + + if (is_array($options) === false) { + throw InvalidArgument::create(2, '$options', 'array', gettype($options)); + } + + $responses = []; $class = get_class($this); foreach ($requests as $id => $request) { try { $handler = new $class(); $responses[$id] = $handler->request($request['url'], $request['headers'], $request['data'], $request['options']); - $request['options']['hooks']->dispatch('transport.internal.parse_response', array(&$responses[$id], $request)); - } - catch (Requests_Exception $e) { + $request['options']['hooks']->dispatch('transport.internal.parse_response', [&$responses[$id], $request]); + } catch (Exception $e) { $responses[$id] = $e; } if (!is_string($responses[$id])) { - $request['options']['hooks']->dispatch('multiple.request.complete', array(&$responses[$id], $id)); + $request['options']['hooks']->dispatch('multiple.request.complete', [&$responses[$id], $id]); } } @@ -332,8 +387,8 @@ public function request_multiple($requests, $options) { * * @return string Accept-Encoding header value */ - protected static function accept_encoding() { - $type = array(); + private static function accept_encoding() { + $type = []; if (function_exists('gzinflate')) { $type[] = 'deflate;q=1.0'; } @@ -351,10 +406,10 @@ protected static function accept_encoding() { * Format a URL given GET data * * @param array $url_parts - * @param array|object $data Data to build query using, see {@see https://secure.php.net/http_build_query} + * @param array|object $data Data to build query using, see {@link https://www.php.net/http_build_query} * @return string URL with data */ - protected static function format_get($url_parts, $data) { + private static function format_get($url_parts, $data) { if (!empty($data)) { if (empty($url_parts['query'])) { $url_parts['query'] = ''; @@ -363,17 +418,17 @@ protected static function format_get($url_parts, $data) { $url_parts['query'] .= '&' . http_build_query($data, '', '&'); $url_parts['query'] = trim($url_parts['query'], '&'); } + if (isset($url_parts['path'])) { if (isset($url_parts['query'])) { $get = $url_parts['path'] . '?' . $url_parts['query']; - } - else { + } else { $get = $url_parts['path']; } - } - else { + } else { $get = '/'; } + return $get; } @@ -401,13 +456,14 @@ public function connect_error_handler($errno, $errstr) { * names, leading things like 'https://www.github.com/' to be invalid. * Instead * - * @see https://tools.ietf.org/html/rfc2818#section-3.1 RFC2818, Section 3.1 + * @link https://tools.ietf.org/html/rfc2818#section-3.1 RFC2818, Section 3.1 * - * @throws Requests_Exception On failure to connect via TLS (`fsockopen.ssl.connect_error`) - * @throws Requests_Exception On not obtaining a match for the host (`fsockopen.ssl.no_match`) * @param string $host Host name to verify against * @param resource $context Stream context * @return bool + * + * @throws \WpOrg\Requests\Exception On failure to connect via TLS (`fsockopen.ssl.connect_error`) + * @throws \WpOrg\Requests\Exception On not obtaining a match for the host (`fsockopen.ssl.no_match`) */ public function verify_certificate_from_context($host, $context) { $meta = stream_context_get_options($context); @@ -415,35 +471,33 @@ public function verify_certificate_from_context($host, $context) { // If we don't have SSL options, then we couldn't make the connection at // all if (empty($meta) || empty($meta['ssl']) || empty($meta['ssl']['peer_certificate'])) { - throw new Requests_Exception(rtrim($this->connect_error), 'ssl.connect_error'); + throw new Exception(rtrim($this->connect_error), 'ssl.connect_error'); } $cert = openssl_x509_parse($meta['ssl']['peer_certificate']); - return Requests_SSL::verify_certificate($host, $cert); + return Ssl::verify_certificate($host, $cert); } /** - * Whether this transport is valid + * Self-test whether the transport can be used. + * + * The available capabilities to test for can be found in {@see \WpOrg\Requests\Capability}. * * @codeCoverageIgnore - * @return boolean True if the transport is valid, false otherwise. + * @param array $capabilities Optional. Associative array of capabilities to test against, i.e. `['' => true]`. + * @return bool Whether the transport can be used. */ - public static function test($capabilities = array()) { + public static function test($capabilities = []) { if (!function_exists('fsockopen')) { return false; } // If needed, check that streams support SSL - if (isset($capabilities['ssl']) && $capabilities['ssl']) { + if (isset($capabilities[Capability::SSL]) && $capabilities[Capability::SSL]) { if (!extension_loaded('openssl') || !function_exists('openssl_x509_parse')) { return false; } - - // Currently broken, thanks to https://github.com/facebook/hhvm/issues/2156 - if (defined('HHVM_VERSION')) { - return false; - } } return true; diff --git a/src/wp-includes/Requests/Utility/CaseInsensitiveDictionary.php b/src/wp-includes/Requests/Utility/CaseInsensitiveDictionary.php index 7eebeb7a7234e..3c24cebd4fc5b 100644 --- a/src/wp-includes/Requests/Utility/CaseInsensitiveDictionary.php +++ b/src/wp-includes/Requests/Utility/CaseInsensitiveDictionary.php @@ -2,92 +2,116 @@ /** * Case-insensitive dictionary, suitable for HTTP headers * - * @package Requests - * @subpackage Utilities + * @package Requests\Utilities */ +namespace WpOrg\Requests\Utility; + +use ArrayAccess; +use ArrayIterator; +use IteratorAggregate; +use ReturnTypeWillChange; +use WpOrg\Requests\Exception; + /** * Case-insensitive dictionary, suitable for HTTP headers * - * @package Requests - * @subpackage Utilities + * @package Requests\Utilities */ -class Requests_Utility_CaseInsensitiveDictionary implements ArrayAccess, IteratorAggregate { +class CaseInsensitiveDictionary implements ArrayAccess, IteratorAggregate { /** * Actual item data * * @var array */ - protected $data = array(); + protected $data = []; /** * Creates a case insensitive dictionary. * * @param array $data Dictionary/map to convert to case-insensitive */ - public function __construct(array $data = array()) { - foreach ($data as $key => $value) { - $this->offsetSet($key, $value); + public function __construct(array $data = []) { + foreach ($data as $offset => $value) { + $this->offsetSet($offset, $value); } } /** * Check if the given item exists * - * @param string $key Item key + * @param string $offset Item key * @return boolean Does the item exist? */ - public function offsetExists($key) { - $key = strtolower($key); - return isset($this->data[$key]); + #[ReturnTypeWillChange] + public function offsetExists($offset) { + if (is_string($offset)) { + $offset = strtolower($offset); + } + + return isset($this->data[$offset]); } /** * Get the value for the item * - * @param string $key Item key - * @return string|null Item value (null if offsetExists is false) + * @param string $offset Item key + * @return string|null Item value (null if the item key doesn't exist) */ - public function offsetGet($key) { - $key = strtolower($key); - if (!isset($this->data[$key])) { + #[ReturnTypeWillChange] + public function offsetGet($offset) { + if (is_string($offset)) { + $offset = strtolower($offset); + } + + if (!isset($this->data[$offset])) { return null; } - return $this->data[$key]; + return $this->data[$offset]; } /** * Set the given item * - * @throws Requests_Exception On attempting to use dictionary as list (`invalidset`) - * - * @param string $key Item name + * @param string $offset Item name * @param string $value Item value + * + * @throws \WpOrg\Requests\Exception On attempting to use dictionary as list (`invalidset`) */ - public function offsetSet($key, $value) { - if ($key === null) { - throw new Requests_Exception('Object is a dictionary, not a list', 'invalidset'); + #[ReturnTypeWillChange] + public function offsetSet($offset, $value) { + if ($offset === null) { + throw new Exception('Object is a dictionary, not a list', 'invalidset'); + } + + if (is_string($offset)) { + $offset = strtolower($offset); } - $key = strtolower($key); - $this->data[$key] = $value; + $this->data[$offset] = $value; } /** * Unset the given header * - * @param string $key + * @param string $offset */ - public function offsetUnset($key) { - unset($this->data[strtolower($key)]); + #[ReturnTypeWillChange] + public function offsetUnset($offset) { + if (is_string($offset)) { + $offset = strtolower($offset); + } + + unset($this->data[$offset]); } /** * Get an iterator for the data * - * @return ArrayIterator + * @return \ArrayIterator */ + #[ReturnTypeWillChange] public function getIterator() { return new ArrayIterator($this->data); } diff --git a/src/wp-includes/Requests/Utility/FilteredIterator.php b/src/wp-includes/Requests/Utility/FilteredIterator.php index 7c6c2c0238c29..973a5d25a5151 100644 --- a/src/wp-includes/Requests/Utility/FilteredIterator.php +++ b/src/wp-includes/Requests/Utility/FilteredIterator.php @@ -2,34 +2,60 @@ /** * Iterator for arrays requiring filtered values * - * @package Requests - * @subpackage Utilities + * @package Requests\Utilities */ +namespace WpOrg\Requests\Utility; + +use ArrayIterator; +use ReturnTypeWillChange; +use WpOrg\Requests\Exception\InvalidArgument; +use WpOrg\Requests\Utility\InputValidator; + /** * Iterator for arrays requiring filtered values * - * @package Requests - * @subpackage Utilities + * @package Requests\Utilities */ -class Requests_Utility_FilteredIterator extends ArrayIterator { +final class FilteredIterator extends ArrayIterator { /** * Callback to run as a filter * * @var callable */ - protected $callback; + private $callback; /** * Create a new iterator * * @param array $data * @param callable $callback Callback to be called on each value + * + * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $data argument is not iterable. */ public function __construct($data, $callback) { + if (InputValidator::is_iterable($data) === false) { + throw InvalidArgument::create(1, '$data', 'iterable', gettype($data)); + } + parent::__construct($data); - $this->callback = $callback; + if (is_callable($callback)) { + $this->callback = $callback; + } + } + + /** + * @inheritdoc + * + * @phpcs:disable PHPCompatibility.FunctionNameRestrictions.NewMagicMethods.__unserializeFound + */ + #[ReturnTypeWillChange] + public function __unserialize($data) {} + // phpcs:enable + + public function __wakeup() { + unset($this->callback); } /** @@ -37,6 +63,7 @@ public function __construct($data, $callback) { * * @return string */ + #[ReturnTypeWillChange] public function current() { $value = parent::current(); @@ -50,16 +77,6 @@ public function current() { /** * @inheritdoc */ - public function unserialize($serialized) {} - - /** - * @inheritdoc - * - * @phpcs:disable PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.MethodDoubleUnderscore,PHPCompatibility.FunctionNameRestrictions.NewMagicMethods.__unserializeFound - */ - public function __unserialize($serialized) {} - - public function __wakeup() { - unset($this->callback); - } + #[ReturnTypeWillChange] + public function unserialize($data) {} } diff --git a/src/wp-includes/Requests/Utility/InputValidator.php b/src/wp-includes/Requests/Utility/InputValidator.php new file mode 100644 index 0000000000000..7c10d61a4b24b --- /dev/null +++ b/src/wp-includes/Requests/Utility/InputValidator.php @@ -0,0 +1,109 @@ +dispatch('requests.before_request', array(&$url, &$headers, &$data, &$type, &$options)); - - if (!empty($options['transport'])) { - $transport = $options['transport']; - - if (is_string($options['transport'])) { - $transport = new $transport(); - } - } - else { - $need_ssl = (stripos($url, 'https://') === 0); - $capabilities = array('ssl' => $need_ssl); - $transport = self::get_transport($capabilities); - } - $response = $transport->request($url, $headers, $data, $options); - - $options['hooks']->dispatch('requests.before_parse', array(&$response, $url, $headers, $data, $type, $options)); - - return self::parse_response($response, $url, $headers, $data, $options); - } - - /** - * Send multiple HTTP requests simultaneously - * - * The `$requests` parameter takes an associative or indexed array of - * request fields. The key of each request can be used to match up the - * request with the returned data, or with the request passed into your - * `multiple.request.complete` callback. - * - * The request fields value is an associative array with the following keys: - * - * - `url`: Request URL Same as the `$url` parameter to - * {@see Requests::request} - * (string, required) - * - `headers`: Associative array of header fields. Same as the `$headers` - * parameter to {@see Requests::request} - * (array, default: `array()`) - * - `data`: Associative array of data fields or a string. Same as the - * `$data` parameter to {@see Requests::request} - * (array|string, default: `array()`) - * - `type`: HTTP request type (use Requests constants). Same as the `$type` - * parameter to {@see Requests::request} - * (string, default: `Requests::GET`) - * - `cookies`: Associative array of cookie name to value, or cookie jar. - * (array|Requests_Cookie_Jar) - * - * If the `$options` parameter is specified, individual requests will - * inherit options from it. This can be used to use a single hooking system, - * or set all the types to `Requests::POST`, for example. - * - * In addition, the `$options` parameter takes the following global options: - * - * - `complete`: A callback for when a request is complete. Takes two - * parameters, a Requests_Response/Requests_Exception reference, and the - * ID from the request array (Note: this can also be overridden on a - * per-request basis, although that's a little silly) - * (callback) - * - * @param array $requests Requests data (see description for more information) - * @param array $options Global and default options (see {@see Requests::request}) - * @return array Responses (either Requests_Response or a Requests_Exception object) - */ - public static function request_multiple($requests, $options = array()) { - $options = array_merge(self::get_default_options(true), $options); - - if (!empty($options['hooks'])) { - $options['hooks']->register('transport.internal.parse_response', array('Requests', 'parse_multiple')); - if (!empty($options['complete'])) { - $options['hooks']->register('multiple.request.complete', $options['complete']); - } - } - - foreach ($requests as $id => &$request) { - if (!isset($request['headers'])) { - $request['headers'] = array(); - } - if (!isset($request['data'])) { - $request['data'] = array(); - } - if (!isset($request['type'])) { - $request['type'] = self::GET; - } - if (!isset($request['options'])) { - $request['options'] = $options; - $request['options']['type'] = $request['type']; - } - else { - if (empty($request['options']['type'])) { - $request['options']['type'] = $request['type']; - } - $request['options'] = array_merge($options, $request['options']); - } - - self::set_defaults($request['url'], $request['headers'], $request['data'], $request['type'], $request['options']); - - // Ensure we only hook in once - if ($request['options']['hooks'] !== $options['hooks']) { - $request['options']['hooks']->register('transport.internal.parse_response', array('Requests', 'parse_multiple')); - if (!empty($request['options']['complete'])) { - $request['options']['hooks']->register('multiple.request.complete', $request['options']['complete']); - } - } - } - unset($request); - - if (!empty($options['transport'])) { - $transport = $options['transport']; - - if (is_string($options['transport'])) { - $transport = new $transport(); - } - } - else { - $transport = self::get_transport(); - } - $responses = $transport->request_multiple($requests, $options); - - foreach ($responses as $id => &$response) { - // If our hook got messed with somehow, ensure we end up with the - // correct response - if (is_string($response)) { - $request = $requests[$id]; - self::parse_multiple($response, $request); - $request['options']['hooks']->dispatch('multiple.request.complete', array(&$response, $id)); - } - } - - return $responses; - } - - /** - * Get the default options - * - * @see Requests::request() for values returned by this method - * @param boolean $multirequest Is this a multirequest? - * @return array Default option values - */ - protected static function get_default_options($multirequest = false) { - $defaults = array( - 'timeout' => 10, - 'connect_timeout' => 10, - 'useragent' => 'php-requests/' . self::VERSION, - 'protocol_version' => 1.1, - 'redirected' => 0, - 'redirects' => 10, - 'follow_redirects' => true, - 'blocking' => true, - 'type' => self::GET, - 'filename' => false, - 'auth' => false, - 'proxy' => false, - 'cookies' => false, - 'max_bytes' => false, - 'idn' => true, - 'hooks' => null, - 'transport' => null, - 'verify' => self::get_certificate_path(), - 'verifyname' => true, - ); - if ($multirequest !== false) { - $defaults['complete'] = null; - } - return $defaults; - } - - /** - * Get default certificate path. - * - * @return string Default certificate path. - */ - public static function get_certificate_path() { - if (!empty(self::$certificate_path)) { - return self::$certificate_path; - } - - return dirname(__FILE__) . '/Requests/Transport/cacert.pem'; - } - - /** - * Set default certificate path. - * - * @param string $path Certificate path, pointing to a PEM file. - */ - public static function set_certificate_path($path) { - self::$certificate_path = $path; - } - - /** - * Set the default values - * - * @param string $url URL to request - * @param array $headers Extra headers to send with the request - * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests - * @param string $type HTTP request type - * @param array $options Options for the request - * @return array $options - */ - protected static function set_defaults(&$url, &$headers, &$data, &$type, &$options) { - if (!preg_match('/^http(s)?:\/\//i', $url, $matches)) { - throw new Requests_Exception('Only HTTP(S) requests are handled.', 'nonhttp', $url); - } - - if (empty($options['hooks'])) { - $options['hooks'] = new Requests_Hooks(); - } - - if (is_array($options['auth'])) { - $options['auth'] = new Requests_Auth_Basic($options['auth']); - } - if ($options['auth'] !== false) { - $options['auth']->register($options['hooks']); - } - - if (is_string($options['proxy']) || is_array($options['proxy'])) { - $options['proxy'] = new Requests_Proxy_HTTP($options['proxy']); - } - if ($options['proxy'] !== false) { - $options['proxy']->register($options['hooks']); - } - - if (is_array($options['cookies'])) { - $options['cookies'] = new Requests_Cookie_Jar($options['cookies']); - } - elseif (empty($options['cookies'])) { - $options['cookies'] = new Requests_Cookie_Jar(); - } - if ($options['cookies'] !== false) { - $options['cookies']->register($options['hooks']); - } - - if ($options['idn'] !== false) { - $iri = new Requests_IRI($url); - $iri->host = Requests_IDNAEncoder::encode($iri->ihost); - $url = $iri->uri; - } - - // Massage the type to ensure we support it. - $type = strtoupper($type); - - if (!isset($options['data_format'])) { - if (in_array($type, array(self::HEAD, self::GET, self::DELETE), true)) { - $options['data_format'] = 'query'; - } - else { - $options['data_format'] = 'body'; - } - } - } - - /** - * HTTP response parser - * - * @throws Requests_Exception On missing head/body separator (`requests.no_crlf_separator`) - * @throws Requests_Exception On missing head/body separator (`noversion`) - * @throws Requests_Exception On missing head/body separator (`toomanyredirects`) - * - * @param string $headers Full response text including headers and body - * @param string $url Original request URL - * @param array $req_headers Original $headers array passed to {@link request()}, in case we need to follow redirects - * @param array $req_data Original $data array passed to {@link request()}, in case we need to follow redirects - * @param array $options Original $options array passed to {@link request()}, in case we need to follow redirects - * @return Requests_Response - */ - protected static function parse_response($headers, $url, $req_headers, $req_data, $options) { - $return = new Requests_Response(); - if (!$options['blocking']) { - return $return; - } - - $return->raw = $headers; - $return->url = (string) $url; - $return->body = ''; - - if (!$options['filename']) { - $pos = strpos($headers, "\r\n\r\n"); - if ($pos === false) { - // Crap! - throw new Requests_Exception('Missing header/body separator', 'requests.no_crlf_separator'); - } - - $headers = substr($return->raw, 0, $pos); - // Headers will always be separated from the body by two new lines - `\n\r\n\r`. - $body = substr($return->raw, $pos + 4); - if (!empty($body)) { - $return->body = $body; - } - } - // Pretend CRLF = LF for compatibility (RFC 2616, section 19.3) - $headers = str_replace("\r\n", "\n", $headers); - // Unfold headers (replace [CRLF] 1*( SP | HT ) with SP) as per RFC 2616 (section 2.2) - $headers = preg_replace('/\n[ \t]/', ' ', $headers); - $headers = explode("\n", $headers); - preg_match('#^HTTP/(1\.\d)[ \t]+(\d+)#i', array_shift($headers), $matches); - if (empty($matches)) { - throw new Requests_Exception('Response could not be parsed', 'noversion', $headers); - } - $return->protocol_version = (float) $matches[1]; - $return->status_code = (int) $matches[2]; - if ($return->status_code >= 200 && $return->status_code < 300) { - $return->success = true; - } - - foreach ($headers as $header) { - list($key, $value) = explode(':', $header, 2); - $value = trim($value); - preg_replace('#(\s+)#i', ' ', $value); - $return->headers[$key] = $value; - } - if (isset($return->headers['transfer-encoding'])) { - $return->body = self::decode_chunked($return->body); - unset($return->headers['transfer-encoding']); - } - if (isset($return->headers['content-encoding'])) { - $return->body = self::decompress($return->body); - } - - //fsockopen and cURL compatibility - if (isset($return->headers['connection'])) { - unset($return->headers['connection']); - } - - $options['hooks']->dispatch('requests.before_redirect_check', array(&$return, $req_headers, $req_data, $options)); - - if ($return->is_redirect() && $options['follow_redirects'] === true) { - if (isset($return->headers['location']) && $options['redirected'] < $options['redirects']) { - if ($return->status_code === 303) { - $options['type'] = self::GET; - } - $options['redirected']++; - $location = $return->headers['location']; - if (strpos($location, 'http://') !== 0 && strpos($location, 'https://') !== 0) { - // relative redirect, for compatibility make it absolute - $location = Requests_IRI::absolutize($url, $location); - $location = $location->uri; - } - - $hook_args = array( - &$location, - &$req_headers, - &$req_data, - &$options, - $return, - ); - $options['hooks']->dispatch('requests.before_redirect', $hook_args); - $redirected = self::request($location, $req_headers, $req_data, $options['type'], $options); - $redirected->history[] = $return; - return $redirected; - } - elseif ($options['redirected'] >= $options['redirects']) { - throw new Requests_Exception('Too many redirects', 'toomanyredirects', $return); - } - } - - $return->redirects = $options['redirected']; - - $options['hooks']->dispatch('requests.after_request', array(&$return, $req_headers, $req_data, $options)); - return $return; - } - - /** - * Callback for `transport.internal.parse_response` - * - * Internal use only. Converts a raw HTTP response to a Requests_Response - * while still executing a multiple request. - * - * @param string $response Full response text including headers and body (will be overwritten with Response instance) - * @param array $request Request data as passed into {@see Requests::request_multiple()} - * @return null `$response` is either set to a Requests_Response instance, or a Requests_Exception object - */ - public static function parse_multiple(&$response, $request) { - try { - $url = $request['url']; - $headers = $request['headers']; - $data = $request['data']; - $options = $request['options']; - $response = self::parse_response($response, $url, $headers, $data, $options); - } - catch (Requests_Exception $e) { - $response = $e; - } - } - - /** - * Decoded a chunked body as per RFC 2616 - * - * @see https://tools.ietf.org/html/rfc2616#section-3.6.1 - * @param string $data Chunked body - * @return string Decoded body - */ - protected static function decode_chunked($data) { - if (!preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', trim($data))) { - return $data; - } - - $decoded = ''; - $encoded = $data; - - while (true) { - $is_chunked = (bool) preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', $encoded, $matches); - if (!$is_chunked) { - // Looks like it's not chunked after all - return $data; - } - - $length = hexdec(trim($matches[1])); - if ($length === 0) { - // Ignore trailer headers - return $decoded; - } - - $chunk_length = strlen($matches[0]); - $decoded .= substr($encoded, $chunk_length, $length); - $encoded = substr($encoded, $chunk_length + $length + 2); - - if (trim($encoded) === '0' || empty($encoded)) { - return $decoded; - } - } - - // We'll never actually get down here - // @codeCoverageIgnoreStart - } - // @codeCoverageIgnoreEnd - - /** - * Convert a key => value array to a 'key: value' array for headers - * - * @param array $array Dictionary of header values - * @return array List of headers - */ - public static function flatten($array) { - $return = array(); - foreach ($array as $key => $value) { - $return[] = sprintf('%s: %s', $key, $value); - } - return $return; - } - - /** - * Convert a key => value array to a 'key: value' array for headers + * @deprecated 6.2.0 Include the `WpOrg\Requests\Autoload` class and + * call `WpOrg\Requests\Autoload::register()` instead. * * @codeCoverageIgnore - * @deprecated Misspelling of {@see Requests::flatten} - * @param array $array Dictionary of header values - * @return array List of headers - */ - public static function flattern($array) { - return self::flatten($array); - } - - /** - * Decompress an encoded body - * - * Implements gzip, compress and deflate. Guesses which it is by attempting - * to decode. - * - * @param string $data Compressed data in one of the above formats - * @return string Decompressed string - */ - public static function decompress($data) { - if (substr($data, 0, 2) !== "\x1f\x8b" && substr($data, 0, 2) !== "\x78\x9c") { - // Not actually compressed. Probably cURL ruining this for us. - return $data; - } - - if (function_exists('gzdecode')) { - // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.gzdecodeFound -- Wrapped in function_exists() for PHP 5.2. - $decoded = @gzdecode($data); - if ($decoded !== false) { - return $decoded; - } - } - - if (function_exists('gzinflate')) { - $decoded = @gzinflate($data); - if ($decoded !== false) { - return $decoded; - } - } - - $decoded = self::compatible_gzinflate($data); - if ($decoded !== false) { - return $decoded; - } - - if (function_exists('gzuncompress')) { - $decoded = @gzuncompress($data); - if ($decoded !== false) { - return $decoded; - } - } - - return $data; - } - - /** - * Decompression of deflated string while staying compatible with the majority of servers. - * - * Certain Servers will return deflated data with headers which PHP's gzinflate() - * function cannot handle out of the box. The following function has been created from - * various snippets on the gzinflate() PHP documentation. - * - * Warning: Magic numbers within. Due to the potential different formats that the compressed - * data may be returned in, some "magic offsets" are needed to ensure proper decompression - * takes place. For a simple progmatic way to determine the magic offset in use, see: - * https://core.trac.wordpress.org/ticket/18273 - * - * @since 2.8.1 - * @link https://core.trac.wordpress.org/ticket/18273 - * @link https://secure.php.net/manual/en/function.gzinflate.php#70875 - * @link https://secure.php.net/manual/en/function.gzinflate.php#77336 - * - * @param string $gz_data String to decompress. - * @return string|bool False on failure. */ - public static function compatible_gzinflate($gz_data) { - // Compressed data might contain a full zlib header, if so strip it for - // gzinflate() - if (substr($gz_data, 0, 3) === "\x1f\x8b\x08") { - $i = 10; - $flg = ord(substr($gz_data, 3, 1)); - if ($flg > 0) { - if ($flg & 4) { - list($xlen) = unpack('v', substr($gz_data, $i, 2)); - $i += 2 + $xlen; - } - if ($flg & 8) { - $i = strpos($gz_data, "\0", $i) + 1; - } - if ($flg & 16) { - $i = strpos($gz_data, "\0", $i) + 1; - } - if ($flg & 2) { - $i += 2; - } - } - $decompressed = self::compatible_gzinflate(substr($gz_data, $i)); - if ($decompressed !== false) { - return $decompressed; - } - } - - // If the data is Huffman Encoded, we must first strip the leading 2 - // byte Huffman marker for gzinflate() - // The response is Huffman coded by many compressors such as - // java.util.zip.Deflater, Ruby’s Zlib::Deflate, and .NET's - // System.IO.Compression.DeflateStream. - // - // See https://decompres.blogspot.com/ for a quick explanation of this - // data type - $huffman_encoded = false; - - // low nibble of first byte should be 0x08 - list(, $first_nibble) = unpack('h', $gz_data); - - // First 2 bytes should be divisible by 0x1F - list(, $first_two_bytes) = unpack('n', $gz_data); - - if ($first_nibble === 0x08 && ($first_two_bytes % 0x1F) === 0) { - $huffman_encoded = true; - } - - if ($huffman_encoded) { - $decompressed = @gzinflate(substr($gz_data, 2)); - if ($decompressed !== false) { - return $decompressed; - } - } - - if (substr($gz_data, 0, 4) === "\x50\x4b\x03\x04") { - // ZIP file format header - // Offset 6: 2 bytes, General-purpose field - // Offset 26: 2 bytes, filename length - // Offset 28: 2 bytes, optional field length - // Offset 30: Filename field, followed by optional field, followed - // immediately by data - list(, $general_purpose_flag) = unpack('v', substr($gz_data, 6, 2)); - - // If the file has been compressed on the fly, 0x08 bit is set of - // the general purpose field. We can use this to differentiate - // between a compressed document, and a ZIP file - $zip_compressed_on_the_fly = ((0x08 & $general_purpose_flag) === 0x08); - - if (!$zip_compressed_on_the_fly) { - // Don't attempt to decode a compressed zip file - return $gz_data; - } - - // Determine the first byte of data, based on the above ZIP header - // offsets: - $first_file_start = array_sum(unpack('v2', substr($gz_data, 26, 4))); - $decompressed = @gzinflate(substr($gz_data, 30 + $first_file_start)); - if ($decompressed !== false) { - return $decompressed; - } - return false; - } - - // Finally fall back to straight gzinflate - $decompressed = @gzinflate($gz_data); - if ($decompressed !== false) { - return $decompressed; - } - - // Fallback for all above failing, not expected, but included for - // debugging and preventing regressions and to track stats - $decompressed = @gzinflate(substr($gz_data, 2)); - if ($decompressed !== false) { - return $decompressed; - } - - return false; - } - - public static function match_domain($host, $reference) { - // Check for a direct match - if ($host === $reference) { - return true; - } - - // Calculate the valid wildcard match if the host is not an IP address - // Also validates that the host has 3 parts or more, as per Firefox's - // ruleset. - $parts = explode('.', $host); - if (ip2long($host) === false && count($parts) >= 3) { - $parts[0] = '*'; - $wildcard = implode('.', $parts); - if ($wildcard === $reference) { - return true; - } - } - - return false; + public static function register_autoloader() { + require_once __DIR__ . '/Requests/Autoload.php'; + WpOrg\Requests\Autoload::register(); } } From 26260c5c5d04969f8dfe09c391a3555c11d775ab Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 24 Nov 2022 00:35:50 +0100 Subject: [PATCH 2/5] MInimal update of WP code after updating the Requests library --- src/wp-includes/class-wp-http.php | 9 ++++++++- tests/phpunit/tests/functions/maybeSerialize.php | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/class-wp-http.php b/src/wp-includes/class-wp-http.php index 3d4e8798aa73e..646184de82ec0 100644 --- a/src/wp-includes/class-wp-http.php +++ b/src/wp-includes/class-wp-http.php @@ -8,10 +8,17 @@ */ if ( ! class_exists( 'Requests' ) ) { + if (!defined('REQUESTS_SILENCE_PSR0_DEPRECATIONS')) { + define('REQUESTS_SILENCE_PSR0_DEPRECATIONS', true); + } + require ABSPATH . WPINC . '/class-requests.php'; Requests::register_autoloader(); Requests::set_certificate_path( ABSPATH . WPINC . '/certificates/ca-bundle.crt' ); + + // Ensure a certain class alias gets created for a class used in type declarations by WP. + class_exists( 'Requests_Response' ); } /** @@ -468,7 +475,7 @@ static function( $attr ) { ); $cookie_jar[ $value->name ] = new Requests_Cookie( $value->name, $value->value, $attributes, array( 'host-only' => $value->host_only ) ); } elseif ( is_scalar( $value ) ) { - $cookie_jar[ $name ] = new Requests_Cookie( $name, $value ); + $cookie_jar[ $name ] = new Requests_Cookie( $name, (string) $value ); } } diff --git a/tests/phpunit/tests/functions/maybeSerialize.php b/tests/phpunit/tests/functions/maybeSerialize.php index 104c1b1134374..b95e270b1aa31 100644 --- a/tests/phpunit/tests/functions/maybeSerialize.php +++ b/tests/phpunit/tests/functions/maybeSerialize.php @@ -213,7 +213,8 @@ public function data_is_not_serialized() { public function test_deserialize_request_utility_filtered_iterator_objects( $value ) { $serialized = maybe_serialize( $value ); - if ( get_class( $value ) === 'Requests_Utility_FilteredIterator' ) { + $className = get_class( $value ); + if ( $className === 'Requests_Utility_FilteredIterator' || $className === 'WpOrg\Requests\Utility\FilteredIterator' ) { $new_value = unserialize( $serialized ); $property = ( new ReflectionClass( 'Requests_Utility_FilteredIterator' ) )->getProperty( 'callback' ); $property->setAccessible( true ); From 5df43680d6249e71fc5af9b5acd95ecf99d9e6e0 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 24 Nov 2022 00:57:31 +0100 Subject: [PATCH 3/5] Upgrade to Requests 2.0: code updates only --- .../class-wp-http-requests-hooks.php | 2 +- .../class-wp-http-requests-response.php | 6 +-- src/wp-includes/class-wp-http.php | 39 ++++++++----------- src/wp-includes/rest-api.php | 2 +- 4 files changed, 21 insertions(+), 28 deletions(-) diff --git a/src/wp-includes/class-wp-http-requests-hooks.php b/src/wp-includes/class-wp-http-requests-hooks.php index dac64dd6eb847..f069e4f7a783a 100644 --- a/src/wp-includes/class-wp-http-requests-hooks.php +++ b/src/wp-includes/class-wp-http-requests-hooks.php @@ -15,7 +15,7 @@ * @see Requests_Hooks */ #[AllowDynamicProperties] -class WP_HTTP_Requests_Hooks extends Requests_Hooks { +class WP_HTTP_Requests_Hooks extends WpOrg\Requests\Hooks { /** * Requested URL. * diff --git a/src/wp-includes/class-wp-http-requests-response.php b/src/wp-includes/class-wp-http-requests-response.php index 45f83e2c7a09e..679d1e0b0b720 100644 --- a/src/wp-includes/class-wp-http-requests-response.php +++ b/src/wp-includes/class-wp-http-requests-response.php @@ -39,7 +39,7 @@ class WP_HTTP_Requests_Response extends WP_HTTP_Response { * @param Requests_Response $response HTTP response. * @param string $filename Optional. File name. Default empty. */ - public function __construct( Requests_Response $response, $filename = '' ) { + public function __construct( WpOrg\Requests\Response $response, $filename = '' ) { $this->response = $response; $this->filename = $filename; } @@ -64,7 +64,7 @@ public function get_response_object() { */ public function get_headers() { // Ensure headers remain case-insensitive. - $converted = new Requests_Utility_CaseInsensitiveDictionary(); + $converted = new WpOrg\Requests\Utility\CaseInsensitiveDictionary(); foreach ( $this->response->headers->getAll() as $key => $value ) { if ( count( $value ) === 1 ) { @@ -85,7 +85,7 @@ public function get_headers() { * @param array $headers Map of header name to header value. */ public function set_headers( $headers ) { - $this->response->headers = new Requests_Response_Headers( $headers ); + $this->response->headers = new WpOrg\Requests\Response\Headers( $headers ); } /** diff --git a/src/wp-includes/class-wp-http.php b/src/wp-includes/class-wp-http.php index 646184de82ec0..6f9f338229f6e 100644 --- a/src/wp-includes/class-wp-http.php +++ b/src/wp-includes/class-wp-http.php @@ -7,18 +7,11 @@ * @since 2.7.0 */ -if ( ! class_exists( 'Requests' ) ) { - if (!defined('REQUESTS_SILENCE_PSR0_DEPRECATIONS')) { - define('REQUESTS_SILENCE_PSR0_DEPRECATIONS', true); - } - - require ABSPATH . WPINC . '/class-requests.php'; - - Requests::register_autoloader(); - Requests::set_certificate_path( ABSPATH . WPINC . '/certificates/ca-bundle.crt' ); +if ( ! class_exists( 'WpOrg\Requests\Autoload' ) ) { + require ABSPATH . WPINC . '/Requests/Autoload.php'; - // Ensure a certain class alias gets created for a class used in type declarations by WP. - class_exists( 'Requests_Response' ); + WpOrg\Requests\Autoload::register(); + WpOrg\Requests\Requests::set_certificate_path( ABSPATH . WPINC . '/certificates/ca-bundle.crt' ); } /** @@ -282,14 +275,14 @@ public function request( $url, $args = array() ) { if ( empty( $url ) || empty( $parsed_url['scheme'] ) ) { $response = new WP_Error( 'http_request_failed', __( 'A valid URL was not provided.' ) ); /** This action is documented in wp-includes/class-wp-http.php */ - do_action( 'http_api_debug', $response, 'response', 'Requests', $parsed_args, $url ); + do_action( 'http_api_debug', $response, 'response', 'WpOrg\Requests\Requests', $parsed_args, $url ); return $response; } if ( $this->block_request( $url ) ) { $response = new WP_Error( 'http_request_not_executed', __( 'User has blocked requests through HTTP.' ) ); /** This action is documented in wp-includes/class-wp-http.php */ - do_action( 'http_api_debug', $response, 'response', 'Requests', $parsed_args, $url ); + do_action( 'http_api_debug', $response, 'response', 'WpOrg\Requests\Requests', $parsed_args, $url ); return $response; } @@ -306,7 +299,7 @@ public function request( $url, $args = array() ) { if ( ! wp_is_writable( dirname( $parsed_args['filename'] ) ) ) { $response = new WP_Error( 'http_request_failed', __( 'Destination directory for file streaming does not exist or is not writable.' ) ); /** This action is documented in wp-includes/class-wp-http.php */ - do_action( 'http_api_debug', $response, 'response', 'Requests', $parsed_args, $url ); + do_action( 'http_api_debug', $response, 'response', 'WpOrg\Requests\Requests', $parsed_args, $url ); return $response; } } @@ -386,7 +379,7 @@ public function request( $url, $args = array() ) { // Check for proxies. $proxy = new WP_HTTP_Proxy(); if ( $proxy->is_enabled() && $proxy->send_through_proxy( $url ) ) { - $options['proxy'] = new Requests_Proxy_HTTP( $proxy->host() . ':' . $proxy->port() ); + $options['proxy'] = new WpOrg\Requests\Proxy\HTTP( $proxy->host() . ':' . $proxy->port() ); if ( $proxy->use_authentication() ) { $options['proxy']->use_authentication = true; @@ -399,7 +392,7 @@ public function request( $url, $args = array() ) { mbstring_binary_safe_encoding(); try { - $requests_response = Requests::request( $url, $headers, $data, $type, $options ); + $requests_response = WpOrg\Requests\Requests::request( $url, $headers, $data, $type, $options ); // Convert the response into an array. $http_response = new WP_HTTP_Requests_Response( $requests_response, $parsed_args['filename'] ); @@ -407,7 +400,7 @@ public function request( $url, $args = array() ) { // Add the original object to the array. $response['http_response'] = $http_response; - } catch ( Requests_Exception $e ) { + } catch ( WpOrg\Requests\Exception $e ) { $response = new WP_Error( 'http_request_failed', $e->getMessage() ); } @@ -424,7 +417,7 @@ public function request( $url, $args = array() ) { * @param array $parsed_args HTTP request arguments. * @param string $url The request URL. */ - do_action( 'http_api_debug', $response, 'response', 'Requests', $parsed_args, $url ); + do_action( 'http_api_debug', $response, 'response', 'WpOrg\Requests\Requests', $parsed_args, $url ); if ( is_wp_error( $response ) ) { return $response; } @@ -463,7 +456,7 @@ public function request( $url, $args = array() ) { * @return Requests_Cookie_Jar Cookie holder object. */ public static function normalize_cookies( $cookies ) { - $cookie_jar = new Requests_Cookie_Jar(); + $cookie_jar = new WpOrg\Requests\Cookie\Jar(); foreach ( $cookies as $name => $value ) { if ( $value instanceof WP_Http_Cookie ) { @@ -473,9 +466,9 @@ static function( $attr ) { return null !== $attr; } ); - $cookie_jar[ $value->name ] = new Requests_Cookie( $value->name, $value->value, $attributes, array( 'host-only' => $value->host_only ) ); + $cookie_jar[ $value->name ] = new WpOrg\Requests\Cookie( $value->name, $value->value, $attributes, array( 'host-only' => $value->host_only ) ); } elseif ( is_scalar( $value ) ) { - $cookie_jar[ $name ] = new Requests_Cookie( $name, (string) $value ); + $cookie_jar[ $name ] = new WpOrg\Requests\Cookie( $name, (string) $value ); } } @@ -500,7 +493,7 @@ static function( $attr ) { public static function browser_redirect_compatibility( $location, $headers, $data, &$options, $original ) { // Browser compatibility. if ( 302 === $original->status_code ) { - $options['type'] = Requests::GET; + $options['type'] = WpOrg\Requests\Requests::GET; } } @@ -514,7 +507,7 @@ public static function browser_redirect_compatibility( $location, $headers, $dat */ public static function validate_redirects( $location ) { if ( ! wp_http_validate_url( $location ) ) { - throw new Requests_Exception( __( 'A valid URL was not provided.' ), 'wp_http.redirect_failed_validation' ); + throw new WpOrg\Requests\Exception( __( 'A valid URL was not provided.' ), 'wp_http.redirect_failed_validation' ); } } diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index b611745ebd661..ef336ef664629 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -1414,7 +1414,7 @@ function rest_parse_request_arg( $value, $request, $param ) { function rest_is_ip_address( $ip ) { $ipv4_pattern = '/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/'; - if ( ! preg_match( $ipv4_pattern, $ip ) && ! Requests_IPv6::check_ipv6( $ip ) ) { + if ( ! preg_match( $ipv4_pattern, $ip ) && ! WpOrg\Requests\Ipv6::check_ipv6( $ip ) ) { return false; } From 0c34efeca3e4fa4a044636fc3915c0eabc297416 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 24 Nov 2022 01:23:30 +0100 Subject: [PATCH 4/5] Upgrade to Requests 2.0: test updates only --- tests/phpunit/tests/feed/wpSimplePieFile.php | 4 ++-- tests/phpunit/tests/functions/maybeSerialize.php | 9 ++++----- tests/phpunit/tests/http/functions.php | 4 ++-- tests/phpunit/tests/http/http.php | 4 ++-- tests/phpunit/tests/rest-api/rest-widgets-controller.php | 2 +- tests/phpunit/tests/widgets/wpWidgetRss.php | 2 +- 6 files changed, 12 insertions(+), 13 deletions(-) diff --git a/tests/phpunit/tests/feed/wpSimplePieFile.php b/tests/phpunit/tests/feed/wpSimplePieFile.php index f9748e3a7bcac..b56c5a0c44c1a 100644 --- a/tests/phpunit/tests/feed/wpSimplePieFile.php +++ b/tests/phpunit/tests/feed/wpSimplePieFile.php @@ -85,7 +85,7 @@ public function mocked_response_single_header_values() { ); return array( - 'headers' => new Requests_Utility_CaseInsensitiveDictionary( $single_value_headers ), + 'headers' => new WpOrg\Requests\Utility\CaseInsensitiveDictionary( $single_value_headers ), 'body' => file_get_contents( DIR_TESTDATA . '/feed/wordpress-org-news.xml' ), 'response' => array( 'code' => 200, @@ -114,7 +114,7 @@ public function mocked_response_multiple_header_values() { ), ); - $response['headers'] = new Requests_Utility_CaseInsensitiveDictionary( $multiple_value_headers ); + $response['headers'] = new WpOrg\Requests\Utility\CaseInsensitiveDictionary( $multiple_value_headers ); return $response; } diff --git a/tests/phpunit/tests/functions/maybeSerialize.php b/tests/phpunit/tests/functions/maybeSerialize.php index b95e270b1aa31..e8986d9818fa3 100644 --- a/tests/phpunit/tests/functions/maybeSerialize.php +++ b/tests/phpunit/tests/functions/maybeSerialize.php @@ -213,10 +213,9 @@ public function data_is_not_serialized() { public function test_deserialize_request_utility_filtered_iterator_objects( $value ) { $serialized = maybe_serialize( $value ); - $className = get_class( $value ); - if ( $className === 'Requests_Utility_FilteredIterator' || $className === 'WpOrg\Requests\Utility\FilteredIterator' ) { + if ( get_class( $value ) === 'WpOrg\Requests\Utility\FilteredIterator' ) { $new_value = unserialize( $serialized ); - $property = ( new ReflectionClass( 'Requests_Utility_FilteredIterator' ) )->getProperty( 'callback' ); + $property = ( new ReflectionClass( 'WpOrg\Requests\Utility\FilteredIterator' ) )->getProperty( 'callback' ); $property->setAccessible( true ); $callback_value = $property->getValue( $new_value ); @@ -234,10 +233,10 @@ public function test_deserialize_request_utility_filtered_iterator_objects( $val public function data_serialize_deserialize_objects() { return array( 'filtered iterator using md5' => array( - new Requests_Utility_FilteredIterator( array( 1 ), 'md5' ), + new WpOrg\Requests\Utility\FilteredIterator( array( 1 ), 'md5' ), ), 'filtered iterator using sha1' => array( - new Requests_Utility_FilteredIterator( array( 1, 2 ), 'sha1' ), + new WpOrg\Requests\Utility\FilteredIterator( array( 1, 2 ), 'sha1' ), ), 'array iterator' => array( new ArrayIterator( array( 1, 2, 3 ) ), diff --git a/tests/phpunit/tests/http/functions.php b/tests/phpunit/tests/http/functions.php index 49854e0053ef8..8461426db0919 100644 --- a/tests/phpunit/tests/http/functions.php +++ b/tests/phpunit/tests/http/functions.php @@ -216,9 +216,9 @@ public function test_get_response_cookies_with_name_value_array() { */ public function test_get_cookie_host_only() { // Emulate WP_Http::request() internals. - $requests_response = new Requests_Response(); + $requests_response = new WpOrg\Requests\Response(); - $requests_response->cookies['test'] = Requests_Cookie::parse( 'test=foo; domain=.wordpress.org' ); + $requests_response->cookies['test'] = WpOrg\Requests\Cookie::parse( 'test=foo; domain=.wordpress.org' ); $requests_response->cookies['test']->flags['host-only'] = false; // https://github.com/WordPress/Requests/issues/306 diff --git a/tests/phpunit/tests/http/http.php b/tests/phpunit/tests/http/http.php index 23a28f4d75fd8..be9545c6f0569 100644 --- a/tests/phpunit/tests/http/http.php +++ b/tests/phpunit/tests/http/http.php @@ -309,13 +309,13 @@ public function test_normalize_cookies_scalar_values() { ) ); - $this->assertInstanceOf( 'Requests_Cookie_Jar', $cookie_jar ); + $this->assertInstanceOf( 'WpOrg\Requests\Cookie\Jar', $cookie_jar ); foreach ( array_keys( $cookies ) as $cookie ) { if ( 'foo' === $cookie ) { $this->assertArrayNotHasKey( $cookie, $cookie_jar ); } else { - $this->assertInstanceOf( 'Requests_Cookie', $cookie_jar[ $cookie ] ); + $this->assertInstanceOf( 'WpOrg\Requests\Cookie', $cookie_jar[ $cookie ] ); } } } diff --git a/tests/phpunit/tests/rest-api/rest-widgets-controller.php b/tests/phpunit/tests/rest-api/rest-widgets-controller.php index 28ac8bc6e3213..ed6fe01e4ec78 100644 --- a/tests/phpunit/tests/rest-api/rest-widgets-controller.php +++ b/tests/phpunit/tests/rest-api/rest-widgets-controller.php @@ -417,7 +417,7 @@ public function mocked_rss_response() { ); return array( - 'headers' => new Requests_Utility_CaseInsensitiveDictionary( $single_value_headers ), + 'headers' => new WpOrg\Requests\Utility\CaseInsensitiveDictionary( $single_value_headers ), 'body' => file_get_contents( DIR_TESTDATA . '/feed/wordpress-org-news.xml' ), 'response' => array( 'code' => 200, diff --git a/tests/phpunit/tests/widgets/wpWidgetRss.php b/tests/phpunit/tests/widgets/wpWidgetRss.php index 495869f47564f..424969c1263af 100644 --- a/tests/phpunit/tests/widgets/wpWidgetRss.php +++ b/tests/phpunit/tests/widgets/wpWidgetRss.php @@ -106,7 +106,7 @@ public function mocked_rss_response() { ); return array( - 'headers' => new Requests_Utility_CaseInsensitiveDictionary( $single_value_headers ), + 'headers' => new WpOrg\Requests\Utility\CaseInsensitiveDictionary( $single_value_headers ), 'body' => file_get_contents( DIR_TESTDATA . '/feed/wordpress-org-news.xml' ), 'response' => array( 'code' => 200, From 593f744198ae85dc69355a237ef1f51e347b40f2 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 24 Nov 2022 01:24:11 +0100 Subject: [PATCH 5/5] Upgrade to Requests 2.0: documentation updates only --- src/wp-includes/class-wp-http-requests-hooks.php | 2 +- .../class-wp-http-requests-response.php | 12 ++++++------ src/wp-includes/class-wp-http.php | 16 ++++++++-------- src/wp-includes/deprecated.php | 2 +- src/wp-includes/functions.php | 2 +- src/wp-includes/http.php | 8 ++++---- tests/phpunit/tests/http/functions.php | 2 +- 7 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/wp-includes/class-wp-http-requests-hooks.php b/src/wp-includes/class-wp-http-requests-hooks.php index f069e4f7a783a..80e4eb7ef4b9f 100644 --- a/src/wp-includes/class-wp-http-requests-hooks.php +++ b/src/wp-includes/class-wp-http-requests-hooks.php @@ -12,7 +12,7 @@ * * @since 4.7.0 * - * @see Requests_Hooks + * @see WpOrg\Requests\Hooks */ #[AllowDynamicProperties] class WP_HTTP_Requests_Hooks extends WpOrg\Requests\Hooks { diff --git a/src/wp-includes/class-wp-http-requests-response.php b/src/wp-includes/class-wp-http-requests-response.php index 679d1e0b0b720..821077656c84b 100644 --- a/src/wp-includes/class-wp-http-requests-response.php +++ b/src/wp-includes/class-wp-http-requests-response.php @@ -8,7 +8,7 @@ */ /** - * Core wrapper object for a Requests_Response for standardisation. + * Core wrapper object for a WpOrg\Requests\Response for standardisation. * * @since 4.6.0 * @@ -19,7 +19,7 @@ class WP_HTTP_Requests_Response extends WP_HTTP_Response { * Requests Response object. * * @since 4.6.0 - * @var Requests_Response + * @var \WpOrg\Requests\Response */ protected $response; @@ -36,8 +36,8 @@ class WP_HTTP_Requests_Response extends WP_HTTP_Response { * * @since 4.6.0 * - * @param Requests_Response $response HTTP response. - * @param string $filename Optional. File name. Default empty. + * @param \WpOrg\Requests\Response $response HTTP response. + * @param string $filename Optional. File name. Default empty. */ public function __construct( WpOrg\Requests\Response $response, $filename = '' ) { $this->response = $response; @@ -49,7 +49,7 @@ public function __construct( WpOrg\Requests\Response $response, $filename = '' ) * * @since 4.6.0 * - * @return Requests_Response HTTP response. + * @return WpOrg\Requests\Response HTTP response. */ public function get_response_object() { return $this->response; @@ -60,7 +60,7 @@ public function get_response_object() { * * @since 4.6.0 * - * @return \Requests_Utility_CaseInsensitiveDictionary Map of header name to header value. + * @return \WpOrg\Requests\Utility\CaseInsensitiveDictionary Map of header name to header value. */ public function get_headers() { // Ensure headers remain case-insensitive. diff --git a/src/wp-includes/class-wp-http.php b/src/wp-includes/class-wp-http.php index 6f9f338229f6e..11bd2f944064f 100644 --- a/src/wp-includes/class-wp-http.php +++ b/src/wp-includes/class-wp-http.php @@ -347,7 +347,7 @@ public function request( $url, $args = array() ) { $options['max_bytes'] = $parsed_args['limit_response_size']; } - // If we've got cookies, use and convert them to Requests_Cookie. + // If we've got cookies, use and convert them to WpOrg\Requests\Cookie. if ( ! empty( $parsed_args['cookies'] ) ) { $options['cookies'] = WP_Http::normalize_cookies( $parsed_args['cookies'] ); } @@ -453,7 +453,7 @@ public function request( $url, $args = array() ) { * @since 4.6.0 * * @param array $cookies Array of cookies to send with the request. - * @return Requests_Cookie_Jar Cookie holder object. + * @return WpOrg\Requests\Cookie\Jar Cookie holder object. */ public static function normalize_cookies( $cookies ) { $cookie_jar = new WpOrg\Requests\Cookie\Jar(); @@ -484,11 +484,11 @@ static function( $attr ) { * * @since 4.6.0 * - * @param string $location URL to redirect to. - * @param array $headers Headers for the redirect. - * @param string|array $data Body to send with the request. - * @param array $options Redirect request options. - * @param Requests_Response $original Response object. + * @param string $location URL to redirect to. + * @param array $headers Headers for the redirect. + * @param string|array $data Body to send with the request. + * @param array $options Redirect request options. + * @param WpOrg\Requests\Response $original Response object. */ public static function browser_redirect_compatibility( $location, $headers, $data, &$options, $original ) { // Browser compatibility. @@ -502,7 +502,7 @@ public static function browser_redirect_compatibility( $location, $headers, $dat * * @since 4.7.5 * - * @throws Requests_Exception On unsuccessful URL validation. + * @throws WpOrg\Requests\Exception On unsuccessful URL validation. * @param string $location URL to redirect to. */ public static function validate_redirects( $location ) { diff --git a/src/wp-includes/deprecated.php b/src/wp-includes/deprecated.php index 8f41b1303810b..bd95c7d64b1f1 100644 --- a/src/wp-includes/deprecated.php +++ b/src/wp-includes/deprecated.php @@ -3654,7 +3654,7 @@ function post_permalink( $post = 0 ) { * @param string|bool $file_path Optional. File path to write request to. Default false. * @param int $red Optional. The number of Redirects followed, Upon 5 being hit, * returns false. Default 1. - * @return \Requests_Utility_CaseInsensitiveDictionary|false Headers on success, false on failure. + * @return \WpOrg\Requests\Utility\CaseInsensitiveDictionary|false Headers on success, false on failure. */ function wp_get_http( $url, $file_path = false, $red = 1 ) { _deprecated_function( __FUNCTION__, '4.4.0', 'WP_Http' ); diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 716be80980c49..3177d389aa929 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -978,7 +978,7 @@ function do_enclose( $content, $post ) { * * @param string $url URL to retrieve HTTP headers from. * @param bool $deprecated Not Used. - * @return \Requests_Utility_CaseInsensitiveDictionary|false Headers on success, false on failure. + * @return \WpOrg\Requests\Utility\CaseInsensitiveDictionary|false Headers on success, false on failure. */ function wp_get_http_headers( $url, $deprecated = false ) { if ( ! empty( $deprecated ) ) { diff --git a/src/wp-includes/http.php b/src/wp-includes/http.php index f77d9a69a7b86..a8468ca17cbbb 100644 --- a/src/wp-includes/http.php +++ b/src/wp-includes/http.php @@ -200,13 +200,13 @@ function wp_remote_head( $url, $args = array() ) { * Retrieve only the headers from the raw response. * * @since 2.7.0 - * @since 4.6.0 Return value changed from an array to an Requests_Utility_CaseInsensitiveDictionary instance. + * @since 4.6.0 Return value changed from an array to an WpOrg\Requests\Utility\CaseInsensitiveDictionary instance. * - * @see \Requests_Utility_CaseInsensitiveDictionary + * @see \WpOrg\Requests\Utility\CaseInsensitiveDictionary * * @param array|WP_Error $response HTTP response. - * @return \Requests_Utility_CaseInsensitiveDictionary|array The headers of the response, or empty array - * if incorrect parameter given. + * @return \WpOrg\Requests\Utility\CaseInsensitiveDictionary|array The headers of the response, or empty array + * if incorrect parameter given. */ function wp_remote_retrieve_headers( $response ) { if ( is_wp_error( $response ) || ! isset( $response['headers'] ) ) { diff --git a/tests/phpunit/tests/http/functions.php b/tests/phpunit/tests/http/functions.php index 8461426db0919..5aa92fdf501d9 100644 --- a/tests/phpunit/tests/http/functions.php +++ b/tests/phpunit/tests/http/functions.php @@ -231,7 +231,7 @@ public function test_get_cookie_host_only() { $this->assertSame( $cookie->domain, 'wordpress.org' ); $this->assertFalse( $cookie->host_only, 'host-only flag not set' ); - // Regurgitate (Requests_Cookie -> WP_Http_Cookie -> Requests_Cookie). + // Regurgitate (WpOrg\Requests\Cookie -> WP_Http_Cookie -> WpOrg\Requests\Cookie). $cookies = WP_Http::normalize_cookies( wp_remote_retrieve_cookies( $response ) ); $this->assertFalse( $cookies['test']->flags['host-only'], 'host-only flag data lost' ); }