diff --git a/.drone.jsonnet b/.drone.jsonnet deleted file mode 100644 index e13974c7..00000000 --- a/.drone.jsonnet +++ /dev/null @@ -1,106 +0,0 @@ -local volumes = [ - { - name: "composer-cache", - path: "/tmp/composer-cache", - }, -]; - -local hostvolumes = [ - { - name: "composer-cache", - host: {path: "/tmp/composer-cache"} - }, -]; - -local composer(phpversion, params) = { - name: "composer", - image: "joomlaprojects/docker-images:php" + phpversion, - volumes: volumes, - commands: [ - "php -v", - "composer update " + params, - ] -}; - -local phpunit(phpversion) = { - name: "PHPUnit", - image: "joomlaprojects/docker-images:php" + phpversion, - [if phpversion == "8.3" then "failure"]: "ignore", - commands: ["vendor/bin/phpunit"] -}; - -local pipeline(name, phpversion, params) = { - kind: "pipeline", - name: "PHP " + name, - volumes: hostvolumes, - steps: [ - composer(phpversion, params), - phpunit(phpversion) - ], -}; - -[ - { - kind: "pipeline", - name: "Codequality", - volumes: hostvolumes, - steps: [ - { - name: "composer", - image: "joomlaprojects/docker-images:php8.1", - volumes: volumes, - commands: [ - "php -v", - "composer update" - ] - }, - { - name: "phpcs", - image: "joomlaprojects/docker-images:php8.1", - depends: [ "composer" ], - commands: [ - "vendor/bin/phpcs --standard=ruleset.xml src/" - ] - }, - { - name: "phan", - image: "joomlaprojects/docker-images:php8.1-ast", - depends: [ "composer" ], - failure: "ignore", - commands: [ - "vendor/bin/phan" - ] - }, - { - name: "phpstan", - image: "joomlaprojects/docker-images:php8.1", - depends: [ "composer" ], - failure: "ignore", - commands: [ - "vendor/bin/phpstan analyse src", - ] - }, - { - name: "phploc", - image: "joomlaprojects/docker-images:php8.1", - depends: [ "composer" ], - failure: "ignore", - commands: [ - "phploc src", - ] - }, - { - name: "phpcpd", - image: "joomlaprojects/docker-images:php8.1", - depends: [ "composer" ], - failure: "ignore", - commands: [ - "phpcpd src", - ] - } - ] - }, - pipeline("8.1 lowest", "8.1", "--prefer-stable --prefer-lowest"), - pipeline("8.1", "8.1", "--prefer-stable"), - pipeline("8.2", "8.2", "--prefer-stable"), -] diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index 4c3a5dbb..00000000 --- a/.drone.yml +++ /dev/null @@ -1,115 +0,0 @@ ---- -kind: pipeline -name: Codequality -steps: -- commands: - - php -v - - composer update - image: joomlaprojects/docker-images:php8.1 - name: composer - volumes: - - name: composer-cache - path: /tmp/composer-cache -- commands: - - vendor/bin/phpcs --standard=ruleset.xml src/ - depends: - - composer - image: joomlaprojects/docker-images:php8.1 - name: phpcs -- commands: - - vendor/bin/phan - depends: - - composer - failure: ignore - image: joomlaprojects/docker-images:php8.1-ast - name: phan -- commands: - - vendor/bin/phpstan analyse src - depends: - - composer - failure: ignore - image: joomlaprojects/docker-images:php8.1 - name: phpstan -- commands: - - phploc src - depends: - - composer - failure: ignore - image: joomlaprojects/docker-images:php8.1 - name: phploc -- commands: - - phpcpd src - depends: - - composer - failure: ignore - image: joomlaprojects/docker-images:php8.1 - name: phpcpd -volumes: -- host: - path: /tmp/composer-cache - name: composer-cache ---- -kind: pipeline -name: PHP 8.1 lowest -steps: -- commands: - - php -v - - composer update --prefer-stable --prefer-lowest - image: joomlaprojects/docker-images:php8.1 - name: composer - volumes: - - name: composer-cache - path: /tmp/composer-cache -- commands: - - vendor/bin/phpunit - image: joomlaprojects/docker-images:php8.1 - name: PHPUnit -volumes: -- host: - path: /tmp/composer-cache - name: composer-cache ---- -kind: pipeline -name: PHP 8.1 -steps: -- commands: - - php -v - - composer update --prefer-stable - image: joomlaprojects/docker-images:php8.1 - name: composer - volumes: - - name: composer-cache - path: /tmp/composer-cache -- commands: - - vendor/bin/phpunit - image: joomlaprojects/docker-images:php8.1 - name: PHPUnit -volumes: -- host: - path: /tmp/composer-cache - name: composer-cache ---- -kind: pipeline -name: PHP 8.2 -steps: -- commands: - - php -v - - composer update --prefer-stable - image: joomlaprojects/docker-images:php8.2 - name: composer - volumes: - - name: composer-cache - path: /tmp/composer-cache -- commands: - - vendor/bin/phpunit - image: joomlaprojects/docker-images:php8.2 - name: PHPUnit -volumes: -- host: - path: /tmp/composer-cache - name: composer-cache ---- -kind: signature -hmac: 2d171065b4a43d332cf4bcdaa8bd705a5419180edd3eba0a101ffb03048a76f5 - -... diff --git a/.gitattributes b/.gitattributes index 32fd9d34..0f255e23 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,11 +1,10 @@ .github/ export-ignore -.phan/ export-ignore docs/ export-ignore Tests/ export-ignore -.drone.jsonnet export-ignore -.drone.yml export-ignore .editorconfig export-ignore .gitattributes export-ignore .gitignore export-ignore +phpstan.neon export-ignore +phpstan-baseline.neon export-ignore phpunit.xml.dist export-ignore ruleset.xml export-ignore diff --git a/README.md b/README.md index 380c868c..a36da109 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,12 @@ ## Installation via Composer -Add `"joomla/oauth2": "~3.0"` to the require block in your composer.json and then run `composer install`. +Add `"joomla/oauth2": "~4.0"` to the require block in your composer.json and then run `composer install`. ```json { "require": { - "joomla/oauth2": "~3.0" + "joomla/oauth2": "~4.0" } } ``` @@ -20,5 +20,5 @@ Add `"joomla/oauth2": "~3.0"` to the require block in your composer.json and the Alternatively, you can simply run the following from the command line: ```sh -composer require joomla/oauth2 "~3.0" +composer require joomla/oauth2 "~4.0" ``` diff --git a/SECURITY.md b/SECURITY.md index 80806aa7..86d5164d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,7 +5,8 @@ These versions are currently being supported with security updates: | Version | Supported | -| ------- | ------------------ | +|---------| ------------------ | +| 4.x.x | :white_check_mark: | | 3.x.x | :white_check_mark: | | 2.0.x | :white_check_mark: | | 1.1.x | :x: | diff --git a/Tests/ClientTest.php b/Tests/ClientTest.php index 6f30b281..98d90379 100644 --- a/Tests/ClientTest.php +++ b/Tests/ClientTest.php @@ -9,6 +9,7 @@ use Joomla\Application\WebApplicationInterface; use Joomla\Http\Http; +use Joomla\Http\Response; use Joomla\Input\Input; use Joomla\OAuth2\Client; use Joomla\Registry\Registry; @@ -34,7 +35,7 @@ class ClientTest extends TestCase * * @var Http|MockObject */ - protected $client; + protected $http; /** * The input object to use in retrieving GET/POST data. @@ -197,24 +198,27 @@ public function testQuery() $token['expires_in'] = 3600; $this->object->setToken($token); + $returnData = new Response('data://text/plain,Lorem ipsum dolor sit amet.', 200, ['Content-Type' => 'text/html']); + $this->http->expects($this->once()) ->method('post') - ->willReturnCallback([$this, 'queryOauthCallback']); + ->willReturn($returnData); $result = $this->object->query('https://www.googleapis.com/auth/calendar', ['param' => 'value'], [], 'post'); - $this->assertEquals($result->body, 'Lorem ipsum dolor sit amet.'); - $this->assertEquals(200, $result->code); + $this->assertEquals('Lorem ipsum dolor sit amet.', $result->getBody()->getContents()); + $this->assertEquals(200, $result->getStatusCode()); + $returnData->getBody()->rewind(); $this->object->setOption('authmethod', 'get'); $this->http->expects($this->once()) ->method('get') - ->willReturnCallback([$this, 'getOauthCallback']); + ->willReturn($returnData); $result = $this->object->query('https://www.googleapis.com/auth/calendar', ['param' => 'value'], [], 'get'); - $this->assertEquals($result->body, 'Lorem ipsum dolor sit amet.'); - $this->assertEquals(200, $result->code); + $this->assertEquals('Lorem ipsum dolor sit amet.', $result->getBody()->getContents()); + $this->assertEquals(200, $result->getStatusCode()); } /** @@ -324,9 +328,11 @@ public function testRefreshTokenJson() $this->object->setOption('userefresh', true); $this->object->setToken(['access_token' => 'RANDOM STRING OF DATA', 'expires' => 3600, 'refresh_token' => ' RANDOM STRING OF DATA']); + $returnData = new Response('data://text/plain,{"access_token":"accessvalue","refresh_token":"refreshvalue","expires_in":3600}', 200, ['Content-Type' => 'application/json']); + $this->http->expects($this->once()) ->method('post') - ->willReturnCallback([$this, 'jsonGrantOauthCallback']); + ->willReturn($returnData); $result = $this->object->refreshToken(); @@ -344,36 +350,11 @@ public function testRefreshTokenJson() * @param ?array $headers An array of name-value pairs to include in the header of the request * @param ?integer $timeout Read timeout in seconds. * - * @return object + * @return Response */ public function encodedGrantOauthCallback($url, $data, ?array $headers = null, $timeout = null) { - $response = new \stdClass(); - - $response->code = 200; - $response->headers = ['Content-Type' => 'x-www-form-urlencoded']; - $response->body = 'access_token=accessvalue&refresh_token=refreshvalue&expires_in=3600'; - - return $response; - } - - /** - * Callback to mock a JSON based & granted OAuth response - * - * @param string $url Path to the resource. - * @param mixed $data Either an associative array or a string to be sent with the request. - * @param ?array $headers An array of name-value pairs to include in the header of the request - * @param ?integer $timeout Read timeout in seconds. - * - * @return object - */ - public function jsonGrantOauthCallback($url, $data, ?array $headers = null, $timeout = null) - { - $response = new \stdClass(); - - $response->code = 200; - $response->headers = ['Content-Type' => 'application/json']; - $response->body = '{"access_token":"accessvalue","refresh_token":"refreshvalue","expires_in":3600}'; + $response = new Response('data://text/plain,access_token=accessvalue&refresh_token=refreshvalue&expires_in=3600', 200, ['Content-Type' => 'x-www-form-urlencoded']); return $response; } @@ -386,15 +367,11 @@ public function jsonGrantOauthCallback($url, $data, ?array $headers = null, $tim * @param ?array $headers An array of name-value pairs to include in the header of the request * @param ?integer $timeout Read timeout in seconds. * - * @return object + * @return Response */ public function queryOauthCallback($url, $data, ?array $headers = null, $timeout = null) { - $response = new \stdClass(); - - $response->code = 200; - $response->headers = ['Content-Type' => 'text/html']; - $response->body = 'Lorem ipsum dolor sit amet.'; + $response = new Response('data://text/plain,Lorem ipsum dolor sit amet.', 200, ['Content-Type' => 'text/html']); return $response; } @@ -406,15 +383,11 @@ public function queryOauthCallback($url, $data, ?array $headers = null, $timeout * @param ?array $headers An array of name-value pairs to include in the header of the request. * @param ?integer $timeout Read timeout in seconds. * - * @return object + * @return Response */ public function getOauthCallback($url, ?array $headers = null, $timeout = null) { - $response = new \stdClass(); - - $response->code = 200; - $response->headers = ['Content-Type' => 'text/html']; - $response->body = 'Lorem ipsum dolor sit amet.'; + $response = new Response('data://text/plain,Lorem ipsum dolor sit amet.', 200, ['Content-Type' => 'text/html']); return $response; } diff --git a/composer.json b/composer.json index af02b95e..8d2f7628 100644 --- a/composer.json +++ b/composer.json @@ -6,18 +6,18 @@ "homepage": "https://github.com/joomla-framework/oauth2", "license": "GPL-2.0-or-later", "require": { - "php": "^8.1.0", - "joomla/application": "^3.0", - "joomla/http": "^3.0", - "joomla/input": "^3.0", - "joomla/session": "^3.0", - "joomla/uri": "^3.0" + "php": "^8.3.0", + "joomla/application": "dev-4.x-dev", + "joomla/http": "dev-4.x-dev", + "joomla/input": "dev-4.x-dev", + "joomla/session": "dev-4.x-dev", + "joomla/uri": "dev-4.x-dev" }, "require-dev": { - "phpunit/phpunit": "^9.5.28", - "squizlabs/php_codesniffer": "^3.7.2", - "phpstan/phpstan": "1.12.27", - "phpstan/phpstan-deprecation-rules": "1.2.1" + "phpunit/phpunit": "^12.0", + "squizlabs/php_codesniffer": "^3.10.2", + "phpstan/phpstan": "^2.1.17", + "phpstan/phpstan-deprecation-rules": "^2.0.3" }, "autoload": { "psr-4": { @@ -33,7 +33,8 @@ "extra": { "branch-alias": { "dev-2.0-dev": "2.0-dev", - "dev-3.x-dev": "3.0-dev" + "dev-3.x-dev": "3.0-dev", + "dev-4.x-dev": "4.0-dev" } } } diff --git a/docs/index.md b/docs/index.md index c87f1b49..8ca42a4a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1 +1,3 @@ * [Overview](overview.md) +* [Updating from v2 to v3](v2-to-v3-update.md) +* [Updating from v3 to v4](v3-to-v4-update.md) diff --git a/docs/v2-to-v3-update.md b/docs/v2-to-v3-update.md new file mode 100644 index 00000000..bdb1f3e8 --- /dev/null +++ b/docs/v2-to-v3-update.md @@ -0,0 +1,5 @@ +## Updating from v2 to v3 + +### Minimum supported PHP version raised + +All Framework packages now require PHP 8.1 or newer. diff --git a/docs/v3-to-v4-update.md b/docs/v3-to-v4-update.md new file mode 100644 index 00000000..847bf62f --- /dev/null +++ b/docs/v3-to-v4-update.md @@ -0,0 +1,5 @@ +## Updating from v3 to v4 + +### Minimum supported PHP version raised + +All Framework packages now require PHP 8.3 or newer. diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 00000000..92dc0323 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,13 @@ +parameters: + ignoreErrors: + - + message: '#^Comparison operation "\<" between \(array\|float\|int\) and int\<21, max\> results in an error\.$#' + identifier: smaller.invalid + count: 2 + path: src/Client.php + + - + message: '#^Method Joomla\\OAuth2\\Client\:\:query\(\) should return Joomla\\Http\\Response but returns false\.$#' + identifier: return.type + count: 1 + path: src/Client.php diff --git a/phpstan.neon b/phpstan.neon index 07d82270..305d72f1 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,6 +1,7 @@ includes: - vendor/phpstan/phpstan-deprecation-rules/rules.neon + - phpstan-baseline.neon parameters: level: 5 diff --git a/src/Client.php b/src/Client.php index 372ecbb5..a6d98fd1 100644 --- a/src/Client.php +++ b/src/Client.php @@ -7,6 +7,8 @@ * @license GNU General Public License version 2 or later; see LICENSE */ +declare(strict_types=1); + namespace Joomla\OAuth2; use Joomla\Application\WebApplicationInterface; @@ -65,14 +67,8 @@ class Client * * @since 1.0 */ - public function __construct($options = [], ?Http $http = null, ?Input $input = null, ?WebApplicationInterface $application = null) + public function __construct(array|\ArrayAccess $options = [], ?Http $http = null, ?Input $input = null, ?WebApplicationInterface $application = null) { - if (!\is_array($options) && !($options instanceof \ArrayAccess)) { - throw new \InvalidArgumentException( - 'The options param must be an array or implement the ArrayAccess interface.' - ); - } - $this->options = $options; $this->http = $http ?: (new HttpFactory())->getHttp($this->options); $this->input = $input ?: ($application ? $application->getInput() : new Input()); @@ -90,7 +86,9 @@ public function __construct($options = [], ?Http $http = null, ?Input $input = n */ public function authenticate() { - if ($dataCode = $this->input->get('code', false, 'raw')) { + $dataCode = $this->input->get('code', false, 'raw'); + + if ($dataCode) { $data = [ 'grant_type' => 'authorization_code', 'redirect_uri' => $this->getOption('redirecturi'), @@ -101,21 +99,21 @@ public function authenticate() $response = $this->http->post($this->getOption('tokenurl'), $data); - if (!($response->code >= 200 && $response->code < 400)) { + if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 400) { throw new UnexpectedResponseException( $response, sprintf( 'Error code %s received requesting access token: %s.', - $response->code, - $response->body + $response->getStatusCode(), + $response->getBody()->getContents() ) ); } - if (strpos($response->headers['Content-Type'], 'application/json') !== false) { - $token = array_merge(json_decode($response->body, true), ['created' => time()]); + if (in_array('application/json', $response->getHeader('Content-Type'))) { + $token = array_merge(json_decode($response->getBody()->getContents(), true), ['created' => time()]); } else { - parse_str($response->body, $token); + parse_str($response->getBody()->getContents(), $token); $token = array_merge($token, ['created' => time()]); } @@ -125,12 +123,6 @@ public function authenticate() } if ($this->getOption('sendheaders')) { - if (!($this->application instanceof WebApplicationInterface)) { - throw new \RuntimeException( - \sprintf('A "%s" implementation is required to process authentication.', WebApplicationInterface::class) - ); - } - $this->application->redirect($this->createUrl()); } @@ -177,16 +169,22 @@ public function createUrl() $url->setVar('response_type', 'code'); $url->setVar('client_id', urlencode($this->getOption('clientid'))); - if ($redirect = $this->getOption('redirecturi')) { + $redirect = $this->getOption('redirecturi'); + + if ($redirect) { $url->setVar('redirect_uri', urlencode($redirect)); } - if ($scope = $this->getOption('scope')) { + $scope = $this->getOption('scope'); + + if ($scope) { $scope = \is_array($scope) ? implode(' ', $scope) : $scope; $url->setVar('scope', urlencode($scope)); } - if ($state = $this->getOption('state')) { + $state = $this->getOption('state'); + + if ($state) { $url->setVar('state', urlencode($state)); } @@ -254,13 +252,13 @@ public function query($url, $data = null, $headers = [], $method = 'get', $timeo throw new \InvalidArgumentException('Unknown HTTP request method: ' . $method . '.'); } - if ($response->code < 200 || $response->code >= 400) { + if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 400) { throw new UnexpectedResponseException( $response, sprintf( 'Error code %s received requesting data: %s.', - $response->code, - $response->body + $response->getStatusCode(), + $response->getBody() ) ); } @@ -321,9 +319,9 @@ public function getToken() * * @since 1.0 */ - public function setToken($value) + public function setToken(array $value) { - if (\is_array($value) && !array_key_exists('expires_in', $value) && array_key_exists('expires', $value)) { + if (!array_key_exists('expires_in', $value) && array_key_exists('expires', $value)) { $value['expires_in'] = $value['expires']; unset($value['expires']); } @@ -369,21 +367,21 @@ public function refreshToken($token = null) $response = $this->http->post($this->getOption('tokenurl'), $data); - if (!($response->code >= 200 || $response->code < 400)) { + if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 400) { throw new UnexpectedResponseException( $response, sprintf( 'Error code %s received refreshing token: %s.', - $response->code, - $response->body + $response->getStatusCode(), + $response->getBody()->getContents() ) ); } - if (strpos($response->headers['Content-Type'], 'application/json') !== false) { - $token = array_merge(json_decode($response->body, true), ['created' => time()]); + if (in_array('application/json', $response->getHeader('Content-Type'))) { + $token = array_merge(json_decode($response->getBody()->getContents(), true), ['created' => time()]); } else { - parse_str($response->body, $token); + parse_str($response->getBody()->getContents(), $token); $token = array_merge($token, ['created' => time()]); }