]> source.dussan.org Git - nextcloud-server.git/commitdiff
feat(HTTPClient): Provide wrapped access to Guzzle's asyncRequest() 38613/head
authorJoas Schilling <coding@schilljs.com>
Fri, 2 Jun 2023 14:05:35 +0000 (16:05 +0200)
committerJoas Schilling <coding@schilljs.com>
Tue, 27 Jun 2023 13:53:57 +0000 (15:53 +0200)
Signed-off-by: Joas Schilling <coding@schilljs.com>
lib/composer/composer/ClassLoader.php
lib/composer/composer/autoload_classmap.php
lib/composer/composer/autoload_static.php
lib/private/Http/Client/Client.php
lib/private/Http/Client/ClientService.php
lib/private/Http/Client/GuzzlePromiseAdapter.php [new file with mode: 0644]
lib/public/Http/Client/IClient.php
lib/public/Http/Client/IPromise.php [new file with mode: 0644]
tests/lib/Http/Client/ClientServiceTest.php
tests/lib/Http/Client/ClientTest.php

index a72151c77c8eb0c43635bfb2ac1ca2d834ac01a7..7824d8f7eafe8db890975f0fa2dfab31435900da 100644 (file)
@@ -45,35 +45,34 @@ class ClassLoader
     /** @var \Closure(string):void */
     private static $includeFile;
 
-    /** @var ?string */
+    /** @var string|null */
     private $vendorDir;
 
     // PSR-4
     /**
-     * @var array[]
-     * @psalm-var array<string, array<string, int>>
+     * @var array<string, array<string, int>>
      */
     private $prefixLengthsPsr4 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, array<int, string>>
+     * @var array<string, list<string>>
      */
     private $prefixDirsPsr4 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, string>
+     * @var list<string>
      */
     private $fallbackDirsPsr4 = array();
 
     // PSR-0
     /**
-     * @var array[]
-     * @psalm-var array<string, array<string, string[]>>
+     * List of PSR-0 prefixes
+     *
+     * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
+     *
+     * @var array<string, array<string, list<string>>>
      */
     private $prefixesPsr0 = array();
     /**
-     * @var array[]
-     * @psalm-var array<string, string>
+     * @var list<string>
      */
     private $fallbackDirsPsr0 = array();
 
@@ -81,8 +80,7 @@ class ClassLoader
     private $useIncludePath = false;
 
     /**
-     * @var string[]
-     * @psalm-var array<string, string>
+     * @var array<string, string>
      */
     private $classMap = array();
 
@@ -90,21 +88,20 @@ class ClassLoader
     private $classMapAuthoritative = false;
 
     /**
-     * @var bool[]
-     * @psalm-var array<string, bool>
+     * @var array<string, bool>
      */
     private $missingClasses = array();
 
-    /** @var ?string */
+    /** @var string|null */
     private $apcuPrefix;
 
     /**
-     * @var self[]
+     * @var array<string, self>
      */
     private static $registeredLoaders = array();
 
     /**
-     * @param ?string $vendorDir
+     * @param string|null $vendorDir
      */
     public function __construct($vendorDir = null)
     {
@@ -113,7 +110,7 @@ class ClassLoader
     }
 
     /**
-     * @return string[]
+     * @return array<string, list<string>>
      */
     public function getPrefixes()
     {
@@ -125,8 +122,7 @@ class ClassLoader
     }
 
     /**
-     * @return array[]
-     * @psalm-return array<string, array<int, string>>
+     * @return array<string, list<string>>
      */
     public function getPrefixesPsr4()
     {
@@ -134,8 +130,7 @@ class ClassLoader
     }
 
     /**
-     * @return array[]
-     * @psalm-return array<string, string>
+     * @return list<string>
      */
     public function getFallbackDirs()
     {
@@ -143,8 +138,7 @@ class ClassLoader
     }
 
     /**
-     * @return array[]
-     * @psalm-return array<string, string>
+     * @return list<string>
      */
     public function getFallbackDirsPsr4()
     {
@@ -152,8 +146,7 @@ class ClassLoader
     }
 
     /**
-     * @return string[] Array of classname => path
-     * @psalm-return array<string, string>
+     * @return array<string, string> Array of classname => path
      */
     public function getClassMap()
     {
@@ -161,8 +154,7 @@ class ClassLoader
     }
 
     /**
-     * @param string[] $classMap Class to filename map
-     * @psalm-param array<string, string> $classMap
+     * @param array<string, string> $classMap Class to filename map
      *
      * @return void
      */
@@ -179,24 +171,25 @@ class ClassLoader
      * Registers a set of PSR-0 directories for a given prefix, either
      * appending or prepending to the ones previously set for this prefix.
      *
-     * @param string          $prefix  The prefix
-     * @param string[]|string $paths   The PSR-0 root directories
-     * @param bool            $prepend Whether to prepend the directories
+     * @param string              $prefix  The prefix
+     * @param list<string>|string $paths   The PSR-0 root directories
+     * @param bool                $prepend Whether to prepend the directories
      *
      * @return void
      */
     public function add($prefix, $paths, $prepend = false)
     {
+        $paths = (array) $paths;
         if (!$prefix) {
             if ($prepend) {
                 $this->fallbackDirsPsr0 = array_merge(
-                    (array) $paths,
+                    $paths,
                     $this->fallbackDirsPsr0
                 );
             } else {
                 $this->fallbackDirsPsr0 = array_merge(
                     $this->fallbackDirsPsr0,
-                    (array) $paths
+                    $paths
                 );
             }
 
@@ -205,19 +198,19 @@ class ClassLoader
 
         $first = $prefix[0];
         if (!isset($this->prefixesPsr0[$first][$prefix])) {
-            $this->prefixesPsr0[$first][$prefix] = (array) $paths;
+            $this->prefixesPsr0[$first][$prefix] = $paths;
 
             return;
         }
         if ($prepend) {
             $this->prefixesPsr0[$first][$prefix] = array_merge(
-                (array) $paths,
+                $paths,
                 $this->prefixesPsr0[$first][$prefix]
             );
         } else {
             $this->prefixesPsr0[$first][$prefix] = array_merge(
                 $this->prefixesPsr0[$first][$prefix],
-                (array) $paths
+                $paths
             );
         }
     }
@@ -226,9 +219,9 @@ class ClassLoader
      * Registers a set of PSR-4 directories for a given namespace, either
      * appending or prepending to the ones previously set for this namespace.
      *
-     * @param string          $prefix  The prefix/namespace, with trailing '\\'
-     * @param string[]|string $paths   The PSR-4 base directories
-     * @param bool            $prepend Whether to prepend the directories
+     * @param string              $prefix  The prefix/namespace, with trailing '\\'
+     * @param list<string>|string $paths   The PSR-4 base directories
+     * @param bool                $prepend Whether to prepend the directories
      *
      * @throws \InvalidArgumentException
      *
@@ -236,17 +229,18 @@ class ClassLoader
      */
     public function addPsr4($prefix, $paths, $prepend = false)
     {
+        $paths = (array) $paths;
         if (!$prefix) {
             // Register directories for the root namespace.
             if ($prepend) {
                 $this->fallbackDirsPsr4 = array_merge(
-                    (array) $paths,
+                    $paths,
                     $this->fallbackDirsPsr4
                 );
             } else {
                 $this->fallbackDirsPsr4 = array_merge(
                     $this->fallbackDirsPsr4,
-                    (array) $paths
+                    $paths
                 );
             }
         } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
@@ -256,18 +250,18 @@ class ClassLoader
                 throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
             }
             $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
-            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+            $this->prefixDirsPsr4[$prefix] = $paths;
         } elseif ($prepend) {
             // Prepend directories for an already registered namespace.
             $this->prefixDirsPsr4[$prefix] = array_merge(
-                (array) $paths,
+                $paths,
                 $this->prefixDirsPsr4[$prefix]
             );
         } else {
             // Append directories for an already registered namespace.
             $this->prefixDirsPsr4[$prefix] = array_merge(
                 $this->prefixDirsPsr4[$prefix],
-                (array) $paths
+                $paths
             );
         }
     }
@@ -276,8 +270,8 @@ class ClassLoader
      * Registers a set of PSR-0 directories for a given prefix,
      * replacing any others previously set for this prefix.
      *
-     * @param string          $prefix The prefix
-     * @param string[]|string $paths  The PSR-0 base directories
+     * @param string              $prefix The prefix
+     * @param list<string>|string $paths  The PSR-0 base directories
      *
      * @return void
      */
@@ -294,8 +288,8 @@ class ClassLoader
      * Registers a set of PSR-4 directories for a given namespace,
      * replacing any others previously set for this namespace.
      *
-     * @param string          $prefix The prefix/namespace, with trailing '\\'
-     * @param string[]|string $paths  The PSR-4 base directories
+     * @param string              $prefix The prefix/namespace, with trailing '\\'
+     * @param list<string>|string $paths  The PSR-4 base directories
      *
      * @throws \InvalidArgumentException
      *
@@ -481,9 +475,9 @@ class ClassLoader
     }
 
     /**
-     * Returns the currently registered loaders indexed by their corresponding vendor directories.
+     * Returns the currently registered loaders keyed by their corresponding vendor directories.
      *
-     * @return self[]
+     * @return array<string, self>
      */
     public static function getRegisteredLoaders()
     {
index 0be1c83f83d3f74a9747e388b316804de3916dda..c4c8fdd75f4218ea6f206a9c14b614477ee56ff6 100644 (file)
@@ -430,6 +430,7 @@ return array(
     'OCP\\HintException' => $baseDir . '/lib/public/HintException.php',
     'OCP\\Http\\Client\\IClient' => $baseDir . '/lib/public/Http/Client/IClient.php',
     'OCP\\Http\\Client\\IClientService' => $baseDir . '/lib/public/Http/Client/IClientService.php',
+    'OCP\\Http\\Client\\IPromise' => $baseDir . '/lib/public/Http/Client/IPromise.php',
     'OCP\\Http\\Client\\IResponse' => $baseDir . '/lib/public/Http/Client/IResponse.php',
     'OCP\\Http\\Client\\LocalServerException' => $baseDir . '/lib/public/Http/Client/LocalServerException.php',
     'OCP\\Http\\WellKnown\\GenericResponse' => $baseDir . '/lib/public/Http/WellKnown/GenericResponse.php',
@@ -1336,6 +1337,7 @@ return array(
     'OC\\Http\\Client\\Client' => $baseDir . '/lib/private/Http/Client/Client.php',
     'OC\\Http\\Client\\ClientService' => $baseDir . '/lib/private/Http/Client/ClientService.php',
     'OC\\Http\\Client\\DnsPinMiddleware' => $baseDir . '/lib/private/Http/Client/DnsPinMiddleware.php',
+    'OC\\Http\\Client\\GuzzlePromiseAdapter' => $baseDir . '/lib/private/Http/Client/GuzzlePromiseAdapter.php',
     'OC\\Http\\Client\\NegativeDnsCache' => $baseDir . '/lib/private/Http/Client/NegativeDnsCache.php',
     'OC\\Http\\Client\\Response' => $baseDir . '/lib/private/Http/Client/Response.php',
     'OC\\Http\\CookieHelper' => $baseDir . '/lib/private/Http/CookieHelper.php',
index aecbb51bb559802bf34f852a36aa3f532d74002d..530869bb744b617612e05ecea542299309308be5 100644 (file)
@@ -463,6 +463,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
         'OCP\\HintException' => __DIR__ . '/../../..' . '/lib/public/HintException.php',
         'OCP\\Http\\Client\\IClient' => __DIR__ . '/../../..' . '/lib/public/Http/Client/IClient.php',
         'OCP\\Http\\Client\\IClientService' => __DIR__ . '/../../..' . '/lib/public/Http/Client/IClientService.php',
+        'OCP\\Http\\Client\\IPromise' => __DIR__ . '/../../..' . '/lib/public/Http/Client/IPromise.php',
         'OCP\\Http\\Client\\IResponse' => __DIR__ . '/../../..' . '/lib/public/Http/Client/IResponse.php',
         'OCP\\Http\\Client\\LocalServerException' => __DIR__ . '/../../..' . '/lib/public/Http/Client/LocalServerException.php',
         'OCP\\Http\\WellKnown\\GenericResponse' => __DIR__ . '/../../..' . '/lib/public/Http/WellKnown/GenericResponse.php',
@@ -1369,6 +1370,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
         'OC\\Http\\Client\\Client' => __DIR__ . '/../../..' . '/lib/private/Http/Client/Client.php',
         'OC\\Http\\Client\\ClientService' => __DIR__ . '/../../..' . '/lib/private/Http/Client/ClientService.php',
         'OC\\Http\\Client\\DnsPinMiddleware' => __DIR__ . '/../../..' . '/lib/private/Http/Client/DnsPinMiddleware.php',
+        'OC\\Http\\Client\\GuzzlePromiseAdapter' => __DIR__ . '/../../..' . '/lib/private/Http/Client/GuzzlePromiseAdapter.php',
         'OC\\Http\\Client\\NegativeDnsCache' => __DIR__ . '/../../..' . '/lib/private/Http/Client/NegativeDnsCache.php',
         'OC\\Http\\Client\\Response' => __DIR__ . '/../../..' . '/lib/private/Http/Client/Response.php',
         'OC\\Http\\CookieHelper' => __DIR__ . '/../../..' . '/lib/private/Http/CookieHelper.php',
index 298749d52e161b370b62cd48c3aa4e09e8757509..3bf43e6c07ec625fa49edacc47b6dfcc9c818895 100644 (file)
@@ -34,13 +34,16 @@ declare(strict_types=1);
 namespace OC\Http\Client;
 
 use GuzzleHttp\Client as GuzzleClient;
+use GuzzleHttp\Promise\PromiseInterface;
 use GuzzleHttp\RequestOptions;
 use OCP\Http\Client\IClient;
+use OCP\Http\Client\IPromise;
 use OCP\Http\Client\IResponse;
 use OCP\Http\Client\LocalServerException;
 use OCP\ICertificateManager;
 use OCP\IConfig;
 use OCP\Security\IRemoteHostValidator;
+use Psr\Log\LoggerInterface;
 use function parse_url;
 
 /**
@@ -61,7 +64,8 @@ class Client implements IClient {
                IConfig $config,
                ICertificateManager $certificateManager,
                GuzzleClient $client,
-               IRemoteHostValidator $remoteHostValidator
+               IRemoteHostValidator $remoteHostValidator,
+               protected LoggerInterface $logger,
        ) {
                $this->config = $config;
                $this->client = $client;
@@ -205,7 +209,7 @@ class Client implements IClient {
         *              'headers' => [
         *                  'foo' => 'bar',
         *              ],
-        *              'cookies' => ['
+        *              'cookies' => [
         *                  'foo' => 'bar',
         *              ],
         *              'allow_redirects' => [
@@ -236,7 +240,7 @@ class Client implements IClient {
         *              'headers' => [
         *                  'foo' => 'bar',
         *              ],
-        *              'cookies' => ['
+        *              'cookies' => [
         *                  'foo' => 'bar',
         *              ],
         *              'allow_redirects' => [
@@ -271,7 +275,7 @@ class Client implements IClient {
         *              'headers' => [
         *                  'foo' => 'bar',
         *              ],
-        *              'cookies' => ['
+        *              'cookies' => [
         *                  'foo' => 'bar',
         *              ],
         *              'allow_redirects' => [
@@ -312,7 +316,7 @@ class Client implements IClient {
         *              'headers' => [
         *                  'foo' => 'bar',
         *              ],
-        *              'cookies' => ['
+        *              'cookies' => [
         *                  'foo' => 'bar',
         *              ],
         *              'allow_redirects' => [
@@ -347,7 +351,7 @@ class Client implements IClient {
         *              'headers' => [
         *                  'foo' => 'bar',
         *              ],
-        *              'cookies' => ['
+        *              'cookies' => [
         *                  'foo' => 'bar',
         *              ],
         *              'allow_redirects' => [
@@ -370,7 +374,7 @@ class Client implements IClient {
        }
 
        /**
-        * Sends a options request
+        * Sends an OPTIONS request
         *
         * @param string $uri
         * @param array $options Array such as
@@ -382,7 +386,7 @@ class Client implements IClient {
         *              'headers' => [
         *                  'foo' => 'bar',
         *              ],
-        *              'cookies' => ['
+        *              'cookies' => [
         *                  'foo' => 'bar',
         *              ],
         *              'allow_redirects' => [
@@ -403,4 +407,215 @@ class Client implements IClient {
                $response = $this->client->request('options', $uri, $this->buildRequestOptions($options));
                return new Response($response);
        }
+
+       protected function wrapGuzzlePromise(PromiseInterface $promise): IPromise {
+               return new GuzzlePromiseAdapter(
+                       $promise,
+                       $this->logger
+               );
+       }
+
+       /**
+        * Sends an asynchronous GET request
+        *
+        * @param string $uri
+        * @param array $options Array such as
+        *              'query' => [
+        *                  'field' => 'abc',
+        *                  'other_field' => '123',
+        *                  'file_name' => fopen('/path/to/file', 'r'),
+        *              ],
+        *              'headers' => [
+        *                  'foo' => 'bar',
+        *              ],
+        *              'cookies' => [
+        *                  'foo' => 'bar',
+        *              ],
+        *              'allow_redirects' => [
+        *                   'max'       => 10,  // allow at most 10 redirects.
+        *                   'strict'    => true,     // use "strict" RFC compliant redirects.
+        *                   'referer'   => true,     // add a Referer header
+        *                   'protocols' => ['https'] // only allow https URLs
+        *              ],
+        *              'sink' => '/path/to/file', // save to a file or a stream
+        *              'verify' => true, // bool or string to CA file
+        *              'debug' => true,
+        *              'timeout' => 5,
+        * @return IPromise
+        */
+       public function getAsync(string $uri, array $options = []): IPromise {
+               $this->preventLocalAddress($uri, $options);
+               $response = $this->client->requestAsync('get', $uri, $this->buildRequestOptions($options));
+               return $this->wrapGuzzlePromise($response);
+       }
+
+       /**
+        * Sends an asynchronous HEAD request
+        *
+        * @param string $uri
+        * @param array $options Array such as
+        *              'headers' => [
+        *                  'foo' => 'bar',
+        *              ],
+        *              'cookies' => [
+        *                  'foo' => 'bar',
+        *              ],
+        *              'allow_redirects' => [
+        *                   'max'       => 10,  // allow at most 10 redirects.
+        *                   'strict'    => true,     // use "strict" RFC compliant redirects.
+        *                   'referer'   => true,     // add a Referer header
+        *                   'protocols' => ['https'] // only allow https URLs
+        *              ],
+        *              'sink' => '/path/to/file', // save to a file or a stream
+        *              'verify' => true, // bool or string to CA file
+        *              'debug' => true,
+        *              'timeout' => 5,
+        * @return IPromise
+        */
+       public function headAsync(string $uri, array $options = []): IPromise {
+               $this->preventLocalAddress($uri, $options);
+               $response = $this->client->requestAsync('head', $uri, $this->buildRequestOptions($options));
+               return $this->wrapGuzzlePromise($response);
+       }
+
+       /**
+        * Sends an asynchronous POST request
+        *
+        * @param string $uri
+        * @param array $options Array such as
+        *              'body' => [
+        *                  'field' => 'abc',
+        *                  'other_field' => '123',
+        *                  'file_name' => fopen('/path/to/file', 'r'),
+        *              ],
+        *              'headers' => [
+        *                  'foo' => 'bar',
+        *              ],
+        *              'cookies' => [
+        *                  'foo' => 'bar',
+        *              ],
+        *              'allow_redirects' => [
+        *                   'max'       => 10,  // allow at most 10 redirects.
+        *                   'strict'    => true,     // use "strict" RFC compliant redirects.
+        *                   'referer'   => true,     // add a Referer header
+        *                   'protocols' => ['https'] // only allow https URLs
+        *              ],
+        *              'sink' => '/path/to/file', // save to a file or a stream
+        *              'verify' => true, // bool or string to CA file
+        *              'debug' => true,
+        *              'timeout' => 5,
+        * @return IPromise
+        */
+       public function postAsync(string $uri, array $options = []): IPromise {
+               $this->preventLocalAddress($uri, $options);
+
+               if (isset($options['body']) && is_array($options['body'])) {
+                       $options['form_params'] = $options['body'];
+                       unset($options['body']);
+               }
+
+               return $this->wrapGuzzlePromise($this->client->requestAsync('post', $uri, $this->buildRequestOptions($options)));
+       }
+
+       /**
+        * Sends an asynchronous PUT request
+        *
+        * @param string $uri
+        * @param array $options Array such as
+        *              'body' => [
+        *                  'field' => 'abc',
+        *                  'other_field' => '123',
+        *                  'file_name' => fopen('/path/to/file', 'r'),
+        *              ],
+        *              'headers' => [
+        *                  'foo' => 'bar',
+        *              ],
+        *              'cookies' => [
+        *                  'foo' => 'bar',
+        *              ],
+        *              'allow_redirects' => [
+        *                   'max'       => 10,  // allow at most 10 redirects.
+        *                   'strict'    => true,     // use "strict" RFC compliant redirects.
+        *                   'referer'   => true,     // add a Referer header
+        *                   'protocols' => ['https'] // only allow https URLs
+        *              ],
+        *              'sink' => '/path/to/file', // save to a file or a stream
+        *              'verify' => true, // bool or string to CA file
+        *              'debug' => true,
+        *              'timeout' => 5,
+        * @return IPromise
+        */
+       public function putAsync(string $uri, array $options = []): IPromise {
+               $this->preventLocalAddress($uri, $options);
+               $response = $this->client->requestAsync('put', $uri, $this->buildRequestOptions($options));
+               return $this->wrapGuzzlePromise($response);
+       }
+
+       /**
+        * Sends an asynchronous DELETE request
+        *
+        * @param string $uri
+        * @param array $options Array such as
+        *              'body' => [
+        *                  'field' => 'abc',
+        *                  'other_field' => '123',
+        *                  'file_name' => fopen('/path/to/file', 'r'),
+        *              ],
+        *              'headers' => [
+        *                  'foo' => 'bar',
+        *              ],
+        *              'cookies' => [
+        *                  'foo' => 'bar',
+        *              ],
+        *              'allow_redirects' => [
+        *                   'max'       => 10,  // allow at most 10 redirects.
+        *                   'strict'    => true,     // use "strict" RFC compliant redirects.
+        *                   'referer'   => true,     // add a Referer header
+        *                   'protocols' => ['https'] // only allow https URLs
+        *              ],
+        *              'sink' => '/path/to/file', // save to a file or a stream
+        *              'verify' => true, // bool or string to CA file
+        *              'debug' => true,
+        *              'timeout' => 5,
+        * @return IPromise
+        */
+       public function deleteAsync(string $uri, array $options = []): IPromise {
+               $this->preventLocalAddress($uri, $options);
+               $response = $this->client->requestAsync('delete', $uri, $this->buildRequestOptions($options));
+               return $this->wrapGuzzlePromise($response);
+       }
+
+       /**
+        * Sends an asynchronous OPTIONS request
+        *
+        * @param string $uri
+        * @param array $options Array such as
+        *              'body' => [
+        *                  'field' => 'abc',
+        *                  'other_field' => '123',
+        *                  'file_name' => fopen('/path/to/file', 'r'),
+        *              ],
+        *              'headers' => [
+        *                  'foo' => 'bar',
+        *              ],
+        *              'cookies' => [
+        *                  'foo' => 'bar',
+        *              ],
+        *              'allow_redirects' => [
+        *                   'max'       => 10,  // allow at most 10 redirects.
+        *                   'strict'    => true,     // use "strict" RFC compliant redirects.
+        *                   'referer'   => true,     // add a Referer header
+        *                   'protocols' => ['https'] // only allow https URLs
+        *              ],
+        *              'sink' => '/path/to/file', // save to a file or a stream
+        *              'verify' => true, // bool or string to CA file
+        *              'debug' => true,
+        *              'timeout' => 5,
+        * @return IPromise
+        */
+       public function optionsAsync(string $uri, array $options = []): IPromise {
+               $this->preventLocalAddress($uri, $options);
+               $response = $this->client->requestAsync('options', $uri, $this->buildRequestOptions($options));
+               return $this->wrapGuzzlePromise($response);
+       }
 }
index d0640680124d0ab854fc9d13f0e9f0c730f747cd..532aa7f566ab5053410151355eb9c53f358d05a7 100644 (file)
@@ -37,6 +37,7 @@ use OCP\ICertificateManager;
 use OCP\IConfig;
 use OCP\Security\IRemoteHostValidator;
 use Psr\Http\Message\RequestInterface;
+use Psr\Log\LoggerInterface;
 
 /**
  * Class ClientService
@@ -59,6 +60,7 @@ class ClientService implements IClientService {
                DnsPinMiddleware $dnsPinMiddleware,
                IRemoteHostValidator $remoteHostValidator,
                IEventLogger $eventLogger,
+               protected LoggerInterface $logger,
        ) {
                $this->config = $config;
                $this->certificateManager = $certificateManager;
@@ -87,6 +89,7 @@ class ClientService implements IClientService {
                        $this->certificateManager,
                        $client,
                        $this->remoteHostValidator,
+                       $this->logger,
                );
        }
 }
diff --git a/lib/private/Http/Client/GuzzlePromiseAdapter.php b/lib/private/Http/Client/GuzzlePromiseAdapter.php
new file mode 100644 (file)
index 0000000..9d0b89b
--- /dev/null
@@ -0,0 +1,139 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023, Joas Schilling <coding@schilljs.com>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OC\Http\Client;
+
+use Exception;
+use GuzzleHttp\Exception\RequestException;
+use GuzzleHttp\Promise\PromiseInterface;
+use LogicException;
+use OCP\Http\Client\IPromise;
+use OCP\Http\Client\IResponse;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Log\LoggerInterface;
+
+/**
+ * A wrapper around Guzzle's PromiseInterface
+ *
+ * @see \GuzzleHttp\Promise\PromiseInterface
+ * @since 28.0.0
+ */
+class GuzzlePromiseAdapter implements IPromise {
+       public function __construct(
+               protected PromiseInterface $promise,
+               protected LoggerInterface $logger,
+       ) {
+       }
+
+       /**
+        * Appends fulfillment and rejection handlers to the promise, and returns
+        * a new promise resolving to the return value of the called handler.
+        *
+        * @param ?callable(IResponse): void $onFulfilled Invoked when the promise fulfills. Gets an \OCP\Http\Client\IResponse passed in as argument
+        * @param ?callable(Exception): void $onRejected  Invoked when the promise is rejected. Gets an \Exception passed in as argument
+        *
+        * @return IPromise
+        * @since 28.0.0
+        */
+       public function then(
+               ?callable $onFulfilled = null,
+               ?callable $onRejected = null,
+       ): IPromise {
+               if ($onFulfilled !== null) {
+                       $wrappedOnFulfilled = static function (ResponseInterface $response) use ($onFulfilled) {
+                               $onFulfilled(new Response($response));
+                       };
+               } else {
+                       $wrappedOnFulfilled = null;
+               }
+
+               if ($onRejected !== null) {
+                       $wrappedOnRejected = static function (RequestException $e) use ($onRejected) {
+                               $onRejected($e);
+                       };
+               } else {
+                       $wrappedOnRejected = null;
+               }
+
+               $this->promise->then($wrappedOnFulfilled, $wrappedOnRejected);
+               return $this;
+       }
+
+       /**
+        * Get the state of the promise ("pending", "rejected", or "fulfilled").
+        *
+        * The three states can be checked against the constants defined:
+        * STATE_PENDING, STATE_FULFILLED, and STATE_REJECTED.
+        *
+        * @return IPromise::STATE_*
+        * @since 28.0.0
+        */
+       public function getState(): string {
+               $state = $this->promise->getState();
+               if ($state === PromiseInterface::FULFILLED) {
+                       return self::STATE_FULFILLED;
+               }
+               if ($state === PromiseInterface::REJECTED) {
+                       return self::STATE_REJECTED;
+               }
+               if ($state === PromiseInterface::PENDING) {
+                       return self::STATE_PENDING;
+               }
+
+               $this->logger->error('Unexpected promise state "{state}" returned by Guzzle', [
+                       'state' => $state,
+               ]);
+               return self::STATE_PENDING;
+       }
+
+       /**
+        * Cancels the promise if possible.
+        *
+        * @link https://github.com/promises-aplus/cancellation-spec/issues/7
+        * @since 28.0.0
+        */
+       public function cancel(): void {
+               $this->promise->cancel();
+       }
+
+       /**
+        * Waits until the promise completes if possible.
+        *
+        * Pass $unwrap as true to unwrap the result of the promise, either
+        * returning the resolved value or throwing the rejected exception.
+        *
+        * If the promise cannot be waited on, then the promise will be rejected.
+        *
+        * @param bool $unwrap
+        *
+        * @return mixed
+        *
+        * @throws LogicException if the promise has no wait function or if the
+        *                         promise does not settle after waiting.
+        * @since 28.0.0
+        */
+       public function wait(bool $unwrap = true): mixed {
+               return $this->promise->wait($unwrap);
+       }
+}
index d692edc5b71d4a048398d2f2524ca2cf22a8ca7c..fb1760c25f2161aeb8a5252c48c4e3739a45e614 100644 (file)
@@ -44,7 +44,7 @@ interface IClient {
         *              'headers' => [
         *                  'foo' => 'bar',
         *              ],
-        *              'cookies' => ['
+        *              'cookies' => [
         *                  'foo' => 'bar',
         *              ],
         *              'allow_redirects' => [
@@ -69,7 +69,7 @@ interface IClient {
         *              'headers' => [
         *                  'foo' => 'bar',
         *              ],
-        *              'cookies' => ['
+        *              'cookies' => [
         *                  'foo' => 'bar',
         *              ],
         *              'allow_redirects' => [
@@ -99,7 +99,7 @@ interface IClient {
         *              'headers' => [
         *                  'foo' => 'bar',
         *              ],
-        *              'cookies' => ['
+        *              'cookies' => [
         *                  'foo' => 'bar',
         *              ],
         *              'allow_redirects' => [
@@ -129,7 +129,7 @@ interface IClient {
         *              'headers' => [
         *                  'foo' => 'bar',
         *              ],
-        *              'cookies' => ['
+        *              'cookies' => [
         *                  'foo' => 'bar',
         *              ],
         *              'allow_redirects' => [
@@ -159,7 +159,7 @@ interface IClient {
         *              'headers' => [
         *                  'foo' => 'bar',
         *              ],
-        *              'cookies' => ['
+        *              'cookies' => [
         *                  'foo' => 'bar',
         *              ],
         *              'allow_redirects' => [
@@ -178,7 +178,7 @@ interface IClient {
        public function delete(string $uri, array $options = []): IResponse;
 
        /**
-        * Sends a options request
+        * Sends an OPTIONS request
         * @param string $uri
         * @param array $options Array such as
         *              'body' => [
@@ -189,7 +189,7 @@ interface IClient {
         *              'headers' => [
         *                  'foo' => 'bar',
         *              ],
-        *              'cookies' => ['
+        *              'cookies' => [
         *                  'foo' => 'bar',
         *              ],
         *              'allow_redirects' => [
@@ -206,4 +206,173 @@ interface IClient {
         * @since 8.1.0
         */
        public function options(string $uri, array $options = []): IResponse;
+
+       /**
+        * Sends an asynchronous GET request
+        * @param string $uri
+        * @param array $options Array such as
+        *              'query' => [
+        *                  'field' => 'abc',
+        *                  'other_field' => '123',
+        *                  'file_name' => fopen('/path/to/file', 'r'),
+        *              ],
+        *              'headers' => [
+        *                  'foo' => 'bar',
+        *              ],
+        *              'cookies' => [
+        *                  'foo' => 'bar',
+        *              ],
+        *              'allow_redirects' => [
+        *                   'max'       => 10,  // allow at most 10 redirects.
+        *                   'strict'    => true,     // use "strict" RFC compliant redirects.
+        *                   'referer'   => true,     // add a Referer header
+        *                   'protocols' => ['https'] // only allow https URLs
+        *              ],
+        *              'sink' => '/path/to/file', // save to a file or a stream
+        *              'verify' => true, // bool or string to CA file
+        *              'debug' => true,
+        * @return IPromise
+        * @since 28.0.0
+        */
+       public function getAsync(string $uri, array $options = []): IPromise;
+
+       /**
+        * Sends an asynchronous HEAD request
+        * @param string $uri
+        * @param array $options Array such as
+        *              'headers' => [
+        *                  'foo' => 'bar',
+        *              ],
+        *              'cookies' => [
+        *                  'foo' => 'bar',
+        *              ],
+        *              'allow_redirects' => [
+        *                   'max'       => 10,  // allow at most 10 redirects.
+        *                   'strict'    => true,     // use "strict" RFC compliant redirects.
+        *                   'referer'   => true,     // add a Referer header
+        *                   'protocols' => ['https'] // only allow https URLs
+        *              ],
+        *              'sink' => '/path/to/file', // save to a file or a stream
+        *              'verify' => true, // bool or string to CA file
+        *              'debug' => true,
+        * @return IPromise
+        * @since 28.0.0
+        */
+       public function headAsync(string $uri, array $options = []): IPromise;
+
+       /**
+        * Sends an asynchronous POST request
+        * @param string $uri
+        * @param array $options Array such as
+        *              'body' => [
+        *                  'field' => 'abc',
+        *                  'other_field' => '123',
+        *                  'file_name' => fopen('/path/to/file', 'r'),
+        *              ],
+        *              'headers' => [
+        *                  'foo' => 'bar',
+        *              ],
+        *              'cookies' => [
+        *                  'foo' => 'bar',
+        *              ],
+        *              'allow_redirects' => [
+        *                   'max'       => 10,  // allow at most 10 redirects.
+        *                   'strict'    => true,     // use "strict" RFC compliant redirects.
+        *                   'referer'   => true,     // add a Referer header
+        *                   'protocols' => ['https'] // only allow https URLs
+        *              ],
+        *              'sink' => '/path/to/file', // save to a file or a stream
+        *              'verify' => true, // bool or string to CA file
+        *              'debug' => true,
+        * @return IPromise
+        * @since 28.0.0
+        */
+       public function postAsync(string $uri, array $options = []): IPromise;
+
+       /**
+        * Sends an asynchronous PUT request
+        * @param string $uri
+        * @param array $options Array such as
+        *              'body' => [
+        *                  'field' => 'abc',
+        *                  'other_field' => '123',
+        *                  'file_name' => fopen('/path/to/file', 'r'),
+        *              ],
+        *              'headers' => [
+        *                  'foo' => 'bar',
+        *              ],
+        *              'cookies' => [
+        *                  'foo' => 'bar',
+        *              ],
+        *              'allow_redirects' => [
+        *                   'max'       => 10,  // allow at most 10 redirects.
+        *                   'strict'    => true,     // use "strict" RFC compliant redirects.
+        *                   'referer'   => true,     // add a Referer header
+        *                   'protocols' => ['https'] // only allow https URLs
+        *              ],
+        *              'sink' => '/path/to/file', // save to a file or a stream
+        *              'verify' => true, // bool or string to CA file
+        *              'debug' => true,
+        * @return IPromise
+        * @since 28.0.0
+        */
+       public function putAsync(string $uri, array $options = []): IPromise;
+
+       /**
+        * Sends an asynchronous DELETE request
+        * @param string $uri
+        * @param array $options Array such as
+        *              'body' => [
+        *                  'field' => 'abc',
+        *                  'other_field' => '123',
+        *                  'file_name' => fopen('/path/to/file', 'r'),
+        *              ],
+        *              'headers' => [
+        *                  'foo' => 'bar',
+        *              ],
+        *              'cookies' => [
+        *                  'foo' => 'bar',
+        *              ],
+        *              'allow_redirects' => [
+        *                   'max'       => 10,  // allow at most 10 redirects.
+        *                   'strict'    => true,     // use "strict" RFC compliant redirects.
+        *                   'referer'   => true,     // add a Referer header
+        *                   'protocols' => ['https'] // only allow https URLs
+        *              ],
+        *              'sink' => '/path/to/file', // save to a file or a stream
+        *              'verify' => true, // bool or string to CA file
+        *              'debug' => true,
+        * @return IPromise
+        * @since 28.0.0
+        */
+       public function deleteAsync(string $uri, array $options = []): IPromise;
+
+       /**
+        * Sends an asynchronous OPTIONS request
+        * @param string $uri
+        * @param array $options Array such as
+        *              'body' => [
+        *                  'field' => 'abc',
+        *                  'other_field' => '123',
+        *                  'file_name' => fopen('/path/to/file', 'r'),
+        *              ],
+        *              'headers' => [
+        *                  'foo' => 'bar',
+        *              ],
+        *              'cookies' => [
+        *                  'foo' => 'bar',
+        *              ],
+        *              'allow_redirects' => [
+        *                   'max'       => 10,  // allow at most 10 redirects.
+        *                   'strict'    => true,     // use "strict" RFC compliant redirects.
+        *                   'referer'   => true,     // add a Referer header
+        *                   'protocols' => ['https'] // only allow https URLs
+        *              ],
+        *              'sink' => '/path/to/file', // save to a file or a stream
+        *              'verify' => true, // bool or string to CA file
+        *              'debug' => true,
+        * @return IPromise
+        * @since 28.0.0
+        */
+       public function optionsAsync(string $uri, array $options = []): IPromise;
 }
diff --git a/lib/public/Http/Client/IPromise.php b/lib/public/Http/Client/IPromise.php
new file mode 100644 (file)
index 0000000..29e186d
--- /dev/null
@@ -0,0 +1,100 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023, Joas Schilling <coding@schilljs.com>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OCP\Http\Client;
+
+use Exception;
+use LogicException;
+
+/**
+ * A wrapper around Guzzle's PromiseInterface
+ * @see \GuzzleHttp\Promise\PromiseInterface
+ * @since 28.0.0
+ */
+interface IPromise {
+       /**
+        * @since 28.0.0
+        */
+       public const STATE_PENDING = 'pending';
+       /**
+        * @since 28.0.0
+        */
+       public const STATE_FULFILLED = 'fulfilled';
+       /**
+        * @since 28.0.0
+        */
+       public const STATE_REJECTED = 'rejected';
+
+       /**
+        * Appends fulfillment and rejection handlers to the promise, and returns
+        * a new promise resolving to the return value of the called handler.
+        *
+        * @param ?callable(IResponse): void $onFulfilled Invoked when the promise fulfills. Gets an \OCP\Http\Client\IResponse passed in as argument
+        * @param ?callable(Exception): void $onRejected  Invoked when the promise is rejected. Gets an \Exception passed in as argument
+        *
+        * @return IPromise
+        * @since 28.0.0
+        */
+       public function then(
+               ?callable $onFulfilled = null,
+               ?callable $onRejected = null,
+       ): IPromise;
+
+       /**
+        * Get the state of the promise ("pending", "rejected", or "fulfilled").
+        *
+        * The three states can be checked against the constants defined:
+        * STATE_PENDING, STATE_FULFILLED, and STATE_REJECTED.
+        *
+        * @return self::STATE_*
+        * @since 28.0.0
+        */
+       public function getState(): string;
+
+       /**
+        * Cancels the promise if possible.
+        *
+        * @link https://github.com/promises-aplus/cancellation-spec/issues/7
+        * @since 28.0.0
+        */
+       public function cancel(): void;
+
+       /**
+        * Waits until the promise completes if possible.
+        *
+        * Pass $unwrap as true to unwrap the result of the promise, either
+        * returning the resolved value or throwing the rejected exception.
+        *
+        * If the promise cannot be waited on, then the promise will be rejected.
+        *
+        * @param bool $unwrap
+        *
+        * @return mixed
+        *
+        * @throws LogicException if the promise has no wait function or if the
+        *                         promise does not settle after waiting.
+        * @since 28.0.0
+        */
+       public function wait(bool $unwrap = true): mixed;
+}
index 72281a0453ce0900c3855401fa911f3e5a2cda5e..40da0a2111c77895a42bae755a4d914494f17d42 100644 (file)
@@ -23,6 +23,7 @@ use OCP\ICertificateManager;
 use OCP\IConfig;
 use OCP\Security\IRemoteHostValidator;
 use Psr\Http\Message\RequestInterface;
+use Psr\Log\LoggerInterface;
 
 /**
  * Class ClientServiceTest
@@ -41,13 +42,15 @@ class ClientServiceTest extends \Test\TestCase {
                        });
                $remoteHostValidator = $this->createMock(IRemoteHostValidator::class);
                $eventLogger = $this->createMock(IEventLogger::class);
+               $logger = $this->createMock(LoggerInterface::class);
 
                $clientService = new ClientService(
                        $config,
                        $certificateManager,
                        $dnsPinMiddleware,
                        $remoteHostValidator,
-                       $eventLogger
+                       $eventLogger,
+                       $logger,
                );
 
                $handler = new CurlHandler();
@@ -65,7 +68,8 @@ class ClientServiceTest extends \Test\TestCase {
                                $config,
                                $certificateManager,
                                $guzzleClient,
-                               $remoteHostValidator
+                               $remoteHostValidator,
+                               $logger,
                        ),
                        $clientService->newClient()
                );
index 9a4fb1c657e0f3b5c9bb925f74d426840dc6a315..e48e237e0cc7a1176e52068fb3154c923bc8e027 100644 (file)
@@ -19,6 +19,7 @@ use OCP\ICertificateManager;
 use OCP\IConfig;
 use OCP\Security\IRemoteHostValidator;
 use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
 use function parse_url;
 
 /**
@@ -44,11 +45,13 @@ class ClientTest extends \Test\TestCase {
                $this->guzzleClient = $this->createMock(\GuzzleHttp\Client::class);
                $this->certificateManager = $this->createMock(ICertificateManager::class);
                $this->remoteHostValidator = $this->createMock(IRemoteHostValidator::class);
+               $this->logger = $this->createMock(LoggerInterface::class);
                $this->client = new Client(
                        $this->config,
                        $this->certificateManager,
                        $this->guzzleClient,
-                       $this->remoteHostValidator
+                       $this->remoteHostValidator,
+                       $this->logger,
                );
        }