From 62791119e02bbfb686d7d931706156da99d1872d Mon Sep 17 00:00:00 2001 From: Gregor Morrill Date: Mon, 20 Feb 2023 14:25:18 -0800 Subject: [PATCH 01/17] Add advanced tools for adding clients/tokens --- CHANGELOG.md | 7 + ProcessIndieAuth.module.php | 592 +++++++++++++++++++- extras/templates/introspection-endpoint.php | 14 + views/execute-add-token.php | 23 + views/execute-clients.php | 31 + views/execute-revoke.php | 29 +- views/execute.php | 32 +- 7 files changed, 698 insertions(+), 30 deletions(-) create mode 100644 extras/templates/introspection-endpoint.php create mode 100644 views/execute-add-token.php create mode 100644 views/execute-clients.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 03be70c..9e31a4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Introspection Endpoint with HTTP Basic Authentication support +- Admin: add clients and generate client_secrets for use with Client Credentials Flow +- Admin: manually add a token with client_id and scopes -- for developer testing + +### Changed +- Noted refresh token expiration in the list of approved applications ## [0.2.1] - 2022-08-06 ### Added diff --git a/ProcessIndieAuth.module.php b/ProcessIndieAuth.module.php index 09cbaeb..647f8cb 100644 --- a/ProcessIndieAuth.module.php +++ b/ProcessIndieAuth.module.php @@ -85,6 +85,15 @@ public function ___install(): void PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); + $this->database->query(" + CREATE TABLE IF NOT EXISTS `indieauth_clients` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `client_id` varchar(255) NOT NULL DEFAULT '', + `client_secret` varchar(255) NOT NULL DEFAULT '', + `redirect_uri` varchar(255) NOT NULL DEFAULT '', + PRIMARY KEY (`id`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"); + $templates = file_get_contents(__DIR__ . '/data/templates.json'); $this->importTemplates($templates); @@ -157,6 +166,7 @@ public function ___uninstall(): void { $this->database->query("DROP TABLE IF EXISTS `indieauth_authorization_codes`"); $this->database->query("DROP TABLE IF EXISTS `indieauth_tokens`"); + $this->database->query("DROP TABLE IF EXISTS `indieauth_clients`"); # attempt to un-publish the indieauth-metadata page $endpoint = $this->pages->get('template=indieauth-metadata-endpoint'); @@ -197,13 +207,13 @@ public function ___uninstall(): void $this->uninstallPage(); } - public function ___execute(): array + public function execute(): array { $table = null; try { # get total tokens $statement = $this->database->query(' - SELECT COUNT(*) FROm indieauth_tokens'); + SELECT COUNT(*) FROM indieauth_tokens'); $total = $statement->fetchColumn(); if ($total == 0) { @@ -233,6 +243,7 @@ public function ___execute(): array ,issued_at ,last_accessed ,expiration + ,refresh_expiration FROM indieauth_tokens LIMIT @@ -272,12 +283,22 @@ public function ___execute(): array $date_expiration = '—'; if ($row['expiration']) { - $dt = new DateTime($row['expiration']); + $dt_expires = new DateTime($row['expiration']); $date_expiration = sprintf('', - $dt->format('c'), - $dt->format('F j, Y g:ia'), - $dt->format('F j, Y') + $dt_expires->format('c'), + $dt_expires->format('F j, Y g:ia'), + $dt_expires->format('F j, Y') ); + + if ($row['refresh_expiration']) { + $dt = new DateTime($row['refresh_expiration']); + $date_expiration = sprintf('R: ', + $dt->format('c'), + $dt_expires->format('F j, Y g:ia'), + $dt->format('F j, Y g:ia'), + $dt->format('F j, Y') + ); + } } $dt = new DateTime($row['issued_at']); @@ -441,6 +462,352 @@ public function ___executeRevoke(): array } } + public function executeClients(): array + { + Wire::setFuel('processHeadline', 'Clients'); + $this->breadcrumbs->add(new Breadcrumb($this->page->url, 'IndieAuth')); + + $table = null; + try { + # get total clients + $statement = $this->database->query(' + SELECT COUNT(*) FROM indieauth_tokens'); + $total = $statement->fetchColumn(); + + if ($total == 0) { + return compact('table'); + } + + $input = $this->wire('input'); + + # set up base PageArray, used for pagination links + $items_per_page = 20; + $start = ($input->pageNum - 1) * $items_per_page; + $end = $input->pageNum * $items_per_page; + + $results = new PageArray(); + $results->setTotal($total); + $results->setLimit($items_per_page); + $results->setStart($start); + + $statement = $this->database->query(" + SELECT + id + ,client_id + ,redirect_uri + FROM + indieauth_clients + LIMIT + {$start}, {$end}"); + + if ($statement->rowCount() == 0) { + return compact('table'); + } + + $table = $this->modules->get('MarkupAdminDataTable'); + $table->setEncodeEntities(false); + $table->setResizable(true); + $table->headerRow([ + 'client_id', + 'redirect_uri', + ]); + + while ($row = $statement->fetch(PDO::FETCH_ASSOC)) { + $table->row([ + $row['client_id'], + $row['redirect_uri'], + 'Regenerate Secret' => $this->page->url . 'regenerate-client-secret/' . $row['id'], + 'Delete' => $this->page->url . 'delete-client/' . $row['id'], + ]); + } + + return compact('table', 'results'); + } catch (PDOException $e) { + $this->log->save('indieauth', sprintf('Error getting clients overview: %s', $e->getMessage())); + return []; + } + } + + public function executeAddClient(): string + { + Wire::setFuel('processHeadline', 'Add Client'); + $this->breadcrumbs->add(new Breadcrumb($this->page->url, 'IndieAuth')); + + $form = $this->modules->get('InputfieldForm'); + $form->attr('action', $this->page->url . 'add-client'); + + $wrapper = new InputfieldWrapper(); + $wrapper->columnWidth = 50; + $wrapper->importArray( + [ + [ + 'type' => 'URL', + 'name' => 'client_id', + 'label' => 'Client ID', + 'description' => 'URL of client', + 'placeholder' => 'https://', + 'required' => true, + ], + [ + 'type' => 'URL', + 'name' => 'redirect_uri', + 'label' => 'Redirect URI', + 'description' => 'Not currently used; may be used in the future', + 'placeholder' => 'https://', + ], + [ + 'type' => 'submit', + 'value' => 'Submit', + ], + ] + ); + $form->append($wrapper); + + if ($this->input->requestMethod('POST')) { + $form->processInput($this->input->post); + + if ($form->getErrors()) { + $this->error('Unable to save record. Please review the error messages below.'); + return $form->render(); + } + + $client_id = $form->get('client_id')->value; + + if ($this->doesClientIdExist($client_id)) { + $this->error('There is already a record for that client_id'); + $this->session->redirect($this->page->url . 'clients', 302); + } + + if ($client_secret = $this->addClient(compact('client_id'))) { + return sprintf('

Client added successfully

Copy this secret and provide it to the client to use for authentication. This secret cannot be retrieved later, only regenerated.

client_secret: %s', + $client_secret + ); + } + + $this->error('An unexpected error occurred'); + $this->session->redirect($this->page->url . 'clients', 302); + } + + return $form->render(); + } + + public function executeDeleteClient() + { + Wire::setFuel('processHeadline', 'Delete Client'); + $this->breadcrumbs->add(new Breadcrumb($this->page->url, 'IndieAuth')); + + $defaults = []; + if ($this->input->requestMethod('GET')) { + $record = $this->getClient((int) $this->input->urlSegment2); + if (!$record) { + $this->error('Invalid or missing ID in URL'); + $this->session->redirect($this->page->url, 302); + } + + $defaults = $record; + } + + $form = $this->modules->get('InputfieldForm'); + $form->attr('action', $this->page->url . 'delete-client'); + + $wrapper = new InputfieldWrapper(); + $wrapper->columnWidth = 50; + $wrapper->importArray( + [ + [ + 'type' => 'URL', + 'name' => 'client_id', + 'label' => 'Client ID', + 'collapsed' => Inputfield::collapsedNoLocked, + ], + [ + 'type' => 'checkbox', + 'name' => 'delete', + 'label' => 'Delete this client', + 'required' => true, + ], + [ + 'type' => 'hidden', + 'name' => 'id', + ], + [ + 'type' => 'submit', + 'value' => 'Submit', + ], + ] + ); + + if ($defaults) { + $wrapper->populateValues($defaults); + } + + $form->append($wrapper); + + if ($this->input->requestMethod('POST')) { + $form->processInput($this->input->post); + + if ($form->getErrors()) { + $this->error('Unable to delete record. Please review the error messages below.'); + return $form->render(); + } + + if ($this->deleteClient((int) $form->get('id')->value)) { + $this->message('Client deleted successfully'); + $this->session->redirect($this->page->url . 'clients', 302); + } + + $this->error('An unexpected error occurred'); + $this->session->redirect($this->page->url . 'clients', 302); + } + + return $form->render(); + } + + public function executeAddToken(): array + { + Wire::setFuel('processHeadline', 'Manually Add a Token'); + $this->breadcrumbs->add(new Breadcrumb($this->page->url, 'IndieAuth')); + + $form = $this->modules->get('InputfieldForm'); + $form->attr('action', $this->page->url . 'add-token'); + + $wrapper = new InputfieldWrapper(); + $wrapper->columnWidth = 50; + $wrapper->importArray( + [ + [ + 'type' => 'URL', + 'name' => 'client_id', + 'label' => 'Client ID', + 'description' => 'URL of client', + 'placeholder' => 'https://', + 'required' => true, + ], + [ + 'type' => 'text', + 'name' => 'scope', + 'label' => 'Scope', + 'description' => 'Space-separated list of scopes', + 'value' => 'create', + 'required' => true, + ], + [ + 'type' => 'hidden', + 'name' => 'id', + 'value' => '', + ], + [ + 'type' => 'submit', + 'value' => 'Submit', + ], + ] + ); + $form->append($wrapper); + + if ($this->input->requestMethod('POST')) { + $form->processInput($this->input->post); + + if ($form->getErrors()) { + $this->error('Unable to save record. Please review the error messages below.'); + return $form->render(); + } + + $token_data = $this->addToken([ + 'id' => $form->get('id')->value, + 'client_id' => $form->get('client_id')->value, + 'scope' => $form->get('scope')->value, + ]); + + if ($token_data) { + $this->message(sprintf('Access token added: %s', + json_encode($token_data, JSON_PRETTY_PRINT) + )); + $this->session->redirect($this->page->url, 302); + } + + $this->error('An unexpected error occurred'); + $this->session->redirect($this->page->url, 302); + } + + return compact('form'); + } + + public function executeRegenerateClientSecret() + { + Wire::setFuel('processHeadline', 'Regenerate Client Secret'); + $this->breadcrumbs->add(new Breadcrumb($this->page->url, 'IndieAuth')); + + $defaults = []; + if ($this->input->requestMethod('GET')) { + $record = $this->getClient((int) $this->input->urlSegment2); + if (!$record) { + $this->error('Invalid or missing ID in URL'); + $this->session->redirect($this->page->url, 302); + } + + $defaults = $record; + } + + $form = $this->modules->get('InputfieldForm'); + $form->attr('action', $this->page->url . 'regenerate-client-secret'); + + $wrapper = new InputfieldWrapper(); + $wrapper->columnWidth = 50; + $wrapper->importArray( + [ + [ + 'type' => 'URL', + 'name' => 'client_id', + 'label' => 'Client ID', + 'collapsed' => Inputfield::collapsedNoLocked, + ], + [ + 'type' => 'checkbox', + 'name' => 'regenerate', + 'label' => 'Regnerate the secret for this client', + 'description' => 'The current client secret will stop working immediately', + 'required' => true, + ], + [ + 'type' => 'hidden', + 'name' => 'id', + ], + [ + 'type' => 'submit', + 'value' => 'Submit', + ], + ] + ); + + if ($defaults) { + $wrapper->populateValues($defaults); + } + + $form->append($wrapper); + + if ($this->input->requestMethod('POST')) { + $form->processInput($this->input->post); + + if ($form->getErrors()) { + $this->error('Unable to save record. Please review the error messages below.'); + return $form->render(); + } + + $id = (int) $form->get('id')->value; + + if ($client_secret = $this->regenerateClientSecret($id)) { + return sprintf('

Client secret regenerated

Copy this secret and provide it to the client to use for authentication. This secret cannot be retrieved later, only regenerated.

client_secret: %s', + $client_secret + ); + } + + $this->error('An unexpected error occurred'); + $this->session->redirect($this->page->url . 'clients', 302); + } + + return $form->render(); + } + /** * Get the HTML elements for authorization and token endpoints */ @@ -657,6 +1024,52 @@ public function tokenEndpoint(): void } } + /** + * Handle introspection endpoint requests + */ + public function introspectionEndpoint(): void + { + $input = $this->wire('input'); + + if ($input->requestMethod('GET')) { + $this->httpResponse('Method not supported', 405, ['Allow' => 'POST']); + } + + $client_id = $_SERVER['PHP_AUTH_USER'] ?? null; + $client_secret = $_SERVER['PHP_AUTH_PW'] ?? null; + + if (!($client_id && $client_secret)) { + $this->httpResponse([ + 'error' => 'unauthorized', + 'error_description' => 'Request must be authenticated', + ], 401, $this->authenticateHeaders('Basic')); + } + + if (!$this->authenticateClient($client_id, $client_secret)) { + $this->httpResponse([ + 'error' => 'unauthorized', + 'error_description' => 'Invalid authentication', + ], 401, $this->authenticateHeaders('Basic')); + } + + $request = $input->post()->getArray(); + + $token_request = $request['token'] ?? null; + if (!$token_request) { + $this->httpResponse([ + 'error' => 'invalid_request', + 'error_description' => 'Missing `token` parameter', + ]); + } + + $response = $this->verifyToken($token_request); + if (!$response) { + $this->httpResponse(['active' => false], 200); + } + + $this->httpResponse($response, 200); + } + /** * Handle token revocation requests * This endpoint does not currently require authorization @@ -1166,7 +1579,7 @@ private function addToken(array $authorization): ?array ,refresh_token = ?'); $statement->execute([ - $authorization['id'], + $authorization['id'] ?? '', substr($token, -7), $authorization['client_name'] ?? '', $authorization['client_logo'] ?? '', @@ -1384,6 +1797,154 @@ public function revokeExpiredTokens() } } + /** + * Note: since HTTP Basic Authentication uses ":" as user/pass + * separator, servers cannot send their URL with scheme as part + * of authenticated requests. Instead they'll send just the + * hostname and this method will assume https:// scheme. + * + * Hopefully this is fine, since ideally no one should be using + * unsecure (http://) clients or endpoints. + */ + private function authenticateClient( + string $client_id, + string $client_secret + ): bool { + try { + $client_id = 'https://' . $client_id; + + $statement = $this->database->prepare(' + SELECT + id + FROM + indieauth_clients + WHERE + client_id = ? + AND client_secret = ?'); + + $statement->execute([ + $client_id, + hash('sha256', $client_secret) + ]); + + if ($statement->rowCount() == 0) { + return false; + } + + return true; + } catch (PDOException $e) { + $this->log->save('indieauth', sprintf('Error authenticating client: %s', $e->getMessage())); + return null; + } + } + + private function doesClientIdExist(string $client_id): bool + { + try { + $client_id = Server::canonizeUrl($client_id); + $statement = $this->database->prepare(' + SELECT + id + FROM + indieauth_clients + WHERE + client_id = ?'); + $statement->execute([$client_id]); + + return ($statement->rowCount() > 0); + } catch (PDOException $e) { + $this->log->save('indieauth', sprintf('Error checking if client_id exists: %s', $e->getMessage())); + return false; + } + } + + private function addClient(array $data): ?string + { + try { + $client_secret = bin2hex(random_bytes(32)); + + $statement = $this->database->prepare(' + INSERT INTO indieauth_clients SET + client_id = ? + ,client_secret = ? + ,redirect_uri = ?'); + + $statement->execute([ + Server::canonizeUrl($data['client_id']), + hash('sha256', $client_secret), + $data['redirect_uri'] ?? '', + ]); + + return $client_secret; + } catch (PDOException $e) { + $this->log->save('indieauth', sprintf('Error adding client: %s', $e->getMessage())); + return null; + } + } + + private function getClient(int $id): ?array + { + try { + $statement = $this->database->prepare(' + SELECT + id + ,client_id + ,redirect_uri + FROM + indieauth_clients + WHERE + id = ?'); + + $statement->execute([$id]); + if ($statement->rowCount() > 0) { + return $statement->fetch(PDO::FETCH_ASSOC); + } + + return null; + } catch (PDOException $e) { + $this->log->save('indieauth', sprintf('Error getting client: %s', $e->getMessage())); + return null; + } + } + + private function regenerateClientSecret(int $id): ?string + { + try { + $client_secret = bin2hex(random_bytes(32)); + + $statement = $this->database->prepare(' + UPDATE indieauth_clients SET + client_secret = ? + WHERE + id = ?'); + + $statement->execute([ + hash('sha256', $client_secret), + $id, + ]); + + return $client_secret; + } catch (PDOException $e) { + $this->log->save('indieauth', sprintf('Error regenerating client secret: %s', $e->getMessage())); + return null; + } + } + + private function deleteClient(int $id): bool + { + try { + $statement = $this->database->prepare(' + DELETE FROM indieauth_clients + WHERE + id = ?'); + + return $statement->execute([$id]); + } catch(PDOException $e) { + $this->log->save('indieauth', sprintf('Error deleting client: %s', $e->getMessage())); + return false; + } + } + /** * Reset the IndieAuth session and redirect to the redirect_uri * with an access_denied error. @@ -1437,6 +1998,19 @@ private function redirectHttpResponse(string $redirect_uri, array $queryParams): $this->session->redirect($url, 302); } + private function authenticateHeaders(string $type = 'Bearer'): array + { + return [ + 'WWW-Authenticate' => sprintf('%s realm="%s"', + $type, + $this->urls->httpRoot + ), + ]; + } + + /** + * @see authenticateHeaders() for 401/403 responses + */ private function httpResponse($response, int $http_status = 400, array $headers = []): void { foreach ($headers as $key => $value) { @@ -1444,10 +2018,6 @@ private function httpResponse($response, int $http_status = 400, array $headers header($header); } - if ($http_status === 401) { - header(sprintf('WWW-Authenticate: Bearer realm="%s"', $this->urls->httpRoot)); - } - http_response_code($http_status); if (is_array($response)) { diff --git a/extras/templates/introspection-endpoint.php b/extras/templates/introspection-endpoint.php new file mode 100644 index 0000000..b1c0643 --- /dev/null +++ b/extras/templates/introspection-endpoint.php @@ -0,0 +1,14 @@ +get('ProcessIndieAuth'); +$IndieAuth->introspectionEndpoint(); + diff --git a/views/execute-add-token.php b/views/execute-add-token.php new file mode 100644 index 0000000..08ce5c4 --- /dev/null +++ b/views/execute-add-token.php @@ -0,0 +1,23 @@ + + +

Advanced: only use this tool if you need to manually add and test an access token. The un-encrypted access token will be displayed on the next page.

+ +

The access token will still encrypted in the database and cannot be retrieved later.

+ +

This should only be used by developers while debugging.

+ +render();?> + diff --git a/views/execute-clients.php b/views/execute-clients.php new file mode 100644 index 0000000..0ef3318 --- /dev/null +++ b/views/execute-clients.php @@ -0,0 +1,31 @@ + + +

Advanced: use this page if you need to set up Client Credentials allowing a client to authenticate as itself instead of a user.

+ +

You do not need to add anything here to use IndieAuth to sign in with your domain name.

+ + render();?> + renderPager();?> + + + +

There are no clients currently.

+ + + +Add Client + diff --git a/views/execute-revoke.php b/views/execute-revoke.php index 32bb87c..b881490 100644 --- a/views/execute-revoke.php +++ b/views/execute-revoke.php @@ -7,20 +7,25 @@ * @license https://opensource.org/licenses/MIT MIT */ +declare(strict_types=1); + +namespace ProcessWire; + $client_name = ($client_name) - ? $client_name - : $client_id; + ? $client_name + : $client_id; ?> -

Are you sure you want to revoke this access token?

+

Are you sure you want to revoke this access token?

+ +

Client:

+

Token Ending With:

+

Scope:

+

Issued:

-

Client:

-

Token Ending With:

-

Scope:

-

Issued:

+
+ + + Cancel +
-
- - - Cancel -
\ No newline at end of file diff --git a/views/execute.php b/views/execute.php index a80f308..224b18b 100644 --- a/views/execute.php +++ b/views/execute.php @@ -7,11 +7,29 @@ * @license https://opensource.org/licenses/MIT MIT */ -if ($table) { - echo '

You have granted access to the following applications.

'; - echo $table->render(); - echo $results->renderPager(); -} else { - echo '

There are no access tokens currently.

'; -} +declare(strict_types=1); + +namespace ProcessWire; + +if ($table): +?> + +

You have granted access to the following applications.

+

An R in the expiration column means the access has expired, but the application can still refresh the access until the listed date.

+ render();?> + renderPager();?> + + + +

There are no access tokens currently.

+ + + +
+ Advanced: + +
From ed6d635868fe7a5a3b457cd5f9a2004fbf6cae4e Mon Sep 17 00:00:00 2001 From: Gregor Morrill Date: Mon, 20 Feb 2023 14:36:16 -0800 Subject: [PATCH 02/17] Update firebase/php-jwt dependency --- composer.json | 2 +- composer.lock | 26 ++++++++++++++++---------- src/IndieAuth/AuthorizationCode.php | 12 ++++++------ 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/composer.json b/composer.json index 4382b51..08498f8 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "require": { - "firebase/php-jwt": "^5.0", + "firebase/php-jwt": "^6.0", "mf2/mf2": "^0.4.6", "barnabywalters/mf-cleaner": "^0.1.4" }, diff --git a/composer.lock b/composer.lock index 84d4591..823cb77 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "eb8ecc4f7646f5206acf363f791f3868", + "content-hash": "062e0afbe1b2dc5ba178eec1b766f65e", "packages": [ { "name": "barnabywalters/mf-cleaner", @@ -52,25 +52,31 @@ }, { "name": "firebase/php-jwt", - "version": "v5.4.0", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/firebase/php-jwt.git", - "reference": "d2113d9b2e0e349796e72d2a63cf9319100382d2" + "reference": "4dd1e007f22a927ac77da5a3fbb067b42d3bc224" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d2113d9b2e0e349796e72d2a63cf9319100382d2", - "reference": "d2113d9b2e0e349796e72d2a63cf9319100382d2", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/4dd1e007f22a927ac77da5a3fbb067b42d3bc224", + "reference": "4dd1e007f22a927ac77da5a3fbb067b42d3bc224", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^7.1||^8.0" }, "require-dev": { - "phpunit/phpunit": ">=4.8 <=9" + "guzzlehttp/guzzle": "^6.5||^7.4", + "phpspec/prophecy-phpunit": "^1.1", + "phpunit/phpunit": "^7.5||^9.5", + "psr/cache": "^1.0||^2.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" }, "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" }, "type": "library", @@ -103,9 +109,9 @@ ], "support": { "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v5.4.0" + "source": "https://github.com/firebase/php-jwt/tree/v6.4.0" }, - "time": "2021-06-23T19:00:23+00:00" + "time": "2023-02-09T21:01:23+00:00" }, { "name": "mf2/mf2", @@ -1950,5 +1956,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "2.0.0" + "plugin-api-version": "2.3.0" } diff --git a/src/IndieAuth/AuthorizationCode.php b/src/IndieAuth/AuthorizationCode.php index 8a65855..f15b860 100644 --- a/src/IndieAuth/AuthorizationCode.php +++ b/src/IndieAuth/AuthorizationCode.php @@ -13,10 +13,10 @@ namespace IndieAuth; use Exception; -use Firebase\JWT\BeforeValidException; -use Firebase\JWT\ExpiredException; -use Firebase\JWT\JWT; -use Firebase\JWT\SignatureInvalidException; +use Firebase\JWT\{ + JWT, + Key +}; use function ProcessWire\wire; final class AuthorizationCode @@ -75,14 +75,14 @@ private function generate(string $secret): void $data['token_lifetime'] = $token_lifetime; } - $this->value = JWT::encode($data, $secret); + $this->value = JWT::encode($data, $secret, 'HS256'); } public static function decode(string $code, string $secret): ?array { try { JWT::$leeway = 30; - $decoded = (array) JWT::decode($code, $secret, ['HS256']); + $decoded = (array) JWT::decode($code, new Key($secret, 'HS256')); return $decoded; } catch (Exception $e) { return null; From 3f1f8ee5cbc5e6e319642a43f637577f1f987ba4 Mon Sep 17 00:00:00 2001 From: Gregor Morrill Date: Mon, 20 Feb 2023 14:44:10 -0800 Subject: [PATCH 03/17] Update mf2 dependencies --- composer.json | 4 ++-- composer.lock | 45 +++++++++++++++++++++++++-------------------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/composer.json b/composer.json index 08498f8..10d2fe4 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,8 @@ { "require": { "firebase/php-jwt": "^6.0", - "mf2/mf2": "^0.4.6", - "barnabywalters/mf-cleaner": "^0.1.4" + "mf2/mf2": "^0.5", + "barnabywalters/mf-cleaner": "^0.2" }, "require-dev": { "phpunit/phpunit": "^8.4" diff --git a/composer.lock b/composer.lock index 823cb77..e47f40f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,25 +4,28 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "062e0afbe1b2dc5ba178eec1b766f65e", + "content-hash": "7479127af8d779a219373339e0b96058", "packages": [ { "name": "barnabywalters/mf-cleaner", - "version": "v0.1.4", + "version": "v0.2.0", "source": { "type": "git", "url": "https://github.com/barnabywalters/php-mf-cleaner.git", - "reference": "ef6a16628db6e8aee2b4f8bb8093d18c24b74cd4" + "reference": "bf86945ccb24093294bd266ee9e874917e762680" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barnabywalters/php-mf-cleaner/zipball/ef6a16628db6e8aee2b4f8bb8093d18c24b74cd4", - "reference": "ef6a16628db6e8aee2b4f8bb8093d18c24b74cd4", + "url": "https://api.github.com/repos/barnabywalters/php-mf-cleaner/zipball/bf86945ccb24093294bd266ee9e874917e762680", + "reference": "bf86945ccb24093294bd266ee9e874917e762680", "shasum": "" }, + "require": { + "php": ">=7.3" + }, "require-dev": { - "php": ">=5.3", - "phpunit/phpunit": "*" + "phpunit/phpunit": "~9", + "vimeo/psalm": "~4" }, "suggest": { "mf2/mf2": "To parse microformats2 structures from (X)HTML" @@ -30,7 +33,7 @@ "type": "library", "autoload": { "files": [ - "src/BarnabyWalters/Mf2/Functions.php" + "src/functions.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -46,9 +49,9 @@ "description": "Cleans up microformats2 array structures", "support": { "issues": "https://github.com/barnabywalters/php-mf-cleaner/issues", - "source": "https://github.com/barnabywalters/php-mf-cleaner/tree/v0.1.4" + "source": "https://github.com/barnabywalters/php-mf-cleaner/tree/v0.2.0" }, - "time": "2014-10-06T23:11:15+00:00" + "time": "2022-11-15T20:04:09+00:00" }, { "name": "firebase/php-jwt", @@ -115,25 +118,27 @@ }, { "name": "mf2/mf2", - "version": "0.4.6", + "version": "v0.5.0", "source": { "type": "git", "url": "https://github.com/microformats/php-mf2.git", - "reference": "00b70ee7eb7f5b0585b1bd467f6c9cbd75055d23" + "reference": "ddc56de6be62ed4a21f569de9b80e17af678ca50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/microformats/php-mf2/zipball/00b70ee7eb7f5b0585b1bd467f6c9cbd75055d23", - "reference": "00b70ee7eb7f5b0585b1bd467f6c9cbd75055d23", + "url": "https://api.github.com/repos/microformats/php-mf2/zipball/ddc56de6be62ed4a21f569de9b80e17af678ca50", + "reference": "ddc56de6be62ed4a21f569de9b80e17af678ca50", "shasum": "" }, "require": { - "php": ">=5.4.0" + "php": ">=5.6.0" }, "require-dev": { - "mf2/tests": "@dev", - "phpdocumentor/phpdocumentor": "v2.8.4", - "phpunit/phpunit": "4.8.*" + "dealerdirect/phpcodesniffer-composer-installer": "^0.7", + "mf2/tests": "dev-master#e9e2b905821ba0a5b59dab1a8eaf40634ce9cd49", + "phpcompatibility/php-compatibility": "^9.3", + "phpunit/phpunit": "^5.7", + "squizlabs/php_codesniffer": "^3.6.2" }, "suggest": { "barnabywalters/mf-cleaner": "To more easily handle the canonical data php-mf2 gives you", @@ -169,9 +174,9 @@ ], "support": { "issues": "https://github.com/microformats/php-mf2/issues", - "source": "https://github.com/microformats/php-mf2/tree/master" + "source": "https://github.com/microformats/php-mf2/tree/v0.5.0" }, - "time": "2018-08-24T14:47:04+00:00" + "time": "2022-02-10T01:05:27+00:00" } ], "packages-dev": [ From a08455ffb97874346a32cfd6c3852633d127c8f9 Mon Sep 17 00:00:00 2001 From: Gregor Morrill Date: Sat, 4 Nov 2023 13:34:22 -0700 Subject: [PATCH 04/17] Update tests/ClientIdTest.php --- tests/ClientIdTest.php | 282 ++++++++++++++++++++--------------------- 1 file changed, 138 insertions(+), 144 deletions(-) diff --git a/tests/ClientIdTest.php b/tests/ClientIdTest.php index 7d27fb0..5038b0d 100644 --- a/tests/ClientIdTest.php +++ b/tests/ClientIdTest.php @@ -2,155 +2,149 @@ declare(strict_types=1); -namespace ProcessWire; +namespace IndieAuth\Tests; use PHPUnit\Framework\TestCase; +use IndieAuth\Server; class ClientIdTest extends TestCase { - private $module; - - protected function setUp(): void - { - $this->module = wire('modules')->get('IndieAuth'); - } - - public function test_has_scheme() - { - $this->assertFalse( - $this->module->is_client_id_valid('example.com') - ); - } - - public function test_scheme_http() - { - $this->assertTrue( - $this->module->is_client_id_valid('http://example.com') - ); - } - - public function test_scheme_https() - { - $this->assertTrue( - $this->module->is_client_id_valid('https://example.com') - ); - } - - public function test_scheme_other() - { - $this->assertFalse( - $this->module->is_client_id_valid('ftp://example.com') - ); - } - - public function test_user() - { - $this->assertFalse( - $this->module->is_client_id_valid('http://user@example.com') - ); - } - - public function test_user_pass() - { - $this->assertFalse( - $this->module->is_client_id_valid('https://user:pass@example.com') - ); - } - - public function test_dot_path() - { - $this->assertFalse( - $this->module->is_client_id_valid('https://example.com/./') - ); - } - - public function test_double_dot_path() - { - $this->assertFalse( - $this->module->is_client_id_valid('https://example.com/../') - ); - } - - public function test_fragment() - { - $this->assertFalse( - $this->module->is_client_id_valid('https://example.com/#id') - ); - } - - public function test_ipv4() - { - $this->assertFalse( - $this->module->is_client_id_valid('https://1.1.1.1') - ); - } - - public function test_ipv6() - { - $this->assertFalse( - $this->module->is_client_id_valid('https://[2606:4700:4700::1111]') - ); - } - - public function test_ipv4_loopback() - { - $this->assertTrue( - $this->module->is_client_id_valid('https://127.0.0.1') - ); - } - - public function test_ipv6_loopback() - { - $this->assertTrue( - $this->module->is_client_id_valid('https://[0000:0000:0000:0000:0000:0000:0000:0001]') - ); - } - - public function test_canonized_path() - { - $this->assertEquals( - $this->module->canonize_url('https://example.com'), - 'https://example.com/' - ); - } - - public function test_canonized_scheme_host() - { - $this->assertEquals( - $this->module->canonize_url('Https://Example.Com/Path'), - 'https://example.com/Path' - ); - } - - public function test_redirect_uri_scheme_mismatch() - { - $this->assertFalse( - $this->module->is_redirect_uri_whitelisted( - 'http://client.example/callback', - 'https://client.example' - ) - ); - } - - public function test_redirect_uri_host_mismatch() - { - $this->assertFalse( - $this->module->is_redirect_uri_whitelisted( - 'https://client.example/redirect', - 'https://example.com' - ) - ); - } - - public function test_redirect_uri_port_mismatch() - { - $this->assertFalse( - $this->module->is_redirect_uri_whitelisted( - 'https://client.example/redirect', - 'https://client.example:2083' - ) - ); - } + public function test_has_scheme() + { + $this->assertFalse( + Server::isClientIdValid('example.com') + ); + } + + public function test_scheme_http() + { + $this->assertTrue( + Server::isClientIdValid('http://example.com') + ); + } + + public function test_scheme_https() + { + $this->assertTrue( + Server::isClientIdValid('https://example.com') + ); + } + + public function test_scheme_other() + { + $this->assertFalse( + Server::isClientIdValid('ftp://example.com') + ); + } + + public function test_user() + { + $this->assertFalse( + Server::isClientIdValid('http://user@example.com') + ); + } + + public function test_user_pass() + { + $this->assertFalse( + Server::isClientIdValid('https://user:pass@example.com') + ); + } + + public function test_dot_path() + { + $this->assertFalse( + Server::isClientIdValid('https://example.com/./') + ); + } + + public function test_double_dot_path() + { + $this->assertFalse( + Server::isClientIdValid('https://example.com/../') + ); + } + + public function test_fragment() + { + $this->assertFalse( + Server::isClientIdValid('https://example.com/#id') + ); + } + + public function test_ipv4() + { + $this->assertFalse( + Server::isClientIdValid('https://1.1.1.1') + ); + } + + public function test_ipv6() + { + $this->assertFalse( + Server::isClientIdValid('https://[2606:4700:4700::1111]') + ); + } + + public function test_ipv4_loopback() + { + $this->assertTrue( + Server::isClientIdValid('https://127.0.0.1') + ); + } + + public function test_ipv6_loopback() + { + $this->assertTrue( + Server::isClientIdValid('https://[0000:0000:0000:0000:0000:0000:0000:0001]') + ); + } + + public function test_canonized_path() + { + $this->assertEquals( + Server::canonizeUrl('https://example.com'), + 'https://example.com/' + ); + } + + public function test_canonized_scheme_host() + { + $this->assertEquals( + Server::canonizeUrl('Https://Example.Com/Path'), + 'https://example.com/Path' + ); + } + + public function test_redirect_uri_scheme_mismatch() + { + $this->assertFalse( + Server::isRedirectUriAllowed( + 'http://client.example/callback', + 'https://client.example' + ) + ); + } + + public function test_redirect_uri_host_mismatch() + { + $this->assertFalse( + Server::isRedirectUriAllowed( + 'https://client.example/redirect', + 'https://example.com' + ) + ); + } + + public function test_redirect_uri_port_mismatch() + { + $this->assertFalse( + Server::isRedirectUriAllowed( + 'https://client.example/redirect', + 'https://client.example:2083' + ) + ); + } } From d8af6717d1a368b39e39fc1e08996528ae743c20 Mon Sep 17 00:00:00 2001 From: Gregor Morrill Date: Sat, 4 Nov 2023 13:35:53 -0700 Subject: [PATCH 05/17] Remove tests/ClientIdTest.php See ServerTest.php --- tests/ClientIdTest.php | 150 ----------------------------------------- 1 file changed, 150 deletions(-) delete mode 100644 tests/ClientIdTest.php diff --git a/tests/ClientIdTest.php b/tests/ClientIdTest.php deleted file mode 100644 index 5038b0d..0000000 --- a/tests/ClientIdTest.php +++ /dev/null @@ -1,150 +0,0 @@ -assertFalse( - Server::isClientIdValid('example.com') - ); - } - - public function test_scheme_http() - { - $this->assertTrue( - Server::isClientIdValid('http://example.com') - ); - } - - public function test_scheme_https() - { - $this->assertTrue( - Server::isClientIdValid('https://example.com') - ); - } - - public function test_scheme_other() - { - $this->assertFalse( - Server::isClientIdValid('ftp://example.com') - ); - } - - public function test_user() - { - $this->assertFalse( - Server::isClientIdValid('http://user@example.com') - ); - } - - public function test_user_pass() - { - $this->assertFalse( - Server::isClientIdValid('https://user:pass@example.com') - ); - } - - public function test_dot_path() - { - $this->assertFalse( - Server::isClientIdValid('https://example.com/./') - ); - } - - public function test_double_dot_path() - { - $this->assertFalse( - Server::isClientIdValid('https://example.com/../') - ); - } - - public function test_fragment() - { - $this->assertFalse( - Server::isClientIdValid('https://example.com/#id') - ); - } - - public function test_ipv4() - { - $this->assertFalse( - Server::isClientIdValid('https://1.1.1.1') - ); - } - - public function test_ipv6() - { - $this->assertFalse( - Server::isClientIdValid('https://[2606:4700:4700::1111]') - ); - } - - public function test_ipv4_loopback() - { - $this->assertTrue( - Server::isClientIdValid('https://127.0.0.1') - ); - } - - public function test_ipv6_loopback() - { - $this->assertTrue( - Server::isClientIdValid('https://[0000:0000:0000:0000:0000:0000:0000:0001]') - ); - } - - public function test_canonized_path() - { - $this->assertEquals( - Server::canonizeUrl('https://example.com'), - 'https://example.com/' - ); - } - - public function test_canonized_scheme_host() - { - $this->assertEquals( - Server::canonizeUrl('Https://Example.Com/Path'), - 'https://example.com/Path' - ); - } - - public function test_redirect_uri_scheme_mismatch() - { - $this->assertFalse( - Server::isRedirectUriAllowed( - 'http://client.example/callback', - 'https://client.example' - ) - ); - } - - public function test_redirect_uri_host_mismatch() - { - $this->assertFalse( - Server::isRedirectUriAllowed( - 'https://client.example/redirect', - 'https://example.com' - ) - ); - } - - public function test_redirect_uri_port_mismatch() - { - $this->assertFalse( - Server::isRedirectUriAllowed( - 'https://client.example/redirect', - 'https://client.example:2083' - ) - ); - } - -} - From 7687e0e770c96dda7337318e5c0ba259787957a8 Mon Sep 17 00:00:00 2001 From: Gregor Morrill Date: Sun, 5 Nov 2023 14:25:17 -0800 Subject: [PATCH 06/17] Scope dependencies --- ProcessIndieAuth.module.php | 16 +- README.md | 23 + composer.json | 32 +- composer.lock | 2405 ++++++++++++++++++++------- config/php-scoper/scoper.inc.php | 18 + src/IndieAuth/AuthorizationCode.php | 2 +- tests/AuthorizationCodeTest.php | 67 + tests/ServerTest.php | 2 +- 8 files changed, 1920 insertions(+), 645 deletions(-) create mode 100644 config/php-scoper/scoper.inc.php create mode 100644 tests/AuthorizationCodeTest.php diff --git a/ProcessIndieAuth.module.php b/ProcessIndieAuth.module.php index 647f8cb..4664e2a 100644 --- a/ProcessIndieAuth.module.php +++ b/ProcessIndieAuth.module.php @@ -17,10 +17,16 @@ use Exception; use PDO; use PDOException; -use Barnabywalters\Mf2 as Mf2Helper; -use IndieAuth\AuthorizationCode; -use IndieAuth\Server; -use Mf2; +use IndieAuth\{ + AuthorizationCode, + Server +}; +use IndieAuth\Libs\{ + Barnabywalters\Mf2 as Mf2Helper, + Mf2 +}; +// use IndieAuth\Libs\; +// use function IndieAuth\Libs\Mf2\parse; class ProcessIndieAuth extends Process implements Module, ConfigurableModule { @@ -48,6 +54,8 @@ public static function getModuleInfo(): array public function init(): void { require_once 'vendor/autoload.php'; + // require_once 'build/vendor/scoper-autoload.php'; + // require_once 'libs/vendor/autoload.php'; $this->addHookAfter('Session::loginSuccess', $this, 'loginSuccess'); if ($this->auto_revoke) { $this->addHook('LazyCron::every12Hours', $this, 'revokeExpiredTokens'); diff --git a/README.md b/README.md index fbc6e9f..c1472eb 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,29 @@ If you prefer to manually install: Continue with the [Setup](#setup) steps. +## Updating Dependencies + +This section is intended for developers. Follow these steps when preparing a new release of the module. If you run into an issue with the dependencies on your server, you can also follow these steps. Please consider filing an issue as well, in case the conflict is something I can improve in the module. + +1. Delete the `vendor` folder +2. Run `composer install` +3. Check that `libs` folder is created and not empty +4. Run `composer install --no-dev` to remove dev dependencies +5. Check that the `vendor` folder only has composer autoload files, no dev dependencies + +Thanks to [this PR](https://github.com/Automattic/sensei/pull/6614) for help setting up this process. + +### Testing + +To run unit tests, you can use a globally installed version of phpunit, or run `composer require phpunit/phpunit ^8.4` to install it temporarily. + +After running tests, be sure to remove phpunit and dev dependencies again: + +1. Run `composer remove phpunit/phpunit` +2. Run `composer install --no-dev` + +This gets you back to step step 4 above. + ## Changelog * [Changelog](CHANGELOG.md) diff --git a/composer.json b/composer.json index 10d2fe4..accbc99 100644 --- a/composer.json +++ b/composer.json @@ -1,15 +1,35 @@ { - "require": { - "firebase/php-jwt": "^6.0", - "mf2/mf2": "^0.5", - "barnabywalters/mf-cleaner": "^0.2" - }, "require-dev": { - "phpunit/phpunit": "^8.4" + "mf2/mf2": "^0.5", + "barnabywalters/mf-cleaner": "^0.2", + "firebase/php-jwt": "^6.0", + "humbug/php-scoper": "^0.18.3" }, "autoload": { + "classmap": [ + "libs/" + ], + "files": [ + "libs/mf2/mf2/Mf2/Parser.php", + "libs/barnabywalters/mf-cleaner/src/functions.php" + ], "psr-4": { "IndieAuth\\": "src/IndieAuth/" } + }, + "scripts": { + "prefix-dependencies": [ + "@php ./vendor/humbug/php-scoper/bin/php-scoper add-prefix --output-dir=./libs --config=config/php-scoper/scoper.inc.php --force --quiet" + ], + "pre-install-cmd": [ + "mkdir -p libs" + ], + "pre-update-cmd": [ + "mkdir -p libs" + ], + "post-autoload-dump": [ + "@prefix-dependencies", + "composer dump-autoload --no-scripts" + ] } } diff --git a/composer.lock b/composer.lock index e47f40f..70789f1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,207 +4,34 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7479127af8d779a219373339e0b96058", + "content-hash": "b21caa02d4ea82487e911aea860f2628", "packages": [ - { - "name": "barnabywalters/mf-cleaner", - "version": "v0.2.0", - "source": { - "type": "git", - "url": "https://github.com/barnabywalters/php-mf-cleaner.git", - "reference": "bf86945ccb24093294bd266ee9e874917e762680" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/barnabywalters/php-mf-cleaner/zipball/bf86945ccb24093294bd266ee9e874917e762680", - "reference": "bf86945ccb24093294bd266ee9e874917e762680", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "~9", - "vimeo/psalm": "~4" - }, - "suggest": { - "mf2/mf2": "To parse microformats2 structures from (X)HTML" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Barnaby Walters", - "email": "barnaby@waterpigs.co.uk" - } - ], - "description": "Cleans up microformats2 array structures", - "support": { - "issues": "https://github.com/barnabywalters/php-mf-cleaner/issues", - "source": "https://github.com/barnabywalters/php-mf-cleaner/tree/v0.2.0" - }, - "time": "2022-11-15T20:04:09+00:00" - }, - { - "name": "firebase/php-jwt", - "version": "v6.4.0", - "source": { - "type": "git", - "url": "https://github.com/firebase/php-jwt.git", - "reference": "4dd1e007f22a927ac77da5a3fbb067b42d3bc224" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/4dd1e007f22a927ac77da5a3fbb067b42d3bc224", - "reference": "4dd1e007f22a927ac77da5a3fbb067b42d3bc224", - "shasum": "" - }, - "require": { - "php": "^7.1||^8.0" - }, - "require-dev": { - "guzzlehttp/guzzle": "^6.5||^7.4", - "phpspec/prophecy-phpunit": "^1.1", - "phpunit/phpunit": "^7.5||^9.5", - "psr/cache": "^1.0||^2.0", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0" - }, - "suggest": { - "ext-sodium": "Support EdDSA (Ed25519) signatures", - "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" - }, - "type": "library", - "autoload": { - "psr-4": { - "Firebase\\JWT\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Neuman Vong", - "email": "neuman+pear@twilio.com", - "role": "Developer" - }, - { - "name": "Anant Narayanan", - "email": "anant@php.net", - "role": "Developer" - } - ], - "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", - "homepage": "https://github.com/firebase/php-jwt", - "keywords": [ - "jwt", - "php" - ], - "support": { - "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v6.4.0" - }, - "time": "2023-02-09T21:01:23+00:00" - }, - { - "name": "mf2/mf2", - "version": "v0.5.0", - "source": { - "type": "git", - "url": "https://github.com/microformats/php-mf2.git", - "reference": "ddc56de6be62ed4a21f569de9b80e17af678ca50" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/microformats/php-mf2/zipball/ddc56de6be62ed4a21f569de9b80e17af678ca50", - "reference": "ddc56de6be62ed4a21f569de9b80e17af678ca50", - "shasum": "" - }, - "require": { - "php": ">=5.6.0" - }, - "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7", - "mf2/tests": "dev-master#e9e2b905821ba0a5b59dab1a8eaf40634ce9cd49", - "phpcompatibility/php-compatibility": "^9.3", - "phpunit/phpunit": "^5.7", - "squizlabs/php_codesniffer": "^3.6.2" - }, - "suggest": { - "barnabywalters/mf-cleaner": "To more easily handle the canonical data php-mf2 gives you", - "masterminds/html5": "Alternative HTML parser for PHP, for better HTML5 support." - }, - "bin": [ - "bin/fetch-mf2", - "bin/parse-mf2" - ], - "type": "library", - "autoload": { - "files": [ - "Mf2/Parser.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "CC0-1.0" - ], - "authors": [ - { - "name": "Barnaby Walters", - "homepage": "http://waterpigs.co.uk" - } - ], - "description": "A pure, generic microformats2 parser — makes HTML as easy to consume as a JSON API", - "keywords": [ - "html", - "microformats", - "microformats 2", - "parser", - "semantic" - ], - "support": { - "issues": "https://github.com/microformats/php-mf2/issues", - "source": "https://github.com/microformats/php-mf2/tree/v0.5.0" - }, - "time": "2022-02-10T01:05:27+00:00" - } - ], - "packages-dev": [ { "name": "doctrine/instantiator", - "version": "1.4.0", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b" + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b", - "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^8.0", + "doctrine/coding-standard": "^9 || ^11", "ext-pdo": "*", "ext-phar": "*", - "phpbench/phpbench": "^0.13 || 1.0.0-alpha2", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "phpbench/phpbench": "^0.16 || ^1", + "phpstan/phpstan": "^1.4", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "vimeo/psalm": "^4.30 || ^5.4" }, "type": "library", "autoload": { @@ -231,7 +58,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.4.0" + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" }, "funding": [ { @@ -247,41 +74,42 @@ "type": "tidelift" } ], - "time": "2020-11-10T18:47:58+00:00" + "time": "2022-12-30T00:15:36+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.10.2", + "version": "1.11.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220" + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220", - "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, - "replace": { - "myclabs/deep-copy": "self.version" + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" }, "require-dev": { - "doctrine/collections": "^1.0", - "doctrine/common": "^2.6", - "phpunit/phpunit": "^7.1" + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", "autoload": { - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - }, "files": [ "src/DeepCopy/deep_copy.php" - ] + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -297,7 +125,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.10.2" + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" }, "funding": [ { @@ -305,20 +133,20 @@ "type": "tidelift" } ], - "time": "2020-11-13T09:40:50+00:00" + "time": "2023-03-08T13:26:56+00:00" }, { "name": "phar-io/manifest", - "version": "2.0.1", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133" + "reference": "97803eca37d319dfa7826cc2437fc020857acb53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", - "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", + "reference": "97803eca37d319dfa7826cc2437fc020857acb53", "shasum": "" }, "require": { @@ -363,22 +191,22 @@ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "support": { "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/master" + "source": "https://github.com/phar-io/manifest/tree/2.0.3" }, - "time": "2020-06-27T14:33:11+00:00" + "time": "2021-07-20T11:28:43+00:00" }, { "name": "phar-io/version", - "version": "3.1.0", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/phar-io/version.git", - "reference": "bae7c545bef187884426f042434e561ab1ddb182" + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/bae7c545bef187884426f042434e561ab1ddb182", - "reference": "bae7c545bef187884426f042434e561ab1ddb182", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", "shasum": "" }, "require": { @@ -414,384 +242,159 @@ "description": "Library for handling version information and constraints", "support": { "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.1.0" + "source": "https://github.com/phar-io/version/tree/3.2.1" }, - "time": "2021-02-23T14:00:09+00:00" + "time": "2022-02-21T01:04:05+00:00" }, { - "name": "phpdocumentor/reflection-common", - "version": "2.2.0", + "name": "phpunit/php-code-coverage", + "version": "7.0.15", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "819f92bba8b001d4363065928088de22f25a3a48" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", - "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/819f92bba8b001d4363065928088de22f25a3a48", + "reference": "819f92bba8b001d4363065928088de22f25a3a48", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "ext-dom": "*", + "ext-xmlwriter": "*", + "php": ">=7.2", + "phpunit/php-file-iterator": "^2.0.2", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-token-stream": "^3.1.3 || ^4.0", + "sebastian/code-unit-reverse-lookup": "^1.0.1", + "sebastian/environment": "^4.2.2", + "sebastian/version": "^2.0.1", + "theseer/tokenizer": "^1.1.3" + }, + "require-dev": { + "phpunit/phpunit": "^8.2.2" + }, + "suggest": { + "ext-xdebug": "^2.7.2" }, "type": "library", "extra": { "branch-alias": { - "dev-2.x": "2.x-dev" + "dev-master": "7.0-dev" } }, "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src/" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Jaap van Otterdijk", - "email": "opensource@ijaap.nl" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Common reflection classes used by phpdocumentor to reflect the code structure", - "homepage": "http://www.phpdoc.org", + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", "keywords": [ - "FQSEN", - "phpDocumentor", - "phpdoc", - "reflection", - "static analysis" + "coverage", + "testing", + "xunit" ], "support": { - "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", - "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/7.0.15" }, - "time": "2020-06-27T09:03:43+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-07-26T12:20:09+00:00" }, { - "name": "phpdocumentor/reflection-docblock", - "version": "5.2.2", + "name": "phpunit/php-file-iterator", + "version": "2.0.5", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "42c5ba5220e6904cbfe8b1a1bda7c0cfdc8c12f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", - "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/42c5ba5220e6904cbfe8b1a1bda7c0cfdc8c12f5", + "reference": "42c5ba5220e6904cbfe8b1a1bda7c0cfdc8c12f5", "shasum": "" }, "require": { - "ext-filter": "*", - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.2", - "phpdocumentor/type-resolver": "^1.3", - "webmozart/assert": "^1.9.1" + "php": ">=7.1" }, "require-dev": { - "mockery/mockery": "~1.3.2" + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - }, - { - "name": "Jaap van Otterdijk", - "email": "account@ijaap.nl" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], "support": { - "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master" + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.5" }, - "time": "2020-09-03T19:13:55+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:42:26+00:00" }, { - "name": "phpdocumentor/type-resolver", - "version": "1.4.0", + "name": "phpunit/php-text-template", + "version": "1.2.1", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", - "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "phpdocumentor/reflection-common": "^2.0" - }, - "require-dev": { - "ext-tokenizer": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-1.x": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "phpDocumentor\\Reflection\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "me@mikevanriel.com" - } - ], - "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "support": { - "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.4.0" - }, - "time": "2020-09-17T18:55:26+00:00" - }, - { - "name": "phpspec/prophecy", - "version": "1.13.0", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/be1996ed8adc35c3fd795488a653f4b518be70ea", - "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.2", - "php": "^7.2 || ~8.0, <8.1", - "phpdocumentor/reflection-docblock": "^5.2", - "sebastian/comparator": "^3.0 || ^4.0", - "sebastian/recursion-context": "^3.0 || ^4.0" - }, - "require-dev": { - "phpspec/phpspec": "^6.0", - "phpunit/phpunit": "^8.0 || ^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.11.x-dev" - } - }, - "autoload": { - "psr-4": { - "Prophecy\\": "src/Prophecy" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "https://github.com/phpspec/prophecy", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "support": { - "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/1.13.0" - }, - "time": "2021-03-17T13:42:18+00:00" - }, - { - "name": "phpunit/php-code-coverage", - "version": "7.0.14", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "bb7c9a210c72e4709cdde67f8b7362f672f2225c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/bb7c9a210c72e4709cdde67f8b7362f672f2225c", - "reference": "bb7c9a210c72e4709cdde67f8b7362f672f2225c", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-xmlwriter": "*", - "php": ">=7.2", - "phpunit/php-file-iterator": "^2.0.2", - "phpunit/php-text-template": "^1.2.1", - "phpunit/php-token-stream": "^3.1.1 || ^4.0", - "sebastian/code-unit-reverse-lookup": "^1.0.1", - "sebastian/environment": "^4.2.2", - "sebastian/version": "^2.0.1", - "theseer/tokenizer": "^1.1.3" - }, - "require-dev": { - "phpunit/phpunit": "^8.2.2" - }, - "suggest": { - "ext-xdebug": "^2.7.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "7.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", - "keywords": [ - "coverage", - "testing", - "xunit" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/7.0.14" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-12-02T13:39:03+00:00" - }, - { - "name": "phpunit/php-file-iterator", - "version": "2.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "4b49fb70f067272b659ef0174ff9ca40fdaa6357" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/4b49fb70f067272b659ef0174ff9ca40fdaa6357", - "reference": "4b49fb70f067272b659ef0174ff9ca40fdaa6357", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "require-dev": { - "phpunit/phpunit": "^8.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", - "keywords": [ - "filesystem", - "iterator" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-11-30T08:25:21+00:00" - }, - { - "name": "phpunit/php-text-template", - "version": "1.2.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" + "php": ">=5.3.3" }, "type": "library", "autoload": { @@ -942,16 +545,16 @@ }, { "name": "phpunit/phpunit", - "version": "8.5.17", + "version": "8.5.34", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "79067856d85421c56d413bd238d4e2cd6b0e54da" + "reference": "622d0186707f39a4ae71df3bcf42d759bb868854" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/79067856d85421c56d413bd238d4e2cd6b0e54da", - "reference": "79067856d85421c56d413bd238d4e2cd6b0e54da", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/622d0186707f39a4ae71df3bcf42d759bb868854", + "reference": "622d0186707f39a4ae71df3bcf42d759bb868854", "shasum": "" }, "require": { @@ -963,31 +566,27 @@ "ext-xml": "*", "ext-xmlwriter": "*", "myclabs/deep-copy": "^1.10.0", - "phar-io/manifest": "^2.0.1", + "phar-io/manifest": "^2.0.3", "phar-io/version": "^3.0.2", "php": ">=7.2", - "phpspec/prophecy": "^1.10.3", "phpunit/php-code-coverage": "^7.0.12", - "phpunit/php-file-iterator": "^2.0.2", + "phpunit/php-file-iterator": "^2.0.4", "phpunit/php-text-template": "^1.2.1", "phpunit/php-timer": "^2.1.2", - "sebastian/comparator": "^3.0.2", + "sebastian/comparator": "^3.0.5", "sebastian/diff": "^3.0.2", "sebastian/environment": "^4.2.3", - "sebastian/exporter": "^3.1.2", + "sebastian/exporter": "^3.1.5", "sebastian/global-state": "^3.0.0", "sebastian/object-enumerator": "^3.0.3", "sebastian/resource-operations": "^2.0.1", "sebastian/type": "^1.1.3", "sebastian/version": "^2.0.1" }, - "require-dev": { - "ext-pdo": "*" - }, "suggest": { - "ext-soap": "*", - "ext-xdebug": "*", - "phpunit/php-invoker": "^2.0.0" + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage", + "phpunit/php-invoker": "To allow enforcing time limits" }, "bin": [ "phpunit" @@ -1023,19 +622,24 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.17" + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.34" }, "funding": [ { - "url": "https://phpunit.de/donate.html", + "url": "https://phpunit.de/sponsors.html", "type": "custom" }, { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" } ], - "time": "2021-06-23T05:12:43+00:00" + "time": "2023-09-19T05:20:51+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -1094,16 +698,16 @@ }, { "name": "sebastian/comparator", - "version": "3.0.3", + "version": "3.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "1071dfcef776a57013124ff35e1fc41ccd294758" + "reference": "1dc7ceb4a24aede938c7af2a9ed1de09609ca770" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1071dfcef776a57013124ff35e1fc41ccd294758", - "reference": "1071dfcef776a57013124ff35e1fc41ccd294758", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1dc7ceb4a24aede938c7af2a9ed1de09609ca770", + "reference": "1dc7ceb4a24aede938c7af2a9ed1de09609ca770", "shasum": "" }, "require": { @@ -1156,7 +760,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/3.0.3" + "source": "https://github.com/sebastianbergmann/comparator/tree/3.0.5" }, "funding": [ { @@ -1164,20 +768,20 @@ "type": "github" } ], - "time": "2020-11-30T08:04:30+00:00" + "time": "2022-09-14T12:31:48+00:00" }, { "name": "sebastian/diff", - "version": "3.0.3", + "version": "3.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211" + "reference": "6296a0c086dd0117c1b78b059374d7fcbe7545ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/14f72dd46eaf2f2293cbe79c93cc0bc43161a211", - "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/6296a0c086dd0117c1b78b059374d7fcbe7545ae", + "reference": "6296a0c086dd0117c1b78b059374d7fcbe7545ae", "shasum": "" }, "require": { @@ -1222,7 +826,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/3.0.3" + "source": "https://github.com/sebastianbergmann/diff/tree/3.0.4" }, "funding": [ { @@ -1230,7 +834,7 @@ "type": "github" } ], - "time": "2020-11-30T07:59:04+00:00" + "time": "2023-05-07T05:30:20+00:00" }, { "name": "sebastian/environment", @@ -1297,16 +901,16 @@ }, { "name": "sebastian/exporter", - "version": "3.1.3", + "version": "3.1.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "6b853149eab67d4da22291d36f5b0631c0fd856e" + "reference": "73a9676f2833b9a7c36968f9d882589cd75511e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/6b853149eab67d4da22291d36f5b0631c0fd856e", - "reference": "6b853149eab67d4da22291d36f5b0631c0fd856e", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/73a9676f2833b9a7c36968f9d882589cd75511e6", + "reference": "73a9676f2833b9a7c36968f9d882589cd75511e6", "shasum": "" }, "require": { @@ -1315,7 +919,7 @@ }, "require-dev": { "ext-mbstring": "*", - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { @@ -1362,7 +966,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.3" + "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.5" }, "funding": [ { @@ -1370,20 +974,20 @@ "type": "github" } ], - "time": "2020-11-30T07:47:53+00:00" + "time": "2022-09-14T06:00:17+00:00" }, { "name": "sebastian/global-state", - "version": "3.0.1", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "474fb9edb7ab891665d3bfc6317f42a0a150454b" + "reference": "66783ce213de415b451b904bfef9dda0cf9aeae0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/474fb9edb7ab891665d3bfc6317f42a0a150454b", - "reference": "474fb9edb7ab891665d3bfc6317f42a0a150454b", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/66783ce213de415b451b904bfef9dda0cf9aeae0", + "reference": "66783ce213de415b451b904bfef9dda0cf9aeae0", "shasum": "" }, "require": { @@ -1426,7 +1030,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/3.0.1" + "source": "https://github.com/sebastianbergmann/global-state/tree/3.0.3" }, "funding": [ { @@ -1434,7 +1038,7 @@ "type": "github" } ], - "time": "2020-11-30T07:43:24+00:00" + "time": "2023-08-02T09:23:32+00:00" }, { "name": "sebastian/object-enumerator", @@ -1767,71 +1371,1182 @@ "time": "2016-10-03T07:35:21+00:00" }, { - "name": "symfony/polyfill-ctype", - "version": "v1.23.0", + "name": "theseer/tokenizer", + "version": "1.2.1", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce" + "url": "https://github.com/theseer/tokenizer.git", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/46cd95797e9df938fdd2b03693b5fca5e64b01ce", - "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", + "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", "shasum": "" }, "require": { - "php": ">=7.1" - }, - "suggest": { - "ext-ctype": "For best performance" + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.23-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, - "files": [ - "bootstrap.php" + "classmap": [ + "src/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" } ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.23.0" + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.1" }, "funding": [ { - "url": "https://symfony.com/sponsor", + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2021-07-28T10:34:58+00:00" + } + ], + "packages-dev": [ + { + "name": "barnabywalters/mf-cleaner", + "version": "v0.2.0", + "source": { + "type": "git", + "url": "https://github.com/barnabywalters/php-mf-cleaner.git", + "reference": "bf86945ccb24093294bd266ee9e874917e762680" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barnabywalters/php-mf-cleaner/zipball/bf86945ccb24093294bd266ee9e874917e762680", + "reference": "bf86945ccb24093294bd266ee9e874917e762680", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "~9", + "vimeo/psalm": "~4" + }, + "suggest": { + "mf2/mf2": "To parse microformats2 structures from (X)HTML" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barnaby Walters", + "email": "barnaby@waterpigs.co.uk" + } + ], + "description": "Cleans up microformats2 array structures", + "support": { + "issues": "https://github.com/barnabywalters/php-mf-cleaner/issues", + "source": "https://github.com/barnabywalters/php-mf-cleaner/tree/v0.2.0" + }, + "time": "2022-11-15T20:04:09+00:00" + }, + { + "name": "fidry/console", + "version": "0.5.5", + "source": { + "type": "git", + "url": "https://github.com/theofidry/console.git", + "reference": "bc1fe03f600c63f12ec0a39c6b746c1a1fb77bf7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/console/zipball/bc1fe03f600c63f12ec0a39c6b746c1a1fb77bf7", + "reference": "bc1fe03f600c63f12ec0a39c6b746c1a1fb77bf7", + "shasum": "" + }, + "require": { + "php": "^7.4.0 || ^8.0.0", + "symfony/console": "^4.4 || ^5.4 || ^6.1", + "symfony/event-dispatcher-contracts": "^1.0 || ^2.5 || ^3.0", + "symfony/service-contracts": "^1.0 || ^2.5 || ^3.0", + "thecodingmachine/safe": "^1.3 || ^2.0", + "webmozart/assert": "^1.11" + }, + "conflict": { + "symfony/dependency-injection": "<5.3.0", + "symfony/framework-bundle": "<5.3.0", + "symfony/http-kernel": "<5.3.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4", + "composer/semver": "^3.3", + "ergebnis/composer-normalize": "^2.28", + "infection/infection": "^0.26", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.4.3", + "symfony/dependency-injection": "^4.4 || ^5.4 || ^6.1", + "symfony/framework-bundle": "^4.4 || ^5.4 || ^6.1", + "symfony/http-kernel": "^4.4 || ^5.4 || ^6.1", + "symfony/phpunit-bridge": "^4.4.47 || ^5.4 || ^6.0", + "symfony/yaml": "^4.4 || ^5.4 || ^6.1", + "webmozarts/strict-phpunit": "^7.3" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Fidry\\Console\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo Fidry", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Library to create CLI applications", + "keywords": [ + "cli", + "console", + "symfony" + ], + "support": { + "issues": "https://github.com/theofidry/console/issues", + "source": "https://github.com/theofidry/console/tree/0.5.5" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2022-12-18T10:49:34+00:00" + }, + { + "name": "fidry/filesystem", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/filesystem.git", + "reference": "1dd372ab3eb8b84ffe9578bff576b00c9a44ee46" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/filesystem/zipball/1dd372ab3eb8b84ffe9578bff576b00c9a44ee46", + "reference": "1dd372ab3eb8b84ffe9578bff576b00c9a44ee46", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/filesystem": "^6.3", + "thecodingmachine/safe": "^2.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4", + "ergebnis/composer-normalize": "^2.28", + "infection/infection": "^0.26", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^10.3", + "symfony/finder": "^6.3", + "symfony/phpunit-bridge": "^6.2" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Fidry\\FileSystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Théo Fidry", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Symfony Filesystem with a few more utilities.", + "keywords": [ + "filesystem" + ], + "support": { + "issues": "https://github.com/theofidry/filesystem/issues", + "source": "https://github.com/theofidry/filesystem/tree/1.1.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2023-10-07T07:32:54+00:00" + }, + { + "name": "firebase/php-jwt", + "version": "v6.9.0", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "f03270e63eaccf3019ef0f32849c497385774e11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/f03270e63eaccf3019ef0f32849c497385774e11", + "reference": "f03270e63eaccf3019ef0f32849c497385774e11", + "shasum": "" + }, + "require": { + "php": "^7.4||^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^6.5||^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^1.0||^2.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v6.9.0" + }, + "time": "2023-10-05T00:24:42+00:00" + }, + { + "name": "humbug/php-scoper", + "version": "0.18.7", + "source": { + "type": "git", + "url": "https://github.com/humbug/php-scoper.git", + "reference": "9386a0af946f175d7a1ebfb68851bc2bb8ad7858" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/humbug/php-scoper/zipball/9386a0af946f175d7a1ebfb68851bc2bb8ad7858", + "reference": "9386a0af946f175d7a1ebfb68851bc2bb8ad7858", + "shasum": "" + }, + "require": { + "fidry/console": "^0.5.0", + "fidry/filesystem": "^1.1", + "jetbrains/phpstorm-stubs": "^v2022.2", + "nikic/php-parser": "^4.12", + "php": "^8.1", + "symfony/console": "^5.2 || ^6.0", + "symfony/filesystem": "^5.2 || ^6.0", + "symfony/finder": "^5.2 || ^6.0", + "thecodingmachine/safe": "^2.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.1", + "ergebnis/composer-normalize": "^2.28", + "fidry/makefile": "^1.0", + "humbug/box": "^4.5.1", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.0", + "symfony/yaml": "^6.1" + }, + "bin": [ + "bin/php-scoper" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Humbug\\PhpScoper\\": "src/" + }, + "classmap": [ + "vendor-hotfix/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Théo Fidry", + "email": "theo.fidry@gmail.com" + }, + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com" + } + ], + "description": "Prefixes all PHP namespaces in a file or directory.", + "support": { + "issues": "https://github.com/humbug/php-scoper/issues", + "source": "https://github.com/humbug/php-scoper/tree/0.18.7" + }, + "time": "2023-11-04T18:01:12+00:00" + }, + { + "name": "jetbrains/phpstorm-stubs", + "version": "v2022.3", + "source": { + "type": "git", + "url": "https://github.com/JetBrains/phpstorm-stubs.git", + "reference": "6b568c153cea002dc6fad96285c3063d07cab18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JetBrains/phpstorm-stubs/zipball/6b568c153cea002dc6fad96285c3063d07cab18d", + "reference": "6b568c153cea002dc6fad96285c3063d07cab18d", + "shasum": "" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "@stable", + "nikic/php-parser": "@stable", + "php": "^8.0", + "phpdocumentor/reflection-docblock": "@stable", + "phpunit/phpunit": "@stable" + }, + "type": "library", + "autoload": { + "files": [ + "PhpStormStubsMap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "PHP runtime & extensions header files for PhpStorm", + "homepage": "https://www.jetbrains.com/phpstorm", + "keywords": [ + "autocomplete", + "code", + "inference", + "inspection", + "jetbrains", + "phpstorm", + "stubs", + "type" + ], + "support": { + "source": "https://github.com/JetBrains/phpstorm-stubs/tree/v2022.3" + }, + "time": "2022-10-17T09:21:37+00:00" + }, + { + "name": "mf2/mf2", + "version": "v0.5.0", + "source": { + "type": "git", + "url": "https://github.com/microformats/php-mf2.git", + "reference": "ddc56de6be62ed4a21f569de9b80e17af678ca50" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/microformats/php-mf2/zipball/ddc56de6be62ed4a21f569de9b80e17af678ca50", + "reference": "ddc56de6be62ed4a21f569de9b80e17af678ca50", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7", + "mf2/tests": "dev-master#e9e2b905821ba0a5b59dab1a8eaf40634ce9cd49", + "phpcompatibility/php-compatibility": "^9.3", + "phpunit/phpunit": "^5.7", + "squizlabs/php_codesniffer": "^3.6.2" + }, + "suggest": { + "barnabywalters/mf-cleaner": "To more easily handle the canonical data php-mf2 gives you", + "masterminds/html5": "Alternative HTML parser for PHP, for better HTML5 support." + }, + "bin": [ + "bin/fetch-mf2", + "bin/parse-mf2" + ], + "type": "library", + "autoload": { + "files": [ + "Mf2/Parser.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "CC0-1.0" + ], + "authors": [ + { + "name": "Barnaby Walters", + "homepage": "http://waterpigs.co.uk" + } + ], + "description": "A pure, generic microformats2 parser — makes HTML as easy to consume as a JSON API", + "keywords": [ + "html", + "microformats", + "microformats 2", + "parser", + "semantic" + ], + "support": { + "issues": "https://github.com/microformats/php-mf2/issues", + "source": "https://github.com/microformats/php-mf2/tree/v0.5.0" + }, + "time": "2022-02-10T01:05:27+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.17.1", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.0" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1" + }, + "time": "2023-08-13T19:53:39+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "symfony/console", + "version": "v6.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "eca495f2ee845130855ddf1cf18460c38966c8b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/eca495f2ee845130855ddf1cf18460c38966c8b6", + "reference": "eca495f2ee845130855ddf1cf18460c38966c8b6", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/lock": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/var-dumper": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-08-16T10:10:12+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "a76aed96a42d2b521153fb382d418e30d18b59df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/a76aed96a42d2b521153fb382d418e30d18b59df", + "reference": "a76aed96a42d2b521153fb382d418e30d18b59df", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v6.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "edd36776956f2a6fcf577edb5b05eb0e3bdc52ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/edd36776956f2a6fcf577edb5b05eb0e3bdc52ae", + "reference": "edd36776956f2a6fcf577edb5b05eb0e3bdc52ae", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v6.3.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-06-01T08:30:39+00:00" + }, + { + "name": "symfony/finder", + "version": "v6.3.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "a1b31d88c0e998168ca7792f222cbecee47428c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/a1b31d88c0e998168ca7792f222cbecee47428c4", + "reference": "a1b31d88c0e998168ca7792f222cbecee47428c4", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v6.3.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-09-26T12:56:25+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "reference": "ea208ce43cbb04af6867b4fdddb1bdbf84cc28cb", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "875e90aeea2777b6f135677f618529449334a612" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/875e90aeea2777b6f135677f618529449334a612", + "reference": "875e90aeea2777b6f135677f618529449334a612", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", "type": "custom" }, { @@ -1843,75 +2558,499 @@ "type": "tidelift" } ], - "time": "2021-02-19T12:13:01+00:00" + "time": "2023-01-26T09:26:14+00:00" }, { - "name": "theseer/tokenizer", - "version": "1.2.0", + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.28.0", "source": { "type": "git", - "url": "https://github.com/theseer/tokenizer.git", - "reference": "75a63c33a8577608444246075ea0af0d052e452a" + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", - "reference": "75a63c33a8577608444246075ea0af0d052e452a", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", + "reference": "8c4ad05dd0120b6a53c1ca374dca2ad0a1c4ed92", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, "classmap": [ - "src/" + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], "support": { - "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/master" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.28.0" }, "funding": [ { - "url": "https://github.com/theseer", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-26T09:26:14+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.28.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "42292d99c55abe617799667f454222c54c60e229" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/42292d99c55abe617799667f454222c54c60e229", + "reference": "42292d99c55abe617799667f454222c54c60e229", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.28-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.28.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-28T09:04:16+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "40da9cc13ec349d9e4966ce18b5fbcd724ab10a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/40da9cc13ec349d9e4966ce18b5fbcd724ab10a4", + "reference": "40da9cc13ec349d9e4966ce18b5fbcd724ab10a4", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^2.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/string", + "version": "v6.3.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "13d76d0fb049051ed12a04bef4f9de8715bea339" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/13d76d0fb049051ed12a04bef4f9de8715bea339", + "reference": "13d76d0fb049051ed12a04bef4f9de8715bea339", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/error-handler": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/intl": "^6.2", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v6.3.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-09-18T10:38:32+00:00" + }, + { + "name": "thecodingmachine/safe", + "version": "v2.5.0", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/3115ecd6b4391662b4931daac4eba6b07a2ac1f0", + "reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.5", + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "^3.2", + "thecodingmachine/phpstan-strict-rules": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2.x-dev" } + }, + "autoload": { + "files": [ + "deprecated/apc.php", + "deprecated/array.php", + "deprecated/datetime.php", + "deprecated/libevent.php", + "deprecated/misc.php", + "deprecated/password.php", + "deprecated/mssql.php", + "deprecated/stats.php", + "deprecated/strings.php", + "lib/special_cases.php", + "deprecated/mysqli.php", + "generated/apache.php", + "generated/apcu.php", + "generated/array.php", + "generated/bzip2.php", + "generated/calendar.php", + "generated/classobj.php", + "generated/com.php", + "generated/cubrid.php", + "generated/curl.php", + "generated/datetime.php", + "generated/dir.php", + "generated/eio.php", + "generated/errorfunc.php", + "generated/exec.php", + "generated/fileinfo.php", + "generated/filesystem.php", + "generated/filter.php", + "generated/fpm.php", + "generated/ftp.php", + "generated/funchand.php", + "generated/gettext.php", + "generated/gmp.php", + "generated/gnupg.php", + "generated/hash.php", + "generated/ibase.php", + "generated/ibmDb2.php", + "generated/iconv.php", + "generated/image.php", + "generated/imap.php", + "generated/info.php", + "generated/inotify.php", + "generated/json.php", + "generated/ldap.php", + "generated/libxml.php", + "generated/lzf.php", + "generated/mailparse.php", + "generated/mbstring.php", + "generated/misc.php", + "generated/mysql.php", + "generated/network.php", + "generated/oci8.php", + "generated/opcache.php", + "generated/openssl.php", + "generated/outcontrol.php", + "generated/pcntl.php", + "generated/pcre.php", + "generated/pgsql.php", + "generated/posix.php", + "generated/ps.php", + "generated/pspell.php", + "generated/readline.php", + "generated/rpminfo.php", + "generated/rrd.php", + "generated/sem.php", + "generated/session.php", + "generated/shmop.php", + "generated/sockets.php", + "generated/sodium.php", + "generated/solr.php", + "generated/spl.php", + "generated/sqlsrv.php", + "generated/ssdeep.php", + "generated/ssh2.php", + "generated/stream.php", + "generated/strings.php", + "generated/swoole.php", + "generated/uodbc.php", + "generated/uopz.php", + "generated/url.php", + "generated/var.php", + "generated/xdiff.php", + "generated/xml.php", + "generated/xmlrpc.php", + "generated/yaml.php", + "generated/yaz.php", + "generated/zip.php", + "generated/zlib.php" + ], + "classmap": [ + "lib/DateTime.php", + "lib/DateTimeImmutable.php", + "lib/Exceptions/", + "deprecated/Exceptions/", + "generated/Exceptions/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" ], - "time": "2020-07-12T23:59:07+00:00" + "description": "PHP core functions that throw exceptions instead of returning FALSE on error", + "support": { + "issues": "https://github.com/thecodingmachine/safe/issues", + "source": "https://github.com/thecodingmachine/safe/tree/v2.5.0" + }, + "time": "2023-04-05T11:54:14+00:00" }, { "name": "webmozart/assert", - "version": "1.10.0", + "version": "1.11.0", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", - "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", - "symfony/polyfill-ctype": "^1.8" + "ext-ctype": "*", + "php": "^7.2 || ^8.0" }, "conflict": { "phpstan/phpstan": "<0.12.20", @@ -1949,9 +3088,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.10.0" + "source": "https://github.com/webmozarts/assert/tree/1.11.0" }, - "time": "2021-03-09T10:59:23+00:00" + "time": "2022-06-03T18:03:27+00:00" } ], "aliases": [], diff --git a/config/php-scoper/scoper.inc.php b/config/php-scoper/scoper.inc.php new file mode 100644 index 0000000..702f3ff --- /dev/null +++ b/config/php-scoper/scoper.inc.php @@ -0,0 +1,18 @@ + 'IndieAuth\\Libs', + + 'finders' => [ + Finder::create()->files()->in( 'vendor/mf2/mf2' )->name( [ '*.php', 'LICENSE', 'composer.json' ] ), + Finder::create()->files()->in( 'vendor/barnabywalters/mf-cleaner' )->name( [ '*.php', 'LICENSE', 'composer.json' ] ), + Finder::create()->files()->in( 'vendor/firebase/php-jwt' )->name( [ '*.php', 'LICENSE', 'composer.json' ] ), + ], +]; + diff --git a/src/IndieAuth/AuthorizationCode.php b/src/IndieAuth/AuthorizationCode.php index f15b860..c5f6af7 100644 --- a/src/IndieAuth/AuthorizationCode.php +++ b/src/IndieAuth/AuthorizationCode.php @@ -13,7 +13,7 @@ namespace IndieAuth; use Exception; -use Firebase\JWT\{ +use IndieAuth\Libs\Firebase\JWT\{ JWT, Key }; diff --git a/tests/AuthorizationCodeTest.php b/tests/AuthorizationCodeTest.php new file mode 100644 index 0000000..9b3a3f7 --- /dev/null +++ b/tests/AuthorizationCodeTest.php @@ -0,0 +1,67 @@ + $client_id, + 'redirect_uri' => $redirect_uri, + 'scope' => $scope, + ], 'secret'); + + # code_id is 8 bytes, returned in hex, so expect 16 bytes + $this->assertEquals(16, mb_strlen($AuthorizationCode->code_id)); + + $this->assertArrayHasKey('client_id', $AuthorizationCode->request); + $this->assertArrayHasKey('redirect_uri', $AuthorizationCode->request); + $this->assertArrayHasKey('scope', $AuthorizationCode->request); + + $this->assertEquals($client_id, $AuthorizationCode->request['client_id']); + $this->assertEquals($redirect_uri, $AuthorizationCode->request['redirect_uri']); + $this->assertEquals($scope, $AuthorizationCode->request['scope']); + + $this->assertNotEmpty($AuthorizationCode->value); + } + + public function testDecode() + { + $client_id = 'https://another.example.com/'; + $redirect_uri = 'https://another.example.com/redirect'; + $scope = 'read'; + $secret = 'secret2'; + + $AuthorizationCode = AuthorizationCode::fromRequest([ + 'client_id' => $client_id, + 'redirect_uri' => $redirect_uri, + 'scope' => $scope, + ], $secret); + + $decoded = AuthorizationCode::decode( + $AuthorizationCode->value, + $secret); + + # code_id is 8 bytes, returned in hex, so expect 16 bytes + $this->assertEquals(16, mb_strlen($decoded['id'])); + + $this->assertArrayHasKey('client_id', $decoded); + $this->assertArrayHasKey('redirect_uri', $decoded); + $this->assertArrayHasKey('scope', $decoded); + + $this->assertEquals($client_id, $decoded['client_id']); + $this->assertEquals($redirect_uri, $decoded['redirect_uri']); + $this->assertEquals($scope, $decoded['scope']); + } +} + diff --git a/tests/ServerTest.php b/tests/ServerTest.php index 0e22281..8eabfa2 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace ProcessWire; +namespace IndieAuth\Tests; use IndieAuth\Server; use PHPUnit\Framework\TestCase; From 416ab00d34e6781a044b10dbed2892969286a0d9 Mon Sep 17 00:00:00 2001 From: Gregor Morrill Date: Sun, 5 Nov 2023 14:30:26 -0800 Subject: [PATCH 07/17] Add scoped dependencies Remove previous dependencies from /vendor --- libs/barnabywalters/mf-cleaner/composer.json | 27 + .../mf-cleaner/src/functions.php | 712 ++++ .../mf-cleaner/tests/CleanerTest.php | 434 ++ {vendor => libs}/firebase/php-jwt/LICENSE | 0 .../firebase/php-jwt/composer.json | 20 +- .../php-jwt/src/BeforeValidException.php | 17 + libs/firebase/php-jwt/src/CachedKeySet.php | 227 ++ .../firebase/php-jwt/src/ExpiredException.php | 17 + libs/firebase/php-jwt/src/JWK.php | 268 ++ libs/firebase/php-jwt/src/JWT.php | 572 +++ .../src/JWTExceptionWithPayloadInterface.php | 21 + libs/firebase/php-jwt/src/Key.php | 51 + .../php-jwt/src/SignatureInvalidException.php | 3 +- libs/mf2/mf2/Mf2/Parser.php | 1800 ++++++++ libs/mf2/mf2/composer.json | 45 + .../mf2/tests/Mf2/ClassicMicroformatsTest.php | 962 +++++ .../tests/Mf2/CombinedMicroformatsTest.php | 415 ++ .../tests/Mf2/MicroformatsTestSuiteTest.php | 140 + .../Mf2/MicroformatsWikiExamplesTest.php | 196 + libs/mf2/mf2/tests/Mf2/ParseDTTest.php | 510 +++ libs/mf2/mf2/tests/Mf2/ParseHtmlIdTest.php | 40 + libs/mf2/mf2/tests/Mf2/ParseImpliedTest.php | 389 ++ libs/mf2/mf2/tests/Mf2/ParseLanguageTest.php | 253 ++ libs/mf2/mf2/tests/Mf2/ParsePTest.php | 131 + libs/mf2/mf2/tests/Mf2/ParseUTest.php | 319 ++ .../tests/Mf2/ParseValueClassTitleTest.php | 100 + libs/mf2/mf2/tests/Mf2/ParserTest.php | 791 ++++ libs/mf2/mf2/tests/Mf2/PlainTextTest.php | 24 + libs/mf2/mf2/tests/Mf2/RelTest.php | 211 + libs/mf2/mf2/tests/Mf2/URLTest.php | 187 + libs/mf2/mf2/tests/Mf2/bootstrap.php | 5 + phpunit.xml | 4 + vendor/autoload.php | 20 +- vendor/barnabywalters/mf-cleaner/.gitignore | 6 - vendor/barnabywalters/mf-cleaner/README.md | 111 - .../barnabywalters/mf-cleaner/composer.json | 23 - .../barnabywalters/mf-cleaner/composer.lock | 601 --- .../mf-cleaner/nbproject/project.properties | 11 - .../mf-cleaner/nbproject/project.xml | 9 - vendor/barnabywalters/mf-cleaner/phpunit.xml | 29 - .../src/BarnabyWalters/Mf2/Functions.php | 330 -- .../mf-cleaner/test/CleanerTest.php | 386 -- .../mf-cleaner/test/bootstrap.php | 5 - vendor/bin/fetch-mf2 | 40 - vendor/bin/parse-mf2 | 37 - vendor/composer/ClassLoader.php | 198 +- vendor/composer/InstalledVersions.php | 585 +-- vendor/composer/autoload_classmap.php | 28 +- vendor/composer/autoload_files.php | 6 +- vendor/composer/autoload_namespaces.php | 2 +- vendor/composer/autoload_psr4.php | 3 +- vendor/composer/autoload_real.php | 57 +- vendor/composer/autoload_static.php | 46 +- vendor/composer/installed.json | 173 +- vendor/composer/installed.php | 68 +- vendor/composer/platform_check.php | 26 - vendor/firebase/php-jwt/README.md | 282 -- .../php-jwt/src/BeforeValidException.php | 7 - .../firebase/php-jwt/src/ExpiredException.php | 7 - vendor/firebase/php-jwt/src/JWK.php | 172 - vendor/firebase/php-jwt/src/JWT.php | 544 --- vendor/mf2/mf2/.editorconfig | 7 - vendor/mf2/mf2/.gitignore | 7 - vendor/mf2/mf2/.travis.yml | 15 - vendor/mf2/mf2/LICENSE.md | 36 - vendor/mf2/mf2/Mf2/Parser.php | 2313 ----------- vendor/mf2/mf2/README.md | 645 --- vendor/mf2/mf2/bin/fetch-mf2 | 40 - vendor/mf2/mf2/bin/parse-mf2 | 37 - vendor/mf2/mf2/composer.json | 29 - vendor/mf2/mf2/composer.lock | 3619 ----------------- vendor/mf2/mf2/phpunit.xml | 8 - 72 files changed, 9517 insertions(+), 9942 deletions(-) create mode 100644 libs/barnabywalters/mf-cleaner/composer.json create mode 100644 libs/barnabywalters/mf-cleaner/src/functions.php create mode 100644 libs/barnabywalters/mf-cleaner/tests/CleanerTest.php rename {vendor => libs}/firebase/php-jwt/LICENSE (100%) rename {vendor => libs}/firebase/php-jwt/composer.json (52%) create mode 100644 libs/firebase/php-jwt/src/BeforeValidException.php create mode 100644 libs/firebase/php-jwt/src/CachedKeySet.php create mode 100644 libs/firebase/php-jwt/src/ExpiredException.php create mode 100644 libs/firebase/php-jwt/src/JWK.php create mode 100644 libs/firebase/php-jwt/src/JWT.php create mode 100644 libs/firebase/php-jwt/src/JWTExceptionWithPayloadInterface.php create mode 100644 libs/firebase/php-jwt/src/Key.php rename {vendor => libs}/firebase/php-jwt/src/SignatureInvalidException.php (58%) create mode 100644 libs/mf2/mf2/Mf2/Parser.php create mode 100644 libs/mf2/mf2/composer.json create mode 100644 libs/mf2/mf2/tests/Mf2/ClassicMicroformatsTest.php create mode 100644 libs/mf2/mf2/tests/Mf2/CombinedMicroformatsTest.php create mode 100644 libs/mf2/mf2/tests/Mf2/MicroformatsTestSuiteTest.php create mode 100644 libs/mf2/mf2/tests/Mf2/MicroformatsWikiExamplesTest.php create mode 100644 libs/mf2/mf2/tests/Mf2/ParseDTTest.php create mode 100644 libs/mf2/mf2/tests/Mf2/ParseHtmlIdTest.php create mode 100644 libs/mf2/mf2/tests/Mf2/ParseImpliedTest.php create mode 100644 libs/mf2/mf2/tests/Mf2/ParseLanguageTest.php create mode 100644 libs/mf2/mf2/tests/Mf2/ParsePTest.php create mode 100644 libs/mf2/mf2/tests/Mf2/ParseUTest.php create mode 100644 libs/mf2/mf2/tests/Mf2/ParseValueClassTitleTest.php create mode 100644 libs/mf2/mf2/tests/Mf2/ParserTest.php create mode 100644 libs/mf2/mf2/tests/Mf2/PlainTextTest.php create mode 100644 libs/mf2/mf2/tests/Mf2/RelTest.php create mode 100644 libs/mf2/mf2/tests/Mf2/URLTest.php create mode 100644 libs/mf2/mf2/tests/Mf2/bootstrap.php create mode 100644 phpunit.xml delete mode 100644 vendor/barnabywalters/mf-cleaner/.gitignore delete mode 100644 vendor/barnabywalters/mf-cleaner/README.md delete mode 100644 vendor/barnabywalters/mf-cleaner/composer.json delete mode 100644 vendor/barnabywalters/mf-cleaner/composer.lock delete mode 100644 vendor/barnabywalters/mf-cleaner/nbproject/project.properties delete mode 100644 vendor/barnabywalters/mf-cleaner/nbproject/project.xml delete mode 100644 vendor/barnabywalters/mf-cleaner/phpunit.xml delete mode 100644 vendor/barnabywalters/mf-cleaner/src/BarnabyWalters/Mf2/Functions.php delete mode 100644 vendor/barnabywalters/mf-cleaner/test/CleanerTest.php delete mode 100644 vendor/barnabywalters/mf-cleaner/test/bootstrap.php delete mode 100644 vendor/bin/fetch-mf2 delete mode 100644 vendor/bin/parse-mf2 delete mode 100644 vendor/composer/platform_check.php delete mode 100644 vendor/firebase/php-jwt/README.md delete mode 100644 vendor/firebase/php-jwt/src/BeforeValidException.php delete mode 100644 vendor/firebase/php-jwt/src/ExpiredException.php delete mode 100644 vendor/firebase/php-jwt/src/JWK.php delete mode 100644 vendor/firebase/php-jwt/src/JWT.php delete mode 100644 vendor/mf2/mf2/.editorconfig delete mode 100644 vendor/mf2/mf2/.gitignore delete mode 100644 vendor/mf2/mf2/.travis.yml delete mode 100644 vendor/mf2/mf2/LICENSE.md delete mode 100644 vendor/mf2/mf2/Mf2/Parser.php delete mode 100644 vendor/mf2/mf2/README.md delete mode 100644 vendor/mf2/mf2/bin/fetch-mf2 delete mode 100644 vendor/mf2/mf2/bin/parse-mf2 delete mode 100644 vendor/mf2/mf2/composer.json delete mode 100644 vendor/mf2/mf2/composer.lock delete mode 100644 vendor/mf2/mf2/phpunit.xml diff --git a/libs/barnabywalters/mf-cleaner/composer.json b/libs/barnabywalters/mf-cleaner/composer.json new file mode 100644 index 0000000..717afcf --- /dev/null +++ b/libs/barnabywalters/mf-cleaner/composer.json @@ -0,0 +1,27 @@ +{ + "name": "barnabywalters\/mf-cleaner", + "description": "Cleans up microformats2 array structures", + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit\/phpunit": "~9", + "vimeo\/psalm": "~4" + }, + "autoload": { + "files": [ + "src\/functions.php" + ] + }, + "license": "MIT", + "authors": [ + { + "name": "Barnaby Walters", + "email": "barnaby@waterpigs.co.uk" + } + ], + "suggest": { + "mf2\/mf2": "To parse microformats2 structures from (X)HTML" + }, + "minimum-stability": "dev" +} \ No newline at end of file diff --git a/libs/barnabywalters/mf-cleaner/src/functions.php b/libs/barnabywalters/mf-cleaner/src/functions.php new file mode 100644 index 0000000..7073a2c --- /dev/null +++ b/libs/barnabywalters/mf-cleaner/src/functions.php @@ -0,0 +1,712 @@ + $val) { + if (\is_numeric($key)) { + return \true; + } + } + return \false; +} +/** + * Verifies if $mf is an array without numeric keys, and has a 'properties' key. + * @param $mf + * @return bool + * @internal + */ +function isMicroformat($mf) : bool +{ + return \is_array($mf) and !hasNumericKeys($mf) and !empty($mf['type']) and isset($mf['properties']); +} +/** + * Verifies if $mf has an 'items' key which is also an array, returns true. + * @param $mf + * @return bool + * @internal + */ +function isMicroformatCollection($mf) : bool +{ + return \is_array($mf) and isset($mf['items']) and \is_array($mf['items']); +} +/** + * Verifies if $p is an array without numeric keys and has key 'value' and 'html' set. + * @param $p + * @return bool + * @internal + */ +function isEmbeddedHtml($p) : bool +{ + return \is_array($p) and !hasNumericKeys($p) and isset($p['value']) and isset($p['html']); +} +/** + * Checks to see if the passed value is an img-alt structure. + * @param $p + * @return bool + * @internal + */ +function isImgAlt($p) : bool +{ + return \is_array($p) and !hasNumericKeys($p) and isset($p['value']) and isset($p['alt']); +} +/** + * Verifies if property named $propName is in array $mf. + * @param array $mf + * @param $propName + * @return bool + * @internal + */ +function hasProp(array $mf, $propName) : bool +{ + return !empty($mf['properties'][$propName]) and \is_array($mf['properties'][$propName]); +} +/** + * shortcut for getPlaintext. + * @deprecated use getPlaintext from now on + * @param array $mf + * @param $propName + * @param null|string $fallback + * @return mixed|null + * @internal + */ +function getProp(array $mf, $propName, $fallback = null) +{ + return getPlaintext($mf, $propName, $fallback); +} +/** + * If $v is a microformat, embedded html, or img-alt structure, return $v['value']. Else return v. + * @param $v + * @return mixed + * @internal + */ +function toPlaintext($v) +{ + if (isMicroformat($v) or isEmbeddedHtml($v) or isImgAlt($v)) { + return $v['value']; + } + return $v; +} +/** + * Returns plaintext of $propName with optional $fallback + * @param array $mf + * @param $propName + * @param null|string $fallback + * @return mixed|null + * @link http://php.net/manual/en/function.current.php + * @internal + */ +function getPlaintext(array $mf, $propName, $fallback = null) +{ + if (!empty($mf['properties'][$propName]) and \is_array($mf['properties'][$propName])) { + return toPlaintext(\current($mf['properties'][$propName])); + } + return $fallback; +} +/** + * Converts $propName in $mf into array_map plaintext, or $fallback if not valid. + * @param array $mf + * @param $propName + * @param mixed $fallback default null + * @return mixed + * @internal + */ +function getPlaintextArray(array $mf, $propName, $fallback = null) +{ + if (!empty($mf['properties'][$propName]) and \is_array($mf['properties'][$propName])) { + return \array_map(__NAMESPACE__ . '\\toPlaintext', $mf['properties'][$propName]); + } + return $fallback; +} +/** + * Returns ['html'] element of $v, or ['value'] or just $v, in order of availablility. + * @param $v + * @return mixed + * @internal + */ +function toHtml($v) +{ + if (isEmbeddedHtml($v)) { + return $v['html']; + } elseif (isMicroformat($v)) { + return \htmlspecialchars($v['value']); + } + return \htmlspecialchars($v); +} +/** + * Gets HTML of $propName or if not, $fallback + * @param array $mf + * @param $propName + * @param null|string $fallback + * @return mixed|null + * @internal + */ +function getHtml(array $mf, $propName, $fallback = null) +{ + if (!empty($mf['properties'][$propName]) and \is_array($mf['properties'][$propName])) { + return toHtml(\current($mf['properties'][$propName])); + } + return $fallback; +} +/** + * To img-alt + * + * Converts a value to an img-alt `{'value': '', 'alt': ''}` structure. Passes through existing + * img-alt structures unchanged. For anything else, converts it to its plaintext representation, + * put that in the `value` key, and adds an empty `alt` key. + * + * @param $v + * @return array + * @internal + */ +function toImgAlt($v) +{ + if (isImgAlt($v)) { + return $v; + } + return ['value' => toPlaintext($v), 'alt' => '']; +} +/** + * Get img-alt + * + * If `$propName` exists on `$mf`, return an img-alt representation of it (via `toImgAlt`). If the + * property does not exist, return `$fallback` (default null) + * + * @param array $mf + * @param string $propName + * @param mixed $fallback + * @return array|mixed + * @internal + */ +function getImgAlt(array $mf, string $propName, $fallback = null) +{ + if (isMicroformat($mf) and !empty($mf['properties'][$propName]) and \is_array($mf['properties'][$propName])) { + return toImgAlt(\current($mf['properties'][$propName])); + } + return $fallback; +} +/** + * Returns 'summary' element of $mf or a truncated Plaintext of $mf['properties']['content'] with 19 chars and ellipsis. + * @deprecated as not often used + * @param array $mf + * @return mixed|null|string + * @internal + */ +function getSummary(array $mf) +{ + if (hasProp($mf, 'summary')) { + return getPlaintext($mf, 'summary'); + } + if (!empty($mf['properties']['content'])) { + return \substr(\strip_tags(getPlaintext($mf, 'content') ?? ''), 0, 19) . '…'; + } +} +/** + * Gets the date published of $mf array. + * @param array $mf + * @param bool $ensureValid + * @param null|string $fallback optional result if date not available + * @return mixed|null + * @internal + */ +function getPublished(array $mf, $ensureValid = \false, $fallback = null) +{ + return getDateTimeProperty('published', $mf, $ensureValid, $fallback); +} +/** + * Gets the date updated of $mf array. + * @param array $mf + * @param bool $ensureValid + * @param null $fallback + * @return mixed|null + * @internal + */ +function getUpdated(array $mf, $ensureValid = \false, $fallback = null) +{ + return getDateTimeProperty('updated', $mf, $ensureValid, $fallback); +} +/** + * Gets the DateTime properties including published or updated, depending on params. + * @param $name string updated or published + * @param array $mf + * @param bool $ensureValid + * @param null|string $fallback + * @return mixed|null + * @internal + */ +function getDateTimeProperty($name, array $mf, $ensureValid = \false, $fallback = null) +{ + $compliment = 'published' === $name ? 'updated' : 'published'; + if (hasProp($mf, $name)) { + $return = getPlaintext($mf, $name); + } elseif (hasProp($mf, $compliment)) { + $return = getPlaintext($mf, $compliment); + } else { + return $fallback; + } + if (!$ensureValid) { + return $return; + } else { + try { + new DateTime($return ?? ''); + return $return; + } catch (Exception $e) { + return $fallback; + } + } +} +/** + * True if same hostname is parsed on both + * @param $u1 string url + * @param $u2 string url + * @return bool + * @link http://php.net/manual/en/function.parse-url.php + * @internal + */ +function sameHostname($u1, $u2) +{ + return \parse_url($u1, \PHP_URL_HOST) === \parse_url($u2, \PHP_URL_HOST); +} +/** + * Large function for fishing out author of $mf from various possible array elements. + * @param array $mf + * @param array|null $context + * @param null $url + * @param bool $matchName + * @param bool $matchHostname + * @return mixed|null + * @todo: this needs to be just part of an indiewebcamp.com/authorship algorithm, at the moment it tries to do too much + * @todo: maybe split some bits of this out into separate functions + * + * @internal + */ +function getAuthor(array $mf, array $context = null, $url = null, $matchName = \true, $matchHostname = \true) +{ + $entryAuthor = null; + if (null === $url and hasProp($mf, 'url')) { + $url = getPlaintext($mf, 'url'); + } + if (hasProp($mf, 'author') and isMicroformat(\current($mf['properties']['author']))) { + $entryAuthor = \current($mf['properties']['author']); + } elseif (hasProp($mf, 'reviewer') and isMicroformat(\current($mf['properties']['author']))) { + $entryAuthor = \current($mf['properties']['reviewer']); + } elseif (hasProp($mf, 'author')) { + $entryAuthor = getPlaintext($mf, 'author'); + } + // If we have no context that’s the best we can do + if (null === $context) { + return $entryAuthor; + } + // Whatever happens after this we’ll need these + $flattenedMf = flattenMicroformats($context); + $hCards = findMicroformatsByType($flattenedMf, 'h-card', \false); + if (\is_string($entryAuthor)) { + // look through all page h-cards for one with this URL + $authorHCards = findMicroformatsByProperty($hCards, 'url', $entryAuthor, \false); + if (!empty($authorHCards)) { + $entryAuthor = \current($authorHCards); + } + } + if (\is_string($entryAuthor) and $matchName) { + // look through all page h-cards for one with this name + $authorHCards = findMicroformatsByProperty($hCards, 'name', $entryAuthor, \false); + if (!empty($authorHCards)) { + $entryAuthor = \current($authorHCards); + } + } + if (null !== $entryAuthor) { + return $entryAuthor; + } + // Look for an "author" property on the top-level "h-feed" if present + $feed = findMicroformatsByType($flattenedMf, 'h-feed', \false); + if ($feed) { + $feed = \current($feed); + if ($feed && isMicroformat($feed) && !empty($feed['properties']) && !empty($feed['properties']['author'])) { + return \current($feed['properties']['author']); + } + } + // look for page-wide rel-author, h-card with that + if (!empty($context['rels']) and !empty($context['rels']['author'])) { + // Grab first href with rel=author + $relAuthorHref = \current($context['rels']['author']); + $relAuthorHCards = findMicroformatsByProperty($hCards, 'url', $relAuthorHref); + if (!empty($relAuthorHCards)) { + return \current($relAuthorHCards); + } + } + // look for h-card with same hostname as $url if given + if (null !== $url and $matchHostname) { + $sameHostnameHCards = findMicroformatsByCallable($hCards, function ($mf) use($url) { + if (!hasProp($mf, 'url')) { + return \false; + } + foreach ($mf['properties']['url'] as $u) { + if (sameHostname($url, $u)) { + return \true; + } + } + }, \false); + if (!empty($sameHostnameHCards)) { + return \current($sameHostnameHCards); + } + } + // Without fetching, this is the best we can do. Return the found string value, or null. + return empty($relAuthorHref) ? null : $relAuthorHref; +} +/** + * Returns array per parse_url standard with pathname key added. + * @param $url + * @return mixed + * @link http://php.net/manual/en/function.parse-url.php + * @internal + */ +function parseUrl($url) +{ + $r = \parse_url($url); + if (empty($r['path'])) { + $r['path'] = '/'; + } + return $r; +} +/** + * See if urls match for each component of parsed urls. Return true if so. + * @param $url1 + * @param $url2 + * @return bool + * @see parseUrl() + * @internal + */ +function urlsMatch($url1, $url2) +{ + $u1 = parseUrl($url1); + $u2 = parseUrl($url2); + foreach (\array_unique(\array_merge(\array_keys($u1), \array_keys($u2))) as $component) { + if (!\array_key_exists($component, $u1) or !\array_key_exists($component, $u2)) { + return \false; + } + if ($u1[$component] != $u2[$component]) { + return \false; + } + } + return \true; +} +/** + * Given two arrays of URLs, determine if any of them match + * @return bool + * @internal + */ +function anyUrlsMatch($array1, $array2) +{ + if (!(\is_array($array1) && \is_array($array2))) { + throw new \InvalidArgumentException('anyUrlsMatch must be called with two arrays'); + } + foreach ($array1 as $url1) { + foreach ($array2 as $url2) { + if (urlsMatch($url1, $url2)) { + return \true; + } + } + } + return \false; +} +/** + * Representative h-card + * + * Given the microformats on a page representing a person or organisation (h-card), find the single h-card which is + * representative of the page, or null if none is found. + * + * @see http://microformats.org/wiki/representative-h-card-parsing + * + * @param array $mfs The parsed microformats of a page to search for a representative h-card + * @param string $url The URL the microformats were fetched from + * @return array|null Either a single h-card array structure, or null if none was found + * @internal + */ +function getRepresentativeHCard(array $mfs, $url) +{ + $hCards = findMicroformatsByType($mfs, 'h-card'); + /** + * If the page contains an h-card with uid and url properties + * both matching the page URL, the first such h-card is the + * representative h-card + */ + $hCardMatches = findMicroformatsByCallable($hCards, function ($hCard) use($url) { + $hCardUid = getPlaintext($hCard, 'uid'); + $hCardUrls = getPlaintextArray($hCard, 'url'); + # h-card must have uid and url properties + if (!($hCardUid && $hCardUrls)) { + return \false; + } + # uid must match the page URL + if (!urlsMatch($hCardUid, $url)) { + return \false; + } + # at least one h-card.url property must match the page URL + if (anyUrlsMatch($hCardUrls, [$url])) { + return \true; + } + return \false; + }); + if (\count($hCardMatches) > 0) { + return $hCardMatches[0]; + } + /** + * If no representative h-card was found, if the page contains an h-card + * with a url property value which also has a rel=me relation + * (i.e. matches a URL in parse_results.rels.me), the first such h-card + * is the representative h-card + */ + if (!empty($mfs['rels']['me'])) { + $hCardMatches = findMicroformatsByCallable($hCards, function ($hCard) use($mfs) { + $hCardUrls = getPlaintextArray($hCard, 'url'); + # h-card must have url property + if (!$hCardUrls) { + return \false; + } + # at least one h-card.url property must match a rel-me URL + if (anyUrlsMatch($hCardUrls, $mfs['rels']['me'])) { + return \true; + } + return \false; + }); + if (\count($hCardMatches) > 0) { + return $hCardMatches[0]; + } + } + /** + * If no representative h-card was found, if the page contains + * one single h-card, and the h-card has a url property matching + * the page URL, that h-card is the representative h-card + */ + $hCardMatches = []; + if (\count($hCards) == 1) { + $hCardMatches = findMicroformatsByCallable($hCards, function ($hCard) use($url) { + $hCardUrls = getPlaintextArray($hCard, 'url'); + # h-card must have url property + if (!$hCardUrls) { + return \false; + } + # at least one h-card.url property must match the page URL + if (anyUrlsMatch($hCardUrls, [$url])) { + return \true; + } + return \false; + }); + if (\count($hCardMatches) === 1) { + return $hCardMatches[0]; + } + } + // Otherwise, no representative h-card could be found. + return null; +} +/** + * Makes microformat properties into a flattened array, returned. + * @param array $mf + * @return array + * @internal + */ +function flattenMicroformatProperties(array $mf) +{ + $items = array(); + if (!isMicroformat($mf)) { + return $items; + } + foreach ($mf['properties'] as $propArray) { + foreach ($propArray as $prop) { + if (isMicroformat($prop)) { + $items[] = $prop; + $items = \array_merge($items, flattenMicroformatProperties($prop)); + } + } + } + return $items; +} +/** + * Flattens microformats. Can intake multiple Microformats including possible MicroformatCollection. + * @param array $mfs + * @return array + * @internal + */ +function flattenMicroformats(array $mfs) +{ + if (isMicroformatCollection($mfs)) { + $mfs = $mfs['items']; + } elseif (isMicroformat($mfs)) { + $mfs = array($mfs); + } + $items = array(); + foreach ($mfs as $mf) { + $items[] = $mf; + $items = \array_merge($items, flattenMicroformatProperties($mf)); + if (empty($mf['children'])) { + continue; + } + foreach ($mf['children'] as $child) { + $items[] = $child; + $items = \array_merge($items, flattenMicroformatProperties($child)); + } + } + return $items; +} +/** + * Find Microformats By Type + * + * Traverses a mf2 tree and returns all microformats objects whose type matches the one + * given. + * + * @param array $mfs + * @param $name + * @param bool $flatten + * @return mixed + * @internal + */ +function findMicroformatsByType(array $mfs, $name, $flatten = \true) +{ + return findMicroformatsByCallable($mfs, function ($mf) use($name) { + return \in_array($name, $mf['type']); + }, $flatten); +} +/** + * + * @param array $mfs + * @param $propName + * @param $propValue + * @param bool $flatten + * @return mixed + * @see findMicroformatsByCallable() + * @internal + */ +function findMicroformatsByProperty(array $mfs, $propName, $propValue, $flatten = \true) +{ + return findMicroformatsByCallable($mfs, function ($mf) use($propName, $propValue) { + if (!hasProp($mf, $propName)) { + return \false; + } + if (\in_array($propValue, $mf['properties'][$propName])) { + return \true; + } + return \false; + }, $flatten); +} +/** + * $callable should be a function or an exception will be thrown. $mfs can accept microformat collections. + * If $flatten is true then the result will be flattened. + * @param array $mfs + * @param $callable + * @param bool $flatten + * @return mixed + * @link http://php.net/manual/en/function.is-callable.php + * @see flattenMicroformats() + * @internal + */ +function findMicroformatsByCallable(array $mfs, $callable, $flatten = \true) +{ + if (!\is_callable($callable)) { + throw new \InvalidArgumentException('$callable must be callable'); + } + if ($flatten and (isMicroformat($mfs) or isMicroformatCollection($mfs))) { + $mfs = flattenMicroformats($mfs); + } + return \array_values(\array_filter($mfs, $callable)); +} +/** + * Remove False Positive Root Microformats + * + * Unfortunately, a well-known CSS framework uses some non-semantic classnames which look like root + * classnames to the microformats2 parsing algorithm. This function takes either a single microformat + * or a mf2 tree and restructures it as if the false positive classnames had never been there. + * + * Always returns a microformat collection (`{"items": []}`) even when passed a single microformat, as + * if the single microformat was a false positive, it may be replaced with more than one child. + * + * The default list of known false positives is stored in `FALSE_POSITIVE_ROOT_CLASSNAME_REGEXES` and + * is used by default. You can provide your own list if you want. Some of the known false positives are + * prefixes, so the values of `$classnamesToRemove` must all be regexes (e.g. `'/h-wrong/'`). + * @internal + */ +function removeFalsePositiveRootMicroformats(array $mfs, ?array $classnamesToRemove = null) +{ + if (\is_null($classnamesToRemove)) { + $classnamesToRemove = FALSE_POSITIVE_ROOT_CLASSNAME_REGEXES; + } + if (!isMicroformatCollection($mfs)) { + if (isMicroformat($mfs)) { + $mfs = ['items' => [$mfs]]; + } + // Nothing we can do with this, return it as-is. + return $mfs; + } + $correctedTree = ['items' => []]; + $recurse = function ($mf) use(&$recurse, $classnamesToRemove) { + foreach ($mf['properties'] as $prop => $values) { + $newPropVals = []; + foreach ($values as $value) { + if (isMicroformat($value)) { + $newPropVals = \array_merge($newPropVals, $recurse($value)); + } else { + $newPropVals[] = $value; + } + } + $mf['properties'][$prop] = $newPropVals; + } + if (!empty($mf['children'])) { + $correctedChildren = []; + foreach ($mf['children'] as $child) { + $correctedChildren = \array_merge($correctedChildren, $recurse($child)); + } + $mf['children'] = $correctedChildren; + } + // If this mf structure’s types are all false-positive classnames, replace it with its children. + $hasOnlyFalsePositiveRootClassnames = \true; + foreach ($mf['type'] as $mft) { + $currentTypeIsFalsePositive = \false; + foreach ($classnamesToRemove as $ctr) { + if (1 === \preg_match($ctr, $mft)) { + $currentTypeIsFalsePositive = \true; + break; + } + } + if (\false === $currentTypeIsFalsePositive) { + $hasOnlyFalsePositiveRootClassnames = \false; + break; + } + } + if ($hasOnlyFalsePositiveRootClassnames) { + return \array_key_exists('children', $mf) ? $mf['children'] : []; + } else { + return [$mf]; + } + }; + foreach ($mfs['items'] as $mf) { + $correctedTree['items'] = \array_merge($correctedTree['items'], $recurse($mf)); + } + return $correctedTree; +} +/** @internal */ +const FALSE_POSITIVE_ROOT_CLASSNAME_REGEXES = [ + // https://tailwindcss.com/docs/height + '/h-px/', + '/h-auto/', + '/h-full/', + '/h-screen/', + '/h-min/', + '/h-max/', + '/h-fit/', + // https://chat.indieweb.org/dev/2022-11-14/1668463558928800 + '/h-screen-[a-zA-Z0-9\\-\\_]+/', + '/h-full-[a-zA-Z0-9\\-\\_]+/', +]; diff --git a/libs/barnabywalters/mf-cleaner/tests/CleanerTest.php b/libs/barnabywalters/mf-cleaner/tests/CleanerTest.php new file mode 100644 index 0000000..1dcf645 --- /dev/null +++ b/libs/barnabywalters/mf-cleaner/tests/CleanerTest.php @@ -0,0 +1,434 @@ + $arg) { + if (\is_array($arg) and !isMicroformat($arg) and !isEmbeddedHtml($arg)) { + $properties[$name] = $arg; + } else { + $properties[$name] = [$arg]; + } + } + return ['type' => $type, 'properties' => $properties, 'value' => $value]; + } + public function testIsMicroformatReturnsFalseIfNotArray() + { + $this->assertFalse(isMicroformat('')); + } + public function testIsMicroformatReturnsFalseIfTypeMissing() + { + $this->assertFalse(isMicroformat(['properties' => []])); + } + public function testIsMicroformatReturnsFalseIfPropertiesMissing() + { + $this->assertFalse(isMicroformat(['type' => ['h-thing']])); + } + public function testIsMicroformatReturnsFalseIfHasNumericKeys() + { + $this->assertFalse(isMicroformat([[], 'thing' => []])); + } + public function testIsMicroformatReturnsTrueIfValueIsSet() + { + $this->assertTrue(isMicroformat(['type' => ['h-card'], 'properties' => [], 'value' => 'a string'])); + } + public function testHasNumericKeysWorks() + { + $withNumericKeys = ['a', 'b', 'c']; + $noNumericKeys = ['key' => 'value']; + $this->assertTrue(hasNumericKeys($withNumericKeys)); + $this->assertFalse(hasNumericKeys($noNumericKeys)); + } + public function testIsMicroformatCollectionChecksForItemsKey() + { + $this->assertTrue(isMicroformatCollection(['items' => []])); + $this->assertFalse(isMicroformatCollection(['notItems' => []])); + } + public function testGetSummaryPassesIfSummaryPresent() + { + $mf = $this->mf('h-entry', ['summary' => 'Hello Summary']); + $result = getSummary($mf); + $this->assertEquals($mf['properties']['summary'][0], $result); + } + public function testGetSummaryUsesStrippedFirstCharactersOfContent() + { + $result = getSummary(['type' => ['h-entry'], 'properties' => ['content' => ['

Hello hello hello there indeed

']]]); + $this->assertEquals('Hello hello hello t…', $result); + } + public function testGetPublishedPassesIfPublishedPresent() + { + $mf = $this->mf('h-entry', ['published' => '2013-12-06']); + $result = getPublished($mf); + $this->assertEquals(getPlaintext($mf, 'published'), $result); + } + public function testGetPublishedFallsBackToUpdated() + { + $mf = $this->mf('h-entry', ['updated' => '2013-12-06']); + $result = getPublished($mf); + $this->assertEquals(getPlaintext($mf, 'updated'), $result); + } + public function testGetPublishedReturnsNullIfValidDatetimeRequested() + { + $mf = $this->mf('h-entry', ['published' => 'werty']); + $this->assertNull(getPublished($mf, \true)); + $mf = $this->mf('h-entry', ['published' => '2022-01-01 10:00:00']); + $this->assertEquals('2022-01-01 10:00:00', getPublished($mf, \true)); + } + public function testGetPublishedReturnsNullIfNoPotentialValueFound() + { + $mf = $this->mf('h-entry', []); + $result = getPublished($mf); + $this->assertNull($result); + } + public function testGetPublishedReturnsFallbackIfProvided() + { + $mf = $this->mf('h-entry', []); + $this->assertEquals('fallback', getPublished($mf, \true, 'fallback')); + } + public function testGetUpdated() + { + $mf = $this->mf('h-entry', ['updated' => '2013-12-06']); + $this->assertEquals('2013-12-06', getUpdated($mf)); + } + public function testGetAuthorPassesIfAuthorPresent() + { + $mf = $this->mf('h-entry', ['author' => [$this->mf('h-card', ['name' => 'Me'])]]); + $this->assertEquals('Me', getPlaintext(getAuthor($mf), 'name')); + } + public function testGetAuthorFindsSeparateHCardWithSameName() + { + $nonAuthorCard = $this->mf('h-card', ['name' => 'Someone Else', 'url' => 'http://example.org']); + $card = $this->mf('h-card', ['name' => 'Me', 'url' => 'http://waterpigs.co.uk']); + $entry = $this->mf('h-entry', ['name' => 'Entry', 'author' => 'Me']); + $mfs = ['items' => [$nonAuthorCard, $card, $entry]]; + $result = getAuthor($entry, $mfs); + $this->assertEquals('http://waterpigs.co.uk', getPlaintext($result, 'url')); + } + public function testGetAuthorFindsSeparateHCardWithSameDomain() + { + $card = $this->mf('h-card', ['name' => 'Me', 'url' => 'http://waterpigs.co.uk']); + $entry = $this->mf('h-entry', ['name' => 'The Entry']); + $mfs = ['items' => [$entry, $card]]; + $result = getAuthor($entry, $mfs, 'http://waterpigs.co.uk/notes/1234'); + $this->assertEquals('Me', getPlaintext($result, 'name')); + } + public function testGetAuthorDerivesMissingUrlFromMf() + { + $card = $this->mf('h-card', ['name' => 'Me', 'url' => 'https://waterpigs.co.uk']); + $entry = $this->mf('h-entry', ['name' => 'The Entry', 'url' => 'https://waterpigs.co.uk/posts/1']); + $mfs = ['items' => [$entry, $card]]; + $this->assertEquals('Me', getPlaintext(getAuthor($entry, $mfs), 'name')); + } + public function testGetAuthorDoesntFallBackToFirstHCard() + { + $cards = [$this->mf('h-card', ['name' => 'Bill']), $this->mf('h-card', ['name' => 'James'])]; + $entry = $this->mf('h-entry', ['name' => 'Entry']); + $mfs = ['items' => $cards]; + $result = getAuthor($entry, $mfs); + $this->assertEquals(null, $result); + } + public function testGetAuthorFindsAuthorWithUrlOfPageRelAuthor() + { + $cards = [$this->mf('h-card', ['name' => 'N. T. Author']), $this->mf('h-card', ['name' => 'The Author', 'url' => 'http://example.com'])]; + $entry = $this->mf('h-entry', ['name' => 'Entry']); + $mfs = ['items' => $cards, 'rels' => ['author' => ['http://example.com']]]; + $result = getAuthor($entry, $mfs); + $this->assertEquals($cards[1], $result); + } + public function testFindMicroformatsByTypeFindsRootMicroformats() + { + $mfs = ['items' => [['type' => ['h-card'], 'properties' => ['name' => ['me']]]]]; + $result = findMicroformatsByType($mfs, 'h-card'); + $this->assertEquals('me', getPlaintext($result[0], 'name')); + } + public function testFlattenMicroformatsReturnsFlatArrayOfMicroformats() + { + $org = $this->mf('h-card', ['name' => 'organisation']); + $card = $this->mf('h-card', ['name' => 'me', 'org' => [$org]]); + $entry = $this->mf('h-entry', ['name' => 'blog posting']); + $card['children'] = [$entry]; + $mfs = ['items' => [$card]]; + $result = flattenMicroformats($mfs); + $this->assertTrue(\in_array($org, $result)); + $this->assertTrue(\in_array($card, $result)); + $this->assertTrue(\in_array($entry, $result)); + } + public function testFindMicroformatsByProperty() + { + $mfs = ['items' => [$this->mf('h-card', ['name' => 'Me'])]]; + $results = findMicroformatsByProperty($mfs, 'name', 'Me'); + $this->assertEquals(1, \count($results)); + } + public function testFindMicroformatsByCallable() + { + $mfs = ['items' => [$this->mf('h-card', ['url' => 'http://waterpigs.co.uk/'])]]; + $results = findMicroformatsByCallable($mfs, function ($mf) { + if (!hasProp($mf, 'url')) { + return \false; + } + $urls = $mf['properties']['url']; + foreach ($urls as $url) { + if (\parse_url($url, \PHP_URL_HOST) === \parse_url('http://waterpigs.co.uk', \PHP_URL_HOST)) { + return \true; + } + } + return \false; + }); + $this->assertEquals(1, \count($results)); + try { + findMicroformatsByCallable($mfs, 'not a callable :P'); + $this->fail('No InvalidArgumentException thrown when a non-callable was passed to findMicroformatsByCallable'); + } catch (InvalidArgumentException $e) { + // Pass! + } + } + public function testFindMicroformatsSearchesSingleMicroformatStructure() + { + $card = $this->mf('h-card', ['name' => 'Me']); + $entry = $this->mf('h-entry', ['author' => [$card], 'name' => 'entry']); + $results = findMicroformatsByType($entry, 'h-card'); + $this->assertEquals(1, \count($results)); + } + public function testIsEmbeddedHtml() + { + $e = array('value' => '', 'html' => ''); + $this->assertTrue(isEmbeddedHtml($e)); + $this->assertFalse(isEmbeddedHtml(array())); + } + public function testIsImgAlt() + { + $this->assertFalse(isImgAlt(\false)); + $this->assertFalse(isImgAlt('string')); + $this->assertFalse(isImgAlt(['value' => 'no alt key tho'])); + $this->assertFalse(isImgAlt(['alt' => 'no value key tho'])); + $this->assertFalse(isImgAlt(['value' => 'yup', 'alt' => 'yup', 0 => 'got numeric keys tho'])); + $this->assertTrue(isImgAlt(['value' => 'yup', 'alt' => 'yup'])); + } + public function testGetPlaintextProperty() + { + $e = $this->mf('h-entry', ['name' => 'text', 'content' => ['text' => 'content', 'html' => 'content'], 'author' => [$this->mf('h-card', [], 'name')], 'photo' => [['value' => 'value', 'alt' => 'alt']]]); + $this->assertEquals('text', getPlaintext($e, 'name')); + $this->assertEquals('content', getPlaintext($e, 'content')); + $this->assertEquals('name', getPlaintext($e, 'author')); + $this->assertNull(getPlaintext($e, 'badprop')); + $this->assertEquals('fallback', getPlaintext($e, 'badprop', 'fallback')); + $this->assertEquals('value', getPlaintext($e, 'photo')); + // Deprecated, tested here to prevent regression and for coverage + $this->assertEquals('text', getProp($e, 'name')); + } + public function testGetPlaintextArray() + { + $e = $this->mf('h-entry', ['category' => ['text', 'more'], 'photo' => ['value1', ['value' => 'value2', 'alt' => 'alt']]]); + $this->assertEquals(['text', 'more'], getPlaintextArray($e, 'category')); + $this->assertNull(getPlaintextArray($e, 'badprop')); + $this->assertEquals('fallback', getPlaintextArray($e, 'badprop', 'fallback')); + } + public function testGetHtmlProperty() + { + $e = $this->mf('h-entry', ['name' => ['"text"<>'], 'content' => ['value' => 'content', 'html' => 'content'], 'author' => [$this->mf('h-card', [], '"name"<>')], 'photo' => [['value' => 'value', 'alt' => 'alt']]]); + $this->assertEquals('"text"<>', getHtml($e, 'name')); + $this->assertEquals('content', getHtml($e, 'content')); + $this->assertEquals('"name"<>', getHtml($e, 'author')); + $this->assertNull(getHtml($e, 'badprop')); + $this->assertEquals('fallback', getHtml($e, 'badprop', 'fallback')); + } + public function testGetImgAlt() + { + $e = $this->mf('h-entry', ['photo' => ['pval'], 'featured' => [['value' => 'fval', 'alt' => 'falt']], 'html' => [['value' => 'plain', 'html' => 'html']], 'embedded' => [$this->mf('h-card', [], 'epval')]]); + $this->assertEquals(['value' => 'pval', 'alt' => ''], getImgAlt($e, 'photo')); + $this->assertEquals(['value' => 'fval', 'alt' => 'falt'], getImgAlt($e, 'featured')); + $this->assertEquals(['value' => 'plain', 'alt' => ''], getImgAlt($e, 'html')); + $this->assertEquals(['value' => 'epval', 'alt' => ''], getImgAlt($e, 'embedded')); + } + public function testExpandAuthorExpandsFromLargerHCardsInContext() + { + $this->markTestSkipped(); + } + public function testMergeMicroformatsRecursivelyMerges() + { + $this->markTestSkipped(); + } + public function testGetAuthorDoesntReturnNonHCards() + { + $mf = ['items' => [['type' => ['h-entry'], 'properties' => ['url' => ['http://example.com/post/100'], 'name' => ['Some Entry']]], ['type' => ['h-card'], 'properties' => ['url' => ['http://example.com/'], 'name' => ['Mrs. Example']]]]]; + $author = getAuthor($mf['items'][0], $mf, 'http://example.com/post/100'); + $this->assertContains('h-card', $author['type']); + } + /** + * Test that URL path / and empty path match + */ + public function testUrlsMatchEmptyPath() + { + $url1 = 'https://example.com'; + $url2 = 'https://example.com/'; + $this->assertTrue(urlsMatch($url1, $url2)); + $this->assertTrue(urlsMatch($url2, $url1)); + } + /** + * Test that URL paths with different trailing slash don't match + */ + public function testUrlsTrailingSlashDontMatch() + { + $url1 = 'https://example.com/path'; + $url2 = 'https://example.com/path/'; + $this->assertFalse(urlsMatch($url1, $url2)); + $this->assertFalse(urlsMatch($url2, $url1)); + } + /** + * Test that URLs with different schemes don't match + */ + public function testUrlsDifferentSchemeDontMatch() + { + $url1 = 'http://example.com/path/post/'; + $url2 = 'https://example.com/path/post/'; + $this->assertFalse(urlsMatch($url1, $url2)); + $this->assertFalse(urlsMatch($url2, $url1)); + } + /** + * Test anyUrlsMatch() method comparing arrays of URLs + */ + public function testAnyUrlsMatchParameter1() + { + $this->expectException('InvalidArgumentException'); + $array = ['https://example.com/']; + anyUrlsMatch('string', $array); + } + public function testAnyUrlsMatchParameter2() + { + $this->expectException('InvalidArgumentException'); + $array = ['https://example.com/']; + anyUrlsMatch($array, 'string'); + } + public function testAnyUrlsMatchNoMatch() + { + $array1 = ['https://example.com/']; + $array2 = ['https://example.com/profile']; + $this->assertFalse(anyUrlsMatch($array1, $array2)); + $this->assertFalse(anyUrlsMatch($array2, $array1)); + } + public function testAnyUrlsMatch1() + { + $array1 = ['https://example.com/']; + $array2 = ['https://example.com/']; + $this->assertTrue(anyUrlsMatch($array1, $array2)); + $this->assertTrue(anyUrlsMatch($array2, $array1)); + } + public function testAnyUrlsMatch2() + { + $array1 = ['https://example.com/profile1', 'https://example.com/profile2', 'https://example.com/profile3']; + $array2 = ['https://example.com/profile3', 'https://example.com/profile2', 'https://example.com/profile5']; + $this->assertTrue(anyUrlsMatch($array1, $array2)); + $this->assertTrue(anyUrlsMatch($array2, $array1)); + } + /** + * Test the h-card `url` == `uid` == page URL method + * Use the first h-card that meets the criteria + */ + public function testGetRepresentativeHCardUrlUidSourceMethod() + { + $url = 'https://example.com'; + $mfs = ['items' => [['type' => ['h-card'], 'properties' => ['url' => ['https://example.com'], 'uid' => ['https://example.com'], 'name' => ['Correct h-card']]], ['type' => ['h-card'], 'properties' => ['url' => ['https://example.com'], 'uid' => ['https://example.com'], 'name' => ['Second h-card']]]]]; + $repHCard = getRepresentativeHCard($mfs, $url); + $this->assertNotNull($repHCard); + $this->assertEquals('Correct h-card', getPlaintext($repHCard, 'name')); + } + /** + * Test the h-card `url` == `rel-me` method + * Use the first h-card that meets the criteria + */ + public function testGetRepresentativeHCardUrlRelMeMethod() + { + $url = 'https://example.com'; + $mfs = ['items' => [['type' => ['h-card'], 'properties' => ['url' => ['https://example.org'], 'name' => ['Correct h-card']]], ['type' => ['h-card'], 'properties' => ['url' => ['https://example.org'], 'name' => ['Second h-card']]]], 'rels' => ['me' => ['https://example.org']]]; + $repHCard = getRepresentativeHCard($mfs, $url); + $this->assertNotNull($repHCard); + $this->assertEquals('Correct h-card', getPlaintext($repHCard, 'name')); + } + /** + * Test the *single* h-card with `url` == page URL method + */ + public function testGetRepresentativeHCardSingleHCardUrlSourceMethod() + { + $url = 'https://example.com'; + $mfs = ['items' => [['type' => ['h-card'], 'properties' => ['url' => ['https://example.com'], 'name' => ['Correct h-card']]]]]; + $repHCard = getRepresentativeHCard($mfs, $url); + $this->assertNotNull($repHCard); + $this->assertEquals('Correct h-card', getPlaintext($repHCard, 'name')); + } + /** + * Test no representative h-card when *multiple* h-card with `url` == page URL method + */ + public function testGetRepresentativeHCardMultipleHCardUrlSourceMethod() + { + $url = 'https://example.com'; + $mfs = ['items' => [['type' => ['h-card'], 'properties' => ['url' => ['https://example.com'], 'name' => ['First h-card']]], ['type' => ['h-card'], 'properties' => ['url' => ['https://example.com/user'], 'name' => ['Second h-card']]]]]; + $repHCard = getRepresentativeHCard($mfs, $url); + $this->assertNull($repHCard); + } + /** + * The getRepresentativeHCard() method used to return other h-* roots. + * Modified this previous test to ensure the h-entry is not returned + * even when its `url` == `uid` == page URL + */ + public function testGetRepresentativeHCardOnlyFindsHCard1() + { + $url = 'https://example.com'; + $mf = ['items' => [['type' => ['h-entry'], 'properties' => ['url' => ['https://example.com'], 'uid' => ['https://example.com'], 'name' => ['Not an h-card']]]]]; + $repHCard = getRepresentativeHCard($mf, $url); + $this->assertNull($repHCard); + } + /** + * The getRepresentativeHCard() method used to return other h-* roots. + * Modified this previous test to ensure the h-entry is not returned + * even when `url` == `rel-me` + */ + public function testGetRepresentativeHCardOnlyFindsHCard2() + { + $url = 'https://example.com'; + $mfs = ['items' => [['type' => ['h-entry'], 'properties' => ['url' => ['https://example.org'], 'name' => ['Not an h-card']]]], 'rels' => ['me' => ['https://example.org']]]; + $repHCard = getRepresentativeHCard($mfs, $url); + $this->assertNull($repHCard); + } + /** + * The getRepresentativeHCard() method used to return other h-* roots. + * Modified this previous test to ensure the h-entry is not returned + * even when *single* h-* with `url` == page URL + */ + public function testGetRepresentativeHCardOnlyFindsHCard3() + { + $url = 'https://example.com'; + $mfs = ['items' => [['type' => ['h-entry'], 'properties' => ['url' => ['https://example.com'], 'name' => ['Not an h-card']]]]]; + $repHCard = getRepresentativeHCard($mfs, $url); + $this->assertNull($repHCard); + } + public function testGetRepresentativeHCardIgnoresMultipleUrlPageUrlMatching() + { + $url = 'https://example.com'; + $mfs = ['items' => [['type' => ['h-entry'], 'properties' => ['url' => ['https://example.com'], 'name' => ['Not an h-card']]], ['type' => ['h-entry'], 'properties' => ['url' => ['https://example.com'], 'name' => ['Also not an h-card']]]]]; + $repHCard = getRepresentativeHCard($mfs, $url); + $this->assertNull($repHCard); + } + public function testRemoveFalsePositiveRootMicroformats() + { + // Based on https://www.lifelog.be/ninety-days-in-a-new-country as of 2022-11-15 + $test = ['items' => [['type' => ['h-full'], 'properties' => [], 'children' => [['type' => ['h-auto'], 'properties' => ['name' => ['']]], ['type' => ['h-entry'], 'properties' => ['name' => ['Ninety days in a new country"']]]]]]]; + $expected = ['items' => [['type' => ['h-entry'], 'properties' => ['name' => ['Ninety days in a new country"']]]]]; + $this->assertEquals($expected, removeFalsePositiveRootMicroformats($test)); + } +} diff --git a/vendor/firebase/php-jwt/LICENSE b/libs/firebase/php-jwt/LICENSE similarity index 100% rename from vendor/firebase/php-jwt/LICENSE rename to libs/firebase/php-jwt/LICENSE diff --git a/vendor/firebase/php-jwt/composer.json b/libs/firebase/php-jwt/composer.json similarity index 52% rename from vendor/firebase/php-jwt/composer.json rename to libs/firebase/php-jwt/composer.json index 6146e2d..062f239 100644 --- a/vendor/firebase/php-jwt/composer.json +++ b/libs/firebase/php-jwt/composer.json @@ -1,7 +1,7 @@ { - "name": "firebase/php-jwt", + "name": "firebase\/php-jwt", "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", - "homepage": "https://github.com/firebase/php-jwt", + "homepage": "https:\/\/github.com\/firebase\/php-jwt", "keywords": [ "php", "jwt" @@ -20,17 +20,23 @@ ], "license": "BSD-3-Clause", "require": { - "php": ">=5.3.0" + "php": "^7.4||^8.0" }, "suggest": { - "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + "paragonie\/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present", + "ext-sodium": "Support EdDSA (Ed25519) signatures" }, "autoload": { "psr-4": { - "Firebase\\JWT\\": "src" + "IndieAuth\\Libs\\Firebase\\JWT\\": "src" } }, "require-dev": { - "phpunit/phpunit": ">=4.8 <=9" + "guzzlehttp\/guzzle": "^6.5||^7.4", + "phpspec\/prophecy-phpunit": "^2.0", + "phpunit\/phpunit": "^9.5", + "psr\/cache": "^1.0||^2.0", + "psr\/http-client": "^1.0", + "psr\/http-factory": "^1.0" } -} +} \ No newline at end of file diff --git a/libs/firebase/php-jwt/src/BeforeValidException.php b/libs/firebase/php-jwt/src/BeforeValidException.php new file mode 100644 index 0000000..c39d607 --- /dev/null +++ b/libs/firebase/php-jwt/src/BeforeValidException.php @@ -0,0 +1,17 @@ +payload = $payload; + } + public function getPayload() : object + { + return $this->payload; + } +} diff --git a/libs/firebase/php-jwt/src/CachedKeySet.php b/libs/firebase/php-jwt/src/CachedKeySet.php new file mode 100644 index 0000000..b3e271d --- /dev/null +++ b/libs/firebase/php-jwt/src/CachedKeySet.php @@ -0,0 +1,227 @@ + + * @internal + */ +class CachedKeySet implements ArrayAccess +{ + /** + * @var string + */ + private $jwksUri; + /** + * @var ClientInterface + */ + private $httpClient; + /** + * @var RequestFactoryInterface + */ + private $httpFactory; + /** + * @var CacheItemPoolInterface + */ + private $cache; + /** + * @var ?int + */ + private $expiresAfter; + /** + * @var ?CacheItemInterface + */ + private $cacheItem; + /** + * @var array> + */ + private $keySet; + /** + * @var string + */ + private $cacheKey; + /** + * @var string + */ + private $cacheKeyPrefix = 'jwks'; + /** + * @var int + */ + private $maxKeyLength = 64; + /** + * @var bool + */ + private $rateLimit; + /** + * @var string + */ + private $rateLimitCacheKey; + /** + * @var int + */ + private $maxCallsPerMinute = 10; + /** + * @var string|null + */ + private $defaultAlg; + public function __construct(string $jwksUri, ClientInterface $httpClient, RequestFactoryInterface $httpFactory, CacheItemPoolInterface $cache, int $expiresAfter = null, bool $rateLimit = \false, string $defaultAlg = null) + { + $this->jwksUri = $jwksUri; + $this->httpClient = $httpClient; + $this->httpFactory = $httpFactory; + $this->cache = $cache; + $this->expiresAfter = $expiresAfter; + $this->rateLimit = $rateLimit; + $this->defaultAlg = $defaultAlg; + $this->setCacheKeys(); + } + /** + * @param string $keyId + * @return Key + */ + public function offsetGet($keyId) : Key + { + if (!$this->keyIdExists($keyId)) { + throw new OutOfBoundsException('Key ID not found'); + } + return JWK::parseKey($this->keySet[$keyId], $this->defaultAlg); + } + /** + * @param string $keyId + * @return bool + */ + public function offsetExists($keyId) : bool + { + return $this->keyIdExists($keyId); + } + /** + * @param string $offset + * @param Key $value + */ + public function offsetSet($offset, $value) : void + { + throw new LogicException('Method not implemented'); + } + /** + * @param string $offset + */ + public function offsetUnset($offset) : void + { + throw new LogicException('Method not implemented'); + } + /** + * @return array + */ + private function formatJwksForCache(string $jwks) : array + { + $jwks = \json_decode($jwks, \true); + if (!isset($jwks['keys'])) { + throw new UnexpectedValueException('"keys" member must exist in the JWK Set'); + } + if (empty($jwks['keys'])) { + throw new InvalidArgumentException('JWK Set did not contain any keys'); + } + $keys = []; + foreach ($jwks['keys'] as $k => $v) { + $kid = isset($v['kid']) ? $v['kid'] : $k; + $keys[(string) $kid] = $v; + } + return $keys; + } + private function keyIdExists(string $keyId) : bool + { + if (null === $this->keySet) { + $item = $this->getCacheItem(); + // Try to load keys from cache + if ($item->isHit()) { + // item found! retrieve it + $this->keySet = $item->get(); + // If the cached item is a string, the JWKS response was cached (previous behavior). + // Parse this into expected format array instead. + if (\is_string($this->keySet)) { + $this->keySet = $this->formatJwksForCache($this->keySet); + } + } + } + if (!isset($this->keySet[$keyId])) { + if ($this->rateLimitExceeded()) { + return \false; + } + $request = $this->httpFactory->createRequest('GET', $this->jwksUri); + $jwksResponse = $this->httpClient->sendRequest($request); + if ($jwksResponse->getStatusCode() !== 200) { + throw new UnexpectedValueException(\sprintf('HTTP Error: %d %s for URI "%s"', $jwksResponse->getStatusCode(), $jwksResponse->getReasonPhrase(), $this->jwksUri), $jwksResponse->getStatusCode()); + } + $this->keySet = $this->formatJwksForCache((string) $jwksResponse->getBody()); + if (!isset($this->keySet[$keyId])) { + return \false; + } + $item = $this->getCacheItem(); + $item->set($this->keySet); + if ($this->expiresAfter) { + $item->expiresAfter($this->expiresAfter); + } + $this->cache->save($item); + } + return \true; + } + private function rateLimitExceeded() : bool + { + if (!$this->rateLimit) { + return \false; + } + $cacheItem = $this->cache->getItem($this->rateLimitCacheKey); + if (!$cacheItem->isHit()) { + $cacheItem->expiresAfter(1); + // # of calls are cached each minute + } + $callsPerMinute = (int) $cacheItem->get(); + if (++$callsPerMinute > $this->maxCallsPerMinute) { + return \true; + } + $cacheItem->set($callsPerMinute); + $this->cache->save($cacheItem); + return \false; + } + private function getCacheItem() : CacheItemInterface + { + if (\is_null($this->cacheItem)) { + $this->cacheItem = $this->cache->getItem($this->cacheKey); + } + return $this->cacheItem; + } + private function setCacheKeys() : void + { + if (empty($this->jwksUri)) { + throw new RuntimeException('JWKS URI is empty'); + } + // ensure we do not have illegal characters + $key = \preg_replace('|[^a-zA-Z0-9_\\.!]|', '', $this->jwksUri); + // add prefix + $key = $this->cacheKeyPrefix . $key; + // Hash keys if they exceed $maxKeyLength of 64 + if (\strlen($key) > $this->maxKeyLength) { + $key = \substr(\hash('sha256', $key), 0, $this->maxKeyLength); + } + $this->cacheKey = $key; + if ($this->rateLimit) { + // add prefix + $rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key; + // Hash keys if they exceed $maxKeyLength of 64 + if (\strlen($rateLimitKey) > $this->maxKeyLength) { + $rateLimitKey = \substr(\hash('sha256', $rateLimitKey), 0, $this->maxKeyLength); + } + $this->rateLimitCacheKey = $rateLimitKey; + } + } +} diff --git a/libs/firebase/php-jwt/src/ExpiredException.php b/libs/firebase/php-jwt/src/ExpiredException.php new file mode 100644 index 0000000..aebb23c --- /dev/null +++ b/libs/firebase/php-jwt/src/ExpiredException.php @@ -0,0 +1,17 @@ +payload = $payload; + } + public function getPayload() : object + { + return $this->payload; + } +} diff --git a/libs/firebase/php-jwt/src/JWK.php b/libs/firebase/php-jwt/src/JWK.php new file mode 100644 index 0000000..c16037a --- /dev/null +++ b/libs/firebase/php-jwt/src/JWK.php @@ -0,0 +1,268 @@ + + * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD + * @link https://github.com/firebase/php-jwt + * @internal + */ +class JWK +{ + private const OID = '1.2.840.10045.2.1'; + private const ASN1_OBJECT_IDENTIFIER = 0x6; + private const ASN1_SEQUENCE = 0x10; + // also defined in JWT + private const ASN1_BIT_STRING = 0x3; + private const EC_CURVES = [ + 'P-256' => '1.2.840.10045.3.1.7', + // Len: 64 + 'secp256k1' => '1.3.132.0.10', + // Len: 64 + 'P-384' => '1.3.132.0.34', + ]; + // For keys with "kty" equal to "OKP" (Octet Key Pair), the "crv" parameter must contain the key subtype. + // This library supports the following subtypes: + private const OKP_SUBTYPES = ['Ed25519' => \true]; + /** + * Parse a set of JWK keys + * + * @param array $jwks The JSON Web Key Set as an associative array + * @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the + * JSON Web Key Set + * + * @return array An associative array of key IDs (kid) to Key objects + * + * @throws InvalidArgumentException Provided JWK Set is empty + * @throws UnexpectedValueException Provided JWK Set was invalid + * @throws DomainException OpenSSL failure + * + * @uses parseKey + */ + public static function parseKeySet(array $jwks, string $defaultAlg = null) : array + { + $keys = []; + if (!isset($jwks['keys'])) { + throw new UnexpectedValueException('"keys" member must exist in the JWK Set'); + } + if (empty($jwks['keys'])) { + throw new InvalidArgumentException('JWK Set did not contain any keys'); + } + foreach ($jwks['keys'] as $k => $v) { + $kid = isset($v['kid']) ? $v['kid'] : $k; + if ($key = self::parseKey($v, $defaultAlg)) { + $keys[(string) $kid] = $key; + } + } + if (0 === \count($keys)) { + throw new UnexpectedValueException('No supported algorithms found in JWK Set'); + } + return $keys; + } + /** + * Parse a JWK key + * + * @param array $jwk An individual JWK + * @param string $defaultAlg The algorithm for the Key object if "alg" is not set in the + * JSON Web Key Set + * + * @return Key The key object for the JWK + * + * @throws InvalidArgumentException Provided JWK is empty + * @throws UnexpectedValueException Provided JWK was invalid + * @throws DomainException OpenSSL failure + * + * @uses createPemFromModulusAndExponent + */ + public static function parseKey(array $jwk, string $defaultAlg = null) : ?Key + { + if (empty($jwk)) { + throw new InvalidArgumentException('JWK must not be empty'); + } + if (!isset($jwk['kty'])) { + throw new UnexpectedValueException('JWK must contain a "kty" parameter'); + } + if (!isset($jwk['alg'])) { + if (\is_null($defaultAlg)) { + // The "alg" parameter is optional in a KTY, but an algorithm is required + // for parsing in this library. Use the $defaultAlg parameter when parsing the + // key set in order to prevent this error. + // @see https://datatracker.ietf.org/doc/html/rfc7517#section-4.4 + throw new UnexpectedValueException('JWK must contain an "alg" parameter'); + } + $jwk['alg'] = $defaultAlg; + } + switch ($jwk['kty']) { + case 'RSA': + if (!empty($jwk['d'])) { + throw new UnexpectedValueException('RSA private keys are not supported'); + } + if (!isset($jwk['n']) || !isset($jwk['e'])) { + throw new UnexpectedValueException('RSA keys must contain values for both "n" and "e"'); + } + $pem = self::createPemFromModulusAndExponent($jwk['n'], $jwk['e']); + $publicKey = \openssl_pkey_get_public($pem); + if (\false === $publicKey) { + throw new DomainException('OpenSSL error: ' . \openssl_error_string()); + } + return new Key($publicKey, $jwk['alg']); + case 'EC': + if (isset($jwk['d'])) { + // The key is actually a private key + throw new UnexpectedValueException('Key data must be for a public key'); + } + if (empty($jwk['crv'])) { + throw new UnexpectedValueException('crv not set'); + } + if (!isset(self::EC_CURVES[$jwk['crv']])) { + throw new DomainException('Unrecognised or unsupported EC curve'); + } + if (empty($jwk['x']) || empty($jwk['y'])) { + throw new UnexpectedValueException('x and y not set'); + } + $publicKey = self::createPemFromCrvAndXYCoordinates($jwk['crv'], $jwk['x'], $jwk['y']); + return new Key($publicKey, $jwk['alg']); + case 'OKP': + if (isset($jwk['d'])) { + // The key is actually a private key + throw new UnexpectedValueException('Key data must be for a public key'); + } + if (!isset($jwk['crv'])) { + throw new UnexpectedValueException('crv not set'); + } + if (empty(self::OKP_SUBTYPES[$jwk['crv']])) { + throw new DomainException('Unrecognised or unsupported OKP key subtype'); + } + if (empty($jwk['x'])) { + throw new UnexpectedValueException('x not set'); + } + // This library works internally with EdDSA keys (Ed25519) encoded in standard base64. + $publicKey = JWT::convertBase64urlToBase64($jwk['x']); + return new Key($publicKey, $jwk['alg']); + default: + break; + } + return null; + } + /** + * Converts the EC JWK values to pem format. + * + * @param string $crv The EC curve (only P-256 & P-384 is supported) + * @param string $x The EC x-coordinate + * @param string $y The EC y-coordinate + * + * @return string + */ + private static function createPemFromCrvAndXYCoordinates(string $crv, string $x, string $y) : string + { + $pem = self::encodeDER(self::ASN1_SEQUENCE, self::encodeDER(self::ASN1_SEQUENCE, self::encodeDER(self::ASN1_OBJECT_IDENTIFIER, self::encodeOID(self::OID)) . self::encodeDER(self::ASN1_OBJECT_IDENTIFIER, self::encodeOID(self::EC_CURVES[$crv]))) . self::encodeDER(self::ASN1_BIT_STRING, \chr(0x0) . \chr(0x4) . JWT::urlsafeB64Decode($x) . JWT::urlsafeB64Decode($y))); + return \sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----\n", \wordwrap(\base64_encode($pem), 64, "\n", \true)); + } + /** + * Create a public key represented in PEM format from RSA modulus and exponent information + * + * @param string $n The RSA modulus encoded in Base64 + * @param string $e The RSA exponent encoded in Base64 + * + * @return string The RSA public key represented in PEM format + * + * @uses encodeLength + */ + private static function createPemFromModulusAndExponent(string $n, string $e) : string + { + $mod = JWT::urlsafeB64Decode($n); + $exp = JWT::urlsafeB64Decode($e); + $modulus = \pack('Ca*a*', 2, self::encodeLength(\strlen($mod)), $mod); + $publicExponent = \pack('Ca*a*', 2, self::encodeLength(\strlen($exp)), $exp); + $rsaPublicKey = \pack('Ca*a*a*', 48, self::encodeLength(\strlen($modulus) + \strlen($publicExponent)), $modulus, $publicExponent); + // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption. + $rsaOID = \pack('H*', '300d06092a864886f70d0101010500'); + // hex version of MA0GCSqGSIb3DQEBAQUA + $rsaPublicKey = \chr(0) . $rsaPublicKey; + $rsaPublicKey = \chr(3) . self::encodeLength(\strlen($rsaPublicKey)) . $rsaPublicKey; + $rsaPublicKey = \pack('Ca*a*', 48, self::encodeLength(\strlen($rsaOID . $rsaPublicKey)), $rsaOID . $rsaPublicKey); + return "-----BEGIN PUBLIC KEY-----\r\n" . \chunk_split(\base64_encode($rsaPublicKey), 64) . '-----END PUBLIC KEY-----'; + } + /** + * DER-encode the length + * + * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See + * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information. + * + * @param int $length + * @return string + */ + private static function encodeLength(int $length) : string + { + if ($length <= 0x7f) { + return \chr($length); + } + $temp = \ltrim(\pack('N', $length), \chr(0)); + return \pack('Ca*', 0x80 | \strlen($temp), $temp); + } + /** + * Encodes a value into a DER object. + * Also defined in Firebase\JWT\JWT + * + * @param int $type DER tag + * @param string $value the value to encode + * @return string the encoded object + */ + private static function encodeDER(int $type, string $value) : string + { + $tag_header = 0; + if ($type === self::ASN1_SEQUENCE) { + $tag_header |= 0x20; + } + // Type + $der = \chr($tag_header | $type); + // Length + $der .= \chr(\strlen($value)); + return $der . $value; + } + /** + * Encodes a string into a DER-encoded OID. + * + * @param string $oid the OID string + * @return string the binary DER-encoded OID + */ + private static function encodeOID(string $oid) : string + { + $octets = \explode('.', $oid); + // Get the first octet + $first = (int) \array_shift($octets); + $second = (int) \array_shift($octets); + $oid = \chr($first * 40 + $second); + // Iterate over subsequent octets + foreach ($octets as $octet) { + if ($octet == 0) { + $oid .= \chr(0x0); + continue; + } + $bin = ''; + while ($octet) { + $bin .= \chr(0x80 | $octet & 0x7f); + $octet >>= 7; + } + $bin[0] = $bin[0] & \chr(0x7f); + // Convert to big endian if necessary + if (\pack('V', 65534) == \pack('L', 65534)) { + $oid .= \strrev($bin); + } else { + $oid .= $bin; + } + } + return $oid; + } +} diff --git a/libs/firebase/php-jwt/src/JWT.php b/libs/firebase/php-jwt/src/JWT.php new file mode 100644 index 0000000..d2f27ed --- /dev/null +++ b/libs/firebase/php-jwt/src/JWT.php @@ -0,0 +1,572 @@ + + * @author Anant Narayanan + * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD + * @link https://github.com/firebase/php-jwt + * @internal + */ +class JWT +{ + private const ASN1_INTEGER = 0x2; + private const ASN1_SEQUENCE = 0x10; + private const ASN1_BIT_STRING = 0x3; + /** + * When checking nbf, iat or expiration times, + * we want to provide some extra leeway time to + * account for clock skew. + * + * @var int + */ + public static $leeway = 0; + /** + * Allow the current timestamp to be specified. + * Useful for fixing a value within unit testing. + * Will default to PHP time() value if null. + * + * @var ?int + */ + public static $timestamp = null; + /** + * @var array + */ + public static $supported_algs = ['ES384' => ['openssl', 'SHA384'], 'ES256' => ['openssl', 'SHA256'], 'ES256K' => ['openssl', 'SHA256'], 'HS256' => ['hash_hmac', 'SHA256'], 'HS384' => ['hash_hmac', 'SHA384'], 'HS512' => ['hash_hmac', 'SHA512'], 'RS256' => ['openssl', 'SHA256'], 'RS384' => ['openssl', 'SHA384'], 'RS512' => ['openssl', 'SHA512'], 'EdDSA' => ['sodium_crypto', 'EdDSA']]; + /** + * Decodes a JWT string into a PHP object. + * + * @param string $jwt The JWT + * @param Key|ArrayAccess|array $keyOrKeyArray The Key or associative array of key IDs + * (kid) to Key objects. + * If the algorithm used is asymmetric, this is + * the public key. + * Each Key object contains an algorithm and + * matching key. + * Supported algorithms are 'ES384','ES256', + * 'HS256', 'HS384', 'HS512', 'RS256', 'RS384' + * and 'RS512'. + * @param stdClass $headers Optional. Populates stdClass with headers. + * + * @return stdClass The JWT's payload as a PHP object + * + * @throws InvalidArgumentException Provided key/key-array was empty or malformed + * @throws DomainException Provided JWT is malformed + * @throws UnexpectedValueException Provided JWT was invalid + * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed + * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' + * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat' + * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim + * + * @uses jsonDecode + * @uses urlsafeB64Decode + */ + public static function decode(string $jwt, $keyOrKeyArray, stdClass &$headers = null) : stdClass + { + // Validate JWT + $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp; + if (empty($keyOrKeyArray)) { + throw new InvalidArgumentException('Key may not be empty'); + } + $tks = \explode('.', $jwt); + if (\count($tks) !== 3) { + throw new UnexpectedValueException('Wrong number of segments'); + } + list($headb64, $bodyb64, $cryptob64) = $tks; + $headerRaw = static::urlsafeB64Decode($headb64); + if (null === ($header = static::jsonDecode($headerRaw))) { + throw new UnexpectedValueException('Invalid header encoding'); + } + if ($headers !== null) { + $headers = $header; + } + $payloadRaw = static::urlsafeB64Decode($bodyb64); + if (null === ($payload = static::jsonDecode($payloadRaw))) { + throw new UnexpectedValueException('Invalid claims encoding'); + } + if (\is_array($payload)) { + // prevent PHP Fatal Error in edge-cases when payload is empty array + $payload = (object) $payload; + } + if (!$payload instanceof stdClass) { + throw new UnexpectedValueException('Payload must be a JSON object'); + } + $sig = static::urlsafeB64Decode($cryptob64); + if (empty($header->alg)) { + throw new UnexpectedValueException('Empty algorithm'); + } + if (empty(static::$supported_algs[$header->alg])) { + throw new UnexpectedValueException('Algorithm not supported'); + } + $key = self::getKey($keyOrKeyArray, \property_exists($header, 'kid') ? $header->kid : null); + // Check the algorithm + if (!self::constantTimeEquals($key->getAlgorithm(), $header->alg)) { + // See issue #351 + throw new UnexpectedValueException('Incorrect key for this algorithm'); + } + if (\in_array($header->alg, ['ES256', 'ES256K', 'ES384'], \true)) { + // OpenSSL expects an ASN.1 DER sequence for ES256/ES256K/ES384 signatures + $sig = self::signatureToDER($sig); + } + if (!self::verify("{$headb64}.{$bodyb64}", $sig, $key->getKeyMaterial(), $header->alg)) { + throw new SignatureInvalidException('Signature verification failed'); + } + // Check the nbf if it is defined. This is the time that the + // token can actually be used. If it's not yet that time, abort. + if (isset($payload->nbf) && \floor($payload->nbf) > $timestamp + static::$leeway) { + $ex = new BeforeValidException('Cannot handle token with nbf prior to ' . \date(DateTime::ISO8601, (int) $payload->nbf)); + $ex->setPayload($payload); + throw $ex; + } + // Check that this token has been created before 'now'. This prevents + // using tokens that have been created for later use (and haven't + // correctly used the nbf claim). + if (!isset($payload->nbf) && isset($payload->iat) && \floor($payload->iat) > $timestamp + static::$leeway) { + $ex = new BeforeValidException('Cannot handle token with iat prior to ' . \date(DateTime::ISO8601, (int) $payload->iat)); + $ex->setPayload($payload); + throw $ex; + } + // Check if this token has expired. + if (isset($payload->exp) && $timestamp - static::$leeway >= $payload->exp) { + $ex = new ExpiredException('Expired token'); + $ex->setPayload($payload); + throw $ex; + } + return $payload; + } + /** + * Converts and signs a PHP array into a JWT string. + * + * @param array $payload PHP array + * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. + * @param string $alg Supported algorithms are 'ES384','ES256', 'ES256K', 'HS256', + * 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' + * @param string $keyId + * @param array $head An array with header elements to attach + * + * @return string A signed JWT + * + * @uses jsonEncode + * @uses urlsafeB64Encode + */ + public static function encode(array $payload, $key, string $alg, string $keyId = null, array $head = null) : string + { + $header = ['typ' => 'JWT', 'alg' => $alg]; + if ($keyId !== null) { + $header['kid'] = $keyId; + } + if (isset($head) && \is_array($head)) { + $header = \array_merge($head, $header); + } + $segments = []; + $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($header)); + $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($payload)); + $signing_input = \implode('.', $segments); + $signature = static::sign($signing_input, $key, $alg); + $segments[] = static::urlsafeB64Encode($signature); + return \implode('.', $segments); + } + /** + * Sign a string with a given key and algorithm. + * + * @param string $msg The message to sign + * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. + * @param string $alg Supported algorithms are 'EdDSA', 'ES384', 'ES256', 'ES256K', 'HS256', + * 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' + * + * @return string An encrypted message + * + * @throws DomainException Unsupported algorithm or bad key was specified + */ + public static function sign(string $msg, $key, string $alg) : string + { + if (empty(static::$supported_algs[$alg])) { + throw new DomainException('Algorithm not supported'); + } + list($function, $algorithm) = static::$supported_algs[$alg]; + switch ($function) { + case 'hash_hmac': + if (!\is_string($key)) { + throw new InvalidArgumentException('key must be a string when using hmac'); + } + return \hash_hmac($algorithm, $msg, $key, \true); + case 'openssl': + $signature = ''; + $success = \openssl_sign($msg, $signature, $key, $algorithm); + // @phpstan-ignore-line + if (!$success) { + throw new DomainException('OpenSSL unable to sign data'); + } + if ($alg === 'ES256' || $alg === 'ES256K') { + $signature = self::signatureFromDER($signature, 256); + } elseif ($alg === 'ES384') { + $signature = self::signatureFromDER($signature, 384); + } + return $signature; + case 'sodium_crypto': + if (!\function_exists('sodium_crypto_sign_detached')) { + throw new DomainException('libsodium is not available'); + } + if (!\is_string($key)) { + throw new InvalidArgumentException('key must be a string when using EdDSA'); + } + try { + // The last non-empty line is used as the key. + $lines = \array_filter(\explode("\n", $key)); + $key = \base64_decode((string) \end($lines)); + if (\strlen($key) === 0) { + throw new DomainException('Key cannot be empty string'); + } + return \sodium_crypto_sign_detached($msg, $key); + } catch (Exception $e) { + throw new DomainException($e->getMessage(), 0, $e); + } + } + throw new DomainException('Algorithm not supported'); + } + /** + * Verify a signature with the message, key and method. Not all methods + * are symmetric, so we must have a separate verify and sign method. + * + * @param string $msg The original message (header and body) + * @param string $signature The original signature + * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For Ed*, ES*, HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey + * @param string $alg The algorithm + * + * @return bool + * + * @throws DomainException Invalid Algorithm, bad key, or OpenSSL failure + */ + private static function verify(string $msg, string $signature, $keyMaterial, string $alg) : bool + { + if (empty(static::$supported_algs[$alg])) { + throw new DomainException('Algorithm not supported'); + } + list($function, $algorithm) = static::$supported_algs[$alg]; + switch ($function) { + case 'openssl': + $success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); + // @phpstan-ignore-line + if ($success === 1) { + return \true; + } + if ($success === 0) { + return \false; + } + // returns 1 on success, 0 on failure, -1 on error. + throw new DomainException('OpenSSL error: ' . \openssl_error_string()); + case 'sodium_crypto': + if (!\function_exists('sodium_crypto_sign_verify_detached')) { + throw new DomainException('libsodium is not available'); + } + if (!\is_string($keyMaterial)) { + throw new InvalidArgumentException('key must be a string when using EdDSA'); + } + try { + // The last non-empty line is used as the key. + $lines = \array_filter(\explode("\n", $keyMaterial)); + $key = \base64_decode((string) \end($lines)); + if (\strlen($key) === 0) { + throw new DomainException('Key cannot be empty string'); + } + if (\strlen($signature) === 0) { + throw new DomainException('Signature cannot be empty string'); + } + return \sodium_crypto_sign_verify_detached($signature, $msg, $key); + } catch (Exception $e) { + throw new DomainException($e->getMessage(), 0, $e); + } + case 'hash_hmac': + default: + if (!\is_string($keyMaterial)) { + throw new InvalidArgumentException('key must be a string when using hmac'); + } + $hash = \hash_hmac($algorithm, $msg, $keyMaterial, \true); + return self::constantTimeEquals($hash, $signature); + } + } + /** + * Decode a JSON string into a PHP object. + * + * @param string $input JSON string + * + * @return mixed The decoded JSON string + * + * @throws DomainException Provided string was invalid JSON + */ + public static function jsonDecode(string $input) + { + $obj = \json_decode($input, \false, 512, \JSON_BIGINT_AS_STRING); + if ($errno = \json_last_error()) { + self::handleJsonError($errno); + } elseif ($obj === null && $input !== 'null') { + throw new DomainException('Null result with non-null input'); + } + return $obj; + } + /** + * Encode a PHP array into a JSON string. + * + * @param array $input A PHP array + * + * @return string JSON representation of the PHP array + * + * @throws DomainException Provided object could not be encoded to valid JSON + */ + public static function jsonEncode(array $input) : string + { + if (\PHP_VERSION_ID >= 50400) { + $json = \json_encode($input, \JSON_UNESCAPED_SLASHES); + } else { + // PHP 5.3 only + $json = \json_encode($input); + } + if ($errno = \json_last_error()) { + self::handleJsonError($errno); + } elseif ($json === 'null') { + throw new DomainException('Null result with non-null input'); + } + if ($json === \false) { + throw new DomainException('Provided object could not be encoded to valid JSON'); + } + return $json; + } + /** + * Decode a string with URL-safe Base64. + * + * @param string $input A Base64 encoded string + * + * @return string A decoded string + * + * @throws InvalidArgumentException invalid base64 characters + */ + public static function urlsafeB64Decode(string $input) : string + { + return \base64_decode(self::convertBase64UrlToBase64($input)); + } + /** + * Convert a string in the base64url (URL-safe Base64) encoding to standard base64. + * + * @param string $input A Base64 encoded string with URL-safe characters (-_ and no padding) + * + * @return string A Base64 encoded string with standard characters (+/) and padding (=), when + * needed. + * + * @see https://www.rfc-editor.org/rfc/rfc4648 + */ + public static function convertBase64UrlToBase64(string $input) : string + { + $remainder = \strlen($input) % 4; + if ($remainder) { + $padlen = 4 - $remainder; + $input .= \str_repeat('=', $padlen); + } + return \strtr($input, '-_', '+/'); + } + /** + * Encode a string with URL-safe Base64. + * + * @param string $input The string you want encoded + * + * @return string The base64 encode of what you passed in + */ + public static function urlsafeB64Encode(string $input) : string + { + return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_')); + } + /** + * Determine if an algorithm has been provided for each Key + * + * @param Key|ArrayAccess|array $keyOrKeyArray + * @param string|null $kid + * + * @throws UnexpectedValueException + * + * @return Key + */ + private static function getKey($keyOrKeyArray, ?string $kid) : Key + { + if ($keyOrKeyArray instanceof Key) { + return $keyOrKeyArray; + } + if (empty($kid) && $kid !== '0') { + throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); + } + if ($keyOrKeyArray instanceof CachedKeySet) { + // Skip "isset" check, as this will automatically refresh if not set + return $keyOrKeyArray[$kid]; + } + if (!isset($keyOrKeyArray[$kid])) { + throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); + } + return $keyOrKeyArray[$kid]; + } + /** + * @param string $left The string of known length to compare against + * @param string $right The user-supplied string + * @return bool + */ + public static function constantTimeEquals(string $left, string $right) : bool + { + if (\function_exists('hash_equals')) { + return \hash_equals($left, $right); + } + $len = \min(self::safeStrlen($left), self::safeStrlen($right)); + $status = 0; + for ($i = 0; $i < $len; $i++) { + $status |= \ord($left[$i]) ^ \ord($right[$i]); + } + $status |= self::safeStrlen($left) ^ self::safeStrlen($right); + return $status === 0; + } + /** + * Helper method to create a JSON error. + * + * @param int $errno An error number from json_last_error() + * + * @throws DomainException + * + * @return void + */ + private static function handleJsonError(int $errno) : void + { + $messages = [\JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', \JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON', \JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', \JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', \JSON_ERROR_UTF8 => 'Malformed UTF-8 characters']; + throw new DomainException(isset($messages[$errno]) ? $messages[$errno] : 'Unknown JSON error: ' . $errno); + } + /** + * Get the number of bytes in cryptographic strings. + * + * @param string $str + * + * @return int + */ + private static function safeStrlen(string $str) : int + { + if (\function_exists('mb_strlen')) { + return \mb_strlen($str, '8bit'); + } + return \strlen($str); + } + /** + * Convert an ECDSA signature to an ASN.1 DER sequence + * + * @param string $sig The ECDSA signature to convert + * @return string The encoded DER object + */ + private static function signatureToDER(string $sig) : string + { + // Separate the signature into r-value and s-value + $length = \max(1, (int) (\strlen($sig) / 2)); + list($r, $s) = \str_split($sig, $length); + // Trim leading zeros + $r = \ltrim($r, "\x00"); + $s = \ltrim($s, "\x00"); + // Convert r-value and s-value from unsigned big-endian integers to + // signed two's complement + if (\ord($r[0]) > 0x7f) { + $r = "\x00" . $r; + } + if (\ord($s[0]) > 0x7f) { + $s = "\x00" . $s; + } + return self::encodeDER(self::ASN1_SEQUENCE, self::encodeDER(self::ASN1_INTEGER, $r) . self::encodeDER(self::ASN1_INTEGER, $s)); + } + /** + * Encodes a value into a DER object. + * + * @param int $type DER tag + * @param string $value the value to encode + * + * @return string the encoded object + */ + private static function encodeDER(int $type, string $value) : string + { + $tag_header = 0; + if ($type === self::ASN1_SEQUENCE) { + $tag_header |= 0x20; + } + // Type + $der = \chr($tag_header | $type); + // Length + $der .= \chr(\strlen($value)); + return $der . $value; + } + /** + * Encodes signature from a DER object. + * + * @param string $der binary signature in DER format + * @param int $keySize the number of bits in the key + * + * @return string the signature + */ + private static function signatureFromDER(string $der, int $keySize) : string + { + // OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE + list($offset, $_) = self::readDER($der); + list($offset, $r) = self::readDER($der, $offset); + list($offset, $s) = self::readDER($der, $offset); + // Convert r-value and s-value from signed two's compliment to unsigned + // big-endian integers + $r = \ltrim($r, "\x00"); + $s = \ltrim($s, "\x00"); + // Pad out r and s so that they are $keySize bits long + $r = \str_pad($r, $keySize / 8, "\x00", \STR_PAD_LEFT); + $s = \str_pad($s, $keySize / 8, "\x00", \STR_PAD_LEFT); + return $r . $s; + } + /** + * Reads binary DER-encoded data and decodes into a single object + * + * @param string $der the binary data in DER format + * @param int $offset the offset of the data stream containing the object + * to decode + * + * @return array{int, string|null} the new offset and the decoded object + */ + private static function readDER(string $der, int $offset = 0) : array + { + $pos = $offset; + $size = \strlen($der); + $constructed = \ord($der[$pos]) >> 5 & 0x1; + $type = \ord($der[$pos++]) & 0x1f; + // Length + $len = \ord($der[$pos++]); + if ($len & 0x80) { + $n = $len & 0x1f; + $len = 0; + while ($n-- && $pos < $size) { + $len = $len << 8 | \ord($der[$pos++]); + } + } + // Value + if ($type === self::ASN1_BIT_STRING) { + $pos++; + // Skip the first contents octet (padding indicator) + $data = \substr($der, $pos, $len - 1); + $pos += $len - 1; + } elseif (!$constructed) { + $data = \substr($der, $pos, $len); + $pos += $len; + } else { + $data = null; + } + return [$pos, $data]; + } +} diff --git a/libs/firebase/php-jwt/src/JWTExceptionWithPayloadInterface.php b/libs/firebase/php-jwt/src/JWTExceptionWithPayloadInterface.php new file mode 100644 index 0000000..08f21aa --- /dev/null +++ b/libs/firebase/php-jwt/src/JWTExceptionWithPayloadInterface.php @@ -0,0 +1,21 @@ +keyMaterial = $keyMaterial; + $this->algorithm = $algorithm; + } + /** + * Return the algorithm valid for this key + * + * @return string + */ + public function getAlgorithm() : string + { + return $this->algorithm; + } + /** + * @return string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate + */ + public function getKeyMaterial() + { + return $this->keyMaterial; + } +} diff --git a/vendor/firebase/php-jwt/src/SignatureInvalidException.php b/libs/firebase/php-jwt/src/SignatureInvalidException.php similarity index 58% rename from vendor/firebase/php-jwt/src/SignatureInvalidException.php rename to libs/firebase/php-jwt/src/SignatureInvalidException.php index d35dee9..937fecd 100644 --- a/vendor/firebase/php-jwt/src/SignatureInvalidException.php +++ b/libs/firebase/php-jwt/src/SignatureInvalidException.php @@ -1,7 +1,8 @@ Barnaby Walters'); + * echo json_encode($output, JSON_PRETTY_PRINT); + * + * Produces: + * + * { + * "items": [ + * { + * "type": ["h-card"], + * "properties": { + * "name": ["Barnaby Walters"] + * } + * } + * ], + * "rels": {} + * } + * + * @param string|DOMDocument $input The HTML string or DOMDocument object to parse + * @param string $url The URL the input document was found at, for relative URL resolution + * @param bool $convertClassic whether or not to convert classic microformats + * @return array Canonical MF2 array structure + * @internal + */ +function parse($input, $url = null, $convertClassic = \true) +{ + $parser = new Parser($input, $url); + return $parser->parse($convertClassic); +} +/** + * Fetch microformats2 + * + * Given a URL, fetches it (following up to 5 redirects) and, if the content-type appears to be HTML, returns the parsed + * microformats2 array structure. + * + * Not that even if the response code was a 4XX or 5XX error, if the content-type is HTML-like then it will be parsed + * all the same, as there are legitimate cases where error pages might contain useful microformats (for example a deleted + * h-entry resulting in a 410 Gone page with a stub h-entry explaining the reason for deletion). Look in $curlInfo['http_code'] + * for the actual value. + * + * @param string $url The URL to fetch + * @param bool $convertClassic (optional, default true) whether or not to convert classic microformats + * @param &array $curlInfo (optional) the results of curl_getinfo will be placed in this variable for debugging + * @return array|null canonical microformats2 array structure on success, null on failure + * @internal + */ +function fetch($url, $convertClassic = \true, &$curlInfo = null) +{ + $ch = \curl_init(); + \curl_setopt($ch, \CURLOPT_URL, $url); + \curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1); + \curl_setopt($ch, \CURLOPT_HEADER, 0); + \curl_setopt($ch, \CURLOPT_FOLLOWLOCATION, 1); + \curl_setopt($ch, \CURLOPT_MAXREDIRS, 5); + \curl_setopt($ch, \CURLOPT_HTTPHEADER, array('Accept: text/html')); + $html = \curl_exec($ch); + $info = $curlInfo = \curl_getinfo($ch); + \curl_close($ch); + if (\strpos(\strtolower($info['content_type']), 'html') === \false) { + // The content was not delivered as HTML, do not attempt to parse it. + return null; + } + # ensure the final URL is used to resolve relative URLs + $url = $info['url']; + return parse($html, $url, $convertClassic); +} +/** + * Unicode to HTML Entities + * @param string $input String containing characters to convert into HTML entities + * @return string + * @internal + */ +function unicodeToHtmlEntities($input) +{ + return \mb_convert_encoding($input, 'HTML-ENTITIES', \mb_detect_encoding($input)); +} +/** + * Collapse Whitespace + * + * Collapses any sequences of whitespace within a string into a single space + * character. + * + * @deprecated since v0.2.3 + * @param string $str + * @return string + * @internal + */ +function collapseWhitespace($str) +{ + return \preg_replace('/[\\s|\\n]+/', ' ', $str); +} +/** @internal */ +function unicodeTrim($str) +{ + // this is cheating. TODO: find a better way if this causes any problems + $str = \str_replace(\mb_convert_encoding(' ', 'UTF-8', 'HTML-ENTITIES'), ' ', $str); + $str = \preg_replace('/^\\s+/', '', $str); + return \preg_replace('/\\s+$/', '', $str); +} +/** + * Microformat Name From Class string + * + * Given the value of @class, get the relevant mf classnames (e.g. h-card, + * p-name). + * + * @param string $class A space delimited list of classnames + * @param string $prefix The prefix to look for + * @return string|array The prefixed name of the first microfomats class found or false + * @internal + */ +function mfNamesFromClass($class, $prefix = 'h-') +{ + $class = \str_replace(array(' ', ' ', "\n"), ' ', $class); + $classes = \explode(' ', $class); + $classes = \preg_grep('#^(h|p|u|dt|e)-([a-z0-9]+-)?[a-z]+(-[a-z]+)*$#', $classes); + $matches = array(); + foreach ($classes as $classname) { + $compare_classname = ' ' . $classname; + $compare_prefix = ' ' . $prefix; + if (\strstr($compare_classname, $compare_prefix) !== \false && $compare_classname != $compare_prefix) { + $matches[] = $prefix === 'h-' ? $classname : \substr($classname, \strlen($prefix)); + } + } + return $matches; +} +/** + * Get Nested µf Property Name From Class + * + * Returns all the p-, u-, dt- or e- prefixed classnames it finds in a + * space-separated string. + * + * @param string $class + * @return array + * @internal + */ +function nestedMfPropertyNamesFromClass($class) +{ + $prefixes = array('p-', 'u-', 'dt-', 'e-'); + $propertyNames = array(); + $class = \str_replace(array(' ', ' ', "\n"), ' ', $class); + foreach (\explode(' ', $class) as $classname) { + foreach ($prefixes as $prefix) { + // Check if $classname is a valid property classname for $prefix. + if (\mb_substr($classname, 0, \mb_strlen($prefix)) == $prefix && $classname != $prefix) { + $propertyName = \mb_substr($classname, \mb_strlen($prefix)); + $propertyNames[$propertyName][] = $prefix; + } + } + } + foreach ($propertyNames as $property => $prefixes) { + $propertyNames[$property] = \array_unique($prefixes); + } + return $propertyNames; +} +/** + * Wraps mfNamesFromClass to handle an element as input (common) + * + * @param DOMElement $e The element to get the classname for + * @param string $prefix The prefix to look for + * @return mixed See return value of mf2\Parser::mfNameFromClass() + * @internal + */ +function mfNamesFromElement(\DOMElement $e, $prefix = 'h-') +{ + $class = $e->getAttribute('class'); + return mfNamesFromClass($class, $prefix); +} +/** + * Wraps nestedMfPropertyNamesFromClass to handle an element as input + * @internal + */ +function nestedMfPropertyNamesFromElement(\DOMElement $e) +{ + $class = $e->getAttribute('class'); + return nestedMfPropertyNamesFromClass($class); +} +/** + * Converts various time formats to HH:MM + * @param string $time The time to convert + * @return string + * @internal + */ +function convertTimeFormat($time) +{ + $hh = $mm = $ss = ''; + \preg_match('/(\\d{1,2}):?(\\d{2})?:?(\\d{2})?(a\\.?m\\.?|p\\.?m\\.?)?/i', $time, $matches); + // If no am/pm is specified: + if (empty($matches[4])) { + return $time; + } else { + // Otherwise, am/pm is specified. + $meridiem = \strtolower(\str_replace('.', '', $matches[4])); + // Hours. + $hh = $matches[1]; + // Add 12 to hours if pm applies. + if ($meridiem == 'pm' && $hh < 12) { + $hh += 12; + } + $hh = \str_pad($hh, 2, '0', \STR_PAD_LEFT); + // Minutes. + $mm = empty($matches[2]) ? '00' : $matches[2]; + // Seconds, only if supplied. + if (!empty($matches[3])) { + $ss = $matches[3]; + } + if (empty($ss)) { + return \sprintf('%s:%s', $hh, $mm); + } else { + return \sprintf('%s:%s:%s', $hh, $mm, $ss); + } + } +} +/** + * Normalize an ordinal date to YYYY-MM-DD + * This function should only be called after validating the $dtValue + * matches regex \d{4}-\d{2} + * @param string $dtValue + * @return string + * @internal + */ +function normalizeOrdinalDate($dtValue) +{ + list($year, $day) = \explode('-', $dtValue, 2); + $day = \intval($day); + if ($day < 367 && $day > 0) { + $date = \DateTime::createFromFormat('Y-z', $dtValue); + $date->modify('-1 day'); + # 'z' format is zero-based so need to adjust + if ($date->format('Y') === $year) { + return $date->format('Y-m-d'); + } + } + return ''; +} +/** + * If a date value has a timezone offset, normalize it. + * @param string $dtValue + * @return string isolated, normalized TZ offset for implied TZ for other dt- properties + * @internal + */ +function normalizeTimezoneOffset(&$dtValue) +{ + \preg_match('/Z|[+-]\\d{1,2}:?(\\d{2})?$/i', $dtValue, $matches); + if (empty($matches)) { + return null; + } + $timezoneOffset = null; + if ($matches[0] != 'Z') { + $timezoneString = \str_replace(':', '', $matches[0]); + $plus_minus = \substr($timezoneString, 0, 1); + $timezoneOffset = \substr($timezoneString, 1); + if (\strlen($timezoneOffset) <= 2) { + $timezoneOffset .= '00'; + } + $timezoneOffset = \str_pad($timezoneOffset, 4, 0, \STR_PAD_LEFT); + $timezoneOffset = $plus_minus . $timezoneOffset; + $dtValue = \preg_replace('/Z?[+-]\\d{1,2}:?(\\d{2})?$/i', $timezoneOffset, $dtValue); + } + return $timezoneOffset; +} +/** @internal */ +function applySrcsetUrlTransformation($srcset, $transformation) +{ + return \implode(', ', \array_filter(\array_map(function ($srcsetPart) use($transformation) { + $parts = \explode(" \t\n\r\x00\v", \trim($srcsetPart), 2); + $parts[0] = \rtrim($parts[0]); + if (empty($parts[0])) { + return \false; + } + $parts[0] = \call_user_func($transformation, $parts[0]); + return $parts[0] . (empty($parts[1]) ? '' : ' ' . $parts[1]); + }, \explode(',', \trim($srcset))))); +} +/** + * Microformats2 Parser + * + * A class which holds state for parsing microformats2 from HTML. + * + * Example usage: + * + * use Mf2; + * $parser = new Mf2\Parser('

Barnaby Walters

'); + * $output = $parser->parse(); + * @internal + */ +class Parser +{ + /** @var string The baseurl (if any) to use for this parse */ + public $baseurl; + /** @var DOMXPath object which can be used to query over any fragment*/ + public $xpath; + /** @var DOMDocument */ + public $doc; + /** @var SplObjectStorage */ + protected $parsed; + /** + * @var bool + */ + public $jsonMode; + /** @var boolean Whether to include experimental language parsing in the result */ + public $lang = \false; + /** @var bool Whether to include alternates object (dropped from spec in favor of rel-urls) */ + public $enableAlternates = \false; + /** + * Elements upgraded to mf2 during backcompat + * @var SplObjectStorage + */ + protected $upgraded; + /** + * Whether to convert classic microformats + * @var bool + */ + public $convertClassic; + /** + * Constructor + * + * @param DOMDocument|string $input The data to parse. A string of HTML or a DOMDocument + * @param string $url The URL of the parsed document, for relative URL resolution + * @param boolean $jsonMode Whether or not to use a stdClass instance for an empty `rels` dictionary. This breaks PHP looping over rels, but allows the output to be correctly serialized as JSON. + */ + public function __construct($input, $url = null, $jsonMode = \false) + { + \libxml_use_internal_errors(\true); + if (\is_string($input)) { + if (\class_exists('IndieAuth\\Libs\\Masterminds\\HTML5')) { + $doc = new \IndieAuth\Libs\Masterminds\HTML5(array('disable_html_ns' => \true)); + $doc = $doc->loadHTML($input); + } else { + $doc = new DOMDocument(); + @$doc->loadHTML(unicodeToHtmlEntities($input), \LIBXML_NOWARNING); + } + } elseif (\is_a($input, 'DOMDocument')) { + $doc = clone $input; + } else { + $doc = new DOMDocument(); + @$doc->loadHTML(''); + } + $this->xpath = new DOMXPath($doc); + $baseurl = $url; + foreach ($this->xpath->query('//base[@href]') as $base) { + $baseElementUrl = $base->getAttribute('href'); + if (\parse_url($baseElementUrl, \PHP_URL_SCHEME) === null) { + /* The base element URL is relative to the document URL. + * + * :/ + * + * Perhaps the author was high? */ + $baseurl = resolveUrl($url, $baseElementUrl); + } else { + $baseurl = $baseElementUrl; + } + break; + } + // Ignore