Skip to content

Commit

Permalink
Merge pull request #492 from WordPress/feature/251-add-requests-has-c…
Browse files Browse the repository at this point in the history
…apability
  • Loading branch information
schlessera authored Nov 10, 2021
2 parents ffcd411 + 0038704 commit 5f09e38
Show file tree
Hide file tree
Showing 11 changed files with 172 additions and 30 deletions.
10 changes: 10 additions & 0 deletions docs/usage-advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ Alternatively, if you want to disable verification completely, this is possible
with `'verify' => false`, but note that this is extremely insecure and should be
avoided.

Note that SSL verification might not be available depending on what extensions
are enabled for your PHP environment. You can test whether Requests has
access to a transport with SSL capabilities with the following call:

```php
use WpOrg\Requests\Capability;

$ssl_available = WpOrg\Requests\Requests::test(array(Capability::SSL => true));
```

### Security Note
Requests supports SSL across both cURL and fsockopen in a transparent manner.
Unlike other PHP HTTP libraries, support for verifying the certificate name is
Expand Down
36 changes: 36 additions & 0 deletions src/Capability.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php
/**
* Capability interface declaring the known capabilities.
*
* @package Requests\Utilities
*/

namespace WpOrg\Requests;

/**
* Capability interface declaring the known capabilities.
*
* This is used as the authoritative source for which capabilities can be queried.
*
* @package Requests\Utilities
*/
interface Capability {

/**
* Support for SSL.
*
* @var string
*/
const SSL = 'ssl';

/**
* Collection of all capabilities supported in Requests.
*
* Note: this does not automatically mean that the capability will be supported for your chosen transport!
*
* @var array<string>
*/
const ALL = array(
self::SSL,
);
}
62 changes: 47 additions & 15 deletions src/Requests.php
Original file line number Diff line number Diff line change
Expand Up @@ -224,49 +224,81 @@ public static function add_transport($transport) {
}

/**
* Get a working transport
* Get the fully qualified class name (FQCN) for a working transport.
*
* @throws \WpOrg\Requests\Exception If no valid transport is found (`notransport`)
* @return \WpOrg\Requests\Transport
* @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => 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($capabilities = array()) {
// Caching code, don't bother testing coverage
protected static function get_transport_class($capabilities = array()) {
// Caching code, don't bother testing coverage.
// @codeCoverageIgnoreStart
// array of capabilities as a string to be used as an array key
// 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]) && self::$transport[$cap_string] !== null) {
$class = self::$transport[$cap_string];
return new $class();
// 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
// Find us a working transport.
foreach (self::$transports as $class) {
if (!class_exists($class)) {
continue;
}

$result = call_user_func(array($class, 'test'), $capabilities);
if ($result) {
$result = $class::test($capabilities);
if ($result === true) {
self::$transport[$cap_string] = $class;
break;
}
}
if (self::$transport[$cap_string] === null) {

return self::$transport[$cap_string];
}

/**
* Get a working transport.
*
* @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`.
* @return \WpOrg\Requests\Transport
* @throws \WpOrg\Requests\Exception If no valid transport is found (`notransport`).
*/
protected static function get_transport($capabilities = array()) {
$class = self::get_transport_class($capabilities);

if ($class === '') {
throw new Exception('No working transports found', 'notransport', self::$transports);
}

$class = self::$transport[$cap_string];
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<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`.
* @return bool Whether the transport has the requested capabilities.
*/
public static function has_capabilities($capabilities = array()) {
return self::get_transport_class($capabilities) !== '';
}

/**#@+
* @see \WpOrg\Requests\Requests::request()
* @param string $url
Expand Down
10 changes: 7 additions & 3 deletions src/Transport.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,12 @@ public function request($url, $headers = array(), $data = array(), $options = ar
public function request_multiple($requests, $options);

/**
* Self-test whether the transport can be used
* @return bool
* Self-test whether the transport can be used.
*
* The available capabilities to test for can be found in {@see \WpOrg\Requests\Capability}.
*
* @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`.
* @return bool Whether the transport can be used.
*/
public static function test();
public static function test($capabilities = array());
}
10 changes: 7 additions & 3 deletions src/Transport/Curl.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

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;
Expand Down Expand Up @@ -563,18 +564,21 @@ private static function format_get($url, $data) {
}

/**
* 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<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`.
* @return bool Whether the transport can be used.
*/
public static function test($capabilities = array()) {
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;
Expand Down
10 changes: 7 additions & 3 deletions src/Transport/Fsockopen.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace WpOrg\Requests\Transport;

use WpOrg\Requests\Capability;
use WpOrg\Requests\Exception;
use WpOrg\Requests\Exception\InvalidArgument;
use WpOrg\Requests\Port;
Expand Down Expand Up @@ -448,18 +449,21 @@ public function verify_certificate_from_context($host, $context) {
}

/**
* 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<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`.
* @return bool Whether the transport can be used.
*/
public static function test($capabilities = array()) {
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;
}
Expand Down
2 changes: 1 addition & 1 deletion tests/Fixtures/RawTransportMock.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public function request_multiple($requests, $options) {

return $requests;
}
public static function test() {
public static function test($capabilities = array()) {
return true;
}
}
21 changes: 21 additions & 0 deletions tests/Fixtures/TestTransportMock.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace WpOrg\Requests\Tests\Fixtures;

use WpOrg\Requests\Transport;

final class TestTransportMock implements Transport {
public function request($url, $headers = array(), $data = array(), $options = array()) {
return '';
}
public function request_multiple($requests, $options) {
return array();
}
public static function test($capabilities = array()) {
// Time travel is not yet supported by this transport.
if (isset($capabilities['time-travel']) && $capabilities['time-travel']) {
return false;
}
return true;
}
}
2 changes: 1 addition & 1 deletion tests/Fixtures/TransportMock.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public function request_multiple($requests, $options) {
return $responses;
}

public static function test() {
public static function test($capabilities = array()) {
return true;
}
}
30 changes: 29 additions & 1 deletion tests/RequestsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

namespace WpOrg\Requests\Tests;

use ReflectionProperty;
use WpOrg\Requests\Capability;
use WpOrg\Requests\Exception;
use WpOrg\Requests\Requests;
use WpOrg\Requests\Response\Headers;
use WpOrg\Requests\Tests\Fixtures\RawTransportMock;
use WpOrg\Requests\Tests\Fixtures\TestTransportMock;
use WpOrg\Requests\Tests\Fixtures\TransportMock;
use WpOrg\Requests\Tests\TestCase;

final class RequestsTest extends TestCase {

Expand Down Expand Up @@ -169,4 +171,30 @@ public function testTimeoutException() {
$this->expectExceptionMessage('timed out');
Requests::get(httpbin('/delay/3'), array(), $options);
}

/**
* @covers \WpOrg\Requests\Requests::has_capabilities
*/
public function testHasCapabilitiesSucceedsForDetectingSsl() {
if (!extension_loaded('curl') && !extension_loaded('openssl')) {
$this->markTestSkipped('Testing for SSL requires either the curl or the openssl extension');
}
$this->assertTrue(Requests::has_capabilities(array(Capability::SSL => true)));
}

/**
* @covers \WpOrg\Requests\Requests::has_capabilities
*/
public function testHasCapabilitiesFailsForUnsupportedCapabilities() {
$transports = new ReflectionProperty(Requests::class, 'transports');
$transports->setAccessible(true);
$transports->setValue(array(TestTransportMock::class));

$result = Requests::has_capabilities(array('time-travel' => true));

$transports->setValue(array());
$transports->setAccessible(false);

$this->assertFalse($result);
}
}
9 changes: 6 additions & 3 deletions tests/Transport/BaseTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace WpOrg\Requests\Tests\Transport;

use stdClass;
use WpOrg\Requests\Capability;
use WpOrg\Requests\Exception;
use WpOrg\Requests\Exception\Http\StatusUnknown;
use WpOrg\Requests\Exception\InvalidArgument;
Expand All @@ -17,15 +18,17 @@ abstract class BaseTestCase extends TestCase {
protected $skip_https = false;

public function set_up() {
$callback = array($this->transport, 'test');
$supported = call_user_func($callback);
// Intermediary variable $test_method. This can be simplified (removed) once the minimum supported PHP version is 7.0 or higher.
$test_method = array($this->transport, 'test');

$supported = $test_method();

if (!$supported) {
$this->markTestSkipped($this->transport . ' is not available');
return;
}

$ssl_supported = call_user_func($callback, array('ssl' => true));
$ssl_supported = $test_method(array(Capability::SSL => true));
if (!$ssl_supported) {
$this->skip_https = true;
}
Expand Down

0 comments on commit 5f09e38

Please sign in to comment.