From 645e753a51db743658e95570caae5eda6c9cff3b Mon Sep 17 00:00:00 2001 From: hellofromTonya Date: Thu, 15 Dec 2022 21:32:17 +0000 Subject: [PATCH] External Libraries: Update Requests library to version 2.0.0. This is a major release and contains breaking changes. Most important changes to be aware of for this release: * All code is now namespaced. Though there is a full backward compatibility layer available and the old class names are still supported, using them will generate a deprecation notice (which can be silenced by plugins if they'd need to support multiple WP versions). See the [https://requests.ryanmccue.info/docs/upgrading.html upgrade guide] for more details. * A lot of classes have been marked `final`. This should generally not affect userland code as care has been taken to not apply the `final` keyword to classes which are known to be extended in userland code. * Extensive input validation has been added to Requests. When Requests is used as documented though, this will be unnoticable. * A new `WpOrg\Requests\Requests::has_capabilities()` method has been introduced which can be used to address #37708. * A new `WpOrg\Requests\Response::decode_body()` method has been introduced which may be usable to simplify some of the WP native wrapper code. * Remaining PHP 8.0 compatibility fixed (support for named parameters). * PHP 8.1 compatibility. Release notes: https://github.com/WordPress/Requests/releases/tag/v2.0.0 For a full list of changes in this update, see the Requests GitHub: https://github.com/WordPress/Requests/compare/v1.8.1...v2.0.0 This commit also resolves 2 blocking issues which previously caused the revert of [52244]: * New Requests files are loaded into `wp-includes/Requests/src/`, matching the location of the library. In doing so, filesystems that are case-insensitive are not impacted (see #54582). * Preload: During a Core update, the old Requests files are preloaded into memory before the update deletes the files. Preloading avoids fatal errors noted in #54562. Follow-up to [50842], [51078], [52244], [52315], [52327], [52328]. Props jrf, schlessera, datagutten, wojsmol, dustinrue, soulseekah, szepeviktor. costdev, sergeybiryukov, peterwilsoncc, ironprogrammer, antonvlasenko, hellofromTonya, swissspidy, dd32, azaozz, TobiasBg, audrasjb. Fixes #54504. See #54582, #54562. Built from https://develop.svn.wordpress.org/trunk@54997 git-svn-id: http://core.svn.wordpress.org/trunk@54530 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-admin/includes/update-core.php | 143 ++- wp-includes/Requests/Auth.php | 33 - wp-includes/Requests/Auth/Basic.php | 88 -- wp-includes/Requests/Cookie/Jar.php | 175 --- wp-includes/Requests/Exception/HTTP/429.php | 29 - wp-includes/Requests/Exception/Transport.php | 5 - wp-includes/Requests/Hooks.php | 68 - wp-includes/Requests/Proxy.php | 35 - wp-includes/Requests/Response.php | 122 -- wp-includes/Requests/Response/Headers.php | 98 -- wp-includes/Requests/Transport.php | 41 - .../Utility/CaseInsensitiveDictionary.php | 103 -- .../Requests/Utility/FilteredIterator.php | 65 - wp-includes/Requests/src/Auth.php | 36 + wp-includes/Requests/src/Auth/Basic.php | 103 ++ wp-includes/Requests/src/Autoload.php | 187 +++ wp-includes/Requests/src/Capability.php | 36 + wp-includes/Requests/{ => src}/Cookie.php | 217 ++-- wp-includes/Requests/src/Cookie/Jar.php | 186 +++ wp-includes/Requests/{ => src}/Exception.php | 12 +- .../Requests/src/Exception/ArgumentCount.php | 47 + .../HTTP.php => src/Exception/Http.php} | 21 +- .../Exception/Http/Status304.php} | 10 +- .../Exception/Http/Status305.php} | 10 +- .../Exception/Http/Status306.php} | 10 +- .../Exception/Http/Status400.php} | 10 +- .../Exception/Http/Status401.php} | 10 +- .../Exception/Http/Status402.php} | 10 +- .../Exception/Http/Status403.php} | 10 +- .../Exception/Http/Status404.php} | 10 +- .../Exception/Http/Status405.php} | 10 +- .../Exception/Http/Status406.php} | 10 +- .../Exception/Http/Status407.php} | 10 +- .../Exception/Http/Status408.php} | 10 +- .../Exception/Http/Status409.php} | 10 +- .../Exception/Http/Status410.php} | 10 +- .../Exception/Http/Status411.php} | 10 +- .../Exception/Http/Status412.php} | 10 +- .../Exception/Http/Status413.php} | 10 +- .../Exception/Http/Status414.php} | 10 +- .../Exception/Http/Status415.php} | 10 +- .../Exception/Http/Status416.php} | 10 +- .../Exception/Http/Status417.php} | 10 +- .../Exception/Http/Status418.php} | 16 +- .../Exception/Http/Status428.php} | 16 +- .../Requests/src/Exception/Http/Status429.php | 35 + .../Exception/Http/Status431.php} | 16 +- .../Exception/Http/Status500.php} | 10 +- .../Exception/Http/Status501.php} | 10 +- .../Exception/Http/Status502.php} | 10 +- .../Exception/Http/Status503.php} | 10 +- .../Exception/Http/Status504.php} | 10 +- .../Exception/Http/Status505.php} | 10 +- .../Exception/Http/Status511.php} | 16 +- .../Exception/Http/StatusUnknown.php} | 17 +- .../src/Exception/InvalidArgument.php | 41 + .../Requests/src/Exception/Transport.php | 17 + .../Exception/Transport/Curl.php} | 30 +- .../{Hooker.php => src/HookManager.php} | 14 +- wp-includes/Requests/src/Hooks.php | 99 ++ .../{IDNAEncoder.php => src/IdnaEncoder.php} | 205 +-- .../Requests/{IPv6.php => src/Ipv6.php} | 69 +- wp-includes/Requests/{IRI.php => src/Iri.php} | 105 +- wp-includes/Requests/src/Port.php | 75 ++ wp-includes/Requests/src/Proxy.php | 38 + .../{Proxy/HTTP.php => src/Proxy/Http.php} | 67 +- wp-includes/Requests/src/Requests.php | 1095 +++++++++++++++++ wp-includes/Requests/src/Response.php | 165 +++ wp-includes/Requests/src/Response/Headers.php | 124 ++ wp-includes/Requests/{ => src}/Session.php | 148 ++- wp-includes/Requests/{SSL.php => src/Ssl.php} | 78 +- wp-includes/Requests/src/Transport.php | 45 + .../cURL.php => src/Transport/Curl.php} | 223 ++-- .../Transport/Fsockopen.php} | 218 ++-- .../src/Utility/CaseInsensitiveDictionary.php | 127 ++ .../Requests/src/Utility/FilteredIterator.php | 82 ++ .../Requests/src/Utility/InputValidator.php | 109 ++ wp-includes/class-requests.php | 997 +-------------- wp-includes/class-wp-http-requests-hooks.php | 4 +- .../class-wp-http-requests-response.php | 18 +- wp-includes/class-wp-http.php | 48 +- wp-includes/deprecated.php | 2 +- wp-includes/functions.php | 2 +- wp-includes/http.php | 8 +- wp-includes/rest-api.php | 2 +- wp-includes/version.php | 2 +- 86 files changed, 3965 insertions(+), 2528 deletions(-) delete mode 100644 wp-includes/Requests/Auth.php delete mode 100644 wp-includes/Requests/Auth/Basic.php delete mode 100644 wp-includes/Requests/Cookie/Jar.php delete mode 100644 wp-includes/Requests/Exception/HTTP/429.php delete mode 100644 wp-includes/Requests/Exception/Transport.php delete mode 100644 wp-includes/Requests/Hooks.php delete mode 100644 wp-includes/Requests/Proxy.php delete mode 100644 wp-includes/Requests/Response.php delete mode 100644 wp-includes/Requests/Response/Headers.php delete mode 100644 wp-includes/Requests/Transport.php delete mode 100644 wp-includes/Requests/Utility/CaseInsensitiveDictionary.php delete mode 100644 wp-includes/Requests/Utility/FilteredIterator.php create mode 100644 wp-includes/Requests/src/Auth.php create mode 100644 wp-includes/Requests/src/Auth/Basic.php create mode 100644 wp-includes/Requests/src/Autoload.php create mode 100644 wp-includes/Requests/src/Capability.php rename wp-includes/Requests/{ => src}/Cookie.php (68%) create mode 100644 wp-includes/Requests/src/Cookie/Jar.php rename wp-includes/Requests/{ => src}/Exception.php (80%) create mode 100644 wp-includes/Requests/src/Exception/ArgumentCount.php rename wp-includes/Requests/{Exception/HTTP.php => src/Exception/Http.php} (74%) rename wp-includes/Requests/{Exception/HTTP/304.php => src/Exception/Http/Status304.php} (61%) rename wp-includes/Requests/{Exception/HTTP/305.php => src/Exception/Http/Status305.php} (60%) rename wp-includes/Requests/{Exception/HTTP/306.php => src/Exception/Http/Status306.php} (61%) rename wp-includes/Requests/{Exception/HTTP/400.php => src/Exception/Http/Status400.php} (60%) rename wp-includes/Requests/{Exception/HTTP/401.php => src/Exception/Http/Status401.php} (61%) rename wp-includes/Requests/{Exception/HTTP/402.php => src/Exception/Http/Status402.php} (62%) rename wp-includes/Requests/{Exception/HTTP/403.php => src/Exception/Http/Status403.php} (60%) rename wp-includes/Requests/{Exception/HTTP/404.php => src/Exception/Http/Status404.php} (60%) rename wp-includes/Requests/{Exception/HTTP/405.php => src/Exception/Http/Status405.php} (62%) rename wp-includes/Requests/{Exception/HTTP/406.php => src/Exception/Http/Status406.php} (61%) rename wp-includes/Requests/{Exception/HTTP/407.php => src/Exception/Http/Status407.php} (64%) rename wp-includes/Requests/{Exception/HTTP/408.php => src/Exception/Http/Status408.php} (61%) rename wp-includes/Requests/{Exception/HTTP/409.php => src/Exception/Http/Status409.php} (60%) rename wp-includes/Requests/{Exception/HTTP/410.php => src/Exception/Http/Status410.php} (58%) rename wp-includes/Requests/{Exception/HTTP/411.php => src/Exception/Http/Status411.php} (61%) rename wp-includes/Requests/{Exception/HTTP/412.php => src/Exception/Http/Status412.php} (62%) rename wp-includes/Requests/{Exception/HTTP/413.php => src/Exception/Http/Status413.php} (63%) rename wp-includes/Requests/{Exception/HTTP/414.php => src/Exception/Http/Status414.php} (63%) rename wp-includes/Requests/{Exception/HTTP/415.php => src/Exception/Http/Status415.php} (63%) rename wp-includes/Requests/{Exception/HTTP/416.php => src/Exception/Http/Status416.php} (65%) rename wp-includes/Requests/{Exception/HTTP/417.php => src/Exception/Http/Status417.php} (62%) rename wp-includes/Requests/{Exception/HTTP/418.php => src/Exception/Http/Status418.php} (50%) rename wp-includes/Requests/{Exception/HTTP/428.php => src/Exception/Http/Status428.php} (52%) create mode 100644 wp-includes/Requests/src/Exception/Http/Status429.php rename wp-includes/Requests/{Exception/HTTP/431.php => src/Exception/Http/Status431.php} (55%) rename wp-includes/Requests/{Exception/HTTP/500.php => src/Exception/Http/Status500.php} (63%) rename wp-includes/Requests/{Exception/HTTP/501.php => src/Exception/Http/Status501.php} (61%) rename wp-includes/Requests/{Exception/HTTP/502.php => src/Exception/Http/Status502.php} (60%) rename wp-includes/Requests/{Exception/HTTP/503.php => src/Exception/Http/Status503.php} (62%) rename wp-includes/Requests/{Exception/HTTP/504.php => src/Exception/Http/Status504.php} (61%) rename wp-includes/Requests/{Exception/HTTP/505.php => src/Exception/Http/Status505.php} (64%) rename wp-includes/Requests/{Exception/HTTP/511.php => src/Exception/Http/Status511.php} (55%) rename wp-includes/Requests/{Exception/HTTP/Unknown.php => src/Exception/Http/StatusUnknown.php} (61%) create mode 100644 wp-includes/Requests/src/Exception/InvalidArgument.php create mode 100644 wp-includes/Requests/src/Exception/Transport.php rename wp-includes/Requests/{Exception/Transport/cURL.php => src/Exception/Transport/Curl.php} (56%) rename wp-includes/Requests/{Hooker.php => src/HookManager.php} (66%) create mode 100644 wp-includes/Requests/src/Hooks.php rename wp-includes/Requests/{IDNAEncoder.php => src/IdnaEncoder.php} (60%) rename wp-includes/Requests/{IPv6.php => src/Ipv6.php} (79%) rename wp-includes/Requests/{IRI.php => src/Iri.php} (91%) create mode 100644 wp-includes/Requests/src/Port.php create mode 100644 wp-includes/Requests/src/Proxy.php rename wp-includes/Requests/{Proxy/HTTP.php => src/Proxy/Http.php} (56%) create mode 100644 wp-includes/Requests/src/Requests.php create mode 100644 wp-includes/Requests/src/Response.php create mode 100644 wp-includes/Requests/src/Response/Headers.php rename wp-includes/Requests/{ => src}/Session.php (51%) rename wp-includes/Requests/{SSL.php => src/Ssl.php} (55%) create mode 100644 wp-includes/Requests/src/Transport.php rename wp-includes/Requests/{Transport/cURL.php => src/Transport/Curl.php} (69%) rename wp-includes/Requests/{Transport/fsockopen.php => src/Transport/Fsockopen.php} (59%) create mode 100644 wp-includes/Requests/src/Utility/CaseInsensitiveDictionary.php create mode 100644 wp-includes/Requests/src/Utility/FilteredIterator.php create mode 100644 wp-includes/Requests/src/Utility/InputValidator.php diff --git a/wp-admin/includes/update-core.php b/wp-admin/includes/update-core.php index c240fb0ee6..a17c03359a 100644 --- a/wp-admin/includes/update-core.php +++ b/wp-admin/includes/update-core.php @@ -865,6 +865,96 @@ $_old_files = array( 'wp-includes/blocks/comments-query-loop', ); +/** + * Stores Requests files to be preloaded and deleted. + * + * For classes/interfaces, use the class/interface name + * as the array key. + * + * All other files/directories should not have a key. + * + * @since 6.2.0 + * + * @global array $_old_requests_files + * @var array + * @name $_old_requests_files + */ +global $_old_requests_files; + +$_old_requests_files = array( + // Interfaces. + 'Requests_Auth' => 'wp-includes/Requests/Auth.php', + 'Requests_Hooker' => 'wp-includes/Requests/Hooker.php', + 'Requests_Proxy' => 'wp-includes/Requests/Proxy.php', + 'Requests_Transport' => 'wp-includes/Requests/Transport.php', + + // Classes. + 'Requests_Auth_Basic' => 'wp-includes/Requests/Auth/Basic.php', + 'Requests_Cookie_Jar' => 'wp-includes/Requests/Cookie/Jar.php', + 'Requests_Exception_HTTP' => 'wp-includes/Requests/Exception/HTTP.php', + 'Requests_Exception_Transport' => 'wp-includes/Requests/Exception/Transport.php', + 'Requests_Exception_HTTP_304' => 'wp-includes/Requests/Exception/HTTP/304.php', + 'Requests_Exception_HTTP_305' => 'wp-includes/Requests/Exception/HTTP/305.php', + 'Requests_Exception_HTTP_306' => 'wp-includes/Requests/Exception/HTTP/306.php', + 'Requests_Exception_HTTP_400' => 'wp-includes/Requests/Exception/HTTP/400.php', + 'Requests_Exception_HTTP_401' => 'wp-includes/Requests/Exception/HTTP/401.php', + 'Requests_Exception_HTTP_402' => 'wp-includes/Requests/Exception/HTTP/402.php', + 'Requests_Exception_HTTP_403' => 'wp-includes/Requests/Exception/HTTP/403.php', + 'Requests_Exception_HTTP_404' => 'wp-includes/Requests/Exception/HTTP/404.php', + 'Requests_Exception_HTTP_405' => 'wp-includes/Requests/Exception/HTTP/405.php', + 'Requests_Exception_HTTP_406' => 'wp-includes/Requests/Exception/HTTP/406.php', + 'Requests_Exception_HTTP_407' => 'wp-includes/Requests/Exception/HTTP/407.php', + 'Requests_Exception_HTTP_408' => 'wp-includes/Requests/Exception/HTTP/408.php', + 'Requests_Exception_HTTP_409' => 'wp-includes/Requests/Exception/HTTP/409.php', + 'Requests_Exception_HTTP_410' => 'wp-includes/Requests/Exception/HTTP/410.php', + 'Requests_Exception_HTTP_411' => 'wp-includes/Requests/Exception/HTTP/411.php', + 'Requests_Exception_HTTP_412' => 'wp-includes/Requests/Exception/HTTP/412.php', + 'Requests_Exception_HTTP_413' => 'wp-includes/Requests/Exception/HTTP/413.php', + 'Requests_Exception_HTTP_414' => 'wp-includes/Requests/Exception/HTTP/414.php', + 'Requests_Exception_HTTP_415' => 'wp-includes/Requests/Exception/HTTP/415.php', + 'Requests_Exception_HTTP_416' => 'wp-includes/Requests/Exception/HTTP/416.php', + 'Requests_Exception_HTTP_417' => 'wp-includes/Requests/Exception/HTTP/417.php', + 'Requests_Exception_HTTP_418' => 'wp-includes/Requests/Exception/HTTP/418.php', + 'Requests_Exception_HTTP_428' => 'wp-includes/Requests/Exception/HTTP/428.php', + 'Requests_Exception_HTTP_429' => 'wp-includes/Requests/Exception/HTTP/429.php', + 'Requests_Exception_HTTP_431' => 'wp-includes/Requests/Exception/HTTP/431.php', + 'Requests_Exception_HTTP_500' => 'wp-includes/Requests/Exception/HTTP/500.php', + 'Requests_Exception_HTTP_501' => 'wp-includes/Requests/Exception/HTTP/501.php', + 'Requests_Exception_HTTP_502' => 'wp-includes/Requests/Exception/HTTP/502.php', + 'Requests_Exception_HTTP_503' => 'wp-includes/Requests/Exception/HTTP/503.php', + 'Requests_Exception_HTTP_504' => 'wp-includes/Requests/Exception/HTTP/504.php', + 'Requests_Exception_HTTP_505' => 'wp-includes/Requests/Exception/HTTP/505.php', + 'Requests_Exception_HTTP_511' => 'wp-includes/Requests/Exception/HTTP/511.php', + 'Requests_Exception_HTTP_Unknown' => 'wp-includes/Requests/Exception/HTTP/Unknown.php', + 'Requests_Exception_Transport_cURL' => 'wp-includes/Requests/Exception/Transport/cURL.php', + 'Requests_Proxy_HTTP' => 'wp-includes/Requests/Proxy/HTTP.php', + 'Requests_Response_Headers' => 'wp-includes/Requests/Response/Headers.php', + 'Requests_Transport_cURL' => 'wp-includes/Requests/Transport/cURL.php', + 'Requests_Transport_fsockopen' => 'wp-includes/Requests/Transport/fsockopen.php', + 'Requests_Utility_CaseInsensitiveDictionary' => 'wp-includes/Requests/Utility/CaseInsensitiveDictionary.php', + 'Requests_Utility_FilteredIterator' => 'wp-includes/Requests/Utility/FilteredIterator.php', + 'Requests_Cookie' => 'wp-includes/Requests/Cookie.php', + 'Requests_Exception' => 'wp-includes/Requests/Exception.php', + 'Requests_Hooks' => 'wp-includes/Requests/Hooks.php', + 'Requests_IDNAEncoder' => 'wp-includes/Requests/IDNAEncoder.php', + 'Requests_IPv6' => 'wp-includes/Requests/IPv6.php', + 'Requests_IRI' => 'wp-includes/Requests/IRI.php', + 'Requests_Response' => 'wp-includes/Requests/Response.php', + 'Requests_SSL' => 'wp-includes/Requests/SSL.php', + 'Requests_Session' => 'wp-includes/Requests/Session.php', + + // Directories. + 'wp-includes/Requests/Auth/', + 'wp-includes/Requests/Cookie/', + 'wp-includes/Requests/Exception/HTTP/', + 'wp-includes/Requests/Exception/Transport/', + 'wp-includes/Requests/Exception/', + 'wp-includes/Requests/Proxy/', + 'wp-includes/Requests/Response/', + 'wp-includes/Requests/Transport/', + 'wp-includes/Requests/Utility/', +); + /** * Stores new files in wp-content to copy * @@ -949,6 +1039,7 @@ $_new_bundled_files = array( * * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. * @global array $_old_files + * @global array $_old_requests_files * @global array $_new_bundled_files * @global wpdb $wpdb WordPress database abstraction object. * @global string $wp_version @@ -960,10 +1051,18 @@ $_new_bundled_files = array( * @return string|WP_Error New WordPress version on success, WP_Error on failure. */ function update_core( $from, $to ) { - global $wp_filesystem, $_old_files, $_new_bundled_files, $wpdb; + global $wp_filesystem, $_old_files, $_old_requests_files, $_new_bundled_files, $wpdb; set_time_limit( 300 ); + /* + * Merge the old Requests files and directories into the `$_old_files`. + * Then preload these Requests files first, before the files are deleted + * and replaced to ensure the code is in memory if needed. + */ + $_old_files = array_merge( $_old_files, array_values( $_old_requests_files ) ); + _preload_old_requests_classes_and_interfaces( $to ); + /** * Filters feedback messages displayed during the core update process. * @@ -1478,6 +1577,48 @@ function update_core( $from, $to ) { return $wp_version; } +/** + * Preloads old Requests classes and interfaces. + * + * This function preloads the old Requests code into memory before the + * upgrade process deletes the files. Why? Requests code is loaded into + * memory via an autoloader, meaning when a class or interface is needed + * If a request is in process, Requests could attempt to access code. If + * the file is not there, a fatal error could occur. If the file was + * replaced, the new code is not compatible with the old, resulting in + * a fatal error. Preloading ensures the code is in memory before the + * code is updated. + * + * @since 6.2.0 + * + * @global array $_old_requests_files Requests files to be preloaded. + * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. + * + * @param string $to Path to old WordPress installation. + */ +function _preload_old_requests_classes_and_interfaces( $to ) { + global $_old_requests_files, $wp_filesystem; + + foreach ( $_old_requests_files as $name => $file ) { + // Skip files that aren't interfaces or classes. + if ( is_int( $name ) ) { + continue; + } + + // Skip if it's already loaded. + if ( class_exists( $name ) || interface_exists( $name ) ) { + continue; + } + + // Skip if the file is missing. + if ( ! $wp_filesystem->is_file( $to . $file ) ) { + continue; + } + + require_once $to . $file; + } +} + /** * Redirect to the About WordPress page after a successful upgrade. * diff --git a/wp-includes/Requests/Auth.php b/wp-includes/Requests/Auth.php deleted file mode 100644 index 914c7449ca..0000000000 --- a/wp-includes/Requests/Auth.php +++ /dev/null @@ -1,33 +0,0 @@ -user, $this->pass) = $args; - } - } - - /** - * Register the necessary callbacks - * - * @see curl_before_send - * @see fsockopen_header - * @param 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')); - } - - /** - * Set cURL parameters before the data is sent - * - * @param resource $handle cURL resource - */ - public function curl_before_send(&$handle) { - curl_setopt($handle, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); - curl_setopt($handle, CURLOPT_USERPWD, $this->getAuthString()); - } - - /** - * Add extra headers to the request before sending - * - * @param string $out HTTP header string - */ - public function fsockopen_header(&$out) { - $out .= sprintf("Authorization: Basic %s\r\n", base64_encode($this->getAuthString())); - } - - /** - * Get the authentication string (user:pass) - * - * @return string - */ - public function getAuthString() { - return $this->user . ':' . $this->pass; - } -} diff --git a/wp-includes/Requests/Cookie/Jar.php b/wp-includes/Requests/Cookie/Jar.php deleted file mode 100644 index a816f90a0a..0000000000 --- a/wp-includes/Requests/Cookie/Jar.php +++ /dev/null @@ -1,175 +0,0 @@ -cookies = $cookies; - } - - /** - * Normalise cookie data into a Requests_Cookie - * - * @param string|Requests_Cookie $cookie - * @return Requests_Cookie - */ - public function normalize_cookie($cookie, $key = null) { - if ($cookie instanceof Requests_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); - } - - /** - * Check if the given item exists - * - * @param string $key Item key - * @return boolean Does the item exist? - */ - public function offsetExists($key) { - return isset($this->cookies[$key]); - } - - /** - * Get the value for the item - * - * @param string $key Item key - * @return string|null Item value (null if offsetExists is false) - */ - public function offsetGet($key) { - if (!isset($this->cookies[$key])) { - return null; - } - - return $this->cookies[$key]; - } - - /** - * Set the given item - * - * @throws Requests_Exception On attempting to use dictionary as list (`invalidset`) - * - * @param string $key Item name - * @param string $value Item value - */ - public function offsetSet($key, $value) { - if ($key === null) { - throw new Requests_Exception('Object is a dictionary, not a list', 'invalidset'); - } - - $this->cookies[$key] = $value; - } - - /** - * Unset the given header - * - * @param string $key - */ - public function offsetUnset($key) { - unset($this->cookies[$key]); - } - - /** - * Get an iterator for the data - * - * @return ArrayIterator - */ - public function getIterator() { - return new ArrayIterator($this->cookies); - } - - /** - * Register the cookie handler with the request's hooking system - * - * @param Requests_Hooker $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')); - } - - /** - * Add Cookie header to a request if we have any - * - * As per RFC 6265, cookies are separated by '; ' - * - * @param string $url - * @param array $headers - * @param array $data - * @param string $type - * @param array $options - */ - public function before_request($url, &$headers, &$data, &$type, &$options) { - if (!$url instanceof Requests_IRI) { - $url = new Requests_IRI($url); - } - - if (!empty($this->cookies)) { - $cookies = array(); - foreach ($this->cookies as $key => $cookie) { - $cookie = $this->normalize_cookie($cookie, $key); - - // Skip expired cookies - if ($cookie->is_expired()) { - continue; - } - - if ($cookie->domain_matches($url->host)) { - $cookies[] = $cookie->format_for_header(); - } - } - - $headers['Cookie'] = implode('; ', $cookies); - } - } - - /** - * Parse all cookies from a response and attach them to the response - * - * @var Requests_Response $response - */ - public function before_redirect_check(Requests_Response $return) { - $url = $return->url; - if (!$url instanceof Requests_IRI) { - $url = new Requests_IRI($url); - } - - $cookies = Requests_Cookie::parse_from_headers($return->headers, $url); - $this->cookies = array_merge($this->cookies, $cookies); - $return->cookies = $this; - } -} diff --git a/wp-includes/Requests/Exception/HTTP/429.php b/wp-includes/Requests/Exception/HTTP/429.php deleted file mode 100644 index 78a3148adf..0000000000 --- a/wp-includes/Requests/Exception/HTTP/429.php +++ /dev/null @@ -1,29 +0,0 @@ -0 is executed later - */ - public function register($hook, $callback, $priority = 0) { - if (!isset($this->hooks[$hook])) { - $this->hooks[$hook] = array(); - } - if (!isset($this->hooks[$hook][$priority])) { - $this->hooks[$hook][$priority] = array(); - } - - $this->hooks[$hook][$priority][] = $callback; - } - - /** - * Dispatch a message - * - * @param string $hook Hook name - * @param array $parameters Parameters to pass to callbacks - * @return boolean Successfulness - */ - public function dispatch($hook, $parameters = array()) { - if (empty($this->hooks[$hook])) { - return false; - } - - foreach ($this->hooks[$hook] as $priority => $hooked) { - foreach ($hooked as $callback) { - call_user_func_array($callback, $parameters); - } - } - - return true; - } -} diff --git a/wp-includes/Requests/Proxy.php b/wp-includes/Requests/Proxy.php deleted file mode 100644 index 0a798dbe9e..0000000000 --- a/wp-includes/Requests/Proxy.php +++ /dev/null @@ -1,35 +0,0 @@ -headers = new Requests_Response_Headers(); - $this->cookies = new Requests_Cookie_Jar(); - } - - /** - * Response body - * - * @var string - */ - public $body = ''; - - /** - * Raw HTTP data from the transport - * - * @var string - */ - public $raw = ''; - - /** - * Headers, as an associative array - * - * @var Requests_Response_Headers Array-like object representing headers - */ - public $headers = array(); - - /** - * Status code, false if non-blocking - * - * @var integer|boolean - */ - public $status_code = false; - - /** - * Protocol version, false if non-blocking - * - * @var float|boolean - */ - public $protocol_version = false; - - /** - * Whether the request succeeded or not - * - * @var boolean - */ - public $success = false; - - /** - * Number of redirects the request used - * - * @var integer - */ - public $redirects = 0; - - /** - * URL requested - * - * @var string - */ - public $url = ''; - - /** - * Previous requests (from redirects) - * - * @var array Array of Requests_Response objects - */ - public $history = array(); - - /** - * Cookies from the request - * - * @var Requests_Cookie_Jar Array-like object representing a cookie jar - */ - public $cookies = array(); - - /** - * Is the response a redirect? - * - * @return boolean True if redirect (3xx status), false if not. - */ - public function is_redirect() { - $code = $this->status_code; - return in_array($code, array(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 - */ - 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); - } - } - elseif (!$this->success) { - $exception = Requests_Exception_HTTP::get_class($this->status_code); - throw new $exception(null, $this); - } - } -} diff --git a/wp-includes/Requests/Response/Headers.php b/wp-includes/Requests/Response/Headers.php deleted file mode 100644 index 12db128ae5..0000000000 --- a/wp-includes/Requests/Response/Headers.php +++ /dev/null @@ -1,98 +0,0 @@ -data[$key])) { - return null; - } - - return $this->flatten($this->data[$key]); - } - - /** - * Set the given item - * - * @throws Requests_Exception On attempting to use dictionary as list (`invalidset`) - * - * @param string $key Item name - * @param string $value Item value - */ - public function offsetSet($key, $value) { - if ($key === null) { - throw new Requests_Exception('Object is a dictionary, not a list', 'invalidset'); - } - - $key = strtolower($key); - - if (!isset($this->data[$key])) { - $this->data[$key] = array(); - } - - $this->data[$key][] = $value; - } - - /** - * Get all values for a given header - * - * @param string $key - * @return array|null Header values - */ - public function getValues($key) { - $key = strtolower($key); - if (!isset($this->data[$key])) { - return null; - } - - return $this->data[$key]; - } - - /** - * Flattens a value into a string - * - * Converts an array into a string by imploding values with a comma, as per - * RFC2616's rules for folding headers. - * - * @param string|array $value Value to flatten - * @return string Flattened value - */ - public function flatten($value) { - if (is_array($value)) { - $value = implode(',', $value); - } - - return $value; - } - - /** - * Get an iterator for the data - * - * Converts the internal - * @return ArrayIterator - */ - public function getIterator() { - return new Requests_Utility_FilteredIterator($this->data, array($this, 'flatten')); - } -} diff --git a/wp-includes/Requests/Transport.php b/wp-includes/Requests/Transport.php deleted file mode 100644 index 5146c01d87..0000000000 --- a/wp-includes/Requests/Transport.php +++ /dev/null @@ -1,41 +0,0 @@ - $value) { - $this->offsetSet($key, $value); - } - } - - /** - * Check if the given item exists - * - * @param string $key Item key - * @return boolean Does the item exist? - */ - public function offsetExists($key) { - $key = strtolower($key); - return isset($this->data[$key]); - } - - /** - * Get the value for the item - * - * @param string $key Item key - * @return string|null Item value (null if offsetExists is false) - */ - public function offsetGet($key) { - $key = strtolower($key); - if (!isset($this->data[$key])) { - return null; - } - - return $this->data[$key]; - } - - /** - * Set the given item - * - * @throws Requests_Exception On attempting to use dictionary as list (`invalidset`) - * - * @param string $key Item name - * @param string $value Item value - */ - public function offsetSet($key, $value) { - if ($key === null) { - throw new Requests_Exception('Object is a dictionary, not a list', 'invalidset'); - } - - $key = strtolower($key); - $this->data[$key] = $value; - } - - /** - * Unset the given header - * - * @param string $key - */ - public function offsetUnset($key) { - unset($this->data[strtolower($key)]); - } - - /** - * Get an iterator for the data - * - * @return ArrayIterator - */ - public function getIterator() { - return new ArrayIterator($this->data); - } - - /** - * Get the headers as an array - * - * @return array Header data - */ - public function getAll() { - return $this->data; - } -} diff --git a/wp-includes/Requests/Utility/FilteredIterator.php b/wp-includes/Requests/Utility/FilteredIterator.php deleted file mode 100644 index 7c6c2c0238..0000000000 --- a/wp-includes/Requests/Utility/FilteredIterator.php +++ /dev/null @@ -1,65 +0,0 @@ -callback = $callback; - } - - /** - * Get the current item's value after filtering - * - * @return string - */ - public function current() { - $value = parent::current(); - - if (is_callable($this->callback)) { - $value = call_user_func($this->callback, $value); - } - - return $value; - } - - /** - * @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); - } -} diff --git a/wp-includes/Requests/src/Auth.php b/wp-includes/Requests/src/Auth.php new file mode 100644 index 0000000000..63ebb08333 --- /dev/null +++ b/wp-includes/Requests/src/Auth.php @@ -0,0 +1,36 @@ +user, $this->pass) = $args; + return; + } + + if ($args !== null) { + throw InvalidArgument::create(1, '$args', 'array|null', gettype($args)); + } + } + + /** + * Register the necessary callbacks + * + * @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(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|\CurlHandle $handle cURL handle + */ + public function curl_before_send(&$handle) { + curl_setopt($handle, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + curl_setopt($handle, CURLOPT_USERPWD, $this->getAuthString()); + } + + /** + * Add extra headers to the request before sending + * + * @param string $out HTTP header string + */ + public function fsockopen_header(&$out) { + $out .= sprintf("Authorization: Basic %s\r\n", base64_encode($this->getAuthString())); + } + + /** + * Get the authentication string (user:pass) + * + * @return string + */ + public function getAuthString() { + return $this->user . ':' . $this->pass; + } +} diff --git a/wp-includes/Requests/src/Autoload.php b/wp-includes/Requests/src/Autoload.php new file mode 100644 index 0000000000..26dd280ee8 --- /dev/null +++ b/wp-includes/Requests/src/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/wp-includes/Requests/src/Capability.php b/wp-includes/Requests/src/Capability.php new file mode 100644 index 0000000000..87b8340a34 --- /dev/null +++ b/wp-includes/Requests/src/Capability.php @@ -0,0 +1,36 @@ + + */ + const ALL = [ + self::SSL, + ]; +} diff --git a/wp-includes/Requests/Cookie.php b/wp-includes/Requests/src/Cookie.php similarity index 68% rename from wp-includes/Requests/Cookie.php rename to wp-includes/Requests/src/Cookie.php index 7dd5b6822d..ccbbc73dbc 100644 --- a/wp-includes/Requests/Cookie.php +++ b/wp-includes/Requests/src/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 @@ class Requests_Cookie { $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 @@ class Requests_Cookie { /** * 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 @@ class Requests_Cookie { /** * 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 @@ class Requests_Cookie { 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 @@ class Requests_Cookie { return true; } + if (is_scalar($request_path) === false) { + return false; + } + $cookie_path = $this->attributes['path']; if ($cookie_path === $request_path) { @@ -280,8 +331,7 @@ class Requests_Cookie { $delta_seconds = (int) $value; if ($delta_seconds <= 0) { $expiry_time = 0; - } - else { + } else { $expiry_time = $this->reference_time + $delta_seconds; } @@ -316,17 +366,6 @@ class Requests_Cookie { 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,42 +377,22 @@ class Requests_Cookie { 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; - } - /** * Parse a cookie string into a cookie object * @@ -381,17 +400,29 @@ class Requests_Cookie { * 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 @@ class Requests_Cookie { // 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 @@ class Requests_Cookie { } } - 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 @@ class Requests_Cookie { 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 @@ class Requests_Cookie { // 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 @@ class Requests_Cookie { 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/wp-includes/Requests/src/Cookie/Jar.php b/wp-includes/Requests/src/Cookie/Jar.php new file mode 100644 index 0000000000..dfbb8b739b --- /dev/null +++ b/wp-includes/Requests/src/Cookie/Jar.php @@ -0,0 +1,186 @@ +cookies = $cookies; + } + + /** + * Normalise cookie data into a \WpOrg\Requests\Cookie + * + * @param string|\WpOrg\Requests\Cookie $cookie + * @return \WpOrg\Requests\Cookie + */ + public function normalize_cookie($cookie, $key = '') { + if ($cookie instanceof Cookie) { + return $cookie; + } + + return Cookie::parse($cookie, $key); + } + + /** + * Check if the given item exists + * + * @param string $offset Item key + * @return boolean Does the item exist? + */ + #[ReturnTypeWillChange] + public function offsetExists($offset) { + return isset($this->cookies[$offset]); + } + + /** + * Get the value for the item + * + * @param string $offset Item key + * @return string|null Item value (null if offsetExists is false) + */ + #[ReturnTypeWillChange] + public function offsetGet($offset) { + if (!isset($this->cookies[$offset])) { + return null; + } + + return $this->cookies[$offset]; + } + + /** + * Set the given item + * + * @param string $offset Item name + * @param string $value Item value + * + * @throws \WpOrg\Requests\Exception On attempting to use dictionary as list (`invalidset`) + */ + #[ReturnTypeWillChange] + public function offsetSet($offset, $value) { + if ($offset === null) { + throw new Exception('Object is a dictionary, not a list', 'invalidset'); + } + + $this->cookies[$offset] = $value; + } + + /** + * Unset the given header + * + * @param string $offset + */ + #[ReturnTypeWillChange] + public function offsetUnset($offset) { + unset($this->cookies[$offset]); + } + + /** + * Get an iterator for the data + * + * @return \ArrayIterator + */ + #[ReturnTypeWillChange] + public function getIterator() { + return new ArrayIterator($this->cookies); + } + + /** + * Register the cookie handler with the request's hooking system + * + * @param \WpOrg\Requests\HookManager $hooks Hooking system + */ + public function register(HookManager $hooks) { + $hooks->register('requests.before_request', [$this, 'before_request']); + $hooks->register('requests.before_redirect_check', [$this, 'before_redirect_check']); + } + + /** + * Add Cookie header to a request if we have any + * + * As per RFC 6265, cookies are separated by '; ' + * + * @param string $url + * @param array $headers + * @param array $data + * @param string $type + * @param array $options + */ + public function before_request($url, &$headers, &$data, &$type, &$options) { + if (!$url instanceof Iri) { + $url = new Iri($url); + } + + if (!empty($this->cookies)) { + $cookies = []; + foreach ($this->cookies as $key => $cookie) { + $cookie = $this->normalize_cookie($cookie, $key); + + // Skip expired cookies + if ($cookie->is_expired()) { + continue; + } + + if ($cookie->domain_matches($url->host)) { + $cookies[] = $cookie->format_for_header(); + } + } + + $headers['Cookie'] = implode('; ', $cookies); + } + } + + /** + * Parse all cookies from a response and attach them to the response + * + * @param \WpOrg\Requests\Response $response + */ + public function before_redirect_check(Response $response) { + $url = $response->url; + if (!$url instanceof Iri) { + $url = new Iri($url); + } + + $cookies = Cookie::parse_from_headers($response->headers, $url); + $this->cookies = array_merge($this->cookies, $cookies); + $response->cookies = $this; + } +} diff --git a/wp-includes/Requests/Exception.php b/wp-includes/Requests/src/Exception.php similarity index 80% rename from wp-includes/Requests/Exception.php rename to wp-includes/Requests/src/Exception.php index 1080efd971..b67d1b1a40 100644 --- a/wp-includes/Requests/Exception.php +++ b/wp-includes/Requests/src/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 @@ class Requests_Exception extends Exception { } /** - * Like {@see getCode()}, but a string code. + * Like {@see \Exception::getCode()}, but a string code. * * @codeCoverageIgnore * @return string diff --git a/wp-includes/Requests/src/Exception/ArgumentCount.php b/wp-includes/Requests/src/Exception/ArgumentCount.php new file mode 100644 index 0000000000..b5773ddf33 --- /dev/null +++ b/wp-includes/Requests/src/Exception/ArgumentCount.php @@ -0,0 +1,47 @@ +reason; @@ -58,14 +65,14 @@ class Requests_Exception_HTTP extends Requests_Exception { */ 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/wp-includes/Requests/Exception/HTTP/304.php b/wp-includes/Requests/src/Exception/Http/Status304.php similarity index 61% rename from wp-includes/Requests/Exception/HTTP/304.php rename to wp-includes/Requests/src/Exception/Http/Status304.php index 2636ba4517..d510ae7d4e 100644 --- a/wp-includes/Requests/Exception/HTTP/304.php +++ b/wp-includes/Requests/src/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/wp-includes/Requests/Exception/HTTP/305.php b/wp-includes/Requests/src/Exception/Http/Status305.php similarity index 60% rename from wp-includes/Requests/Exception/HTTP/305.php rename to wp-includes/Requests/src/Exception/Http/Status305.php index 37d115a81a..8be63c3f21 100644 --- a/wp-includes/Requests/Exception/HTTP/305.php +++ b/wp-includes/Requests/src/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/wp-includes/Requests/Exception/HTTP/306.php b/wp-includes/Requests/src/Exception/Http/Status306.php similarity index 61% rename from wp-includes/Requests/Exception/HTTP/306.php rename to wp-includes/Requests/src/Exception/Http/Status306.php index 743a4ed1d0..2f3534ab9d 100644 --- a/wp-includes/Requests/Exception/HTTP/306.php +++ b/wp-includes/Requests/src/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/wp-includes/Requests/Exception/HTTP/400.php b/wp-includes/Requests/src/Exception/Http/Status400.php similarity index 60% rename from wp-includes/Requests/Exception/HTTP/400.php rename to wp-includes/Requests/src/Exception/Http/Status400.php index 5bd5428c0f..4e39623578 100644 --- a/wp-includes/Requests/Exception/HTTP/400.php +++ b/wp-includes/Requests/src/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/wp-includes/Requests/Exception/HTTP/401.php b/wp-includes/Requests/src/Exception/Http/Status401.php similarity index 61% rename from wp-includes/Requests/Exception/HTTP/401.php rename to wp-includes/Requests/src/Exception/Http/Status401.php index 62a283ab09..2a76429ce7 100644 --- a/wp-includes/Requests/Exception/HTTP/401.php +++ b/wp-includes/Requests/src/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/wp-includes/Requests/Exception/HTTP/402.php b/wp-includes/Requests/src/Exception/Http/Status402.php similarity index 62% rename from wp-includes/Requests/Exception/HTTP/402.php rename to wp-includes/Requests/src/Exception/Http/Status402.php index f287fd4b22..09d42879d5 100644 --- a/wp-includes/Requests/Exception/HTTP/402.php +++ b/wp-includes/Requests/src/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/wp-includes/Requests/Exception/HTTP/403.php b/wp-includes/Requests/src/Exception/Http/Status403.php similarity index 60% rename from wp-includes/Requests/Exception/HTTP/403.php rename to wp-includes/Requests/src/Exception/Http/Status403.php index 2ae1c44459..0b1fc3963d 100644 --- a/wp-includes/Requests/Exception/HTTP/403.php +++ b/wp-includes/Requests/src/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/wp-includes/Requests/Exception/HTTP/404.php b/wp-includes/Requests/src/Exception/Http/Status404.php similarity index 60% rename from wp-includes/Requests/Exception/HTTP/404.php rename to wp-includes/Requests/src/Exception/Http/Status404.php index e6e28672c3..ef39a853e3 100644 --- a/wp-includes/Requests/Exception/HTTP/404.php +++ b/wp-includes/Requests/src/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/wp-includes/Requests/Exception/HTTP/405.php b/wp-includes/Requests/src/Exception/Http/Status405.php similarity index 62% rename from wp-includes/Requests/Exception/HTTP/405.php rename to wp-includes/Requests/src/Exception/Http/Status405.php index 0461e6108c..666fbfe7c7 100644 --- a/wp-includes/Requests/Exception/HTTP/405.php +++ b/wp-includes/Requests/src/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/wp-includes/Requests/Exception/HTTP/406.php b/wp-includes/Requests/src/Exception/Http/Status406.php similarity index 61% rename from wp-includes/Requests/Exception/HTTP/406.php rename to wp-includes/Requests/src/Exception/Http/Status406.php index 980ef0efc5..37952f80a2 100644 --- a/wp-includes/Requests/Exception/HTTP/406.php +++ b/wp-includes/Requests/src/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/wp-includes/Requests/Exception/HTTP/407.php b/wp-includes/Requests/src/Exception/Http/Status407.php similarity index 64% rename from wp-includes/Requests/Exception/HTTP/407.php rename to wp-includes/Requests/src/Exception/Http/Status407.php index d08c6af7cc..d8796f9b54 100644 --- a/wp-includes/Requests/Exception/HTTP/407.php +++ b/wp-includes/Requests/src/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/wp-includes/Requests/Exception/HTTP/408.php b/wp-includes/Requests/src/Exception/Http/Status408.php similarity index 61% rename from wp-includes/Requests/Exception/HTTP/408.php rename to wp-includes/Requests/src/Exception/Http/Status408.php index db15bc2910..6718d064a1 100644 --- a/wp-includes/Requests/Exception/HTTP/408.php +++ b/wp-includes/Requests/src/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/wp-includes/Requests/Exception/HTTP/409.php b/wp-includes/Requests/src/Exception/Http/Status409.php similarity index 60% rename from wp-includes/Requests/Exception/HTTP/409.php rename to wp-includes/Requests/src/Exception/Http/Status409.php index 83002f9361..711a5c43f7 100644 --- a/wp-includes/Requests/Exception/HTTP/409.php +++ b/wp-includes/Requests/src/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/wp-includes/Requests/Exception/HTTP/410.php b/wp-includes/Requests/src/Exception/Http/Status410.php similarity index 58% rename from wp-includes/Requests/Exception/HTTP/410.php rename to wp-includes/Requests/src/Exception/Http/Status410.php index 5bf7aa6d46..127443b781 100644 --- a/wp-includes/Requests/Exception/HTTP/410.php +++ b/wp-includes/Requests/src/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/wp-includes/Requests/Exception/HTTP/411.php b/wp-includes/Requests/src/Exception/Http/Status411.php similarity index 61% rename from wp-includes/Requests/Exception/HTTP/411.php rename to wp-includes/Requests/src/Exception/Http/Status411.php index 25517b4f36..e70e63c649 100644 --- a/wp-includes/Requests/Exception/HTTP/411.php +++ b/wp-includes/Requests/src/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/wp-includes/Requests/Exception/HTTP/412.php b/wp-includes/Requests/src/Exception/Http/Status412.php similarity index 62% rename from wp-includes/Requests/Exception/HTTP/412.php rename to wp-includes/Requests/src/Exception/Http/Status412.php index e89533a7ef..4a8b9185b2 100644 --- a/wp-includes/Requests/Exception/HTTP/412.php +++ b/wp-includes/Requests/src/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/wp-includes/Requests/Exception/HTTP/413.php b/wp-includes/Requests/src/Exception/Http/Status413.php similarity index 63% rename from wp-includes/Requests/Exception/HTTP/413.php rename to wp-includes/Requests/src/Exception/Http/Status413.php index a7b38fce11..96a96fb163 100644 --- a/wp-includes/Requests/Exception/HTTP/413.php +++ b/wp-includes/Requests/src/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/wp-includes/Requests/Exception/HTTP/414.php b/wp-includes/Requests/src/Exception/Http/Status414.php similarity index 63% rename from wp-includes/Requests/Exception/HTTP/414.php rename to wp-includes/Requests/src/Exception/Http/Status414.php index 54c8b8c9ae..b65ec937af 100644 --- a/wp-includes/Requests/Exception/HTTP/414.php +++ b/wp-includes/Requests/src/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/wp-includes/Requests/Exception/HTTP/415.php b/wp-includes/Requests/src/Exception/Http/Status415.php similarity index 63% rename from wp-includes/Requests/Exception/HTTP/415.php rename to wp-includes/Requests/src/Exception/Http/Status415.php index 6b5f0785b8..cb45655e97 100644 --- a/wp-includes/Requests/Exception/HTTP/415.php +++ b/wp-includes/Requests/src/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/wp-includes/Requests/Exception/HTTP/416.php b/wp-includes/Requests/src/Exception/Http/Status416.php similarity index 65% rename from wp-includes/Requests/Exception/HTTP/416.php rename to wp-includes/Requests/src/Exception/Http/Status416.php index 48a4ecdb8f..c3661a193c 100644 --- a/wp-includes/Requests/Exception/HTTP/416.php +++ b/wp-includes/Requests/src/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/wp-includes/Requests/Exception/HTTP/417.php b/wp-includes/Requests/src/Exception/Http/Status417.php similarity index 62% rename from wp-includes/Requests/Exception/HTTP/417.php rename to wp-includes/Requests/src/Exception/Http/Status417.php index 81570330aa..4adba9a23e 100644 --- a/wp-includes/Requests/Exception/HTTP/417.php +++ b/wp-includes/Requests/src/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/wp-includes/Requests/Exception/HTTP/418.php b/wp-includes/Requests/src/Exception/Http/Status418.php similarity index 50% rename from wp-includes/Requests/Exception/HTTP/418.php rename to wp-includes/Requests/src/Exception/Http/Status418.php index 0fcc87d614..5bcb2b84a9 100644 --- a/wp-includes/Requests/Exception/HTTP/418.php +++ b/wp-includes/Requests/src/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/wp-includes/Requests/Exception/HTTP/428.php b/wp-includes/Requests/src/Exception/Http/Status428.php similarity index 52% rename from wp-includes/Requests/Exception/HTTP/428.php rename to wp-includes/Requests/src/Exception/Http/Status428.php index 799ddffd54..4d7ea51f5c 100644 --- a/wp-includes/Requests/Exception/HTTP/428.php +++ b/wp-includes/Requests/src/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/wp-includes/Requests/src/Exception/Http/Status429.php b/wp-includes/Requests/src/Exception/Http/Status429.php new file mode 100644 index 0000000000..2018196c7b --- /dev/null +++ b/wp-includes/Requests/src/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/wp-includes/Requests/src/Exception/InvalidArgument.php b/wp-includes/Requests/src/Exception/InvalidArgument.php new file mode 100644 index 0000000000..0ab7332f4f --- /dev/null +++ b/wp-includes/Requests/src/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 @@ class Requests_Exception_Transport_cURL extends Requests_Exception_Transport { } /** - * Get the error message + * Get the error message. + * + * @return string */ public function getReason() { return $this->reason; diff --git a/wp-includes/Requests/Hooker.php b/wp-includes/Requests/src/HookManager.php similarity index 66% rename from wp-includes/Requests/Hooker.php rename to wp-includes/Requests/src/HookManager.php index c852931f1a..f2920170b9 100644 --- a/wp-includes/Requests/Hooker.php +++ b/wp-includes/Requests/src/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 @@ interface Requests_Hooker { * @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/wp-includes/Requests/src/Hooks.php b/wp-includes/Requests/src/Hooks.php new file mode 100644 index 0000000000..74fba0b3e1 --- /dev/null +++ b/wp-includes/Requests/src/Hooks.php @@ -0,0 +1,99 @@ +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 (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])) { + $this->hooks[$hook] = [ + $priority => [], + ]; + } elseif (!isset($this->hooks[$hook][$priority])) { + $this->hooks[$hook][$priority] = []; + } + + $this->hooks[$hook][$priority][] = $callback; + } + + /** + * Dispatch a message + * + * @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 = []) { + 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) { + $callback(...$parameters); + } + } + + return true; + } +} diff --git a/wp-includes/Requests/IDNAEncoder.php b/wp-includes/Requests/src/IdnaEncoder.php similarity index 60% rename from wp-includes/Requests/IDNAEncoder.php rename to wp-includes/Requests/src/IdnaEncoder.php index 881142935b..094fff3d52 100644 --- a/wp-includes/Requests/IDNAEncoder.php +++ b/wp-includes/Requests/src/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 @@ class Requests_IDNAEncoder { || $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 @@ class Requests_IDNAEncoder { * 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 @@ class Requests_IDNAEncoder { $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 @@ class Requests_IDNAEncoder { // 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 @@ class Requests_IDNAEncoder { 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 @@ class Requests_IDNAEncoder { // 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 @@ class Requests_IDNAEncoder { // 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 @@ class Requests_IDNAEncoder { /** * 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 @@ class Requests_IDNAEncoder { /** * 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 @@ class Requests_IDNAEncoder { // 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/wp-includes/Requests/IPv6.php b/wp-includes/Requests/src/Ipv6.php similarity index 79% rename from wp-includes/Requests/IPv6.php rename to wp-includes/Requests/src/Ipv6.php index ba88786be6..a90ab8a831 100644 --- a/wp-includes/Requests/IPv6.php +++ b/wp-includes/Requests/src/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 @@ class Requests_IPv6 { 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 @@ class Requests_IPv6 { * 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 @@ class Requests_IPv6 { if ($ip_parts[1] !== '') { return implode(':', $ip_parts); - } - else { + } else { return $ip_parts[0]; } } @@ -124,15 +136,14 @@ class Requests_IPv6 { * @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 @@ class Requests_IPv6 { * @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 @@ class Requests_IPv6 { return false; } } + if (count($ipv4) === 4) { foreach ($ipv4 as $ipv4_part) { $value = (int) $ipv4_part; @@ -181,9 +194,9 @@ class Requests_IPv6 { } } } + return true; - } - else { + } else { return false; } } diff --git a/wp-includes/Requests/IRI.php b/wp-includes/Requests/src/Iri.php similarity index 91% rename from wp-includes/Requests/IRI.php rename to wp-includes/Requests/src/Iri.php index 5d80e49572..244578d344 100644 --- a/wp-includes/Requests/IRI.php +++ b/wp-includes/Requests/src/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 @@ class Requests_IRI { /** * 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 @@ class Requests_IRI { * * 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 @@ class Requests_IRI { 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 @@ class Requests_IRI { $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 @@ class Requests_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 @@ class Requests_IRI { /** * 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 @@ class Requests_IRI { // 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 @@ class Requests_IRI { 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 @@ class Requests_IRI { } 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 @@ class Requests_IRI { } } - return $string; + return $text; } /** @@ -539,13 +551,13 @@ class Requests_IRI { * 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 @@ class Requests_IRI { if ($iri === null) { return true; } + + $iri = (string) $iri; + if (isset($cache[$iri])) { list($this->scheme, $this->iuserinfo, @@ -733,7 +748,7 @@ class Requests_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 @@ class Requests_IRI { 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 @@ class Requests_IRI { /** * 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 @@ class Requests_IRI { } $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/wp-includes/Requests/src/Port.php b/wp-includes/Requests/src/Port.php new file mode 100644 index 0000000000..5545409385 --- /dev/null +++ b/wp-includes/Requests/src/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 @@ class Requests_Proxy_HTTP implements Requests_Proxy { * 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 @@ class Requests_Proxy_HTTP implements Requests_Proxy { * 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/wp-includes/Requests/src/Requests.php b/wp-includes/Requests/src/Requests.php new file mode 100644 index 0000000000..a8d9d7e539 --- /dev/null +++ b/wp-includes/Requests/src/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/wp-includes/Requests/src/Response.php b/wp-includes/Requests/src/Response.php new file mode 100644 index 0000000000..8964521a81 --- /dev/null +++ b/wp-includes/Requests/src/Response.php @@ -0,0 +1,165 @@ +headers = new Headers(); + $this->cookies = new Jar(); + } + + /** + * Is the response a redirect? + * + * @return boolean True if redirect (3xx status), false if not. + */ + public function is_redirect() { + $code = $this->status_code; + return in_array($code, [300, 301, 302, 303, 307], true) || $code > 307 && $code < 400; + } + + /** + * Throws an exception if the request was not successful + * + * @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 !== true) { + throw new Exception('Redirection not allowed', 'response.no_redirects', $this); + } + } 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/wp-includes/Requests/src/Response/Headers.php b/wp-includes/Requests/src/Response/Headers.php new file mode 100644 index 0000000000..eb4f68736b --- /dev/null +++ b/wp-includes/Requests/src/Response/Headers.php @@ -0,0 +1,124 @@ +data[$offset])) { + return null; + } + + return $this->flatten($this->data[$offset]); + } + + /** + * Set the given item + * + * @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($offset, $value) { + if ($offset === null) { + throw new Exception('Object is a dictionary, not a list', 'invalidset'); + } + + if (is_string($offset)) { + $offset = strtolower($offset); + } + + if (!isset($this->data[$offset])) { + $this->data[$offset] = []; + } + + $this->data[$offset][] = $value; + } + + /** + * Get all values for a given header + * + * @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($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[$offset]; + } + + /** + * Flattens a value into a string + * + * Converts an array into a string by imploding values with a comma, as per + * RFC2616's rules for folding headers. + * + * @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)) { + return implode(',', $value); + } + + throw InvalidArgument::create(1, '$value', 'string|array', gettype($value)); + } + + /** + * Get an iterator for the data + * + * 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 FilteredIterator($this->data, [$this, 'flatten']); + } +} diff --git a/wp-includes/Requests/Session.php b/wp-includes/Requests/src/Session.php similarity index 51% rename from wp-includes/Requests/Session.php rename to wp-includes/Requests/src/Session.php index b2e10991d6..000d2526d4 100644 --- a/wp-includes/Requests/Session.php +++ b/wp-includes/Requests/src/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 @@ class Requests_Session { /** * 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 @@ class Requests_Session { * 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 @@ class Requests_Session { /** * 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 @@ class Requests_Session { /** * 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/wp-includes/Requests/SSL.php b/wp-includes/Requests/src/Ssl.php similarity index 55% rename from wp-includes/Requests/SSL.php rename to wp-includes/Requests/src/Ssl.php index f7ecf3fbdb..99da11d8f7 100644 --- a/wp-includes/Requests/SSL.php +++ b/wp-includes/Requests/src/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 @@ class Requests_SSL { 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 @@ class Requests_SSL { * 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 @@ class Requests_SSL { /** * 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/wp-includes/Requests/src/Transport.php b/wp-includes/Requests/src/Transport.php new file mode 100644 index 0000000000..f2e1c6ed73 --- /dev/null +++ b/wp-includes/Requests/src/Transport.php @@ -0,0 +1,45 @@ + $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 = []); +} diff --git a/wp-includes/Requests/Transport/cURL.php b/wp-includes/Requests/src/Transport/Curl.php similarity index 69% rename from wp-includes/Requests/Transport/cURL.php rename to wp-includes/Requests/src/Transport/Curl.php index 01bcf3b719..8b0a13080e 100644 --- a/wp-includes/Requests/Transport/cURL.php +++ b/wp-includes/Requests/src/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 @@ class Requests_Transport_cURL implements Requests_Transport { 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 @@ class Requests_Transport_cURL implements Requests_Transport { /** * 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 @@ class Requests_Transport_cURL implements Requests_Transport { 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 @@ class Requests_Transport_cURL implements Requests_Transport { 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 @@ class Requests_Transport_cURL implements Requests_Transport { $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 @@ class Requests_Transport_cURL implements Requests_Transport { * * @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 @@ class Requests_Transport_cURL implements Requests_Transport { 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 @@ class Requests_Transport_cURL implements Requests_Transport { * @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 @@ class Requests_Transport_cURL implements Requests_Transport { if ($options['max_bytes'] !== false) { $this->response_byte_limit = $options['max_bytes']; } + $this->hooks = $options['hooks']; return $this->handle; @@ -307,10 +357,10 @@ class Requests_Transport_cURL implements Requests_Transport { * @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 @@ class Requests_Transport_cURL implements Requests_Transport { 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 @@ class Requests_Transport_cURL implements Requests_Transport { 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 @@ class Requests_Transport_cURL implements Requests_Transport { * @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 @@ class Requests_Transport_cURL implements Requests_Transport { 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 @@ class Requests_Transport_cURL implements Requests_Transport { $this->headers = ''; $this->done_headers = false; } + $this->headers .= $headers; if ($headers === "\r\n") { $this->done_headers = true; } + return strlen($headers); } @@ -476,12 +526,12 @@ class Requests_Transport_cURL implements Requests_Transport { * * @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 @@ class Requests_Transport_cURL implements Requests_Transport { if ($this->stream_handle) { fwrite($this->stream_handle, $data); - } - else { + } else { $this->response_data .= $data; } @@ -513,46 +562,48 @@ class Requests_Transport_cURL implements Requests_Transport { * 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 @@ class Requests_Transport_cURL implements Requests_Transport { * @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/wp-includes/Requests/Transport/fsockopen.php b/wp-includes/Requests/src/Transport/Fsockopen.php similarity index 59% rename from wp-includes/Requests/Transport/fsockopen.php rename to wp-includes/Requests/src/Transport/Fsockopen.php index 56f94c99f2..c3bd4a63d5 100644 --- a/wp-includes/Requests/Transport/fsockopen.php +++ b/wp-includes/Requests/src/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 @@ class Requests_Transport_fsockopen implements Requests_Transport { $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 @@ class Requests_Transport_fsockopen implements Requests_Transport { $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 @@ class Requests_Transport_fsockopen implements Requests_Transport { 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 @@ class Requests_Transport_fsockopen implements Requests_Transport { if ($options['type'] !== Requests::TRACE) { if (is_array($data)) { $request_body = http_build_query($data, '', '&'); - } - else { + } else { $request_body = $data; } @@ -177,11 +209,13 @@ class Requests_Transport_fsockopen implements Requests_Transport { } 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 @@ class Requests_Transport_fsockopen implements Requests_Transport { $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 @@ class Requests_Transport_fsockopen implements Requests_Transport { $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 @@ class Requests_Transport_fsockopen implements Requests_Transport { $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 @@ class Requests_Transport_fsockopen implements Requests_Transport { // 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 @@ class Requests_Transport_fsockopen implements Requests_Transport { $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 @@ class Requests_Transport_fsockopen implements Requests_Transport { * * @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 @@ class Requests_Transport_fsockopen implements Requests_Transport { * 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 @@ class Requests_Transport_fsockopen implements Requests_Transport { $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 @@ class Requests_Transport_fsockopen implements Requests_Transport { * 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 @@ class Requests_Transport_fsockopen implements Requests_Transport { // 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/wp-includes/Requests/src/Utility/CaseInsensitiveDictionary.php b/wp-includes/Requests/src/Utility/CaseInsensitiveDictionary.php new file mode 100644 index 0000000000..3c24cebd4f --- /dev/null +++ b/wp-includes/Requests/src/Utility/CaseInsensitiveDictionary.php @@ -0,0 +1,127 @@ + $value) { + $this->offsetSet($offset, $value); + } + } + + /** + * Check if the given item exists + * + * @param string $offset Item key + * @return boolean Does the item exist? + */ + #[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 $offset Item key + * @return string|null Item value (null if the item key doesn't exist) + */ + #[ReturnTypeWillChange] + public function offsetGet($offset) { + if (is_string($offset)) { + $offset = strtolower($offset); + } + + if (!isset($this->data[$offset])) { + return null; + } + + return $this->data[$offset]; + } + + /** + * Set the given item + * + * @param string $offset Item name + * @param string $value Item value + * + * @throws \WpOrg\Requests\Exception On attempting to use dictionary as 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); + } + + $this->data[$offset] = $value; + } + + /** + * Unset the given header + * + * @param string $offset + */ + #[ReturnTypeWillChange] + public function offsetUnset($offset) { + if (is_string($offset)) { + $offset = strtolower($offset); + } + + unset($this->data[$offset]); + } + + /** + * Get an iterator for the data + * + * @return \ArrayIterator + */ + #[ReturnTypeWillChange] + public function getIterator() { + return new ArrayIterator($this->data); + } + + /** + * Get the headers as an array + * + * @return array Header data + */ + public function getAll() { + return $this->data; + } +} diff --git a/wp-includes/Requests/src/Utility/FilteredIterator.php b/wp-includes/Requests/src/Utility/FilteredIterator.php new file mode 100644 index 0000000000..973a5d25a5 --- /dev/null +++ b/wp-includes/Requests/src/Utility/FilteredIterator.php @@ -0,0 +1,82 @@ +callback = $callback; + } + } + + /** + * @inheritdoc + * + * @phpcs:disable PHPCompatibility.FunctionNameRestrictions.NewMagicMethods.__unserializeFound + */ + #[ReturnTypeWillChange] + public function __unserialize($data) {} + // phpcs:enable + + public function __wakeup() { + unset($this->callback); + } + + /** + * Get the current item's value after filtering + * + * @return string + */ + #[ReturnTypeWillChange] + public function current() { + $value = parent::current(); + + if (is_callable($this->callback)) { + $value = call_user_func($this->callback, $value); + } + + return $value; + } + + /** + * @inheritdoc + */ + #[ReturnTypeWillChange] + public function unserialize($data) {} +} diff --git a/wp-includes/Requests/src/Utility/InputValidator.php b/wp-includes/Requests/src/Utility/InputValidator.php new file mode 100644 index 0000000000..7c10d61a4b --- /dev/null +++ b/wp-includes/Requests/src/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 - * - * @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; + require_once __DIR__ . '/Requests/src/Autoload.php'; + WpOrg\Requests\Autoload::register(); } } diff --git a/wp-includes/class-wp-http-requests-hooks.php b/wp-includes/class-wp-http-requests-hooks.php index dac64dd6eb..80e4eb7ef4 100644 --- a/wp-includes/class-wp-http-requests-hooks.php +++ b/wp-includes/class-wp-http-requests-hooks.php @@ -12,10 +12,10 @@ * * @since 4.7.0 * - * @see Requests_Hooks + * @see WpOrg\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/wp-includes/class-wp-http-requests-response.php b/wp-includes/class-wp-http-requests-response.php index 45f83e2c7a..821077656c 100644 --- a/wp-includes/class-wp-http-requests-response.php +++ b/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,10 +36,10 @@ 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( Requests_Response $response, $filename = '' ) { + public function __construct( WpOrg\Requests\Response $response, $filename = '' ) { $this->response = $response; $this->filename = $filename; } @@ -49,7 +49,7 @@ class WP_HTTP_Requests_Response extends WP_HTTP_Response { * * @since 4.6.0 * - * @return Requests_Response HTTP response. + * @return WpOrg\Requests\Response HTTP response. */ public function get_response_object() { return $this->response; @@ -60,11 +60,11 @@ class WP_HTTP_Requests_Response extends WP_HTTP_Response { * * @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. - $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 @@ class WP_HTTP_Requests_Response extends WP_HTTP_Response { * @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/wp-includes/class-wp-http.php b/wp-includes/class-wp-http.php index 3d90085e49..d364cf4cd5 100644 --- a/wp-includes/class-wp-http.php +++ b/wp-includes/class-wp-http.php @@ -7,11 +7,11 @@ * @since 2.7.0 */ -if ( ! class_exists( 'Requests' ) ) { - require ABSPATH . WPINC . '/class-requests.php'; +if ( ! class_exists( 'WpOrg\Requests\Autoload' ) ) { + require ABSPATH . WPINC . '/Requests/src/Autoload.php'; - Requests::register_autoloader(); - Requests::set_certificate_path( ABSPATH . WPINC . '/certificates/ca-bundle.crt' ); + WpOrg\Requests\Autoload::register(); + WpOrg\Requests\Requests::set_certificate_path( ABSPATH . WPINC . '/certificates/ca-bundle.crt' ); } /** @@ -275,14 +275,14 @@ class WP_Http { 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; } @@ -299,7 +299,7 @@ class WP_Http { 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; } } @@ -347,7 +347,7 @@ class WP_Http { $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'] ); } @@ -379,7 +379,7 @@ class WP_Http { // 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; @@ -392,7 +392,7 @@ class WP_Http { 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'] ); @@ -400,7 +400,7 @@ class WP_Http { // 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() ); } @@ -417,7 +417,7 @@ class WP_Http { * @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; } @@ -453,10 +453,10 @@ class WP_Http { * @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 Requests_Cookie_Jar(); + $cookie_jar = new WpOrg\Requests\Cookie\Jar(); foreach ( $cookies as $name => $value ) { if ( $value instanceof WP_Http_Cookie ) { @@ -466,9 +466,9 @@ class WP_Http { 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, $value ); + $cookie_jar[ $name ] = new WpOrg\Requests\Cookie( $name, (string) $value ); } } @@ -484,16 +484,16 @@ class WP_Http { * * @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. if ( 302 === $original->status_code ) { - $options['type'] = Requests::GET; + $options['type'] = WpOrg\Requests\Requests::GET; } } @@ -502,12 +502,12 @@ class WP_Http { * * @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 ) { 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/wp-includes/deprecated.php b/wp-includes/deprecated.php index 26be922a77..62ff08b534 100644 --- a/wp-includes/deprecated.php +++ b/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/wp-includes/functions.php b/wp-includes/functions.php index b9ed7dc2ec..620ff23ed7 100644 --- a/wp-includes/functions.php +++ b/wp-includes/functions.php @@ -976,7 +976,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/wp-includes/http.php b/wp-includes/http.php index f77d9a69a7..a8468ca17c 100644 --- a/wp-includes/http.php +++ b/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/wp-includes/rest-api.php b/wp-includes/rest-api.php index 64ceac73df..9898501cba 100644 --- a/wp-includes/rest-api.php +++ b/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; } diff --git a/wp-includes/version.php b/wp-includes/version.php index 0aaad16cd1..5dc3a81274 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -16,7 +16,7 @@ * * @global string $wp_version */ -$wp_version = '6.2-alpha-54996'; +$wp_version = '6.2-alpha-54997'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.