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