Initial commit!
This commit is contained in:
commit
8712013261
39 changed files with 4105 additions and 0 deletions
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
* text=auto
|
||||||
|
*.sh text eol=lf
|
||||||
|
*.php text eol=lf
|
||||||
|
VERSION text eol=lf
|
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
[Tt]humbs.db
|
||||||
|
[Dd]esktop.ini
|
||||||
|
.DS_Store
|
||||||
|
.vscode/
|
||||||
|
.vs/
|
||||||
|
.idea/
|
||||||
|
html/
|
||||||
|
.phpdoc*
|
||||||
|
.phpunit*
|
||||||
|
vendor/
|
30
LICENCE
Normal file
30
LICENCE
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
Copyright (c) 2024, flashwave <me@flash.moe>
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted (subject to the limitations in the disclaimer
|
||||||
|
below) provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
* Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from this
|
||||||
|
software without specific prior written permission.
|
||||||
|
|
||||||
|
NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||||
|
THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
||||||
|
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||||
|
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||||
|
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||||
|
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
|
||||||
|
IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGE.
|
31
README.md
Normal file
31
README.md
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# Flashii PHP API client
|
||||||
|
|
||||||
|
This is the source for the first party Flashii API client implementation in PHP.
|
||||||
|
|
||||||
|
## Requirements and Dependencies
|
||||||
|
|
||||||
|
This library currently targets **PHP 8.1**.
|
||||||
|
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
Versioning will follows the [Semantic Versioning specification v2.0.0](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
The MAJOR version component will always be in sync with the implemented version of the Flashii API.
|
||||||
|
|
||||||
|
The MINOR version component will be incremented depending changes to the library itself, be it feature additions or removals or changes to minimum required PHP version.
|
||||||
|
|
||||||
|
The version is stored in the root of the repository in a file called `VERSION` and can be read using `Flashii\FlashiiClient::version()`.
|
||||||
|
|
||||||
|
|
||||||
|
## Contribution
|
||||||
|
|
||||||
|
By submitting code for inclusion in the main source tree you agree to transfer ownership of the code to the project owner.
|
||||||
|
The contributor will still be attributed for the contributed code, unless they ask for this attribution to be removed.
|
||||||
|
This is to avoid intellectual property rights traps and drama that could lead to blackmail situations.
|
||||||
|
If you do not agree with these terms, you are free to fork off.
|
||||||
|
|
||||||
|
|
||||||
|
## Licencing
|
||||||
|
|
||||||
|
The Flashii API client is available under the BSD 3-Clause Clear License, a full version of which is enclosed in the LICENCE file.
|
1
VERSION
Normal file
1
VERSION
Normal file
|
@ -0,0 +1 @@
|
||||||
|
0.1.0
|
27
composer.json
Normal file
27
composer.json
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "flashii/apii",
|
||||||
|
"description": "Client library for the Flashii.net API.",
|
||||||
|
"type": "library",
|
||||||
|
"homepage": "https://api.flashii.net",
|
||||||
|
"license": "bsd-3-clause-clear",
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^10.5",
|
||||||
|
"phpstan/phpstan": "^1.12"
|
||||||
|
},
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "flashwave",
|
||||||
|
"email": "packagist@flash.moe",
|
||||||
|
"homepage": "https://flash.moe",
|
||||||
|
"role": "mom"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Flashii\\": "src"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1703
composer.lock
generated
Normal file
1703
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
35
phpdoc.xml
Normal file
35
phpdoc.xml
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<phpdocumentor configVersion="3.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns="https://www.phpdoc.org"
|
||||||
|
xsi:noNamespaceSchemaLocation="data/xsd/phpdoc.xsd">
|
||||||
|
<title>Flashii PHP API Client Documentation</title>
|
||||||
|
<template name="default"/>
|
||||||
|
<paths>
|
||||||
|
<output>html</output>
|
||||||
|
</paths>
|
||||||
|
<version number="1.0">
|
||||||
|
<folder>latest</folder>
|
||||||
|
<api format="php">
|
||||||
|
<output>api</output>
|
||||||
|
<visibility>public</visibility>
|
||||||
|
<default-package-name>Flashii</default-package-name>
|
||||||
|
<source dsn=".">
|
||||||
|
<path>src</path>
|
||||||
|
</source>
|
||||||
|
<extensions>
|
||||||
|
<extension>php</extension>
|
||||||
|
</extensions>
|
||||||
|
<ignore hidden="true" symlinks="true">
|
||||||
|
<path>tests/**/*</path>
|
||||||
|
</ignore>
|
||||||
|
<ignore-tags>
|
||||||
|
<ignore-tag>template</ignore-tag>
|
||||||
|
<ignore-tag>template-extends</ignore-tag>
|
||||||
|
<ignore-tag>template-implements</ignore-tag>
|
||||||
|
<ignore-tag>extends</ignore-tag>
|
||||||
|
<ignore-tag>implements</ignore-tag>
|
||||||
|
</ignore-tags>
|
||||||
|
</api>
|
||||||
|
</version>
|
||||||
|
</phpdocumentor>
|
8
phpstan.neon
Normal file
8
phpstan.neon
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
parameters:
|
||||||
|
level: 9
|
||||||
|
checkUninitializedProperties: true
|
||||||
|
checkImplicitMixed: true
|
||||||
|
checkBenevolentUnionTypes: true
|
||||||
|
paths:
|
||||||
|
- src
|
||||||
|
- tests
|
23
phpunit.xml
Normal file
23
phpunit.xml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
|
||||||
|
colors="true"
|
||||||
|
executionOrder="depends,defects"
|
||||||
|
beStrictAboutOutputDuringTests="true"
|
||||||
|
failOnRisky="true"
|
||||||
|
failOnWarning="true"
|
||||||
|
cacheDirectory=".phpunit.cache"
|
||||||
|
requireCoverageMetadata="true"
|
||||||
|
beStrictAboutCoverageMetadata="true">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="default">
|
||||||
|
<directory suffix="Test.php">tests</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<coverage/>
|
||||||
|
<source>
|
||||||
|
<include>
|
||||||
|
<directory suffix=".php">src</directory>
|
||||||
|
</include>
|
||||||
|
</source>
|
||||||
|
</phpunit>
|
63
src/AuthorizedHttpClient.php
Normal file
63
src/AuthorizedHttpClient.php
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
// AuthorizedHttpClient.php
|
||||||
|
// Created: 2024-11-16
|
||||||
|
// Updated: 2024-11-16
|
||||||
|
|
||||||
|
namespace Flashii;
|
||||||
|
|
||||||
|
use Flashii\Credentials\Credentials;
|
||||||
|
use Flashii\Http\{HttpClient,HttpRequest,HttpResponse};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a proxy HttpClient that ensures the HTTP Authorization header is set on requests.
|
||||||
|
*/
|
||||||
|
class AuthorizedHttpClient implements HttpClient {
|
||||||
|
/**
|
||||||
|
* @param HttpClient $httpClient Underlying HttpClient implementation.
|
||||||
|
* @param Credentials $credentials Credentials to use for the Authorization header.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private HttpClient $httpClient,
|
||||||
|
private Credentials $credentials
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the underlying HttpClient instance.
|
||||||
|
*
|
||||||
|
* @return HttpClient
|
||||||
|
*/
|
||||||
|
public function getHttpClient(): HttpClient {
|
||||||
|
return $this->httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the authorization credentials.
|
||||||
|
*
|
||||||
|
* @return Credentials
|
||||||
|
*/
|
||||||
|
public function getCredentials(): Credentials {
|
||||||
|
return $this->credentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createRequest(string $method, string $url): HttpRequest {
|
||||||
|
return $this->httpClient->createRequest($method, $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendRequest(HttpRequest $request): HttpResponse {
|
||||||
|
$authz = $this->credentials->getHttpAuthorization();
|
||||||
|
if($authz !== '')
|
||||||
|
$request->setHeader('Authorization', $authz);
|
||||||
|
|
||||||
|
return $this->httpClient->sendRequest($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a HTTP request and returns a HTTP response without applying the HTTP authorization header.
|
||||||
|
*
|
||||||
|
* @param HttpRequest $request Request to send.
|
||||||
|
* @return HttpResponse
|
||||||
|
*/
|
||||||
|
public function sendRequestAnonymous(HttpRequest $request): HttpResponse {
|
||||||
|
return $this->httpClient->sendRequest($request);
|
||||||
|
}
|
||||||
|
}
|
15
src/Credentials/AnonymousCredentials.php
Normal file
15
src/Credentials/AnonymousCredentials.php
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
// AnonymousCredentials.php
|
||||||
|
// Created: 2024-11-16
|
||||||
|
// Updated: 2024-11-16
|
||||||
|
|
||||||
|
namespace Flashii\Credentials;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements anonymous credentials.
|
||||||
|
*/
|
||||||
|
class AnonymousCredentials implements Credentials {
|
||||||
|
public function getHttpAuthorization(): string {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
41
src/Credentials/BasicCredentials.php
Normal file
41
src/Credentials/BasicCredentials.php
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
// BasicCredentials.php
|
||||||
|
// Created: 2024-11-16
|
||||||
|
// Updated: 2024-11-16
|
||||||
|
|
||||||
|
namespace Flashii\Credentials;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements basic HTTP credentials for OAuth2 apps.
|
||||||
|
*/
|
||||||
|
class BasicCredentials implements Credentials {
|
||||||
|
private string $authz;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $userName User name/Client ID.
|
||||||
|
* @param string $password Password/Client secret.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private string $userName,
|
||||||
|
#[\SensitiveParameter]
|
||||||
|
string $password = ''
|
||||||
|
) {
|
||||||
|
$authz = $userName;
|
||||||
|
if($password !== '')
|
||||||
|
$authz .= sprintf(':%s', $password);
|
||||||
|
|
||||||
|
$this->authz = sprintf('Basic %s', base64_encode($authz));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUserName(): string {
|
||||||
|
return $this->userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHttpAuthorization(): string {
|
||||||
|
return $this->authz;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __debugInfo(): array {
|
||||||
|
return ['authz' => 'Basic #####'];
|
||||||
|
}
|
||||||
|
}
|
31
src/Credentials/BearerCredentials.php
Normal file
31
src/Credentials/BearerCredentials.php
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
// BearerCredentials.php
|
||||||
|
// Created: 2024-11-16
|
||||||
|
// Updated: 2024-11-16
|
||||||
|
|
||||||
|
namespace Flashii\Credentials;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements bearer HTTP credentials for OAuth2 access tokens.
|
||||||
|
*/
|
||||||
|
class BearerCredentials implements Credentials {
|
||||||
|
private string $authz;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $accessToken Bearer access token.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
#[\SensitiveParameter]
|
||||||
|
string $accessToken
|
||||||
|
) {
|
||||||
|
$this->authz = sprintf('Bearer %s', $accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHttpAuthorization(): string {
|
||||||
|
return $this->authz;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __debugInfo(): array {
|
||||||
|
return ['authz' => 'Bearer #####'];
|
||||||
|
}
|
||||||
|
}
|
20
src/Credentials/Credentials.php
Normal file
20
src/Credentials/Credentials.php
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
// Credentials.php
|
||||||
|
// Created: 2024-11-16
|
||||||
|
// Updated: 2024-11-16
|
||||||
|
|
||||||
|
namespace Flashii\Credentials;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides an interface for Flashii API credentials.
|
||||||
|
*/
|
||||||
|
interface Credentials {
|
||||||
|
/**
|
||||||
|
* Returns what to set the HTTP Authorization header to.
|
||||||
|
*
|
||||||
|
* If the output is blank the header is not supplied.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function getHttpAuthorization(): string;
|
||||||
|
}
|
33
src/Credentials/MisuzuCredentials.php
Normal file
33
src/Credentials/MisuzuCredentials.php
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
// MisuzuCredentials.php
|
||||||
|
// Created: 2024-11-16
|
||||||
|
// Updated: 2024-11-16
|
||||||
|
|
||||||
|
namespace Flashii\Credentials;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements Misuzu HTTP credentials for internal session token auth.
|
||||||
|
*
|
||||||
|
* Do not use this unless you're flashwave!!!!!!! bad!!!!!!!!
|
||||||
|
*/
|
||||||
|
class MisuzuCredentials implements Credentials {
|
||||||
|
private string $authz;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $sessionToken Misuzu session token.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
#[\SensitiveParameter]
|
||||||
|
string $sessionToken
|
||||||
|
) {
|
||||||
|
$this->authz = sprintf('Misuzu %s', $sessionToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHttpAuthorization(): string {
|
||||||
|
return $this->authz;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __debugInfo(): array {
|
||||||
|
return ['authz' => 'Misuzu #####'];
|
||||||
|
}
|
||||||
|
}
|
235
src/FlashiiClient.php
Normal file
235
src/FlashiiClient.php
Normal file
|
@ -0,0 +1,235 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
// FlashiiClient.php
|
||||||
|
// Created: 2024-11-16
|
||||||
|
// Updated: 2024-11-16
|
||||||
|
|
||||||
|
namespace Flashii;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
use Flashii\Credentials\{AnonymousCredentials,BasicCredentials,BearerCredentials,Credentials,MisuzuCredentials};
|
||||||
|
use Flashii\Http\HttpClient;
|
||||||
|
use Flashii\Http\Curl\CurlHttpClient;
|
||||||
|
use Flashii\Http\Stream\StreamHttpClient;
|
||||||
|
use Flashii\OAuth2\OAuth2Client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flashii API client.
|
||||||
|
*/
|
||||||
|
class FlashiiClient {
|
||||||
|
/**
|
||||||
|
* Library source directory path.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public const LIB_PATH_SOURCE = __DIR__;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Library root directory path.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public const LIB_PATH_ROOT = self::LIB_PATH_SOURCE . DIRECTORY_SEPARATOR . '..';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Library version file path.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public const LIB_PATH_VERSION = self::LIB_PATH_ROOT . DIRECTORY_SEPARATOR . 'VERSION';
|
||||||
|
|
||||||
|
private AuthorizedHttpClient $httpClient;
|
||||||
|
private Credentials $credentials;
|
||||||
|
private FlashiiUrls $urls;
|
||||||
|
|
||||||
|
// Use newLazyGhost to initialise these once PHP 8.4 can be targeted proper
|
||||||
|
private ?OAuth2Client $oauth2 = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $userAgentOrHttpClient User agent string for your application or a HttpClient implementation, providing a string will autodetect whether to use cURL or PHP streams.
|
||||||
|
* @param ?Credentials $credentials Credentials to authenticate with, leave null for unauthorised.
|
||||||
|
* @param ?FlashiiUrls $urls Object containing base URLs, leave null to use the production environment.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
string|HttpClient $userAgentOrHttpClient = '',
|
||||||
|
?Credentials $credentials = null,
|
||||||
|
?FlashiiUrls $urls = null
|
||||||
|
) {
|
||||||
|
$this->urls = $urls ?? FlashiiUrls::production();
|
||||||
|
$this->credentials = $credentials ?? new AnonymousCredentials;
|
||||||
|
|
||||||
|
if(is_string($userAgentOrHttpClient))
|
||||||
|
$httpClient = self::createHttpClient($userAgentOrHttpClient);
|
||||||
|
else {
|
||||||
|
if($userAgentOrHttpClient instanceof AuthorizedHttpClient)
|
||||||
|
$httpClient = $userAgentOrHttpClient->getHttpClient();
|
||||||
|
else
|
||||||
|
$httpClient = $userAgentOrHttpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->httpClient = new AuthorizedHttpClient(
|
||||||
|
$httpClient,
|
||||||
|
$this->credentials
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the OAuth2 API client portion.
|
||||||
|
*
|
||||||
|
* @return OAuth2Client
|
||||||
|
*/
|
||||||
|
public function oauth2(): OAuth2Client {
|
||||||
|
$this->oauth2 ??= new OAuth2Client(
|
||||||
|
$this,
|
||||||
|
$this->httpClient,
|
||||||
|
$this->credentials,
|
||||||
|
$this->urls
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->oauth2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fork this API client with different credentials.
|
||||||
|
*
|
||||||
|
* @param Credentials $credentials Credentials to create a new client with.
|
||||||
|
* @return FlashiiClient
|
||||||
|
*/
|
||||||
|
public function fork(Credentials $credentials): self {
|
||||||
|
return new FlashiiClient(
|
||||||
|
$this->httpClient,
|
||||||
|
$credentials,
|
||||||
|
$this->urls
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forks the current Flashii API client with a bearer access token.
|
||||||
|
*
|
||||||
|
* Given that refresh tokens have to be used with app authorization, it probably makes sense to have this around.
|
||||||
|
*
|
||||||
|
* @param string $accessToken Bearer access token.
|
||||||
|
* @return FlashiiClient
|
||||||
|
*/
|
||||||
|
public function forkWithAccessToken(
|
||||||
|
#[\SensitiveParameter]
|
||||||
|
string $accessToken
|
||||||
|
): self {
|
||||||
|
return $this->fork(new BearerCredentials($accessToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an anonymous Flashii API client.
|
||||||
|
*
|
||||||
|
* @param string $userAgent Application user agent.
|
||||||
|
* @return FlashiiClient
|
||||||
|
*/
|
||||||
|
public static function anonymous(string $userAgent = ''): self {
|
||||||
|
return new FlashiiClient($userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an app authenticated Flashii API client.
|
||||||
|
*
|
||||||
|
* @param string $clientId OAuth2 application client id.
|
||||||
|
* @param string $clientSecret OAuth2 application client secret.
|
||||||
|
* @param string $userAgent Application user agent.
|
||||||
|
* @return FlashiiClient
|
||||||
|
*/
|
||||||
|
public static function withClientId(
|
||||||
|
string $clientId,
|
||||||
|
#[\SensitiveParameter]
|
||||||
|
string $clientSecret = '',
|
||||||
|
string $userAgent = ''
|
||||||
|
): self {
|
||||||
|
return new FlashiiClient(
|
||||||
|
$userAgent,
|
||||||
|
new BasicCredentials($clientId, $clientSecret)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a bearer authenticated Flashii API client.
|
||||||
|
*
|
||||||
|
* @param string $accessToken Bearer access token.
|
||||||
|
* @param string $userAgent Application user agent.
|
||||||
|
* @return FlashiiClient
|
||||||
|
*/
|
||||||
|
public static function withAccessToken(
|
||||||
|
#[\SensitiveParameter]
|
||||||
|
string $accessToken,
|
||||||
|
string $userAgent = ''
|
||||||
|
): self {
|
||||||
|
return new FlashiiClient(
|
||||||
|
$userAgent,
|
||||||
|
new BearerCredentials($accessToken)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Misuzu authenticated Flashii API client.
|
||||||
|
*
|
||||||
|
* Do not use this unless you're flashwave!!!!!!! bad!!!!!!!!
|
||||||
|
*
|
||||||
|
* @param string $sessionToken Misuzu session token.
|
||||||
|
* @param string $userAgent Application user agent.
|
||||||
|
* @return FlashiiClient
|
||||||
|
*/
|
||||||
|
public static function withMisuzuToken(
|
||||||
|
#[\SensitiveParameter]
|
||||||
|
string $sessionToken,
|
||||||
|
string $userAgent = ''
|
||||||
|
): self {
|
||||||
|
return new FlashiiClient(
|
||||||
|
$userAgent,
|
||||||
|
new MisuzuCredentials($sessionToken)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a HTTP client instance.
|
||||||
|
*
|
||||||
|
* Will use the cURL implementation if the extension is loaded, otherwise it will fall back to PHP streams.
|
||||||
|
*
|
||||||
|
* @param string $userAgent User-Agent string to use in requests.
|
||||||
|
* @return HttpClient
|
||||||
|
*/
|
||||||
|
public static function createHttpClient(string $userAgent): HttpClient {
|
||||||
|
$userAgent = trim(sprintf('%s %s', trim($userAgent), self::userAgentString()));
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(extension_loaded('curl'))
|
||||||
|
return new CurlHttpClient($userAgent);
|
||||||
|
} catch(RuntimeException $ex) {
|
||||||
|
// fall through to the stream implementation if cURL fails to initialise for some reason
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StreamHttpClient($userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the User-Agent string for the client library.
|
||||||
|
*
|
||||||
|
* If you're writing a custom HttpClient, please make sure to include this!
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function userAgentString(): string {
|
||||||
|
return sprintf('ApiiPHP/%s', self::version());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current version of the Flashii API client library.
|
||||||
|
*
|
||||||
|
* Reads the VERSION file in the root of the Flashii directory.
|
||||||
|
* Returns 0.0.0 if reading the file failed for any reason.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function version(): string {
|
||||||
|
$version = file_get_contents(self::LIB_PATH_VERSION);
|
||||||
|
if($version === false || trim($version) === '')
|
||||||
|
return '0.0.0';
|
||||||
|
|
||||||
|
return trim($version);
|
||||||
|
}
|
||||||
|
}
|
85
src/FlashiiUrls.php
Normal file
85
src/FlashiiUrls.php
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
// FlashiiUrls.php
|
||||||
|
// Created: 2024-11-16
|
||||||
|
// Updated: 2024-11-16
|
||||||
|
|
||||||
|
namespace Flashii;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the base URLs the client has to be aware of to use the API.
|
||||||
|
*/
|
||||||
|
final class FlashiiUrls {
|
||||||
|
/**
|
||||||
|
* Production API base URL.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public const PROD_API_URL = 'https://api.flashii.net';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Production ID service base URL.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public const PROD_ID_URL = 'https://id.flashii.net';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $apiUrl API base URL, without trailing /.
|
||||||
|
* @param string $idUrl ID service base URL, without trailing /.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private string $apiUrl,
|
||||||
|
private string $idUrl
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $url
|
||||||
|
* @param string $path
|
||||||
|
* @param array<string, mixed> $args
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private static function buildUrl(string $url, string $path, array $args): string {
|
||||||
|
$url .= $path;
|
||||||
|
|
||||||
|
if(!empty($args)) {
|
||||||
|
$url .= str_contains($url, '?') ? '&' : '?';
|
||||||
|
$url .= http_build_query($args, encoding_type: PHP_QUERY_RFC3986);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the API base URL.
|
||||||
|
*
|
||||||
|
* @param string $path Path to append to the URL.
|
||||||
|
* @param array<string, mixed> $args Query arguments to append to the URL.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getApiUrl(string $path = '', array $args = []): string {
|
||||||
|
return self::buildUrl($this->apiUrl, $path, $args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the ID service base URL.
|
||||||
|
*
|
||||||
|
* @param string $path Path to append to the URL.
|
||||||
|
* @param array<string, mixed> $args Query arguments to append to the URL.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getIdUrl(string $path = '', array $args = []): string {
|
||||||
|
return self::buildUrl($this->idUrl, $path, $args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance with the production URLs preset.
|
||||||
|
*
|
||||||
|
* @return FlashiiUrls
|
||||||
|
*/
|
||||||
|
public static function production(): self {
|
||||||
|
return new static(
|
||||||
|
self::PROD_API_URL,
|
||||||
|
self::PROD_ID_URL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
83
src/Http/Curl/CurlHttpClient.php
Normal file
83
src/Http/Curl/CurlHttpClient.php
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
// CurlHttpClient.php
|
||||||
|
// Created: 2024-11-16
|
||||||
|
// Updated: 2024-11-16
|
||||||
|
|
||||||
|
namespace Flashii\Http\Curl;
|
||||||
|
|
||||||
|
use CurlHandle;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use RuntimeException;
|
||||||
|
use Flashii\Http\{HttpClient,HttpRequest,HttpResponse};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* cURL extension backed HTTP client implementation.
|
||||||
|
*/
|
||||||
|
class CurlHttpClient implements HttpClient {
|
||||||
|
private CurlHandle $handle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $userAgent User-Agent string.
|
||||||
|
* @throws RuntimeException If cURL initialisation failed.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private string $userAgent
|
||||||
|
) {
|
||||||
|
$handle = curl_init();
|
||||||
|
if($handle === false)
|
||||||
|
throw new RuntimeException('Failed to initialise cURL.');
|
||||||
|
|
||||||
|
$version = curl_version();
|
||||||
|
if(!is_array($version) || !array_key_exists('version', $version) || !is_string($version['version']))
|
||||||
|
throw new RuntimeException('Failed to retrieve cURL version info.');
|
||||||
|
|
||||||
|
$this->handle = $handle;
|
||||||
|
$this->userAgent = trim($userAgent) . sprintf(' cURL/%s', $version['version']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __destruct() {
|
||||||
|
curl_close($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createRequest(string $method, string $url): HttpRequest {
|
||||||
|
return new CurlHttpRequest($this->userAgent, $method, $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendRequest(HttpRequest $request): HttpResponse {
|
||||||
|
if(!($request instanceof CurlHttpRequest))
|
||||||
|
throw new InvalidArgumentException('Type of $request is not understood by this HttpClient.');
|
||||||
|
|
||||||
|
curl_reset($this->handle);
|
||||||
|
curl_setopt_array($this->handle, $request->getOpts());
|
||||||
|
|
||||||
|
$response = curl_exec($this->handle);
|
||||||
|
if($response === false)
|
||||||
|
throw new RuntimeException(curl_error($this->handle), curl_errno($this->handle));
|
||||||
|
if($response === true)
|
||||||
|
throw new RuntimeException('Request executed successfully but cURL returned no data???');
|
||||||
|
|
||||||
|
$parts = explode("\r\n\r\n", $response, 2);
|
||||||
|
|
||||||
|
$headerLines = array_reverse(explode("\r\n", $parts[0]));
|
||||||
|
$statusLine = array_pop($headerLines);
|
||||||
|
if(!is_string($statusLine))
|
||||||
|
throw new RuntimeException('Unable to read status header.');
|
||||||
|
|
||||||
|
$statusLineScan = sscanf($statusLine, 'HTTP/%s %d %[^\t\r\n]');
|
||||||
|
if($statusLineScan === null)
|
||||||
|
throw new RuntimeException('Failed to decode HTTP status line.');
|
||||||
|
[$_, $statusCode, $statusLine] = $statusLineScan;
|
||||||
|
|
||||||
|
$headers = [];
|
||||||
|
while(($headerLine = array_pop($headerLines)) !== null) {
|
||||||
|
$headerLine = trim((string)$headerLine);
|
||||||
|
if($headerLine === '')
|
||||||
|
continue;
|
||||||
|
|
||||||
|
$headerParts = explode(':', $headerLine, 2);
|
||||||
|
$headers[strtolower(trim($headerParts[0]))] = count($headerParts) > 1 ? trim($headerParts[1]) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CurlHttpResponse($statusCode ?? 0, $statusLine ?? '', $headers, count($parts) > 1 ? $parts[1] : '');
|
||||||
|
}
|
||||||
|
}
|
73
src/Http/Curl/CurlHttpRequest.php
Normal file
73
src/Http/Curl/CurlHttpRequest.php
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
// CurlHttpRequest.php
|
||||||
|
// Created: 2024-11-16
|
||||||
|
// Updated: 2024-11-16
|
||||||
|
|
||||||
|
namespace Flashii\Http\Curl;
|
||||||
|
|
||||||
|
use Flashii\Http\HttpRequest;
|
||||||
|
/**
|
||||||
|
* Provides a cURL extension backed HTTP request implementation.
|
||||||
|
*/
|
||||||
|
class CurlHttpRequest implements HttpRequest {
|
||||||
|
/** @var array<string, string> */
|
||||||
|
private array $headers = [];
|
||||||
|
|
||||||
|
private ?string $body = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $userAgent User-Agent string.
|
||||||
|
* @param string $method Request method.
|
||||||
|
* @param string $url Request URI.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private string $userAgent,
|
||||||
|
private string $method,
|
||||||
|
private string $url
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function setHeader(string $name, string $value): void {
|
||||||
|
$this->headers[strtolower($name)] = sprintf('%s: %s', $name, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeHeader(string $name): void {
|
||||||
|
unset($this->headers[strtolower($name)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setBody(string $body, string $contentType = 'application/octet-stream'): void {
|
||||||
|
$this->setHeader('content-type', $contentType);
|
||||||
|
$this->body = $body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setBodyParams(array $params): void {
|
||||||
|
$this->setBody(
|
||||||
|
http_build_query($params, encoding_type: PHP_QUERY_RFC3986),
|
||||||
|
'application/x-www-form-urlencoded'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts this request to an array of CURLOPT_* settings.
|
||||||
|
*
|
||||||
|
* @return array<int, mixed>
|
||||||
|
*/
|
||||||
|
public function getOpts(): array {
|
||||||
|
return [
|
||||||
|
CURLOPT_AUTOREFERER => false,
|
||||||
|
CURLOPT_CUSTOMREQUEST => $this->method,
|
||||||
|
CURLOPT_FAILONERROR => false,
|
||||||
|
CURLOPT_FOLLOWLOCATION => true,
|
||||||
|
CURLOPT_HEADER => true,
|
||||||
|
CURLOPT_HTTPHEADER => $this->headers,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TCP_FASTOPEN => true,
|
||||||
|
CURLOPT_CONNECTTIMEOUT => 2,
|
||||||
|
CURLOPT_MAXREDIRS => 5,
|
||||||
|
CURLOPT_POSTFIELDS => $this->body,
|
||||||
|
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
|
||||||
|
CURLOPT_TIMEOUT => 5,
|
||||||
|
CURLOPT_URL => $this->url,
|
||||||
|
CURLOPT_USERAGENT => $this->userAgent,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
55
src/Http/Curl/CurlHttpResponse.php
Normal file
55
src/Http/Curl/CurlHttpResponse.php
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
// CurlHttpResponse.php
|
||||||
|
// Created: 2024-11-16
|
||||||
|
// Updated: 2024-11-16
|
||||||
|
|
||||||
|
namespace Flashii\Http\Curl;
|
||||||
|
|
||||||
|
use Flashii\Http\{HttpResponse,JsonBody,SuccessStatusCode};
|
||||||
|
/**
|
||||||
|
* cURL extension backed HTTP response.
|
||||||
|
*/
|
||||||
|
class CurlHttpResponse implements HttpResponse {
|
||||||
|
use JsonBody, SuccessStatusCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $statusCode HTTP status code.
|
||||||
|
* @param string $statusText HTTP status text.
|
||||||
|
* @param array<string, string> $headers HTTP response headers.
|
||||||
|
* @param string $body HTTP body.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private int $statusCode,
|
||||||
|
private string $statusText,
|
||||||
|
private array $headers,
|
||||||
|
private string $body
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getStatusCode(): int {
|
||||||
|
return $this->statusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatusText(): string {
|
||||||
|
return $this->statusText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHeaders(): array {
|
||||||
|
return $this->headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasHeader(string $name): bool {
|
||||||
|
return array_key_exists(strtolower($name), $this->headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHeader(string $name): string {
|
||||||
|
return $this->headers[strtolower($name)] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasBody(): bool {
|
||||||
|
return $this->body !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBody(): string {
|
||||||
|
return $this->body;
|
||||||
|
}
|
||||||
|
}
|
34
src/Http/HttpClient.php
Normal file
34
src/Http/HttpClient.php
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
// HttpClient.php
|
||||||
|
// Created: 2024-11-16
|
||||||
|
// Updated: 2024-11-16
|
||||||
|
|
||||||
|
namespace Flashii\Http;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a common interface for making HTTP clients.
|
||||||
|
*/
|
||||||
|
interface HttpClient {
|
||||||
|
/**
|
||||||
|
* Creates a HTTP request object.
|
||||||
|
*
|
||||||
|
* @param string $method Desired request method.
|
||||||
|
* @param string $url Target URL.
|
||||||
|
* @throws RuntimeException If the request could not be initialised.
|
||||||
|
* @return HttpRequest
|
||||||
|
*/
|
||||||
|
function createRequest(string $method, string $url): HttpRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a HTTP request and returns a HTTP response.
|
||||||
|
*
|
||||||
|
* @param HttpRequest $request Request to send.
|
||||||
|
* @throws InvalidArgumentException If $request is not understood by the HttpClient implementation.
|
||||||
|
* @throws RuntimeException If the HTTP request failed in an unexpected way.
|
||||||
|
* @return HttpResponse
|
||||||
|
*/
|
||||||
|
function sendRequest(HttpRequest $request): HttpResponse;
|
||||||
|
}
|
41
src/Http/HttpRequest.php
Normal file
41
src/Http/HttpRequest.php
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
// HttpRequest.php
|
||||||
|
// Created: 2024-11-16
|
||||||
|
// Updated: 2024-11-16
|
||||||
|
|
||||||
|
namespace Flashii\Http;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a common interface for HTTP requests.
|
||||||
|
*/
|
||||||
|
interface HttpRequest {
|
||||||
|
/**
|
||||||
|
* Sets a header value.
|
||||||
|
*
|
||||||
|
* @param string $name Header name.
|
||||||
|
* @param string $value Header value.
|
||||||
|
*/
|
||||||
|
function setHeader(string $name, string $value): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a header value.
|
||||||
|
*
|
||||||
|
* @param string $name.
|
||||||
|
*/
|
||||||
|
function removeHeader(string $name): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a raw body with a provided media type.
|
||||||
|
*
|
||||||
|
* @param string $body Request body.
|
||||||
|
* @param string $contentType Request body content type.
|
||||||
|
*/
|
||||||
|
function setBody(string $body, string $contentType = 'application/octet-stream'): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a urlencoded form request body.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $params Form parameters.
|
||||||
|
*/
|
||||||
|
function setBodyParams(array $params): void;
|
||||||
|
}
|
85
src/Http/HttpResponse.php
Normal file
85
src/Http/HttpResponse.php
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
// HttpResponse.php
|
||||||
|
// Created: 2024-11-16
|
||||||
|
// Updated: 2024-11-16
|
||||||
|
|
||||||
|
namespace Flashii\Http;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
/**
|
||||||
|
* Provides a common interface for HTTP responses.
|
||||||
|
*/
|
||||||
|
interface HttpResponse {
|
||||||
|
/**
|
||||||
|
* Returns the status code.
|
||||||
|
*
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
function getStatusCode(): int;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the status text.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function getStatusText(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the status code indicates success (200-299).
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
function isSuccessStatusCode(): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws an exception if isSuccessStatusCode was false.
|
||||||
|
*
|
||||||
|
* @throws RuntimeException if the status code did not indicate success.
|
||||||
|
*/
|
||||||
|
function ensureSuccessStatusCode(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all response headers.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
function getHeaders(): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a response header is present.
|
||||||
|
*
|
||||||
|
* @param string $name Name of the header.
|
||||||
|
* @return bool true if it is present, false if not.
|
||||||
|
*/
|
||||||
|
function hasHeader(string $name): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value of a response header.
|
||||||
|
*
|
||||||
|
* @param string $name Name of the header.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function getHeader(string $name): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the response has a body.
|
||||||
|
*
|
||||||
|
* @return bool true if it has a body, false if not.
|
||||||
|
*/
|
||||||
|
function hasBody(): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the body of this response.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function getBody(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to json_decode the body string.
|
||||||
|
*
|
||||||
|
* @param bool $associative true to return JSON objects as PHP arrays instead of PHP objects.
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
function getJsonBody(bool $associative = true): mixed;
|
||||||
|
}
|
22
src/Http/JsonBody.php
Normal file
22
src/Http/JsonBody.php
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
// JsonBody.php
|
||||||
|
// Created: 2024-11-16
|
||||||
|
// Updated: 2024-11-16
|
||||||
|
|
||||||
|
namespace Flashii\Http;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
/**
|
||||||
|
* Common implementation for the getJsonBody method of HttpResponse.
|
||||||
|
*/
|
||||||
|
trait JsonBody {
|
||||||
|
/**
|
||||||
|
* Attempts to json_decode the body string.
|
||||||
|
*
|
||||||
|
* @param bool $associative true to return JSON objects as PHP arrays instead of PHP objects.
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getJsonBody(bool $associative = false): mixed {
|
||||||
|
return json_decode($this->getBody(), $associative, 512, JSON_INVALID_UTF8_SUBSTITUTE);
|
||||||
|
}
|
||||||
|
}
|
73
src/Http/Stream/StreamHttpClient.php
Normal file
73
src/Http/Stream/StreamHttpClient.php
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
// StreamHttpClient.php
|
||||||
|
// Created: 2024-11-16
|
||||||
|
// Updated: 2024-11-16
|
||||||
|
|
||||||
|
namespace Flashii\Http\Stream;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use RuntimeException;
|
||||||
|
use Flashii\Http\{HttpClient,HttpRequest,HttpResponse};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PHP stream backed HTTP client implementation.
|
||||||
|
*/
|
||||||
|
class StreamHttpClient implements HttpClient {
|
||||||
|
/**
|
||||||
|
* @param string $userAgent User-Agent string.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private string $userAgent
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function createRequest(string $method, string $url): HttpRequest {
|
||||||
|
return new StreamHttpRequest($this->userAgent, $method, $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sendRequest(HttpRequest $request): HttpResponse {
|
||||||
|
if(!($request instanceof StreamHttpRequest))
|
||||||
|
throw new InvalidArgumentException('Type of $request is not understood by this HttpClient.');
|
||||||
|
|
||||||
|
$ctx = stream_context_create($request->getStreamOptions());
|
||||||
|
$handle = fopen($request->getUrl(), 'rb', context: $ctx);
|
||||||
|
if($handle === false)
|
||||||
|
throw new RuntimeException('Failed create HTTP request.');
|
||||||
|
|
||||||
|
$metaData = stream_get_meta_data($handle);
|
||||||
|
if(!array_key_exists('wrapper_data', $metaData) || !is_array($metaData['wrapper_data']))
|
||||||
|
throw new RuntimeException('wrapper_data missing from stream metadata.');
|
||||||
|
|
||||||
|
$wrapperData = array_reverse($metaData['wrapper_data']);
|
||||||
|
|
||||||
|
$statusLine = array_pop($wrapperData);
|
||||||
|
if(!is_string($statusLine))
|
||||||
|
throw new RuntimeException('Unable to read status header.');
|
||||||
|
|
||||||
|
$statusLineScan = sscanf($statusLine, 'HTTP/%s %d %[^\t\r\n]');
|
||||||
|
if($statusLineScan === null)
|
||||||
|
throw new RuntimeException('Failed to decode HTTP status line.');
|
||||||
|
[$_, $statusCode, $statusLine] = $statusLineScan;
|
||||||
|
|
||||||
|
$headers = [];
|
||||||
|
while(($headerLine = array_pop($wrapperData)) !== null) {
|
||||||
|
if(!is_string($headerLine)) {
|
||||||
|
if(is_scalar($headerLine))
|
||||||
|
$headerLine = (string)$headerLine;
|
||||||
|
else continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$headerLine = trim($headerLine);
|
||||||
|
if($headerLine === '')
|
||||||
|
continue;
|
||||||
|
|
||||||
|
$headerParts = explode(':', $headerLine, 2);
|
||||||
|
$headers[strtolower(trim($headerParts[0]))] = count($headerParts) > 1 ? trim($headerParts[1]) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = stream_get_contents($handle);
|
||||||
|
if($body === false)
|
||||||
|
throw new RuntimeException('Request executed successfully but failed to retrieve data.');
|
||||||
|
|
||||||
|
return new StreamHttpResponse($statusCode ?? 0, $statusLine ?? '', $headers, $body);
|
||||||
|
}
|
||||||
|
}
|
88
src/Http/Stream/StreamHttpRequest.php
Normal file
88
src/Http/Stream/StreamHttpRequest.php
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
// StreamHttpRequest.php
|
||||||
|
// Created: 2024-11-16
|
||||||
|
// Updated: 2024-11-16
|
||||||
|
|
||||||
|
namespace Flashii\Http\Stream;
|
||||||
|
|
||||||
|
use Flashii\Http\HttpRequest;
|
||||||
|
/**
|
||||||
|
* Provides a builder for PHP stream backed HTTP requests.
|
||||||
|
*/
|
||||||
|
class StreamHttpRequest implements HttpRequest {
|
||||||
|
/** @var array<string, string> */
|
||||||
|
private array $headers = [];
|
||||||
|
|
||||||
|
private ?string $body = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $userAgent User-Agent string.
|
||||||
|
* @param string $method Request method.
|
||||||
|
* @param string $url Request URI.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private string $userAgent,
|
||||||
|
private string $method,
|
||||||
|
private string $url
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function setHeader(string $name, string $value): void {
|
||||||
|
$this->headers[strtolower($name)] = sprintf('%s: %s', $name, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeHeader(string $name): void {
|
||||||
|
unset($this->headers[strtolower($name)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setBody(string $body, string $contentType = 'application/octet-stream'): void {
|
||||||
|
$this->setHeader('content-type', $contentType);
|
||||||
|
$this->body = $body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setBodyParams(array $params): void {
|
||||||
|
$this->setBody(
|
||||||
|
http_build_query($params, encoding_type: PHP_QUERY_RFC3986),
|
||||||
|
'application/x-www-form-urlencoded'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the request URI.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getUrl(): string {
|
||||||
|
return $this->url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts this request to an array that can be passed to stream_context_create.
|
||||||
|
*
|
||||||
|
* @return array{'http': array{
|
||||||
|
* method: string,
|
||||||
|
* header: array<string, string>,
|
||||||
|
* user_agent: string,
|
||||||
|
* content: ?string,
|
||||||
|
* follow_location: int,
|
||||||
|
* max_redirects: int,
|
||||||
|
* protocol_version: string,
|
||||||
|
* timeout: float,
|
||||||
|
* ignore_errors: bool
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
public function getStreamOptions(): array {
|
||||||
|
return [
|
||||||
|
'http' => [
|
||||||
|
'method' => $this->method,
|
||||||
|
'header' => $this->headers,
|
||||||
|
'user_agent' => $this->userAgent,
|
||||||
|
'content' => $this->body,
|
||||||
|
'follow_location' => 1,
|
||||||
|
'max_redirects' => 5,
|
||||||
|
'protocol_version' => '1.1',
|
||||||
|
'timeout' => 7,
|
||||||
|
'ignore_errors' => true,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
55
src/Http/Stream/StreamHttpResponse.php
Normal file
55
src/Http/Stream/StreamHttpResponse.php
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
// StreamHttpResponse.php
|
||||||
|
// Created: 2024-11-16
|
||||||
|
// Updated: 2024-11-16
|
||||||
|
|
||||||
|
namespace Flashii\Http\Stream;
|
||||||
|
|
||||||
|
use Flashii\Http\{HttpResponse,JsonBody,SuccessStatusCode};
|
||||||
|
/**
|
||||||
|
* PHP stream backed HTTP response.
|
||||||
|
*/
|
||||||
|
class StreamHttpResponse implements HttpResponse {
|
||||||
|
use JsonBody, SuccessStatusCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $statusCode HTTP status code.
|
||||||
|
* @param string $statusText HTTP status text.
|
||||||
|
* @param array<string, string> $headers HTTP response headers.
|
||||||
|
* @param string $body HTTP body.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private int $statusCode,
|
||||||
|
private string $statusText,
|
||||||
|
private array $headers,
|
||||||
|
private string $body
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getStatusCode(): int {
|
||||||
|
return $this->statusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatusText(): string {
|
||||||
|
return $this->statusText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHeaders(): array {
|
||||||
|
return $this->headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasHeader(string $name): bool {
|
||||||
|
return array_key_exists(strtolower($name), $this->headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHeader(string $name): string {
|
||||||
|
return $this->headers[strtolower($name)] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasBody(): bool {
|
||||||
|
return $this->body !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBody(): string {
|
||||||
|
return $this->body;
|
||||||
|
}
|
||||||
|
}
|
32
src/Http/SuccessStatusCode.php
Normal file
32
src/Http/SuccessStatusCode.php
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
// SuccessStatusCode.php
|
||||||
|
// Created: 2024-11-16
|
||||||
|
// Updated: 2024-11-16
|
||||||
|
|
||||||
|
namespace Flashii\Http;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
/**
|
||||||
|
* Common implementation for the SuccessStatusCode methods of HttpResponse.
|
||||||
|
*/
|
||||||
|
trait SuccessStatusCode {
|
||||||
|
/**
|
||||||
|
* Returns true if the status code indicates success (200-299).
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isSuccessStatusCode(): bool {
|
||||||
|
$statusCode = $this->getStatusCode();
|
||||||
|
return $statusCode >= 200 && $statusCode <= 299;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws an exception if isSuccessStatusCode was false.
|
||||||
|
*
|
||||||
|
* @throws RuntimeException if the status code did not indicate success.
|
||||||
|
*/
|
||||||
|
public function ensureSuccessStatusCode(): void {
|
||||||
|
if(!$this->isSuccessStatusCode())
|
||||||
|
throw new RuntimeException(sprintf('HTTP request returned status code %03d.', $this->getStatusCode()));
|
||||||
|
}
|
||||||
|
}
|
69
src/OAuth2/OAuth2AuthorizationRequest.php
Normal file
69
src/OAuth2/OAuth2AuthorizationRequest.php
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
// OAuth2AuthorizationRequest.php
|
||||||
|
// Created: 2024-11-16
|
||||||
|
// Updated: 2024-11-16
|
||||||
|
|
||||||
|
namespace Flashii\OAuth2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAuth2 authorisation request response.
|
||||||
|
*/
|
||||||
|
final class OAuth2AuthorizationRequest {
|
||||||
|
/**
|
||||||
|
* @param string $deviceCode Authorisation request code.
|
||||||
|
* @param string $userCode Code for user entry.
|
||||||
|
* @param string $verificationUri Verification URI sans code.
|
||||||
|
* @param string $verificationUriComplete Verification URI but with code.
|
||||||
|
* @param int $expiresIn Amount of seconds until the thing expires.
|
||||||
|
* @param int $interval Interval of seconds to check at.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private string $deviceCode,
|
||||||
|
private string $userCode,
|
||||||
|
private string $verificationUri,
|
||||||
|
private string $verificationUriComplete,
|
||||||
|
private int $expiresIn,
|
||||||
|
private int $interval
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getDeviceCode(): string {
|
||||||
|
return $this->deviceCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUserCode(): string {
|
||||||
|
return $this->userCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVerificationUri(): string {
|
||||||
|
return $this->verificationUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVerificationUriComplete(): string {
|
||||||
|
return $this->verificationUriComplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExpiresIn(): int {
|
||||||
|
return $this->expiresIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getInterval(): int {
|
||||||
|
return $this->interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes an object into this proper object.
|
||||||
|
*
|
||||||
|
* @param object $info
|
||||||
|
* @return OAuth2AuthorizationRequest
|
||||||
|
*/
|
||||||
|
public static function decode(object $info): self {
|
||||||
|
return new static(
|
||||||
|
isset($info->device_code) && is_string($info->device_code) ? $info->device_code : '',
|
||||||
|
isset($info->user_code) && is_string($info->user_code) ? $info->user_code : '',
|
||||||
|
isset($info->verification_uri) && is_string($info->verification_uri) ? $info->verification_uri : '',
|
||||||
|
isset($info->verification_uri_complete) && is_string($info->verification_uri_complete) ? $info->verification_uri_complete : '',
|
||||||
|
isset($info->expires_in) && is_int($info->expires_in) ? $info->expires_in : 600,
|
||||||
|
isset($info->interval) && is_int($info->interval) ? $info->interval : 5
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
273
src/OAuth2/OAuth2Client.php
Normal file
273
src/OAuth2/OAuth2Client.php
Normal file
|
@ -0,0 +1,273 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
// OAuth2Client.php
|
||||||
|
// Created: 2024-11-16
|
||||||
|
// Updated: 2024-11-16
|
||||||
|
|
||||||
|
namespace Flashii\OAuth2;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use Flashii\{AuthorizedHttpClient,FlashiiClient,FlashiiUrls,UriBase64};
|
||||||
|
use Flashii\Credentials\{BasicCredentials,Credentials};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements the OAuth2 portions of the API.
|
||||||
|
*/
|
||||||
|
class OAuth2Client {
|
||||||
|
/**
|
||||||
|
* @param FlashiiClient $flashiiClient Parent client instance to fork upon successful auth.
|
||||||
|
* @param AuthorizedHttpClient $httpClient Authorized HTTP client instance.
|
||||||
|
* @param Credentials $credentials API credentials.
|
||||||
|
* @param FlashiiUrls $urls API URLs.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private FlashiiClient $flashiiClient,
|
||||||
|
private AuthorizedHttpClient $httpClient,
|
||||||
|
private Credentials $credentials,
|
||||||
|
private FlashiiUrls $urls
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an OAuth2 authorisation URL to redirect the user to.
|
||||||
|
*
|
||||||
|
* @param string $codeVerifier Code verifier to hash and encode as a code challenge.
|
||||||
|
* @param ?string $state State variable, null to omit.
|
||||||
|
* @param ?string $scope Scope to request, null to omit.
|
||||||
|
* @param ?string $redirectUri URI to redirect to, required if the app has more than one redirect URI registered.
|
||||||
|
* @param ?string $clientId Client ID of the app attempting to authorise.
|
||||||
|
* @param array<string, mixed> $args Additional arguments to add to the URL.
|
||||||
|
* @throws InvalidArgumentException If the Credentials instance passed to the constructor is not an instance of BasicCredentials and $clientId was left as null.
|
||||||
|
* @return string Authorisation URL that the user can be redirected to.
|
||||||
|
*/
|
||||||
|
public function createAuthorizeUrl(
|
||||||
|
string $codeVerifier,
|
||||||
|
?string $state = null,
|
||||||
|
?string $scope = null,
|
||||||
|
?string $redirectUri = null,
|
||||||
|
?string $clientId = null,
|
||||||
|
array $args = []
|
||||||
|
): string {
|
||||||
|
if($clientId === null) {
|
||||||
|
if($this->credentials instanceof BasicCredentials)
|
||||||
|
$clientId = $this->credentials->getUserName();
|
||||||
|
else
|
||||||
|
throw new InvalidArgumentException('$clientId may not be null if $this->credentials is not an instance of BasicCredentials.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$args['response_type'] = 'code';
|
||||||
|
$args['client_id'] = $clientId;
|
||||||
|
$args['code_challenge'] = UriBase64::encode(hash('sha256', $codeVerifier, true));
|
||||||
|
$args['code_challenge_method'] = 'S256';
|
||||||
|
|
||||||
|
if($state !== null)
|
||||||
|
$args['state'] = $state;
|
||||||
|
if($scope !== null)
|
||||||
|
$args['scope'] = $scope;
|
||||||
|
if($redirectUri !== null)
|
||||||
|
$args['redirect_uri'] = $redirectUri;
|
||||||
|
|
||||||
|
return $this->urls->getIdUrl('/oauth2/authorise', $args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request device authorisation.
|
||||||
|
*
|
||||||
|
* @param ?string $scope Scope to request, null to omit.
|
||||||
|
* @param ?string $clientId Client ID, if Basic auth is impossible for some reason.
|
||||||
|
* @return OAuth2Error|OAuth2AuthorizationRequest
|
||||||
|
*/
|
||||||
|
public function requestAuthorise(
|
||||||
|
?string $scope = null,
|
||||||
|
?string $clientId = null
|
||||||
|
): OAuth2Error|OAuth2AuthorizationRequest {
|
||||||
|
$args = [];
|
||||||
|
|
||||||
|
if($scope !== null)
|
||||||
|
$args['scope'] = $scope;
|
||||||
|
|
||||||
|
$useAnonymous = false;
|
||||||
|
if($clientId !== null) {
|
||||||
|
$useAnonymous = true;
|
||||||
|
$args['client_id'] = $clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$request = $this->httpClient->createRequest('POST', '/oauth2/request-authorise');
|
||||||
|
$request->setBodyParams($args);
|
||||||
|
|
||||||
|
$response = $useAnonymous
|
||||||
|
? $this->httpClient->sendRequestAnonymous($request)
|
||||||
|
: $this->httpClient->sendRequest($request);
|
||||||
|
|
||||||
|
$body = $response->getJsonBody();
|
||||||
|
if(!is_object($body))
|
||||||
|
throw new RuntimeException('API response was not a JSON object.');
|
||||||
|
|
||||||
|
if(isset($body->error))
|
||||||
|
return OAuth2Error::decode($body);
|
||||||
|
|
||||||
|
return OAuth2AuthorizationRequest::decode($body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alias for requestAuthorise.
|
||||||
|
*
|
||||||
|
* @param ?string $scope See requestAuthorise documentation.
|
||||||
|
* @param ?string $clientId See requestAuthorise documentation.
|
||||||
|
* @return OAuth2Error|OAuth2AuthorizationRequest
|
||||||
|
*/
|
||||||
|
public function requestAuthorize(
|
||||||
|
?string $scope = null,
|
||||||
|
?string $clientId = null
|
||||||
|
): OAuth2Error|OAuth2AuthorizationRequest {
|
||||||
|
return $this->requestAuthorise($scope, $clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to request an access token.
|
||||||
|
*
|
||||||
|
* @param string $grantType Grant type used to attempt to create an access token.
|
||||||
|
* @param array<string, string> $args Additional arguments for the grant.
|
||||||
|
* @param ?string $clientId Client ID, if Basic auth is impossible for some reason.
|
||||||
|
* @param ?string $clientSecret Client Secret, if Basic auth is impossible for some reason.
|
||||||
|
* @return OAuth2Error|OAuth2Token
|
||||||
|
*/
|
||||||
|
public function token(
|
||||||
|
string $grantType,
|
||||||
|
array $args,
|
||||||
|
?string $clientId = null,
|
||||||
|
#[\SensitiveParameter]
|
||||||
|
?string $clientSecret = null
|
||||||
|
): OAuth2Error|OAuth2Token {
|
||||||
|
$args['grant_type'] = $grantType;
|
||||||
|
|
||||||
|
$useAnonymous = false;
|
||||||
|
if($clientId !== null) {
|
||||||
|
$useAnonymous = true;
|
||||||
|
$args['client_id'] = $clientId;
|
||||||
|
if($clientSecret !== null)
|
||||||
|
$args['client_secret'] = $clientSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
$request = $this->httpClient->createRequest('POST', '/oauth2/token');
|
||||||
|
$request->setBodyParams($args);
|
||||||
|
|
||||||
|
$response = $useAnonymous
|
||||||
|
? $this->httpClient->sendRequestAnonymous($request)
|
||||||
|
: $this->httpClient->sendRequest($request);
|
||||||
|
|
||||||
|
$body = $response->getJsonBody();
|
||||||
|
if(!is_object($body))
|
||||||
|
throw new RuntimeException('API response was not a JSON object.');
|
||||||
|
|
||||||
|
if(isset($body->error))
|
||||||
|
return OAuth2Error::decode($body);
|
||||||
|
|
||||||
|
return OAuth2Token::decode($body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to request an access token using an authorization code.
|
||||||
|
*
|
||||||
|
* @param string $code Authorization code received from the client.
|
||||||
|
* @param string $codeVerifier Code verifier received from the client.
|
||||||
|
* @param ?string $clientId Client ID, if Basic auth is impossible for some reason.
|
||||||
|
* @param ?string $clientSecret Client Secret, if Basic auth is impossible for some reason.
|
||||||
|
* @return OAuth2Error|OAuth2Token
|
||||||
|
*/
|
||||||
|
public function tokenWithAuthorizationCode(
|
||||||
|
#[\SensitiveParameter]
|
||||||
|
string $code,
|
||||||
|
#[\SensitiveParameter]
|
||||||
|
string $codeVerifier,
|
||||||
|
?string $clientId = null,
|
||||||
|
#[\SensitiveParameter]
|
||||||
|
?string $clientSecret = null
|
||||||
|
): OAuth2Error|OAuth2Token {
|
||||||
|
return $this->token(
|
||||||
|
'authorization_code',
|
||||||
|
[
|
||||||
|
'code' => $code,
|
||||||
|
'code_verifier' => $codeVerifier,
|
||||||
|
],
|
||||||
|
$clientId,
|
||||||
|
$clientSecret
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to request a new access token using a refresh token.
|
||||||
|
*
|
||||||
|
* @param string $refreshToken Refresh token received in previous access token request.
|
||||||
|
* @param ?string $scope Adjusted scope, null to leave unchanged.
|
||||||
|
* @param ?string $clientId Client ID, if Basic auth is impossible for some reason.
|
||||||
|
* @param ?string $clientSecret Client Secret, if Basic auth is impossible for some reason.
|
||||||
|
* @return OAuth2Error|OAuth2Token
|
||||||
|
*/
|
||||||
|
public function tokenWithRefreshToken(
|
||||||
|
#[\SensitiveParameter]
|
||||||
|
string $refreshToken,
|
||||||
|
?string $scope = null,
|
||||||
|
?string $clientId = null,
|
||||||
|
#[\SensitiveParameter]
|
||||||
|
?string $clientSecret = null
|
||||||
|
): OAuth2Error|OAuth2Token {
|
||||||
|
$args = ['refresh_token' => $refreshToken];
|
||||||
|
if($scope !== null)
|
||||||
|
$args['scope'] = $scope;
|
||||||
|
|
||||||
|
return $this->token(
|
||||||
|
'refresh_token',
|
||||||
|
$args,
|
||||||
|
$clientId,
|
||||||
|
$clientSecret
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to request an app access token.
|
||||||
|
*
|
||||||
|
* @param ?string $scope Desired scope.
|
||||||
|
* @param ?string $clientId Client ID, if Basic auth is impossible for some reason.
|
||||||
|
* @param ?string $clientSecret Client Secret, if Basic auth is impossible for some reason.
|
||||||
|
* @return OAuth2Error|OAuth2Token
|
||||||
|
*/
|
||||||
|
public function tokenWithClientCredentials(
|
||||||
|
?string $scope = null,
|
||||||
|
?string $clientId = null,
|
||||||
|
#[\SensitiveParameter]
|
||||||
|
?string $clientSecret = null
|
||||||
|
): OAuth2Error|OAuth2Token {
|
||||||
|
$args = [];
|
||||||
|
if($scope !== null)
|
||||||
|
$args['scope'] = $scope;
|
||||||
|
|
||||||
|
return $this->token(
|
||||||
|
'client_credentials',
|
||||||
|
$args,
|
||||||
|
$clientId,
|
||||||
|
$clientSecret
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to request an access token using a device code.
|
||||||
|
*
|
||||||
|
* @param string $deviceCode Device code.
|
||||||
|
* @param ?string $clientId Client ID, if Basic auth is impossible for some reason.
|
||||||
|
* @param ?string $clientSecret Client Secret, if Basic auth is impossible for some reason.
|
||||||
|
* @return OAuth2Error|OAuth2Token
|
||||||
|
*/
|
||||||
|
public function tokenWithDeviceCode(
|
||||||
|
string $deviceCode,
|
||||||
|
?string $clientId = null,
|
||||||
|
#[\SensitiveParameter]
|
||||||
|
?string $clientSecret = null
|
||||||
|
): OAuth2Error|OAuth2Token {
|
||||||
|
return $this->token(
|
||||||
|
'urn:ietf:params:oauth:grant-type:device_code',
|
||||||
|
['device_code' => $deviceCode],
|
||||||
|
$clientId,
|
||||||
|
$clientSecret
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
63
src/OAuth2/OAuth2Error.php
Normal file
63
src/OAuth2/OAuth2Error.php
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
// OAuth2Error.php
|
||||||
|
// Created: 2024-11-16
|
||||||
|
// Updated: 2024-11-16
|
||||||
|
|
||||||
|
namespace Flashii\OAuth2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAuth2 error response.
|
||||||
|
*/
|
||||||
|
final class OAuth2Error {
|
||||||
|
/**
|
||||||
|
* @param string $error Error code.
|
||||||
|
* @param string $description Error description.
|
||||||
|
* @param string $uri Error URI.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private string $error,
|
||||||
|
private string $description,
|
||||||
|
private string $uri
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the OAuth2 error code.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getCode(): string {
|
||||||
|
return $this->error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the OAuth2 error description.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getDescription(): string {
|
||||||
|
return $this->description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the OAuth2 error documentation URI.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getUri(): string {
|
||||||
|
return $this->uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes an object into this proper object.
|
||||||
|
*
|
||||||
|
* @param object $info
|
||||||
|
* @return OAuth2Error
|
||||||
|
*/
|
||||||
|
public static function decode(object $info): self {
|
||||||
|
return new static(
|
||||||
|
isset($info->error) && is_string($info->error) ? $info->error : '',
|
||||||
|
isset($info->error_description) && is_string($info->error_description) ? $info->error_description : '',
|
||||||
|
isset($info->error_uri) && is_string($info->error_uri) ? $info->error_uri : ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
63
src/OAuth2/OAuth2Token.php
Normal file
63
src/OAuth2/OAuth2Token.php
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
// OAuth2Token.php
|
||||||
|
// Created: 2024-11-16
|
||||||
|
// Updated: 2024-11-16
|
||||||
|
|
||||||
|
namespace Flashii\OAuth2;
|
||||||
|
|
||||||
|
use Flashii\Credentials\BearerCredentials;
|
||||||
|
/**
|
||||||
|
* OAuth2 token response.
|
||||||
|
*/
|
||||||
|
final class OAuth2Token {
|
||||||
|
/**
|
||||||
|
* @param string $accessToken Access token.
|
||||||
|
* @param string $tokenType Token type.
|
||||||
|
* @param int $expiresIn Seconds until the token expires.
|
||||||
|
* @param string $scope Scope of the token.
|
||||||
|
* @param string $refreshToken Refresh token.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private string $accessToken,
|
||||||
|
private string $tokenType,
|
||||||
|
private int $expiresIn,
|
||||||
|
private string $scope,
|
||||||
|
private string $refreshToken
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getAccessToken(): string {
|
||||||
|
return $this->accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTokenType(): string {
|
||||||
|
return $this->tokenType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExpiresIn(): int {
|
||||||
|
return $this->expiresIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getScope(): string {
|
||||||
|
return $this->scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRefreshToken(): string {
|
||||||
|
return $this->refreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes an object into this proper object.
|
||||||
|
*
|
||||||
|
* @param object $info
|
||||||
|
* @return OAuth2Token
|
||||||
|
*/
|
||||||
|
public static function decode(object $info): self {
|
||||||
|
return new static(
|
||||||
|
isset($info->access_token) && is_string($info->access_token) ? $info->access_token : '',
|
||||||
|
isset($info->token_type) && is_string($info->token_type) ? $info->token_type : 'Bearer',
|
||||||
|
isset($info->expires_in) && is_int($info->expires_in) ? $info->expires_in : 3600,
|
||||||
|
isset($info->scope) && is_string($info->scope) ? $info->scope : '',
|
||||||
|
isset($info->refresh_token) && is_string($info->refresh_token) ? $info->refresh_token : ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
32
src/UriBase64.php
Normal file
32
src/UriBase64.php
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
// UriBase64.php
|
||||||
|
// Created: 2024-11-16
|
||||||
|
// Updated: 2024-11-16
|
||||||
|
|
||||||
|
namespace Flashii;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides URL-safe Base64 encoding.
|
||||||
|
*/
|
||||||
|
final class UriBase64 {
|
||||||
|
/**
|
||||||
|
* Encodes data with URI-safe MIME base64.
|
||||||
|
*
|
||||||
|
* @param string $string The data to encode.
|
||||||
|
* @return string The encoded data, as a string.
|
||||||
|
*/
|
||||||
|
public static function encode(string $string): string {
|
||||||
|
return rtrim(strtr(base64_encode($string), '+/', '-_'), '=');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes data encoded with URI-safe MIME base64.
|
||||||
|
*
|
||||||
|
* @param string $string The encoded data.
|
||||||
|
* @param bool $strict If the strict parameter is set to true then the base64_decode() function will return false if the input contains character from outside the base64 alphabet. Otherwise invalid characters will be silently discarded.
|
||||||
|
* @return string|false Returns the decoded data or false on failure. The returned data may be binary.
|
||||||
|
*/
|
||||||
|
public static function decode(string $string, bool $strict = false): string|false {
|
||||||
|
return base64_decode(str_pad(strtr($string, '-_', '+/'), strlen($string) % 4, '=', STR_PAD_RIGHT));
|
||||||
|
}
|
||||||
|
}
|
179
tests/CurlHttpTest.php
Normal file
179
tests/CurlHttpTest.php
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
// CurlHttpTest.php
|
||||||
|
// Created: 2024-11-16
|
||||||
|
// Updated: 2024-11-16
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Flashii\{AuthorizedHttpClient,FlashiiClient};
|
||||||
|
use Flashii\Credentials\{BasicCredentials,BearerCredentials};
|
||||||
|
use Flashii\Http\Curl\{CurlHttpClient,CurlHttpRequest,CurlHttpResponse};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers CurlHttpClient
|
||||||
|
* @covers CurlHttpRequest
|
||||||
|
* @covers CurlHttpResponse
|
||||||
|
* @uses AuthorizedHttpClient
|
||||||
|
* @uses FlashiiClient
|
||||||
|
* @uses BasicCredentials
|
||||||
|
* @uses BearerCredentials
|
||||||
|
*/
|
||||||
|
final class CurlHttpTest extends TestCase {
|
||||||
|
public function testHttpGet(): void {
|
||||||
|
$client = new CurlHttpClient(FlashiiClient::userAgentString());
|
||||||
|
|
||||||
|
$request = $client->createRequest('GET', 'https://httpbin.org/get?alreadyhere=true');
|
||||||
|
$request->setHeader('X-Test', 'meow');
|
||||||
|
$request->setHeader('X-Test2', 'mewow');
|
||||||
|
$request->removeHeader('x-test2');
|
||||||
|
|
||||||
|
$response = $client->sendRequest($request);
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
$this->assertTrue($response->hasHeader('Content-Type'));
|
||||||
|
$this->assertEquals('application/json', $response->getHeader('Content-type'));
|
||||||
|
$this->assertTrue($response->hasBody());
|
||||||
|
|
||||||
|
$body = $response->getJsonBody();
|
||||||
|
$this->assertIsObject($body);
|
||||||
|
|
||||||
|
$body = $response->getJsonBody(true);
|
||||||
|
$this->assertIsArray($body);
|
||||||
|
$this->assertArrayHasKey('headers', $body);
|
||||||
|
$this->assertIsArray($body['headers']);
|
||||||
|
$this->assertArrayHasKey('X-Test', $body['headers']);
|
||||||
|
$this->assertEquals('meow', $body['headers']['X-Test']);
|
||||||
|
$this->assertArrayNotHasKey('X-Test2', $body['headers']);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('args', $body);
|
||||||
|
$this->assertIsArray($body['args']);
|
||||||
|
$this->assertArrayHasKey('alreadyhere', $body['args']);
|
||||||
|
$this->assertEquals('true', $body['args']['alreadyhere']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHttpFailStatus(): void {
|
||||||
|
$client = new CurlHttpClient(FlashiiClient::userAgentString());
|
||||||
|
|
||||||
|
$request = $client->createRequest('GET', 'https://httpbin.org/status/404');
|
||||||
|
$response = $client->sendRequest($request);
|
||||||
|
$this->assertEquals(404, $response->getStatusCode());
|
||||||
|
|
||||||
|
$request = $client->createRequest('POST', 'https://httpbin.org/status/503');
|
||||||
|
$response = $client->sendRequest($request);
|
||||||
|
$this->assertEquals(503, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEnsureSuccess(): void {
|
||||||
|
$client = new CurlHttpClient(FlashiiClient::userAgentString());
|
||||||
|
|
||||||
|
$request = $client->createRequest('PATCH', 'https://httpbin.org/status/204');
|
||||||
|
$response = $client->sendRequest($request);
|
||||||
|
$this->assertTrue($response->isSuccessStatusCode());
|
||||||
|
|
||||||
|
$response->ensureSuccessStatusCode();
|
||||||
|
|
||||||
|
$request = $client->createRequest('DELETE', 'https://httpbin.org/status/418');
|
||||||
|
$response = $client->sendRequest($request);
|
||||||
|
$this->assertFalse($response->isSuccessStatusCode());
|
||||||
|
|
||||||
|
$this->expectException(RuntimeException::class);
|
||||||
|
$this->expectExceptionMessage('HTTP request returned status code 418.');
|
||||||
|
$response->ensureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAuthorizedHttp(): void {
|
||||||
|
$realClient = new CurlHttpClient(FlashiiClient::userAgentString());
|
||||||
|
|
||||||
|
$client = new AuthorizedHttpClient($realClient, new BasicCredentials('freakyfurball', 'express1'));
|
||||||
|
$this->assertFalse(
|
||||||
|
$realClient->sendRequest(
|
||||||
|
$realClient->createRequest('GET', 'https://httpbin.org/basic-auth/freakyfurball/express1')
|
||||||
|
)->isSuccessStatusCode()
|
||||||
|
);
|
||||||
|
$this->assertTrue(
|
||||||
|
$client->sendRequest(
|
||||||
|
$client->createRequest('GET', 'https://httpbin.org/basic-auth/freakyfurball/express1')
|
||||||
|
)->isSuccessStatusCode()
|
||||||
|
);
|
||||||
|
$this->assertFalse(
|
||||||
|
$client->sendRequest(
|
||||||
|
$client->createRequest('GET', 'https://httpbin.org/basic-auth/freakyfurball/express2')
|
||||||
|
)->isSuccessStatusCode()
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertFalse(
|
||||||
|
$realClient->sendRequest(
|
||||||
|
$realClient->createRequest('GET', 'https://httpbin.org/bearer')
|
||||||
|
)->isSuccessStatusCode()
|
||||||
|
);
|
||||||
|
|
||||||
|
$accessToken = 'DSw7ih2fMfXW3fAkUKJitltBtc7JT8TA';
|
||||||
|
$client = new AuthorizedHttpClient($realClient, new BearerCredentials($accessToken));
|
||||||
|
$response = $client->sendRequest(
|
||||||
|
$client->createRequest('GET', 'https://httpbin.org/bearer')
|
||||||
|
);
|
||||||
|
$this->assertTrue($response->hasBody());
|
||||||
|
|
||||||
|
$body = $response->getJsonBody(true);
|
||||||
|
$this->assertIsArray($body);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('authenticated', $body);
|
||||||
|
$this->assertIsBool($body['authenticated']);
|
||||||
|
$this->assertTrue($body['authenticated']);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('token', $body);
|
||||||
|
$this->assertIsString($body['token']);
|
||||||
|
$this->assertEquals($accessToken, $body['token']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHttpBody(): void {
|
||||||
|
$client = new CurlHttpClient(FlashiiClient::userAgentString());
|
||||||
|
|
||||||
|
$random = base64_encode(random_bytes(100));
|
||||||
|
$request = $client->createRequest('PUT', 'https://httpbin.org/anything');
|
||||||
|
$request->setBody($random, 'application/x-test-data');
|
||||||
|
$response = $client->sendRequest($request);
|
||||||
|
$this->assertTrue($response->hasBody());
|
||||||
|
|
||||||
|
$body = $response->getJsonBody(true);
|
||||||
|
$this->assertIsArray($body);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('method', $body);
|
||||||
|
$this->assertIsString($body['method']);
|
||||||
|
$this->assertEquals('PUT', $body['method']);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('headers', $body);
|
||||||
|
$this->assertIsArray($body['headers']);
|
||||||
|
$this->assertArrayHasKey('Content-Type', $body['headers']);
|
||||||
|
$this->assertIsString($body['headers']['Content-Type']);
|
||||||
|
$this->assertEquals('application/x-test-data', $body['headers']['Content-Type']);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('data', $body);
|
||||||
|
$this->assertIsString($body['data']);
|
||||||
|
$this->assertEquals($random, $body['data']);
|
||||||
|
|
||||||
|
$request = $client->createRequest('POST', 'https://httpbin.org/post');
|
||||||
|
$request->setBodyParams([
|
||||||
|
'string' => 'meow',
|
||||||
|
'more' => 'wowzers this one has spaces crazy',
|
||||||
|
]);
|
||||||
|
$response = $client->sendRequest($request);
|
||||||
|
$this->assertTrue($response->hasBody());
|
||||||
|
|
||||||
|
$body = $response->getJsonBody(true);
|
||||||
|
$this->assertIsArray($body);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('headers', $body);
|
||||||
|
$this->assertIsArray($body['headers']);
|
||||||
|
$this->assertArrayHasKey('Content-Type', $body['headers']);
|
||||||
|
$this->assertIsString($body['headers']['Content-Type']);
|
||||||
|
$this->assertEquals('application/x-www-form-urlencoded', $body['headers']['Content-Type']);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('form', $body);
|
||||||
|
$this->assertIsArray($body['form']);
|
||||||
|
$this->assertArrayHasKey('string', $body['form']);
|
||||||
|
$this->assertIsString($body['form']['string']);
|
||||||
|
$this->assertEquals('meow', $body['form']['string']);
|
||||||
|
$this->assertArrayHasKey('more', $body['form']);
|
||||||
|
$this->assertIsString($body['form']['more']);
|
||||||
|
$this->assertEquals('wowzers this one has spaces crazy', $body['form']['more']);
|
||||||
|
}
|
||||||
|
}
|
179
tests/StreamHttpTest.php
Normal file
179
tests/StreamHttpTest.php
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
// StreamHttpTest.php
|
||||||
|
// Created: 2024-11-16
|
||||||
|
// Updated: 2024-11-16
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Flashii\{AuthorizedHttpClient,FlashiiClient};
|
||||||
|
use Flashii\Credentials\{BasicCredentials,BearerCredentials};
|
||||||
|
use Flashii\Http\Stream\{StreamHttpClient,StreamHttpRequest,StreamHttpResponse};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers StreamHttpClient
|
||||||
|
* @covers StreamHttpRequest
|
||||||
|
* @covers StreamHttpResponse
|
||||||
|
* @uses AuthorizedHttpClient
|
||||||
|
* @uses FlashiiClient
|
||||||
|
* @uses BasicCredentials
|
||||||
|
* @uses BearerCredentials
|
||||||
|
*/
|
||||||
|
final class StreamHttpTest extends TestCase {
|
||||||
|
public function testHttpGet(): void {
|
||||||
|
$client = new StreamHttpClient(FlashiiClient::userAgentString());
|
||||||
|
|
||||||
|
$request = $client->createRequest('GET', 'https://httpbin.org/get?alreadyhere=true');
|
||||||
|
$request->setHeader('X-Test', 'meow');
|
||||||
|
$request->setHeader('X-Test2', 'mewow');
|
||||||
|
$request->removeHeader('x-test2');
|
||||||
|
|
||||||
|
$response = $client->sendRequest($request);
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
$this->assertTrue($response->hasHeader('Content-Type'));
|
||||||
|
$this->assertEquals('application/json', $response->getHeader('Content-type'));
|
||||||
|
$this->assertTrue($response->hasBody());
|
||||||
|
|
||||||
|
$body = $response->getJsonBody();
|
||||||
|
$this->assertIsObject($body);
|
||||||
|
|
||||||
|
$body = $response->getJsonBody(true);
|
||||||
|
$this->assertIsArray($body);
|
||||||
|
$this->assertArrayHasKey('headers', $body);
|
||||||
|
$this->assertIsArray($body['headers']);
|
||||||
|
$this->assertArrayHasKey('X-Test', $body['headers']);
|
||||||
|
$this->assertEquals('meow', $body['headers']['X-Test']);
|
||||||
|
$this->assertArrayNotHasKey('X-Test2', $body['headers']);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('args', $body);
|
||||||
|
$this->assertIsArray($body['args']);
|
||||||
|
$this->assertArrayHasKey('alreadyhere', $body['args']);
|
||||||
|
$this->assertEquals('true', $body['args']['alreadyhere']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHttpFailStatus(): void {
|
||||||
|
$client = new StreamHttpClient(FlashiiClient::userAgentString());
|
||||||
|
|
||||||
|
$request = $client->createRequest('GET', 'https://httpbin.org/status/404');
|
||||||
|
$response = $client->sendRequest($request);
|
||||||
|
$this->assertEquals(404, $response->getStatusCode());
|
||||||
|
|
||||||
|
$request = $client->createRequest('POST', 'https://httpbin.org/status/503');
|
||||||
|
$response = $client->sendRequest($request);
|
||||||
|
$this->assertEquals(503, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEnsureSuccess(): void {
|
||||||
|
$client = new StreamHttpClient(FlashiiClient::userAgentString());
|
||||||
|
|
||||||
|
$request = $client->createRequest('PATCH', 'https://httpbin.org/status/204');
|
||||||
|
$response = $client->sendRequest($request);
|
||||||
|
$this->assertTrue($response->isSuccessStatusCode());
|
||||||
|
|
||||||
|
$response->ensureSuccessStatusCode();
|
||||||
|
|
||||||
|
$request = $client->createRequest('DELETE', 'https://httpbin.org/status/418');
|
||||||
|
$response = $client->sendRequest($request);
|
||||||
|
$this->assertFalse($response->isSuccessStatusCode());
|
||||||
|
|
||||||
|
$this->expectException(RuntimeException::class);
|
||||||
|
$this->expectExceptionMessage('HTTP request returned status code 418.');
|
||||||
|
$response->ensureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAuthorizedHttp(): void {
|
||||||
|
$realClient = new StreamHttpClient(FlashiiClient::userAgentString());
|
||||||
|
|
||||||
|
$client = new AuthorizedHttpClient($realClient, new BasicCredentials('freakyfurball', 'express1'));
|
||||||
|
$this->assertFalse(
|
||||||
|
$realClient->sendRequest(
|
||||||
|
$realClient->createRequest('GET', 'https://httpbin.org/basic-auth/freakyfurball/express1')
|
||||||
|
)->isSuccessStatusCode()
|
||||||
|
);
|
||||||
|
$this->assertTrue(
|
||||||
|
$client->sendRequest(
|
||||||
|
$client->createRequest('GET', 'https://httpbin.org/basic-auth/freakyfurball/express1')
|
||||||
|
)->isSuccessStatusCode()
|
||||||
|
);
|
||||||
|
$this->assertFalse(
|
||||||
|
$client->sendRequest(
|
||||||
|
$client->createRequest('GET', 'https://httpbin.org/basic-auth/freakyfurball/express2')
|
||||||
|
)->isSuccessStatusCode()
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertFalse(
|
||||||
|
$realClient->sendRequest(
|
||||||
|
$realClient->createRequest('GET', 'https://httpbin.org/bearer')
|
||||||
|
)->isSuccessStatusCode()
|
||||||
|
);
|
||||||
|
|
||||||
|
$accessToken = 'DSw7ih2fMfXW3fAkUKJitltBtc7JT8TA';
|
||||||
|
$client = new AuthorizedHttpClient($realClient, new BearerCredentials($accessToken));
|
||||||
|
$response = $client->sendRequest(
|
||||||
|
$client->createRequest('GET', 'https://httpbin.org/bearer')
|
||||||
|
);
|
||||||
|
$this->assertTrue($response->hasBody());
|
||||||
|
|
||||||
|
$body = $response->getJsonBody(true);
|
||||||
|
$this->assertIsArray($body);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('authenticated', $body);
|
||||||
|
$this->assertIsBool($body['authenticated']);
|
||||||
|
$this->assertTrue($body['authenticated']);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('token', $body);
|
||||||
|
$this->assertIsString($body['token']);
|
||||||
|
$this->assertEquals($accessToken, $body['token']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testHttpBody(): void {
|
||||||
|
$client = new StreamHttpClient(FlashiiClient::userAgentString());
|
||||||
|
|
||||||
|
$random = base64_encode(random_bytes(100));
|
||||||
|
$request = $client->createRequest('PUT', 'https://httpbin.org/anything');
|
||||||
|
$request->setBody($random, 'application/x-test-data');
|
||||||
|
$response = $client->sendRequest($request);
|
||||||
|
$this->assertTrue($response->hasBody());
|
||||||
|
|
||||||
|
$body = $response->getJsonBody(true);
|
||||||
|
$this->assertIsArray($body);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('method', $body);
|
||||||
|
$this->assertIsString($body['method']);
|
||||||
|
$this->assertEquals('PUT', $body['method']);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('headers', $body);
|
||||||
|
$this->assertIsArray($body['headers']);
|
||||||
|
$this->assertArrayHasKey('Content-Type', $body['headers']);
|
||||||
|
$this->assertIsString($body['headers']['Content-Type']);
|
||||||
|
$this->assertEquals('application/x-test-data', $body['headers']['Content-Type']);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('data', $body);
|
||||||
|
$this->assertIsString($body['data']);
|
||||||
|
$this->assertEquals($random, $body['data']);
|
||||||
|
|
||||||
|
$request = $client->createRequest('POST', 'https://httpbin.org/post');
|
||||||
|
$request->setBodyParams([
|
||||||
|
'string' => 'meow',
|
||||||
|
'more' => 'wowzers this one has spaces crazy',
|
||||||
|
]);
|
||||||
|
$response = $client->sendRequest($request);
|
||||||
|
$this->assertTrue($response->hasBody());
|
||||||
|
|
||||||
|
$body = $response->getJsonBody(true);
|
||||||
|
$this->assertIsArray($body);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('headers', $body);
|
||||||
|
$this->assertIsArray($body['headers']);
|
||||||
|
$this->assertArrayHasKey('Content-Type', $body['headers']);
|
||||||
|
$this->assertIsString($body['headers']['Content-Type']);
|
||||||
|
$this->assertEquals('application/x-www-form-urlencoded', $body['headers']['Content-Type']);
|
||||||
|
|
||||||
|
$this->assertArrayHasKey('form', $body);
|
||||||
|
$this->assertIsArray($body['form']);
|
||||||
|
$this->assertArrayHasKey('string', $body['form']);
|
||||||
|
$this->assertIsString($body['form']['string']);
|
||||||
|
$this->assertEquals('meow', $body['form']['string']);
|
||||||
|
$this->assertArrayHasKey('more', $body['form']);
|
||||||
|
$this->assertIsString($body['form']['more']);
|
||||||
|
$this->assertEquals('wowzers this one has spaces crazy', $body['form']['more']);
|
||||||
|
}
|
||||||
|
}
|
30
tools/create-tag
Executable file
30
tools/create-tag
Executable file
|
@ -0,0 +1,30 @@
|
||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
$path = (function($path) {
|
||||||
|
if(!str_starts_with($path, '/'))
|
||||||
|
die('Cannot be bothered to support non-UNIX style paths, sorry!' . PHP_EOL);
|
||||||
|
|
||||||
|
while($path !== '/') {
|
||||||
|
$vPath = $path . DIRECTORY_SEPARATOR . 'VERSION';
|
||||||
|
if(is_file($vPath))
|
||||||
|
return $vPath;
|
||||||
|
|
||||||
|
$path = dirname($path);
|
||||||
|
}
|
||||||
|
})(__DIR__);
|
||||||
|
|
||||||
|
$version = file_get_contents($path);
|
||||||
|
if($version === false)
|
||||||
|
die('Failed to read VERSION file.' . PHP_EOL);
|
||||||
|
$version = trim($version);
|
||||||
|
|
||||||
|
$workingDir = getcwd();
|
||||||
|
try {
|
||||||
|
chdir(dirname($path));
|
||||||
|
echo shell_exec(sprintf('git tag v%s', $version));
|
||||||
|
echo shell_exec(sprintf('git push origin v%s', $version));
|
||||||
|
} finally {
|
||||||
|
chdir($workingDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo $version . PHP_EOL;
|
8
tools/precommit
Executable file
8
tools/precommit
Executable file
|
@ -0,0 +1,8 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
pushd .
|
||||||
|
cd $(dirname "$0")
|
||||||
|
|
||||||
|
./update-headers
|
||||||
|
|
||||||
|
popd
|
173
tools/update-headers
Executable file
173
tools/update-headers
Executable file
|
@ -0,0 +1,173 @@
|
||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
// this file sucks, screws up the header if incomplete
|
||||||
|
// successfully corrects filenames though
|
||||||
|
// fix this if you ever feel like it, or don't
|
||||||
|
|
||||||
|
date_default_timezone_set('utc');
|
||||||
|
|
||||||
|
function git_changes(): array {
|
||||||
|
$files = [];
|
||||||
|
|
||||||
|
$dir = getcwd();
|
||||||
|
try {
|
||||||
|
chdir(__DIR__ . '/..');
|
||||||
|
|
||||||
|
$output = explode("\n", trim(shell_exec('git status --short --porcelain=v1 --untracked-files=all --no-column')));
|
||||||
|
|
||||||
|
foreach($output as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
$file = realpath(explode(' ', $line)[1]);
|
||||||
|
$files[] = $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
chdir($dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collect_files(string $directory): array {
|
||||||
|
$files = [];
|
||||||
|
$dir = glob(realpath($directory) . DIRECTORY_SEPARATOR . '*');
|
||||||
|
|
||||||
|
foreach($dir as $file) {
|
||||||
|
if(is_dir($file)) {
|
||||||
|
$files = array_merge($files, collect_files($file));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$files[] = $file;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo 'Indexing changed files according to git...' . PHP_EOL;
|
||||||
|
|
||||||
|
$changed = git_changes();
|
||||||
|
|
||||||
|
echo 'Collecting files...' . PHP_EOL;
|
||||||
|
|
||||||
|
$sources = collect_files(__DIR__ . '/../src');
|
||||||
|
$tests = collect_files(__DIR__ . '/../tests');
|
||||||
|
|
||||||
|
$files = array_merge($sources, $tests);
|
||||||
|
|
||||||
|
$topDir = dirname(__DIR__) . DIRECTORY_SEPARATOR;
|
||||||
|
|
||||||
|
$now = gmdate('Y-m-d');
|
||||||
|
|
||||||
|
foreach($files as $file) {
|
||||||
|
echo 'Scanning ' . str_replace($topDir, '', $file) . '...' . PHP_EOL;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$handle = fopen($file, 'rb');
|
||||||
|
|
||||||
|
if(!str_starts_with(trim(fgets($handle)), '<?php')) {
|
||||||
|
echo 'File is not PHP.' . PHP_EOL;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$headerLines = [];
|
||||||
|
|
||||||
|
$expectLine = '// ' . basename($file);
|
||||||
|
$nameLine = trim(fgets($handle));
|
||||||
|
if($nameLine !== $expectLine) {
|
||||||
|
echo ' File name is missing or invalid, queuing update...' . PHP_EOL;
|
||||||
|
$headerLines['name'] = $expectLine;
|
||||||
|
}
|
||||||
|
|
||||||
|
$createdPrefix = '// Created: ';
|
||||||
|
$createdLine = trim(fgets($handle));
|
||||||
|
if(strpos($createdLine, $createdPrefix) !== 0) {
|
||||||
|
echo ' Creation date is missing, queuing update...' . PHP_EOL;
|
||||||
|
$headerLines['created'] = $createdPrefix . $now;
|
||||||
|
}
|
||||||
|
|
||||||
|
$updatedPrefix = '// Updated: ';
|
||||||
|
$updatedLine = trim(fgets($handle));
|
||||||
|
$updatedDate = substr($updatedLine, strlen($updatedPrefix));
|
||||||
|
if(strpos($updatedLine, $updatedPrefix) !== 0 || (in_array($file, $changed) && $updatedDate !== $now)) {
|
||||||
|
echo ' Updated date is inaccurate, queuing update...' . PHP_EOL;
|
||||||
|
$headerLines['updated'] = $updatedPrefix . $now;
|
||||||
|
}
|
||||||
|
|
||||||
|
$blankLine = trim(fgets($handle));
|
||||||
|
if(!empty($blankLine)) {
|
||||||
|
echo ' Trailing newline missing, queuing update...' . PHP_EOL;
|
||||||
|
$headerLines['blank'] = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!empty($headerLines)) {
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$tmpName = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'ndx-uh-' . bin2hex(random_bytes(8)) . '.tmp';
|
||||||
|
copy($file, $tmpName);
|
||||||
|
|
||||||
|
$read = fopen($tmpName, 'rb');
|
||||||
|
$handle = fopen($file, 'wb');
|
||||||
|
|
||||||
|
fwrite($handle, fgets($read));
|
||||||
|
|
||||||
|
$insertAfter = [];
|
||||||
|
|
||||||
|
if(empty($headerLines['name']))
|
||||||
|
fwrite($handle, fgets($read));
|
||||||
|
else {
|
||||||
|
$line = fgets($read);
|
||||||
|
if(strpos($line, '// ') !== 0)
|
||||||
|
$insertAfter[] = $line;
|
||||||
|
|
||||||
|
fwrite($handle, $headerLines['name'] . "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(empty($headerLines['created']))
|
||||||
|
fwrite($handle, fgets($read));
|
||||||
|
else {
|
||||||
|
$line = fgets($read);
|
||||||
|
if(strpos($line, '// Created: ') !== 0)
|
||||||
|
$insertAfter[] = $line;
|
||||||
|
|
||||||
|
fwrite($handle, $headerLines['created'] . "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(empty($headerLines['updated']))
|
||||||
|
fwrite($handle, fgets($read));
|
||||||
|
else {
|
||||||
|
$line = fgets($read);
|
||||||
|
if(strpos($line, '// Updated: ') !== 0)
|
||||||
|
$insertAfter[] = $line;
|
||||||
|
|
||||||
|
fwrite($handle, $headerLines['updated'] . "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!isset($headerLines['blank']))
|
||||||
|
fwrite($handle, fgets($read));
|
||||||
|
else {
|
||||||
|
$line = fgets($read);
|
||||||
|
if(!empty($line)) {
|
||||||
|
$insertAfter[] = $line;
|
||||||
|
fwrite($handle, "\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($insertAfter as $line)
|
||||||
|
fwrite($handle, $line);
|
||||||
|
|
||||||
|
while(($line = fgets($read)) !== false)
|
||||||
|
fwrite($handle, $line);
|
||||||
|
} finally {
|
||||||
|
if(is_resource($read))
|
||||||
|
fclose($read);
|
||||||
|
|
||||||
|
if(is_file($tmpName))
|
||||||
|
unlink($tmpName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
fclose($handle);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue