diff --git a/analytics/meta/dashboard.php b/analytics/meta/dashboard.php new file mode 100644 index 00000000..b8e3ea5b --- /dev/null +++ b/analytics/meta/dashboard.php @@ -0,0 +1,72 @@ +add(new DateInterval('PT10M')); // Token expires in 10 minutes + +// You can add parameters if needed for filtering or other purposes +$params = (object)[ + // Add any parameters required by your dashboard or charts here + // For example: 'region' => 'North', 'date' => '2024-01-01' + // Leave this as an empty object if there are no params +]; + +$token = $config->builder() + ->issuedBy($metabaseSiteUrl) // Configures the issuer (iss claim) + ->issuedAt($now) // Configures the time that the token was issued (iat claim) + ->expiresAt($exp) // Configures the expiration time of the token (exp claim) + ->withClaim('resource', ['dashboard' => $dashboardId]) // Add resource claim + ->withClaim('params', $params) // Add params claim as an object + ->getToken($config->signer(), $config->signingKey()); // Retrieves the generated token + +// Generate iframe URL +$iframeUrl = $metabaseSiteUrl . "/embed/dashboard/" . $token->toString() . "#theme=transparent&bordered=false&titled=true"; +?> + + + + + + + Embedded Metabase Dashboard + + + + + + diff --git a/analytics/meta/embed.php b/analytics/meta/embed.php new file mode 100644 index 00000000..e2c2def2 --- /dev/null +++ b/analytics/meta/embed.php @@ -0,0 +1,16 @@ + 'past26weeks']; + +$metabase = new \Metabase\Embed($metabaseUrl, $metabaseKey); +// Generate the HTML to create an iframe with the embedded dashboard +echo $metabase->dashboardIframe($dashboardId, $params); +?> \ No newline at end of file diff --git a/analytics/meta/metabase.php b/analytics/meta/metabase.php new file mode 100644 index 00000000..8ba1218b --- /dev/null +++ b/analytics/meta/metabase.php @@ -0,0 +1,188 @@ +url = $url; + $this->key = $key; + $this->border = $border; + $this->title = $title; + $this->width = $width; + $this->height = $height; + $this->expirationSeconds = $expirationSeconds; + + $this->jwtConfig = Configuration::forSymmetricSigner(new Sha256(), InMemory::plainText($this->key)); + } + + /** + * Get the embed URL for a Metabase question + * + * @param int $questionId The id of the question to embed + * @param array $params An associate array with variables to be passed to the question + * + * @return string Embed URL + */ + public function questionUrl($questionId, $params = []) + { + return $this->url('question', $questionId, $params); + } + + /** + * Get the embed URL for a Metabase dashboard + * + * @param int $dashboardId The id of the dashboard to embed + * @param array $params An associate array with variables to be passed to the dashboard + * + * @return string Embed URL + */ + public function dashboardUrl($dashboardId, $params = []) + { + return $this->url('dashboard', $dashboardId, $params); + } + + /** + * Use JWT to encode tokens + * + * @param array $resource Resource to encode (question or dashboard) + * @param array $params An associate array with variables to be passed to the dashboard + * + * @return string Token + */ + private function encode($resource, $params) + { + $jwt = $this->jwtConfig->builder(); + $jwt->withClaim('resource', $resource); + if (empty($params)) { + $jwt->withClaim('params', (object)[]); + } else { + $jwt->withClaim('params', $params); + } + if (!is_null($this->expirationSeconds)) { + $jwt->expiresAt((new DateTimeImmutable())->modify('+' . $this->expirationSeconds . ' seconds')); + } + + return $jwt->getToken($this->jwtConfig->signer(), $this->jwtConfig->signingKey()); + } + + protected function url($resource, $id, $params) + { + // Generate auth token, using JWT + $token = $this->encode([$resource => $id], $params); + + // Generate embed URL + $url = $this->url . '/embed/' . $resource . '/' . $token->toString() . '#'; + + // Should border be included + if ($this->border) { + $url .= 'bordered=true&'; + } else { + $url .= 'bordered=false&'; + } + + // Should title be included + if ($this->title) { + $url .= 'titled=true&'; + } else { + $url .= 'titled=false&'; + } + + // Set selected theme (if any) + if (!empty($this->theme)) { + $url .= 'theme=' . $this->theme . '&'; + } + + // Remove trailing & + $url = rtrim($url, '&'); + + return $url; + } + + /** + * Generate the HTML to embed a question iframe with a given question id. + * It assumes no iframe border. Size can be manipulated via + * class $width/$height + * + * @param int $questionId The id of the question to embed + * @param array $params An associate array with variables to be passed to the question + * + * @return string Code to embed + */ + public function questionIFrame($questionId, $params = []) + { + $url = $this->questionUrl($questionId, $params); + return $this->iframe($url); + } + + /** + * Generate the HTML to embed a dashboard iframe with a given dashboard id. + * It assumes no iframe border. Size can be manipulated via + * class $width/$height + * + * @param int $dashboardId The id of the dashboard to embed + * @param array $params An associate array with variables to be passed to the dashboard + * + * @return string Code to embed + */ + public function dashboardIFrame($dashboardId, $params = []) + { + $url = $this->dashboardUrl($dashboardId, $params); + return $this->iframe($url); + } + + /** + * Generate the HTML to embed an iframe with a given URL. + * It assumes no iframe border. Size can be manipulated via + * class $width/$height + * + * @param string $iframeUrl The URL to create an iframe for + * + * @return string Code to embed + */ + protected function iframe($iframeUrl) + { + return ''; + } +} \ No newline at end of file diff --git a/analytics/tests/index.php b/analytics/tests/index.php new file mode 100644 index 00000000..de69ced5 --- /dev/null +++ b/analytics/tests/index.php @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/composer.json b/composer.json index d7c6bb72..cf40a678 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,25 @@ { + "name": "mikeintoshsystems/hispmd", + "type": "library", + "description": "HISPMD", + "keywords": ["metabase, HISPMD"], + "homepage": "https://github.com/mikeintoshsystems/hispmd", + "license": "MIT", + "authors": [ + { + "name": "Michael Kifle Teferra", + "email": "mikeintoshsys@gmail.com", + "homepage": "https://mikeintoshsys.com", + "role": "Developer" + } + ], "require": { - "vlucas/phpdotenv": "^5.6" - } + "vlucas/phpdotenv": "^5.6", + "lcobucci/jwt": "^5.3" + }, + "autoload": { + "psr-4": { + "Metabase\\": "analytics" + } + } } diff --git a/composer.lock b/composer.lock index 093122ee..f897e9c1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "108be68e4e2b97fed51d36a10eed0849", + "content-hash": "0b8a074fb783e46f6e4d1090fe323914", "packages": [ { "name": "graham-campbell/result-type", @@ -68,6 +68,79 @@ ], "time": "2023-11-12T22:16:48+00:00" }, + { + "name": "lcobucci/jwt", + "version": "5.3.0", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/jwt.git", + "reference": "08071d8d2c7f4b00222cc4b1fb6aa46990a80f83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/08071d8d2c7f4b00222cc4b1fb6aa46990a80f83", + "reference": "08071d8d2c7f4b00222cc4b1fb6aa46990a80f83", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-sodium": "*", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0", + "psr/clock": "^1.0" + }, + "require-dev": { + "infection/infection": "^0.27.0", + "lcobucci/clock": "^3.0", + "lcobucci/coding-standard": "^11.0", + "phpbench/phpbench": "^1.2.9", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.10.7", + "phpstan/phpstan-deprecation-rules": "^1.1.3", + "phpstan/phpstan-phpunit": "^1.3.10", + "phpstan/phpstan-strict-rules": "^1.5.0", + "phpunit/phpunit": "^10.2.6" + }, + "suggest": { + "lcobucci/clock": ">= 3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Lcobucci\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Luís Cobucci", + "email": "lcobucci@gmail.com", + "role": "Developer" + } + ], + "description": "A simple library to work with JSON Web Token and JSON Web Signature", + "keywords": [ + "JWS", + "jwt" + ], + "support": { + "issues": "https://github.com/lcobucci/jwt/issues", + "source": "https://github.com/lcobucci/jwt/tree/5.3.0" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2024-04-11T23:07:54+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.2", @@ -143,6 +216,54 @@ ], "time": "2023-11-12T21:59:55+00:00" }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, { "name": "symfony/polyfill-ctype", "version": "v1.30.0", diff --git a/docker-compose.yml b/docker-compose.yml index 02ff271a..a5de3b3e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,7 @@ services: - ./.env:/var/www/html/.env - ./index.php:/var/www/html/index.php - ./LICENSE:/var/www/html/LICENSE - - ./map_files:/var/www/html/map_files + - ./maps:/var/www/html/maps - ./mysql-init:/var/www/html/mysql-init - ./README.md:/var/www/html/README.md - ./server.html:/var/www/html/server.html @@ -115,7 +115,7 @@ services: container_name: hispmd_viz hostname: metabase volumes: - - hispmd_vizdata:/metabase-data + - ./src/hispmd_vizdata:/metabase-data ports: - "4004:3000" environment: diff --git a/map_files/MIKEINTOSH_ethiopia_region_Zone_map_MAIN.geojson b/maps/ethiopia_region_Zone_map_MAIN.geojson similarity index 100% rename from map_files/MIKEINTOSH_ethiopia_region_Zone_map_MAIN.geojson rename to maps/ethiopia_region_Zone_map_MAIN.geojson diff --git a/map_files/mikeintosh_ethiopia_regions_map_simple.geojson b/maps/ethiopia_regions_map_simple.geojson similarity index 100% rename from map_files/mikeintosh_ethiopia_regions_map_simple.geojson rename to maps/ethiopia_regions_map_simple.geojson diff --git a/maps/mapbox/index.html b/maps/mapbox/index.html new file mode 100644 index 00000000..83608891 --- /dev/null +++ b/maps/mapbox/index.html @@ -0,0 +1,74 @@ + + + + + + Change a map's style + + + + + + + + + +
+ + + + + + + \ No newline at end of file diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php index bb79e2a5..0f06ef54 100644 --- a/vendor/composer/autoload_psr4.php +++ b/vendor/composer/autoload_psr4.php @@ -9,7 +9,10 @@ 'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'), 'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'), 'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'), + 'Psr\\Clock\\' => array($vendorDir . '/psr/clock/src'), 'PhpOption\\' => array($vendorDir . '/phpoption/phpoption/src/PhpOption'), + 'Metabase\\' => array($baseDir . '/analytics'), + 'Lcobucci\\JWT\\' => array($vendorDir . '/lcobucci/jwt/src'), 'GrahamCampbell\\ResultType\\' => array($vendorDir . '/graham-campbell/result-type/src'), 'Dotenv\\' => array($vendorDir . '/vlucas/phpdotenv/src'), ); diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index 6668bae5..219a4401 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -21,8 +21,17 @@ class ComposerStaticInite875ae8441d070d7dda5f4b47a2117aa ), 'P' => array ( + 'Psr\\Clock\\' => 10, 'PhpOption\\' => 10, ), + 'M' => + array ( + 'Metabase\\' => 9, + ), + 'L' => + array ( + 'Lcobucci\\JWT\\' => 13, + ), 'G' => array ( 'GrahamCampbell\\ResultType\\' => 26, @@ -46,10 +55,22 @@ class ComposerStaticInite875ae8441d070d7dda5f4b47a2117aa array ( 0 => __DIR__ . '/..' . '/symfony/polyfill-ctype', ), + 'Psr\\Clock\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/clock/src', + ), 'PhpOption\\' => array ( 0 => __DIR__ . '/..' . '/phpoption/phpoption/src/PhpOption', ), + 'Metabase\\' => + array ( + 0 => __DIR__ . '/../..' . '/analytics', + ), + 'Lcobucci\\JWT\\' => + array ( + 0 => __DIR__ . '/..' . '/lcobucci/jwt/src', + ), 'GrahamCampbell\\ResultType\\' => array ( 0 => __DIR__ . '/..' . '/graham-campbell/result-type/src', diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 53ec7b41..8f73bf03 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -65,6 +65,82 @@ ], "install-path": "../graham-campbell/result-type" }, + { + "name": "lcobucci/jwt", + "version": "5.3.0", + "version_normalized": "5.3.0.0", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/jwt.git", + "reference": "08071d8d2c7f4b00222cc4b1fb6aa46990a80f83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/08071d8d2c7f4b00222cc4b1fb6aa46990a80f83", + "reference": "08071d8d2c7f4b00222cc4b1fb6aa46990a80f83", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-sodium": "*", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0", + "psr/clock": "^1.0" + }, + "require-dev": { + "infection/infection": "^0.27.0", + "lcobucci/clock": "^3.0", + "lcobucci/coding-standard": "^11.0", + "phpbench/phpbench": "^1.2.9", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.10.7", + "phpstan/phpstan-deprecation-rules": "^1.1.3", + "phpstan/phpstan-phpunit": "^1.3.10", + "phpstan/phpstan-strict-rules": "^1.5.0", + "phpunit/phpunit": "^10.2.6" + }, + "suggest": { + "lcobucci/clock": ">= 3.0" + }, + "time": "2024-04-11T23:07:54+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Lcobucci\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Luís Cobucci", + "email": "lcobucci@gmail.com", + "role": "Developer" + } + ], + "description": "A simple library to work with JSON Web Token and JSON Web Signature", + "keywords": [ + "JWS", + "jwt" + ], + "support": { + "issues": "https://github.com/lcobucci/jwt/issues", + "source": "https://github.com/lcobucci/jwt/tree/5.3.0" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "install-path": "../lcobucci/jwt" + }, { "name": "phpoption/phpoption", "version": "1.9.2", @@ -143,6 +219,57 @@ ], "install-path": "../phpoption/phpoption" }, + { + "name": "psr/clock", + "version": "1.0.0", + "version_normalized": "1.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "time": "2022-11-25T14:36:26+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "install-path": "../psr/clock" + }, { "name": "symfony/polyfill-ctype", "version": "v1.30.0", diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index 55d26a5d..95e2cda9 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -1,24 +1,15 @@ array( - 'name' => '__root__', + 'name' => 'mikeintoshsystems/hispmd', 'pretty_version' => 'dev-main', 'version' => 'dev-main', - 'reference' => '95455018a2dc4b6b98a0a3922c2621621955fdba', + 'reference' => '7f82c073b142b223f67ebbb2c18014835aab71fb', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev' => true, ), 'versions' => array( - '__root__' => array( - 'pretty_version' => 'dev-main', - 'version' => 'dev-main', - 'reference' => '95455018a2dc4b6b98a0a3922c2621621955fdba', - 'type' => 'library', - 'install_path' => __DIR__ . '/../../', - 'aliases' => array(), - 'dev_requirement' => false, - ), 'graham-campbell/result-type' => array( 'pretty_version' => 'v1.1.2', 'version' => '1.1.2.0', @@ -28,6 +19,24 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'lcobucci/jwt' => array( + 'pretty_version' => '5.3.0', + 'version' => '5.3.0.0', + 'reference' => '08071d8d2c7f4b00222cc4b1fb6aa46990a80f83', + 'type' => 'library', + 'install_path' => __DIR__ . '/../lcobucci/jwt', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'mikeintoshsystems/hispmd' => array( + 'pretty_version' => 'dev-main', + 'version' => 'dev-main', + 'reference' => '7f82c073b142b223f67ebbb2c18014835aab71fb', + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'phpoption/phpoption' => array( 'pretty_version' => '1.9.2', 'version' => '1.9.2.0', @@ -37,6 +46,15 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'psr/clock' => array( + 'pretty_version' => '1.0.0', + 'version' => '1.0.0.0', + 'reference' => 'e41a24703d4560fd0acb709162f73b8adfc3aa0d', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/clock', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'symfony/polyfill-ctype' => array( 'pretty_version' => 'v1.30.0', 'version' => '1.30.0.0', diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php index a8b98d5c..4c3a5d68 100644 --- a/vendor/composer/platform_check.php +++ b/vendor/composer/platform_check.php @@ -4,8 +4,8 @@ $issues = array(); -if (!(PHP_VERSION_ID >= 70205)) { - $issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.5". You are running ' . PHP_VERSION . '.'; +if (!(PHP_VERSION_ID >= 80100)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.'; } if ($issues) { diff --git a/vendor/lcobucci/jwt/.readthedocs.yaml b/vendor/lcobucci/jwt/.readthedocs.yaml new file mode 100644 index 00000000..aa49c943 --- /dev/null +++ b/vendor/lcobucci/jwt/.readthedocs.yaml @@ -0,0 +1,9 @@ +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3" + +mkdocs: + configuration: mkdocs.yml diff --git a/vendor/lcobucci/jwt/LICENSE b/vendor/lcobucci/jwt/LICENSE new file mode 100644 index 00000000..cc7e28f1 --- /dev/null +++ b/vendor/lcobucci/jwt/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2014, Luís Cobucci +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the {organization} nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/lcobucci/jwt/composer.json b/vendor/lcobucci/jwt/composer.json new file mode 100644 index 00000000..a6186894 --- /dev/null +++ b/vendor/lcobucci/jwt/composer.json @@ -0,0 +1,63 @@ +{ + "name": "lcobucci/jwt", + "description": "A simple library to work with JSON Web Token and JSON Web Signature", + "license": [ + "BSD-3-Clause" + ], + "type": "library", + "keywords": [ + "JWT", + "JWS" + ], + "authors": [ + { + "name": "Luís Cobucci", + "email": "lcobucci@gmail.com", + "role": "Developer" + } + ], + "require": { + "php": "~8.1.0 || ~8.2.0 || ~8.3.0", + "ext-openssl": "*", + "ext-sodium": "*", + "psr/clock": "^1.0" + }, + "require-dev": { + "infection/infection": "^0.27.0", + "lcobucci/clock": "^3.0", + "lcobucci/coding-standard": "^11.0", + "phpbench/phpbench": "^1.2.9", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.10.7", + "phpstan/phpstan-deprecation-rules": "^1.1.3", + "phpstan/phpstan-phpunit": "^1.3.10", + "phpstan/phpstan-strict-rules": "^1.5.0", + "phpunit/phpunit": "^10.2.6" + }, + "suggest": { + "lcobucci/clock": ">= 3.0" + }, + "autoload": { + "psr-4": { + "Lcobucci\\JWT\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Lcobucci\\JWT\\Tests\\": "tests" + } + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true, + "infection/extension-installer": true, + "ocramius/package-versions": true, + "phpstan/extension-installer": true + }, + "platform": { + "php": "8.1.99" + }, + "preferred-install": "dist", + "sort-packages": true + } +} diff --git a/vendor/lcobucci/jwt/renovate.json b/vendor/lcobucci/jwt/renovate.json new file mode 100644 index 00000000..d0adcc3a --- /dev/null +++ b/vendor/lcobucci/jwt/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "local>lcobucci/.github:renovate-config" + ] +} diff --git a/vendor/lcobucci/jwt/src/Builder.php b/vendor/lcobucci/jwt/src/Builder.php new file mode 100644 index 00000000..4295e3f0 --- /dev/null +++ b/vendor/lcobucci/jwt/src/Builder.php @@ -0,0 +1,85 @@ + $claims + * + * @return array + */ + public function formatClaims(array $claims): array; +} diff --git a/vendor/lcobucci/jwt/src/Configuration.php b/vendor/lcobucci/jwt/src/Configuration.php new file mode 100644 index 00000000..488ea3e2 --- /dev/null +++ b/vendor/lcobucci/jwt/src/Configuration.php @@ -0,0 +1,131 @@ +parser = new Token\Parser($decoder); + $this->validator = new Validation\Validator(); + + $this->builderFactory = static function (ClaimsFormatter $claimFormatter) use ($encoder): Builder { + return new Token\Builder($encoder, $claimFormatter); + }; + } + + public static function forAsymmetricSigner( + Signer $signer, + Key $signingKey, + Key $verificationKey, + Encoder $encoder = new JoseEncoder(), + Decoder $decoder = new JoseEncoder(), + ): self { + return new self( + $signer, + $signingKey, + $verificationKey, + $encoder, + $decoder, + ); + } + + public static function forSymmetricSigner( + Signer $signer, + Key $key, + Encoder $encoder = new JoseEncoder(), + Decoder $decoder = new JoseEncoder(), + ): self { + return new self( + $signer, + $key, + $key, + $encoder, + $decoder, + ); + } + + /** @param callable(ClaimsFormatter): Builder $builderFactory */ + public function setBuilderFactory(callable $builderFactory): void + { + $this->builderFactory = $builderFactory(...); + } + + public function builder(?ClaimsFormatter $claimFormatter = null): Builder + { + return ($this->builderFactory)($claimFormatter ?? ChainedFormatter::default()); + } + + public function parser(): Parser + { + return $this->parser; + } + + public function setParser(Parser $parser): void + { + $this->parser = $parser; + } + + public function signer(): Signer + { + return $this->signer; + } + + public function signingKey(): Key + { + return $this->signingKey; + } + + public function verificationKey(): Key + { + return $this->verificationKey; + } + + public function validator(): Validator + { + return $this->validator; + } + + public function setValidator(Validator $validator): void + { + $this->validator = $validator; + } + + /** @return Constraint[] */ + public function validationConstraints(): array + { + return $this->validationConstraints; + } + + public function setValidationConstraints(Constraint ...$validationConstraints): void + { + $this->validationConstraints = $validationConstraints; + } +} diff --git a/vendor/lcobucci/jwt/src/Decoder.php b/vendor/lcobucci/jwt/src/Decoder.php new file mode 100644 index 00000000..6b24b926 --- /dev/null +++ b/vendor/lcobucci/jwt/src/Decoder.php @@ -0,0 +1,29 @@ + */ + private array $formatters; + + public function __construct(ClaimsFormatter ...$formatters) + { + $this->formatters = $formatters; + } + + public static function default(): self + { + return new self(new UnifyAudience(), new MicrosecondBasedDateConversion()); + } + + public static function withUnixTimestampDates(): self + { + return new self(new UnifyAudience(), new UnixTimestampDates()); + } + + /** @inheritdoc */ + public function formatClaims(array $claims): array + { + foreach ($this->formatters as $formatter) { + $claims = $formatter->formatClaims($claims); + } + + return $claims; + } +} diff --git a/vendor/lcobucci/jwt/src/Encoding/JoseEncoder.php b/vendor/lcobucci/jwt/src/Encoding/JoseEncoder.php new file mode 100644 index 00000000..0d904442 --- /dev/null +++ b/vendor/lcobucci/jwt/src/Encoding/JoseEncoder.php @@ -0,0 +1,56 @@ +convertDate($claims[$claim]); + } + + return $claims; + } + + private function convertDate(DateTimeImmutable $date): int|float + { + if ($date->format('u') === '000000') { + return (int) $date->format('U'); + } + + return (float) $date->format('U.u'); + } +} diff --git a/vendor/lcobucci/jwt/src/Encoding/UnifyAudience.php b/vendor/lcobucci/jwt/src/Encoding/UnifyAudience.php new file mode 100644 index 00000000..cf57252e --- /dev/null +++ b/vendor/lcobucci/jwt/src/Encoding/UnifyAudience.php @@ -0,0 +1,29 @@ +convertDate($claims[$claim]); + } + + return $claims; + } + + private function convertDate(DateTimeImmutable $date): int + { + return $date->getTimestamp(); + } +} diff --git a/vendor/lcobucci/jwt/src/Exception.php b/vendor/lcobucci/jwt/src/Exception.php new file mode 100644 index 00000000..4b3916e6 --- /dev/null +++ b/vendor/lcobucci/jwt/src/Exception.php @@ -0,0 +1,10 @@ +clock = $clock ?? new class implements Clock { + public function now(): DateTimeImmutable + { + return new DateTimeImmutable(); + } + }; + } + + /** @param Closure(Builder, DateTimeImmutable):Builder $customiseBuilder */ + public function issue( + Signer $signer, + Key $signingKey, + Closure $customiseBuilder, + ): UnencryptedToken { + $builder = new Token\Builder(new JoseEncoder(), ChainedFormatter::withUnixTimestampDates()); + + $now = $this->clock->now(); + $builder = $builder + ->issuedAt($now) + ->canOnlyBeUsedAfter($now) + ->expiresAt($now->modify('+5 minutes')); + + return $customiseBuilder($builder, $now)->getToken($signer, $signingKey); + } + + /** @param non-empty-string $jwt */ + public function parse( + string $jwt, + SignedWith $signedWith, + ValidAt $validAt, + Constraint ...$constraints, + ): UnencryptedToken { + $token = $this->parser->parse($jwt); + assert($token instanceof UnencryptedToken); + + (new Validator())->assert( + $token, + $signedWith, + $validAt, + ...$constraints, + ); + + return $token; + } +} diff --git a/vendor/lcobucci/jwt/src/Parser.php b/vendor/lcobucci/jwt/src/Parser.php new file mode 100644 index 00000000..fa77f04e --- /dev/null +++ b/vendor/lcobucci/jwt/src/Parser.php @@ -0,0 +1,22 @@ +contents()); + + if ($actualKeyLength < self::MINIMUM_KEY_LENGTH_IN_BITS) { + throw InvalidKeyProvided::tooShort(self::MINIMUM_KEY_LENGTH_IN_BITS, $actualKeyLength); + } + + return sodium_crypto_generichash($payload, $key->contents()); + } + + public function verify(string $expected, string $payload, Key $key): bool + { + return hash_equals($expected, $this->sign($payload, $key)); + } +} diff --git a/vendor/lcobucci/jwt/src/Signer/CannotSignPayload.php b/vendor/lcobucci/jwt/src/Signer/CannotSignPayload.php new file mode 100644 index 00000000..35cc4d6d --- /dev/null +++ b/vendor/lcobucci/jwt/src/Signer/CannotSignPayload.php @@ -0,0 +1,15 @@ +converter->fromAsn1( + $this->createSignature($key->contents(), $key->passphrase(), $payload), + $this->pointLength(), + ); + } + + final public function verify(string $expected, string $payload, Key $key): bool + { + return $this->verifySignature( + $this->converter->toAsn1($expected, $this->pointLength()), + $payload, + $key->contents(), + ); + } + + /** {@inheritDoc} */ + final protected function guardAgainstIncompatibleKey(int $type, int $lengthInBits): void + { + if ($type !== OPENSSL_KEYTYPE_EC) { + throw InvalidKeyProvided::incompatibleKeyType( + self::KEY_TYPE_MAP[OPENSSL_KEYTYPE_EC], + self::KEY_TYPE_MAP[$type], + ); + } + + $expectedKeyLength = $this->expectedKeyLength(); + + if ($lengthInBits !== $expectedKeyLength) { + throw InvalidKeyProvided::incompatibleKeyLength($expectedKeyLength, $lengthInBits); + } + } + + /** + * @internal + * + * @return positive-int + */ + abstract public function expectedKeyLength(): int; + + /** + * Returns the length of each point in the signature, so that we can calculate and verify R and S points properly + * + * @internal + * + * @return positive-int + */ + abstract public function pointLength(): int; +} diff --git a/vendor/lcobucci/jwt/src/Signer/Ecdsa/ConversionFailed.php b/vendor/lcobucci/jwt/src/Signer/Ecdsa/ConversionFailed.php new file mode 100644 index 00000000..d9ca751d --- /dev/null +++ b/vendor/lcobucci/jwt/src/Signer/Ecdsa/ConversionFailed.php @@ -0,0 +1,25 @@ + self::ASN1_MAX_SINGLE_BYTE ? self::ASN1_LENGTH_2BYTES : ''; + + $asn1 = hex2bin( + self::ASN1_SEQUENCE + . $lengthPrefix . dechex($totalLength) + . self::ASN1_INTEGER . dechex($lengthR) . $pointR + . self::ASN1_INTEGER . dechex($lengthS) . $pointS, + ); + assert(is_string($asn1)); + assert($asn1 !== ''); + + return $asn1; + } + + private static function octetLength(string $data): int + { + return (int) (strlen($data) / self::BYTE_SIZE); + } + + private static function preparePositiveInteger(string $data): string + { + if (substr($data, 0, self::BYTE_SIZE) > self::ASN1_BIG_INTEGER_LIMIT) { + return self::ASN1_NEGATIVE_INTEGER . $data; + } + + while ( + substr($data, 0, self::BYTE_SIZE) === self::ASN1_NEGATIVE_INTEGER + && substr($data, 2, self::BYTE_SIZE) <= self::ASN1_BIG_INTEGER_LIMIT + ) { + $data = substr($data, 2, null); + } + + return $data; + } + + public function fromAsn1(string $signature, int $length): string + { + $message = bin2hex($signature); + $position = 0; + + if (self::readAsn1Content($message, $position, self::BYTE_SIZE) !== self::ASN1_SEQUENCE) { + throw ConversionFailed::incorrectStartSequence(); + } + + // @phpstan-ignore-next-line + if (self::readAsn1Content($message, $position, self::BYTE_SIZE) === self::ASN1_LENGTH_2BYTES) { + $position += self::BYTE_SIZE; + } + + $pointR = self::retrievePositiveInteger(self::readAsn1Integer($message, $position)); + $pointS = self::retrievePositiveInteger(self::readAsn1Integer($message, $position)); + + $points = hex2bin(str_pad($pointR, $length, '0', STR_PAD_LEFT) . str_pad($pointS, $length, '0', STR_PAD_LEFT)); + assert(is_string($points)); + assert($points !== ''); + + return $points; + } + + private static function readAsn1Content(string $message, int &$position, int $length): string + { + $content = substr($message, $position, $length); + $position += $length; + + return $content; + } + + private static function readAsn1Integer(string $message, int &$position): string + { + if (self::readAsn1Content($message, $position, self::BYTE_SIZE) !== self::ASN1_INTEGER) { + throw ConversionFailed::integerExpected(); + } + + $length = (int) hexdec(self::readAsn1Content($message, $position, self::BYTE_SIZE)); + + return self::readAsn1Content($message, $position, $length * self::BYTE_SIZE); + } + + private static function retrievePositiveInteger(string $data): string + { + while ( + substr($data, 0, self::BYTE_SIZE) === self::ASN1_NEGATIVE_INTEGER + && substr($data, 2, self::BYTE_SIZE) > self::ASN1_BIG_INTEGER_LIMIT + ) { + $data = substr($data, 2, null); + } + + return $data; + } +} diff --git a/vendor/lcobucci/jwt/src/Signer/Ecdsa/Sha256.php b/vendor/lcobucci/jwt/src/Signer/Ecdsa/Sha256.php new file mode 100644 index 00000000..ff00f4d4 --- /dev/null +++ b/vendor/lcobucci/jwt/src/Signer/Ecdsa/Sha256.php @@ -0,0 +1,31 @@ +contents()); + } catch (SodiumException $sodiumException) { + throw new InvalidKeyProvided($sodiumException->getMessage(), 0, $sodiumException); + } + } + + public function verify(string $expected, string $payload, Key $key): bool + { + try { + return sodium_crypto_sign_verify_detached($expected, $payload, $key->contents()); + } catch (SodiumException $sodiumException) { + throw new InvalidKeyProvided($sodiumException->getMessage(), 0, $sodiumException); + } + } +} diff --git a/vendor/lcobucci/jwt/src/Signer/Hmac.php b/vendor/lcobucci/jwt/src/Signer/Hmac.php new file mode 100644 index 00000000..815f84c3 --- /dev/null +++ b/vendor/lcobucci/jwt/src/Signer/Hmac.php @@ -0,0 +1,44 @@ +contents()); + $expectedKeyLength = $this->minimumBitsLengthForKey(); + + if ($actualKeyLength < $expectedKeyLength) { + throw InvalidKeyProvided::tooShort($expectedKeyLength, $actualKeyLength); + } + + return hash_hmac($this->algorithm(), $payload, $key->contents(), true); + } + + final public function verify(string $expected, string $payload, Key $key): bool + { + return hash_equals($expected, $this->sign($payload, $key)); + } + + /** + * @internal + * + * @return non-empty-string + */ + abstract public function algorithm(): string; + + /** + * @internal + * + * @return positive-int + */ + abstract public function minimumBitsLengthForKey(): int; +} diff --git a/vendor/lcobucci/jwt/src/Signer/Hmac/Sha256.php b/vendor/lcobucci/jwt/src/Signer/Hmac/Sha256.php new file mode 100644 index 00000000..e19992ec --- /dev/null +++ b/vendor/lcobucci/jwt/src/Signer/Hmac/Sha256.php @@ -0,0 +1,24 @@ +getSize(); + $contents = $fileSize > 0 ? $file->fread($file->getSize()) : ''; + assert(is_string($contents)); + + self::guardAgainstEmptyKey($contents); + + return new self($contents, $passphrase); + } + + /** @phpstan-assert non-empty-string $contents */ + private static function guardAgainstEmptyKey(string $contents): void + { + if ($contents === '') { + throw InvalidKeyProvided::cannotBeEmpty(); + } + } + + public function contents(): string + { + return $this->contents; + } + + public function passphrase(): string + { + return $this->passphrase; + } +} diff --git a/vendor/lcobucci/jwt/src/Signer/OpenSSL.php b/vendor/lcobucci/jwt/src/Signer/OpenSSL.php new file mode 100644 index 00000000..bcc7065c --- /dev/null +++ b/vendor/lcobucci/jwt/src/Signer/OpenSSL.php @@ -0,0 +1,126 @@ + 'RSA', + OPENSSL_KEYTYPE_DSA => 'DSA', + OPENSSL_KEYTYPE_DH => 'DH', + OPENSSL_KEYTYPE_EC => 'EC', + ]; + + /** + * @return non-empty-string + * + * @throws CannotSignPayload + * @throws InvalidKeyProvided + */ + final protected function createSignature( + string $pem, + string $passphrase, + string $payload, + ): string { + $key = $this->getPrivateKey($pem, $passphrase); + + $signature = ''; + + if (! openssl_sign($payload, $signature, $key, $this->algorithm())) { + throw CannotSignPayload::errorHappened($this->fullOpenSSLErrorString()); + } + + return $signature; + } + + /** @throws CannotSignPayload */ + private function getPrivateKey(string $pem, string $passphrase): OpenSSLAsymmetricKey + { + return $this->validateKey(openssl_pkey_get_private($pem, $passphrase)); + } + + /** @throws InvalidKeyProvided */ + final protected function verifySignature( + string $expected, + string $payload, + string $pem, + ): bool { + $key = $this->getPublicKey($pem); + $result = openssl_verify($payload, $expected, $key, $this->algorithm()); + + return $result === 1; + } + + /** @throws InvalidKeyProvided */ + private function getPublicKey(string $pem): OpenSSLAsymmetricKey + { + return $this->validateKey(openssl_pkey_get_public($pem)); + } + + /** + * Raises an exception when the key type is not the expected type + * + * @throws InvalidKeyProvided + */ + private function validateKey(OpenSSLAsymmetricKey|bool $key): OpenSSLAsymmetricKey + { + if (is_bool($key)) { + throw InvalidKeyProvided::cannotBeParsed($this->fullOpenSSLErrorString()); + } + + $details = openssl_pkey_get_details($key); + assert(is_array($details)); + + assert(array_key_exists('bits', $details)); + assert(is_int($details['bits'])); + assert(array_key_exists('type', $details)); + assert(is_int($details['type'])); + + $this->guardAgainstIncompatibleKey($details['type'], $details['bits']); + + return $key; + } + + private function fullOpenSSLErrorString(): string + { + $error = ''; + + while ($msg = openssl_error_string()) { + $error .= PHP_EOL . '* ' . $msg; + } + + return $error; + } + + /** @throws InvalidKeyProvided */ + abstract protected function guardAgainstIncompatibleKey(int $type, int $lengthInBits): void; + + /** + * Returns which algorithm to be used to create/verify the signature (using OpenSSL constants) + * + * @internal + */ + abstract public function algorithm(): int; +} diff --git a/vendor/lcobucci/jwt/src/Signer/Rsa.php b/vendor/lcobucci/jwt/src/Signer/Rsa.php new file mode 100644 index 00000000..ba7d72d5 --- /dev/null +++ b/vendor/lcobucci/jwt/src/Signer/Rsa.php @@ -0,0 +1,35 @@ +createSignature($key->contents(), $key->passphrase(), $payload); + } + + final public function verify(string $expected, string $payload, Key $key): bool + { + return $this->verifySignature($expected, $payload, $key->contents()); + } + + final protected function guardAgainstIncompatibleKey(int $type, int $lengthInBits): void + { + if ($type !== OPENSSL_KEYTYPE_RSA) { + throw InvalidKeyProvided::incompatibleKeyType( + self::KEY_TYPE_MAP[OPENSSL_KEYTYPE_RSA], + self::KEY_TYPE_MAP[$type], + ); + } + + if ($lengthInBits < self::MINIMUM_KEY_LENGTH) { + throw InvalidKeyProvided::tooShort(self::MINIMUM_KEY_LENGTH, $lengthInBits); + } + } +} diff --git a/vendor/lcobucci/jwt/src/Signer/Rsa/Sha256.php b/vendor/lcobucci/jwt/src/Signer/Rsa/Sha256.php new file mode 100644 index 00000000..9e56c70f --- /dev/null +++ b/vendor/lcobucci/jwt/src/Signer/Rsa/Sha256.php @@ -0,0 +1,21 @@ + */ + private array $headers = ['typ' => 'JWT', 'alg' => null]; + + /** @var array */ + private array $claims = []; + + public function __construct(private readonly Encoder $encoder, private readonly ClaimsFormatter $claimFormatter) + { + } + + public function permittedFor(string ...$audiences): BuilderInterface + { + $configured = $this->claims[RegisteredClaims::AUDIENCE] ?? []; + $toAppend = array_diff($audiences, $configured); + + return $this->setClaim(RegisteredClaims::AUDIENCE, array_merge($configured, $toAppend)); + } + + public function expiresAt(DateTimeImmutable $expiration): BuilderInterface + { + return $this->setClaim(RegisteredClaims::EXPIRATION_TIME, $expiration); + } + + public function identifiedBy(string $id): BuilderInterface + { + return $this->setClaim(RegisteredClaims::ID, $id); + } + + public function issuedAt(DateTimeImmutable $issuedAt): BuilderInterface + { + return $this->setClaim(RegisteredClaims::ISSUED_AT, $issuedAt); + } + + public function issuedBy(string $issuer): BuilderInterface + { + return $this->setClaim(RegisteredClaims::ISSUER, $issuer); + } + + public function canOnlyBeUsedAfter(DateTimeImmutable $notBefore): BuilderInterface + { + return $this->setClaim(RegisteredClaims::NOT_BEFORE, $notBefore); + } + + public function relatedTo(string $subject): BuilderInterface + { + return $this->setClaim(RegisteredClaims::SUBJECT, $subject); + } + + public function withHeader(string $name, mixed $value): BuilderInterface + { + $new = clone $this; + $new->headers[$name] = $value; + + return $new; + } + + public function withClaim(string $name, mixed $value): BuilderInterface + { + if (in_array($name, RegisteredClaims::ALL, true)) { + throw RegisteredClaimGiven::forClaim($name); + } + + return $this->setClaim($name, $value); + } + + /** @param non-empty-string $name */ + private function setClaim(string $name, mixed $value): BuilderInterface + { + $new = clone $this; + $new->claims[$name] = $value; + + return $new; + } + + /** + * @param array $items + * + * @throws CannotEncodeContent When data cannot be converted to JSON. + */ + private function encode(array $items): string + { + return $this->encoder->base64UrlEncode( + $this->encoder->jsonEncode($items), + ); + } + + public function getToken(Signer $signer, Key $key): UnencryptedToken + { + $headers = $this->headers; + $headers['alg'] = $signer->algorithmId(); + + $encodedHeaders = $this->encode($headers); + $encodedClaims = $this->encode($this->claimFormatter->formatClaims($this->claims)); + + $signature = $signer->sign($encodedHeaders . '.' . $encodedClaims, $key); + $encodedSignature = $this->encoder->base64UrlEncode($signature); + + return new Plain( + new DataSet($headers, $encodedHeaders), + new DataSet($this->claims, $encodedClaims), + new Signature($signature, $encodedSignature), + ); + } +} diff --git a/vendor/lcobucci/jwt/src/Token/DataSet.php b/vendor/lcobucci/jwt/src/Token/DataSet.php new file mode 100644 index 00000000..6c0b98ab --- /dev/null +++ b/vendor/lcobucci/jwt/src/Token/DataSet.php @@ -0,0 +1,37 @@ + $data */ + public function __construct(private readonly array $data, private readonly string $encoded) + { + } + + /** @param non-empty-string $name */ + public function get(string $name, mixed $default = null): mixed + { + return $this->data[$name] ?? $default; + } + + /** @param non-empty-string $name */ + public function has(string $name): bool + { + return array_key_exists($name, $this->data); + } + + /** @return array */ + public function all(): array + { + return $this->data; + } + + public function toString(): string + { + return $this->encoded; + } +} diff --git a/vendor/lcobucci/jwt/src/Token/InvalidTokenStructure.php b/vendor/lcobucci/jwt/src/Token/InvalidTokenStructure.php new file mode 100644 index 00000000..abba344b --- /dev/null +++ b/vendor/lcobucci/jwt/src/Token/InvalidTokenStructure.php @@ -0,0 +1,41 @@ +splitJwt($jwt); + + if ($encodedHeaders === '') { + throw InvalidTokenStructure::missingHeaderPart(); + } + + if ($encodedClaims === '') { + throw InvalidTokenStructure::missingClaimsPart(); + } + + if ($encodedSignature === '') { + throw InvalidTokenStructure::missingSignaturePart(); + } + + $header = $this->parseHeader($encodedHeaders); + + return new Plain( + new DataSet($header, $encodedHeaders), + new DataSet($this->parseClaims($encodedClaims), $encodedClaims), + $this->parseSignature($encodedSignature), + ); + } + + /** + * Splits the JWT string into an array + * + * @param non-empty-string $jwt + * + * @return string[] + * + * @throws InvalidTokenStructure When JWT doesn't have all parts. + */ + private function splitJwt(string $jwt): array + { + $data = explode('.', $jwt); + + if (count($data) !== 3) { + throw InvalidTokenStructure::missingOrNotEnoughSeparators(); + } + + return $data; + } + + /** + * Parses the header from a string + * + * @param non-empty-string $data + * + * @return array + * + * @throws UnsupportedHeaderFound When an invalid header is informed. + * @throws InvalidTokenStructure When parsed content isn't an array. + */ + private function parseHeader(string $data): array + { + $header = $this->decoder->jsonDecode($this->decoder->base64UrlDecode($data)); + + if (! is_array($header)) { + throw InvalidTokenStructure::arrayExpected('headers'); + } + + $this->guardAgainstEmptyStringKeys($header, 'headers'); + + if (array_key_exists('enc', $header)) { + throw UnsupportedHeaderFound::encryption(); + } + + if (! array_key_exists('typ', $header)) { + $header['typ'] = 'JWT'; + } + + return $header; + } + + /** + * Parses the claim set from a string + * + * @param non-empty-string $data + * + * @return array + * + * @throws InvalidTokenStructure When parsed content isn't an array or contains non-parseable dates. + */ + private function parseClaims(string $data): array + { + $claims = $this->decoder->jsonDecode($this->decoder->base64UrlDecode($data)); + + if (! is_array($claims)) { + throw InvalidTokenStructure::arrayExpected('claims'); + } + + $this->guardAgainstEmptyStringKeys($claims, 'claims'); + + if (array_key_exists(RegisteredClaims::AUDIENCE, $claims)) { + $claims[RegisteredClaims::AUDIENCE] = (array) $claims[RegisteredClaims::AUDIENCE]; + } + + foreach (RegisteredClaims::DATE_CLAIMS as $claim) { + if (! array_key_exists($claim, $claims)) { + continue; + } + + $claims[$claim] = $this->convertDate($claims[$claim]); + } + + return $claims; + } + + /** + * @param array $array + * @param non-empty-string $part + * + * @phpstan-assert array $array + */ + private function guardAgainstEmptyStringKeys(array $array, string $part): void + { + foreach ($array as $key => $value) { + if ($key === '') { + throw InvalidTokenStructure::arrayExpected($part); + } + } + } + + /** @throws InvalidTokenStructure */ + private function convertDate(int|float|string $timestamp): DateTimeImmutable + { + if (! is_numeric($timestamp)) { + throw InvalidTokenStructure::dateIsNotParseable($timestamp); + } + + $normalizedTimestamp = number_format((float) $timestamp, self::MICROSECOND_PRECISION, '.', ''); + + $date = DateTimeImmutable::createFromFormat('U.u', $normalizedTimestamp); + + if ($date === false) { + throw InvalidTokenStructure::dateIsNotParseable($normalizedTimestamp); + } + + return $date; + } + + /** + * Returns the signature from given data + * + * @param non-empty-string $data + */ + private function parseSignature(string $data): Signature + { + $hash = $this->decoder->base64UrlDecode($data); + + return new Signature($hash, $data); + } +} diff --git a/vendor/lcobucci/jwt/src/Token/Plain.php b/vendor/lcobucci/jwt/src/Token/Plain.php new file mode 100644 index 00000000..6af388d5 --- /dev/null +++ b/vendor/lcobucci/jwt/src/Token/Plain.php @@ -0,0 +1,85 @@ +headers; + } + + public function claims(): DataSet + { + return $this->claims; + } + + public function signature(): Signature + { + return $this->signature; + } + + public function payload(): string + { + return $this->headers->toString() . '.' . $this->claims->toString(); + } + + public function isPermittedFor(string $audience): bool + { + return in_array($audience, $this->claims->get(RegisteredClaims::AUDIENCE, []), true); + } + + public function isIdentifiedBy(string $id): bool + { + return $this->claims->get(RegisteredClaims::ID) === $id; + } + + public function isRelatedTo(string $subject): bool + { + return $this->claims->get(RegisteredClaims::SUBJECT) === $subject; + } + + public function hasBeenIssuedBy(string ...$issuers): bool + { + return in_array($this->claims->get(RegisteredClaims::ISSUER), $issuers, true); + } + + public function hasBeenIssuedBefore(DateTimeInterface $now): bool + { + return $now >= $this->claims->get(RegisteredClaims::ISSUED_AT); + } + + public function isMinimumTimeBefore(DateTimeInterface $now): bool + { + return $now >= $this->claims->get(RegisteredClaims::NOT_BEFORE); + } + + public function isExpired(DateTimeInterface $now): bool + { + if (! $this->claims->has(RegisteredClaims::EXPIRATION_TIME)) { + return false; + } + + return $now >= $this->claims->get(RegisteredClaims::EXPIRATION_TIME); + } + + public function toString(): string + { + return $this->headers->toString() . '.' + . $this->claims->toString() . '.' + . $this->signature->toString(); + } +} diff --git a/vendor/lcobucci/jwt/src/Token/RegisteredClaimGiven.php b/vendor/lcobucci/jwt/src/Token/RegisteredClaimGiven.php new file mode 100644 index 00000000..ce40a6ab --- /dev/null +++ b/vendor/lcobucci/jwt/src/Token/RegisteredClaimGiven.php @@ -0,0 +1,21 @@ +hash; + } + + /** + * Returns the encoded version of the signature + * + * @return non-empty-string + */ + public function toString(): string + { + return $this->encoded; + } +} diff --git a/vendor/lcobucci/jwt/src/Token/UnsupportedHeaderFound.php b/vendor/lcobucci/jwt/src/Token/UnsupportedHeaderFound.php new file mode 100644 index 00000000..18240784 --- /dev/null +++ b/vendor/lcobucci/jwt/src/Token/UnsupportedHeaderFound.php @@ -0,0 +1,15 @@ +claims(); + + if (! $claims->has($this->claim)) { + throw ConstraintViolation::error('The token does not have the claim "' . $this->claim . '"', $this); + } + } +} diff --git a/vendor/lcobucci/jwt/src/Validation/Constraint/HasClaimWithValue.php b/vendor/lcobucci/jwt/src/Validation/Constraint/HasClaimWithValue.php new file mode 100644 index 00000000..d3ba1d6e --- /dev/null +++ b/vendor/lcobucci/jwt/src/Validation/Constraint/HasClaimWithValue.php @@ -0,0 +1,42 @@ +claims(); + + if (! $claims->has($this->claim)) { + throw ConstraintViolation::error('The token does not have the claim "' . $this->claim . '"', $this); + } + + if ($claims->get($this->claim) !== $this->expectedValue) { + throw ConstraintViolation::error( + 'The claim "' . $this->claim . '" does not have the expected value', + $this, + ); + } + } +} diff --git a/vendor/lcobucci/jwt/src/Validation/Constraint/IdentifiedBy.php b/vendor/lcobucci/jwt/src/Validation/Constraint/IdentifiedBy.php new file mode 100644 index 00000000..44541a75 --- /dev/null +++ b/vendor/lcobucci/jwt/src/Validation/Constraint/IdentifiedBy.php @@ -0,0 +1,26 @@ +isIdentifiedBy($this->id)) { + throw ConstraintViolation::error( + 'The token is not identified with the expected ID', + $this, + ); + } + } +} diff --git a/vendor/lcobucci/jwt/src/Validation/Constraint/IssuedBy.php b/vendor/lcobucci/jwt/src/Validation/Constraint/IssuedBy.php new file mode 100644 index 00000000..8ba3890d --- /dev/null +++ b/vendor/lcobucci/jwt/src/Validation/Constraint/IssuedBy.php @@ -0,0 +1,30 @@ +issuers = $issuers; + } + + public function assert(Token $token): void + { + if (! $token->hasBeenIssuedBy(...$this->issuers)) { + throw ConstraintViolation::error( + 'The token was not issued by the given issuers', + $this, + ); + } + } +} diff --git a/vendor/lcobucci/jwt/src/Validation/Constraint/LeewayCannotBeNegative.php b/vendor/lcobucci/jwt/src/Validation/Constraint/LeewayCannotBeNegative.php new file mode 100644 index 00000000..53abc0d4 --- /dev/null +++ b/vendor/lcobucci/jwt/src/Validation/Constraint/LeewayCannotBeNegative.php @@ -0,0 +1,15 @@ +leeway = $this->guardLeeway($leeway); + } + + private function guardLeeway(?DateInterval $leeway): DateInterval + { + if ($leeway === null) { + return new DateInterval('PT0S'); + } + + if ($leeway->invert === 1) { + throw LeewayCannotBeNegative::create(); + } + + return $leeway; + } + + public function assert(Token $token): void + { + $now = $this->clock->now(); + + $this->assertIssueTime($token, $now->add($this->leeway)); + $this->assertMinimumTime($token, $now->add($this->leeway)); + $this->assertExpiration($token, $now->sub($this->leeway)); + } + + /** @throws ConstraintViolation */ + private function assertExpiration(Token $token, DateTimeInterface $now): void + { + if ($token->isExpired($now)) { + throw ConstraintViolation::error('The token is expired', $this); + } + } + + /** @throws ConstraintViolation */ + private function assertMinimumTime(Token $token, DateTimeInterface $now): void + { + if (! $token->isMinimumTimeBefore($now)) { + throw ConstraintViolation::error('The token cannot be used yet', $this); + } + } + + /** @throws ConstraintViolation */ + private function assertIssueTime(Token $token, DateTimeInterface $now): void + { + if (! $token->hasBeenIssuedBefore($now)) { + throw ConstraintViolation::error('The token was issued in the future', $this); + } + } +} diff --git a/vendor/lcobucci/jwt/src/Validation/Constraint/PermittedFor.php b/vendor/lcobucci/jwt/src/Validation/Constraint/PermittedFor.php new file mode 100644 index 00000000..48544c9a --- /dev/null +++ b/vendor/lcobucci/jwt/src/Validation/Constraint/PermittedFor.php @@ -0,0 +1,26 @@ +isPermittedFor($this->audience)) { + throw ConstraintViolation::error( + 'The token is not allowed to be used by this audience', + $this, + ); + } + } +} diff --git a/vendor/lcobucci/jwt/src/Validation/Constraint/RelatedTo.php b/vendor/lcobucci/jwt/src/Validation/Constraint/RelatedTo.php new file mode 100644 index 00000000..16493623 --- /dev/null +++ b/vendor/lcobucci/jwt/src/Validation/Constraint/RelatedTo.php @@ -0,0 +1,26 @@ +isRelatedTo($this->subject)) { + throw ConstraintViolation::error( + 'The token is not related to the expected subject', + $this, + ); + } + } +} diff --git a/vendor/lcobucci/jwt/src/Validation/Constraint/SignedWith.php b/vendor/lcobucci/jwt/src/Validation/Constraint/SignedWith.php new file mode 100644 index 00000000..5c8e2656 --- /dev/null +++ b/vendor/lcobucci/jwt/src/Validation/Constraint/SignedWith.php @@ -0,0 +1,32 @@ +headers()->get('alg') !== $this->signer->algorithmId()) { + throw ConstraintViolation::error('Token signer mismatch', $this); + } + + if (! $this->signer->verify($token->signature()->hash(), $token->payload(), $this->key)) { + throw ConstraintViolation::error('Token signature mismatch', $this); + } + } +} diff --git a/vendor/lcobucci/jwt/src/Validation/Constraint/SignedWithOneInSet.php b/vendor/lcobucci/jwt/src/Validation/Constraint/SignedWithOneInSet.php new file mode 100644 index 00000000..fb542fb3 --- /dev/null +++ b/vendor/lcobucci/jwt/src/Validation/Constraint/SignedWithOneInSet.php @@ -0,0 +1,38 @@ + */ + private readonly array $constraints; + + public function __construct(SignedWithUntilDate ...$constraints) + { + $this->constraints = $constraints; + } + + public function assert(Token $token): void + { + $errorMessage = 'It was not possible to verify the signature of the token, reasons:'; + + foreach ($this->constraints as $constraint) { + try { + $constraint->assert($token); + + return; + } catch (ConstraintViolation $violation) { + $errorMessage .= PHP_EOL . '- ' . $violation->getMessage(); + } + } + + throw ConstraintViolation::error($errorMessage, $this); + } +} diff --git a/vendor/lcobucci/jwt/src/Validation/Constraint/SignedWithUntilDate.php b/vendor/lcobucci/jwt/src/Validation/Constraint/SignedWithUntilDate.php new file mode 100644 index 00000000..85429e89 --- /dev/null +++ b/vendor/lcobucci/jwt/src/Validation/Constraint/SignedWithUntilDate.php @@ -0,0 +1,47 @@ +verifySignature = new SignedWith($signer, $key); + + $this->clock = $clock ?? new class () implements ClockInterface { + public function now(): DateTimeImmutable + { + return new DateTimeImmutable(); + } + }; + } + + public function assert(Token $token): void + { + if ($this->validUntil < $this->clock->now()) { + throw ConstraintViolation::error( + 'This constraint was only usable until ' + . $this->validUntil->format(DateTimeInterface::RFC3339), + $this, + ); + } + + $this->verifySignature->assert($token); + } +} diff --git a/vendor/lcobucci/jwt/src/Validation/Constraint/StrictValidAt.php b/vendor/lcobucci/jwt/src/Validation/Constraint/StrictValidAt.php new file mode 100644 index 00000000..93db0a3a --- /dev/null +++ b/vendor/lcobucci/jwt/src/Validation/Constraint/StrictValidAt.php @@ -0,0 +1,84 @@ +leeway = $this->guardLeeway($leeway); + } + + private function guardLeeway(?DateInterval $leeway): DateInterval + { + if ($leeway === null) { + return new DateInterval('PT0S'); + } + + if ($leeway->invert === 1) { + throw LeewayCannotBeNegative::create(); + } + + return $leeway; + } + + public function assert(Token $token): void + { + if (! $token instanceof UnencryptedToken) { + throw ConstraintViolation::error('You should pass a plain token', $this); + } + + $now = $this->clock->now(); + + $this->assertIssueTime($token, $now->add($this->leeway)); + $this->assertMinimumTime($token, $now->add($this->leeway)); + $this->assertExpiration($token, $now->sub($this->leeway)); + } + + /** @throws ConstraintViolation */ + private function assertExpiration(UnencryptedToken $token, DateTimeInterface $now): void + { + if (! $token->claims()->has(Token\RegisteredClaims::EXPIRATION_TIME)) { + throw ConstraintViolation::error('"Expiration Time" claim missing', $this); + } + + if ($token->isExpired($now)) { + throw ConstraintViolation::error('The token is expired', $this); + } + } + + /** @throws ConstraintViolation */ + private function assertMinimumTime(UnencryptedToken $token, DateTimeInterface $now): void + { + if (! $token->claims()->has(Token\RegisteredClaims::NOT_BEFORE)) { + throw ConstraintViolation::error('"Not Before" claim missing', $this); + } + + if (! $token->isMinimumTimeBefore($now)) { + throw ConstraintViolation::error('The token cannot be used yet', $this); + } + } + + /** @throws ConstraintViolation */ + private function assertIssueTime(UnencryptedToken $token, DateTimeInterface $now): void + { + if (! $token->claims()->has(Token\RegisteredClaims::ISSUED_AT)) { + throw ConstraintViolation::error('"Issued At" claim missing', $this); + } + + if (! $token->hasBeenIssuedBefore($now)) { + throw ConstraintViolation::error('The token was issued in the future', $this); + } + } +} diff --git a/vendor/lcobucci/jwt/src/Validation/ConstraintViolation.php b/vendor/lcobucci/jwt/src/Validation/ConstraintViolation.php new file mode 100644 index 00000000..17c75468 --- /dev/null +++ b/vendor/lcobucci/jwt/src/Validation/ConstraintViolation.php @@ -0,0 +1,24 @@ +|null $constraint */ + public function __construct( + string $message = '', + public readonly ?string $constraint = null, + ) { + parent::__construct($message); + } + + /** @param non-empty-string $message */ + public static function error(string $message, Constraint $constraint): self + { + return new self(message: $message, constraint: $constraint::class); + } +} diff --git a/vendor/lcobucci/jwt/src/Validation/NoConstraintsGiven.php b/vendor/lcobucci/jwt/src/Validation/NoConstraintsGiven.php new file mode 100644 index 00000000..0ef80d25 --- /dev/null +++ b/vendor/lcobucci/jwt/src/Validation/NoConstraintsGiven.php @@ -0,0 +1,11 @@ +getMessage(); + }, + $violations, + ); + + $message = "The token violates some mandatory constraints, details:\n"; + $message .= implode("\n", $violations); + + return $message; + } + + /** @return ConstraintViolation[] */ + public function violations(): array + { + return $this->violations; + } +} diff --git a/vendor/lcobucci/jwt/src/Validation/SignedWith.php b/vendor/lcobucci/jwt/src/Validation/SignedWith.php new file mode 100644 index 00000000..e721095f --- /dev/null +++ b/vendor/lcobucci/jwt/src/Validation/SignedWith.php @@ -0,0 +1,8 @@ +checkConstraint($constraint, $token, $violations); + } + + if ($violations) { + throw RequiredConstraintsViolated::fromViolations(...$violations); + } + } + + /** @param ConstraintViolation[] $violations */ + private function checkConstraint( + Constraint $constraint, + Token $token, + array &$violations, + ): void { + try { + $constraint->assert($token); + } catch (ConstraintViolation $e) { + $violations[] = $e; + } + } + + public function validate(Token $token, Constraint ...$constraints): bool + { + if ($constraints === []) { + throw new NoConstraintsGiven('No constraint given.'); + } + + try { + foreach ($constraints as $constraint) { + $constraint->assert($token); + } + + return true; + } catch (ConstraintViolation) { + return false; + } + } +} diff --git a/vendor/lcobucci/jwt/src/Validator.php b/vendor/lcobucci/jwt/src/Validator.php new file mode 100644 index 00000000..d0ce4b8c --- /dev/null +++ b/vendor/lcobucci/jwt/src/Validator.php @@ -0,0 +1,20 @@ +clock = $clock; + } + + public function doSomething() + { + /** @var DateTimeImmutable $currentDateAndTime */ + $currentDateAndTime = $this->clock->now(); + // do something useful with that information + } +} +``` + +You can then pick one of the [implementations][implementation-url] of the interface to get a clock. + +If you want to implement the interface, you can require this package and +implement `Psr\Clock\ClockInterface` in your code. + +Don't forget to add `psr/clock-implementation` to your `composer.json`s `provides`-section like this: + +```json +{ + "provides": { + "psr/clock-implementation": "1.0" + } +} +``` + +And please read the [specification text][specification-url] for details on the interface. + +[psr-url]: https://www.php-fig.org/psr/psr-20 +[package-url]: https://packagist.org/packages/psr/clock +[implementation-url]: https://packagist.org/providers/psr/clock-implementation +[specification-url]: https://github.com/php-fig/fig-standards/blob/master/proposed/clock.md diff --git a/vendor/psr/clock/composer.json b/vendor/psr/clock/composer.json new file mode 100644 index 00000000..77992eda --- /dev/null +++ b/vendor/psr/clock/composer.json @@ -0,0 +1,21 @@ +{ + "name": "psr/clock", + "description": "Common interface for reading the clock.", + "keywords": ["psr", "psr-20", "time", "clock", "now"], + "homepage": "https://github.com/php-fig/clock", + "license": "MIT", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "require": { + "php": "^7.0 || ^8.0" + }, + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + } +} diff --git a/vendor/psr/clock/src/ClockInterface.php b/vendor/psr/clock/src/ClockInterface.php new file mode 100644 index 00000000..7b6d8d8a --- /dev/null +++ b/vendor/psr/clock/src/ClockInterface.php @@ -0,0 +1,13 @@ +