From cf7beb95b2fecdc39a39caf9aacaa9d12dbb3786 Mon Sep 17 00:00:00 2001 From: Andrew Zhdanovskih Date: Wed, 23 Jun 2021 08:38:40 +0500 Subject: [PATCH 01/18] Test github-action run --- .github/workflows/phpunit.yml | 21 +++++++++++++++++++++ .travis.yml | 24 ------------------------ README.md | 5 ++--- 3 files changed, 23 insertions(+), 27 deletions(-) create mode 100644 .github/workflows/phpunit.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml new file mode 100644 index 0000000..068382d --- /dev/null +++ b/.github/workflows/phpunit.yml @@ -0,0 +1,21 @@ +name: CI-coverage + +on: [ push ] + +jobs: + test-php-7_1: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + - uses: php-actions/composer@v5 + - name: PHPUnit Tests + uses: php-actions/phpunit@v2 + with: + version: 7 + php_version: 7.1 + php_extensions: xdebug + bootstrap: vendor/autoload.php + configuration: phpunit.xml + args: --coverage-text + env: + XDEBUG_MODE: coverage diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 1b0a128..0000000 --- a/.travis.yml +++ /dev/null @@ -1,24 +0,0 @@ -# http://php.net/supported-versions.php - -language: php -os: linux -dist: xenial - -jobs: - include: - - - php: 7.1 - - php: 7.4 - - php: 8.0 - - fast_finish: false - -before_script: - - COMPOSER_MEMORY_LIMIT=-1 travis_retry composer install - -script: - php vendor/bin/phpunit --exclude-group local-only - -notifications: - email: - - devops@uploadcare.com diff --git a/README.md b/README.md index 1bc272e..8cd1aa8 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,9 @@ Uploadcare PHP integration handles uploads and further operations with files by wrapping Upload and REST APIs. -[![Build Status][travis-img]][travis] [![Uploadcare stack on StackShare][stack-img]][stack] +[![Uploadcare stack on StackShare][stack-img]][stack] [![Test workflow][action-img]] -[travis-img]: https://api.travis-ci.com/uploadcare/uploadcare-php.svg?branch=master -[travis]: https://travis-ci.com/uploadcare/uploadcare-php +[action-img]: https://github.com/uploadcare/uploadcare-php/actions/workflows/phpunit.yml/badge.svg [stack-img]: http://img.shields.io/badge/tech-stack-0690fa.svg?style=flat [stack]: https://stackshare.io/uploadcare/stacks/ From 3ef56bdcf888d4755f8e55858986d8d1e5a74abd Mon Sep 17 00:00:00 2001 From: Andrew Zhdanovskih Date: Wed, 23 Jun 2021 09:17:05 +0500 Subject: [PATCH 02/18] Add version to composer action --- .github/workflows/phpunit.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 068382d..518edd7 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -7,7 +7,11 @@ jobs: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - - uses: php-actions/composer@v5 + - uses: php-actions/composer@v6 + with: + php_version: 7.1 + dev: yes + interaction: no - name: PHPUnit Tests uses: php-actions/phpunit@v2 with: From 544825c9c3753b08e2b5d2e9d399986fa9385bf2 Mon Sep 17 00:00:00 2001 From: Andrew Zhdanovskih Date: Wed, 23 Jun 2021 09:20:09 +0500 Subject: [PATCH 03/18] Exclude local-only test group --- .github/workflows/phpunit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 518edd7..ac95a8e 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -20,6 +20,6 @@ jobs: php_extensions: xdebug bootstrap: vendor/autoload.php configuration: phpunit.xml - args: --coverage-text + args: --coverage-text --exclude-group local-only env: XDEBUG_MODE: coverage From c6e1d05fecb9e1693b01be1aa00d6889c9e2d841 Mon Sep 17 00:00:00 2001 From: Andrew Zhdanovskih Date: Wed, 23 Jun 2021 09:24:36 +0500 Subject: [PATCH 04/18] Add tests with php 7.4 --- .github/workflows/phpunit.yml | 22 +++++++++++++++++++++- README.md | 2 +- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index ac95a8e..8f8c4d3 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -12,7 +12,7 @@ jobs: php_version: 7.1 dev: yes interaction: no - - name: PHPUnit Tests + - name: PHPUnit Tests php 7.1 uses: php-actions/phpunit@v2 with: version: 7 @@ -23,3 +23,23 @@ jobs: args: --coverage-text --exclude-group local-only env: XDEBUG_MODE: coverage + test-php-7_4: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + - uses: php-actions/composer@v6 + with: + php_version: 7.4 + dev: yes + interaction: no + - name: PHPUnit Tests php 7.4 + uses: php-actions/phpunit@v2 + with: + version: 9 + php_version: 7.4 + php_extensions: xdebug + bootstrap: vendor/autoload.php + configuration: phpunit.xml + args: --coverage-text --exclude-group local-only + env: + XDEBUG_MODE: coverage diff --git a/README.md b/README.md index 8cd1aa8..a0c05d7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Uploadcare PHP integration handles uploads and further operations with files by wrapping Upload and REST APIs. -[![Uploadcare stack on StackShare][stack-img]][stack] [![Test workflow][action-img]] +[![Uploadcare stack on StackShare][stack-img]][stack] ![Test workflow][action-img] [action-img]: https://github.com/uploadcare/uploadcare-php/actions/workflows/phpunit.yml/badge.svg [stack-img]: http://img.shields.io/badge/tech-stack-0690fa.svg?style=flat From a55785d9ac8cef230a214513f706029af9b61989 Mon Sep 17 00:00:00 2001 From: Andrew Zhdanovskih Date: Wed, 23 Jun 2021 09:39:14 +0500 Subject: [PATCH 05/18] Experiment with version-matrix --- .github/workflows/phpunit.yml | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 8f8c4d3..1c274f9 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -5,38 +5,23 @@ on: [ push ] jobs: test-php-7_1: runs-on: ubuntu-20.04 + strategy: + fail-fast: true + matrix: + php-versions: [ "7.1", "7.4", "8.0" ] + phpunit-versions: [ "7", "9" ] steps: - uses: actions/checkout@v2 - uses: php-actions/composer@v6 with: - php_version: 7.1 + php_version: "${{ matrix.php-versions }}" dev: yes interaction: no - - name: PHPUnit Tests php 7.1 + - name: PHPUnit Tests php uses: php-actions/phpunit@v2 with: - version: 7 - php_version: 7.1 - php_extensions: xdebug - bootstrap: vendor/autoload.php - configuration: phpunit.xml - args: --coverage-text --exclude-group local-only - env: - XDEBUG_MODE: coverage - test-php-7_4: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - uses: php-actions/composer@v6 - with: - php_version: 7.4 - dev: yes - interaction: no - - name: PHPUnit Tests php 7.4 - uses: php-actions/phpunit@v2 - with: - version: 9 - php_version: 7.4 + version: "${{ matrix.phpunit-versions }}" + php_version: "${{ matrix.php-versions }}" php_extensions: xdebug bootstrap: vendor/autoload.php configuration: phpunit.xml From 7a68ca563168740f8e6f86c646250c4ecb0fa401 Mon Sep 17 00:00:00 2001 From: Andrew Zhdanovskih Date: Wed, 23 Jun 2021 09:47:34 +0500 Subject: [PATCH 06/18] Exclutions for version matrix --- .github/workflows/phpunit.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 1c274f9..7702efa 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -10,6 +10,13 @@ jobs: matrix: php-versions: [ "7.1", "7.4", "8.0" ] phpunit-versions: [ "7", "9" ] + exclude: + - php-versions: "7.1" + phpunit-versions: "9" + - php-versions: "7.4" + phpunit-versions: "7" + - php-versions: "8.0" + phpunit-versions: "7" steps: - uses: actions/checkout@v2 - uses: php-actions/composer@v6 From 4844813e550ade32dcaab05bce68efeb70ee95aa Mon Sep 17 00:00:00 2001 From: Andrew Zhdanovskih Date: Wed, 23 Jun 2021 09:51:53 +0500 Subject: [PATCH 07/18] Add Codecov step --- .github/workflows/phpunit.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 7702efa..d017115 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -3,7 +3,7 @@ name: CI-coverage on: [ push ] jobs: - test-php-7_1: + test-php: runs-on: ubuntu-20.04 strategy: fail-fast: true @@ -32,6 +32,10 @@ jobs: php_extensions: xdebug bootstrap: vendor/autoload.php configuration: phpunit.xml - args: --coverage-text --exclude-group local-only + args: --coverage-clover=coverage.xml --exclude-group local-only env: XDEBUG_MODE: coverage + - name: Upload to Codecov + uses: codecov/codecov-action@v1 + with: + file: ./coverage.xml From 416cebd344010729f3e6e63a0152891a19b4600d Mon Sep 17 00:00:00 2001 From: Andrew Zhdanovskih Date: Wed, 23 Jun 2021 15:25:06 +0500 Subject: [PATCH 08/18] Add Codecov key --- .github/workflows/phpunit.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index d017115..8b4f66b 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -38,4 +38,5 @@ jobs: - name: Upload to Codecov uses: codecov/codecov-action@v1 with: + token: ${{ secrets.CODECOV_TOKEN }} file: ./coverage.xml From dc5aafd4e93d8d1e01104687a830991cec30ef7c Mon Sep 17 00:00:00 2001 From: Andrew Zhdanovskih Date: Wed, 23 Jun 2021 15:51:27 +0500 Subject: [PATCH 09/18] Add Codecov badge to Readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a0c05d7..3e0251d 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,12 @@ Uploadcare PHP integration handles uploads and further operations with files by wrapping Upload and REST APIs. -[![Uploadcare stack on StackShare][stack-img]][stack] ![Test workflow][action-img] +[![Uploadcare stack on StackShare][stack-img]][stack] ![Test workflow][action-img] ![Code coverage][codecov-img] [action-img]: https://github.com/uploadcare/uploadcare-php/actions/workflows/phpunit.yml/badge.svg [stack-img]: http://img.shields.io/badge/tech-stack-0690fa.svg?style=flat [stack]: https://stackshare.io/uploadcare/stacks/ +[codecov-img]: https://codecov.io/gh/uploadcare/uploadcare-php/branch/master/graph/badge.svg * [Requirements](#requirements) * [Install](#install) From b24ada00a5a603fbd322ff56c6ce7d8b7c61367d Mon Sep 17 00:00:00 2001 From: Andrew Zhdanovskih Date: Fri, 2 Jul 2021 09:26:02 +0500 Subject: [PATCH 10/18] Extend exceptions for respect server status --- src/Exception/HttpException.php | 3 +- .../Upload/AbstractClientException.php | 19 +++++ src/Exception/Upload/AccountException.php | 7 ++ .../Upload/FileTooLargeException.php | 7 ++ .../Upload/RequestParametersException.php | 7 ++ src/Exception/Upload/ThrottledException.php | 37 +++++++++ src/Uploader/AbstractUploader.php | 41 ++++++++++ src/Uploader/Uploader.php | 20 +++-- tests/Api/MultipartUploadTest.php | 41 ++++------ tests/AuthUrl/GenerateSecureUrlTest.php | 3 +- tests/CertainHttpExceptionTest.php | 81 +++++++++++++++++++ tests/HttpExceptionTest.php | 6 +- 12 files changed, 229 insertions(+), 43 deletions(-) create mode 100644 src/Exception/Upload/AbstractClientException.php create mode 100644 src/Exception/Upload/AccountException.php create mode 100644 src/Exception/Upload/FileTooLargeException.php create mode 100644 src/Exception/Upload/RequestParametersException.php create mode 100644 src/Exception/Upload/ThrottledException.php create mode 100644 tests/CertainHttpExceptionTest.php diff --git a/src/Exception/HttpException.php b/src/Exception/HttpException.php index c3b0fc4..d6f14e4 100644 --- a/src/Exception/HttpException.php +++ b/src/Exception/HttpException.php @@ -10,6 +10,7 @@ public function __construct($message = '', $code = 0, \Exception $previous = nul { if ($previous !== null) { $message = $this->makeMessage($previous, $message); + $code = (int) $previous->getCode(); } parent::__construct($message, $code, $previous); @@ -21,7 +22,7 @@ public function __construct($message = '', $code = 0, \Exception $previous = nul * * @return string */ - protected function makeMessage(\Exception $exception, $message = ''): string + protected function makeMessage(\Exception $exception, string $message = ''): string { $messages = []; if (!empty($message)) { diff --git a/src/Exception/Upload/AbstractClientException.php b/src/Exception/Upload/AbstractClientException.php new file mode 100644 index 0000000..062e510 --- /dev/null +++ b/src/Exception/Upload/AbstractClientException.php @@ -0,0 +1,19 @@ +getCode(); + $message = ($response = $previous->getResponse()) !== null ? Message::toString($response) : 'Bad request'; + } + + parent::__construct($message, $code, $previous); + } +} diff --git a/src/Exception/Upload/AccountException.php b/src/Exception/Upload/AccountException.php new file mode 100644 index 0000000..589171e --- /dev/null +++ b/src/Exception/Upload/AccountException.php @@ -0,0 +1,7 @@ +getResponse(); + if ($response instanceof ResponseInterface) { + $retryHeader = $response->getHeader('Retry-After'); + + $this->retryAfter = $retryHeader[0] ?? 10; + } + } + } + + public function setRetryAfter(int $retryAfter): self + { + $this->retryAfter = $retryAfter; + + return $this; + } + + public function getRetryAfter(): int + { + return (int) $this->retryAfter; + } +} diff --git a/src/Uploader/AbstractUploader.php b/src/Uploader/AbstractUploader.php index 6ea8785..da3eb3a 100644 --- a/src/Uploader/AbstractUploader.php +++ b/src/Uploader/AbstractUploader.php @@ -2,11 +2,16 @@ namespace Uploadcare\Uploader; +use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Exception\GuzzleException; use Psr\Http\Message\ResponseInterface; use Uploadcare\Apis\FileApi; use Uploadcare\Exception\HttpException; use Uploadcare\Exception\InvalidArgumentException; +use Uploadcare\Exception\Upload\AccountException; +use Uploadcare\Exception\Upload\FileTooLargeException; +use Uploadcare\Exception\Upload\RequestParametersException; +use Uploadcare\Exception\Upload\ThrottledException; use Uploadcare\Interfaces\ConfigurationInterface; use Uploadcare\Interfaces\File\FileInfoInterface; use Uploadcare\Interfaces\UploaderInterface; @@ -335,4 +340,40 @@ protected function rewind($handle): void \rewind($handle); } } + + /** + * @param \Throwable $e + * + * @return \RuntimeException + */ + protected function handleException(\Throwable $e): \RuntimeException + { + if ($e instanceof ClientException) { + $response = $e->getResponse(); + if ($response === null) { + return new HttpException('', 0, $e); + } + switch ($response->getStatusCode()) { + case 400: + $throw = new RequestParametersException('', 0, $e); + break; + case 403: + $throw = new AccountException('', 0, $e); + break; + case 413: + $throw = new FileTooLargeException('', 0, $e); + break; + case 429: + $throw = new ThrottledException('', 0, $e); + break; + default: + $throw = new HttpException('', 0, $e); + break; + } + + return $throw; + } + + return new HttpException('', 0, ($e instanceof \Exception ? $e : null)); + } } diff --git a/src/Uploader/Uploader.php b/src/Uploader/Uploader.php index c5571ad..06bd3c9 100644 --- a/src/Uploader/Uploader.php +++ b/src/Uploader/Uploader.php @@ -1,10 +1,8 @@ -sendRequest('POST', 'base/', $parameters); - } catch (GuzzleException $e) { - throw new HttpException('', 0, ($e instanceof \Exception ? $e : null)); + } catch (\Throwable $e) { + throw $this->handleException($e); } if (\is_resource($handle)) { \fclose($handle); @@ -136,8 +134,8 @@ private function startUpload(int $fileSize, string $mimeType, string $filename, $response = $this->sendRequest('POST', 'multipart/start/', $parameters); $startData = $this->configuration->getSerializer() ->deserialize($response->getBody()->getContents(), MultipartStartResponse::class); - } catch (GuzzleException $e) { - throw new HttpException('', 0, ($e instanceof \Exception ? $e : null)); + } catch (\Throwable $e) { + throw $this->handleException($e); } if (!$startData instanceof MultipartStartResponse) { throw new \RuntimeException(\sprintf('Unable to get %s class from response. Call to support', MultipartStartResponse::class)); @@ -163,8 +161,8 @@ private function uploadParts(MultipartStartResponse $response, $handle): void try { $this->sendRequest('PUT', $signedUrl->getUrl(), ['body' => $part]); - } catch (GuzzleException $e) { - throw new HttpException(\sprintf('Upload to %s failed', $signedUrl->getUrl()), 0, ($e instanceof \Exception ? $e : null)); + } catch (\Throwable $e) { + throw $this->handleException($e); } } } @@ -183,8 +181,8 @@ private function finishUpload(MultipartStartResponse $response): ResponseInterfa try { return $this->sendRequest('POST', 'multipart/complete/', $this->makeMultipartParameters($data)); - } catch (GuzzleException $e) { - throw new HttpException('Unable to finish multipart-upload request', 0, ($e instanceof \Exception ? $e : null)); + } catch (\Throwable $e) { + throw $this->handleException($e); } } } diff --git a/tests/Api/MultipartUploadTest.php b/tests/Api/MultipartUploadTest.php index c7ee2af..0f26c57 100644 --- a/tests/Api/MultipartUploadTest.php +++ b/tests/Api/MultipartUploadTest.php @@ -6,17 +6,18 @@ use GuzzleHttp\Exception\TooManyRedirectsException; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; -use function GuzzleHttp\Psr7\stream_for; +use GuzzleHttp\Psr7\Utils; use PHPUnit\Framework\TestCase; use Tests\DataFile; use Uploadcare\Configuration; use Uploadcare\Exception\HttpException; +use Uploadcare\Exception\Upload\RequestParametersException; use Uploadcare\MultipartResponse\MultipartStartResponse; use Uploadcare\Uploader\Uploader; class MultipartUploadTest extends TestCase { - protected function getConfiguration() + protected function getConfiguration(): Configuration { return Configuration::create('public-key', 'private-key'); } @@ -26,7 +27,7 @@ protected function getConfiguration() * * @return \PHPUnit_Framework_MockObject_MockObject|Uploader */ - protected function getMockUploader($methods = []) + protected function getMockUploader(array $methods = []) { return $this->getMockBuilder(Uploader::class) ->setConstructorArgs([$this->getConfiguration()]) @@ -34,9 +35,9 @@ protected function getMockUploader($methods = []) ->getMock(); } - public function testStartUploadMethod() + public function testStartUploadMethod(): void { - $response = new Response(200, [], stream_for(DataFile::contents('startResponse.json'))); + $response = new Response(200, [], Utils::streamFor(DataFile::contents('startResponse.json'))); $uploader = $this->getMockUploader(['sendRequest']); $uploader ->expects(self::once()) @@ -50,13 +51,9 @@ public function testStartUploadMethod() self::assertInstanceOf(MultipartStartResponse::class, $result); } - public function testResponseExceptionInStartUpload() + public function testResponseExceptionInStartUpload(): void { - if (PHP_MAJOR_VERSION >= 7) { - $exception = new ClientException('Wrong request', new Request('POST', 'uri'), new Response(400)); - } else { - $exception = new ClientException('Wrong request', new Request('POST', 'uri')); - } + $exception = new ClientException('Wrong request', new Request('POST', 'uri'), new Response(400, [], 'Wrong request')); $uploader = $this->getMockUploader(['sendRequest']); $uploader @@ -67,18 +64,14 @@ public function testResponseExceptionInStartUpload() $startUpload = (new \ReflectionObject($uploader))->getMethod('startUpload'); $startUpload->setAccessible(true); - $this->expectException(HttpException::class); + $this->expectException(RequestParametersException::class); $startUpload->invokeArgs($uploader, [100, 'text/html', 'no-name', 'auto']); $this->expectExceptionMessageRegExp('Wrong request'); } - public function testExceptionInUploadPartsMethod() + public function testExceptionInUploadPartsMethod(): void { - if (PHP_MAJOR_VERSION >= 7) { - $exception = new ClientException('Wrong request', new Request('POST', 'https://some-middleware-endpoint'), new Response(400)); - } else { - $exception = new ClientException('Wrong request', new Request('POST', 'https://some-middleware-endpoint')); - } + $exception = new ClientException('Wrong request', new Request('POST', 'https://some-middleware-endpoint'), new Response(400)); $uploader = $this->getMockUploader(['sendRequest']); $uploader @@ -92,18 +85,14 @@ public function testExceptionInUploadPartsMethod() $uploadParts = (new \ReflectionObject($uploader))->getMethod('uploadParts'); $uploadParts->setAccessible(true); - $this->expectException(HttpException::class); + $this->expectException(RequestParametersException::class); $uploadParts->invokeArgs($uploader, [$mr, $handle]); - $this->expectExceptionMessageRegExp('some-middleware-endpoint'); + $this->expectExceptionMessageRegExp('Bad request'); } - public function testExceptionInFinishUpload() + public function testExceptionInFinishUpload(): void { - if (PHP_MAJOR_VERSION >= 7) { - $exception = new TooManyRedirectsException('Too many redirects', new Request('POST', 'https://final-endpoint'), new Response(400)); - } else { - $exception = new TooManyRedirectsException('Too many redirects', new Request('POST', 'https://final-endpoint')); - } + $exception = new TooManyRedirectsException('Too many redirects', new Request('POST', 'https://final-endpoint'), new Response(400)); $uploader = $this->getMockUploader(['sendRequest']); $uploader diff --git a/tests/AuthUrl/GenerateSecureUrlTest.php b/tests/AuthUrl/GenerateSecureUrlTest.php index 1413a71..83ddc90 100644 --- a/tests/AuthUrl/GenerateSecureUrlTest.php +++ b/tests/AuthUrl/GenerateSecureUrlTest.php @@ -3,7 +3,6 @@ namespace Tests\AuthUrl; use PHPUnit\Framework\TestCase; -use Uploadcare\Api; use Uploadcare\Apis\FileApi; use Uploadcare\AuthUrl\AuthUrlConfig; use Uploadcare\AuthUrl\Token\AkamaiToken; @@ -44,7 +43,7 @@ public function provideValidUrls(): array { return [ ['/1a86c6b8-4e77-44c2-9da7-4228ad9a2dd8/-/format/auto/-/quality/smart/-/resize/950x/result.jpg'], - ['/*/'] + ['/*/'], ]; } diff --git a/tests/CertainHttpExceptionTest.php b/tests/CertainHttpExceptionTest.php new file mode 100644 index 0000000..0f76e26 --- /dev/null +++ b/tests/CertainHttpExceptionTest.php @@ -0,0 +1,81 @@ + [ + new ClientException('', $request, new Response(403, [], 'Account has been blocked.')), + AccountException::class, + ], + FileTooLargeException::class => [ + new ClientException('', $request, new Response(413, [], 'Direct uploads only support files smaller than 100MB')), + FileTooLargeException::class, + ], + RequestParametersException::class => [ + new ClientException('', $request, new Response(400, [], 'File is too large.')), + RequestParametersException::class, + ], + ThrottledException::class => [ + new ClientException('', $request, new Response(429, ['Retry-After' => 40], 'Request was throttled.')), + ThrottledException::class, + ], + ]; + } + + /** + * @dataProvider provideExceptions + */ + public function testExceptionMessage(ClientException $exception, string $class): void + { + /** @var AbstractClientException $ex */ + $ex = new $class('', 0, $exception); + /** @var Response $response */ + $response = $exception->getResponse(); + self::assertEquals($ex->getMessage(), Message::toString($response)); + self::assertEquals($ex->getCode(), $response->getStatusCode()); + + if ($class === ThrottledException::class) { + self::assertNotEquals(10, $ex->getRetryAfter()); + } + } + + /** + * @dataProvider provideExceptions + */ + public function testUploaderDefineExceptionMethod(ClientException $exception, string $class): void + { + $uploader = new Uploader(Configuration::create('demopublickey', 'demosecretkey')); + $handleException = (new \ReflectionObject($uploader))->getMethod('handleException'); + $handleException->setAccessible(true); + + $result = $handleException->invokeArgs($uploader, [$exception]); + self::assertInstanceOf($class, $result); + } + + public function testUploaderWithUnknownExceptions(): void + { + $uploader = new Uploader(Configuration::create('demopublickey', 'demosecretkey')); + $handleException = (new \ReflectionObject($uploader))->getMethod('handleException'); + $handleException->setAccessible(true); + + $unknownClientException = new ClientException('', new Request('GET', 'https://example.com'), new Response(411, ['Retry-After' => 40], 'Request was throttled.')); + self::assertInstanceOf(HttpException::class, $handleException->invokeArgs($uploader, [$unknownClientException])); + + $abstractException = new \Exception(); + self::assertInstanceOf(HttpException::class, $handleException->invokeArgs($uploader, [$abstractException])); + } +} diff --git a/tests/HttpExceptionTest.php b/tests/HttpExceptionTest.php index 20d45dd..108a119 100644 --- a/tests/HttpExceptionTest.php +++ b/tests/HttpExceptionTest.php @@ -13,7 +13,7 @@ class HttpExceptionTest extends TestCase { - public function provideHttpExceptions() + public function provideHttpExceptions(): array { $request = new Request('GET', 'https://localhost'); @@ -31,7 +31,7 @@ public function provideHttpExceptions() * * @param \Exception $exception */ - public function testExceptionMessages(\Exception $exception) + public function testExceptionMessages(\Exception $exception): void { $httpException = new HttpException('', 0, $exception); self::assertStringContainsString($exception->getMessage(), $httpException->getMessage()); @@ -39,7 +39,7 @@ public function testExceptionMessages(\Exception $exception) public function testEmptyMessageInException() { - $ex = new ServerException('', new Request('GET', 'https://localhost'), new Response(400)); + $ex = new ServerException('', new Request('GET', 'https://localhost'), new Response(503)); $httpException = new HttpException('', 503, $ex); self::assertStringContainsString('Fail', $httpException->getMessage()); self::assertEquals(503, $httpException->getCode()); From b572dc52a14b992d83e833afeca13f98e32f7b4a Mon Sep 17 00:00:00 2001 From: Andrej Zhdanovskikh Date: Sun, 14 Nov 2021 10:23:27 +0100 Subject: [PATCH 11/18] Modify Webhook API to use application/json data in requests --- src/Apis/WebhookApi.php | 18 ++++++++---------- src/Response/WebhookResponse.php | 1 + tests/Serializer/DeserializeCollectionTest.php | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/Apis/WebhookApi.php b/src/Apis/WebhookApi.php index 20da89f..d7d48b2 100644 --- a/src/Apis/WebhookApi.php +++ b/src/Apis/WebhookApi.php @@ -40,12 +40,11 @@ public function listWebhooks(): CollectionInterface public function createWebhook(string $targetUrl, bool $isActive = true, string $event = 'file.uploaded'): WebhookInterface { $response = $this->request('POST', 'webhooks/', [ - 'form_params' => [ + 'body' => \json_encode([ 'target_url' => $targetUrl, 'event' => $event, 'is_active' => $isActive, - ], - 'Content-Type' => 'application/x-www-form-urlencoded', + ]), ]); $webhook = $this->configuration->getSerializer() @@ -64,20 +63,19 @@ public function createWebhook(string $targetUrl, bool $isActive = true, string $ public function updateWebhook(int $id, array $parameters): WebhookInterface { $uri = \sprintf('webhooks/%s/', $id); - $formData = []; + $data = []; if (isset($parameters['target_url'])) { - $formData['target_url'] = (string) $parameters['target_url']; + $data['target_url'] = (string) $parameters['target_url']; } if (isset($parameters['event'])) { - $formData['event'] = (string) $parameters['event']; + $data['event'] = (string) $parameters['event']; } if (isset($parameters['is_active'])) { - $formData['is_active'] = (bool) $parameters['is_active']; + $data['is_active'] = (bool) $parameters['is_active']; } $response = $this->request('PUT', $uri, [ - 'form_params' => $formData, - 'Content-Type' => 'application/x-www-form-urlencoded', + 'body' => \json_encode($data), ]); $result = $this->configuration->getSerializer() @@ -96,7 +94,7 @@ public function updateWebhook(int $id, array $parameters): WebhookInterface public function deleteWebhook(string $targetUrl): bool { $response = $this->request('DELETE', 'webhooks/unsubscribe/', [ - 'form_params' => ['target_url' => $targetUrl], + 'body' => \json_encode(['target_url' => $targetUrl]), ]); return $response->getStatusCode() === 204; diff --git a/src/Response/WebhookResponse.php b/src/Response/WebhookResponse.php index 2385fc7..361a8a2 100644 --- a/src/Response/WebhookResponse.php +++ b/src/Response/WebhookResponse.php @@ -51,6 +51,7 @@ public static function rules(): array 'event' => 'string', 'targetUrl' => 'string', 'project' => 'int', + 'isActive' => 'bool', ]; } diff --git a/tests/Serializer/DeserializeCollectionTest.php b/tests/Serializer/DeserializeCollectionTest.php index 9ccfe7a..e6125d7 100644 --- a/tests/Serializer/DeserializeCollectionTest.php +++ b/tests/Serializer/DeserializeCollectionTest.php @@ -4,6 +4,7 @@ use PHPUnit\Framework\TestCase; use Uploadcare\Interfaces\File\FileInfoInterface; +use Uploadcare\Interfaces\File\ImageInfoInterface; use Uploadcare\Interfaces\Response\ListResponseInterface; use Uploadcare\Response\FileListResponse; use Uploadcare\Serializer\Serializer; @@ -30,5 +31,18 @@ public function testDeserializeCollection() $result = $serializer->deserialize($content, FileListResponse::class); self::assertInstanceOf(ListResponseInterface::class, $result); self::assertInstanceOf(FileInfoInterface::class, $result->getResults()->first()); + + /** @var FileInfoInterface $file */ + $file = $result->getResults()->first(); + + self::assertNull($file->getDatetimeRemoved()); + self::assertInstanceOf(\DateTimeInterface::class, $file->getDatetimeStored()); + self::assertInstanceOf(\DateTimeInterface::class, $file->getDatetimeUploaded()); + self::assertInstanceOf(ImageInfoInterface::class, $file->getImageInfo()); + self::assertTrue($file->isImage()); + self::assertTrue($file->isReady()); + self::assertIsString($file->getMimeType()); + self::assertNotEmpty($file->getOriginalFileUrl()); + self::assertNotEmpty($file->getOriginalFilename()); } } From 9bf0f1ade31716190a1cb71dc2fda7dffa73f2e5 Mon Sep 17 00:00:00 2001 From: Andrej Zhdanovskikh Date: Sun, 14 Nov 2021 11:52:28 +0100 Subject: [PATCH 12/18] Add 'signing_secret' to Webhook --- src/Apis/WebhookApi.php | 10 +++++++++- src/Interfaces/Api/WebhookApiInterface.php | 2 +- src/Interfaces/Response/WebhookInterface.php | 5 +++++ src/Response/WebhookResponse.php | 18 ++++++++++++++++++ src/Webhook.php | 5 +++++ 5 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/Apis/WebhookApi.php b/src/Apis/WebhookApi.php index d7d48b2..fe54cc4 100644 --- a/src/Apis/WebhookApi.php +++ b/src/Apis/WebhookApi.php @@ -37,13 +37,18 @@ public function listWebhooks(): CollectionInterface /** * {@inheritDoc} */ - public function createWebhook(string $targetUrl, bool $isActive = true, string $event = 'file.uploaded'): WebhookInterface + public function createWebhook(string $targetUrl, bool $isActive = true, string $signingSecret = null, string $event = 'file.uploaded'): WebhookInterface { + if ($signingSecret !== null) { + $signingSecret = \substr($signingSecret, 0, 32); + } + $response = $this->request('POST', 'webhooks/', [ 'body' => \json_encode([ 'target_url' => $targetUrl, 'event' => $event, 'is_active' => $isActive, + 'signing_secret' => $signingSecret, ]), ]); @@ -73,6 +78,9 @@ public function updateWebhook(int $id, array $parameters): WebhookInterface if (isset($parameters['is_active'])) { $data['is_active'] = (bool) $parameters['is_active']; } + if (\array_key_exists('signing_secret', $parameters)) { + $data['signing_secret'] = $parameters['signing_secret']; + } $response = $this->request('PUT', $uri, [ 'body' => \json_encode($data), diff --git a/src/Interfaces/Api/WebhookApiInterface.php b/src/Interfaces/Api/WebhookApiInterface.php index 280e3b8..afed0b5 100644 --- a/src/Interfaces/Api/WebhookApiInterface.php +++ b/src/Interfaces/Api/WebhookApiInterface.php @@ -24,7 +24,7 @@ public function listWebhooks(): CollectionInterface; * * @return WebhookInterface */ - public function createWebhook(string $targetUrl, bool $isActive = true, string $event = 'file.uploaded'): WebhookInterface; + public function createWebhook(string $targetUrl, bool $isActive = true, string $signingSecret = null, string $event = 'file.uploaded'): WebhookInterface; /** * @param int $id diff --git a/src/Interfaces/Response/WebhookInterface.php b/src/Interfaces/Response/WebhookInterface.php index 6afc56a..6c3f4f9 100644 --- a/src/Interfaces/Response/WebhookInterface.php +++ b/src/Interfaces/Response/WebhookInterface.php @@ -38,4 +38,9 @@ public function getProject(): int; * @return bool */ public function isActive(): bool; + + /** + * @return string|null + */ + public function getSigningSecret(): ?string; } diff --git a/src/Response/WebhookResponse.php b/src/Response/WebhookResponse.php index 361a8a2..81ab42e 100644 --- a/src/Response/WebhookResponse.php +++ b/src/Response/WebhookResponse.php @@ -42,6 +42,11 @@ class WebhookResponse implements WebhookInterface, SerializableInterface */ private $isActive = true; + /** + * @var string|null + */ + private $signingSecret; + public static function rules(): array { return [ @@ -52,6 +57,7 @@ public static function rules(): array 'targetUrl' => 'string', 'project' => 'int', 'isActive' => 'bool', + 'signingSecret' => 'string', ]; } @@ -182,4 +188,16 @@ public function setIsActive(bool $isActive): self return $this; } + + public function setSigningSecret(?string $signingSecret): self + { + $this->signingSecret = $signingSecret; + + return $this; + } + + public function getSigningSecret(): ?string + { + return $this->signingSecret; + } } diff --git a/src/Webhook.php b/src/Webhook.php index f658054..f98f435 100644 --- a/src/Webhook.php +++ b/src/Webhook.php @@ -78,4 +78,9 @@ public function isActive(): bool { return $this->inner->isActive(); } + + public function getSigningSecret(): ?string + { + return $this->inner->getSigningSecret(); + } } From 01f8cca336089744c773bb385b64b251b9c7e2b8 Mon Sep 17 00:00:00 2001 From: Andrej Zhdanovskikh Date: Sun, 14 Nov 2021 11:52:49 +0100 Subject: [PATCH 13/18] Fix types and codestyle --- src/Exception/Upload/AbstractClientException.php | 2 +- src/Exception/Upload/ThrottledException.php | 8 ++------ src/File/ImageInfo.php | 4 ++-- src/Interfaces/File/FileInfoInterface.php | 2 +- src/Interfaces/File/ImageInfoInterface.php | 2 +- src/Uploader/AbstractUploader.php | 3 --- 6 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/Exception/Upload/AbstractClientException.php b/src/Exception/Upload/AbstractClientException.php index 062e510..32da058 100644 --- a/src/Exception/Upload/AbstractClientException.php +++ b/src/Exception/Upload/AbstractClientException.php @@ -11,7 +11,7 @@ public function __construct($message = '', $code = 0, \Exception $previous = nul { if ($previous instanceof ClientException) { $code = $previous->getCode(); - $message = ($response = $previous->getResponse()) !== null ? Message::toString($response) : 'Bad request'; + $message = Message::toString($previous->getResponse()); } parent::__construct($message, $code, $previous); diff --git a/src/Exception/Upload/ThrottledException.php b/src/Exception/Upload/ThrottledException.php index d9437bc..dfe0905 100644 --- a/src/Exception/Upload/ThrottledException.php +++ b/src/Exception/Upload/ThrottledException.php @@ -3,7 +3,6 @@ namespace Uploadcare\Exception\Upload; use GuzzleHttp\Exception\ClientException; -use Psr\Http\Message\ResponseInterface; class ThrottledException extends AbstractClientException { @@ -15,11 +14,8 @@ public function __construct($message = '', $code = 0, \Exception $previous = nul if ($previous instanceof ClientException) { $response = $previous->getResponse(); - if ($response instanceof ResponseInterface) { - $retryHeader = $response->getHeader('Retry-After'); - - $this->retryAfter = $retryHeader[0] ?? 10; - } + $retryHeader = $response->getHeader('Retry-After'); + $this->retryAfter = $retryHeader[0] ?? 10; } } diff --git a/src/File/ImageInfo.php b/src/File/ImageInfo.php index a44838b..5283e35 100644 --- a/src/File/ImageInfo.php +++ b/src/File/ImageInfo.php @@ -52,7 +52,7 @@ final class ImageInfo implements ImageInfoInterface, SerializableInterface private $datetimeOriginal; /** - * @var null|array + * @var array|null */ private $dpi; @@ -232,7 +232,7 @@ public function setDatetimeOriginal(?\DateTimeInterface $datetimeOriginal): self } /** - * @return null|array + * @return array|null */ public function getDpi(): ?array { diff --git a/src/Interfaces/File/FileInfoInterface.php b/src/Interfaces/File/FileInfoInterface.php index eea83fb..ea94a56 100644 --- a/src/Interfaces/File/FileInfoInterface.php +++ b/src/Interfaces/File/FileInfoInterface.php @@ -96,7 +96,7 @@ public function getUuid(): string; /** * Dictionary of other files that has been created using this file as source. Used for video, document and etc. conversion. * - * @return null|array + * @return array|null */ public function getVariations(): ?array; diff --git a/src/Interfaces/File/ImageInfoInterface.php b/src/Interfaces/File/ImageInfoInterface.php index 7098d5b..1aee78d 100644 --- a/src/Interfaces/File/ImageInfoInterface.php +++ b/src/Interfaces/File/ImageInfoInterface.php @@ -64,7 +64,7 @@ public function getDatetimeOriginal(): ?\DateTimeInterface; /** * Image DPI for two dimensions. * - * @return null|array + * @return array|null */ public function getDpi(): ?array; } diff --git a/src/Uploader/AbstractUploader.php b/src/Uploader/AbstractUploader.php index da3eb3a..5cad84c 100644 --- a/src/Uploader/AbstractUploader.php +++ b/src/Uploader/AbstractUploader.php @@ -350,9 +350,6 @@ protected function handleException(\Throwable $e): \RuntimeException { if ($e instanceof ClientException) { $response = $e->getResponse(); - if ($response === null) { - return new HttpException('', 0, $e); - } switch ($response->getStatusCode()) { case 400: $throw = new RequestParametersException('', 0, $e); From b0ae4442fc2fa79d8edf5dd7cd6a2ea0b6dd2d59 Mon Sep 17 00:00:00 2001 From: Andrej Zhdanovskikh Date: Sun, 14 Nov 2021 12:01:18 +0100 Subject: [PATCH 14/18] Update changelog, update README --- CHANGELOG.md | 5 +++++ README.md | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c29a351..9668fd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ The format is based now on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [3.1.2] +### Webhook sing +- add possibility to sign webhooks +- fix some minor issues + ## [3.1.1] ### Secure CDN URLs for transformed images - Now you can generate Secure CDN URLs for images with transformations. diff --git a/README.md b/README.md index 3e0251d..2ecd533 100644 --- a/README.md +++ b/README.md @@ -281,11 +281,12 @@ $webhookApi = (new \Uploadcare\Api($config))->webhook(); The methods are: - `listWebhooks()` — Returns a list of project webhooks as an instance of an `Uploadcare\WebhookCollection` class. Each element of this collection is an instance of a `Uploadcare\Webhook` class (see below); -- `createWebhook($targetUrl, $isActive = true, $event = 'file.uploaded')` — Creates a new webhook for the event. Returns the `Uploadcare\Webhook` class. +- `createWebhook(string $targetUrl, bool $isActive = true, string $signingSecret = null, string $event = 'file.uploaded')` — Creates a new webhook for the event. Returns the `Uploadcare\Webhook` class. - `updateWebhook($id, array $parameters)` — Updates an existing webhook with these parameters. Parameters can be: - `target_url` — A target callback URL; - `event` — The only `file.uploaded` event is supported at the moment. - `is_active` — Returns the webhook activity status. + - `signing_secret` — Webhook signing secret. See [Secure Webhooks](https://uploadcare.com/docs/security/secure-webhooks/) - `deleteWebhook` — Deletes a webhook by URL. #### `Uploadcare\Webhook` class From f93a8411a56df8fa3617b37420587f1b3925828b Mon Sep 17 00:00:00 2001 From: Roman Sedykh Date: Mon, 15 Nov 2021 14:33:17 +0300 Subject: [PATCH 15/18] Update CHANGELOG.md --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9668fd9..67994c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,9 @@ Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [3.1.2] -### Webhook sing -- add possibility to sign webhooks -- fix some minor issues +### Secure Webhooks +- Each webhook payload now can be [signed with a secret](https://uploadcare.com/docs/security/secure-webhooks/) to ensure that the request comes from the expected sender. +- Fix some minor issues. ## [3.1.1] ### Secure CDN URLs for transformed images From d3e62a28eeb98d5c267266194734fc24500d7172 Mon Sep 17 00:00:00 2001 From: Roman Sedykh Date: Mon, 15 Nov 2021 14:33:19 +0300 Subject: [PATCH 16/18] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2ecd533..08d0aca 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Uploadcare PHP integration handles uploads and further operations with files by wrapping Upload and REST APIs. -[![Uploadcare stack on StackShare][stack-img]][stack] ![Test workflow][action-img] ![Code coverage][codecov-img] +![Test workflow][action-img] ![Code coverage][codecov-img] [![Uploadcare stack on StackShare][stack-img]][stack] [action-img]: https://github.com/uploadcare/uploadcare-php/actions/workflows/phpunit.yml/badge.svg [stack-img]: http://img.shields.io/badge/tech-stack-0690fa.svg?style=flat From 1602cdde31519f8a8255b15d45ee9c8844933e07 Mon Sep 17 00:00:00 2001 From: AlexJezior Date: Mon, 7 Feb 2022 04:59:09 -0800 Subject: [PATCH 17/18] Resolving issue with secure delivery and image transformations. (#187) * Resolving issue with secure delivery and image transformations. * Updated changelog. --- CHANGELOG.md | 1 + src/Apis/FileApi.php | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67994c8..d494c5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to ### Secure Webhooks - Each webhook payload now can be [signed with a secret](https://uploadcare.com/docs/security/secure-webhooks/) to ensure that the request comes from the expected sender. - Fix some minor issues. +- Resolved issue with invalid signed urls being generated for image transformations. ## [3.1.1] ### Secure CDN URLs for transformed images diff --git a/src/Apis/FileApi.php b/src/Apis/FileApi.php index 37fe411..e751f19 100644 --- a/src/Apis/FileApi.php +++ b/src/Apis/FileApi.php @@ -326,7 +326,9 @@ private function checkTransformationUrl(string $url): string throw new InvalidArgumentException('URL must contain the file UUID'); } - return $url; + // Remove starting and end slash from the uuid with transformation string. The generator url template will + // already append these. + return trim(rtrim($url, '/'), '/'); } /** From b92f46670cbca4df9403d12045685e7865d58fdf Mon Sep 17 00:00:00 2001 From: Roman Sedykh Date: Mon, 7 Feb 2022 16:00:44 +0300 Subject: [PATCH 18/18] Update CHANGELOG.md --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d494c5b..7fb0a1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,14 @@ The format is based now on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## [3.1.2] +## [3.2.1] +### Secure delivery image processing fix +- Resolved issue with invalid signed urls being generated for image transformations. + +## [3.2.0] ### Secure Webhooks - Each webhook payload now can be [signed with a secret](https://uploadcare.com/docs/security/secure-webhooks/) to ensure that the request comes from the expected sender. - Fix some minor issues. -- Resolved issue with invalid signed urls being generated for image transformations. ## [3.1.1] ### Secure CDN URLs for transformed images