April was pretty decent. I got to attend two very good conferences and I got to speak at them.
+\t\t\tdiff --git a/CHANGELOG.md b/CHANGELOG.md index 03be70c..c70a40e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,17 @@ All notable changes to this project will be documented in this file. 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] +## [0.2.2] - 2024-07-12 +### 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 +- Support for clients with h-x-app [#3](https://github.com/gRegorLove/ProcessWire-IndieAuth/issues/3) + +### Changed +- Updated client information discovery [#5](https://github.com/gRegorLove/ProcessWire-IndieAuth/issues/5) +- Noted refresh token expiration in the list of approved applications +- Scoped dependencies to avoid namespace collisions when other plugins have same dependencies ## [0.2.1] - 2022-08-06 ### Added diff --git a/ProcessIndieAuth.module.php b/ProcessIndieAuth.module.php index 09cbaeb..e9811c3 100644 --- a/ProcessIndieAuth.module.php +++ b/ProcessIndieAuth.module.php @@ -17,10 +17,14 @@ 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 +}; class ProcessIndieAuth extends Process implements Module, ConfigurableModule { @@ -31,7 +35,7 @@ public static function getModuleInfo(): array { return [ 'title' => 'IndieAuth', - 'version' => '021', + 'version' => '022', 'author' => 'gRegor Morrill, https://gregorlove.com/', 'summary' => 'Use your domain name as an IndieAuth provider', 'href' => 'https://indieauth.com/', @@ -85,6 +89,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 +170,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 +211,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 +247,7 @@ public function ___execute(): array ,issued_at ,last_accessed ,expiration + ,refresh_expiration FROM indieauth_tokens LIMIT @@ -272,12 +287,22 @@ public function ___execute(): array $date_expiration = '—'; if ($row['expiration']) { - $dt = new DateTime($row['expiration']); + $dt_expires = new DateTime($row['expiration']); $date_expiration = sprintf('', - $dt->format('c'), - $dt->format('F j, Y g:ia'), - $dt->format('F j, Y') + $dt_expires->format('c'), + $dt_expires->format('F j, Y g:ia'), + $dt_expires->format('F j, Y') ); + + if ($row['refresh_expiration']) { + $dt = new DateTime($row['refresh_expiration']); + $date_expiration = sprintf('R: ', + $dt->format('c'), + $dt_expires->format('F j, Y g:ia'), + $dt->format('F j, Y g:ia'), + $dt->format('F j, Y') + ); + } } $dt = new DateTime($row['issued_at']); @@ -441,6 +466,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('
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('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
*/
@@ -555,9 +926,9 @@ public function authorizationEndpoint(): void
$request['client_id'] = Server::canonizeUrl($request['client_id']);
$request['redirect_uri'] = Server::canonizeUrl($request['redirect_uri']);
- $client = $this->getClientInfo($request['client_id']);
+ $client = Server::getClientInfo($request['client_id']);
- if (!Server::isRedirectUriAllowed($request['redirect_uri'], $request['client_id'], $client['redirect_uri'])) {
+ if (!Server::isRedirectUriAllowed($request['redirect_uri'], $request['client_id'], $client['redirect_uris'])) {
$this->httpResponse('mismatched redirect_uri');
}
@@ -657,6 +1028,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 +1583,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 +1801,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 +2002,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 +2022,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)) {
@@ -1464,6 +2038,11 @@ private function httpResponse($response, int $http_status = 400, array $headers
exit;
}
+ /**
+ * DEPRECATED
+ *
+ * @see Client::getInfo()
+ */
private function getClientInfo(string $url): array
{
$info = array_fill_keys([
@@ -1493,6 +2072,10 @@ private function getClientInfo(string $url): array
$apps = Mf2Helper\findMicroformatsByType($mf, 'h-app');
+ if (!$apps) {
+ $apps = Mf2Helper\findMicroformatsByType($mf, 'h-x-app');
+ }
+
if (!$apps) {
return $info;
}
diff --git a/README.md b/README.md
index fbc6e9f..d772a21 100644
--- a/README.md
+++ b/README.md
@@ -50,6 +50,30 @@ 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 `env COMPOSER=scoped-composer.json composer install`
+3. Check that `scoped-libs` folder is created and not empty
+4. Run `env COMPOSER=scoped-composer.json composer install --no-dev` to remove dev dependencies
+5. Run `env COMPOSER=composer.json composer dump-autoload`
+6. 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 dump-autoload`
+
+This gets you back to step step 5 above.
+
## Changelog
* [Changelog](CHANGELOG.md)
diff --git a/composer.json b/composer.json
index 4382b51..8129a30 100644
--- a/composer.json
+++ b/composer.json
@@ -1,15 +1,17 @@
{
- "require": {
- "firebase/php-jwt": "^5.0",
- "mf2/mf2": "^0.4.6",
- "barnabywalters/mf-cleaner": "^0.1.4"
- },
- "require-dev": {
- "phpunit/phpunit": "^8.4"
- },
"autoload": {
"psr-4": {
"IndieAuth\\": "src/IndieAuth/"
- }
+ },
+ "classmap": [
+ "scoped-libs/"
+ ],
+ "files": [
+ "scoped-libs/mf2/mf2/Mf2/Parser.php",
+ "scoped-libs/barnabywalters/mf-cleaner/src/functions.php"
+ ]
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5"
}
}
diff --git a/composer.lock b/composer.lock
index 84d4591..c9691bb 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,196 +4,35 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "eb8ecc4f7646f5206acf363f791f3868",
- "packages": [
- {
- "name": "barnabywalters/mf-cleaner",
- "version": "v0.1.4",
- "source": {
- "type": "git",
- "url": "https://github.com/barnabywalters/php-mf-cleaner.git",
- "reference": "ef6a16628db6e8aee2b4f8bb8093d18c24b74cd4"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/barnabywalters/php-mf-cleaner/zipball/ef6a16628db6e8aee2b4f8bb8093d18c24b74cd4",
- "reference": "ef6a16628db6e8aee2b4f8bb8093d18c24b74cd4",
- "shasum": ""
- },
- "require-dev": {
- "php": ">=5.3",
- "phpunit/phpunit": "*"
- },
- "suggest": {
- "mf2/mf2": "To parse microformats2 structures from (X)HTML"
- },
- "type": "library",
- "autoload": {
- "files": [
- "src/BarnabyWalters/Mf2/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.1.4"
- },
- "time": "2014-10-06T23:11:15+00:00"
- },
- {
- "name": "firebase/php-jwt",
- "version": "v5.4.0",
- "source": {
- "type": "git",
- "url": "https://github.com/firebase/php-jwt.git",
- "reference": "d2113d9b2e0e349796e72d2a63cf9319100382d2"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d2113d9b2e0e349796e72d2a63cf9319100382d2",
- "reference": "d2113d9b2e0e349796e72d2a63cf9319100382d2",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.0"
- },
- "require-dev": {
- "phpunit/phpunit": ">=4.8 <=9"
- },
- "suggest": {
- "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/v5.4.0"
- },
- "time": "2021-06-23T19:00:23+00:00"
- },
- {
- "name": "mf2/mf2",
- "version": "0.4.6",
- "source": {
- "type": "git",
- "url": "https://github.com/microformats/php-mf2.git",
- "reference": "00b70ee7eb7f5b0585b1bd467f6c9cbd75055d23"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/microformats/php-mf2/zipball/00b70ee7eb7f5b0585b1bd467f6c9cbd75055d23",
- "reference": "00b70ee7eb7f5b0585b1bd467f6c9cbd75055d23",
- "shasum": ""
- },
- "require": {
- "php": ">=5.4.0"
- },
- "require-dev": {
- "mf2/tests": "@dev",
- "phpdocumentor/phpdocumentor": "v2.8.4",
- "phpunit/phpunit": "4.8.*"
- },
- "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/master"
- },
- "time": "2018-08-24T14:47:04+00:00"
- }
- ],
+ "content-hash": "d04a6a4216eec9684d7a430e94632a78",
+ "packages": [],
"packages-dev": [
{
"name": "doctrine/instantiator",
- "version": "1.4.0",
+ "version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/instantiator.git",
- "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b"
+ "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b",
- "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0",
+ "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0",
"shasum": ""
},
"require": {
- "php": "^7.1 || ^8.0"
+ "php": "^8.1"
},
"require-dev": {
- "doctrine/coding-standard": "^8.0",
+ "doctrine/coding-standard": "^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": "^1.2",
+ "phpstan/phpstan": "^1.9.4",
+ "phpstan/phpstan-phpunit": "^1.3",
+ "phpunit/phpunit": "^9.5.27",
+ "vimeo/psalm": "^5.4"
},
"type": "library",
"autoload": {
@@ -220,7 +59,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/2.0.0"
},
"funding": [
{
@@ -236,41 +75,43 @@
"type": "tidelift"
}
],
- "time": "2020-11-10T18:47:58+00:00"
+ "time": "2022-12-30T00:23:10+00:00"
},
{
"name": "myclabs/deep-copy",
- "version": "1.10.2",
+ "version": "1.12.0",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
- "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220"
+ "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220",
- "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c",
+ "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c",
"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",
+ "phpspec/prophecy": "^1.10",
+ "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": [
@@ -286,7 +127,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.12.0"
},
"funding": [
{
@@ -294,24 +135,83 @@
"type": "tidelift"
}
],
- "time": "2020-11-13T09:40:50+00:00"
+ "time": "2024-06-12T14:39:25+00:00"
+ },
+ {
+ "name": "nikic/php-parser",
+ "version": "v5.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/PHP-Parser.git",
+ "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/683130c2ff8c2739f4822ff7ac5c873ec529abd1",
+ "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "ext-json": "*",
+ "ext-tokenizer": "*",
+ "php": ">=7.4"
+ },
+ "require-dev": {
+ "ircmaxell/php-yacc": "^0.0.7",
+ "phpunit/phpunit": "^9.0"
+ },
+ "bin": [
+ "bin/php-parse"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-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/v5.1.0"
+ },
+ "time": "2024-07-01T20:03:41+00:00"
},
{
"name": "phar-io/manifest",
- "version": "2.0.1",
+ "version": "2.0.4",
"source": {
"type": "git",
"url": "https://github.com/phar-io/manifest.git",
- "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133"
+ "reference": "54750ef60c58e43759730615a392c31c80e23176"
},
"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/54750ef60c58e43759730615a392c31c80e23176",
+ "reference": "54750ef60c58e43759730615a392c31c80e23176",
"shasum": ""
},
"require": {
"ext-dom": "*",
+ "ext-libxml": "*",
"ext-phar": "*",
"ext-xmlwriter": "*",
"phar-io/version": "^3.0.1",
@@ -352,22 +252,28 @@
"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.4"
},
- "time": "2020-06-27T14:33:11+00:00"
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-03T12:33:53+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": {
@@ -403,271 +309,176 @@
"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": "9.2.31",
"source": {
"type": "git",
- "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
- "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
+ "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+ "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965"
},
"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/48c34b5d8d983006bd2adc2d0de92963b9155965",
+ "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965",
"shasum": ""
},
"require": {
- "php": "^7.2 || ^8.0"
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-xmlwriter": "*",
+ "nikic/php-parser": "^4.18 || ^5.0",
+ "php": ">=7.3",
+ "phpunit/php-file-iterator": "^3.0.3",
+ "phpunit/php-text-template": "^2.0.2",
+ "sebastian/code-unit-reverse-lookup": "^2.0.2",
+ "sebastian/complexity": "^2.0",
+ "sebastian/environment": "^5.1.2",
+ "sebastian/lines-of-code": "^1.0.3",
+ "sebastian/version": "^3.0.1",
+ "theseer/tokenizer": "^1.2.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "ext-pcov": "PHP extension that provides line coverage",
+ "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-2.x": "2.x-dev"
+ "dev-master": "9.2-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"
- },
- "time": "2020-06-27T09:03:43+00:00"
- },
- {
- "name": "phpdocumentor/reflection-docblock",
- "version": "5.2.2",
- "source": {
- "type": "git",
- "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
- "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556",
- "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556",
- "shasum": ""
- },
- "require": {
- "ext-filter": "*",
- "php": "^7.2 || ^8.0",
- "phpdocumentor/reflection-common": "^2.2",
- "phpdocumentor/type-resolver": "^1.3",
- "webmozart/assert": "^1.9.1"
- },
- "require-dev": {
- "mockery/mockery": "~1.3.2"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "5.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "phpDocumentor\\Reflection\\": "src"
- }
+ "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
+ "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31"
},
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Mike van Riel",
- "email": "me@mikevanriel.com"
- },
+ "funding": [
{
- "name": "Jaap van Otterdijk",
- "email": "account@ijaap.nl"
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
}
],
- "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
- "support": {
- "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
- "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/master"
- },
- "time": "2020-09-03T19:13:55+00:00"
+ "time": "2024-03-02T06:37:42+00:00"
},
{
- "name": "phpdocumentor/type-resolver",
- "version": "1.4.0",
+ "name": "phpunit/php-file-iterator",
+ "version": "3.0.6",
"source": {
"type": "git",
- "url": "https://github.com/phpDocumentor/TypeResolver.git",
- "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0"
+ "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+ "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0",
- "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
+ "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf",
"shasum": ""
},
"require": {
- "php": "^7.2 || ^8.0",
- "phpdocumentor/reflection-common": "^2.0"
+ "php": ">=7.3"
},
"require-dev": {
- "ext-tokenizer": "*"
+ "phpunit/phpunit": "^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-1.x": "1.x-dev"
+ "dev-master": "3.0-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": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
}
],
- "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
+ "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/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"
- }
+ "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6"
},
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Konstantin Kudryashov",
- "email": "ever.zet@gmail.com",
- "homepage": "http://everzet.com"
- },
+ "funding": [
{
- "name": "Marcello Duarte",
- "email": "marcello.duarte@gmail.com"
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
}
],
- "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"
+ "time": "2021-12-02T12:48:52+00:00"
},
{
- "name": "phpunit/php-code-coverage",
- "version": "7.0.14",
+ "name": "phpunit/php-invoker",
+ "version": "3.1.1",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "bb7c9a210c72e4709cdde67f8b7362f672f2225c"
+ "url": "https://github.com/sebastianbergmann/php-invoker.git",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/bb7c9a210c72e4709cdde67f8b7362f672f2225c",
- "reference": "bb7c9a210c72e4709cdde67f8b7362f672f2225c",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
+ "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67",
"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"
+ "php": ">=7.3"
},
"require-dev": {
- "phpunit/phpunit": "^8.2.2"
+ "ext-pcntl": "*",
+ "phpunit/phpunit": "^9.3"
},
"suggest": {
- "ext-xdebug": "^2.7.2"
+ "ext-pcntl": "*"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "7.0-dev"
+ "dev-master": "3.1-dev"
}
},
"autoload": {
@@ -686,16 +497,14 @@
"role": "lead"
}
],
- "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
- "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+ "description": "Invoke callables with a timeout",
+ "homepage": "https://github.com/sebastianbergmann/php-invoker/",
"keywords": [
- "coverage",
- "testing",
- "xunit"
+ "process"
],
"support": {
- "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
- "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/7.0.14"
+ "issues": "https://github.com/sebastianbergmann/php-invoker/issues",
+ "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1"
},
"funding": [
{
@@ -703,32 +512,32 @@
"type": "github"
}
],
- "time": "2020-12-02T13:39:03+00:00"
+ "time": "2020-09-28T05:58:55+00:00"
},
{
- "name": "phpunit/php-file-iterator",
- "version": "2.0.3",
+ "name": "phpunit/php-text-template",
+ "version": "2.0.4",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
- "reference": "4b49fb70f067272b659ef0174ff9ca40fdaa6357"
+ "url": "https://github.com/sebastianbergmann/php-text-template.git",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/4b49fb70f067272b659ef0174ff9ca40fdaa6357",
- "reference": "4b49fb70f067272b659ef0174ff9ca40fdaa6357",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
+ "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=7.3"
},
"require-dev": {
- "phpunit/phpunit": "^8.5"
+ "phpunit/phpunit": "^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0.x-dev"
+ "dev-master": "2.0-dev"
}
},
"autoload": {
@@ -747,15 +556,14 @@
"role": "lead"
}
],
- "description": "FilterIterator implementation that filters files based on a list of suffixes.",
- "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
+ "description": "Simple template engine.",
+ "homepage": "https://github.com/sebastianbergmann/php-text-template/",
"keywords": [
- "filesystem",
- "iterator"
+ "template"
],
"support": {
- "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
- "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.3"
+ "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
+ "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4"
},
"funding": [
{
@@ -763,26 +571,34 @@
"type": "github"
}
],
- "time": "2020-11-30T08:25:21+00:00"
+ "time": "2020-10-26T05:33:50+00:00"
},
{
- "name": "phpunit/php-text-template",
- "version": "1.2.1",
+ "name": "phpunit/php-timer",
+ "version": "5.0.3",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/php-text-template.git",
- "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686"
+ "url": "https://github.com/sebastianbergmann/php-timer.git",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
- "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
+ "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2",
"shasum": ""
},
"require": {
- "php": ">=5.3.3"
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
},
"type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.0-dev"
+ }
+ },
"autoload": {
"classmap": [
"src/"
@@ -799,44 +615,83 @@
"role": "lead"
}
],
- "description": "Simple template engine.",
- "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+ "description": "Utility class for timing",
+ "homepage": "https://github.com/sebastianbergmann/php-timer/",
"keywords": [
- "template"
+ "timer"
],
"support": {
- "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
- "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1"
+ "issues": "https://github.com/sebastianbergmann/php-timer/issues",
+ "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3"
},
- "time": "2015-06-21T13:50:34+00:00"
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-10-26T13:16:10+00:00"
},
{
- "name": "phpunit/php-timer",
- "version": "2.1.3",
+ "name": "phpunit/phpunit",
+ "version": "9.6.19",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/php-timer.git",
- "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662"
+ "url": "https://github.com/sebastianbergmann/phpunit.git",
+ "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/2454ae1765516d20c4ffe103d85a58a9a3bd5662",
- "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a1a54a473501ef4cdeaae4e06891674114d79db8",
+ "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "doctrine/instantiator": "^1.3.1 || ^2",
+ "ext-dom": "*",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-xml": "*",
+ "ext-xmlwriter": "*",
+ "myclabs/deep-copy": "^1.10.1",
+ "phar-io/manifest": "^2.0.3",
+ "phar-io/version": "^3.0.2",
+ "php": ">=7.3",
+ "phpunit/php-code-coverage": "^9.2.28",
+ "phpunit/php-file-iterator": "^3.0.5",
+ "phpunit/php-invoker": "^3.1.1",
+ "phpunit/php-text-template": "^2.0.3",
+ "phpunit/php-timer": "^5.0.2",
+ "sebastian/cli-parser": "^1.0.1",
+ "sebastian/code-unit": "^1.0.6",
+ "sebastian/comparator": "^4.0.8",
+ "sebastian/diff": "^4.0.3",
+ "sebastian/environment": "^5.1.3",
+ "sebastian/exporter": "^4.0.5",
+ "sebastian/global-state": "^5.0.1",
+ "sebastian/object-enumerator": "^4.0.3",
+ "sebastian/resource-operations": "^3.0.3",
+ "sebastian/type": "^3.2",
+ "sebastian/version": "^3.0.2"
},
- "require-dev": {
- "phpunit/phpunit": "^8.5"
+ "suggest": {
+ "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"
},
+ "bin": [
+ "phpunit"
+ ],
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.1-dev"
+ "dev-master": "9.6-dev"
}
},
"autoload": {
+ "files": [
+ "src/Framework/Assert/Functions.php"
+ ],
"classmap": [
"src/"
]
@@ -852,48 +707,58 @@
"role": "lead"
}
],
- "description": "Utility class for timing",
- "homepage": "https://github.com/sebastianbergmann/php-timer/",
+ "description": "The PHP Unit Testing framework.",
+ "homepage": "https://phpunit.de/",
"keywords": [
- "timer"
+ "phpunit",
+ "testing",
+ "xunit"
],
"support": {
- "issues": "https://github.com/sebastianbergmann/php-timer/issues",
- "source": "https://github.com/sebastianbergmann/php-timer/tree/2.1.3"
+ "issues": "https://github.com/sebastianbergmann/phpunit/issues",
+ "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.19"
},
"funding": [
+ {
+ "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": "2020-11-30T08:20:02+00:00"
+ "time": "2024-04-05T04:35:58+00:00"
},
{
- "name": "phpunit/php-token-stream",
- "version": "4.0.4",
+ "name": "sebastian/cli-parser",
+ "version": "1.0.2",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/php-token-stream.git",
- "reference": "a853a0e183b9db7eed023d7933a858fa1c8d25a3"
+ "url": "https://github.com/sebastianbergmann/cli-parser.git",
+ "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/a853a0e183b9db7eed023d7933a858fa1c8d25a3",
- "reference": "a853a0e183b9db7eed023d7933a858fa1c8d25a3",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
+ "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b",
"shasum": ""
},
"require": {
- "ext-tokenizer": "*",
- "php": "^7.3 || ^8.0"
+ "php": ">=7.3"
},
"require-dev": {
- "phpunit/phpunit": "^9.0"
+ "phpunit/phpunit": "^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "4.0-dev"
+ "dev-master": "1.0-dev"
}
},
"autoload": {
@@ -908,17 +773,15 @@
"authors": [
{
"name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de"
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
}
],
- "description": "Wrapper around PHP's tokenizer extension.",
- "homepage": "https://github.com/sebastianbergmann/php-token-stream/",
- "keywords": [
- "tokenizer"
- ],
+ "description": "Library for parsing CLI options",
+ "homepage": "https://github.com/sebastianbergmann/cli-parser",
"support": {
- "issues": "https://github.com/sebastianbergmann/php-token-stream/issues",
- "source": "https://github.com/sebastianbergmann/php-token-stream/tree/master"
+ "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
+ "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2"
},
"funding": [
{
@@ -926,65 +789,32 @@
"type": "github"
}
],
- "abandoned": true,
- "time": "2020-08-04T08:28:15+00:00"
+ "time": "2024-03-02T06:27:43+00:00"
},
{
- "name": "phpunit/phpunit",
- "version": "8.5.17",
+ "name": "sebastian/code-unit",
+ "version": "1.0.8",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "79067856d85421c56d413bd238d4e2cd6b0e54da"
+ "url": "https://github.com/sebastianbergmann/code-unit.git",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/79067856d85421c56d413bd238d4e2cd6b0e54da",
- "reference": "79067856d85421c56d413bd238d4e2cd6b0e54da",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120",
+ "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120",
"shasum": ""
},
"require": {
- "doctrine/instantiator": "^1.3.1",
- "ext-dom": "*",
- "ext-json": "*",
- "ext-libxml": "*",
- "ext-mbstring": "*",
- "ext-xml": "*",
- "ext-xmlwriter": "*",
- "myclabs/deep-copy": "^1.10.0",
- "phar-io/manifest": "^2.0.1",
- "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-text-template": "^1.2.1",
- "phpunit/php-timer": "^2.1.2",
- "sebastian/comparator": "^3.0.2",
- "sebastian/diff": "^3.0.2",
- "sebastian/environment": "^4.2.3",
- "sebastian/exporter": "^3.1.2",
- "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"
+ "php": ">=7.3"
},
"require-dev": {
- "ext-pdo": "*"
- },
- "suggest": {
- "ext-soap": "*",
- "ext-xdebug": "*",
- "phpunit/php-invoker": "^2.0.0"
+ "phpunit/phpunit": "^9.3"
},
- "bin": [
- "phpunit"
- ],
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "8.5-dev"
+ "dev-master": "1.0-dev"
}
},
"autoload": {
@@ -1003,53 +833,44 @@
"role": "lead"
}
],
- "description": "The PHP Unit Testing framework.",
- "homepage": "https://phpunit.de/",
- "keywords": [
- "phpunit",
- "testing",
- "xunit"
- ],
+ "description": "Collection of value objects that represent the PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/code-unit",
"support": {
- "issues": "https://github.com/sebastianbergmann/phpunit/issues",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.17"
+ "issues": "https://github.com/sebastianbergmann/code-unit/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8"
},
"funding": [
- {
- "url": "https://phpunit.de/donate.html",
- "type": "custom"
- },
{
"url": "https://github.com/sebastianbergmann",
"type": "github"
}
],
- "time": "2021-06-23T05:12:43+00:00"
+ "time": "2020-10-26T13:08:54+00:00"
},
{
"name": "sebastian/code-unit-reverse-lookup",
- "version": "1.0.2",
+ "version": "2.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
- "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619"
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/1de8cd5c010cb153fcd68b8d0f64606f523f7619",
- "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
+ "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5",
"shasum": ""
},
"require": {
- "php": ">=5.6"
+ "php": ">=7.3"
},
"require-dev": {
- "phpunit/phpunit": "^8.5"
+ "phpunit/phpunit": "^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0.x-dev"
+ "dev-master": "2.0-dev"
}
},
"autoload": {
@@ -1071,7 +892,7 @@
"homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
"support": {
"issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
- "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.2"
+ "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3"
},
"funding": [
{
@@ -1079,34 +900,34 @@
"type": "github"
}
],
- "time": "2020-11-30T08:15:22+00:00"
+ "time": "2020-09-28T05:30:19+00:00"
},
{
"name": "sebastian/comparator",
- "version": "3.0.3",
+ "version": "4.0.8",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
- "reference": "1071dfcef776a57013124ff35e1fc41ccd294758"
+ "reference": "fa0f136dd2334583309d32b62544682ee972b51a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1071dfcef776a57013124ff35e1fc41ccd294758",
- "reference": "1071dfcef776a57013124ff35e1fc41ccd294758",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a",
+ "reference": "fa0f136dd2334583309d32b62544682ee972b51a",
"shasum": ""
},
"require": {
- "php": ">=7.1",
- "sebastian/diff": "^3.0",
- "sebastian/exporter": "^3.1"
+ "php": ">=7.3",
+ "sebastian/diff": "^4.0",
+ "sebastian/exporter": "^4.0"
},
"require-dev": {
- "phpunit/phpunit": "^8.5"
+ "phpunit/phpunit": "^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.0-dev"
+ "dev-master": "4.0-dev"
}
},
"autoload": {
@@ -1145,7 +966,64 @@
],
"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/4.0.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2022-09-14T12:41:17+00:00"
+ },
+ {
+ "name": "sebastian/complexity",
+ "version": "2.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/complexity.git",
+ "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a",
+ "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.18 || ^5.0",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.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 for calculating the complexity of PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/complexity",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/complexity/issues",
+ "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3"
},
"funding": [
{
@@ -1153,33 +1031,33 @@
"type": "github"
}
],
- "time": "2020-11-30T08:04:30+00:00"
+ "time": "2023-12-22T06:19:30+00:00"
},
{
"name": "sebastian/diff",
- "version": "3.0.3",
+ "version": "4.0.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
- "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211"
+ "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/14f72dd46eaf2f2293cbe79c93cc0bc43161a211",
- "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc",
+ "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=7.3"
},
"require-dev": {
- "phpunit/phpunit": "^7.5 || ^8.0",
- "symfony/process": "^2 || ^3.3 || ^4"
+ "phpunit/phpunit": "^9.3",
+ "symfony/process": "^4.2 || ^5"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.0-dev"
+ "dev-master": "4.0-dev"
}
},
"autoload": {
@@ -1211,7 +1089,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/4.0.6"
},
"funding": [
{
@@ -1219,27 +1097,27 @@
"type": "github"
}
],
- "time": "2020-11-30T07:59:04+00:00"
+ "time": "2024-03-02T06:30:58+00:00"
},
{
"name": "sebastian/environment",
- "version": "4.2.4",
+ "version": "5.1.5",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
- "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0"
+ "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d47bbbad83711771f167c72d4e3f25f7fcc1f8b0",
- "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
+ "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=7.3"
},
"require-dev": {
- "phpunit/phpunit": "^7.5"
+ "phpunit/phpunit": "^9.3"
},
"suggest": {
"ext-posix": "*"
@@ -1247,7 +1125,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "4.2-dev"
+ "dev-master": "5.1-dev"
}
},
"autoload": {
@@ -1274,7 +1152,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/environment/issues",
- "source": "https://github.com/sebastianbergmann/environment/tree/4.2.4"
+ "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5"
},
"funding": [
{
@@ -1282,34 +1160,34 @@
"type": "github"
}
],
- "time": "2020-11-30T07:53:42+00:00"
+ "time": "2023-02-03T06:03:51+00:00"
},
{
"name": "sebastian/exporter",
- "version": "3.1.3",
+ "version": "4.0.6",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
- "reference": "6b853149eab67d4da22291d36f5b0631c0fd856e"
+ "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/6b853149eab67d4da22291d36f5b0631c0fd856e",
- "reference": "6b853149eab67d4da22291d36f5b0631c0fd856e",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72",
+ "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72",
"shasum": ""
},
"require": {
- "php": ">=7.0",
- "sebastian/recursion-context": "^3.0"
+ "php": ">=7.3",
+ "sebastian/recursion-context": "^4.0"
},
"require-dev": {
"ext-mbstring": "*",
- "phpunit/phpunit": "^6.0"
+ "phpunit/phpunit": "^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.1.x-dev"
+ "dev-master": "4.0-dev"
}
},
"autoload": {
@@ -1344,14 +1222,14 @@
}
],
"description": "Provides the functionality to export PHP variables for visualization",
- "homepage": "http://www.github.com/sebastianbergmann/exporter",
+ "homepage": "https://www.github.com/sebastianbergmann/exporter",
"keywords": [
"export",
"exporter"
],
"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/4.0.6"
},
"funding": [
{
@@ -1359,30 +1237,30 @@
"type": "github"
}
],
- "time": "2020-11-30T07:47:53+00:00"
+ "time": "2024-03-02T06:33:00+00:00"
},
{
"name": "sebastian/global-state",
- "version": "3.0.1",
+ "version": "5.0.7",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/global-state.git",
- "reference": "474fb9edb7ab891665d3bfc6317f42a0a150454b"
+ "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9"
},
"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/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9",
+ "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9",
"shasum": ""
},
"require": {
- "php": ">=7.2",
- "sebastian/object-reflector": "^1.1.1",
- "sebastian/recursion-context": "^3.0"
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
},
"require-dev": {
"ext-dom": "*",
- "phpunit/phpunit": "^8.0"
+ "phpunit/phpunit": "^9.3"
},
"suggest": {
"ext-uopz": "*"
@@ -1390,7 +1268,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.0-dev"
+ "dev-master": "5.0-dev"
}
},
"autoload": {
@@ -1415,7 +1293,64 @@
],
"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/5.0.7"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-02T06:35:11+00:00"
+ },
+ {
+ "name": "sebastian/lines-of-code",
+ "version": "1.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/lines-of-code.git",
+ "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5",
+ "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.18 || ^5.0",
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.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 for counting the lines of code in PHP source code",
+ "homepage": "https://github.com/sebastianbergmann/lines-of-code",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
+ "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4"
},
"funding": [
{
@@ -1423,34 +1358,34 @@
"type": "github"
}
],
- "time": "2020-11-30T07:43:24+00:00"
+ "time": "2023-12-22T06:20:34+00:00"
},
{
"name": "sebastian/object-enumerator",
- "version": "3.0.4",
+ "version": "4.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/object-enumerator.git",
- "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2"
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2",
- "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71",
+ "reference": "5c9eeac41b290a3712d88851518825ad78f45c71",
"shasum": ""
},
"require": {
- "php": ">=7.0",
- "sebastian/object-reflector": "^1.1.1",
- "sebastian/recursion-context": "^3.0"
+ "php": ">=7.3",
+ "sebastian/object-reflector": "^2.0",
+ "sebastian/recursion-context": "^4.0"
},
"require-dev": {
- "phpunit/phpunit": "^6.0"
+ "phpunit/phpunit": "^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.0.x-dev"
+ "dev-master": "4.0-dev"
}
},
"autoload": {
@@ -1472,7 +1407,7 @@
"homepage": "https://github.com/sebastianbergmann/object-enumerator/",
"support": {
"issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
- "source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.4"
+ "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4"
},
"funding": [
{
@@ -1480,32 +1415,32 @@
"type": "github"
}
],
- "time": "2020-11-30T07:40:27+00:00"
+ "time": "2020-10-26T13:12:34+00:00"
},
{
"name": "sebastian/object-reflector",
- "version": "1.1.2",
+ "version": "2.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/object-reflector.git",
- "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d"
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/9b8772b9cbd456ab45d4a598d2dd1a1bced6363d",
- "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
+ "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7",
"shasum": ""
},
"require": {
- "php": ">=7.0"
+ "php": ">=7.3"
},
"require-dev": {
- "phpunit/phpunit": "^6.0"
+ "phpunit/phpunit": "^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.1-dev"
+ "dev-master": "2.0-dev"
}
},
"autoload": {
@@ -1527,7 +1462,7 @@
"homepage": "https://github.com/sebastianbergmann/object-reflector/",
"support": {
"issues": "https://github.com/sebastianbergmann/object-reflector/issues",
- "source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.2"
+ "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4"
},
"funding": [
{
@@ -1535,32 +1470,32 @@
"type": "github"
}
],
- "time": "2020-11-30T07:37:18+00:00"
+ "time": "2020-10-26T13:14:26+00:00"
},
{
"name": "sebastian/recursion-context",
- "version": "3.0.1",
+ "version": "4.0.5",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/recursion-context.git",
- "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb"
+ "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/367dcba38d6e1977be014dc4b22f47a484dac7fb",
- "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
+ "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1",
"shasum": ""
},
"require": {
- "php": ">=7.0"
+ "php": ">=7.3"
},
"require-dev": {
- "phpunit/phpunit": "^6.0"
+ "phpunit/phpunit": "^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.0.x-dev"
+ "dev-master": "4.0-dev"
}
},
"autoload": {
@@ -1587,10 +1522,10 @@
}
],
"description": "Provides functionality to recursively process PHP variables",
- "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
+ "homepage": "https://github.com/sebastianbergmann/recursion-context",
"support": {
"issues": "https://github.com/sebastianbergmann/recursion-context/issues",
- "source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.1"
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5"
},
"funding": [
{
@@ -1598,29 +1533,32 @@
"type": "github"
}
],
- "time": "2020-11-30T07:34:24+00:00"
+ "time": "2023-02-03T06:07:39+00:00"
},
{
"name": "sebastian/resource-operations",
- "version": "2.0.2",
+ "version": "3.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/resource-operations.git",
- "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3"
+ "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/31d35ca87926450c44eae7e2611d45a7a65ea8b3",
- "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3",
+ "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
+ "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e",
"shasum": ""
},
"require": {
- "php": ">=7.1"
+ "php": ">=7.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0-dev"
+ "dev-main": "3.0-dev"
}
},
"autoload": {
@@ -1641,8 +1579,7 @@
"description": "Provides a list of PHP built-in functions that operate on resources",
"homepage": "https://www.github.com/sebastianbergmann/resource-operations",
"support": {
- "issues": "https://github.com/sebastianbergmann/resource-operations/issues",
- "source": "https://github.com/sebastianbergmann/resource-operations/tree/2.0.2"
+ "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4"
},
"funding": [
{
@@ -1650,32 +1587,32 @@
"type": "github"
}
],
- "time": "2020-11-30T07:30:19+00:00"
+ "time": "2024-03-14T16:00:52+00:00"
},
{
"name": "sebastian/type",
- "version": "1.1.4",
+ "version": "3.2.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/type.git",
- "reference": "0150cfbc4495ed2df3872fb31b26781e4e077eb4"
+ "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/0150cfbc4495ed2df3872fb31b26781e4e077eb4",
- "reference": "0150cfbc4495ed2df3872fb31b26781e4e077eb4",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
+ "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7",
"shasum": ""
},
"require": {
- "php": ">=7.2"
+ "php": ">=7.3"
},
"require-dev": {
- "phpunit/phpunit": "^8.2"
+ "phpunit/phpunit": "^9.5"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.1-dev"
+ "dev-master": "3.2-dev"
}
},
"autoload": {
@@ -1698,7 +1635,7 @@
"homepage": "https://github.com/sebastianbergmann/type",
"support": {
"issues": "https://github.com/sebastianbergmann/type/issues",
- "source": "https://github.com/sebastianbergmann/type/tree/1.1.4"
+ "source": "https://github.com/sebastianbergmann/type/tree/3.2.1"
},
"funding": [
{
@@ -1706,29 +1643,29 @@
"type": "github"
}
],
- "time": "2020-11-30T07:25:11+00:00"
+ "time": "2023-02-03T06:13:03+00:00"
},
{
"name": "sebastian/version",
- "version": "2.0.1",
+ "version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/version.git",
- "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019"
+ "reference": "c6c1022351a901512170118436c764e473f6de8c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019",
- "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c",
+ "reference": "c6c1022351a901512170118436c764e473f6de8c",
"shasum": ""
},
"require": {
- "php": ">=5.6"
+ "php": ">=7.3"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0.x-dev"
+ "dev-master": "3.0-dev"
}
},
"autoload": {
@@ -1751,101 +1688,28 @@
"homepage": "https://github.com/sebastianbergmann/version",
"support": {
"issues": "https://github.com/sebastianbergmann/version/issues",
- "source": "https://github.com/sebastianbergmann/version/tree/master"
- },
- "time": "2016-10-03T07:35:21+00:00"
- },
- {
- "name": "symfony/polyfill-ctype",
- "version": "v1.23.0",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/polyfill-ctype.git",
- "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/46cd95797e9df938fdd2b03693b5fca5e64b01ce",
- "reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce",
- "shasum": ""
- },
- "require": {
- "php": ">=7.1"
- },
- "suggest": {
- "ext-ctype": "For best performance"
- },
- "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"
- ]
- },
- "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.23.0"
+ "source": "https://github.com/sebastianbergmann/version/tree/3.0.2"
},
"funding": [
{
- "url": "https://symfony.com/sponsor",
- "type": "custom"
- },
- {
- "url": "https://github.com/fabpot",
+ "url": "https://github.com/sebastianbergmann",
"type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
- "type": "tidelift"
}
],
- "time": "2021-02-19T12:13:01+00:00"
+ "time": "2020-09-28T06:39:44+00:00"
},
{
"name": "theseer/tokenizer",
- "version": "1.2.0",
+ "version": "1.2.3",
"source": {
"type": "git",
"url": "https://github.com/theseer/tokenizer.git",
- "reference": "75a63c33a8577608444246075ea0af0d052e452a"
+ "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a",
- "reference": "75a63c33a8577608444246075ea0af0d052e452a",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
+ "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2",
"shasum": ""
},
"require": {
@@ -1874,7 +1738,7 @@
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
"support": {
"issues": "https://github.com/theseer/tokenizer/issues",
- "source": "https://github.com/theseer/tokenizer/tree/master"
+ "source": "https://github.com/theseer/tokenizer/tree/1.2.3"
},
"funding": [
{
@@ -1882,65 +1746,7 @@
"type": "github"
}
],
- "time": "2020-07-12T23:59:07+00:00"
- },
- {
- "name": "webmozart/assert",
- "version": "1.10.0",
- "source": {
- "type": "git",
- "url": "https://github.com/webmozarts/assert.git",
- "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25",
- "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25",
- "shasum": ""
- },
- "require": {
- "php": "^7.2 || ^8.0",
- "symfony/polyfill-ctype": "^1.8"
- },
- "conflict": {
- "phpstan/phpstan": "<0.12.20",
- "vimeo/psalm": "<4.6.1 || 4.6.2"
- },
- "require-dev": {
- "phpunit/phpunit": "^8.5.13"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.10-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Webmozart\\Assert\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Bernhard Schussek",
- "email": "bschussek@gmail.com"
- }
- ],
- "description": "Assertions to validate method input/output with nice error messages.",
- "keywords": [
- "assert",
- "check",
- "validate"
- ],
- "support": {
- "issues": "https://github.com/webmozarts/assert/issues",
- "source": "https://github.com/webmozarts/assert/tree/1.10.0"
- },
- "time": "2021-03-09T10:59:23+00:00"
+ "time": "2024-03-03T12:36:25+00:00"
}
],
"aliases": [],
@@ -1950,5 +1756,5 @@
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
- "plugin-api-version": "2.0.0"
+ "plugin-api-version": "2.6.0"
}
diff --git a/config/php-scoper/scoper.inc.php b/config/php-scoper/scoper.inc.php
new file mode 100644
index 0000000..de0e7d3
--- /dev/null
+++ b/config/php-scoper/scoper.inc.php
@@ -0,0 +1,18 @@
+ 'IndieAuth\\Libs',
+ 'output-dir' => 'scoped-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/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/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..ea5f257
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,4 @@
+
+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/scoped-libs/firebase/php-jwt/LICENSE similarity index 100% rename from vendor/firebase/php-jwt/LICENSE rename to scoped-libs/firebase/php-jwt/LICENSE diff --git a/vendor/firebase/php-jwt/composer.json b/scoped-libs/firebase/php-jwt/composer.json similarity index 52% rename from vendor/firebase/php-jwt/composer.json rename to scoped-libs/firebase/php-jwt/composer.json index 6146e2d..4bccac2 100644 --- a/vendor/firebase/php-jwt/composer.json +++ b/scoped-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": "^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": "^7.4", + "phpspec\/prophecy-phpunit": "^2.0", + "phpunit\/phpunit": "^9.5", + "psr\/cache": "^2.0||^3.0", + "psr\/http-client": "^1.0", + "psr\/http-factory": "^1.0" } -} +} \ No newline at end of file diff --git a/scoped-libs/firebase/php-jwt/src/BeforeValidException.php b/scoped-libs/firebase/php-jwt/src/BeforeValidException.php new file mode 100644 index 0000000..c39d607 --- /dev/null +++ b/scoped-libs/firebase/php-jwt/src/BeforeValidException.php @@ -0,0 +1,17 @@ +payload = $payload; + } + public function getPayload() : object + { + return $this->payload; + } +} diff --git a/scoped-libs/firebase/php-jwt/src/CachedKeySet.php b/scoped-libs/firebase/php-jwt/src/CachedKeySet.php new file mode 100644 index 0000000..2b9ca88 --- /dev/null +++ b/scoped-libs/firebase/php-jwt/src/CachedKeySet.php @@ -0,0 +1,229 @@ + + * @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 arrayBarnaby 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/scoped-libs/mf2/mf2/composer.json b/scoped-libs/mf2/mf2/composer.json new file mode 100644 index 0000000..7242aaa --- /dev/null +++ b/scoped-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/scoped-libs/mf2/mf2/tests/Mf2/ClassicMicroformatsTest.php b/scoped-libs/mf2/mf2/tests/Mf2/ClassicMicroformatsTest.php new file mode 100644 index 0000000..f47f985 --- /dev/null +++ b/scoped-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 = 'yes yes it is.
April was pretty decent. I got to attend two very good conferences and I got to speak at them.
+\t\t\tProject 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 = <<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
+ + +
++ + +
++ + +
+ +
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 …
+