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('%s ',
- $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: %s ',
+ $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.
+
+=$form->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.
+
+ =$table->render();?>
+ =$results->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: =$client_name;?>
+ Token Ending With: =$ending;?>
+ Scope: =$scope;?>
+ Issued: =$issued_at;?>
- Client: =$client_name;?>
- Token Ending With: =$ending;?>
- Scope: =$scope;?>
- Issued: =$issued_at;?>
+
-
\ 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.
+ =$table->render();?>
+ =$results->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 elements as per the HTML5 spec
+ foreach ($this->xpath->query('//template') as $templateEl) {
+ $templateEl->parentNode->removeChild($templateEl);
+ }
+ $this->baseurl = $baseurl;
+ $this->doc = $doc;
+ $this->parsed = new SplObjectStorage();
+ $this->upgraded = new SplObjectStorage();
+ $this->jsonMode = $jsonMode;
+ }
+ private function elementPrefixParsed(\DOMElement $e, $prefix)
+ {
+ if (!$this->parsed->contains($e)) {
+ $this->parsed->attach($e, array());
+ }
+ $prefixes = $this->parsed[$e];
+ $prefixes[] = $prefix;
+ $this->parsed[$e] = $prefixes;
+ }
+ /**
+ * Determine if the element has already been parsed
+ * @param DOMElement $e
+ * @param string $prefix
+ * @return bool
+ */
+ private function isElementParsed(\DOMElement $e, $prefix)
+ {
+ if (!$this->parsed->contains($e)) {
+ return \false;
+ }
+ $prefixes = $this->parsed[$e];
+ if (!\in_array($prefix, $prefixes)) {
+ return \false;
+ }
+ return \true;
+ }
+ /**
+ * Determine if the element's specified property has already been upgraded during backcompat
+ * @param DOMElement $el
+ * @param string $property
+ * @return bool
+ */
+ private function isElementUpgraded(\DOMElement $el, $property)
+ {
+ if ($this->upgraded->contains($el)) {
+ if (\in_array($property, $this->upgraded[$el])) {
+ return \true;
+ }
+ }
+ return \false;
+ }
+ private function resolveChildUrls(DOMElement $el)
+ {
+ $hyperlinkChildren = $this->xpath->query('.//*[@src or @href or @data]', $el);
+ foreach ($hyperlinkChildren as $child) {
+ if ($child->hasAttribute('href')) {
+ $child->setAttribute('href', $this->resolveUrl($child->getAttribute('href')));
+ }
+ if ($child->hasAttribute('src')) {
+ $child->setAttribute('src', $this->resolveUrl($child->getAttribute('src')));
+ }
+ if ($child->hasAttribute('srcset')) {
+ $child->setAttribute('srcset', applySrcsetUrlTransformation($child->getAttribute('href'), array($this, 'resolveUrl')));
+ }
+ if ($child->hasAttribute('data')) {
+ $child->setAttribute('data', $this->resolveUrl($child->getAttribute('data')));
+ }
+ }
+ }
+ /**
+ * The following two methods implements plain text parsing.
+ * @param DOMElement $element
+ * @param bool $implied
+ * @see https://wiki.zegnat.net/media/textparsing.html
+ **/
+ public function textContent(DOMElement $element, $implied = \false)
+ {
+ return \preg_replace('/(^[\\t\\n\\f\\r ]+| +(?=\\n)|(?<=\\n) +| +(?= )|[\\t\\n\\f\\r ]+$)/', '', $this->elementToString($element, $implied));
+ }
+ private function elementToString(DOMElement $input, $implied = \false)
+ {
+ $output = '';
+ foreach ($input->childNodes as $child) {
+ if ($child->nodeType === \XML_TEXT_NODE) {
+ $output .= \str_replace(array("\t", "\n", "\r"), ' ', $child->textContent);
+ } else {
+ if ($child->nodeType === \XML_ELEMENT_NODE) {
+ $tagName = \strtoupper($child->tagName);
+ if (\in_array($tagName, array('SCRIPT', 'STYLE'))) {
+ continue;
+ } else {
+ if ($tagName === 'IMG') {
+ if ($child->hasAttribute('alt')) {
+ $output .= ' ' . \trim($child->getAttribute('alt'), "\t\n\f\r ") . ' ';
+ } else {
+ if (!$implied && $child->hasAttribute('src')) {
+ $output .= ' ' . $this->resolveUrl(\trim($child->getAttribute('src'), "\t\n\f\r ")) . ' ';
+ }
+ }
+ } else {
+ if ($tagName === 'BR') {
+ $output .= "\n";
+ } else {
+ if ($tagName === 'P') {
+ $output .= "\n" . $this->elementToString($child);
+ } else {
+ $output .= $this->elementToString($child);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return $output;
+ }
+ /**
+ * Given an img property, parse its value and/or alt text
+ * @param DOMElement $el
+ * @access public
+ * @return string|array
+ */
+ public function parseImg(DOMElement $el)
+ {
+ if ($el->hasAttribute('alt')) {
+ return ['value' => $this->resolveUrl($el->getAttribute('src')), 'alt' => $el->getAttribute('alt')];
+ }
+ return $el->getAttribute('src');
+ }
+ /**
+ * This method parses the language of an element
+ * @param DOMElement $el
+ * @access public
+ * @return string
+ */
+ public function language(DOMElement $el)
+ {
+ // element has a lang attribute; use it
+ if ($el->hasAttribute('lang')) {
+ return unicodeTrim($el->getAttribute('lang'));
+ }
+ if ($el->tagName == 'html') {
+ // we're at the element and no lang; check http-equiv Content-Language
+ foreach ($this->xpath->query('.//meta[@http-equiv]') as $node) {
+ if ($node->hasAttribute('http-equiv') && $node->hasAttribute('content') && \strtolower($node->getAttribute('http-equiv')) == 'content-language') {
+ return unicodeTrim($node->getAttribute('content'));
+ }
+ }
+ } elseif ($el->parentNode instanceof DOMElement) {
+ // check the parent node
+ return $this->language($el->parentNode);
+ }
+ return '';
+ }
+ # end method language()
+ // TODO: figure out if this has problems with sms: and geo: URLs
+ public function resolveUrl($url)
+ {
+ // If not a string then return.
+ if (!\is_string($url)) {
+ return $url;
+ }
+ // If the URL is seriously malformed it’s probably beyond the scope of this
+ // parser to try to do anything with it.
+ if (\parse_url($url) === \false) {
+ return $url;
+ }
+ // per issue #40 valid URLs could have a space on either side
+ $url = \trim($url);
+ $scheme = \parse_url($url, \PHP_URL_SCHEME);
+ if (empty($scheme) and !empty($this->baseurl)) {
+ return resolveUrl($this->baseurl, $url);
+ } else {
+ return $url;
+ }
+ }
+ // Parsing Functions
+ /**
+ * Parse value-class/value-title on an element, joining with $separator if
+ * there are multiple.
+ *
+ * @param \DOMElement $e
+ * @param string $separator = '' if multiple value-title elements, join with this string
+ * @return string|null the parsed value or null if value-class or -title aren’t in use
+ */
+ public function parseValueClassTitle(\DOMElement $e, $separator = '')
+ {
+ $valueClassElements = $this->xpath->query('./*[contains(concat(" ", @class, " "), " value ")]', $e);
+ if ($valueClassElements->length !== 0) {
+ // Process value-class stuff
+ $val = '';
+ foreach ($valueClassElements as $el) {
+ $val .= $this->textContent($el);
+ }
+ return unicodeTrim($val);
+ }
+ $valueTitleElements = $this->xpath->query('./*[contains(concat(" ", @class, " "), " value-title ")]', $e);
+ if ($valueTitleElements->length !== 0) {
+ // Process value-title stuff
+ $val = '';
+ foreach ($valueTitleElements as $el) {
+ $val .= $el->getAttribute('title');
+ }
+ return unicodeTrim($val);
+ }
+ // No value-title or -class in this element
+ return null;
+ }
+ /**
+ * Given an element with class="p-*", get its value
+ *
+ * @param DOMElement $p The element to parse
+ * @return string The plaintext value of $p, dependant on type
+ * @todo Make this adhere to value-class
+ */
+ public function parseP(\DOMElement $p)
+ {
+ $classTitle = $this->parseValueClassTitle($p, ' ');
+ if ($classTitle !== null) {
+ return $classTitle;
+ }
+ $this->resolveChildUrls($p);
+ if ($p->tagName == 'img' and $p->hasAttribute('alt')) {
+ $pValue = $p->getAttribute('alt');
+ } elseif ($p->tagName == 'area' and $p->hasAttribute('alt')) {
+ $pValue = $p->getAttribute('alt');
+ } elseif (($p->tagName == 'abbr' or $p->tagName == 'link') and $p->hasAttribute('title')) {
+ $pValue = $p->getAttribute('title');
+ } elseif (\in_array($p->tagName, array('data', 'input')) and $p->hasAttribute('value')) {
+ $pValue = $p->getAttribute('value');
+ } else {
+ $pValue = $this->textContent($p);
+ }
+ return $pValue;
+ }
+ /**
+ * Given an element with class="u-*", get the value of the URL
+ *
+ * @param DOMElement $u The element to parse
+ * @return string The plaintext value of $u, dependant on type
+ * @todo make this adhere to value-class
+ */
+ public function parseU(\DOMElement $u)
+ {
+ if (($u->tagName == 'a' or $u->tagName == 'area' or $u->tagName == 'link') and $u->hasAttribute('href')) {
+ $uValue = $u->getAttribute('href');
+ } elseif ($u->tagName == 'img' and $u->hasAttribute('src')) {
+ $uValue = $this->parseImg($u);
+ } elseif (\in_array($u->tagName, array('audio', 'video', 'source', 'iframe')) and $u->hasAttribute('src')) {
+ $uValue = $u->getAttribute('src');
+ } elseif ($u->tagName == 'video' and !$u->hasAttribute('src') and $u->hasAttribute('poster')) {
+ $uValue = $u->getAttribute('poster');
+ } elseif ($u->tagName == 'object' and $u->hasAttribute('data')) {
+ $uValue = $u->getAttribute('data');
+ } elseif (($classTitle = $this->parseValueClassTitle($u)) !== null) {
+ $uValue = $classTitle;
+ } elseif (($u->tagName == 'abbr' or $u->tagName == 'link') and $u->hasAttribute('title')) {
+ $uValue = $u->getAttribute('title');
+ } elseif (\in_array($u->tagName, array('data', 'input')) and $u->hasAttribute('value')) {
+ $uValue = $u->getAttribute('value');
+ } else {
+ $uValue = $this->textContent($u);
+ }
+ return $this->resolveUrl($uValue);
+ }
+ /**
+ * Given an element with class="dt-*", get the value of the datetime as a php date object
+ *
+ * @param DOMElement $dt The element to parse
+ * @param array $dates Array of dates processed so far
+ * @param string $impliedTimezone
+ * @return string The datetime string found
+ */
+ public function parseDT(\DOMElement $dt, &$dates = array(), &$impliedTimezone = null)
+ {
+ // Check for value-class pattern
+ $valueClassChildren = $this->xpath->query('./*[contains(concat(" ", @class, " "), " value ") or contains(concat(" ", @class, " "), " value-title ")]', $dt);
+ $dtValue = \false;
+ if ($valueClassChildren->length > 0) {
+ // They’re using value-class
+ $dateParts = array();
+ foreach ($valueClassChildren as $e) {
+ if (\strstr(' ' . $e->getAttribute('class') . ' ', ' value-title ')) {
+ $title = $e->getAttribute('title');
+ if (!empty($title)) {
+ $dateParts[] = $title;
+ }
+ } elseif ($e->tagName == 'img' or $e->tagName == 'area') {
+ // Use @alt
+ $alt = $e->getAttribute('alt');
+ if (!empty($alt)) {
+ $dateParts[] = $alt;
+ }
+ } elseif ($e->tagName == 'data') {
+ // Use @value, otherwise innertext
+ $value = $e->hasAttribute('value') ? $e->getAttribute('value') : unicodeTrim($e->nodeValue);
+ if (!empty($value)) {
+ $dateParts[] = $value;
+ }
+ } elseif ($e->tagName == 'abbr') {
+ // Use @title, otherwise innertext
+ $title = $e->hasAttribute('title') ? $e->getAttribute('title') : unicodeTrim($e->nodeValue);
+ if (!empty($title)) {
+ $dateParts[] = $title;
+ }
+ } elseif ($e->tagName == 'del' or $e->tagName == 'ins' or $e->tagName == 'time') {
+ // Use @datetime if available, otherwise innertext
+ $dtAttr = $e->hasAttribute('datetime') ? $e->getAttribute('datetime') : unicodeTrim($e->nodeValue);
+ if (!empty($dtAttr)) {
+ $dateParts[] = $dtAttr;
+ }
+ } else {
+ if (!empty($e->nodeValue)) {
+ $dateParts[] = unicodeTrim($e->nodeValue);
+ }
+ }
+ }
+ // Look through dateParts
+ $datePart = '';
+ $timePart = '';
+ $timezonePart = '';
+ foreach ($dateParts as $part) {
+ // Is this part a full ISO8601 datetime?
+ if (\preg_match('/^\\d{4}-\\d{2}-\\d{2}[ T]\\d{2}:\\d{2}(:\\d{2})?(Z|[+-]\\d{2}:?\\d{2})?$/', $part)) {
+ // Break completely, we’ve got our value.
+ $dtValue = $part;
+ break;
+ } else {
+ // Is the current part a valid time(+TZ?) AND no other time representation has been found?
+ if ((\preg_match('/^\\d{1,2}:\\d{2}(:\\d{2})?(Z|[+-]\\d{1,2}:?\\d{2})?$/', $part) or \preg_match('/^\\d{1,2}(:\\d{2})?(:\\d{2})?[ap]\\.?m\\.?$/i', $part)) and empty($timePart)) {
+ $timePart = $part;
+ $timezoneOffset = normalizeTimezoneOffset($timePart);
+ if (!$impliedTimezone && $timezoneOffset) {
+ $impliedTimezone = $timezoneOffset;
+ }
+ // Is the current part a valid date AND no other date representation has been found?
+ } elseif (\preg_match('/^\\d{4}-\\d{2}-\\d{2}$/', $part) and empty($datePart)) {
+ $datePart = $part;
+ // Is the current part a valid ordinal date AND no other date representation has been found?
+ } elseif (\preg_match('/^\\d{4}-\\d{3}$/', $part) and empty($datePart)) {
+ $datePart = normalizeOrdinalDate($part);
+ // Is the current part a valid timezone offset AND no other timezone part has been found?
+ } elseif (\preg_match('/^(Z|[+-]\\d{1,2}:?(\\d{2})?)$/', $part) and empty($timezonePart)) {
+ $timezonePart = $part;
+ $timezoneOffset = normalizeTimezoneOffset($timezonePart);
+ if (!$impliedTimezone && $timezoneOffset) {
+ $impliedTimezone = $timezoneOffset;
+ }
+ // Current part already represented by other VCP parts; do nothing with it
+ } else {
+ continue;
+ }
+ if (!empty($datePart) && !\in_array($datePart, $dates)) {
+ $dates[] = $datePart;
+ }
+ if (!empty($timezonePart) && !empty($timePart)) {
+ $timePart .= $timezonePart;
+ }
+ $dtValue = '';
+ if (empty($datePart) && !empty($timePart)) {
+ $timePart = convertTimeFormat($timePart);
+ $dtValue = unicodeTrim($timePart);
+ } else {
+ if (!empty($datePart) && empty($timePart)) {
+ $dtValue = \rtrim($datePart, 'T');
+ } else {
+ $timePart = convertTimeFormat($timePart);
+ $dtValue = \rtrim($datePart, 'T') . ' ' . unicodeTrim($timePart);
+ }
+ }
+ }
+ }
+ } else {
+ // Not using value-class (phew).
+ if ($dt->tagName == 'img' or $dt->tagName == 'area') {
+ // Use @alt
+ // Is it an entire dt?
+ $alt = $dt->getAttribute('alt');
+ if (!empty($alt)) {
+ $dtValue = $alt;
+ }
+ } elseif (\in_array($dt->tagName, array('data'))) {
+ // Use @value, otherwise innertext
+ // Is it an entire dt?
+ $value = $dt->getAttribute('value');
+ if (!empty($value)) {
+ $dtValue = $value;
+ } else {
+ $dtValue = $this->textContent($dt);
+ }
+ } elseif ($dt->tagName == 'abbr') {
+ // Use @title, otherwise innertext
+ // Is it an entire dt?
+ $title = $dt->getAttribute('title');
+ if (!empty($title)) {
+ $dtValue = $title;
+ } else {
+ $dtValue = $this->textContent($dt);
+ }
+ } elseif ($dt->tagName == 'del' or $dt->tagName == 'ins' or $dt->tagName == 'time') {
+ // Use @datetime if available, otherwise innertext
+ // Is it an entire dt?
+ $dtAttr = $dt->getAttribute('datetime');
+ if (!empty($dtAttr)) {
+ $dtValue = $dtAttr;
+ } else {
+ $dtValue = $this->textContent($dt);
+ }
+ } else {
+ $dtValue = $this->textContent($dt);
+ }
+ // if the dtValue is not just YYYY-MM-DD
+ if (!\preg_match('/^(\\d{4}-\\d{2}-\\d{2})$/', $dtValue)) {
+ // no implied timezone set and dtValue has a TZ offset, use un-normalized TZ offset
+ \preg_match('/Z|[+-]\\d{1,2}:?(\\d{2})?$/i', $dtValue, $matches);
+ if (!$impliedTimezone && !empty($matches[0])) {
+ $impliedTimezone = $matches[0];
+ }
+ }
+ $dtValue = unicodeTrim($dtValue);
+ // Store the date part so that we can use it when assembling the final timestamp if the next one is missing a date part
+ if (\preg_match('/(\\d{4}-\\d{2}-\\d{2})/', $dtValue, $matches)) {
+ $dates[] = $matches[0];
+ }
+ }
+ /**
+ * if $dtValue is only a time and there are recently parsed dates,
+ * form the full date-time using the most recently parsed dt- value
+ */
+ if ((\preg_match('/^\\d{1,2}:\\d{2}(:\\d{2})?(Z|[+-]\\d{2}:?\\d{2}?)?$/', $dtValue) or \preg_match('/^\\d{1,2}(:\\d{2})?(:\\d{2})?[ap]\\.?m\\.?$/i', $dtValue)) && !empty($dates)) {
+ $timezoneOffset = normalizeTimezoneOffset($dtValue);
+ if (!$impliedTimezone && $timezoneOffset) {
+ $impliedTimezone = $timezoneOffset;
+ }
+ $dtValue = convertTimeFormat($dtValue);
+ $dtValue = \end($dates) . ' ' . unicodeTrim($dtValue);
+ }
+ return $dtValue;
+ }
+ /**
+ * Given the root element of some embedded markup, return a string representing that markup
+ *
+ * @param DOMElement $e The element to parse
+ * @return string $e’s innerHTML
+ *
+ * @todo need to mark this element as e- parsed so it doesn’t get parsed as it’s parent’s e-* too
+ */
+ public function parseE(\DOMElement $e)
+ {
+ $classTitle = $this->parseValueClassTitle($e);
+ if ($classTitle !== null) {
+ return $classTitle;
+ }
+ // Expand relative URLs within children of this element
+ // TODO: as it is this is not relative to only children, make this .// and rerun tests
+ $this->resolveChildUrls($e);
+ // Temporarily move all descendants into a separate DocumentFragment.
+ // This way we can DOMDocument::saveHTML on the entire collection at once.
+ // Running DOMDocument::saveHTML per node may add whitespace that isn't in source.
+ // See https://stackoverflow.com/q/38317903
+ if ($innerNodes = $e->ownerDocument->createDocumentFragment()) {
+ while ($e->hasChildNodes()) {
+ $innerNodes->appendChild($e->firstChild);
+ }
+ $html = $e->ownerDocument->saveHtml($innerNodes);
+ // Put the nodes back in place.
+ if ($innerNodes->hasChildNodes()) {
+ $e->appendChild($innerNodes);
+ }
+ }
+ $return = array('html' => unicodeTrim($html), 'value' => $this->textContent($e));
+ if ($this->lang) {
+ // Language
+ if ($html_lang = $this->language($e)) {
+ $return['lang'] = $html_lang;
+ }
+ }
+ return $return;
+ }
+ private function removeTags(\DOMElement &$e, $tagName)
+ {
+ while (($r = $e->getElementsByTagName($tagName)) && $r->length) {
+ $r->item(0)->parentNode->removeChild($r->item(0));
+ }
+ }
+ /**
+ * Recursively parse microformats
+ *
+ * @param DOMElement $e The element to parse
+ * @param bool $is_backcompat Whether using backcompat parsing or not
+ * @param bool $has_nested_mf Whether this microformat has a nested microformat
+ * @return array A representation of the values contained within microformat $e
+ */
+ public function parseH(\DOMElement $e, $is_backcompat = \false, $has_nested_mf = \false)
+ {
+ // If it’s already been parsed (e.g. is a child mf), skip
+ if ($this->parsed->contains($e)) {
+ return null;
+ }
+ // Get current µf name
+ $mfTypes = mfNamesFromElement($e, 'h-');
+ if (!$mfTypes) {
+ return null;
+ }
+ // Initalise var to store the representation in
+ $return = array();
+ $children = array();
+ $dates = array();
+ $prefixes = array();
+ $impliedTimezone = null;
+ if ($e->tagName == 'area') {
+ $coords = $e->getAttribute('coords');
+ $shape = $e->getAttribute('shape');
+ }
+ // Handle p-*
+ foreach ($this->xpath->query('.//*[contains(concat(" ", @class) ," p-")]', $e) as $p) {
+ // element is already parsed
+ if ($this->isElementParsed($p, 'p')) {
+ continue;
+ // backcompat parsing and element was not upgraded; skip it
+ } else {
+ if ($is_backcompat && empty($this->upgraded[$p])) {
+ $this->elementPrefixParsed($p, 'p');
+ continue;
+ }
+ }
+ $prefixes[] = 'p-';
+ $pValue = $this->parseP($p);
+ // Add the value to the array for it’s p- properties
+ foreach (mfNamesFromElement($p, 'p-') as $propName) {
+ if (!empty($propName)) {
+ $return[$propName][] = $pValue;
+ }
+ }
+ // Make sure this sub-mf won’t get parsed as a top level mf
+ $this->elementPrefixParsed($p, 'p');
+ }
+ // Handle u-*
+ foreach ($this->xpath->query('.//*[contains(concat(" ", @class)," u-")]', $e) as $u) {
+ // element is already parsed
+ if ($this->isElementParsed($u, 'u')) {
+ continue;
+ // backcompat parsing and element was not upgraded; skip it
+ } else {
+ if ($is_backcompat && empty($this->upgraded[$u])) {
+ $this->elementPrefixParsed($u, 'u');
+ continue;
+ }
+ }
+ $prefixes[] = 'u-';
+ $uValue = $this->parseU($u);
+ // Add the value to the array for it’s property types
+ foreach (mfNamesFromElement($u, 'u-') as $propName) {
+ $return[$propName][] = $uValue;
+ }
+ // Make sure this sub-mf won’t get parsed as a top level mf
+ $this->elementPrefixParsed($u, 'u');
+ }
+ $temp_dates = array();
+ // Handle dt-*
+ foreach ($this->xpath->query('.//*[contains(concat(" ", @class), " dt-")]', $e) as $dt) {
+ // element is already parsed
+ if ($this->isElementParsed($dt, 'dt')) {
+ continue;
+ // backcompat parsing and element was not upgraded; skip it
+ } else {
+ if ($is_backcompat && empty($this->upgraded[$dt])) {
+ $this->elementPrefixParsed($dt, 'dt');
+ continue;
+ }
+ }
+ $prefixes[] = 'dt-';
+ $dtValue = $this->parseDT($dt, $dates, $impliedTimezone);
+ if ($dtValue) {
+ // Add the value to the array for dt- properties
+ foreach (mfNamesFromElement($dt, 'dt-') as $propName) {
+ $temp_dates[$propName][] = $dtValue;
+ }
+ }
+ // Make sure this sub-mf won’t get parsed as a top level mf
+ $this->elementPrefixParsed($dt, 'dt');
+ }
+ foreach ($temp_dates as $propName => $data) {
+ foreach ($data as $dtValue) {
+ // var_dump(preg_match('/[+-]\d{2}(\d{2})?$/i', $dtValue));
+ if ($impliedTimezone && \preg_match('/(Z|[+-]\\d{2}:?(\\d{2})?)$/i', $dtValue, $matches) == 0) {
+ $dtValue .= $impliedTimezone;
+ }
+ $return[$propName][] = $dtValue;
+ }
+ }
+ // Handle e-*
+ foreach ($this->xpath->query('.//*[contains(concat(" ", @class)," e-")]', $e) as $em) {
+ // element is already parsed
+ if ($this->isElementParsed($em, 'e')) {
+ continue;
+ // backcompat parsing and element was not upgraded; skip it
+ } else {
+ if ($is_backcompat && empty($this->upgraded[$em])) {
+ $this->elementPrefixParsed($em, 'e');
+ continue;
+ }
+ }
+ $prefixes[] = 'e-';
+ $eValue = $this->parseE($em);
+ if ($eValue) {
+ // Add the value to the array for e- properties
+ foreach (mfNamesFromElement($em, 'e-') as $propName) {
+ $return[$propName][] = $eValue;
+ }
+ }
+ // Make sure this sub-mf won’t get parsed as a top level mf
+ $this->elementPrefixParsed($em, 'e');
+ }
+ // Do we need to imply a name property?
+ // if no explicit "name" property, and no other p-* or e-* properties, and no nested microformats,
+ if (!\array_key_exists('name', $return) && !\in_array('p-', $prefixes) && !\in_array('e-', $prefixes) && !$has_nested_mf && !$is_backcompat && empty($this->upgraded[$e])) {
+ $name = \false;
+ // img.h-x[alt] or area.h-x[alt]
+ if (($e->tagName === 'img' || $e->tagName === 'area') && $e->hasAttribute('alt')) {
+ $name = $e->getAttribute('alt');
+ // abbr.h-x[title]
+ } elseif ($e->tagName === 'abbr' && $e->hasAttribute('title')) {
+ $name = $e->getAttribute('title');
+ } else {
+ $xpaths = array(
+ // .h-x>img:only-child[alt]:not([alt=""]):not[.h-*]
+ './img[not(contains(concat(" ", @class), " h-")) and count(../*) = 1 and @alt and string-length(@alt) != 0]',
+ // .h-x>area:only-child[alt]:not([alt=""]):not[.h-*]
+ './area[not(contains(concat(" ", @class), " h-")) and count(../*) = 1 and @alt and string-length(@alt) != 0]',
+ // .h-x>abbr:only-child[title]:not([title=""]):not[.h-*]
+ './abbr[not(contains(concat(" ", @class), " h-")) and count(../*) = 1 and @title and string-length(@title) != 0]',
+ // .h-x>:only-child:not[.h-*]>img:only-child[alt]:not([alt=""]):not[.h-*]
+ './*[not(contains(concat(" ", @class), " h-")) and count(../*) = 1 and count(*) = 1]/img[not(contains(concat(" ", @class), " h-")) and @alt and string-length(@alt) != 0]',
+ // .h-x>:only-child:not[.h-*]>area:only-child[alt]:not([alt=""]):not[.h-*]
+ './*[not(contains(concat(" ", @class), " h-")) and count(../*) = 1 and count(*) = 1]/area[not(contains(concat(" ", @class), " h-")) and @alt and string-length(@alt) != 0]',
+ // .h-x>:only-child:not[.h-*]>abbr:only-child[title]:not([title=""]):not[.h-*]
+ './*[not(contains(concat(" ", @class), " h-")) and count(../*) = 1 and count(*) = 1]/abbr[not(contains(concat(" ", @class), " h-")) and @title and string-length(@title) != 0]',
+ );
+ foreach ($xpaths as $xpath) {
+ $nameElement = $this->xpath->query($xpath, $e);
+ if ($nameElement !== \false && $nameElement->length === 1) {
+ $nameElement = $nameElement->item(0);
+ if ($nameElement->tagName === 'img' || $nameElement->tagName === 'area') {
+ $name = $nameElement->getAttribute('alt');
+ } else {
+ $name = $nameElement->getAttribute('title');
+ }
+ break;
+ }
+ }
+ }
+ if ($name === \false) {
+ $name = $this->textContent($e, \true);
+ }
+ $return['name'][] = unicodeTrim($name);
+ }
+ // Check for u-photo
+ if (!\array_key_exists('photo', $return) && !\in_array('u-', $prefixes) && !$has_nested_mf && !$is_backcompat) {
+ $photo = $this->parseImpliedPhoto($e);
+ if ($photo !== \false) {
+ $return['photo'][] = $photo;
+ }
+ }
+ // Do we need to imply a url property?
+ // if no explicit "url" property, and no other explicit u-* properties, and no nested microformats
+ if (!\array_key_exists('url', $return) && !\in_array('u-', $prefixes) && !$has_nested_mf && !$is_backcompat) {
+ // a.h-x[href] or area.h-x[href]
+ if (($e->tagName === 'a' || $e->tagName === 'area') && $e->hasAttribute('href')) {
+ $return['url'][] = $this->resolveUrl($e->getAttribute('href'));
+ } else {
+ $xpaths = array(
+ // .h-x>a[href]:only-of-type:not[.h-*]
+ './a[not(contains(concat(" ", @class), " h-")) and count(../a) = 1 and @href]',
+ // .h-x>area[href]:only-of-type:not[.h-*]
+ './area[not(contains(concat(" ", @class), " h-")) and count(../area) = 1 and @href]',
+ // .h-x>:only-child:not[.h-*]>a[href]:only-of-type:not[.h-*]
+ './*[not(contains(concat(" ", @class), " h-")) and count(../*) = 1 and count(a) = 1]/a[not(contains(concat(" ", @class), " h-")) and @href]',
+ // .h-x>:only-child:not[.h-*]>area[href]:only-of-type:not[.h-*]
+ './*[not(contains(concat(" ", @class), " h-")) and count(../*) = 1 and count(area) = 1]/area[not(contains(concat(" ", @class), " h-")) and @href]',
+ );
+ foreach ($xpaths as $xpath) {
+ $url = $this->xpath->query($xpath, $e);
+ if ($url !== \false && $url->length === 1) {
+ $return['url'][] = $this->resolveUrl($url->item(0)->getAttribute('href'));
+ break;
+ }
+ }
+ }
+ }
+ // Make sure things are unique and in alphabetical order
+ $mfTypes = \array_unique($mfTypes);
+ \sort($mfTypes);
+ // Properties should be an object when JSON serialised
+ if (empty($return) and $this->jsonMode) {
+ $return = new stdClass();
+ }
+ // Phew. Return the final result.
+ $parsed = array('type' => $mfTypes, 'properties' => $return);
+ if (\trim($e->getAttribute('id')) !== '') {
+ $parsed['id'] = \trim($e->getAttribute("id"));
+ }
+ if ($this->lang) {
+ // Language
+ if ($html_lang = $this->language($e)) {
+ $parsed['lang'] = $html_lang;
+ }
+ }
+ if (!empty($shape)) {
+ $parsed['shape'] = $shape;
+ }
+ if (!empty($coords)) {
+ $parsed['coords'] = $coords;
+ }
+ if (!empty($children)) {
+ $parsed['children'] = \array_values(\array_filter($children));
+ }
+ return $parsed;
+ }
+ /**
+ * @see http://microformats.org/wiki/microformats2-parsing#parsing_for_implied_properties
+ */
+ public function parseImpliedPhoto(\DOMElement $e)
+ {
+ // img.h-x[src]
+ if ($e->tagName == 'img') {
+ return $this->resolveUrl($this->parseImg($e));
+ }
+ // object.h-x[data]
+ if ($e->tagName == 'object' && $e->hasAttribute('data')) {
+ return $this->resolveUrl($e->getAttribute('data'));
+ }
+ $xpaths = array(
+ // .h-x>img[src]:only-of-type:not[.h-*]
+ './img[not(contains(concat(" ", @class), " h-")) and count(../img) = 1 and @src]',
+ // .h-x>object[data]:only-of-type:not[.h-*]
+ './object[not(contains(concat(" ", @class), " h-")) and count(../object) = 1 and @data]',
+ // .h-x>:only-child:not[.h-*]>img[src]:only-of-type:not[.h-*]
+ './*[not(contains(concat(" ", @class), " h-")) and count(../*) = 1 and count(img) = 1]/img[not(contains(concat(" ", @class), " h-")) and @src]',
+ // .h-x>:only-child:not[.h-*]>object[data]:only-of-type:not[.h-*]
+ './*[not(contains(concat(" ", @class), " h-")) and count(../*) = 1 and count(object) = 1]/object[not(contains(concat(" ", @class), " h-")) and @data]',
+ );
+ foreach ($xpaths as $path) {
+ $els = $this->xpath->query($path, $e);
+ if ($els !== \false && $els->length === 1) {
+ $el = $els->item(0);
+ if ($el->tagName == 'img') {
+ $return = $this->parseImg($el);
+ return $this->resolveUrl($return);
+ } else {
+ if ($el->tagName == 'object') {
+ return $this->resolveUrl($el->getAttribute('data'));
+ }
+ }
+ }
+ }
+ // no implied photo
+ return \false;
+ }
+ /**
+ * Parse rels and alternates
+ *
+ * Returns [$rels, $rel_urls, $alternates].
+ * For $rels and $rel_urls, if they are empty and $this->jsonMode = true, they will be returned as stdClass,
+ * optimizing for JSON serialization. Otherwise they will be returned as an empty array.
+ * Note that $alternates is deprecated in the microformats spec in favor of $rel_urls. $alternates only appears
+ * in parsed results if $this->enableAlternates = true.
+ * @return array|stdClass
+ */
+ public function parseRelsAndAlternates()
+ {
+ $rels = array();
+ $rel_urls = array();
+ $alternates = array();
+ // Iterate through all a, area and link elements with rel attributes
+ foreach ($this->xpath->query('//a[@rel and @href] | //link[@rel and @href] | //area[@rel and @href]') as $hyperlink) {
+ // Parse the set of rels for the current link
+ $linkRels = \array_unique(\array_filter(\preg_split('/[\\t\\n\\f\\r ]/', $hyperlink->getAttribute('rel'))));
+ if (\count($linkRels) === 0) {
+ continue;
+ }
+ // Resolve the href
+ $href = $this->resolveUrl($hyperlink->getAttribute('href'));
+ $rel_attributes = array();
+ if ($hyperlink->hasAttribute('media')) {
+ $rel_attributes['media'] = $hyperlink->getAttribute('media');
+ }
+ if ($hyperlink->hasAttribute('hreflang')) {
+ $rel_attributes['hreflang'] = $hyperlink->getAttribute('hreflang');
+ }
+ if ($hyperlink->hasAttribute('title')) {
+ $rel_attributes['title'] = $hyperlink->getAttribute('title');
+ }
+ if ($hyperlink->hasAttribute('type')) {
+ $rel_attributes['type'] = $hyperlink->getAttribute('type');
+ }
+ if (\strlen($hyperlink->textContent) > 0) {
+ $rel_attributes['text'] = $hyperlink->textContent;
+ }
+ if ($this->enableAlternates) {
+ // If 'alternate' in rels, create 'alternates' structure, append
+ if (\in_array('alternate', $linkRels)) {
+ $alternates[] = \array_merge($rel_attributes, array('url' => $href, 'rel' => \implode(' ', \array_diff($linkRels, array('alternate')))));
+ }
+ }
+ foreach ($linkRels as $rel) {
+ if (!\array_key_exists($rel, $rels)) {
+ $rels[$rel] = array($href);
+ } elseif (!\in_array($href, $rels[$rel])) {
+ $rels[$rel][] = $href;
+ }
+ }
+ if (!\array_key_exists($href, $rel_urls)) {
+ $rel_urls[$href] = array('rels' => array());
+ }
+ // Add the attributes collected only if they were not already set
+ $rel_urls[$href] = \array_merge($rel_attributes, $rel_urls[$href]);
+ // Merge current rels with those already set
+ $rel_urls[$href]['rels'] = \array_merge($rel_urls[$href]['rels'], $linkRels);
+ }
+ // Alphabetically sort the rels arrays after removing duplicates
+ foreach ($rel_urls as $href => $object) {
+ $rel_urls[$href]['rels'] = \array_unique($rel_urls[$href]['rels']);
+ \sort($rel_urls[$href]['rels']);
+ }
+ if (empty($rels) and $this->jsonMode) {
+ $rels = new stdClass();
+ }
+ if (empty($rel_urls) and $this->jsonMode) {
+ $rel_urls = new stdClass();
+ }
+ return array($rels, $rel_urls, $alternates);
+ }
+ /**
+ * Find rel=tag elements that don't have class=category and have an href.
+ * For each element, get the last non-empty URL segment. Append a
+ * element with that value as the category. Uses the mf1 class 'category'
+ * which will then be upgraded to p-category during backcompat.
+ * @param DOMElement $el
+ */
+ public function upgradeRelTagToCategory(DOMElement $el)
+ {
+ $rel_tag = $this->xpath->query('.//a[contains(concat(" ",normalize-space(@rel)," ")," tag ") and not(contains(concat(" ", normalize-space(@class), " "), " category ")) and @href]', $el);
+ if ($rel_tag->length) {
+ foreach ($rel_tag as $tempEl) {
+ $path = \trim(\parse_url($tempEl->getAttribute('href'), \PHP_URL_PATH), ' /');
+ $segments = \explode('/', $path);
+ $value = \array_pop($segments);
+ # build the element
+ $dataEl = $tempEl->ownerDocument->createElement('data');
+ $dataEl->setAttribute('class', 'category');
+ $dataEl->setAttribute('value', $value);
+ # append as child of input element. this should ensure added element does get parsed inside e-*
+ $el->appendChild($dataEl);
+ }
+ }
+ }
+ /**
+ * Kicks off the parsing routine
+ * @param bool $convertClassic whether to do backcompat parsing on microformats1. Defaults to true.
+ * @param DOMElement $context optionally specify an element from which to parse microformats
+ * @return array An array containing all the microformats found in the current document
+ */
+ public function parse($convertClassic = \true, DOMElement $context = null)
+ {
+ $this->convertClassic = $convertClassic;
+ $mfs = $this->parse_recursive($context);
+ // Parse rels
+ list($rels, $rel_urls, $alternates) = $this->parseRelsAndAlternates();
+ $top = array('items' => \array_values(\array_filter($mfs)), 'rels' => $rels, 'rel-urls' => $rel_urls);
+ if ($this->enableAlternates && \count($alternates)) {
+ $top['alternates'] = $alternates;
+ }
+ return $top;
+ }
+ /**
+ * Parse microformats recursively
+ * Keeps track of whether inside a backcompat root or not
+ * @param DOMElement $context: node to start with
+ * @param int $depth: recursion depth
+ * @return array
+ */
+ public function parse_recursive(DOMElement $context = null, $depth = 0)
+ {
+ $mfs = array();
+ $mfElements = $this->getRootMF($context);
+ foreach ($mfElements as $node) {
+ $is_backcompat = !$this->hasRootMf2($node);
+ if ($this->convertClassic && $is_backcompat) {
+ $this->backcompat($node);
+ }
+ $recurse = $this->parse_recursive($node, $depth + 1);
+ // set bool flag for nested mf
+ $has_nested_mf = (bool) $recurse;
+ // parse for root mf
+ $result = $this->parseH($node, $is_backcompat, $has_nested_mf);
+ // TODO: Determine if clearing this is required?
+ $this->elementPrefixParsed($node, 'h');
+ $this->elementPrefixParsed($node, 'p');
+ $this->elementPrefixParsed($node, 'u');
+ $this->elementPrefixParsed($node, 'dt');
+ $this->elementPrefixParsed($node, 'e');
+ // parseH returned a parsed result
+ if ($result) {
+ // merge recursive results into current results
+ if ($recurse) {
+ $result = \array_merge_recursive($result, $recurse);
+ }
+ // currently a nested mf; check if node is an mf property of parent
+ if ($depth > 0) {
+ $temp_properties = nestedMfPropertyNamesFromElement($node);
+ // properties found; set up parsed result in 'properties'
+ if (!empty($temp_properties)) {
+ foreach ($temp_properties as $property => $prefixes) {
+ // Note: handling microformat nesting under multiple conflicting prefixes is not currently specified by the mf2 parsing spec.
+ $prefixSpecificResult = $result;
+ if (\in_array('p-', $prefixes)) {
+ $prefixSpecificResult['value'] = !\is_array($prefixSpecificResult['properties']) || empty($prefixSpecificResult['properties']['name'][0]) ? $this->parseP($node) : $prefixSpecificResult['properties']['name'][0];
+ } elseif (\in_array('e-', $prefixes)) {
+ $eParsedResult = $this->parseE($node);
+ $prefixSpecificResult['html'] = $eParsedResult['html'];
+ $prefixSpecificResult['value'] = $eParsedResult['value'];
+ } elseif (\in_array('u-', $prefixes)) {
+ $prefixSpecificResult['value'] = !\is_array($result['properties']) || empty($result['properties']['url']) ? $this->parseU($node) : \reset($result['properties']['url']);
+ } elseif (\in_array('dt-', $prefixes)) {
+ $parsed_property = $this->parseDT($node);
+ $prefixSpecificResult['value'] = $parsed_property ? $parsed_property : '';
+ }
+ $prefixSpecificResult['value'] = \is_array($prefixSpecificResult['value']) ? $prefixSpecificResult['value']['value'] : $prefixSpecificResult['value'];
+ $mfs['properties'][$property][] = $prefixSpecificResult;
+ }
+ // otherwise, set up in 'children'
+ } else {
+ $mfs['children'][] = $result;
+ }
+ // otherwise, top-level mf
+ } else {
+ $mfs[] = $result;
+ }
+ }
+ }
+ return $mfs;
+ }
+ /**
+ * Parse From ID
+ *
+ * Given an ID, parse all microformats which are children of the element with
+ * that ID.
+ *
+ * Note that rel values are still document-wide.
+ *
+ * If an element with the ID is not found, an empty skeleton mf2 array structure
+ * will be returned.
+ *
+ * @param string $id
+ * @param bool $htmlSafe = false whether or not to HTML-encode angle brackets in non e-* properties
+ * @return array
+ */
+ public function parseFromId($id, $convertClassic = \true)
+ {
+ $matches = $this->xpath->query("//*[@id='{$id}']");
+ if (empty($matches)) {
+ return array('items' => array(), 'rels' => array(), 'alternates' => array());
+ }
+ return $this->parse($convertClassic, $matches->item(0));
+ }
+ /**
+ * Get the root microformat elements
+ * @param DOMElement $context
+ * @return DOMNodeList
+ */
+ public function getRootMF(DOMElement $context = null)
+ {
+ // start with mf2 root class name xpath
+ $xpaths = array('contains(concat(" ",normalize-space(@class)), " h-")');
+ // add mf1 root class names
+ foreach ($this->classicRootMap as $old => $new) {
+ $xpaths[] = '( contains(concat(" ",normalize-space(@class), " "), " ' . $old . ' ") )';
+ }
+ // final xpath with OR
+ $xpath = '//*[' . \implode(' or ', $xpaths) . ']';
+ $mfElements = null === $context ? $this->xpath->query($xpath) : $this->xpath->query('.' . $xpath, $context);
+ return $mfElements;
+ }
+ /**
+ * Apply the backcompat algorithm to upgrade mf1 classes to mf2.
+ * This method is called recursively.
+ * @param DOMElement $el
+ * @param string $context
+ * @param bool $isParentMf2
+ * @see http://microformats.org/wiki/microformats2-parsing#algorithm
+ */
+ public function backcompat(DOMElement $el, $context = '', $isParentMf2 = \false)
+ {
+ if ($context) {
+ $mf1Classes = array($context);
+ } else {
+ $class = \str_replace(array("\t", "\n"), ' ', $el->getAttribute('class'));
+ $classes = \array_filter(\explode(' ', $class));
+ $mf1Classes = \array_intersect($classes, \array_keys($this->classicRootMap));
+ }
+ $elHasMf2 = $this->hasRootMf2($el);
+ foreach ($mf1Classes as $classname) {
+ // special handling for specific properties
+ switch ($classname) {
+ case 'hentry':
+ $this->upgradeRelTagToCategory($el);
+ $rel_bookmark = $this->xpath->query('.//a[contains(concat(" ",normalize-space(@rel)," ")," bookmark ") and @href]', $el);
+ if ($rel_bookmark->length) {
+ foreach ($rel_bookmark as $tempEl) {
+ $this->addMfClasses($tempEl, 'u-url');
+ $this->addUpgraded($tempEl, array('bookmark'));
+ }
+ }
+ break;
+ case 'hreview':
+ $item_and_vcard = $this->xpath->query('.//*[contains(concat(" ", normalize-space(@class), " "), " item ") and contains(concat(" ", normalize-space(@class), " "), " vcard ")]', $el);
+ if ($item_and_vcard->length) {
+ foreach ($item_and_vcard as $tempEl) {
+ if (!$this->hasRootMf2($tempEl)) {
+ $this->backcompat($tempEl, 'vcard');
+ $this->addMfClasses($tempEl, 'p-item h-card');
+ $this->addUpgraded($tempEl, array('item', 'vcard'));
+ }
+ }
+ }
+ $item_and_vevent = $this->xpath->query('.//*[contains(concat(" ", normalize-space(@class), " "), " item ") and contains(concat(" ", normalize-space(@class), " "), " vevent ")]', $el);
+ if ($item_and_vevent->length) {
+ foreach ($item_and_vevent as $tempEl) {
+ if (!$this->hasRootMf2($tempEl)) {
+ $this->addMfClasses($tempEl, 'p-item h-event');
+ $this->backcompat($tempEl, 'vevent');
+ $this->addUpgraded($tempEl, array('item', 'vevent'));
+ }
+ }
+ }
+ $item_and_hproduct = $this->xpath->query('.//*[contains(concat(" ", normalize-space(@class), " "), " item ") and contains(concat(" ", normalize-space(@class), " "), " hproduct ")]', $el);
+ if ($item_and_hproduct->length) {
+ foreach ($item_and_hproduct as $tempEl) {
+ if (!$this->hasRootMf2($tempEl)) {
+ $this->addMfClasses($tempEl, 'p-item h-product');
+ $this->backcompat($tempEl, 'vevent');
+ $this->addUpgraded($tempEl, array('item', 'hproduct'));
+ }
+ }
+ }
+ $this->upgradeRelTagToCategory($el);
+ break;
+ case 'vevent':
+ $location = $this->xpath->query('.//*[contains(concat(" ", normalize-space(@class), " "), " location ")]', $el);
+ if ($location->length) {
+ foreach ($location as $tempEl) {
+ if (!$this->hasRootMf2($tempEl)) {
+ $this->addMfClasses($tempEl, 'h-card');
+ $this->backcompat($tempEl, 'vcard');
+ }
+ }
+ }
+ break;
+ }
+ // root class has mf1 properties to be upgraded
+ if (isset($this->classicPropertyMap[$classname])) {
+ // loop through each property of the mf1 root
+ foreach ($this->classicPropertyMap[$classname] as $property => $data) {
+ $propertyElements = $this->xpath->query('.//*[contains(concat(" ", normalize-space(@class), " "), " ' . $property . ' ")]', $el);
+ // loop through each element with the property
+ foreach ($propertyElements as $propertyEl) {
+ $hasRootMf2 = $this->hasRootMf2($propertyEl);
+ // if the element has not been upgraded and we're not inside an mf2 root, recurse
+ if (!$this->isElementUpgraded($propertyEl, $property) && !$isParentMf2) {
+ $temp_context = isset($data['context']) ? $data['context'] : null;
+ $this->backcompat($propertyEl, $temp_context, $hasRootMf2);
+ $this->addMfClasses($propertyEl, $data['replace']);
+ }
+ $this->addUpgraded($propertyEl, $property);
+ }
+ }
+ }
+ if (empty($context) && isset($this->classicRootMap[$classname]) && !$elHasMf2) {
+ $this->addMfClasses($el, $this->classicRootMap[$classname]);
+ }
+ }
+ return;
+ }
+ /**
+ * Add element + property as upgraded during backcompat
+ * @param DOMElement $el
+ * @param string|array $property
+ */
+ public function addUpgraded(DOMElement $el, $property)
+ {
+ if (!\is_array($property)) {
+ $property = array($property);
+ }
+ // add element to list of upgraded elements
+ if (!$this->upgraded->contains($el)) {
+ $this->upgraded->attach($el, $property);
+ } else {
+ $this->upgraded[$el] = \array_merge($this->upgraded[$el], $property);
+ }
+ }
+ /**
+ * Add the provided classes to an element.
+ * Does not add duplicate if class name already exists.
+ * @param DOMElement $el
+ * @param string $classes
+ */
+ public function addMfClasses(DOMElement $el, $classes)
+ {
+ $existingClasses = \str_replace(array("\t", "\n"), ' ', $el->getAttribute('class'));
+ $existingClasses = \array_filter(\explode(' ', $existingClasses));
+ $addClasses = \array_diff(\explode(' ', $classes), $existingClasses);
+ if ($addClasses) {
+ $el->setAttribute('class', $el->getAttribute('class') . ' ' . \implode(' ', $addClasses));
+ }
+ }
+ /**
+ * Check an element for mf2 h-* class, typically to determine if backcompat should be used
+ * @param DOMElement $el
+ */
+ public function hasRootMf2(\DOMElement $el)
+ {
+ $class = \str_replace(array("\t", "\n"), ' ', $el->getAttribute('class'));
+ $classes = \array_filter(\explode(' ', $class));
+ foreach ($classes as $classname) {
+ if (\strpos($classname, 'h-') === 0) {
+ return \true;
+ }
+ }
+ return \false;
+ }
+ /**
+ * Convert Legacy Classnames
+ *
+ * Adds microformats2 classnames into a document containing only legacy
+ * semantic classnames.
+ *
+ * @return Parser $this
+ */
+ public function convertLegacy()
+ {
+ $doc = $this->doc;
+ $xp = new DOMXPath($doc);
+ // replace all roots
+ foreach ($this->classicRootMap as $old => $new) {
+ foreach ($xp->query('//*[contains(concat(" ", @class, " "), " ' . $old . ' ") and not(contains(concat(" ", @class, " "), " ' . $new . ' "))]') as $el) {
+ $el->setAttribute('class', $el->getAttribute('class') . ' ' . $new);
+ }
+ }
+ foreach ($this->classicPropertyMap as $oldRoot => $properties) {
+ $newRoot = $this->classicRootMap[$oldRoot];
+ foreach ($properties as $old => $data) {
+ foreach ($xp->query('//*[contains(concat(" ", @class, " "), " ' . $oldRoot . ' ")]//*[contains(concat(" ", @class, " "), " ' . $old . ' ") and not(contains(concat(" ", @class, " "), " ' . $data['replace'] . ' "))]') as $el) {
+ $el->setAttribute('class', $el->getAttribute('class') . ' ' . $data['replace']);
+ }
+ }
+ }
+ return $this;
+ }
+ /**
+ * XPath Query
+ *
+ * Runs an XPath query over the current document. Works in exactly the same
+ * way as DOMXPath::query.
+ *
+ * @param string $expression
+ * @param DOMNode $context
+ * @return DOMNodeList
+ */
+ public function query($expression, $context = null)
+ {
+ return $this->xpath->query($expression, $context);
+ }
+ /**
+ * Classic Root Classname map
+ * @var array
+ */
+ public $classicRootMap = array('vcard' => 'h-card', 'hfeed' => 'h-feed', 'hentry' => 'h-entry', 'hrecipe' => 'h-recipe', 'hresume' => 'h-resume', 'vevent' => 'h-event', 'hreview' => 'h-review', 'hproduct' => 'h-product', 'adr' => 'h-adr');
+ /**
+ * Mapping of mf1 properties to mf2 and the context they're parsed with
+ * @var array
+ */
+ public $classicPropertyMap = array('vcard' => array('fn' => array('replace' => 'p-name'), 'honorific-prefix' => array('replace' => 'p-honorific-prefix'), 'given-name' => array('replace' => 'p-given-name'), 'additional-name' => array('replace' => 'p-additional-name'), 'family-name' => array('replace' => 'p-family-name'), 'honorific-suffix' => array('replace' => 'p-honorific-suffix'), 'nickname' => array('replace' => 'p-nickname'), 'email' => array('replace' => 'u-email'), 'logo' => array('replace' => 'u-logo'), 'photo' => array('replace' => 'u-photo'), 'url' => array('replace' => 'u-url'), 'uid' => array('replace' => 'u-uid'), 'category' => array('replace' => 'p-category'), 'adr' => array('replace' => 'p-adr'), 'extended-address' => array('replace' => 'p-extended-address'), 'street-address' => array('replace' => 'p-street-address'), 'locality' => array('replace' => 'p-locality'), 'region' => array('replace' => 'p-region'), 'postal-code' => array('replace' => 'p-postal-code'), 'country-name' => array('replace' => 'p-country-name'), 'label' => array('replace' => 'p-label'), 'geo' => array('replace' => 'p-geo h-geo', 'context' => 'geo'), 'latitude' => array('replace' => 'p-latitude'), 'longitude' => array('replace' => 'p-longitude'), 'tel' => array('replace' => 'p-tel'), 'note' => array('replace' => 'p-note'), 'bday' => array('replace' => 'dt-bday'), 'key' => array('replace' => 'u-key'), 'org' => array('replace' => 'p-org'), 'organization-name' => array('replace' => 'p-organization-name'), 'organization-unit' => array('replace' => 'p-organization-unit'), 'title' => array('replace' => 'p-job-title'), 'role' => array('replace' => 'p-role'), 'tz' => array('replace' => 'p-tz'), 'rev' => array('replace' => 'dt-rev')), 'hfeed' => array(), 'hentry' => array('entry-title' => array('replace' => 'p-name'), 'entry-summary' => array('replace' => 'p-summary'), 'entry-content' => array('replace' => 'e-content'), 'published' => array('replace' => 'dt-published'), 'updated' => array('replace' => 'dt-updated'), 'author' => array('replace' => 'p-author h-card', 'context' => 'vcard'), 'category' => array('replace' => 'p-category')), 'hrecipe' => array('fn' => array('replace' => 'p-name'), 'ingredient' => array('replace' => 'p-ingredient'), 'yield' => array('replace' => 'p-yield'), 'instructions' => array('replace' => 'e-instructions'), 'duration' => array('replace' => 'dt-duration'), 'photo' => array('replace' => 'u-photo'), 'summary' => array('replace' => 'p-summary'), 'author' => array('replace' => 'p-author h-card', 'context' => 'vcard'), 'nutrition' => array('replace' => 'p-nutrition'), 'category' => array('replace' => 'p-category')), 'hresume' => array('summary' => array('replace' => 'p-summary'), 'contact' => array('replace' => 'p-contact h-card', 'context' => 'vcard'), 'education' => array('replace' => 'p-education h-event', 'context' => 'vevent'), 'experience' => array('replace' => 'p-experience h-event', 'context' => 'vevent'), 'skill' => array('replace' => 'p-skill'), 'affiliation' => array('replace' => 'p-affiliation h-card', 'context' => 'vcard')), 'vevent' => array('summary' => array('replace' => 'p-name'), 'dtstart' => array('replace' => 'dt-start'), 'dtend' => array('replace' => 'dt-end'), 'duration' => array('replace' => 'dt-duration'), 'description' => array('replace' => 'p-description'), 'url' => array('replace' => 'u-url'), 'category' => array('replace' => 'p-category'), 'location' => array('replace' => 'h-card', 'context' => 'vcard'), 'geo' => array('replace' => 'p-location h-geo')), 'hreview' => array(
+ 'summary' => array('replace' => 'p-name'),
+ # fn: see item.fn below
+ # photo: see item.photo below
+ # url: see item.url below
+ 'item' => array('replace' => 'p-item h-item', 'context' => 'item'),
+ 'reviewer' => array('replace' => 'p-author h-card', 'context' => 'vcard'),
+ 'dtreviewed' => array('replace' => 'dt-published'),
+ 'rating' => array('replace' => 'p-rating'),
+ 'best' => array('replace' => 'p-best'),
+ 'worst' => array('replace' => 'p-worst'),
+ 'description' => array('replace' => 'e-content'),
+ 'category' => array('replace' => 'p-category'),
+ ), 'hproduct' => array('fn' => array('replace' => 'p-name'), 'photo' => array('replace' => 'u-photo'), 'brand' => array('replace' => 'p-brand'), 'category' => array('replace' => 'p-category'), 'description' => array('replace' => 'p-description'), 'identifier' => array('replace' => 'u-identifier'), 'url' => array('replace' => 'u-url'), 'review' => array('replace' => 'p-review h-review'), 'price' => array('replace' => 'p-price')), 'item' => array('fn' => array('replace' => 'p-name'), 'url' => array('replace' => 'u-url'), 'photo' => array('replace' => 'u-photo')), 'adr' => array('post-office-box' => array('replace' => 'p-post-office-box'), 'extended-address' => array('replace' => 'p-extended-address'), 'street-address' => array('replace' => 'p-street-address'), 'locality' => array('replace' => 'p-locality'), 'region' => array('replace' => 'p-region'), 'postal-code' => array('replace' => 'p-postal-code'), 'country-name' => array('replace' => 'p-country-name')), 'geo' => array('latitude' => array('replace' => 'p-latitude'), 'longitude' => array('replace' => 'p-longitude')));
+}
+/** @internal */
+function parseUriToComponents($uri)
+{
+ $result = array('scheme' => null, 'authority' => null, 'path' => null, 'query' => null, 'fragment' => null);
+ $u = @\parse_url($uri);
+ if (\array_key_exists('scheme', $u)) {
+ $result['scheme'] = $u['scheme'];
+ }
+ if (\array_key_exists('host', $u)) {
+ if (\array_key_exists('user', $u)) {
+ $result['authority'] = $u['user'];
+ }
+ if (\array_key_exists('pass', $u)) {
+ $result['authority'] .= ':' . $u['pass'];
+ }
+ if (\array_key_exists('user', $u) || \array_key_exists('pass', $u)) {
+ $result['authority'] .= '@';
+ }
+ $result['authority'] .= $u['host'];
+ if (\array_key_exists('port', $u)) {
+ $result['authority'] .= ':' . $u['port'];
+ }
+ }
+ if (\array_key_exists('path', $u)) {
+ $result['path'] = $u['path'];
+ }
+ if (\array_key_exists('query', $u)) {
+ $result['query'] = $u['query'];
+ }
+ if (\array_key_exists('fragment', $u)) {
+ $result['fragment'] = $u['fragment'];
+ }
+ return $result;
+}
+/** @internal */
+function resolveUrl($baseURI, $referenceURI)
+{
+ $target = array('scheme' => null, 'authority' => null, 'path' => null, 'query' => null, 'fragment' => null);
+ # 5.2.1 Pre-parse the Base URI
+ # The base URI (Base) is established according to the procedure of
+ # Section 5.1 and parsed into the five main components described in
+ # Section 3
+ $base = parseUriToComponents($baseURI);
+ # If base path is blank (http://example.com) then set it to /
+ # (I can't tell if this is actually in the RFC or not, but seems like it makes sense)
+ if ($base['path'] == null) {
+ $base['path'] = '/';
+ }
+ # 5.2.2. Transform References
+ # The URI reference is parsed into the five URI components
+ # (R.scheme, R.authority, R.path, R.query, R.fragment) = parse(R);
+ $reference = parseUriToComponents($referenceURI);
+ # A non-strict parser may ignore a scheme in the reference
+ # if it is identical to the base URI's scheme.
+ # TODO
+ if ($reference['scheme']) {
+ $target['scheme'] = $reference['scheme'];
+ $target['authority'] = $reference['authority'];
+ $target['path'] = removeDotSegments($reference['path']);
+ $target['query'] = $reference['query'];
+ } else {
+ if ($reference['authority']) {
+ $target['authority'] = $reference['authority'];
+ $target['path'] = removeDotSegments($reference['path']);
+ $target['query'] = $reference['query'];
+ } else {
+ if ($reference['path'] == '') {
+ $target['path'] = $base['path'];
+ if ($reference['query']) {
+ $target['query'] = $reference['query'];
+ } else {
+ $target['query'] = $base['query'];
+ }
+ } else {
+ if (\substr($reference['path'], 0, 1) == '/') {
+ $target['path'] = removeDotSegments($reference['path']);
+ } else {
+ $target['path'] = mergePaths($base, $reference);
+ $target['path'] = removeDotSegments($target['path']);
+ }
+ $target['query'] = $reference['query'];
+ }
+ $target['authority'] = $base['authority'];
+ }
+ $target['scheme'] = $base['scheme'];
+ }
+ $target['fragment'] = $reference['fragment'];
+ # 5.3 Component Recomposition
+ $result = '';
+ if ($target['scheme']) {
+ $result .= $target['scheme'] . ':';
+ }
+ if ($target['authority']) {
+ $result .= '//' . $target['authority'];
+ }
+ $result .= $target['path'];
+ if ($target['query']) {
+ $result .= '?' . $target['query'];
+ }
+ if ($target['fragment']) {
+ $result .= '#' . $target['fragment'];
+ } elseif ($referenceURI == '#') {
+ $result .= '#';
+ }
+ return $result;
+}
+# 5.2.3 Merge Paths
+/** @internal */
+function mergePaths($base, $reference)
+{
+ # If the base URI has a defined authority component and an empty
+ # path,
+ if ($base['authority'] && $base['path'] == null) {
+ # then return a string consisting of "/" concatenated with the
+ # reference's path; otherwise,
+ $merged = '/' . $reference['path'];
+ } else {
+ if (($pos = \strrpos($base['path'], '/')) !== \false) {
+ # return a string consisting of the reference's path component
+ # appended to all but the last segment of the base URI's path (i.e.,
+ # excluding any characters after the right-most "/" in the base URI
+ # path,
+ $merged = \substr($base['path'], 0, $pos + 1) . $reference['path'];
+ } else {
+ # or excluding the entire base URI path if it does not contain
+ # any "/" characters).
+ $merged = $base['path'];
+ }
+ }
+ return $merged;
+}
+# 5.2.4.A Remove leading ../ or ./
+/** @internal */
+function removeLeadingDotSlash(&$input)
+{
+ if (\substr($input, 0, 3) == '../') {
+ $input = \substr($input, 3);
+ } elseif (\substr($input, 0, 2) == './') {
+ $input = \substr($input, 2);
+ }
+}
+# 5.2.4.B Replace leading /. with /
+/** @internal */
+function removeLeadingSlashDot(&$input)
+{
+ if (\substr($input, 0, 3) == '/./') {
+ $input = '/' . \substr($input, 3);
+ } else {
+ $input = '/' . \substr($input, 2);
+ }
+}
+# 5.2.4.C Given leading /../ remove component from output buffer
+/** @internal */
+function removeOneDirLevel(&$input, &$output)
+{
+ if (\substr($input, 0, 4) == '/../') {
+ $input = '/' . \substr($input, 4);
+ } else {
+ $input = '/' . \substr($input, 3);
+ }
+ $output = \substr($output, 0, \strrpos($output, '/'));
+}
+# 5.2.4.D Remove . and .. if it's the only thing in the input
+/** @internal */
+function removeLoneDotDot(&$input)
+{
+ if ($input == '.') {
+ $input = \substr($input, 1);
+ } else {
+ $input = \substr($input, 2);
+ }
+}
+# 5.2.4.E Move one segment from input to output
+/** @internal */
+function moveOneSegmentFromInput(&$input, &$output)
+{
+ if (\substr($input, 0, 1) != '/') {
+ $pos = \strpos($input, '/');
+ } else {
+ $pos = \strpos($input, '/', 1);
+ }
+ if ($pos === \false) {
+ $output .= $input;
+ $input = '';
+ } else {
+ $output .= \substr($input, 0, $pos);
+ $input = \substr($input, $pos);
+ }
+}
+# 5.2.4 Remove Dot Segments
+/** @internal */
+function removeDotSegments($path)
+{
+ # 1. The input buffer is initialized with the now-appended path
+ # components and the output buffer is initialized to the empty
+ # string.
+ $input = $path;
+ $output = '';
+ $step = 0;
+ # 2. While the input buffer is not empty, loop as follows:
+ while ($input) {
+ $step++;
+ if (\substr($input, 0, 3) == '../' || \substr($input, 0, 2) == './') {
+ # A. If the input buffer begins with a prefix of "../" or "./",
+ # then remove that prefix from the input buffer; otherwise,
+ removeLeadingDotSlash($input);
+ } elseif (\substr($input, 0, 3) == '/./' || $input == '/.') {
+ # B. if the input buffer begins with a prefix of "/./" or "/.",
+ # where "." is a complete path segment, then replace that
+ # prefix with "/" in the input buffer; otherwise,
+ removeLeadingSlashDot($input);
+ } elseif (\substr($input, 0, 4) == '/../' || $input == '/..') {
+ # C. if the input buffer begins with a prefix of "/../" or "/..",
+ # where ".." is a complete path segment, then replace that
+ # prefix with "/" in the input buffer and remove the last
+ # segment and its preceding "/" (if any) from the output
+ # buffer; otherwise,
+ removeOneDirLevel($input, $output);
+ } elseif ($input == '.' || $input == '..') {
+ # D. if the input buffer consists only of "." or "..", then remove
+ # that from the input buffer; otherwise,
+ removeLoneDotDot($input);
+ } else {
+ # E. move the first path segment in the input buffer to the end of
+ # the output buffer and any subsequent characters up to, but not including,
+ # the next "/" character or the end of the input buffer
+ moveOneSegmentFromInput($input, $output);
+ }
+ }
+ return $output;
+}
diff --git a/libs/mf2/mf2/composer.json b/libs/mf2/mf2/composer.json
new file mode 100644
index 0000000..7242aaa
--- /dev/null
+++ b/libs/mf2/mf2/composer.json
@@ -0,0 +1,45 @@
+{
+ "name": "mf2\/mf2",
+ "type": "library",
+ "description": "A pure, generic microformats2 parser \u2014 makes HTML as easy to consume as a JSON API",
+ "keywords": [
+ "microformats",
+ "microformats 2",
+ "parser",
+ "semantic",
+ "html"
+ ],
+ "authors": [
+ {
+ "name": "Barnaby Walters",
+ "homepage": "http:\/\/waterpigs.co.uk"
+ }
+ ],
+ "bin": [
+ "bin\/fetch-mf2",
+ "bin\/parse-mf2"
+ ],
+ "require": {
+ "php": ">=5.6.0"
+ },
+ "config": {
+ "platform": {}
+ },
+ "require-dev": {
+ "phpunit\/phpunit": "^5.7",
+ "mf2\/tests": "dev-master#e9e2b905821ba0a5b59dab1a8eaf40634ce9cd49",
+ "squizlabs\/php_codesniffer": "^3.6.2",
+ "dealerdirect\/phpcodesniffer-composer-installer": "^0.7",
+ "phpcompatibility\/php-compatibility": "^9.3"
+ },
+ "autoload": {
+ "files": [
+ "Mf2\/Parser.php"
+ ]
+ },
+ "license": "CC0-1.0",
+ "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."
+ }
+}
\ No newline at end of file
diff --git a/libs/mf2/mf2/tests/Mf2/ClassicMicroformatsTest.php b/libs/mf2/mf2/tests/Mf2/ClassicMicroformatsTest.php
new file mode 100644
index 0000000..f47f985
--- /dev/null
+++ b/libs/mf2/mf2/tests/Mf2/ClassicMicroformatsTest.php
@@ -0,0 +1,962 @@
+ µf2 functionality.
+ *
+ * Mainly based off BC tables on http://microformats.org/wiki/microformats2#v2_vocabularies
+ * @internal
+ */
+class ClassicMicroformatsTest extends PHPUnit_Framework_TestCase
+{
+ public function setUp()
+ {
+ \date_default_timezone_set('Europe/London');
+ }
+ public function testParsesClassicHcard()
+ {
+ $input = 'Barnaby Walters is a person.
';
+ $expected = '{"items": [{"type": ["h-card"], "properties": {"name": ["Barnaby Walters"]}}], "rels": {}, "rel-urls": {}}';
+ $parser = new Parser($input, '', \true);
+ $this->assertJsonStringEqualsJsonString(\json_encode($parser->parse()), $expected);
+ }
+ public function testParsesClassicHEntry()
+ {
+ $input = 'microformats2 Is Great yes yes it is.
';
+ $expected = '{"items": [{"type": ["h-entry"], "properties": {"name": ["microformats2 Is Great"], "summary": ["yes yes it is."]}}], "rels": {}, "rel-urls": {}}';
+ $parser = new Parser($input, '', \true);
+ $this->assertJsonStringEqualsJsonString(\json_encode($parser->parse()), $expected);
+ }
+ public function testIgnoresClassicClassnamesUnderMf2Root()
+ {
+ $input = <<
+\tNot Me
+\tI wrote this
+
+EOT;
+ $parser = new Parser($input);
+ $result = $parser->parse();
+ $this->assertEquals('I wrote this', $result['items'][0]['properties']['author'][0]['properties']['name'][0]);
+ }
+ public function testIgnoresClassicPropertyClassnamesOutsideClassicRoots()
+ {
+ $input = <<Mr. Invisible
+EOT;
+ $parser = new Parser($input);
+ $result = $parser->parse();
+ $this->assertCount(0, $result['items']);
+ }
+ public function testParsesFBerrimanClassicHEntry()
+ {
+ $input = <<
+\t
+
+\t\t
+\t\t
April was pretty decent. I got to attend two very good conferences and I got to speak at them.
+\t\t\t
+
+\t
+
+EOT;
+ $parser = new Parser($input);
+ $result = $parser->parse();
+ $e = $result['items'][0];
+ $this->assertContains('h-entry', $e['type']);
+ $this->assertArrayHasKey('category', $e['properties']);
+ $this->assertCount(7, $e['properties']['category']);
+ $this->assertContains('speaking', $e['properties']['category']);
+ $this->assertContains('web-dev', $e['properties']['category']);
+ $this->assertContains('conferences', $e['properties']['category']);
+ $this->assertContains('front-trends', $e['properties']['category']);
+ $this->assertContains('fronttrends', $e['properties']['category']);
+ $this->assertContains('speaking', $e['properties']['category']);
+ $this->assertContains('txjs', $e['properties']['category']);
+ }
+ public function testParsesSnarfedOrgArticleCorrectly()
+ {
+ $input = \file_get_contents(__DIR__ . '/snarfed.org.html');
+ $result = Mf2\parse($input, 'http://snarfed.org/2013-10-23_oauth-dropins');
+ }
+ public function testParsesHProduct()
+ {
+ $input = <<<'EOT'
+
+Delivery policy content URL Save See more details Additional Offers Includes Free Item* Instant Savings See details RebateURL FREE SHIPPING, plus 5% back for Rewards Members Click here for Printable Coupon Value Estimated to arrive no later than Qty. Choose your Items true <strong>CLEARANCE ITEM:</strong> Eco Fee View larger Next Add to Cart reviews Previous In Stock Online Price <strong>after</strong> rebate PICK UP TODAY SHARE FREE Shipping to store Environmental fee notice: Details stars Learn more. Share on Twitter Available In Store Only See Price in Cart Orders containing this item are not eligible for Gift cards or certain other methods of payment. "My Software Downloads" Buy More Save More prices do not include eco fee. Now Collapse Get Started Print this page Learn More Item /sbdpas/img/ico/ Select another component below Item qualifies entire order for free delivery in cart. selected Price <strong>after</strong> savings Ink and toner Coming Soon Before Was View Full Details Expected Delivery: Share on Pinterest Online Only Email it See Details /sbd/content/help-center/shipping-and-delivery.html Software Download /sbd/content/help/environmental_fee_popup.html Final Price Share on Facebook on this product! Supplied and Shipped by Add to Favorites Note: Shortly after purchase you will be able to access your Software Downloads in the section of your staples.com® account. It's easy and secure! /sbd/cre/marketing/technology-research-centers/software/software-downloads.html#z_faq Instant Coupon to Price <strong>before</strong> Offer valid for 20 minutes. Price Model We have partnered with this trusted supplier to offer you a wider assortment of products and brands for all of your business needs, with the same great level of service you can expect from Staples.com. Item can be shipped only to a retail store location. Rebate Buy More Save More print View video Check in Store Availability As low as Reg FREE Provincial recycling or deposit fees may be applicable upon checkout. Save an extra Special Financing Available Promotions Available In-Store Only Currently Out of Stock. Total Savings Before continuing, please select an item
+
+All-steel construction with non-skid rubber base Spring-loaded inner channel prevents jams Available in black, burgundy and beige <br /> <li>Staples up to 20 sheets</li> Select an Item All-steel construction with non-skid rubber base Spring-loaded inner channel prevents jams Available in black, burgundy and beige <br /> <li>Staples up to 20 sheets</li> Swingline® 747® Classic Desktop Staplers 18.35 USD http://www.staples-3p.com/s7/is/image/Staples/s0021414_sc7?$std$ http://www.staples-3p.com/s7/is/image/Staples/s0021414_sc7?$thb$ http://www.staples-3p.com/s7/is/image/Staples/s0021414 /Swingline-747-Classic-Desktop-Staplers/product_SS264184
+
+All-steel construction with non-skid rubber base Full strip Staples up to 20 sheets Each All-steel construction with non-skid rubber base Full strip Staples up to 20 sheets Swingline® 747® Classic Desktop Full Strip Stapler, 20 Sheet Capacity, Black 18.35 USD http://www.staples-3p.com/s7/is/image/Staples/s0021412_sc7?$std$ http://www.staples-3p.com/s7/is/image/Staples/s0021412_sc7?$thb$ http://www.staples-3p.com/s7/is/image/Staples/s0021412 /Swingline-747-Classic-Desktop-Full-Strip-Stapler-20-Sheet-Capacity-Black/product_264184
+
+All-steel construction with non-skid rubber base Spring-loaded inner channel prevents jams Burgundy <br /> <li>Staples up to 20 sheets</li> Each All-steel construction with non-skid rubber base Spring-loaded inner channel prevents jams Burgundy <br /> <li>Staples up to 20 sheets</li> Swingline® 747® Classic Desktop Stapler, Burgundy 19.59 USD http://www.staples-3p.com/s7/is/image/Staples/m000240695_sc7?$std$ http://www.staples-3p.com/s7/is/image/Staples/m000240695_sc7?$thb$ http://www.staples-3p.com/s7/is/image/Staples/m000240695 /Swingline-747-Classic-Desktop-Stapler-Burgundy/product_413732
+
+20 sheet capacity with Swingline S.F.® 4® Staples Durable metal construction Stapler opens for tacking ability Each 20 sheet capacity with Swingline S.F.® 4® Staples Durable metal construction Stapler opens for tacking ability Swingline® 747® Rio Red Stapler, 20 Sheet Capacity 39.49 USD http://www.staples-3p.com/s7/is/image/Staples/s0446269_sc7?$std$ http://www.staples-3p.com/s7/is/image/Staples/s0446269_sc7?$thb$ http://www.staples-3p.com/s7/is/image/Staples/s0446269 /Swingline-747-Rio-Red-Stapler-20-Sheet-Capacity/product_562485
+
+
+EOT;
+ $result = Mf2\parse($input, 'http://www.staples.com/Swingline-747-Rio-Red-Stapler-20-Sheet-Capacity/product_562485');
+ $this->assertCount(4, $result['items']);
+ }
+ /**
+ * @see https://github.com/indieweb/php-mf2/issues/81
+ */
+ public function test_vevent()
+ {
+ $input = <<
+XYZ Project Review
+Project XYZ Review Meeting
+ http://example.com/xyz-meeting
+To be held on
+
+ the 12th of March
+ from 8:30am EST
+ until
+
+ 9:30am EST
+
+
+Location: 1CP Conference Room 4350
+Booked by: guid-1.host1.com on
+
+ the 9th at 6:00pm
+
+
+
+EOT;
+ $parser = new Parser($input);
+ $output = $parser->parse();
+ $this->assertArrayHasKey('name', $output['items'][0]['properties']);
+ $this->assertArrayHasKey('description', $output['items'][0]['properties']);
+ $this->assertArrayHasKey('url', $output['items'][0]['properties']);
+ $this->assertArrayHasKey('start', $output['items'][0]['properties']);
+ $this->assertArrayHasKey('end', $output['items'][0]['properties']);
+ $this->assertEquals('XYZ Project Review', $output['items'][0]['properties']['name'][0]);
+ $this->assertEquals('Project XYZ Review Meeting', $output['items'][0]['properties']['description'][0]);
+ $this->assertEquals('http://example.com/xyz-meeting', $output['items'][0]['properties']['url'][0]);
+ $this->assertEquals('1998-03-12 08:30-0500', $output['items'][0]['properties']['start'][0]);
+ $this->assertEquals('1998-03-12 09:30-0500', $output['items'][0]['properties']['end'][0]);
+ }
+ /**
+ * @see https://github.com/indieweb/php-mf2/issues/57
+ * @see https://github.com/kartikprabhu/mf2py/pull/50/
+ */
+ public function testRelBookmarkUrl()
+ {
+ $input = <<
+
+
+ Backcompat test for hEntry with nested rel=bookmark
+
+
+
+
+
+
+
+
+ Lee Adama
+ Jumping Rope for Weight Loss
+ Some Content
+ Nov 24, 2014
+
+
+
+ Kara Thrace
+ Abstract Art in Graffiti
+ More Content
+ Nov 23, 2014
+
+
+
+ President Roslyn
+ Dreams of Earth
+ Additional Content
+ Nov 21, 2014
+
+
+
+ Chief Tyrrol
+ Organized Labor in Mining Colonies
+ More Content
+ Nov 19, 2014
+
+
+
+
+END;
+ $output = Mf2\parse($input);
+ $u_urls = array('/2014/11/24/jump-rope', '/2014/11/23/graffiti', '/2014/11/21/earth', '/2014/11/19/labor');
+ foreach ($u_urls as $key => $url) {
+ $this->assertEquals('h-entry', $output['items'][$key]['type'][0]);
+ $this->assertArrayHasKey('url', $output['items'][$key]['properties']);
+ $this->assertEquals($url, $output['items'][$key]['properties']['url'][0]);
+ }
+ }
+ /**
+ * @see http://microformats.org/wiki/microformats2-parsing-issues#any_h-_root_class_name_overrides_and_stops_backcompat_root
+ */
+ public function testMf2RootStopsBackcompatRoot()
+ {
+ $input = '
+
MF1 adr locality
+
MF2 adr locality
+
';
+ $parser = new Parser($input);
+ $result = $parser->parse();
+ $this->assertCount(1, $result['items'][0]['type']);
+ $this->assertEquals('h-adr', $result['items'][0]['type'][0]);
+ $this->assertCount(1, $result['items'][0]['properties']['locality']);
+ $this->assertEquals('MF2 adr locality', $result['items'][0]['properties']['locality'][0]);
+ }
+ /**
+ * @see http://microformats.org/wiki/microformats2-parsing-issues#any_h-_root_class_name_overrides_and_stops_backcompat_root
+ */
+ public function testMf2CustomRootStopsBackcompatRoot()
+ {
+ $input = '
+
MF1 acme locality
+
MF2 acme locality
+
';
+ $parser = new Parser($input);
+ $result = $parser->parse();
+ $this->assertCount(1, $result['items'][0]['type']);
+ $this->assertEquals('h-acme-address', $result['items'][0]['type'][0]);
+ $this->assertCount(1, $result['items'][0]['properties']['locality']);
+ $this->assertEquals('MF2 acme locality', $result['items'][0]['properties']['locality'][0]);
+ }
+ /**
+ * @see http://microformats.org/wiki/microformats2-parsing-issues#uf2_children_on_backcompat_properties
+ */
+ public function testMf2ChildrenOnBackcompatProperties()
+ {
+ $input = '
+
+
MF1 some acme locality
+
MF2 some acme locality
+
+
';
+ $parser = new Parser($input);
+ $result = $parser->parse();
+ $this->assertCount(1, $result['items'][0]['properties']['adr'][0]['type']);
+ $this->assertEquals('h-acme-some-acme-object', $result['items'][0]['properties']['adr'][0]['type'][0]);
+ $this->assertCount(1, $result['items'][0]['properties']['adr'][0]['properties']['locality']);
+ $this->assertEquals('MF2 some acme locality', $result['items'][0]['properties']['adr'][0]['properties']['locality'][0]);
+ }
+ /**
+ * Test mixed microformats2 with mf1 roots + properties
+ * Technically covered by other tests, but this is an additional test with @pfefferle's content
+ * after improvements were made to the backcompat parsing.
+ * @see https://github.com/indieweb/php-mf2/issues/45#issue-33893491
+ */
+ public function testMixedMf2andMf1Case1()
+ {
+ $input = <<
+
+
+
+
+
+
Facebook kauft WhatsApp und ich hab nur wenig Möglichkeiten meine Konsequenzen daraus zu ziehen. Leider sind alle aktuell populären “Chat” Systeme direkt an die App gekoppelt und ich “muss” zwangsläufig die App benutzen die mein Freundeskreis bevorzugt.
+
WhatsApp benutzt intern das XMPP-Protokoll und arbeitet dadurch ja theoretisch dezentral und auch Telegram hat beispielsweise eine Art offenes Protokoll gebaut… Das Problem: Woher wissen auf welchem Server der Andere angemeldet ist.
+
Seit WhatsApp die Identifizierung über die Telefonnummer (statt einer z.B. E-Mail Adresse) eingeführt hat, sind viele anderen diesem Beispiel gefolgt und es gibt nichts Verwerfliches daran. Jeder der eine solche App nutzt hat zwangsläufig ein Telefon, was bedeutet dass er auch eine Telefonnummer hat und die Wahrscheinlichkeit dass in seinem (Telefon-)Adressbuch mehr Telefonnummern als E-Mail Adressen stehen ist auch sehr hoch. Prinzipiell also eine gute Idee! Leider kann man aber anhand einer Telefonnummer nicht auf einen Server (mal abgesehen vom Telekommunikations-unternehmen) schließen und das bedeutet, dass das Verfahren leider auch nur zentral funktionieren kann. Nutze ich WhatsApp, kann man mich nur über die WhatsApp-Server erreichen, für Telegram läuft die Kommunikation nur über die Telegram-Server usw.
+
Um mit XMPP oder anderen Protokollen wirklich dezentral arbeiten zu können, müsste man über die Telefonnummer erfahren können welchen Chat-Server der Andere benutzt. Vielleicht über so eine Art Tel to Id – Service oder über andere Protokolle wie z.B. SMS. Damit könnte sich jeder selbst den Client seines Vertrauens aussuchen und alles wäre gut besser
+
+
+
+
+
+ Beitragsnavigation
+
+
+
+
+
+
+
+
+END;
+ $parser = new Parser($input, 'http://notiz.blog/2014/02/20/wir-brauchen-metadaten-fuer-telefonnummern/');
+ $result = $parser->parse();
+ $this->assertCount(1, $result['items'][0]['properties']['author']);
+ $this->assertCount(1, $result['items'][0]['properties']['author'][0]['type']);
+ $this->assertEquals('h-card', $result['items'][0]['properties']['author'][0]['type'][0]);
+ $this->assertArrayNotHasKey('category', $result['items'][0]['properties']);
+ }
+ /**
+ * Test mixed microformats2 with mf1 roots + properties
+ * Technically covered by other tests, but this is an additional test with @aaronpk's content
+ * after improvements were made to the backcompat parsing.
+ * @see https://github.com/indieweb/php-mf2/issues/45#issuecomment-267621041
+ */
+ public function testMixedMf2andMf1Case2()
+ {
+ $input = <<
+
+
+
+
+
+
+
+
+
+
+
+
5 out of 5
+
+
+
+
Voice memos that record straight to your Dropbox account.
+
+
+
+
+ Portland ,
+ Oregon
+
+
+
+
+
+
+
+
+
+
+
+
+END;
+ $parser = new Parser($input, 'https://aaronparecki.com/2016/12/15/16/dropvox');
+ $result = $parser->parse();
+ $this->assertCount(1, $result['items'][0]['properties']['item']);
+ $this->assertCount(1, $result['items'][0]['properties']['item'][0]['type']);
+ $this->assertCount(1, $result['items'][0]['properties']['name']);
+ $this->assertCount(1, $result['items'][0]['properties']['url']);
+ $this->assertCount(1, $result['items'][0]['properties']['author']);
+ $this->assertArrayNotHasKey('reviewer', $result['items'][0]['properties']);
+ $this->assertArrayNotHasKey('description', $result['items'][0]['properties']);
+ # The following two are correct per backcompat algorithm: classic properties are ignored inside the mf2 root.
+ $this->assertArrayNotHasKey('rating', $result['items'][0]['properties']);
+ $this->assertArrayNotHasKey('best', $result['items'][0]['properties']);
+ $this->assertEquals('h-product', $result['items'][0]['properties']['item'][0]['type'][0]);
+ $this->assertEquals('Dropvox', $result['items'][0]['properties']['name'][0]);
+ $this->assertEquals('https://aaronparecki.com/2016/12/15/16/dropvox', $result['items'][0]['properties']['url'][0]);
+ $this->assertEquals('https://aaronparecki.com/', $result['items'][0]['properties']['author'][0]);
+ }
+ /**
+ * Test mixed microformats2 with mf1 roots + properties
+ * @see https://github.com/microformats/microformats2-parsing/issues/11#issue-246579526
+ */
+ public function testMixedMf2andMf1Case3()
+ {
+ $input = <<
+
+ Cherry Red's
+ ,
+
+ 88-92 John Bright St ,
+ Birmingham ,
+ UK
+
+END;
+ $parser = new Parser($input);
+ $result = $parser->parse();
+ $this->assertCount(2, $result['items'][0]['properties']);
+ $this->assertArrayNotHasKey('street-address', $result['items'][0]['properties']);
+ $this->assertArrayNotHasKey('locality', $result['items'][0]['properties']);
+ $this->assertArrayNotHasKey('country-name', $result['items'][0]['properties']);
+ $this->assertArrayHasKey('children', $result['items'][0]);
+ $this->assertEquals('h-adr', $result['items'][0]['children'][0]['type'][0]);
+ $this->assertArrayHasKey('street-address', $result['items'][0]['children'][0]['properties']);
+ $this->assertArrayHasKey('locality', $result['items'][0]['children'][0]['properties']);
+ $this->assertArrayHasKey('country-name', $result['items'][0]['children'][0]['properties']);
+ }
+ /**
+ * Test mixed microformats2 with mf1 roots + properties
+ * @see https://github.com/microformats/microformats2-parsing/issues/11#issuecomment-352281134
+ */
+ public function testMixedMf2andMf1Case4()
+ {
+ $input = <<
+
+
title
+ other content
+
+
this is a test for indieweb post
Also on:
+
+
+
+
ENUM (RFC6116) macht genau das. Ist zwar für SIP gedacht, passt aber auch auf diese Anforderung.
++-
+
+
+
+
+
+
+
+
+
+
+Das war ne schnelle Antwort
+Vielen Dank für den Tipp mit ENUM (noch nie davon gehört) und den Link… werde mich später mal durch das RFC kämpfen…
+Wollte auch gerade ENUM sagen. Dabei wird die Telefonnummer in einen DNS-Namen konvertiert. Wenn du damit spielen willst, kannst du dir unter http://www.portunity.de/access/produkte/telefonie/enum-domains.html kostenlos eine deutsche Nummer in ENUM eintragen lassen.
++-
+
+
+
+
+
+
+
+
+
+
+Krass dass das so vollkommen an mit vorbei gegangen ist… Gibt es da produktive Anwendungen die ENUM zum Beispiel für Chats o.Ä. verwenden?
+…ich sollte echt mehr bloggen!
++-
+
+
+
+
+
+
+
+
+
+
+ENUM wurde bisher nur als Möglichkeit zur Umgehung der Carrier/Kostenersparnis gesehen, dementsprechend natürlich von Carriern und nahestehenden Hard-/Softwareherstellern nicht unterstützt. Somit kommt es nicht in den Mainstream. Ich sehe es zur Zeit (leider) als reines “Nerd-Tool”, genau wie Diaspora, OpenID, IndieWeb …
++Aber der Gedanke eines “dezentralen WhatsApp” auf ENUM-Basis kam mir auch schon. Interessantes Projekt, aber auch nicht massentauglich wegen Huhn&Ei-Problemen.
+-
+
+
+
+
+
+
+
+
+
+
+Hmmm… Eine Unterstützung von Seiten aller Carrier wäre natürlich wirklich notwendig um massentaugliche Produkte zu bauen…
+Wäre großartig wenn jede Nummer automatisch ne URI bekäme und unter dieser URI ne Art “Registry” zu finden wäre, die auch von Apps erweitert werden kann. So ne Art WebFinger für Telefonnummern quasi…
+Diese Interoperabilität nennt sich gemeinhin “Federation”: http://en.wikipedia.org/wiki/Federation_(information_technology)
+WhatsApp verwendet kein XMPP. XMPP ist für Mobiles der absolute Horror, denn es basiert auf TCP und damit braucht der Client eine stehende TCP-Verbindung, was massiv auf den Akku geht. Außerdem kommt es permanent zu reconnects, wenn sich laufend die IP-Adresse des Clients ändert.
++Aus diesem Grund will man ein verbindungsloses Push-System dahinter haben.
Google und Facebook verwenden XMPP, Facebook hat sich aber noch nie an s2s (Server to Server) Verbindungen beteiligt, Google hat es vor ca 1 Jahr abgeschaltet, damit kann man sich zB. von eigenen XMPP-Servern und damit eigenen XMPP-Accounts nicht mehr mit Google-Usern unterhalten, sonern muss den Google Account verwenden.
++Ich habe zB. sowohl meine Facebook als auch Google-Account in meinem pidgin konfiguriert.
TextSecure (clients momentan nur für Android) ist momentan das IMHO beste System in diesem Bereich:
++- open source
+- harte crypto
+- multi device (man kann einen Account auf meheren Devices nutzen)
+- bald für iOS und Desktop
+und: es unterstützt Federation, man kann sich also seinen eigenen Server hinstellen und es darüber machen.
+Siehe: https://whispersystems.org/blog/the-new-textsecure/
Ich muss natürlich immer noch den Account des anderen Teilnehmers kennen …
+