CMS 2.0 - Theme engine, logging, admin improvements

Major changes:
- New ThemeManager with Twig templating and SCSS compilation
- Dynamic themes system (themes/default, themes/demo)
- LogManager with SQLite storage and syslog forwarding
- RequestLogger with static helper methods
- Admin UI overhaul (Bootstrap 5, dark mode)
- Admin config page with logging and theme settings
- Admin logs page with filters and search
- Removed legacy Mustache templates
- Removed test plugin and theme
- Composer dependencies: Twig, scssphp, CommonMark, MaxMind GeoIP
This commit is contained in:
2026-08-08 18:02:14 +02:00
parent cc0e4c19c8
commit a1e5baacac
833 changed files with 108974 additions and 1386 deletions
+6
View File
@@ -7,5 +7,11 @@ $baseDir = dirname($vendorDir);
return array(
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'89efb1254ef2d1c5d80096acd12c4098' => $vendorDir . '/twig/twig/src/Resources/core.php',
'ffecb95d45175fd40f75be8a23b34f90' => $vendorDir . '/twig/twig/src/Resources/debug.php',
'c7baa00073ee9c61edf148c51917cfb4' => $vendorDir . '/twig/twig/src/Resources/escaper.php',
'f844ccf1d25df8663951193c3fc307c8' => $vendorDir . '/twig/twig/src/Resources/string_loader.php',
);
+13
View File
@@ -6,14 +6,27 @@ $vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Twig\\' => array($vendorDir . '/twig/twig/src'),
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),
'Symfony\\Component\\Filesystem\\' => array($vendorDir . '/symfony/filesystem'),
'SourceSpan\\' => array($vendorDir . '/scssphp/source-span/src'),
'ScssPhp\\ScssPhp\\' => array($vendorDir . '/scssphp/scssphp/src'),
'Psr\\Log\\' => array($vendorDir . '/psr/log/src'),
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
'Psr\\EventDispatcher\\' => array($vendorDir . '/psr/event-dispatcher/src'),
'PhpMqtt\\Client\\' => array($vendorDir . '/php-mqtt/client/src'),
'Nette\\' => array($vendorDir . '/nette/schema/src', $vendorDir . '/nette/utils/src'),
'MyCLabs\\Enum\\' => array($vendorDir . '/myclabs/php-enum/src'),
'Mustache\\' => array($vendorDir . '/mustache/mustache/src'),
'MaxMind\\WebService\\' => array($vendorDir . '/maxmind/web-service-common/src/WebService'),
'MaxMind\\Exception\\' => array($vendorDir . '/maxmind/web-service-common/src/Exception'),
'MaxMind\\Db\\' => array($vendorDir . '/maxmind-db/reader/src/MaxMind/Db'),
'League\\Uri\\' => array($vendorDir . '/league/uri', $vendorDir . '/league/uri-interfaces'),
'League\\Config\\' => array($vendorDir . '/league/config/src'),
'League\\CommonMark\\' => array($vendorDir . '/league/commonmark/src'),
'GeoIp2\\' => array($vendorDir . '/geoip2/geoip2/src'),
'Dflydev\\DotAccessData\\' => array($vendorDir . '/dflydev/dot-access-data/src'),
'Composer\\CaBundle\\' => array($vendorDir . '/composer/ca-bundle/src'),
);
+82
View File
@@ -8,17 +8,33 @@ class ComposerStaticInit071586d19f5409de22b3235d85d8476c
{
public static $files = array (
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
'89efb1254ef2d1c5d80096acd12c4098' => __DIR__ . '/..' . '/twig/twig/src/Resources/core.php',
'ffecb95d45175fd40f75be8a23b34f90' => __DIR__ . '/..' . '/twig/twig/src/Resources/debug.php',
'c7baa00073ee9c61edf148c51917cfb4' => __DIR__ . '/..' . '/twig/twig/src/Resources/escaper.php',
'f844ccf1d25df8663951193c3fc307c8' => __DIR__ . '/..' . '/twig/twig/src/Resources/string_loader.php',
);
public static $prefixLengthsPsr4 = array (
'T' =>
array (
'Twig\\' => 5,
),
'S' =>
array (
'Symfony\\Polyfill\\Php80\\' => 23,
'Symfony\\Polyfill\\Mbstring\\' => 26,
'Symfony\\Polyfill\\Ctype\\' => 23,
'Symfony\\Component\\Filesystem\\' => 29,
'SourceSpan\\' => 11,
'ScssPhp\\ScssPhp\\' => 16,
),
'P' =>
array (
'Psr\\Log\\' => 8,
'Psr\\Http\\Message\\' => 17,
'Psr\\EventDispatcher\\' => 20,
'PhpMqtt\\Client\\' => 15,
),
@@ -30,27 +46,68 @@ class ComposerStaticInit071586d19f5409de22b3235d85d8476c
array (
'MyCLabs\\Enum\\' => 13,
'Mustache\\' => 9,
'MaxMind\\WebService\\' => 19,
'MaxMind\\Exception\\' => 18,
'MaxMind\\Db\\' => 11,
),
'L' =>
array (
'League\\Uri\\' => 11,
'League\\Config\\' => 14,
'League\\CommonMark\\' => 18,
),
'G' =>
array (
'GeoIp2\\' => 7,
),
'D' =>
array (
'Dflydev\\DotAccessData\\' => 22,
),
'C' =>
array (
'Composer\\CaBundle\\' => 18,
),
);
public static $prefixDirsPsr4 = array (
'Twig\\' =>
array (
0 => __DIR__ . '/..' . '/twig/twig/src',
),
'Symfony\\Polyfill\\Php80\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
),
'Symfony\\Polyfill\\Mbstring\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
),
'Symfony\\Polyfill\\Ctype\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-ctype',
),
'Symfony\\Component\\Filesystem\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/filesystem',
),
'SourceSpan\\' =>
array (
0 => __DIR__ . '/..' . '/scssphp/source-span/src',
),
'ScssPhp\\ScssPhp\\' =>
array (
0 => __DIR__ . '/..' . '/scssphp/scssphp/src',
),
'Psr\\Log\\' =>
array (
0 => __DIR__ . '/..' . '/psr/log/src',
),
'Psr\\Http\\Message\\' =>
array (
0 => __DIR__ . '/..' . '/psr/http-factory/src',
1 => __DIR__ . '/..' . '/psr/http-message/src',
),
'Psr\\EventDispatcher\\' =>
array (
0 => __DIR__ . '/..' . '/psr/event-dispatcher/src',
@@ -72,6 +129,23 @@ class ComposerStaticInit071586d19f5409de22b3235d85d8476c
array (
0 => __DIR__ . '/..' . '/mustache/mustache/src',
),
'MaxMind\\WebService\\' =>
array (
0 => __DIR__ . '/..' . '/maxmind/web-service-common/src/WebService',
),
'MaxMind\\Exception\\' =>
array (
0 => __DIR__ . '/..' . '/maxmind/web-service-common/src/Exception',
),
'MaxMind\\Db\\' =>
array (
0 => __DIR__ . '/..' . '/maxmind-db/reader/src/MaxMind/Db',
),
'League\\Uri\\' =>
array (
0 => __DIR__ . '/..' . '/league/uri',
1 => __DIR__ . '/..' . '/league/uri-interfaces',
),
'League\\Config\\' =>
array (
0 => __DIR__ . '/..' . '/league/config/src',
@@ -80,10 +154,18 @@ class ComposerStaticInit071586d19f5409de22b3235d85d8476c
array (
0 => __DIR__ . '/..' . '/league/commonmark/src',
),
'GeoIp2\\' =>
array (
0 => __DIR__ . '/..' . '/geoip2/geoip2/src',
),
'Dflydev\\DotAccessData\\' =>
array (
0 => __DIR__ . '/..' . '/dflydev/dot-access-data/src',
),
'Composer\\CaBundle\\' =>
array (
0 => __DIR__ . '/..' . '/composer/ca-bundle/src',
),
);
public static $classMap = array (
+19
View File
@@ -0,0 +1,19 @@
Copyright (C) 2016 Composer
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+85
View File
@@ -0,0 +1,85 @@
composer/ca-bundle
==================
Small utility library that lets you find a path to the system CA bundle,
and includes a fallback to the Mozilla CA bundle.
Originally written as part of [composer/composer](https://github.com/composer/composer),
now extracted and made available as a stand-alone library.
Installation
------------
Install the latest version with:
```bash
$ composer require composer/ca-bundle
```
Requirements
------------
* PHP 5.3.2 is required but using the latest version of PHP is highly recommended.
Basic usage
-----------
### `Composer\CaBundle\CaBundle`
- `CaBundle::getSystemCaRootBundlePath()`: Returns the system CA bundle path, or a path to the bundled one as fallback
- `CaBundle::getBundledCaBundlePath()`: Returns the path to the bundled CA file
- `CaBundle::validateCaFile($filename)`: Validates a CA file using openssl_x509_parse only if it is safe to use
- `CaBundle::isOpensslParseSafe()`: Test if it is safe to use the PHP function openssl_x509_parse()
- `CaBundle::reset()`: Resets the static caches
#### To use with curl
```php
$curl = curl_init("https://example.org/");
$caPathOrFile = \Composer\CaBundle\CaBundle::getSystemCaRootBundlePath();
if (is_dir($caPathOrFile)) {
curl_setopt($curl, CURLOPT_CAPATH, $caPathOrFile);
} else {
curl_setopt($curl, CURLOPT_CAINFO, $caPathOrFile);
}
$result = curl_exec($curl);
```
#### To use with php streams
```php
$opts = array(
'http' => array(
'method' => "GET"
)
);
$caPathOrFile = \Composer\CaBundle\CaBundle::getSystemCaRootBundlePath();
if (is_dir($caPathOrFile)) {
$opts['ssl']['capath'] = $caPathOrFile;
} else {
$opts['ssl']['cafile'] = $caPathOrFile;
}
$context = stream_context_create($opts);
$result = file_get_contents('https://example.com', false, $context);
```
#### To use with Guzzle
```php
$client = new \GuzzleHttp\Client([
\GuzzleHttp\RequestOptions::VERIFY => \Composer\CaBundle\CaBundle::getSystemCaRootBundlePath()
]);
```
License
-------
composer/ca-bundle is licensed under the MIT License, see the LICENSE file for details.
+54
View File
@@ -0,0 +1,54 @@
{
"name": "composer/ca-bundle",
"description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.",
"type": "library",
"license": "MIT",
"keywords": [
"cabundle",
"cacert",
"certificate",
"ssl",
"tls"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http://seld.be"
}
],
"support": {
"irc": "irc://irc.freenode.org/composer",
"issues": "https://github.com/composer/ca-bundle/issues"
},
"require": {
"ext-openssl": "*",
"ext-pcre": "*",
"php": "^7.2 || ^8.0"
},
"require-dev": {
"phpunit/phpunit": "^8 || ^9",
"phpstan/phpstan": "^1.10",
"psr/log": "^1.0 || ^2.0 || ^3.0",
"symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0"
},
"autoload": {
"psr-4": {
"Composer\\CaBundle\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Composer\\CaBundle\\": "tests"
}
},
"extra": {
"branch-alias": {
"dev-main": "1.x-dev"
}
},
"scripts": {
"test": "@php phpunit",
"phpstan": "@php phpstan analyse"
}
}
File diff suppressed because it is too large Load Diff
+322
View File
@@ -0,0 +1,322 @@
<?php
/*
* This file is part of composer/ca-bundle.
*
* (c) Composer <https://github.com/composer>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Composer\CaBundle;
use Psr\Log\LoggerInterface;
use Symfony\Component\Process\PhpProcess;
/**
* @author Chris Smith <chris@cs278.org>
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class CaBundle
{
/** @var string|null */
private static $caPath;
/** @var array<string, bool> */
private static $caFileValidity = array();
/**
* Returns the system CA bundle path, or a path to the bundled one
*
* This method was adapted from Sslurp.
* https://github.com/EvanDotPro/Sslurp
*
* (c) Evan Coury <me@evancoury.com>
*
* For the full copyright and license information, please see below:
*
* Copyright (c) 2013, Evan Coury
* 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.
*
* 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 OWNER 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.
*
* @param LoggerInterface $logger optional logger for information about which CA files were loaded
* @return string path to a CA bundle file or directory
*/
public static function getSystemCaRootBundlePath(?LoggerInterface $logger = null)
{
if (self::$caPath !== null) {
return self::$caPath;
}
$caBundlePaths = array();
// If SSL_CERT_FILE env variable points to a valid certificate/bundle, use that.
// This mimics how OpenSSL uses the SSL_CERT_FILE env variable.
$caBundlePaths[] = self::getEnvVariable('SSL_CERT_FILE');
// If SSL_CERT_DIR env variable points to a valid certificate/bundle, use that.
// This mimics how OpenSSL uses the SSL_CERT_FILE env variable.
$caBundlePaths[] = self::getEnvVariable('SSL_CERT_DIR');
$caBundlePaths[] = ini_get('openssl.cafile');
$caBundlePaths[] = ini_get('openssl.capath');
$otherLocations = array(
'/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem', // Fedora, RHEL, CentOS (ca-certificates package) - NEW
'/etc/pki/tls/certs/ca-bundle.crt', // Fedora, RHEL, CentOS (ca-certificates package) - Deprecated
'/etc/ssl/certs/ca-certificates.crt', // Debian, Ubuntu, Gentoo, Arch Linux (ca-certificates package)
'/etc/ssl/ca-bundle.pem', // SUSE, openSUSE (ca-certificates package)
'/usr/ssl/certs/ca-bundle.crt', // Cygwin
'/opt/local/share/curl/curl-ca-bundle.crt', // OS X macports, curl-ca-bundle package
'/usr/local/share/curl/curl-ca-bundle.crt', // Default cURL CA bunde path (without --with-ca-bundle option)
'/usr/share/ssl/certs/ca-bundle.crt', // Really old RedHat?
'/etc/ssl/cert.pem', // OpenBSD
'/usr/local/etc/openssl/cert.pem', // OS X homebrew, openssl package
'/usr/local/etc/openssl@1.1/cert.pem', // OS X homebrew, openssl@1.1 package
'/opt/homebrew/etc/openssl@3/cert.pem', // macOS silicon homebrew, openssl@3 package
'/opt/homebrew/etc/openssl@1.1/cert.pem', // macOS silicon homebrew, openssl@1.1 package
'/etc/pki/tls/certs',
'/etc/ssl/certs', // FreeBSD
);
$caBundlePaths = array_merge($caBundlePaths, $otherLocations);
foreach ($caBundlePaths as $caBundle) {
if ($caBundle && self::caFileUsable($caBundle, $logger)) {
return self::$caPath = $caBundle;
}
if ($caBundle && self::caDirUsable($caBundle, $logger)) {
return self::$caPath = $caBundle;
}
}
return self::$caPath = static::getBundledCaBundlePath(); // Bundled CA file, last resort
}
/**
* Returns the path to the bundled CA file
*
* In case you don't want to trust the user or the system, you can use this directly
*
* @return string path to a CA bundle file
*/
public static function getBundledCaBundlePath()
{
$caBundleFile = __DIR__.'/../res/cacert.pem';
// cURL does not understand 'phar://' paths
// see https://github.com/composer/ca-bundle/issues/10
if (0 === strpos($caBundleFile, 'phar://')) {
$tempCaBundleFile = tempnam(sys_get_temp_dir(), 'openssl-ca-bundle-');
if (false === $tempCaBundleFile) {
throw new \RuntimeException('Could not create a temporary file to store the bundled CA file');
}
file_put_contents(
$tempCaBundleFile,
file_get_contents($caBundleFile)
);
register_shutdown_function(function() use ($tempCaBundleFile) {
@unlink($tempCaBundleFile);
});
$caBundleFile = $tempCaBundleFile;
}
return $caBundleFile;
}
/**
* Validates a CA file using opensl_x509_parse only if it is safe to use
*
* @param string $filename
* @param LoggerInterface $logger optional logger for information about which CA files were loaded
*
* @return bool
*/
public static function validateCaFile($filename, ?LoggerInterface $logger = null)
{
static $warned = false;
if (isset(self::$caFileValidity[$filename])) {
return self::$caFileValidity[$filename];
}
$contents = file_get_contents($filename);
if (is_string($contents) && strlen($contents) > 0) {
$contents = preg_replace("/^(\\-+(?:BEGIN|END))\\s+TRUSTED\\s+(CERTIFICATE\\-+)\$/m", '$1 $2', $contents);
if (null === $contents) {
// regex extraction failed
$isValid = false;
} else {
$isValid = (bool) openssl_x509_parse($contents);
}
} else {
$isValid = false;
}
if ($logger) {
$logger->debug('Checked CA file '.realpath($filename).': '.($isValid ? 'valid' : 'invalid'));
}
return self::$caFileValidity[$filename] = $isValid;
}
/**
* Test if it is safe to use the PHP function openssl_x509_parse().
*
* This checks if OpenSSL extensions is vulnerable to remote code execution
* via the exploit documented as CVE-2013-6420.
*
* @return bool
*/
public static function isOpensslParseSafe()
{
return true;
}
/**
* Resets the static caches
* @return void
*/
public static function reset()
{
self::$caFileValidity = array();
self::$caPath = null;
}
/**
* @param string $name
* @return string|false
*/
private static function getEnvVariable($name)
{
if (isset($_SERVER[$name])) {
return (string) $_SERVER[$name];
}
if (PHP_SAPI === 'cli' && ($value = getenv($name)) !== false && $value !== null) {
return (string) $value;
}
return false;
}
/**
* @param string|false $certFile
* @param LoggerInterface|null $logger
* @return bool
*/
private static function caFileUsable($certFile, ?LoggerInterface $logger = null)
{
return $certFile
&& self::isFile($certFile, $logger)
&& self::isReadable($certFile, $logger)
&& self::validateCaFile($certFile, $logger);
}
/**
* @param string|false $certDir
* @param LoggerInterface|null $logger
* @return bool
*/
private static function caDirUsable($certDir, ?LoggerInterface $logger = null)
{
return $certDir
&& self::isDir($certDir, $logger)
&& self::isReadable($certDir, $logger)
&& self::glob($certDir . '/*', $logger);
}
/**
* @param string $certFile
* @param LoggerInterface|null $logger
* @return bool
*/
private static function isFile($certFile, ?LoggerInterface $logger = null)
{
$isFile = @is_file($certFile);
if (!$isFile && $logger) {
$logger->debug(sprintf('Checked CA file %s does not exist or it is not a file.', $certFile));
}
return $isFile;
}
/**
* @param string $certDir
* @param LoggerInterface|null $logger
* @return bool
*/
private static function isDir($certDir, ?LoggerInterface $logger = null)
{
$isDir = @is_dir($certDir);
if (!$isDir && $logger) {
$logger->debug(sprintf('Checked directory %s does not exist or it is not a directory.', $certDir));
}
return $isDir;
}
/**
* @param string $certFileOrDir
* @param LoggerInterface|null $logger
* @return bool
*/
private static function isReadable($certFileOrDir, ?LoggerInterface $logger = null)
{
$isReadable = @is_readable($certFileOrDir);
if (!$isReadable && $logger) {
$logger->debug(sprintf('Checked file or directory %s is not readable.', $certFileOrDir));
}
return $isReadable;
}
/**
* @param string $pattern
* @param LoggerInterface|null $logger
* @return bool
*/
private static function glob($pattern, ?LoggerInterface $logger = null)
{
$certs = glob($pattern);
if ($certs === false) {
if ($logger) {
$logger->debug(sprintf("An error occurred while trying to find certificates for pattern: %s", $pattern));
}
return false;
}
if (count($certs) === 0) {
if ($logger) {
$logger->debug(sprintf("No CA files found for pattern: %s", $pattern));
}
return false;
}
return true;
}
}
+1027
View File
File diff suppressed because it is too large Load Diff
+128 -2
View File
@@ -3,7 +3,7 @@
'name' => '__root__',
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => 'f685c2490ab18e80afa64695135087eeeec57804',
'reference' => 'd453b8073f07afc24f2a03f94a982977263aa107',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -13,12 +13,21 @@
'__root__' => array(
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => 'f685c2490ab18e80afa64695135087eeeec57804',
'reference' => 'd453b8073f07afc24f2a03f94a982977263aa107',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'composer/ca-bundle' => array(
'pretty_version' => '1.5.13',
'version' => '1.5.13.0',
'reference' => 'c008272789979f709f7fcb32c2ecf1d2db5e84e5',
'type' => 'library',
'install_path' => __DIR__ . '/./ca-bundle',
'aliases' => array(),
'dev_requirement' => false,
),
'dflydev/dot-access-data' => array(
'pretty_version' => 'v3.0.3',
'version' => '3.0.3.0',
@@ -28,6 +37,15 @@
'aliases' => array(),
'dev_requirement' => false,
),
'geoip2/geoip2' => array(
'pretty_version' => 'v2.13.0',
'version' => '2.13.0.0',
'reference' => '6a41d8fbd6b90052bc34dff3b4252d0f88067b23',
'type' => 'library',
'install_path' => __DIR__ . '/../geoip2/geoip2',
'aliases' => array(),
'dev_requirement' => false,
),
'league/commonmark' => array(
'pretty_version' => '2.8.0',
'version' => '2.8.0.0',
@@ -46,6 +64,42 @@
'aliases' => array(),
'dev_requirement' => false,
),
'league/uri' => array(
'pretty_version' => '7.8.1',
'version' => '7.8.1.0',
'reference' => '08cf38e3924d4f56238125547b5720496fac8fd4',
'type' => 'library',
'install_path' => __DIR__ . '/../league/uri',
'aliases' => array(),
'dev_requirement' => false,
),
'league/uri-interfaces' => array(
'pretty_version' => '7.8.1',
'version' => '7.8.1.0',
'reference' => '85d5c77c5d6d3af6c54db4a78246364908f3c928',
'type' => 'library',
'install_path' => __DIR__ . '/../league/uri-interfaces',
'aliases' => array(),
'dev_requirement' => false,
),
'maxmind-db/reader' => array(
'pretty_version' => 'v1.13.1',
'version' => '1.13.1.0',
'reference' => '2194f58d0f024ce923e685cdf92af3daf9951908',
'type' => 'library',
'install_path' => __DIR__ . '/../maxmind-db/reader',
'aliases' => array(),
'dev_requirement' => false,
),
'maxmind/web-service-common' => array(
'pretty_version' => 'v0.11.1',
'version' => '0.11.1.0',
'reference' => 'c309236b5a5555b96cf560089ec3cead12d845d2',
'type' => 'library',
'install_path' => __DIR__ . '/../maxmind/web-service-common',
'aliases' => array(),
'dev_requirement' => false,
),
'mustache/mustache' => array(
'pretty_version' => 'v3.0.0',
'version' => '3.0.0.0',
@@ -100,6 +154,24 @@
'aliases' => array(),
'dev_requirement' => false,
),
'psr/http-factory' => array(
'pretty_version' => '1.1.0',
'version' => '1.1.0.0',
'reference' => '2b4765fddfe3b508ac62f829e852b1501d3f6e8a',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/http-factory',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/http-message' => array(
'pretty_version' => '2.0',
'version' => '2.0.0.0',
'reference' => '402d35bcb92c70c026d1a6a9883f06b2ead23d71',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/http-message',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/log' => array(
'pretty_version' => '3.0.2',
'version' => '3.0.2.0',
@@ -109,6 +181,24 @@
'aliases' => array(),
'dev_requirement' => false,
),
'scssphp/scssphp' => array(
'pretty_version' => 'v2.1.0',
'version' => '2.1.0.0',
'reference' => 'd8450c2baf5fb07d00374999d0ea51276974d1b6',
'type' => 'library',
'install_path' => __DIR__ . '/../scssphp/scssphp',
'aliases' => array(),
'dev_requirement' => false,
),
'scssphp/source-span' => array(
'pretty_version' => 'v1.1.0',
'version' => '1.1.0.0',
'reference' => '37d653206daf11da1ee60b333984101bc4c27ba2',
'type' => 'library',
'install_path' => __DIR__ . '/../scssphp/source-span',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/deprecation-contracts' => array(
'pretty_version' => 'v3.6.0',
'version' => '3.6.0.0',
@@ -118,6 +208,33 @@
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/filesystem' => array(
'pretty_version' => 'v8.1.2',
'version' => '8.1.2.0',
'reference' => '17856b7a222664a26a5ea1cb06ee0721c2438217',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/filesystem',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-ctype' => array(
'pretty_version' => 'v1.37.0',
'version' => '1.37.0.0',
'reference' => '141046a8f9477948ff284fa65be2095baafb94f2',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-ctype',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-mbstring' => array(
'pretty_version' => 'v1.31.0',
'version' => '1.31.0.0',
'reference' => '85181ba99b2345b0ef10ce42ecac37612d9fd341',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-mbstring',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-php80' => array(
'pretty_version' => 'v1.33.0',
'version' => '1.33.0.0',
@@ -127,5 +244,14 @@
'aliases' => array(),
'dev_requirement' => false,
),
'twig/twig' => array(
'pretty_version' => 'v3.28.0',
'version' => '3.28.0.0',
'reference' => '597c12ed286fb9d1701a36684ce6e0cbe28ebc8b',
'type' => 'library',
'install_path' => __DIR__ . '/../twig/twig',
'aliases' => array(),
'dev_requirement' => false,
),
),
);
+2 -2
View File
@@ -4,8 +4,8 @@
$issues = array();
if (!(PHP_VERSION_ID >= 80100)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.';
if (!(PHP_VERSION_ID >= 80401)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.4.1". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
+323
View File
@@ -0,0 +1,323 @@
CHANGELOG
=========
2.13.0 (2022-08-05)
-------------------
* The model class names are no longer constructed by concatenating strings.
This change was made to improve support for tools like PHP-Scoper.
Reported by Andrew Mead. GitHub #194.
* Box 4.0.1 is now used to generate the `geoip2.phar` file.
2.12.2 (2021-11-30)
-------------------
* The `geoip2.phar` now works when included from another directory.
Reported by Eduardo Ruiz. GitHub #179.
2.12.1 (2021-11-23)
-------------------
* The `geoip2.phar` included in 2.12.0 would only work in CLI applications.
This was due to a change in Box 3.x. The Phar should now work in all
applications. This release only affects users of the Phar file.
2.12.0 (2021-11-18)
-------------------
* Support for mobile country code (MCC) and mobile network codes (MNC) was
added for the GeoIP2 ISP and Enterprise databases as well as the GeoIP2
City and Insights web services. `$mobileCountryCode` and
`$mobileNetworkCode` properties were added to `GeoIp2\Model\Isp`
for the GeoIP2 ISP database and `GeoIp2\Record\Traits` for the Enterprise
database and the GeoIP2 City and Insights web services. We expect this data
to be available by late January, 2022.
* `geoip2.phar` is now generated with Box 3.x.
2.11.0 (2020-10-01)
-------------------
* IMPORTANT: PHP 7.2 or greater is now required.
* Added the `isResidentialProxy` property to `GeoIp2\Model\AnonymousIP` and
`GeoIp2\Record\Traits`.
* Additional type hints have been added.
2.10.0 (2019-12-12)
-------------------
* PHP 5.6 or greater is now required.
* The `network` property was added to `GeoIp2\Record\Traits`,
`GeoIp2\Model\AnonymousIp`, `GeoIp2\Model\Asn`,
`GeoIp2\Model\ConnectionType`, `Geoip2\Model\Domain`,
and `GeoIp2\Model\Isp`. This is a string in CIDR format representing the
largest network where all of the properties besides `ipAddress` have the
same value.
* Updated documentation of anonymizer properties - `isAnonymousVpn`
and `isHostingProvider` - to be more descriptive.
* The `userCount` property was added to `GeoIp2\Record\Traits`. This is an
integer which indicates the estimated number of users sharing the
IP/network during the past 24 hours. This output is available from GeoIP2
Precision Insights.
* The `staticIpScore` property was added to `GeoIp2\Record\Traits`. This is
a float which indicates how static or dynamic an IP address is. This
output is available from GeoIP2 Precision Insights.
2.9.0 (2018-04-10)
------------------
* Refer to account IDs using the terminology "account" rather than "user".
2.8.0 (2018-01-18)
------------------
* The `isInEuropeanUnion` property was added to `GeoIp2\Record\Country`
and `GeoIp2\Record\RepresentedCountry`. This property is `true` if the
country is a member state of the European Union.
2.7.0 (2017-10-27)
------------------
* The following new anonymizer properties were added to `GeoIp2\Record\Traits`
for use with GeoIP2 Precision Insights: `isAnonymous`, `isAnonymousVpn`,
`isHostingProvider`, `isPublicProxy`, and `isTorExitNode`.
2.6.0 (2017-07-10)
-----------------
* Code clean-up and tidying.
* Set minimum required PHP version to 5.4 in `composer.json`. Previously,
5.3 would work but was not tested. Now 5.4 is hard minimum version.
2.5.0 (2017-05-08)
------------------
* Support for PHP 5.3 was dropped.
* Added support for GeoLite2 ASN database.
2.4.5 (2017-01-31)
------------------
* Additional error checking on the data returned from `MaxMind\Db\Reader`
was added to help detect corrupt databases. GitHub #83.
2.4.4 (2016-10-11)
------------------
* `isset()` on `mostSpecificSubdivision` attribute now returns the
correct value. Reported by Juan Francisco Giordana. GitHub #81.
2.4.3 (2016-10-11)
------------------
* `isset()` on `name` attribute now returns the correct value. Reported by
Juan Francisco Giordana. GitHub #79.
2.4.2 (2016-08-17)
------------------
* Updated documentation to clarify what the accuracy radius refers to.
* Upgraded `maxmind/web-service-common` to 0.3.0. This version uses
`composer/ca-bundle` rather than our own CA bundle. GitHub #75.
* Improved PHP documentation generation.
2.4.1 (2016-06-10)
------------------
* Corrected type annotations in documentation. GitHub #66.
* Updated documentation to reflect that the accuracy radius is now included
in City.
* Upgraded web service client, which supports setting a proxy. GitHub #59.
2.4.0 (2016-04-15)
------------------
* Added support for the GeoIP2 Enterprise database.
2.3.3 (2015-09-24)
------------------
* Corrected case on `JsonSerializable` interface. Reported by Axel Etcheverry.
GitHub #56.
2.3.2 (2015-09-23)
------------------
* `JsonSerializable` compatibility interface was moved to `GeoIp2\Compat`
rather than the global namespace to prevent autoloading issues. Reported by
Tomas Buteler. GitHub #54.
* Missing documentation for the `$postal` property was added to the
`GeoIp2\Model\City` class. Fix by Roy Sindre Norangshol. GitHub #51.
* In the Phar distribution, source files for this module no longer have their
documentation stripped, allowing IDE introspection to work properly.
Reported by Dominic Black. GitHub #52.
2.3.1 (2015-06-30)
------------------
* Updated `maxmind/web-service-common` to version with fixes for PHP 5.3 and
5.4.
2.3.0 (2015-06-29)
------------------
* Support for demographics fields `averageIncome` and `populationDensity` in
the `Location` record, returned by the Insights endpoint.
* The `isAnonymousProxy` and `isSatelliteProvider` properties on
`GeoIP2\Record\Traits` have been deprecated. Please use our [GeoIP2
Anonymous IP database](https://www.maxmind.com/en/geoip2-anonymous-ip-database)
to determine whether an IP address is used by an anonymizing service.
2.2.0-beta1 (2015-06-09)
------------------------
* Typo fix in documentation.
2.2.0-alpha2 (2015-06-01)
-------------------------
* `maxmind-ws/web-service-common` was renamed to `maxmind/web-service-common`.
2.2.0-alpha1 (2015-05-22)
-------------------------
* The library no longer uses Guzzle and instead uses curl directly.
* Support for `timeout` and `connectTimout` were added to the `$options` array
passed to the `GeoIp2\WebService\Client` constructor. Pull request by Will
Bradley. GitHub #36.
2.1.1 (2014-12-03)
------------------
* The 2.1.0 Phar builds included a shebang line, causing issues when loading
it as a library. This has been corrected. GitHub #33.
2.1.0 (2014-10-29)
------------------
* Update ApiGen dependency to version that isn't broken on case sensitive
file systems.
* Added support for the GeoIP2 Anonymous IP database. The
`GeoIP2\Database\Reader` class now has an `anonymousIp` method which returns
a `GeoIP2\Model\AnonymousIp` object.
* Boolean attributes like those in the `GeoIP2\Record\Traits` class now return
`false` instead of `null` when they were not true.
2.0.0 (2014-09-22)
------------------
* First production release.
0.9.0 (2014-09-15)
------------------
* IMPORTANT: The deprecated `omni()` and `cityIspOrg()` methods have been
removed from `GeoIp2\WebService\Client`.
0.8.1 (2014-09-12)
------------------
* The check added to the `GeoIP2\Database\Reader` lookup methods in 0.8.0 did
not work with the GeoIP2 City Database Subset by Continent with World
Countries. This has been fixed. Fixes GitHub issue #23.
0.8.0 (2014-09-10)
------------------
* The `GeoIp2\Database\Reader` lookup methods (e.g., `city()`, `isp()`) now
throw a `BadMethodCallException` if they are used with a database that
does not match the method. In particular, doing a `city()` lookup on a
GeoIP2 Country database will result in an exception, and vice versa.
* A `metadata()` method has been added to the `GeoIP2\Database\Reader` class.
This returns a `MaxMind\Db\Reader\Metadata` class with information about the
database.
* The name attribute was missing from the RepresentedCountry class.
0.7.0 (2014-07-22)
------------------
* The web service client API has been updated for the v2.1 release of the web
service. In particular, the `cityIspOrg` and `omni` methods on
`GeoIp2\WebService\Client` should be considered deprecated. The `city`
method now provides all of the data formerly provided by `cityIspOrg`, and
the `omni` method has been replaced by the `insights` method.
* Support was added for GeoIP2 Connection Type, Domain and ISP databases.
0.6.3 (2014-05-12)
------------------
* With the previous Phar builds, some users received `phar error: invalid url
or non-existent phar` errors. The correct alias is now used for the Phar,
and this should no longer be an issue.
0.6.2 (2014-05-08)
------------------
* The Phar build was broken with Guzzle 3.9.0+. This has been fixed.
0.6.1 (2014-05-01)
------------------
* This API now officially supports HHVM.
* The `maxmind-db/reader` dependency was updated to a version that does not
require BC Math.
* The Composer compatibility autoload rules are now targeted more narrowly.
* A `box.json` file is included to build a Phar package.
0.6.0 (2014-02-19)
------------------
* This API is now licensed under the Apache License, Version 2.0.
* Model and record classes now implement `JsonSerializable`.
* `isset` now works with model and record classes.
0.5.0 (2013-10-21)
------------------
* Renamed $languages constructor parameters to $locales for both the Client
and Reader classes.
* Documentation and code clean-up (Ben Morel).
* Added the interface `GeoIp2\ProviderInterface`, which is implemented by both
`\GeoIp2\Database\Reader` and `\GeoIp2\WebService\Client`.
0.4.0 (2013-07-16)
------------------
* This is the first release with the GeoIP2 database reader. Please see the
`README.md` file and the `\GeoIp2\Database\Reader` class.
* The general exception classes were replaced with specific exception classes
representing particular types of errors, such as an authentication error.
0.3.0 (2013-07-12)
------------------
* In namespaces and class names, "GeoIP2" was renamed to "GeoIp2" to improve
consistency.
0.2.1 (2013-06-10)
------------------
* First official beta release.
* Documentation updates and corrections.
0.2.0 (2013-05-29)
------------------
* `GenericException` was renamed to `GeoIP2Exception`.
* We now support more languages. The new languages are de, es, fr, and pt-BR.
* The REST API now returns a record with data about your account. There is
a new `GeoIP\Records\MaxMind` class for this data.
* The `continentCode` attribute on `Continent` was renamed to `code`.
* Documentation updates.
0.1.1 (2013-05-14)
------------------
* Updated Guzzle version requirement.
* Fixed Composer example in README.md.
0.1.0 (2013-05-13)
------------------
* Initial release.
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+442
View File
@@ -0,0 +1,442 @@
# GeoIP2 PHP API #
## Description ##
This package provides an API for the GeoIP2 and GeoLite2
[web services](https://dev.maxmind.com/geoip/docs/web-services?lang=en) and
[databases](https://dev.maxmind.com/geoip/docs/databases?lang=en).
## Install via Composer ##
We recommend installing this package with [Composer](https://getcomposer.org/).
### Download Composer ###
To download Composer, run in the root directory of your project:
```bash
curl -sS https://getcomposer.org/installer | php
```
You should now have the file `composer.phar` in your project directory.
### Install Dependencies ###
Run in your project root:
```sh
php composer.phar require geoip2/geoip2:~2.0
```
You should now have the files `composer.json` and `composer.lock` as well as
the directory `vendor` in your project directory. If you use a version control
system, `composer.json` should be added to it.
### Require Autoloader ###
After installing the dependencies, you need to require the Composer autoloader
from your code:
```php
require 'vendor/autoload.php';
```
## Install via Phar ##
Although we strongly recommend using Composer, we also provide a
[phar archive](https://php.net/manual/en/book.phar.php) containing most of the
dependencies for GeoIP2. Our latest phar archive is available on
[our releases page](https://github.com/maxmind/GeoIP2-php/releases).
### Install Dependencies ###
In order to use the phar archive, you must have the PHP
[Phar extension](https://php.net/manual/en/book.phar.php) installed and
enabled.
If you will be making web service requests, you must have the PHP
[cURL extension](https://php.net/manual/en/book.curl.php)
installed to use this archive. For Debian based distributions, this can
typically be found in the the `php-curl` package. For other operating
systems, please consult the relevant documentation. After installing the
extension you may need to restart your web server.
If you are missing this extension, you will see errors like the following:
```
PHP Fatal error: Uncaught Error: Call to undefined function MaxMind\WebService\curl_version()
```
### Require Package ###
To use the archive, just require it from your script:
```php
require 'geoip2.phar';
```
## Optional C Extension ##
The [MaxMind DB API](https://github.com/maxmind/MaxMind-DB-Reader-php)
includes an optional C extension that you may install to dramatically increase
the performance of lookups in GeoIP2 or GeoLite2 databases. To install, please
follow the instructions included with that API.
The extension has no effect on web-service lookups.
## IP Geolocation Usage ##
IP geolocation is inherently imprecise. Locations are often near the center of
the population. Any location provided by a GeoIP2 database or web service
should not be used to identify a particular address or household.
## Database Reader ##
### Usage ###
To use this API, you must create a new `\GeoIp2\Database\Reader` object with
the path to the database file as the first argument to the constructor. You
may then call the method corresponding to the database you are using.
If the lookup succeeds, the method call will return a model class for the
record in the database. This model in turn contains multiple container
classes for the different parts of the data such as the city in which the
IP address is located.
If the record is not found, a `\GeoIp2\Exception\AddressNotFoundException`
is thrown. If the database is invalid or corrupt, a
`\MaxMind\Db\InvalidDatabaseException` will be thrown.
See the API documentation for more details.
### City Example ###
```php
<?php
require_once 'vendor/autoload.php';
use GeoIp2\Database\Reader;
// This creates the Reader object, which should be reused across
// lookups.
$reader = new Reader('/usr/local/share/GeoIP/GeoIP2-City.mmdb');
// Replace "city" with the appropriate method for your database, e.g.,
// "country".
$record = $reader->city('128.101.101.101');
print($record->country->isoCode . "\n"); // 'US'
print($record->country->name . "\n"); // 'United States'
print($record->country->names['zh-CN'] . "\n"); // '美国'
print($record->mostSpecificSubdivision->name . "\n"); // 'Minnesota'
print($record->mostSpecificSubdivision->isoCode . "\n"); // 'MN'
print($record->city->name . "\n"); // 'Minneapolis'
print($record->postal->code . "\n"); // '55455'
print($record->location->latitude . "\n"); // 44.9733
print($record->location->longitude . "\n"); // -93.2323
print($record->traits->network . "\n"); // '128.101.101.101/32'
```
### Anonymous IP Example ###
```php
<?php
require_once 'vendor/autoload.php';
use GeoIp2\Database\Reader;
// This creates the Reader object, which should be reused across
// lookups.
$reader = new Reader('/usr/local/share/GeoIP/GeoIP2-Anonymous-IP.mmdb');
$record = $reader->anonymousIp('128.101.101.101');
if ($record->isAnonymous) { print "anon\n"; }
print($record->ipAddress . "\n"); // '128.101.101.101'
print($record->network . "\n"); // '128.101.101.101/32'
```
### Connection-Type Example ###
```php
<?php
require_once 'vendor/autoload.php';
use GeoIp2\Database\Reader;
// This creates the Reader object, which should be reused across
// lookups.
$reader = new Reader('/usr/local/share/GeoIP/GeoIP2-Connection-Type.mmdb');
$record = $reader->connectionType('128.101.101.101');
print($record->connectionType . "\n"); // 'Corporate'
print($record->ipAddress . "\n"); // '128.101.101.101'
print($record->network . "\n"); // '128.101.101.101/32'
```
### Domain Example ###
```php
<?php
require_once 'vendor/autoload.php';
use GeoIp2\Database\Reader;
// This creates the Reader object, which should be reused across
// lookups.
$reader = new Reader('/usr/local/share/GeoIP/GeoIP2-Domain.mmdb');
$record = $reader->domain('128.101.101.101');
print($record->domain . "\n"); // 'umn.edu'
print($record->ipAddress . "\n"); // '128.101.101.101'
print($record->network . "\n"); // '128.101.101.101/32'
```
### Enterprise Example ###
```php
<?php
require_once 'vendor/autoload.php';
use GeoIp2\Database\Reader;
// This creates the Reader object, which should be reused across
// lookups.
$reader = new Reader('/usr/local/share/GeoIP/GeoIP2-Enterprise.mmdb');
// Use the ->enterprise method to do a lookup in the Enterprise database
$record = $reader->enterprise('128.101.101.101');
print($record->country->confidence . "\n"); // 99
print($record->country->isoCode . "\n"); // 'US'
print($record->country->name . "\n"); // 'United States'
print($record->country->names['zh-CN'] . "\n"); // '美国'
print($record->mostSpecificSubdivision->confidence . "\n"); // 77
print($record->mostSpecificSubdivision->name . "\n"); // 'Minnesota'
print($record->mostSpecificSubdivision->isoCode . "\n"); // 'MN'
print($record->city->confidence . "\n"); // 60
print($record->city->name . "\n"); // 'Minneapolis'
print($record->postal->code . "\n"); // '55455'
print($record->location->accuracyRadius . "\n"); // 50
print($record->location->latitude . "\n"); // 44.9733
print($record->location->longitude . "\n"); // -93.2323
print($record->traits->network . "\n"); // '128.101.101.101/32'
```
### ISP Example ###
```php
<?php
require_once 'vendor/autoload.php';
use GeoIp2\Database\Reader;
// This creates the Reader object, which should be reused across
// lookups.
$reader = new Reader('/usr/local/share/GeoIP/GeoIP2-ISP.mmdb');
$record = $reader->isp('128.101.101.101');
print($record->autonomousSystemNumber . "\n"); // 217
print($record->autonomousSystemOrganization . "\n"); // 'University of Minnesota'
print($record->isp . "\n"); // 'University of Minnesota'
print($record->organization . "\n"); // 'University of Minnesota'
print($record->ipAddress . "\n"); // '128.101.101.101'
print($record->network . "\n"); // '128.101.101.101/32'
```
## Database Updates ##
You can keep your databases up to date with our
[GeoIP Update program](https://github.com/maxmind/geoipupdate/releases).
[Learn more about GeoIP Update on our developer
portal.](https://dev.maxmind.com/geoip/updating-databases?lang=en)
There is also a third-party tool for updating databases using PHP and
Composer. MaxMind does not offer support for this tool or maintain it.
[Learn more about the Geoip2 Update tool for PHP and Composer on its
GitHub page.](https://github.com/tronovav/geoip2-update)
## Web Service Client ##
### Usage ###
To use this API, you must create a new `\GeoIp2\WebService\Client`
object with your `$accountId` and `$licenseKey`:
```php
$client = new Client(42, 'abcdef123456');
```
You may also call the constructor with additional arguments. The third argument
specifies the language preferences when using the `->name` method on the model
classes that this client creates. The fourth argument is additional options
such as `host` and `timeout`.
For instance, to call the GeoLite2 web service instead of the GeoIP2 web
service:
```php
$client = new Client(42, 'abcdef123456', ['en'], ['host' => 'geolite.info']);
```
After creating the client, you may now call the method corresponding to a
specific endpoint with the IP address to look up, e.g.:
```php
$record = $client->city('128.101.101.101');
```
If the request succeeds, the method call will return a model class for the
endpoint you called. This model in turn contains multiple record classes, each
of which represents part of the data returned by the web service.
If there is an error, a structured exception is thrown.
See the API documentation for more details.
### Example ###
```php
<?php
require_once 'vendor/autoload.php';
use GeoIp2\WebService\Client;
// This creates a Client object that can be reused across requests.
// Replace "42" with your account ID and "license_key" with your license
// key. Set the "host" to "geolite.info" in the fourth argument options
// array to use the GeoLite2 web service instead of the GeoIP2 web
// service.
$client = new Client(42, 'abcdef123456');
// Replace "city" with the method corresponding to the web service that
// you are using, e.g., "country", "insights".
$record = $client->city('128.101.101.101');
print($record->country->isoCode . "\n"); // 'US'
print($record->country->name . "\n"); // 'United States'
print($record->country->names['zh-CN'] . "\n"); // '美国'
print($record->mostSpecificSubdivision->name . "\n"); // 'Minnesota'
print($record->mostSpecificSubdivision->isoCode . "\n"); // 'MN'
print($record->city->name . "\n"); // 'Minneapolis'
print($record->postal->code . "\n"); // '55455'
print($record->location->latitude . "\n"); // 44.9733
print($record->location->longitude . "\n"); // -93.2323
print($record->traits->network . "\n"); // '128.101.101.101/32'
```
## Values to use for Database or Array Keys ##
**We strongly discourage you from using a value from any `names` property as
a key in a database or array.**
These names may change between releases. Instead we recommend using one of the
following:
* `GeoIp2\Record\City` - `$city->geonameId`
* `GeoIp2\Record\Continent` - `$continent->code` or `$continent->geonameId`
* `GeoIp2\Record\Country` and `GeoIp2\Record\RepresentedCountry` -
`$country->isoCode` or `$country->geonameId`
* `GeoIp2\Record\Subdivision` - `$subdivision->isoCode` or `$subdivision->geonameId`
### What data is returned? ###
While many of the end points return the same basic records, the attributes
which can be populated vary between end points. In addition, while an end
point may offer a particular piece of data, MaxMind does not always have every
piece of data for any given IP address.
Because of these factors, it is possible for any end point to return a record
where some or all of the attributes are unpopulated.
See the
[GeoIP2 web service docs](https://dev.maxmind.com/geoip/docs/web-services?lang=en)
for details on what data each end point may return.
The only piece of data which is always returned is the `ipAddress`
attribute in the `GeoIp2\Record\Traits` record.
## Integration with GeoNames ##
[GeoNames](https://www.geonames.org/) offers web services and downloadable
databases with data on geographical features around the world, including
populated places. They offer both free and paid premium data. Each
feature is unique identified by a `geonameId`, which is an integer.
Many of the records returned by the GeoIP2 web services and databases
include a `geonameId` property. This is the ID of a geographical feature
(city, region, country, etc.) in the GeoNames database.
Some of the data that MaxMind provides is also sourced from GeoNames. We
source things like place names, ISO codes, and other similar data from
the GeoNames premium data set.
## Reporting data problems ##
If the problem you find is that an IP address is incorrectly mapped,
please
[submit your correction to MaxMind](https://www.maxmind.com/en/correction).
If you find some other sort of mistake, like an incorrect spelling,
please check the [GeoNames site](https://www.geonames.org/) first. Once
you've searched for a place and found it on the GeoNames map view, there
are a number of links you can use to correct data ("move", "edit",
"alternate names", etc.). Once the correction is part of the GeoNames
data set, it will be automatically incorporated into future MaxMind
releases.
If you are a paying MaxMind customer and you're not sure where to submit
a correction, please
[contact MaxMind support](https://www.maxmind.com/en/support) for help.
## Other Support ##
Please report all issues with this code using the
[GitHub issue tracker](https://github.com/maxmind/GeoIP2-php/issues).
If you are having an issue with a MaxMind service that is not specific
to the client API, please see
[our support page](https://www.maxmind.com/en/support).
## Requirements ##
This library requires PHP 7.2 or greater.
This library also relies on the [MaxMind DB Reader](https://github.com/maxmind/MaxMind-DB-Reader-php).
## Contributing ##
Patches and pull requests are encouraged. All code should follow the PSR-2
style guidelines. Please include unit tests whenever possible. You may obtain
the test data for the maxmind-db folder by running `git submodule update
--init --recursive` or adding `--recursive` to your initial clone, or from
https://github.com/maxmind/MaxMind-DB
## Versioning ##
The GeoIP2 PHP API uses [Semantic Versioning](https://semver.org/).
## Copyright and License ##
This software is Copyright (c) 2013-2020 by MaxMind, Inc.
This is free software, licensed under the Apache License, Version 2.0.
+32
View File
@@ -0,0 +1,32 @@
{
"name": "geoip2/geoip2",
"description": "MaxMind GeoIP2 PHP API",
"keywords": ["geoip", "geoip2", "geolocation", "ip", "maxmind"],
"homepage": "https://github.com/maxmind/GeoIP2-php",
"type": "library",
"license": "Apache-2.0",
"authors": [
{
"name": "Gregory J. Oschwald",
"email": "goschwald@maxmind.com",
"homepage": "https://www.maxmind.com/"
}
],
"require": {
"maxmind-db/reader": "~1.8",
"maxmind/web-service-common": "~0.8",
"php": ">=7.2",
"ext-json": "*"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "3.*",
"phpunit/phpunit": "^8.0 || ^9.0",
"squizlabs/php_codesniffer": "3.*",
"phpstan/phpstan": "*"
},
"autoload": {
"psr-4": {
"GeoIp2\\": "src"
}
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
require __DIR__ . '/../vendor/autoload.php';
use GeoIp2\Database\Reader;
srand(0);
$reader = new Reader('GeoIP2-City.mmdb');
$count = 500000;
$startTime = microtime(true);
for ($i = 0; $i < $count; ++$i) {
$ip = long2ip(rand(0, 2 ** 32 - 1));
try {
$t = $reader->city($ip);
} catch (\GeoIp2\Exception\AddressNotFoundException $e) {
}
if ($i % 10000 === 0) {
echo $i . ' ' . $ip . "\n";
}
}
$endTime = microtime(true);
$duration = $endTime - $startTime;
echo 'Requests per second: ' . $count / $duration . "\n";
+299
View File
@@ -0,0 +1,299 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Database;
use GeoIp2\Exception\AddressNotFoundException;
use GeoIp2\Model\AbstractModel;
use GeoIp2\Model\AnonymousIp;
use GeoIp2\Model\Asn;
use GeoIp2\Model\City;
use GeoIp2\Model\ConnectionType;
use GeoIp2\Model\Country;
use GeoIp2\Model\Domain;
use GeoIp2\Model\Enterprise;
use GeoIp2\Model\Isp;
use GeoIp2\ProviderInterface;
use MaxMind\Db\Reader as DbReader;
use MaxMind\Db\Reader\InvalidDatabaseException;
/**
* Instances of this class provide a reader for the GeoIP2 database format.
* IP addresses can be looked up using the database specific methods.
*
* ## Usage ##
*
* The basic API for this class is the same for every database. First, you
* create a reader object, specifying a file name. You then call the method
* corresponding to the specific database, passing it the IP address you want
* to look up.
*
* If the request succeeds, the method call will return a model class for
* the method you called. This model in turn contains multiple record classes,
* each of which represents part of the data returned by the database. If
* the database does not contain the requested information, the attributes
* on the record class will have a `null` value.
*
* If the address is not in the database, an
* {@link \GeoIp2\Exception\AddressNotFoundException} exception will be
* thrown. If an invalid IP address is passed to one of the methods, a
* SPL {@link \InvalidArgumentException} will be thrown. If the database is
* corrupt or invalid, a {@link \MaxMind\Db\Reader\InvalidDatabaseException}
* will be thrown.
*/
class Reader implements ProviderInterface
{
/**
* @var DbReader
*/
private $dbReader;
/**
* @var string
*/
private $dbType;
/**
* @var array<string>
*/
private $locales;
/**
* Constructor.
*
* @param string $filename the path to the GeoIP2 database file
* @param array $locales list of locale codes to use in name property
* from most preferred to least preferred
*
* @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
* is corrupt or invalid
*/
public function __construct(
string $filename,
array $locales = ['en']
) {
$this->dbReader = new DbReader($filename);
$this->dbType = $this->dbReader->metadata()->databaseType;
$this->locales = $locales;
}
/**
* This method returns a GeoIP2 City model.
*
* @param string $ipAddress an IPv4 or IPv6 address as a string
*
* @throws \GeoIp2\Exception\AddressNotFoundException if the address is
* not in the database
* @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
* is corrupt or invalid
*/
public function city(string $ipAddress): City
{
// @phpstan-ignore-next-line
return $this->modelFor(City::class, 'City', $ipAddress);
}
/**
* This method returns a GeoIP2 Country model.
*
* @param string $ipAddress an IPv4 or IPv6 address as a string
*
* @throws \GeoIp2\Exception\AddressNotFoundException if the address is
* not in the database
* @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
* is corrupt or invalid
*/
public function country(string $ipAddress): Country
{
// @phpstan-ignore-next-line
return $this->modelFor(Country::class, 'Country', $ipAddress);
}
/**
* This method returns a GeoIP2 Anonymous IP model.
*
* @param string $ipAddress an IPv4 or IPv6 address as a string
*
* @throws \GeoIp2\Exception\AddressNotFoundException if the address is
* not in the database
* @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
* is corrupt or invalid
*/
public function anonymousIp(string $ipAddress): AnonymousIp
{
// @phpstan-ignore-next-line
return $this->flatModelFor(
AnonymousIp::class,
'GeoIP2-Anonymous-IP',
$ipAddress
);
}
/**
* This method returns a GeoLite2 ASN model.
*
* @param string $ipAddress an IPv4 or IPv6 address as a string
*
* @throws \GeoIp2\Exception\AddressNotFoundException if the address is
* not in the database
* @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
* is corrupt or invalid
*/
public function asn(string $ipAddress): Asn
{
// @phpstan-ignore-next-line
return $this->flatModelFor(
Asn::class,
'GeoLite2-ASN',
$ipAddress
);
}
/**
* This method returns a GeoIP2 Connection Type model.
*
* @param string $ipAddress an IPv4 or IPv6 address as a string
*
* @throws \GeoIp2\Exception\AddressNotFoundException if the address is
* not in the database
* @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
* is corrupt or invalid
*/
public function connectionType(string $ipAddress): ConnectionType
{
// @phpstan-ignore-next-line
return $this->flatModelFor(
ConnectionType::class,
'GeoIP2-Connection-Type',
$ipAddress
);
}
/**
* This method returns a GeoIP2 Domain model.
*
* @param string $ipAddress an IPv4 or IPv6 address as a string
*
* @throws \GeoIp2\Exception\AddressNotFoundException if the address is
* not in the database
* @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
* is corrupt or invalid
*/
public function domain(string $ipAddress): Domain
{
// @phpstan-ignore-next-line
return $this->flatModelFor(
Domain::class,
'GeoIP2-Domain',
$ipAddress
);
}
/**
* This method returns a GeoIP2 Enterprise model.
*
* @param string $ipAddress an IPv4 or IPv6 address as a string
*
* @throws \GeoIp2\Exception\AddressNotFoundException if the address is
* not in the database
* @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
* is corrupt or invalid
*/
public function enterprise(string $ipAddress): Enterprise
{
// @phpstan-ignore-next-line
return $this->modelFor(Enterprise::class, 'Enterprise', $ipAddress);
}
/**
* This method returns a GeoIP2 ISP model.
*
* @param string $ipAddress an IPv4 or IPv6 address as a string
*
* @throws \GeoIp2\Exception\AddressNotFoundException if the address is
* not in the database
* @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
* is corrupt or invalid
*/
public function isp(string $ipAddress): Isp
{
// @phpstan-ignore-next-line
return $this->flatModelFor(
Isp::class,
'GeoIP2-ISP',
$ipAddress
);
}
private function modelFor(string $class, string $type, string $ipAddress): AbstractModel
{
[$record, $prefixLen] = $this->getRecord($class, $type, $ipAddress);
$record['traits']['ip_address'] = $ipAddress;
$record['traits']['prefix_len'] = $prefixLen;
return new $class($record, $this->locales);
}
private function flatModelFor(string $class, string $type, string $ipAddress): AbstractModel
{
[$record, $prefixLen] = $this->getRecord($class, $type, $ipAddress);
$record['ip_address'] = $ipAddress;
$record['prefix_len'] = $prefixLen;
return new $class($record);
}
private function getRecord(string $class, string $type, string $ipAddress): array
{
if (strpos($this->dbType, $type) === false) {
$method = lcfirst((new \ReflectionClass($class))->getShortName());
throw new \BadMethodCallException(
"The $method method cannot be used to open a {$this->dbType} database"
);
}
[$record, $prefixLen] = $this->dbReader->getWithPrefixLen($ipAddress);
if ($record === null) {
throw new AddressNotFoundException(
"The address $ipAddress is not in the database."
);
}
if (!\is_array($record)) {
// This can happen on corrupt databases. Generally,
// MaxMind\Db\Reader will throw a
// MaxMind\Db\Reader\InvalidDatabaseException, but occasionally
// the lookup may result in a record that looks valid but is not
// an array. This mostly happens when the user is ignoring all
// exceptions and the more frequent InvalidDatabaseException
// exceptions go unnoticed.
throw new InvalidDatabaseException(
"Expected an array when looking up $ipAddress but received: "
. \gettype($record)
);
}
return [$record, $prefixLen];
}
/**
* @throws \InvalidArgumentException if arguments are passed to the method
* @throws \BadMethodCallException if the database has been closed
*
* @return \MaxMind\Db\Reader\Metadata object for the database
*/
public function metadata(): DbReader\Metadata
{
return $this->dbReader->metadata();
}
/**
* Closes the GeoIP2 database and returns the resources to the system.
*/
public function close(): void
{
$this->dbReader->close();
}
}
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Exception;
/**
* This class represents a generic error.
*/
class AddressNotFoundException extends GeoIp2Exception
{
}
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Exception;
/**
* This class represents a generic error.
*/
class AuthenticationException extends GeoIp2Exception
{
}
+12
View File
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Exception;
/**
* This class represents a generic error.
*/
class GeoIp2Exception extends \Exception
{
}
+28
View File
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Exception;
/**
* This class represents an HTTP transport error.
*/
class HttpException extends GeoIp2Exception
{
/**
* The URI queried.
*
* @var string
*/
public $uri;
public function __construct(
string $message,
int $httpStatus,
string $uri,
\Exception $previous = null
) {
$this->uri = $uri;
parent::__construct($message, $httpStatus, $previous);
}
}
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Exception;
/**
* This class represents an error returned by MaxMind's GeoIP2
* web service.
*/
class InvalidRequestException extends HttpException
{
/**
* The code returned by the MaxMind web service.
*
* @var string
*/
public $error;
public function __construct(
string $message,
string $error,
int $httpStatus,
string $uri,
\Exception $previous = null
) {
$this->error = $error;
parent::__construct($message, $httpStatus, $uri, $previous);
}
}
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Exception;
/**
* This class represents a generic error.
*/
class OutOfQueriesException extends GeoIp2Exception
{
}
+68
View File
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Model;
/**
* @ignore
*/
abstract class AbstractModel implements \JsonSerializable
{
/**
* @var array<string, mixed>
*/
protected $raw;
/**
* @ignore
*/
public function __construct(array $raw)
{
$this->raw = $raw;
}
/**
* @ignore
*
* @return mixed
*/
protected function get(string $field)
{
if (isset($this->raw[$field])) {
return $this->raw[$field];
}
if (preg_match('/^is_/', $field)) {
return false;
}
return null;
}
/**
* @ignore
*
* @return mixed
*/
public function __get(string $attr)
{
if ($attr !== 'instance' && property_exists($this, $attr)) {
return $this->{$attr};
}
throw new \RuntimeException("Unknown attribute: $attr");
}
/**
* @ignore
*/
public function __isset(string $attr): bool
{
return $attr !== 'instance' && isset($this->{$attr});
}
public function jsonSerialize(): array
{
return $this->raw;
}
}
+91
View File
@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Model;
use GeoIp2\Util;
/**
* This class provides the GeoIP2 Anonymous IP model.
*
* @property-read bool $isAnonymous This is true if the IP address belongs to
* any sort of anonymous network.
* @property-read bool $isAnonymousVpn This is true if the IP address is
* registered to an anonymous VPN provider. If a VPN provider does not
* register subnets under names associated with them, we will likely only
* flag their IP ranges using the isHostingProvider property.
* @property-read bool $isHostingProvider This is true if the IP address belongs
* to a hosting or VPN provider (see description of isAnonymousVpn property).
* @property-read bool $isPublicProxy This is true if the IP address belongs to
* a public proxy.
* @property-read bool $isResidentialProxy This is true if the IP address is
* on a suspected anonymizing network and belongs to a residential ISP.
* @property-read bool $isTorExitNode This is true if the IP address is a Tor
* exit node.
* @property-read string $ipAddress The IP address that the data in the model is
* for.
* @property-read string $network The network in CIDR notation associated with
* the record. In particular, this is the largest network where all of the
* fields besides $ipAddress have the same value.
*/
class AnonymousIp extends AbstractModel
{
/**
* @var bool
*/
protected $isAnonymous;
/**
* @var bool
*/
protected $isAnonymousVpn;
/**
* @var bool
*/
protected $isHostingProvider;
/**
* @var bool
*/
protected $isPublicProxy;
/**
* @var bool
*/
protected $isResidentialProxy;
/**
* @var bool
*/
protected $isTorExitNode;
/**
* @var string
*/
protected $ipAddress;
/**
* @var string
*/
protected $network;
/**
* @ignore
*/
public function __construct(array $raw)
{
parent::__construct($raw);
$this->isAnonymous = $this->get('is_anonymous');
$this->isAnonymousVpn = $this->get('is_anonymous_vpn');
$this->isHostingProvider = $this->get('is_hosting_provider');
$this->isPublicProxy = $this->get('is_public_proxy');
$this->isResidentialProxy = $this->get('is_residential_proxy');
$this->isTorExitNode = $this->get('is_tor_exit_node');
$ipAddress = $this->get('ip_address');
$this->ipAddress = $ipAddress;
$this->network = Util::cidr($ipAddress, $this->get('prefix_len'));
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Model;
use GeoIp2\Util;
/**
* This class provides the GeoLite2 ASN model.
*
* @property-read int|null $autonomousSystemNumber The autonomous system number
* associated with the IP address.
* @property-read string|null $autonomousSystemOrganization The organization
* associated with the registered autonomous system number for the IP
* address.
* @property-read string $ipAddress The IP address that the data in the model is
* for.
* @property-read string $network The network in CIDR notation associated with
* the record. In particular, this is the largest network where all of the
* fields besides $ipAddress have the same value.
*/
class Asn extends AbstractModel
{
/**
* @var int|null
*/
protected $autonomousSystemNumber;
/**
* @var string|null
*/
protected $autonomousSystemOrganization;
/**
* @var string
*/
protected $ipAddress;
/**
* @var string
*/
protected $network;
/**
* @ignore
*/
public function __construct(array $raw)
{
parent::__construct($raw);
$this->autonomousSystemNumber = $this->get('autonomous_system_number');
$this->autonomousSystemOrganization =
$this->get('autonomous_system_organization');
$ipAddress = $this->get('ip_address');
$this->ipAddress = $ipAddress;
$this->network = Util::cidr($ipAddress, $this->get('prefix_len'));
}
}
+123
View File
@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Model;
/**
* Model class for the data returned by City Plus web service and City
* database.
*
* See https://dev.maxmind.com/geoip/docs/web-services?lang=en for more
* details.
*
* @property-read \GeoIp2\Record\City $city City data for the requested IP
* address.
* @property-read \GeoIp2\Record\Location $location Location data for the
* requested IP address.
* @property-read \GeoIp2\Record\Postal $postal Postal data for the
* requested IP address.
* @property-read array $subdivisions An array \GeoIp2\Record\Subdivision
* objects representing the country subdivisions for the requested IP
* address. The number and type of subdivisions varies by country, but a
* subdivision is typically a state, province, county, etc. Subdivisions
* are ordered from most general (largest) to most specific (smallest).
* If the response did not contain any subdivisions, this method returns
* an empty array.
* @property-read \GeoIp2\Record\Subdivision $mostSpecificSubdivision An object
* representing the most specific subdivision returned. If the response
* did not contain any subdivisions, this method returns an empty
* \GeoIp2\Record\Subdivision object.
*/
class City extends Country
{
/**
* @ignore
*
* @var \GeoIp2\Record\City
*/
protected $city;
/**
* @ignore
*
* @var \GeoIp2\Record\Location
*/
protected $location;
/**
* @ignore
*
* @var \GeoIp2\Record\Postal
*/
protected $postal;
/**
* @ignore
*
* @var array<\GeoIp2\Record\Subdivision>
*/
protected $subdivisions = [];
/**
* @ignore
*/
public function __construct(array $raw, array $locales = ['en'])
{
parent::__construct($raw, $locales);
$this->city = new \GeoIp2\Record\City($this->get('city'), $locales);
$this->location = new \GeoIp2\Record\Location($this->get('location'));
$this->postal = new \GeoIp2\Record\Postal($this->get('postal'));
$this->createSubdivisions($raw, $locales);
}
private function createSubdivisions(array $raw, array $locales): void
{
if (!isset($raw['subdivisions'])) {
return;
}
foreach ($raw['subdivisions'] as $sub) {
$this->subdivisions[] =
new \GeoIp2\Record\Subdivision($sub, $locales)
;
}
}
/**
* @ignore
*
* @return mixed
*/
public function __get(string $attr)
{
if ($attr === 'mostSpecificSubdivision') {
return $this->{$attr}();
}
return parent::__get($attr);
}
/**
* @ignore
*/
public function __isset(string $attr): bool
{
if ($attr === 'mostSpecificSubdivision') {
// We always return a mostSpecificSubdivision, even if it is the
// empty subdivision
return true;
}
return parent::__isset($attr);
}
private function mostSpecificSubdivision(): \GeoIp2\Record\Subdivision
{
return empty($this->subdivisions) ?
new \GeoIp2\Record\Subdivision([], $this->locales) :
end($this->subdivisions);
}
}
+50
View File
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Model;
use GeoIp2\Util;
/**
* This class provides the GeoIP2 Connection-Type model.
*
* @property-read string|null $connectionType The connection type may take the
* following values: "Dialup", "Cable/DSL", "Corporate", "Cellular".
* Additional values may be added in the future.
* @property-read string $ipAddress The IP address that the data in the model is
* for.
* @property-read string $network The network in CIDR notation associated with
* the record. In particular, this is the largest network where all of the
* fields besides $ipAddress have the same value.
*/
class ConnectionType extends AbstractModel
{
/**
* @var string|null
*/
protected $connectionType;
/**
* @var string
*/
protected $ipAddress;
/**
* @var string
*/
protected $network;
/**
* @ignore
*/
public function __construct(array $raw)
{
parent::__construct($raw);
$this->connectionType = $this->get('connection_type');
$ipAddress = $this->get('ip_address');
$this->ipAddress = $ipAddress;
$this->network = Util::cidr($ipAddress, $this->get('prefix_len'));
}
}
+96
View File
@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Model;
/**
* Model class for the data returned by GeoIP2 Country web service and database.
*
* See https://dev.maxmind.com/geoip/docs/web-services?lang=en for more details.
*
* @property-read \GeoIp2\Record\Continent $continent Continent data for the
* requested IP address.
* @property-read \GeoIp2\Record\Country $country Country data for the requested
* IP address. This object represents the country where MaxMind believes the
* end user is located.
* @property-read \GeoIp2\Record\MaxMind $maxmind Data related to your MaxMind
* account.
* @property-read \GeoIp2\Record\Country $registeredCountry Registered country
* data for the requested IP address. This record represents the country
* where the ISP has registered a given IP block and may differ from the
* user's country.
* @property-read \GeoIp2\Record\RepresentedCountry $representedCountry
* Represented country data for the requested IP address. The represented
* country is used for things like military bases. It is only present when
* the represented country differs from the country.
* @property-read \GeoIp2\Record\Traits $traits Data for the traits of the
* requested IP address.
* @property-read array $raw The raw data from the web service.
*/
class Country extends AbstractModel
{
/**
* @var \GeoIp2\Record\Continent
*/
protected $continent;
/**
* @var \GeoIp2\Record\Country
*/
protected $country;
/**
* @var array<string>
*/
protected $locales;
/**
* @var \GeoIp2\Record\MaxMind
*/
protected $maxmind;
/**
* @var \GeoIp2\Record\Country
*/
protected $registeredCountry;
/**
* @var \GeoIp2\Record\RepresentedCountry
*/
protected $representedCountry;
/**
* @var \GeoIp2\Record\Traits
*/
protected $traits;
/**
* @ignore
*/
public function __construct(array $raw, array $locales = ['en'])
{
parent::__construct($raw);
$this->continent = new \GeoIp2\Record\Continent(
$this->get('continent'),
$locales
);
$this->country = new \GeoIp2\Record\Country(
$this->get('country'),
$locales
);
$this->maxmind = new \GeoIp2\Record\MaxMind($this->get('maxmind'));
$this->registeredCountry = new \GeoIp2\Record\Country(
$this->get('registered_country'),
$locales
);
$this->representedCountry = new \GeoIp2\Record\RepresentedCountry(
$this->get('represented_country'),
$locales
);
$this->traits = new \GeoIp2\Record\Traits($this->get('traits'));
$this->locales = $locales;
}
}
+50
View File
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Model;
use GeoIp2\Util;
/**
* This class provides the GeoIP2 Domain model.
*
* @property-read string|null $domain The second level domain associated with the
* IP address. This will be something like "example.com" or
* "example.co.uk", not "foo.example.com".
* @property-read string $ipAddress The IP address that the data in the model is
* for.
* @property-read string $network The network in CIDR notation associated with
* the record. In particular, this is the largest network where all of the
* fields besides $ipAddress have the same value.
*/
class Domain extends AbstractModel
{
/**
* @var string|null
*/
protected $domain;
/**
* @var string
*/
protected $ipAddress;
/**
* @var string
*/
protected $network;
/**
* @ignore
*/
public function __construct(array $raw)
{
parent::__construct($raw);
$this->domain = $this->get('domain');
$ipAddress = $this->get('ip_address');
$this->ipAddress = $ipAddress;
$this->network = Util::cidr($ipAddress, $this->get('prefix_len'));
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Model;
/**
* Model class for the data returned by GeoIP2 Enterprise database lookups.
*
* See https://dev.maxmind.com/geoip/docs/web-services?lang=en for more
* details.
*/
class Enterprise extends City
{
}
+15
View File
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Model;
/**
* Model class for the data returned by GeoIP2 Insights web service.
*
* See https://dev.maxmind.com/geoip/docs/web-services?lang=en for
* more details.
*/
class Insights extends City
{
}
+93
View File
@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Model;
use GeoIp2\Util;
/**
* This class provides the GeoIP2 ISP model.
*
* @property-read int|null $autonomousSystemNumber The autonomous system number
* associated with the IP address.
* @property-read string|null $autonomousSystemOrganization The organization
* associated with the registered autonomous system number for the IP
* address.
* @property-read string|null $isp The name of the ISP associated with the IP
* address.
* @property-read string|null $mobileCountryCode The [mobile country code
* (MCC)](https://en.wikipedia.org/wiki/Mobile_country_code) associated with
* the IP address and ISP.
* @property-read string|null $mobileNetworkCode The [mobile network code
* (MNC)](https://en.wikipedia.org/wiki/Mobile_country_code) associated with
* the IP address and ISP.
* @property-read string|null $organization The name of the organization associated
* with the IP address.
* @property-read string $ipAddress The IP address that the data in the model is
* for.
* @property-read string $network The network in CIDR notation associated with
* the record. In particular, this is the largest network where all of the
* fields besides $ipAddress have the same value.
*/
class Isp extends AbstractModel
{
/**
* @var int|null
*/
protected $autonomousSystemNumber;
/**
* @var string|null
*/
protected $autonomousSystemOrganization;
/**
* @var string|null
*/
protected $isp;
/**
* @var string|null
*/
protected $mobileCountryCode;
/**
* @var string|null
*/
protected $mobileNetworkCode;
/**
* @var string|null
*/
protected $organization;
/**
* @var string
*/
protected $ipAddress;
/**
* @var string
*/
protected $network;
/**
* @ignore
*/
public function __construct(array $raw)
{
parent::__construct($raw);
$this->autonomousSystemNumber = $this->get('autonomous_system_number');
$this->autonomousSystemOrganization =
$this->get('autonomous_system_organization');
$this->isp = $this->get('isp');
$this->mobileCountryCode = $this->get('mobile_country_code');
$this->mobileNetworkCode = $this->get('mobile_network_code');
$this->organization = $this->get('organization');
$ipAddress = $this->get('ip_address');
$this->ipAddress = $ipAddress;
$this->network = Util::cidr($ipAddress, $this->get('prefix_len'));
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace GeoIp2;
interface ProviderInterface
{
/**
* @param string $ipAddress an IPv4 or IPv6 address to lookup
*
* @return \GeoIp2\Model\Country a Country model for the requested IP address
*/
public function country(string $ipAddress): Model\Country;
/**
* @param string $ipAddress an IPv4 or IPv6 address to lookup
*
* @return \GeoIp2\Model\City a City model for the requested IP address
*/
public function city(string $ipAddress): Model\City;
}
+67
View File
@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Record;
abstract class AbstractPlaceRecord extends AbstractRecord
{
/**
* @var array<string>
*/
private $locales;
/**
* @ignore
*/
public function __construct(?array $record, array $locales = ['en'])
{
$this->locales = $locales;
parent::__construct($record);
}
/**
* @ignore
*
* @return mixed
*/
public function __get(string $attr)
{
if ($attr === 'name') {
return $this->name();
}
return parent::__get($attr);
}
/**
* @ignore
*/
public function __isset(string $attr): bool
{
if ($attr === 'name') {
return $this->firstSetNameLocale() !== null;
}
return parent::__isset($attr);
}
private function name(): ?string
{
$locale = $this->firstSetNameLocale();
// @phpstan-ignore-next-line
return $locale === null ? null : $this->names[$locale];
}
private function firstSetNameLocale(): ?string
{
foreach ($this->locales as $locale) {
if (isset($this->names[$locale])) {
return $locale;
}
}
return null;
}
}
+67
View File
@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Record;
abstract class AbstractRecord implements \JsonSerializable
{
/**
* @var array<string, mixed>
*/
private $record;
/**
* @ignore
*/
public function __construct(?array $record)
{
$this->record = isset($record) ? $record : [];
}
/**
* @ignore
*
* @return mixed
*/
public function __get(string $attr)
{
// XXX - kind of ugly but greatly reduces boilerplate code
$key = $this->attributeToKey($attr);
if ($this->__isset($attr)) {
return $this->record[$key];
}
if ($this->validAttribute($attr)) {
if (preg_match('/^is_/', $key)) {
return false;
}
return null;
}
throw new \RuntimeException("Unknown attribute: $attr");
}
public function __isset(string $attr): bool
{
return $this->validAttribute($attr)
&& isset($this->record[$this->attributeToKey($attr)]);
}
private function attributeToKey(string $attr): string
{
return strtolower(preg_replace('/([A-Z])/', '_\1', $attr));
}
private function validAttribute(string $attr): bool
{
// @phpstan-ignore-next-line
return \in_array($attr, $this->validAttributes, true);
}
public function jsonSerialize(): ?array
{
return $this->record;
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Record;
/**
* City-level data associated with an IP address.
*
* This record is returned by all location services and databases besides
* Country.
*
* @property-read int|null $confidence A value from 0-100 indicating MaxMind's
* confidence that the city is correct. This attribute is only available
* from the Insights service and the GeoIP2 Enterprise database.
* @property-read int|null $geonameId The GeoName ID for the city. This attribute
* is returned by all location services and databases.
* @property-read string|null $name The name of the city based on the locales list
* passed to the constructor. This attribute is returned by all location
* services and databases.
* @property-read array|null $names An array map where the keys are locale codes
* and the values are names. This attribute is returned by all location
* services and databases.
*/
class City extends AbstractPlaceRecord
{
/**
* @ignore
*
* @var array<string>
*/
protected $validAttributes = ['confidence', 'geonameId', 'names'];
}
+36
View File
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Record;
/**
* Contains data for the continent record associated with an IP address.
*
* This record is returned by all location services and databases.
*
* @property-read string|null $code A two character continent code like "NA" (North
* America) or "OC" (Oceania). This attribute is returned by all location
* services and databases.
* @property-read int|null $geonameId The GeoName ID for the continent. This
* attribute is returned by all location services and databases.
* @property-read string|null $name Returns the name of the continent based on the
* locales list passed to the constructor. This attribute is returned by all location
* services and databases.
* @property-read array|null $names An array map where the keys are locale codes
* and the values are names. This attribute is returned by all location
* services and databases.
*/
class Continent extends AbstractPlaceRecord
{
/**
* @ignore
*
* @var array<string>
*/
protected $validAttributes = [
'code',
'geonameId',
'names',
];
}
+44
View File
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Record;
/**
* Contains data for the country record associated with an IP address.
*
* This record is returned by all location services and databases.
*
* @property-read int|null $confidence A value from 0-100 indicating MaxMind's
* confidence that the country is correct. This attribute is only available
* from the Insights service and the GeoIP2 Enterprise database.
* @property-read int|null $geonameId The GeoName ID for the country. This
* attribute is returned by all location services and databases.
* @property-read bool $isInEuropeanUnion This is true if the country is a
* member state of the European Union. This attribute is returned by all
* location services and databases.
* @property-read string|null $isoCode The two-character ISO 3166-1 alpha code
* for the country. See https://en.wikipedia.org/wiki/ISO_3166-1. This
* attribute is returned by all location services and databases.
* @property-read string|null $name The name of the country based on the locales
* list passed to the constructor. This attribute is returned by all location
* services and databases.
* @property-read array|null $names An array map where the keys are locale codes
* and the values are names. This attribute is returned by all location
* services and databases.
*/
class Country extends AbstractPlaceRecord
{
/**
* @ignore
*
* @var array<string>
*/
protected $validAttributes = [
'confidence',
'geonameId',
'isInEuropeanUnion',
'isoCode',
'names',
];
}
+56
View File
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Record;
/**
* Contains data for the location record associated with an IP address.
*
* This record is returned by all location services and databases besides
* Country.
*
* @property-read int|null $averageIncome The average income in US dollars
* associated with the requested IP address. This attribute is only available
* from the Insights service.
* @property-read int|null $accuracyRadius The approximate accuracy radius in
* kilometers around the latitude and longitude for the IP address. This is
* the radius where we have a 67% confidence that the device using the IP
* address resides within the circle centered at the latitude and longitude
* with the provided radius.
* @property-read float|null $latitude The approximate latitude of the location
* associated with the IP address. This value is not precise and should not be
* used to identify a particular address or household.
* @property-read float|null $longitude The approximate longitude of the location
* associated with the IP address. This value is not precise and should not be
* used to identify a particular address or household.
* @property-read int|null $populationDensity The estimated population per square
* kilometer associated with the IP address. This attribute is only available
* from the Insights service.
* @property-read int|null $metroCode The metro code of the location if the location
* is in the US. MaxMind returns the same metro codes as the
* Google AdWords API. See
* https://developers.google.com/adwords/api/docs/appendix/cities-DMAregions.
* @property-read string|null $timeZone The time zone associated with location, as
* specified by the IANA Time Zone Database, e.g., "America/New_York". See
* https://www.iana.org/time-zones.
*/
class Location extends AbstractRecord
{
/**
* @ignore
*
* @var array<string>
*/
protected $validAttributes = [
'averageIncome',
'accuracyRadius',
'latitude',
'longitude',
'metroCode',
'populationDensity',
'postalCode',
'postalConfidence',
'timeZone',
];
}
+23
View File
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Record;
/**
* Contains data about your account.
*
* This record is returned by all location services and databases.
*
* @property-read int|null $queriesRemaining The number of remaining queries you
* have for the service you are calling.
*/
class MaxMind extends AbstractRecord
{
/**
* @ignore
*
* @var array<string>
*/
protected $validAttributes = ['queriesRemaining'];
}
+30
View File
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Record;
/**
* Contains data for the postal record associated with an IP address.
*
* This record is returned by all location databases and services besides
* Country.
*
* @property-read string|null $code The postal code of the location. Postal codes
* are not available for all countries. In some countries, this will only
* contain part of the postal code. This attribute is returned by all location
* databases and services besides Country.
* @property-read int|null $confidence A value from 0-100 indicating MaxMind's
* confidence that the postal code is correct. This attribute is only
* available from the Insights service and the GeoIP2 Enterprise
* database.
*/
class Postal extends AbstractRecord
{
/**
* @ignore
*
* @var array<string>
*/
protected $validAttributes = ['code', 'confidence'];
}
+33
View File
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Record;
/**
* Contains data for the represented country associated with an IP address.
*
* This class contains the country-level data associated with an IP address
* for the IP's represented country. The represented country is the country
* represented by something like a military base.
*
* @property-read string|null $type A string indicating the type of entity that is
* representing the country. Currently we only return <code>military</code>
* but this could expand to include other types in the future.
*/
class RepresentedCountry extends Country
{
/**
* @ignore
*
* @var array<string>
*/
protected $validAttributes = [
'confidence',
'geonameId',
'isInEuropeanUnion',
'isoCode',
'names',
'type',
];
}
+44
View File
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Record;
/**
* Contains data for the subdivisions associated with an IP address.
*
* This record is returned by all location databases and services besides
* Country.
*
* @property-read int|null $confidence This is a value from 0-100 indicating
* MaxMind's confidence that the subdivision is correct. This attribute is
* only available from the Insights service and the GeoIP2 Enterprise
* database.
* @property-read int|null $geonameId This is a GeoName ID for the subdivision.
* This attribute is returned by all location databases and services besides
* Country.
* @property-read string|null $isoCode This is a string up to three characters long
* contain the subdivision portion of the ISO 3166-2 code. See
* https://en.wikipedia.org/wiki/ISO_3166-2. This attribute is returned by all
* location databases and services except Country.
* @property-read string|null $name The name of the subdivision based on the
* locales list passed to the constructor. This attribute is returned by all
* location databases and services besides Country.
* @property-read array|null $names An array map where the keys are locale codes
* and the values are names. This attribute is returned by all location
* databases and services besides Country.
*/
class Subdivision extends AbstractPlaceRecord
{
/**
* @ignore
*
* @var array<string>
*/
protected $validAttributes = [
'confidence',
'geonameId',
'isoCode',
'names',
];
}
+158
View File
@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Record;
use GeoIp2\Util;
/**
* Contains data for the traits record associated with an IP address.
*
* This record is returned by all location services and databases.
*
* @property-read int|null $autonomousSystemNumber The autonomous system number
* associated with the IP address. See
* https://en.wikipedia.org/wiki/Autonomous_system_(Internet%29. This attribute
* is only available from the City Plus and Insights web services and the
* GeoIP2 Enterprise database.
* @property-read string|null $autonomousSystemOrganization The organization
* associated with the registered autonomous system number for the IP address.
* See https://en.wikipedia.org/wiki/Autonomous_system_(Internet%29. This
* attribute is only available from the City Plus and Insights web services and
* the GeoIP2 Enterprise database.
* @property-read string|null $connectionType The connection type may take the
* following values: "Dialup", "Cable/DSL", "Corporate", "Cellular".
* Additional values may be added in the future. This attribute is only
* available in the GeoIP2 Enterprise database.
* @property-read string|null $domain The second level domain associated with the
* IP address. This will be something like "example.com" or "example.co.uk",
* not "foo.example.com". This attribute is only available from the
* City Plus and Insights web services and the GeoIP2 Enterprise
* database.
* @property-read string $ipAddress The IP address that the data in the model
* is for. If you performed a "me" lookup against the web service, this
* will be the externally routable IP address for the system the code is
* running on. If the system is behind a NAT, this may differ from the IP
* address locally assigned to it. This attribute is returned by all end
* points.
* @property-read bool $isAnonymous This is true if the IP address belongs to
* any sort of anonymous network. This property is only available from GeoIP2
* Insights.
* @property-read bool $isAnonymousProxy *Deprecated.* Please see our GeoIP2
* Anonymous IP database
* (https://www.maxmind.com/en/geoip2-anonymous-ip-database) to determine
* whether the IP address is used by an anonymizing service.
* @property-read bool $isAnonymousVpn This is true if the IP address is
* registered to an anonymous VPN provider. If a VPN provider does not register
* subnets under names associated with them, we will likely only flag their IP
* ranges using the isHostingProvider property. This property is only available
* from GeoIP2 Insights.
* @property-read bool $isHostingProvider This is true if the IP address belongs
* to a hosting or VPN provider (see description of isAnonymousVpn property).
* This property is only available from GeoIP2 Insights.
* @property-read bool $isLegitimateProxy This attribute is true if MaxMind
* believes this IP address to be a legitimate proxy, such as an internal
* VPN used by a corporation. This attribute is only available in the GeoIP2
* Enterprise database.
* @property-read bool $isPublicProxy This is true if the IP address belongs to
* a public proxy. This property is only available from GeoIP2 Insights.
* @property-read bool $isResidentialProxy This is true if the IP address is
* on a suspected anonymizing network and belongs to a residential ISP. This
* property is only available from GeoIP2 Insights.
* @property-read bool $isSatelliteProvider *Deprecated.* Due to the
* increased coverage by mobile carriers, very few satellite providers now
* serve multiple countries. As a result, the output does not provide
* sufficiently relevant data for us to maintain it.
* @property-read bool $isTorExitNode This is true if the IP address is a Tor
* exit node. This property is only available from GeoIP2 Insights.
* @property-read string|null $isp The name of the ISP associated with the IP
* address. This attribute is only available from the City Plus and Insights
* web services and the GeoIP2 Enterprise database.
* @property-read string $network The network in CIDR notation associated with
* the record. In particular, this is the largest network where all of the
* fields besides $ipAddress have the same value.
* @property-read string|null $organization The name of the organization
* associated with the IP address. This attribute is only available from the
* City Plus and Insights web services and the GeoIP2 Enterprise database.
* @property-read string|null $mobileCountryCode The [mobile country code
* (MCC)](https://en.wikipedia.org/wiki/Mobile_country_code) associated with
* the IP address and ISP. This property is available from the City Plus and
* Insights web services and the GeoIP2 Enterprise database.
* @property-read string|null $mobileNetworkCode The [mobile network code
* (MNC)](https://en.wikipedia.org/wiki/Mobile_country_code) associated with
* the IP address and ISP. This property is available from the City Plus and
* Insights web services and the GeoIP2 Enterprise database.
* @property-read float|null $staticIpScore An indicator of how static or
* dynamic an IP address is. This property is only available from GeoIP2
* Insights.
* @property-read int|null $userCount The estimated number of users sharing
* the IP/network during the past 24 hours. For IPv4, the count is for the
* individual IP. For IPv6, the count is for the /64 network. This property is
* only available from GeoIP2 Insights.
* @property-read string|null $userType <p>The user type associated with the IP
* address. This can be one of the following values:</p>
* <ul>
* <li>business
* <li>cafe
* <li>cellular
* <li>college
* <li>consumer_privacy_network
* <li>content_delivery_network
* <li>dialup
* <li>government
* <li>hosting
* <li>library
* <li>military
* <li>residential
* <li>router
* <li>school
* <li>search_engine_spider
* <li>traveler
* </ul>
* <p>
* This attribute is only available from the Insights web service and the
* GeoIP2 Enterprise database.
* </p>
*/
class Traits extends AbstractRecord
{
/**
* @ignore
*
* @var array<string>
*/
protected $validAttributes = [
'autonomousSystemNumber',
'autonomousSystemOrganization',
'connectionType',
'domain',
'ipAddress',
'isAnonymous',
'isAnonymousProxy',
'isAnonymousVpn',
'isHostingProvider',
'isLegitimateProxy',
'isp',
'isPublicProxy',
'isResidentialProxy',
'isSatelliteProvider',
'isTorExitNode',
'mobileCountryCode',
'mobileNetworkCode',
'network',
'organization',
'staticIpScore',
'userCount',
'userType',
];
public function __construct(?array $record)
{
if (!isset($record['network']) && isset($record['ip_address'], $record['prefix_len'])) {
$record['network'] = Util::cidr($record['ip_address'], $record['prefix_len']);
}
parent::__construct($record);
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace GeoIp2;
class Util
{
/**
* This returns the network in CIDR notation for the given IP and prefix
* length. This is for internal use only.
*
* @internal
* @ignore
*/
public static function cidr(string $ipAddress, int $prefixLen): string
{
$ipBytes = inet_pton($ipAddress);
$networkBytes = str_repeat("\0", \strlen($ipBytes));
$curPrefix = $prefixLen;
for ($i = 0; $i < \strlen($ipBytes) && $curPrefix > 0; $i++) {
$b = $ipBytes[$i];
if ($curPrefix < 8) {
$shiftN = 8 - $curPrefix;
$b = \chr(0xFF & (\ord($b) >> $shiftN) << $shiftN);
}
$networkBytes[$i] = $b;
$curPrefix -= 8;
}
$network = inet_ntop($networkBytes);
return "$network/$prefixLen";
}
}
+255
View File
@@ -0,0 +1,255 @@
<?php
declare(strict_types=1);
namespace GeoIp2\WebService;
use GeoIp2\Exception\AddressNotFoundException;
use GeoIp2\Exception\AuthenticationException;
use GeoIp2\Exception\GeoIp2Exception;
use GeoIp2\Exception\HttpException;
use GeoIp2\Exception\InvalidRequestException;
use GeoIp2\Exception\OutOfQueriesException;
use GeoIp2\Model\City;
use GeoIp2\Model\Country;
use GeoIp2\Model\Insights;
use GeoIp2\ProviderInterface;
use MaxMind\WebService\Client as WsClient;
/**
* This class provides a client API for all the GeoIP2 web services.
* The services are Country, City Plus, and Insights. Each service returns
* a different set of data about an IP address, with Country returning the
* least data and Insights the most.
*
* Each web service is represented by a different model class, and these model
* classes in turn contain multiple record classes. The record classes have
* attributes which contain data about the IP address.
*
* If the web service does not return a particular piece of data for an IP
* address, the associated attribute is not populated.
*
* The web service may not return any information for an entire record, in
* which case all of the attributes for that record class will be empty.
*
* ## Usage ##
*
* The basic API for this class is the same for all of the web service end
* points. First you create a web service object with your MaxMind `$accountId`
* and `$licenseKey`, then you call the method corresponding to a specific end
* point, passing it the IP address you want to look up.
*
* If the request succeeds, the method call will return a model class for
* the service you called. This model in turn contains multiple record
* classes, each of which represents part of the data returned by the web
* service.
*
* If the request fails, the client class throws an exception.
*/
class Client implements ProviderInterface
{
/**
* @var array<string>
*/
private $locales;
/**
* @var WsClient
*/
private $client;
/**
* @var string
*/
private static $basePath = '/geoip/v2.1';
public const VERSION = 'v2.13.0';
/**
* Constructor.
*
* @param int $accountId your MaxMind account ID
* @param string $licenseKey your MaxMind license key
* @param array $locales list of locale codes to use in name property
* from most preferred to least preferred
* @param array $options array of options. Valid options include:
* * `host` - The host to use when querying the web
* service. To query the GeoLite2 web service
* instead of the GeoIP2 web service, set the
* host to `geolite.info`.
* * `timeout` - Timeout in seconds.
* * `connectTimeout` - Initial connection timeout in seconds.
* * `proxy` - The HTTP proxy to use. May include a schema, port,
* username, and password, e.g.,
* `http://username:password@127.0.0.1:10`.
*/
public function __construct(
int $accountId,
string $licenseKey,
array $locales = ['en'],
array $options = []
) {
$this->locales = $locales;
// This is for backwards compatibility. Do not remove except for a
// major version bump.
// @phpstan-ignore-next-line
if (\is_string($options)) {
$options = ['host' => $options];
}
if (!isset($options['host'])) {
$options['host'] = 'geoip.maxmind.com';
}
$options['userAgent'] = $this->userAgent();
$this->client = new WsClient($accountId, $licenseKey, $options);
}
private function userAgent(): string
{
return 'GeoIP2-API/' . self::VERSION;
}
/**
* This method calls the City Plus service.
*
* @param string $ipAddress IPv4 or IPv6 address as a string. If no
* address is provided, the address that the web service is called
* from will be used.
*
* @throws \GeoIp2\Exception\AddressNotFoundException if the address you
* provided is not in our database (e.g., a private address).
* @throws \GeoIp2\Exception\AuthenticationException if there is a problem
* with the account ID or license key that you provided
* @throws \GeoIp2\Exception\OutOfQueriesException if your account is out
* of queries
* @throws \GeoIp2\Exception\InvalidRequestException} if your request was received by the web service but is
* invalid for some other reason. This may indicate an issue
* with this API. Please report the error to MaxMind.
* @throws \GeoIp2\Exception\HttpException if an unexpected HTTP error code or message was returned.
* This could indicate a problem with the connection between
* your server and the web service or that the web service
* returned an invalid document or 500 error code
* @throws \GeoIp2\Exception\GeoIp2Exception This serves as the parent
* class to the above exceptions. It will be thrown directly
* if a 200 status code is returned but the body is invalid.
*/
public function city(string $ipAddress = 'me'): City
{
// @phpstan-ignore-next-line
return $this->responseFor('city', City::class, $ipAddress);
}
/**
* This method calls the Country service.
*
* @param string $ipAddress IPv4 or IPv6 address as a string. If no
* address is provided, the address that the web service is called
* from will be used.
*
* @throws \GeoIp2\Exception\AddressNotFoundException if the address you provided is not in our database (e.g.,
* a private address).
* @throws \GeoIp2\Exception\AuthenticationException if there is a problem
* with the account ID or license key that you provided
* @throws \GeoIp2\Exception\OutOfQueriesException if your account is out of queries
* @throws \GeoIp2\Exception\InvalidRequestException} if your request was received by the web service but is
* invalid for some other reason. This may indicate an
* issue with this API. Please report the error to MaxMind.
* @throws \GeoIp2\Exception\HttpException if an unexpected HTTP error
* code or message was returned. This could indicate a problem
* with the connection between your server and the web service
* or that the web service returned an invalid document or 500
* error code.
* @throws \GeoIp2\Exception\GeoIp2Exception This serves as the parent class to the above exceptions. It
* will be thrown directly if a 200 status code is returned but
* the body is invalid.
*/
public function country(string $ipAddress = 'me'): Country
{
return $this->responseFor('country', Country::class, $ipAddress);
}
/**
* This method calls the Insights service. Insights is only supported by
* the GeoIP2 web service. The GeoLite2 web service does not support it.
*
* @param string $ipAddress IPv4 or IPv6 address as a string. If no
* address is provided, the address that the web service is called
* from will be used.
*
* @throws \GeoIp2\Exception\AddressNotFoundException if the address you
* provided is not in our database (e.g., a private address).
* @throws \GeoIp2\Exception\AuthenticationException if there is a problem
* with the account ID or license key that you provided
* @throws \GeoIp2\Exception\OutOfQueriesException if your account is out
* of queries
* @throws \GeoIp2\Exception\InvalidRequestException} if your request was received by the web service but is
* invalid for some other reason. This may indicate an
* issue with this API. Please report the error to MaxMind.
* @throws \GeoIp2\Exception\HttpException if an unexpected HTTP error code or message was returned.
* This could indicate a problem with the connection between
* your server and the web service or that the web service
* returned an invalid document or 500 error code
* @throws \GeoIp2\Exception\GeoIp2Exception This serves as the parent
* class to the above exceptions. It will be thrown directly
* if a 200 status code is returned but the body is invalid.
*/
public function insights(string $ipAddress = 'me'): Insights
{
// @phpstan-ignore-next-line
return $this->responseFor('insights', Insights::class, $ipAddress);
}
private function responseFor(string $endpoint, string $class, string $ipAddress): Country
{
$path = implode('/', [self::$basePath, $endpoint, $ipAddress]);
try {
$service = (new \ReflectionClass($class))->getShortName();
$body = $this->client->get('GeoIP2 ' . $service, $path);
} catch (\MaxMind\Exception\IpAddressNotFoundException $ex) {
throw new AddressNotFoundException(
$ex->getMessage(),
$ex->getStatusCode(),
$ex
);
} catch (\MaxMind\Exception\AuthenticationException $ex) {
throw new AuthenticationException(
$ex->getMessage(),
$ex->getStatusCode(),
$ex
);
} catch (\MaxMind\Exception\InsufficientFundsException $ex) {
throw new OutOfQueriesException(
$ex->getMessage(),
$ex->getStatusCode(),
$ex
);
} catch (\MaxMind\Exception\InvalidRequestException $ex) {
throw new InvalidRequestException(
$ex->getMessage(),
$ex->getErrorCode(),
$ex->getStatusCode(),
$ex->getUri(),
$ex
);
} catch (\MaxMind\Exception\HttpException $ex) {
throw new HttpException(
$ex->getMessage(),
$ex->getStatusCode(),
$ex->getUri(),
$ex
);
} catch (\MaxMind\Exception\WebServiceException $ex) {
throw new GeoIp2Exception(
$ex->getMessage(),
$ex->getCode(),
$ex
);
}
return new $class($body, $this->locales);
}
}
@@ -0,0 +1,93 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use League\Uri\Exceptions\MissingFeature;
use League\Uri\Exceptions\SyntaxError;
use Stringable;
interface AuthorityInterface extends UriComponentInterface
{
/**
* Returns the host component of the authority.
*/
public function getHost(): ?string;
/**
* Returns the port component of the authority.
*/
public function getPort(): ?int;
/**
* Returns the user information component of the authority.
*/
public function getUserInfo(): ?string;
/**
* Returns an associative array containing all the Authority components.
*
* The returned a hashmap similar to PHP's parse_url return value
*
* @link https://tools.ietf.org/html/rfc3986
*
* @return array{user: ?string, pass : ?string, host: ?string, port: ?int}
*/
public function components(): array;
/**
* Return an instance with the specified host.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified host.
*
* A null value provided for the host is equivalent to removing the host
* information.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
* @throws MissingFeature for component or transformations
* requiring IDN support when IDN support is not present
* or misconfigured.
*/
public function withHost(Stringable|string|null $host): self;
/**
* Return an instance with the specified port.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified port.
*
* A null value provided for the port is equivalent to removing the port
* information.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withPort(?int $port): self;
/**
* Return an instance with the specified user information.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified user information.
*
* Password is optional, but the user information MUST include the
* user; a null value for the user is equivalent to removing user
* information.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withUserInfo(Stringable|string|null $user, Stringable|string|null $password = null): self;
}
@@ -0,0 +1,26 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
interface Conditionable
{
/**
* Apply the callback if the given "condition" is (or resolves to) true.
*
* @param (callable(static): bool)|bool $condition
* @param callable(static): (static|null) $onSuccess
* @param ?callable(static): (static|null) $onFail
*/
public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): static;
}
@@ -0,0 +1,95 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use SplFileObject;
use Stringable;
interface DataPathInterface extends PathInterface
{
/**
* Retrieve the data mime type associated to the URI.
*
* If no mimetype is present, this method MUST return the default mimetype 'text/plain'.
*
* @see http://tools.ietf.org/html/rfc2397#section-2
*/
public function getMimeType(): string;
/**
* Retrieve the parameters associated with the Mime Type of the URI.
*
* If no parameters is present, this method MUST return the default parameter 'charset=US-ASCII'.
*
* @see http://tools.ietf.org/html/rfc2397#section-2
*/
public function getParameters(): string;
/**
* Retrieve the mediatype associated with the URI.
*
* If no mediatype is present, this method MUST return the default parameter 'text/plain;charset=US-ASCII'.
*
* @see http://tools.ietf.org/html/rfc2397#section-3
*
* @return string The URI scheme.
*/
public function getMediaType(): string;
/**
* Retrieves the data string.
*
* Retrieves the data part of the path. If no data part is provided return
* an empty string
*/
public function getData(): string;
/**
* Tells whether the data is binary safe encoded.
*/
public function isBinaryData(): bool;
/**
* Save the data to a specific file.
*/
public function save(string $path, string $mode = 'w'): SplFileObject;
/**
* Returns an instance where the data part is base64 encoded.
*
* This method MUST retain the state of the current instance, and return
* an instance where the data part is base64 encoded
*/
public function toBinary(): self;
/**
* Returns an instance where the data part is url encoded following RFC3986 rules.
*
* This method MUST retain the state of the current instance, and return
* an instance where the data part is url encoded
*/
public function toAscii(): self;
/**
* Return an instance with the specified mediatype parameters.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified mediatype parameters.
*
* Users must provide encoded characters.
*
* An empty parameters value is equivalent to removing the parameter.
*/
public function withParameters(Stringable|string $parameters): self;
}
@@ -0,0 +1,125 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use BackedEnum;
use Countable;
use Iterator;
use IteratorAggregate;
use League\Uri\Exceptions\SyntaxError;
use Stringable;
/**
* @extends IteratorAggregate<int, string>
*
* @method bool isSubdomainOf(BackedEnum|Stringable|string|null $parentHost) Tells whether the current domain instance is a subdomain of the parent host.
* @method bool hasSubdomain(BackedEnum|Stringable|string|null $childHost) Tells whether the submitted host is a subdomain of the current instance.
* @method bool isSiblingOf(BackedEnum|Stringable|string|null $siblingHost) Tells whether the submitted host share the same parent domain as the current instance.
* @method static commonAncestorWith(BackedEnum|Stringable|string|null $other) Returns the common longest ancestor between 2 domain. The returned domain is empty if no ancestor is found
* @method static parentHost() Returns the current parent domain for the current instance. The returned domain is empty if no ancestor is found
* @method bool isEmpty() Tells whether the domain contains any label.
*/
interface DomainHostInterface extends Countable, HostInterface, IteratorAggregate
{
/**
* Returns the labels total number.
*/
public function count(): int;
/**
* Iterate over the Domain labels.
*
* @return Iterator<string>
*/
public function getIterator(): Iterator;
/**
* Retrieves a single host label.
*
* If the label offset has not been set, returns the null value.
*/
public function get(int $offset): ?string;
/**
* Returns the associated key for a specific label or all the keys.
*
* @return int[]
*/
public function keys(?string $label = null): array;
/**
* Tells whether the domain is absolute.
*/
public function isAbsolute(): bool;
/**
* Prepends a label to the host.
*/
public function prepend(Stringable|string $label): self;
/**
* Appends a label to the host.
*/
public function append(Stringable|string $label): self;
/**
* Extracts a slice of $length elements starting at position $offset from the host.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the selected slice.
*
* If $length is null it returns all elements from $offset to the end of the Domain.
*/
public function slice(int $offset, ?int $length = null): self;
/**
* Returns an instance with its Root label.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
*/
public function withRootLabel(): self;
/**
* Returns an instance without its Root label.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
*/
public function withoutRootLabel(): self;
/**
* Returns an instance with the modified label.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the new label
*
* If $key is non-negative, the added label will be the label at $key position from the start.
* If $key is negative, the added label will be the label at $key position from the end.
*
* @throws SyntaxError If the key is invalid
*/
public function withLabel(int $key, Stringable|string $label): self;
/**
* Returns an instance without the specified label.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified component
*
* If $key is non-negative, the removed label will be the label at $key position from the start.
* If $key is negative, the removed label will be the label at $key position from the end.
*
* @throws SyntaxError If the key is invalid
*/
public function withoutLabel(int ...$keys): self;
}
@@ -0,0 +1,55 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use Stringable;
/**
* @see https://wicg.github.io/scroll-to-text-fragment/#the-fragment-directive
*
* @method string toFragmentValue() returns the encoded string representation of the directive as a fragment string
*/
interface FragmentDirective extends Stringable
{
/**
* The decoded Directive name.
*
* @return non-empty-string
*/
public function name(): string;
/**
* The decoded Directive value.
*/
public function value(): ?string;
/**
* The encoded string representation of the directive.
*/
public function toString(): string;
/**
* The encoded string representation of the fragment using
* the Stringable interface.
*
* @see FragmentDirective::toString()
*/
public function __toString(): string;
/**
* Tells whether the submitted value is equals to the string
* representation of the given directive.
*/
public function equals(mixed $directive): bool;
}
@@ -0,0 +1,25 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
/**
* @method self normalize() returns the normalized string representation of the component
*/
interface FragmentInterface extends UriComponentInterface
{
/**
* Returns the decoded fragment.
*/
public function decoded(): ?string;
}
@@ -0,0 +1,59 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
/**
* @method string|null encoded() returns RFC3986 encoded host
*/
interface HostInterface extends UriComponentInterface
{
/**
* Returns the ascii representation.
*/
public function toAscii(): ?string;
/**
* Returns the unicode representation.
*/
public function toUnicode(): ?string;
/**
* Returns the IP version.
*
* If the host is a not an IP this method will return null
*/
public function getIpVersion(): ?string;
/**
* Returns the IP component If the Host is an IP address.
*
* If the host is a not an IP this method will return null
*/
public function getIp(): ?string;
/**
* Tells whether the host is a domain name.
*/
public function isDomain(): bool;
/**
* Tells whether the host is an IP Address.
*/
public function isIp(): bool;
/**
* Tells whether the host is a registered name.
*/
public function isRegisteredName(): bool;
}
@@ -0,0 +1,49 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
interface IpHostInterface extends HostInterface
{
/**
* Tells whether the host is an IPv4 address.
*/
public function isIpv4(): bool;
/**
* Tells whether the host is an IPv6 address.
*/
public function isIpv6(): bool;
/**
* Tells whether the host is an IPv6 address.
*/
public function isIpFuture(): bool;
/**
* Tells whether the host has a ZoneIdentifier.
*
* @see http://tools.ietf.org/html/rfc6874#section-4
*/
public function hasZoneIdentifier(): bool;
/**
* Returns a host without its zone identifier according to RFC6874.
*
* This method MUST retain the state of the current instance, and return
* an instance without the host zone identifier according to RFC6874
*
* @see http://tools.ietf.org/html/rfc6874#section-4
*/
public function withoutZoneIdentifier(): self;
}
@@ -0,0 +1,93 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use League\Uri\Exceptions\SyntaxError;
/**
* @method static normalize() returns the normalized string representation of the component
*/
interface PathInterface extends UriComponentInterface
{
/**
* Returns the decoded path.
*/
public function decoded(): string;
/**
* Tells whether the path is absolute or relative.
*/
public function isAbsolute(): bool;
/**
* Tells whether the path has a trailing slash.
*/
public function hasTrailingSlash(): bool;
/**
* Returns an instance without dot segments.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the path component normalized by removing
* the dot segment.
*
* @throws SyntaxError for invalid component or transformations
* that would result in a object in invalid state.
*/
public function withoutDotSegments(): self;
/**
* Returns an instance with a leading slash.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the path component with a leading slash
*
* @throws SyntaxError for invalid component or transformations
* that would result in a object in invalid state.
*/
public function withLeadingSlash(): self;
/**
* Returns an instance without a leading slash.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the path component without a leading slash
*
* @throws SyntaxError for invalid component or transformations
* that would result in a object in invalid state.
*/
public function withoutLeadingSlash(): self;
/**
* Returns an instance with a trailing slash.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the path component with a trailing slash
*
* @throws SyntaxError for invalid component or transformations
* that would result in a object in invalid state.
*/
public function withTrailingSlash(): self;
/**
* Returns an instance without a trailing slash.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the path component without a trailing slash
*
* @throws SyntaxError for invalid component or transformations
* that would result in a object in invalid state.
*/
public function withoutTrailingSlash(): self;
}
@@ -0,0 +1,22 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
interface PortInterface extends UriComponentInterface
{
/**
* Returns the integer representation of the Port.
*/
public function toInt(): ?int;
}
@@ -0,0 +1,272 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use BackedEnum;
use Countable;
use Deprecated;
use Iterator;
use IteratorAggregate;
use League\Uri\QueryComposeMode;
use League\Uri\StringCoercionMode;
use Stringable;
/**
* @extends IteratorAggregate<array{0:string, 1:string|null}>
*
* @method string|null toFormData() Returns the string representation using the application/www-form-urlencoded rules
* @method string|null toRFC3986() Returns the string representation using RFC3986 rules
* @method string|null first(string $key) Returns the first value associated with the given name
* @method string|null last(string $key) Returns the first value associated with the given name
* @method int|null indexOf(string $key, int $nth = 0) Returns the offset of the pair based on its key and its nth occurrence; negative occurrences are supported
* @method int|null indexOfValue(?string $value, int $nth = 0) Returns the offset of the pair based on its value and its nth occurrence; negative occurrences are supported
* @method array pair(int $offset) Returns the key/value pair at the given numeric offset; negative occurrences are supported
* @method int countDistinctKeys() Returns the total number of distinct keys
* @method string|null valueAt(int $offset): Returns the value at the given numeric offset; negative occurrences are supported
* @method string keyAt(int $offset): Returns the key at the given numeric offset; negative occurrences are supported
* @method self normalize() returns the normalized string representation of the component
* @method self withoutPairByKey(string ...$keys) Returns an instance without pairs with the specified keys.
* @method self withoutPairByValue(array|BackedEnum|Stringable|string|int|bool|null $values, StringCoercionMode $coercionMode = StringCoercionMode::Native) Returns an instance without pairs with the specified values.
* @method self withoutPairByKeyValue(string $key, BackedEnum|Stringable|string|int|bool|null $value, StringCoercionMode $coercionMode = StringCoercionMode::Native) Returns an instance without pairs with the specified key/value pair
* @method bool hasPair(string $key, ?string $value) Tells whether the pair exists in the query.
* @method array getList(string $name) Returns the list associated with the given name or an empty array if it does not exist.
* @method bool hasList(string ...$names) Tells whether the parameter list exists in the query.
* @method self appendList(string $name, array $values, QueryComposeMode $composeMode = QueryComposeMode::Native) Appends a parameter to the query string
* @method self withList(string $name, array $values, QueryComposeMode $composeMode = QueryComposeMode::Native) Adds a new parameter to the query string and remove any previously set values
* @method self withoutList(string ...$names) Removes any given list associated with the given names
* @method self withoutLists() Removes all lists from the query string
* @method self onlyLists() Removes all pairs without a valid PHP's bracket notation
*/
interface QueryInterface extends Countable, IteratorAggregate, UriComponentInterface
{
/**
* Returns the query separator.
*
* @return non-empty-string
*/
public function getSeparator(): string;
/**
* Returns the number of key/value pairs present in the object.
*/
public function count(): int;
/**
* Returns an iterator allowing to go through all key/value pairs contained in this object.
*
* The pair is represented as an array where the first value is the pair key
* and the second value the pair value.
*
* The key of each pair is a string
* The value of each pair is a scalar or the null value
*
* @return Iterator<int, array{0:string, 1:string|null}>
*/
public function getIterator(): Iterator;
/**
* Returns an iterator allowing to go through all key/value pairs contained in this object.
*
* The return type is as an Iterator where its offset is the pair key and its value the pair value.
*
* The key of each pair is a string
* The value of each pair is a scalar or the null value
*
* @return iterable<string, string|null>
*/
public function pairs(): iterable;
/**
* Tells whether a list of pair with a specific key exists.
*
* @see https://url.spec.whatwg.org/#dom-urlsearchparams-has
*/
public function has(string ...$keys): bool;
/**
* Returns the first value associated to the given pair name.
*
* If no value is found null is returned
*
* @see https://url.spec.whatwg.org/#dom-urlsearchparams-get
*/
public function get(string $key): ?string;
/**
* Returns all the values associated to the given pair name as an array or all
* the instance pairs.
*
* If no value is found an empty array is returned
*
* @see https://url.spec.whatwg.org/#dom-urlsearchparams-getall
*
* @return array<int, string|null>
*/
public function getAll(string $key): array;
/**
* Returns the store PHP variables as elements of an array.
*
* The result is similar as PHP parse_str when used with its
* second argument with the difference that variable names are
* not mangled.
*
* @see http://php.net/parse_str
* @see https://wiki.php.net/rfc/on_demand_name_mangling
*
* @return array the collection of stored PHP variables or the empty array if no input is given,
*/
public function parameters(): array;
/**
* Returns the value attached to the specific key.
*
* The result is similar to PHP parse_str with the difference that variable
* names are not mangled.
*
* If a key is submitted it will return the value attached to it or null
*
* @see http://php.net/parse_str
* @see https://wiki.php.net/rfc/on_demand_name_mangling
*
* @return mixed the collection of stored PHP variables or the empty array if no input is given,
* the single value of a stored PHP variable or null if the variable is not present in the collection
*/
public function parameter(string $name): mixed;
/**
* Tells whether a list of variable with specific names exists.
*
* @see https://url.spec.whatwg.org/#dom-urlsearchparams-has
*/
public function hasParameter(string ...$names): bool;
/**
* Returns the RFC1738 encoded query.
*/
public function toRFC1738(): ?string;
/**
* Returns an instance with a different separator.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the query component with a different separator
*/
public function withSeparator(string $separator): self;
/**
* Returns an instance with the new pairs set to it.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified query
*
* @see ::withPair
*/
public function merge(Stringable|string $query): self;
/**
* Returns an instance with the new pairs appended to it.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified query
*
* If the pair already exists the value will be added to it.
*/
public function append(Stringable|string $query): self;
/**
* Returns a new instance with a specified key/value pair appended as a new pair.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified query
*/
public function appendTo(string $key, Stringable|string|int|bool|null $value): self;
/**
* Sorts the query string by offset, maintaining offset to data correlations.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified query
*
* @see https://url.spec.whatwg.org/#dom-urlsearchparams-sort
*/
public function sort(): self;
/**
* Returns an instance without duplicate key/value pair.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the query component normalized by removing
* duplicate pairs whose key/value are the same.
*/
public function withoutDuplicates(): self;
/**
* Returns an instance without empty key/value where the value is the null value.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the query component normalized by removing
* empty pairs.
*
* A pair is considered empty if its value is equal to the null value
*/
public function withoutEmptyPairs(): self;
/**
* Returns an instance where numeric indices associated to PHP's array like key are removed.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the query component normalized so that numeric indexes
* are removed from the pair key value.
*
* i.e.: toto[3]=bar[3]&foo=bar becomes toto[]=bar[3]&foo=bar
*/
public function withoutNumericIndices(): self;
/**
* Returns an instance with a new key/value pair added to it.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified query
*
* If the pair already exists the value will replace the existing value.
*
* @see https://url.spec.whatwg.org/#dom-urlsearchparams-set
*/
public function withPair(string $key, Stringable|string|int|float|bool|null $value): self;
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.3.0
* @codeCoverageIgnore
* @see QueryInterface::withoutPairByKey()
*
* Returns an instance without the specified keys.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified component
*/
#[Deprecated(message:'use League\Uri\Contracts\QueryInterface::withoutPairByKey() instead', since:'league/uri-interfaces:7.3.0')]
public function withoutPair(string ...$keys): self;
/**
* Returns an instance without the specified params.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified component without PHP's value.
* PHP's mangled is not taken into account.
*/
public function withoutParameters(string ...$names): self;
}
@@ -0,0 +1,149 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use Countable;
use Iterator;
use IteratorAggregate;
use League\Uri\Exceptions\SyntaxError;
use Stringable;
/**
* @extends IteratorAggregate<string>
*/
interface SegmentedPathInterface extends Countable, IteratorAggregate, PathInterface
{
/**
* Returns the total number of segments in the path.
*/
public function count(): int;
/**
* Iterate over the path segment.
*
* @return Iterator<string>
*/
public function getIterator(): Iterator;
/**
* Returns parent directory's path.
*/
public function getDirname(): string;
/**
* Returns the path basename.
*/
public function getBasename(): string;
/**
* Returns the basename extension.
*/
public function getExtension(): string;
/**
* Retrieves a single path segment.
*
* If the segment offset has not been set, returns null.
*/
public function get(int $offset): ?string;
/**
* Returns the associated key for a specific segment.
*
* If a value is specified only the keys associated with
* the given value will be returned
*
* @return array<int>
*/
public function keys(Stringable|string|null $segment = null): array;
/**
* Appends a segment to the path.
*/
public function append(Stringable|string $path): self;
/**
* Extracts a slice of $length elements starting at position $offset from the host.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the selected slice.
*
* If $length is null it returns all elements from $offset to the end of the Path.
*/
public function slice(int $offset, ?int $length = null): self;
/**
* Prepends a segment to the path.
*/
public function prepend(Stringable|string $path): self;
/**
* Returns an instance with the modified segment.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the new segment
*
* If $key is non-negative, the added segment will be the segment at $key position from the start.
* If $key is negative, the added segment will be the segment at $key position from the end.
*
* @throws SyntaxError If the key is invalid
*/
public function withSegment(int $key, Stringable|string $segment): self;
/**
* Returns an instance without the specified segment.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified component
*
* If $key is non-negative, the removed segment will be the segment at $key position from the start.
* If $key is negative, the removed segment will be the segment at $key position from the end.
*
* @throws SyntaxError If the key is invalid
*/
public function withoutSegment(int ...$keys): self;
/**
* Returns an instance without duplicate delimiters.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the path component normalized by removing
* multiple consecutive empty segment
*/
public function withoutEmptySegments(): self;
/**
* Returns an instance with the specified parent directory's path.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the extension basename modified.
*/
public function withDirname(Stringable|string $path): self;
/**
* Returns an instance with the specified basename.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the extension basename modified.
*/
public function withBasename(Stringable|string $basename): self;
/**
* Returns an instance with the specified basename extension.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the extension basename modified.
*/
public function withExtension(Stringable|string $extension): self;
}
@@ -0,0 +1,27 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
interface Transformable
{
/**
* Apply a transformation to this instance and return a new instance.
*
* This method MUST retain the state of the current instance, and return
* a new instance of the same type.
*
* @param callable(static): static $callback
*/
public function transform(callable $callback): static;
}
+29
View File
@@ -0,0 +1,29 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
/**
* @deprecated since version 7.6.0
*/
interface UriAccess
{
public function getUri(): UriInterface|Psr7UriInterface;
/**
* Returns the RFC3986 string representation of the complete URI.
*/
public function getUriString(): string;
}
@@ -0,0 +1,79 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use JsonSerializable;
use Stringable;
/**
* @method static when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null) conditionally return a new instance
* @method bool equals(mixed $value) tells whether the submitted value is equal to the current instance value
*/
interface UriComponentInterface extends JsonSerializable, Stringable
{
/**
* Returns the instance string representation.
*
* If the instance is defined, the value returned MUST be percent-encoded,
* but MUST NOT double-encode any characters. To determine what characters
* to encode, please refer to RFC 3986, Sections 2 and 3.
*
* If the instance is not defined null is returned
*/
public function value(): ?string;
/**
* Returns the instance string representation.
*
* If the instance is defined, the value returned MUST be percent-encoded,
* but MUST NOT double-encode any characters. To determine what characters
* to encode, please refer to RFC 3986, Sections 2 and 3.
*
* If the instance is not defined, an empty string is returned
*/
public function toString(): string;
/**
* Returns the instance string representation.
*
* If the instance is defined, the value returned MUST be percent-encoded,
* but MUST NOT double-encode any characters. To determine what characters
* to encode, please refer to RFC 3986, Sections 2 and 3.
*
* If the instance is not defined, an empty string is returned
*/
public function __toString(): string;
/**
* Returns the instance json representation.
*
* If the instance is defined, the value returned MUST be percent-encoded,
* but MUST NOT double-encode any characters. To determine what characters
* to encode, please refer to RFC 3986 or RFC 1738.
*
* If the instance is not defined, null is returned
*/
public function jsonSerialize(): ?string;
/**
* Returns the instance string representation with its optional URI delimiters.
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode any
* characters. To determine what characters to encode, please refer to RFC 3986,
* Sections 2 and 3.
*
* If the instance is not defined, an empty string is returned
*/
public function getUriComponent(): string;
}
+20
View File
@@ -0,0 +1,20 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use Throwable;
interface UriException extends Throwable
{
}
+321
View File
@@ -0,0 +1,321 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use JsonSerializable;
use League\Uri\Exceptions\MissingFeature;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\UriString;
use Stringable;
/**
* @phpstan-import-type ComponentMap from UriString
*
* @method string|null getUsername() returns the user component of the URI.
* @method self withUsername(?string $user) returns a new URI instance with the user component updated.
* @method string|null getPassword() returns the scheme-specific information about how to gain authorization to access the resource.
* @method self withPassword(?string $password) returns a new URI instance with the password component updated.
* @method string toAsciiString() returns the string representation of the URI in its RFC3986 form
* @method string toUnicodeString() returns the string representation of the URI in its RFC3987 form (the host is in its IDN form)
* @method array toComponents() returns an associative array containing all the URI components.
* @method self normalize() returns a new URI instance with normalized components
* @method self resolve(UriInterface $uri) resolves a URI against a base URI using RFC3986 rules
* @method self relativize(UriInterface $uri) relativize a URI against a base URI using RFC3986 rules
*/
interface UriInterface extends JsonSerializable, Stringable
{
/**
* Returns the string representation as a URI reference.
*
* @see http://tools.ietf.org/html/rfc3986#section-4.1
*/
public function __toString(): string;
/**
* Returns the string representation as a URI reference.
*
* @see http://tools.ietf.org/html/rfc3986#section-4.1
*/
public function toString(): string;
/**
* Returns the string representation as a URI reference.
*
* @see http://tools.ietf.org/html/rfc3986#section-4.1
* @see ::__toString
*/
public function jsonSerialize(): string;
/**
* Retrieve the scheme component of the URI.
*
* If no scheme is present, this method MUST return a null value.
*
* The value returned MUST be normalized to lowercase, per RFC 3986
* Section 3.1.
*
* The trailing ":" character is not part of the scheme and MUST NOT be
* added.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.1
*/
public function getScheme(): ?string;
/**
* Retrieve the authority component of the URI.
*
* If no scheme is present, this method MUST return a null value.
*
* If the port component is not set or is the standard port for the current
* scheme, it SHOULD NOT be included.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.2
*/
public function getAuthority(): ?string;
/**
* Retrieve the user information component of the URI.
*
* If no scheme is present, this method MUST return a null value.
*
* If a user is present in the URI, this will return that value;
* additionally, if the password is also present, it will be appended to the
* user value, with a colon (":") separating the values.
*
* The trailing "@" character is not part of the user information and MUST
* NOT be added.
*/
public function getUserInfo(): ?string;
/**
* Retrieve the host component of the URI.
*
* If no host is present this method MUST return a null value.
*
* The value returned MUST be normalized to lowercase, per RFC 3986
* Section 3.2.2.
*
* @see http://tools.ietf.org/html/rfc3986#section-3.2.2
*/
public function getHost(): ?string;
/**
* Retrieve the port component of the URI.
*
* If a port is present, and it is non-standard for the current scheme,
* this method MUST return it as an integer. If the port is the standard port
* used with the current scheme, this method SHOULD return null.
*
* If no port is present, and no scheme is present, this method MUST return
* a null value.
*
* If no port is present, but a scheme is present, this method MAY return
* the standard port for that scheme, but SHOULD return null.
*/
public function getPort(): ?int;
/**
* Retrieve the path component of the URI.
*
* The path can either be empty or absolute (starting with a slash) or
* rootless (not starting with a slash). Implementations MUST support all
* three syntaxes.
*
* Normally, the empty path "" and absolute path "/" are considered equal as
* defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
* do this normalization because in contexts with a trimmed base path, e.g.
* the front controller, this difference becomes significant. It's the task
* of the user to handle both "" and "/".
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode
* any characters. To determine what characters to encode, please refer to
* RFC 3986, Sections 2 and 3.3.
*
* As an example, if the value should include a slash ("/") not intended as
* delimiter between path segments, that value MUST be passed in encoded
* form (e.g., "%2F") to the instance.
*
* @see https://tools.ietf.org/html/rfc3986#section-2
* @see https://tools.ietf.org/html/rfc3986#section-3.3
*/
public function getPath(): string;
/**
* Retrieve the query string of the URI.
*
* If no host is present this method MUST return a null value.
*
* The leading "?" character is not part of the query and MUST NOT be
* added.
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode
* any characters. To determine what characters to encode, please refer to
* RFC 3986, Sections 2 and 3.4.
*
* As an example, if a value in a key/value pair of the query string should
* include an ampersand ("&") not intended as a delimiter between values,
* that value MUST be passed in encoded form (e.g., "%26") to the instance.
*
* @see https://tools.ietf.org/html/rfc3986#section-2
* @see https://tools.ietf.org/html/rfc3986#section-3.4
*/
public function getQuery(): ?string;
/**
* Retrieve the fragment component of the URI.
*
* If no host is present this method MUST return a null value.
*
* The leading "#" character is not part of the fragment and MUST NOT be
* added.
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode
* any characters. To determine what characters to encode, please refer to
* RFC 3986, Sections 2 and 3.5.
*
* @see https://tools.ietf.org/html/rfc3986#section-2
* @see https://tools.ietf.org/html/rfc3986#section-3.5
*/
public function getFragment(): ?string;
/**
* Returns an associative array containing all the URI components.
*
* The returned array is similar to PHP's parse_url return value with the following
* differences:
*
* <ul>
* <li>All components are present in the returned array</li>
* <li>Empty and undefined component are treated differently. And empty component is
* set to the empty string while an undefined component is set to the `null` value.</li>
* </ul>
*
* @link https://tools.ietf.org/html/rfc3986
*
* @return ComponentMap
*/
public function getComponents(): array;
/**
* Return an instance with the specified scheme.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified scheme.
*
* A null value provided for the scheme is equivalent to removing the scheme
* information.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withScheme(Stringable|string|null $scheme): self;
/**
* Return an instance with the specified user information.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified user information.
*
* Password is optional, but the user information MUST include the
* user; a null value for the user is equivalent to removing user
* information.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withUserInfo(Stringable|string|null $user, Stringable|string|null $password = null): self;
/**
* Return an instance with the specified host.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified host.
*
* A null value provided for the host is equivalent to removing the host
* information.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
* @throws MissingFeature for component or transformations
* requiring IDN support when IDN support is not present
* or misconfigured.
*/
public function withHost(Stringable|string|null $host): self;
/**
* Return an instance with the specified port.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified port.
*
* A null value provided for the port is equivalent to removing the port
* information.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withPort(?int $port): self;
/**
* Return an instance with the specified path.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified path.
*
* The path can either be empty or absolute (starting with a slash) or
* rootless (not starting with a slash). Implementations MUST support all
* three syntaxes.
*
* Users can provide both encoded and decoded path characters.
* Implementations ensure the correct encoding as outlined in getPath().
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withPath(Stringable|string $path): self;
/**
* Return an instance with the specified query string.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified query string.
*
* Users can provide both encoded and decoded query characters.
* Implementations ensure the correct encoding as outlined in getQuery().
*
* A null value provided for the query is equivalent to removing the query
* information.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withQuery(Stringable|string|null $query): self;
/**
* Return an instance with the specified URI fragment.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified URI fragment.
*
* Users can provide both encoded and decoded fragment characters.
* Implementations ensure the correct encoding as outlined in getFragment().
*
* A null value provided for the fragment is equivalent to removing the fragment
* information.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withFragment(Stringable|string|null $fragment): self;
}
@@ -0,0 +1,62 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Contracts;
use Stringable;
interface UserInfoInterface extends UriComponentInterface
{
/**
* Returns the user component part.
*/
public function getUser(): ?string;
/**
* Returns the pass component part.
*/
public function getPass(): ?string;
/**
* Returns an associative array containing all the User Info components.
*
* The returned a hashmap similar to PHP's parse_url return value
*
* @link https://tools.ietf.org/html/rfc3986
*
* @return array{user: ?string, pass : ?string}
*/
public function components(): array;
/**
* Returns an instance with the specified user and/or pass.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified new username
* otherwise it returns the same instance unchanged.
*
* A variable equal to null is equivalent to removing the complete user information.
*/
public function withUser(Stringable|string|null $username): self;
/**
* Returns an instance with the specified user and/or pass.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified password if the user is specified
* otherwise it returns the same instance unchanged.
*
* An empty user is equivalent to removing the user information.
*/
public function withPass(Stringable|string|null $password): self;
}
+506
View File
@@ -0,0 +1,506 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
use BackedEnum;
use Closure;
use Deprecated;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\IPv6\Converter as IPv6Converter;
use SensitiveParameter;
use Stringable;
use Throwable;
use function explode;
use function filter_var;
use function gettype;
use function in_array;
use function preg_match;
use function preg_replace_callback;
use function rawurldecode;
use function rawurlencode;
use function sprintf;
use function str_starts_with;
use function strtolower;
use function strtoupper;
use const FILTER_FLAG_IPV4;
use const FILTER_VALIDATE_IP;
final class Encoder
{
private const REGEXP_CHARS_INVALID = '/[\x00-\x1f\x7f]/';
private const REGEXP_CHARS_ENCODED = ',%[A-Fa-f0-9]{2},';
private const REGEXP_CHARS_PREVENTS_DECODING = ',%
2[A-F|1-2|4-9]|
3[0-9|B|D]|
4[1-9|A-F]|
5[0-9|A|F]|
6[1-9|A-F]|
7[0-9|E]
,ix';
private const REGEXP_PART_SUBDELIM = "\!\$&'\(\)\*\+,;\=%";
private const REGEXP_PART_UNRESERVED = 'A-Za-z\d_\-.~';
private const REGEXP_PART_ENCODED = '%(?![A-Fa-f\d]{2})';
/**
* Unreserved characters.
*
* @see https://www.rfc-editor.org/rfc/rfc3986.html#section-2.3
*/
private const REGEXP_UNRESERVED_CHARACTERS = ',%(2[DdEe]|3[0-9]|4[1-9A-Fa-f]|5[AaFf]|6[1-9A-Fa-f]|7[0-9A-Ea-e]),';
/**
* Tell whether the user component is correctly encoded.
*/
public static function isUserEncoded(BackedEnum|Stringable|string|null $encoded): bool
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.']+|'.self::REGEXP_PART_ENCODED.'/';
if ($encoded instanceof BackedEnum) {
$encoded = $encoded->value;
}
return null === $encoded || 1 !== preg_match($pattern, (string) $encoded);
}
/**
* Encode User.
*
* All generic delimiters MUST be encoded
*/
public static function encodeUser(BackedEnum|Stringable|string|null $user): ?string
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.']+|'.self::REGEXP_PART_ENCODED.'/';
return self::encode($user, $pattern);
}
/**
* Normalize user component.
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode
* any characters. To determine what characters to encode, please refer to
* RFC 3986.
*/
public static function normalizeUser(BackedEnum|Stringable|string|null $user): ?string
{
return self::normalize(self::encodeUser(self::decodeUnreservedCharacters($user)));
}
private static function normalize(?string $component): ?string
{
if (null === $component) {
return null;
}
return (string) preg_replace_callback(
'/%[0-9a-f]{2}/i',
static fn (array $found) => strtoupper($found[0]),
$component
);
}
/**
* Tell whether the password component is correctly encoded.
*/
public static function isPasswordEncoded(#[SensitiveParameter] BackedEnum|Stringable|string|null $encoded): bool
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':]+|'.self::REGEXP_PART_ENCODED.'/';
if ($encoded instanceof BackedEnum) {
$encoded = $encoded->value;
}
return null === $encoded || 1 !== preg_match($pattern, (string) $encoded);
}
/**
* Encode Password.
*
* Generic delimiters ":" MUST NOT be encoded
*/
public static function encodePassword(#[SensitiveParameter] BackedEnum|Stringable|string|null $component): ?string
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':]+|'.self::REGEXP_PART_ENCODED.'/';
return self::encode($component, $pattern);
}
/**
* Normalize password component.
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode
* any characters. To determine what characters to encode, please refer to
* RFC 3986.
*/
public static function normalizePassword(#[SensitiveParameter] BackedEnum|Stringable|string|null $password): ?string
{
return self::normalize(self::encodePassword(self::decodeUnreservedCharacters($password)));
}
/**
* Tell whether the userInfo component is correctly encoded.
*/
public static function isUserInfoEncoded(#[SensitiveParameter] BackedEnum|Stringable|string|null $userInfo): bool
{
if (null === $userInfo) {
return true;
}
if ($userInfo instanceof BackedEnum) {
$userInfo = $userInfo->value;
}
[$user, $password] = explode(':', (string) $userInfo, 2) + [1 => null];
return self::isUserEncoded($user)
&& self::isPasswordEncoded($password);
}
public static function encodeUserInfo(#[SensitiveParameter] BackedEnum|Stringable|string|null $userInfo): ?string
{
if (null === $userInfo) {
return null;
}
if ($userInfo instanceof BackedEnum) {
$userInfo = $userInfo->value;
}
[$user, $password] = explode(':', (string) $userInfo, 2) + [1 => null];
$userInfo = self::encodeUser($user);
if (null === $password) {
return $userInfo;
}
return $userInfo.':'.self::encodePassword($password);
}
public static function normalizeUserInfo(#[SensitiveParameter] BackedEnum|Stringable|string|null $userInfo): ?string
{
if (null === $userInfo) {
return null;
}
if ($userInfo instanceof BackedEnum) {
$userInfo = $userInfo->value;
}
[$user, $password] = explode(':', (string) $userInfo, 2) + [1 => null];
$userInfo = self::normalizeUser($user);
if (null === $password) {
return $userInfo;
}
return $userInfo.':'.self::normalizePassword($password);
}
/**
* Decodes all the URI component characters.
*/
public static function decodeAll(BackedEnum|Stringable|string|null $component): ?string
{
return self::decode($component, static fn (array $matches): string => rawurldecode($matches[0]));
}
/**
* Decodes the URI component without decoding the unreserved characters which are already encoded.
*/
public static function decodeNecessary(BackedEnum|Stringable|string|int|null $component): ?string
{
$decoder = static function (array $matches): string {
if (1 === preg_match(self::REGEXP_CHARS_PREVENTS_DECODING, $matches[0])) {
return strtoupper($matches[0]);
}
return rawurldecode($matches[0]);
};
return self::decode($component, $decoder);
}
/**
* Decodes the component unreserved characters.
*/
public static function decodeUnreservedCharacters(BackedEnum|Stringable|string|null $str): ?string
{
if ($str instanceof BackedEnum) {
$str = $str->value;
}
if (null === $str) {
return null;
}
return preg_replace_callback(
self::REGEXP_UNRESERVED_CHARACTERS,
static fn (array $matches): string => rawurldecode($matches[0]),
(string) $str
);
}
/**
* Tell whether the path component is correctly encoded.
*/
public static function isPathEncoded(BackedEnum|Stringable|string|null $encoded): bool
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':@\/]+|'.self::REGEXP_PART_ENCODED.'/';
if ($encoded instanceof BackedEnum) {
$encoded = $encoded->value;
}
return null === $encoded || 1 !== preg_match($pattern, (string) $encoded);
}
/**
* Encode Path.
*
* Generic delimiters ":", "@", and "/" MUST NOT be encoded
*/
public static function encodePath(BackedEnum|Stringable|string|null $component): string
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':@\/]+|'.self::REGEXP_PART_ENCODED.'/';
return (string) self::encode($component, $pattern);
}
/**
* Decodes the path component while preserving characters that should not be decoded in the context of a full valid URI.
*/
public static function decodePath(BackedEnum|Stringable|string|null $path): ?string
{
$decoder = static function (array $matches): string {
$encodedChar = strtoupper($matches[0]);
return in_array($encodedChar, ['%2F', '%20', '%3F', '%23'], true) ? $encodedChar : rawurldecode($encodedChar);
};
return self::decode($path, $decoder);
}
/**
* Normalize path component.
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode
* any characters. To determine what characters to encode, please refer to
* RFC 3986.
*/
public static function normalizePath(BackedEnum|Stringable|string|null $component): ?string
{
return self::normalize(self::encodePath(self::decodePath($component)));
}
/**
* Tell whether the query component is correctly encoded.
*/
public static function isQueryEncoded(BackedEnum|Stringable|string|null $encoded): bool
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.'\/?%]+|'.self::REGEXP_PART_ENCODED.'/';
if ($encoded instanceof BackedEnum) {
$encoded = $encoded->value;
}
return null === $encoded || 1 !== preg_match($pattern, (string) $encoded);
}
/**
* Decodes the query component while preserving characters that should not be decoded in the context of a full valid URI.
*/
public static function decodeQuery(BackedEnum|Stringable|string|null $path): ?string
{
$decoder = static function (array $matches): string {
$encodedChar = strtoupper($matches[0]);
return in_array($encodedChar, ['%26', '%3D', '%20', '%23', '%3F'], true) ? $encodedChar : rawurldecode($encodedChar);
};
return self::decode($path, $decoder);
}
/**
* Normalize the query component.
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode
* any characters. To determine what characters to encode, please refer to
* RFC 3986.
*/
public static function normalizeQuery(BackedEnum|Stringable|string|null $query): ?string
{
return self::normalize(self::encodeQueryOrFragment(self::decodeQuery($query)));
}
/**
* Tell whether the query component is correctly encoded.
*/
public static function isFragmentEncoded(BackedEnum|Stringable|string|null $encoded): bool
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':@\/?%]|'.self::REGEXP_PART_ENCODED.'/';
if ($encoded instanceof BackedEnum) {
$encoded = $encoded->value;
}
return null === $encoded || 1 !== preg_match($pattern, (string) $encoded);
}
/**
* Decodes the fragment component while preserving characters that should not be decoded in the context of a full valid URI.
*/
public static function decodeFragment(BackedEnum|Stringable|string|null $path): ?string
{
return self::decode($path, static fn (array $matches): string => '%20' === $matches[0] ? $matches[0] : rawurldecode($matches[0]));
}
/**
* Normalize the fragment component.
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode
* any characters. To determine what characters to encode, please refer to
* RFC 3986.
*/
public static function normalizeFragment(BackedEnum|Stringable|string|null $fragment): ?string
{
return self::normalize(self::encodeQueryOrFragment(self::decodeFragment($fragment)));
}
/**
* Normalize the host component.
*
* @see https://www.rfc-editor.org/rfc/rfc3986.html#section-3.2.2
*
* The value returned MUST be percent-encoded, but MUST NOT double-encode
* any characters. To determine what characters to encode, please refer to
* RFC 3986.
*/
public static function normalizeHost(BackedEnum|Stringable|string|null $host): ?string
{
if ($host instanceof BackedEnum) {
$host = (string) $host->value;
}
if ($host instanceof Stringable) {
$host = (string) $host;
}
if (null === $host || '' === $host || false !== filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return $host;
}
if (str_starts_with($host, '[')) {
return IPv6Converter::normalize($host);
}
$host = strtolower($host);
return (!str_contains($host, '%')) ? $host : preg_replace_callback(
'/%[a-f0-9]{2}/',
fn (array $matches) => 1 === preg_match('/%([0-7][0-9a-f])/', $matches[0]) ? rawurldecode($matches[0]) : strtoupper($matches[0]),
$host
);
}
/**
* Encode Query or Fragment.
*
* Generic delimiters ":", "@", "?", and "/" MUST NOT be encoded
*/
public static function encodeQueryOrFragment(BackedEnum|Stringable|string|null $component): ?string
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':@\/?]+|'.self::REGEXP_PART_ENCODED.'/';
return self::encode($component, $pattern);
}
public static function encodeQueryKeyValue(mixed $component): ?string
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.']+|'.self::REGEXP_PART_ENCODED.'/';
$encoder = static fn (array $found): string => 1 === preg_match('/[^'.self::REGEXP_PART_UNRESERVED.']/', rawurldecode($found[0])) ? rawurlencode($found[0]) : $found[0];
$filteredComponent = self::filterComponent($component);
return match (true) {
null === $filteredComponent => throw new SyntaxError(sprintf('A pair key/value must be a scalar value `%s` given.', gettype($component))),
1 === preg_match(self::REGEXP_CHARS_INVALID, $filteredComponent) => rawurlencode($filteredComponent),
default => (string) preg_replace_callback($pattern, $encoder, $filteredComponent),
};
}
private static function filterComponent(mixed $component): ?string
{
try {
return StringCoercionMode::Native->coerce($component);
} catch (Throwable $exception) {
throw new SyntaxError(
sprintf('The component must be a scalar value `%s` given.', gettype($component)),
previous: $exception
);
}
}
/**
* Encodes the URI component characters using a regular expression to find which characters need encoding.
*/
private static function encode(BackedEnum|Stringable|string|int|bool|null $component, string $pattern): ?string
{
$component = self::filterComponent($component);
if (null === $component || '' === $component) {
return $component;
}
return (string) preg_replace_callback(
$pattern,
static fn (array $found): string => 1 === preg_match('/[^'.self::REGEXP_PART_UNRESERVED.']/', rawurldecode($found[0])) ? rawurlencode($found[0]) : $found[0],
$component
);
}
/**
* Decodes the URI component characters using a closure.
*/
private static function decode(BackedEnum|Stringable|string|int|null $component, Closure $decoder): ?string
{
$component = self::filterComponent($component);
if (null === $component || '' === $component) {
return $component;
}
if (1 === preg_match(self::REGEXP_CHARS_INVALID, $component)) {
throw new SyntaxError('Invalid component string: '.$component.'.');
}
if (1 === preg_match(self::REGEXP_CHARS_ENCODED, $component)) {
return (string) preg_replace_callback(self::REGEXP_CHARS_ENCODED, $decoder, $component);
}
return $component;
}
/**
* Decodes the URI component without decoding the unreserved characters which are already encoded.
*
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.6.0
* @codeCoverageIgnore
* @see Encoder::decodeNecessary()
*
* Create a new instance from the environment.
*/
#[Deprecated(message:'use League\Uri\Encoder::decodeNecessary() instead', since:'league/uri:7.6.0')]
public static function decodePartial(BackedEnum|Stringable|string|int|null $component): ?string
{
return self::decodeNecessary($component);
}
}
@@ -0,0 +1,51 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Exceptions;
use BackedEnum;
use League\Uri\Idna\Error;
use League\Uri\Idna\Result;
use Stringable;
final class ConversionFailed extends SyntaxError
{
private function __construct(
string $message,
private readonly string $host,
private readonly Result $result
) {
parent::__construct($message);
}
public static function dueToIdnError(BackedEnum|Stringable|string $host, Result $result): self
{
$reasons = array_map(fn (Error $error): string => $error->description(), $result->errors());
if ($host instanceof BackedEnum) {
$host = (string) $host->value;
}
return new self('Host `'.$host.'` is invalid: '.implode('; ', $reasons).'.', (string) $host, $result);
}
public function getHost(): string
{
return $this->host;
}
public function getResult(): Result
{
return $this->result;
}
}
@@ -0,0 +1,21 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Exceptions;
use League\Uri\Contracts\UriException;
use RuntimeException;
class MissingFeature extends RuntimeException implements UriException
{
}
@@ -0,0 +1,18 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Exceptions;
class OffsetOutOfBounds extends SyntaxError
{
}
+21
View File
@@ -0,0 +1,21 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Exceptions;
use InvalidArgumentException;
use League\Uri\Contracts\UriException;
class SyntaxError extends InvalidArgumentException implements UriException
{
}
+64
View File
@@ -0,0 +1,64 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
use finfo;
use League\Uri\Exceptions\MissingFeature;
use League\Uri\IPv4\Calculator;
use function class_exists;
use function defined;
use function extension_loaded;
use function function_exists;
use const PHP_INT_SIZE;
/**
* Allow detecting features needed to make the packages work.
*/
final class FeatureDetection
{
public static function supportsFileDetection(): void
{
static $isSupported = null;
$isSupported = $isSupported ?? class_exists(finfo::class);
$isSupported || throw new MissingFeature('Support for file type detection requires the `fileinfo` extension.');
}
public static function supportsIdn(): void
{
static $isSupported = null;
$isSupported = $isSupported ?? (function_exists('\idn_to_ascii') && defined('\INTL_IDNA_VARIANT_UTS46'));
$isSupported || throw new MissingFeature('Support for IDN host requires the `intl` extension for best performance or run "composer require symfony/polyfill-intl-idn" to install a polyfill.');
}
public static function supportsIPv4Conversion(): void
{
static $isSupported = null;
$isSupported = $isSupported ?? (extension_loaded('gmp') || extension_loaded('bcmath') || (4 < PHP_INT_SIZE));
$isSupported || throw new MissingFeature('A '.Calculator::class.' implementation could not be automatically loaded. To perform IPv4 conversion use a x.64 PHP build or install one of the following extension GMP or BCMath. You can also ship your own implementation.');
}
public static function supportsDom(): void
{
static $isSupported = null;
$isSupported = $isSupported ?? extension_loaded('dom');
$isSupported || throw new MissingFeature('To use a DOM related feature, the DOM extension must be installed in your system.');
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
enum HostFormat
{
case Ascii;
case Unicode;
}
+448
View File
@@ -0,0 +1,448 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
use BackedEnum;
use Exception;
use JsonSerializable;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\Idna\Converter as IdnConverter;
use Stringable;
use Throwable;
use function array_key_first;
use function count;
use function explode;
use function filter_var;
use function get_object_vars;
use function in_array;
use function inet_pton;
use function is_object;
use function preg_match;
use function rawurldecode;
use function strpos;
use function strtolower;
use function substr;
use const FILTER_FLAG_IPV4;
use const FILTER_FLAG_IPV6;
use const FILTER_VALIDATE_IP;
/**
* @phpstan-type HostRecordSerializedShape array{0: array{host: ?string}, 1: array{}}
*/
final class HostRecord implements JsonSerializable
{
/**
* Maximum number of host cached.
*
* @var int
*/
private const MAXIMUM_HOST_CACHED = 100;
private const REGEXP_NON_ASCII_PATTERN = '/[^\x20-\x7f]/';
/**
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
*
* invalid characters in host regular expression
*/
private const REGEXP_INVALID_HOST_CHARS = '/
[:\/?#\[\]@ ] # gen-delims characters as well as the space character
/ix';
/**
* General registered name regular expression.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
* @see https://regex101.com/r/fptU8V/1
*/
private const REGEXP_REGISTERED_NAME = '/
(?(DEFINE)
(?<unreserved>[a-z0-9_~\-]) # . is missing as it is used to separate labels
(?<sub_delims>[!$&\'()*+,;=])
(?<encoded>%[A-F0-9]{2})
(?<reg_name>(?:(?&unreserved)|(?&sub_delims)|(?&encoded))*)
)
^(?:(?&reg_name)\.)*(?&reg_name)\.?$
/ix';
/**
* Domain name regular expression.
*
* Everything but the domain name length is validated
*
* @see https://tools.ietf.org/html/rfc1034#section-3.5
* @see https://tools.ietf.org/html/rfc1123#section-2.1
* @see https://regex101.com/r/71j6rt/1
*/
private const REGEXP_DOMAIN_NAME = '/
(?(DEFINE)
(?<let_dig> [a-z0-9]) # alpha digit
(?<let_dig_hyp> [a-z0-9-]) # alpha digit and hyphen
(?<ldh_str> (?&let_dig_hyp){0,61}(?&let_dig)) # domain label end
(?<label> (?&let_dig)((?&ldh_str))?) # domain label
(?<domain> (?&label)(\.(?&label)){0,126}\.?) # domain name
)
^(?&domain)$
/ix';
/**
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
*
* IPvFuture regular expression
*/
private const REGEXP_IP_FUTURE = '/^
v(?<version>[A-F\d])+\.
(?:
(?<unreserved>[a-z\d_~\-\.])|
(?<sub_delims>[!$&\'()*+,;=:]) # also include the : character
)+
$/ix';
private const REGEXP_GEN_DELIMS = '/[:\/?#\[\]@ ]/';
private const ADDRESS_BLOCK = "\xfe\x80";
private ?bool $isDomainName = null;
private ?bool $hasZoneIdentifier = null;
private bool $asciiIsLoaded = false;
private ?string $hostAsAscii = null;
private bool $unicodeIsLoaded = false;
private ?string $hostAsUnicode = null;
private bool $isIpVersionLoaded = false;
private ?string $ipVersion = null;
private bool $isIpValueLoaded = false;
private ?string $ipValue = null;
private function __construct(
public readonly ?string $value,
public readonly HostType $type,
public readonly HostFormat $format
) {
}
public function hasZoneIdentifier(): bool
{
return $this->hasZoneIdentifier ??= HostType::Ipv6 === $this->type && str_contains((string) $this->value, '%');
}
public function toAscii(): ?string
{
if (!$this->asciiIsLoaded) {
$this->asciiIsLoaded = true;
$this->hostAsAscii = (function (): ?string {
if (HostType::RegisteredName !== $this->type || null === $this->value) {
return $this->value;
}
$formattedHost = rawurldecode($this->value);
if ($formattedHost === $this->value) {
return $this->isDomainType() ? IdnConverter::toAscii($this->value)->domain() : strtolower($formattedHost);
}
return Encoder::normalizeHost($this->value);
})();
}
return $this->hostAsAscii;
}
public function toUnicode(): ?string
{
if (!$this->unicodeIsLoaded) {
$this->unicodeIsLoaded = true;
$this->hostAsUnicode = $this->isDomainType() && null !== $this->value ? IdnConverter::toUnicode($this->value)->domain() : $this->value;
}
return $this->hostAsUnicode;
}
public function isDomainType(): bool
{
return $this->isDomainName ??= match (true) {
HostType::RegisteredName !== $this->type, '' === $this->value => false,
null === $this->value => true,
default => is_object($result = IdnConverter::toAscii($this->value))
&& !$result->hasErrors()
&& self::isValidDomain($result->domain()),
};
}
public function ipVersion(): ?string
{
if (!$this->isIpVersionLoaded) {
$this->isIpVersionLoaded = true;
$this->ipVersion = match (true) {
HostType::Ipv4 === $this->type => '4',
HostType::Ipv6 === $this->type => '6',
1 === preg_match(self::REGEXP_IP_FUTURE, substr((string) $this->value, 1, -1), $matches) => $matches['version'],
default => null,
};
}
return $this->ipVersion;
}
public function ipValue(): ?string
{
if (!$this->isIpValueLoaded) {
$this->isIpValueLoaded = true;
$this->ipValue = (function (): ?string {
if (HostType::RegisteredName === $this->type) {
return null;
}
if (HostType::Ipv4 === $this->type) {
return $this->value;
}
$ip = substr((string) $this->value, 1, -1);
if (HostType::Ipv6 !== $this->type) {
return substr($ip, (int) strpos($ip, '.') + 1);
}
$pos = strpos($ip, '%');
if (false === $pos) {
return $ip;
}
return substr($ip, 0, $pos).'%'.rawurldecode(substr($ip, $pos + 3));
})();
}
return $this->ipValue;
}
public static function isValid(BackedEnum|Stringable|string|null $host): bool
{
try {
HostRecord::from($host);
return true;
} catch (Throwable) {
return false;
}
}
public static function isIpv4(Stringable|string|null $host): bool
{
try {
return HostType::Ipv4 === HostRecord::from($host)->type;
} catch (Throwable) {
return false;
}
}
public static function isIpv6(Stringable|string|null $host): bool
{
try {
return HostType::Ipv6 === HostRecord::from($host)->type;
} catch (Throwable) {
return false;
}
}
public static function isIpvFuture(Stringable|string|null $host): bool
{
try {
return HostType::IpvFuture === HostRecord::from($host)->type;
} catch (Throwable) {
return false;
}
}
public static function isIp(Stringable|string|null $host): bool
{
return self::isIpv4($host)
|| self::isIpv6($host)
|| self::isIpvFuture($host);
}
public static function isRegisteredName(Stringable|string|null $host): bool
{
try {
return HostType::RegisteredName === HostRecord::from($host)->type;
} catch (Throwable) {
return false;
}
}
public static function isDomain(Stringable|string|null $host): bool
{
try {
return HostRecord::from($host)->isDomainType();
} catch (Throwable) {
return false;
}
}
/**
* @throws SyntaxError
*/
public static function from(BackedEnum|Stringable|string|null $host): self
{
if ($host instanceof BackedEnum) {
$host = $host->value;
}
if ($host instanceof UriComponentInterface) {
$host = $host->value();
}
if (null === $host) {
return new self(
value: null,
type: HostType::RegisteredName,
format: HostFormat::Ascii,
);
}
$host = (string) $host;
if ('' === $host) {
return new self(
value: '',
type: HostType::RegisteredName,
format: HostFormat::Ascii,
);
}
static $inMemoryCache = [];
if (isset($inMemoryCache[$host])) {
return $inMemoryCache[$host];
}
if (self::MAXIMUM_HOST_CACHED < count($inMemoryCache)) {
unset($inMemoryCache[array_key_first($inMemoryCache)]);
}
if ($host === filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return $inMemoryCache[$host] = new self(
value: $host,
type: HostType::Ipv4,
format: HostFormat::Ascii,
);
}
if (str_starts_with($host, '[')) {
str_ends_with($host, ']') || throw new SyntaxError('The host '.$host.' is not a valid IPv6 host.');
$ipHost = substr($host, 1, -1);
if (1 === preg_match(self::REGEXP_IP_FUTURE, $ipHost, $matches)) {
return !in_array($matches['version'], ['4', '6'], true) ? ($inMemoryCache[$host] = new self(
value: $host,
type: HostType::IpvFuture,
format: HostFormat::Ascii,
)) : throw new SyntaxError('The host '.$host.' is not a valid IPvFuture host.');
}
if (self::isValidIpv6Hostname($ipHost)) {
return $inMemoryCache[$host] = new self(
value: $host,
type: HostType::Ipv6,
format: HostFormat::Ascii,
);
}
throw new SyntaxError('The host '.$host.' is not a valid IPv6 host.');
}
$domainName = rawurldecode($host);
$format = HostFormat::Unicode;
if (1 !== preg_match(self::REGEXP_NON_ASCII_PATTERN, $domainName)) {
$domainName = strtolower($domainName);
$format = HostFormat::Ascii;
}
if (1 === preg_match(self::REGEXP_REGISTERED_NAME, $domainName)) {
return $inMemoryCache[$host] = new self(
value: $host,
type: HostType::RegisteredName,
format: $format,
);
}
(HostFormat::Ascii !== $format && 1 !== preg_match(self::REGEXP_INVALID_HOST_CHARS, $domainName)) || throw new SyntaxError('`'.$host.'` is an invalid domain name : the host contains invalid characters.');
IdnConverter::toAsciiOrFail($domainName);
return $inMemoryCache[$host] = new self(
value: $host,
type: HostType::RegisteredName,
format: $format,
);
}
/**
* Tells whether the registered name is a valid domain name according to RFC1123.
*
* @see http://man7.org/linux/man-pages/man7/hostname.7.html
* @see https://tools.ietf.org/html/rfc1123#section-2.1
*/
private static function isValidDomain(string $hostname): bool
{
$domainMaxLength = str_ends_with($hostname, '.') ? 254 : 253;
return !isset($hostname[$domainMaxLength])
&& 1 === preg_match(self::REGEXP_DOMAIN_NAME, $hostname);
}
/**
* Validates an Ipv6 as Host.
*
* @see http://tools.ietf.org/html/rfc6874#section-2
* @see http://tools.ietf.org/html/rfc6874#section-4
*/
private static function isValidIpv6Hostname(string $host): bool
{
[$ipv6, $scope] = explode('%', $host, 2) + [1 => null];
if (null === $scope) {
return (bool) filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
}
$scope = rawurldecode('%'.$scope);
return 1 !== preg_match(self::REGEXP_NON_ASCII_PATTERN, $scope)
&& 1 !== preg_match(self::REGEXP_GEN_DELIMS, $scope)
&& false !== filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)
&& str_starts_with((string)inet_pton((string)$ipv6), self::ADDRESS_BLOCK);
}
public function jsonSerialize(): ?string
{
return $this->value;
}
/**
* @return HostRecordSerializedShape
*/
public function __serialize(): array
{
return [['host' => $this->value], []];
}
/**
* @param HostRecordSerializedShape $data
*
* @throws Exception|SyntaxError
*/
public function __unserialize(array $data): void
{
[$properties] = $data;
$record = self::from($properties['host'] ?? throw new Exception('The `host` property is missing from the serialized object.'));
//if the Host computed value are already cache this avoid recomputing them
foreach (get_object_vars($record) as $prop => $value) {
/* @phpstan-ignore-next-line */
$this->{$prop} = $value;
}
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
enum HostType
{
case RegisteredName;
case Ipv4;
case Ipv6;
case IpvFuture;
}
+85
View File
@@ -0,0 +1,85 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\IPv4;
use function bcadd;
use function bccomp;
use function bcdiv;
use function bcmod;
use function bcmul;
use function bcpow;
use function bcsub;
use function str_split;
final class BCMathCalculator implements Calculator
{
private const SCALE = 0;
private const CONVERSION_TABLE = [
'0' => '0', '1' => '1', '2' => '2', '3' => '3',
'4' => '4', '5' => '5', '6' => '6', '7' => '7',
'8' => '8', '9' => '9', 'a' => '10', 'b' => '11',
'c' => '12', 'd' => '13', 'e' => '14', 'f' => '15',
];
public function baseConvert(mixed $value, int $base): string
{
$value = (string) $value;
if (10 === $base) {
return $value;
}
$base = (string) $base;
$decimal = '0';
foreach (str_split($value) as $char) {
$decimal = bcadd($this->multiply($decimal, $base), self::CONVERSION_TABLE[$char], self::SCALE);
}
return $decimal;
}
public function pow(mixed $value, int $exponent): string
{
return bcpow((string) $value, (string) $exponent, self::SCALE);
}
public function compare(mixed $value1, mixed $value2): int
{
return bccomp((string) $value1, (string) $value2, self::SCALE);
}
public function multiply(mixed $value1, mixed $value2): string
{
return bcmul((string) $value1, (string) $value2, self::SCALE);
}
public function div(mixed $value, mixed $base): string
{
return bcdiv((string) $value, (string) $base, self::SCALE);
}
public function mod(mixed $value, mixed $base): string
{
return bcmod((string) $value, (string) $base, self::SCALE);
}
public function add(mixed $value1, mixed $value2): string
{
return bcadd((string) $value1, (string) $value2, self::SCALE);
}
public function sub(mixed $value1, mixed $value2): string
{
return bcsub((string) $value1, (string) $value2, self::SCALE);
}
}
+95
View File
@@ -0,0 +1,95 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\IPv4;
interface Calculator
{
/**
* Add numbers.
*
* @param mixed $value1 a number that will be added to $value2
* @param mixed $value2 a number that will be added to $value1
*
* @return mixed the addition result
*/
public function add(mixed $value1, mixed $value2);
/**
* Subtract one number from another.
*
* @param mixed $value1 a number that will be subtracted of $value2
* @param mixed $value2 a number that will be subtracted to $value1
*
* @return mixed the subtraction result
*/
public function sub(mixed $value1, mixed $value2);
/**
* Multiply numbers.
*
* @param mixed $value1 a number that will be multiplied by $value2
* @param mixed $value2 a number that will be multiplied by $value1
*
* @return mixed the multiplication result
*/
public function multiply(mixed $value1, mixed $value2);
/**
* Divide numbers.
*
* @param mixed $value The number being divided.
* @param mixed $base The number that $value is being divided by.
*
* @return mixed the result of the division
*/
public function div(mixed $value, mixed $base);
/**
* Raise an number to the power of exponent.
*
* @param mixed $value scalar, the base to use
*
* @return mixed the value raised to the power of exp.
*/
public function pow(mixed $value, int $exponent);
/**
* Returns the int point remainder (modulo) of the division of the arguments.
*
* @param mixed $value The dividend
* @param mixed $base The divisor
*
* @return mixed the remainder
*/
public function mod(mixed $value, mixed $base);
/**
* Number comparison.
*
* @param mixed $value1 the first value
* @param mixed $value2 the second value
*
* @return int Returns < 0 if value1 is less than value2; > 0 if value1 is greater than value2, and 0 if they are equal.
*/
public function compare(mixed $value1, mixed $value2): int;
/**
* Get the decimal integer value of a variable.
*
* @param mixed $value The scalar value being converted to an integer
*
* @return mixed the integer value
*/
public function baseConvert(mixed $value, int $base);
}
+318
View File
@@ -0,0 +1,318 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\IPv4;
use BackedEnum;
use League\Uri\Exceptions\MissingFeature;
use League\Uri\FeatureDetection;
use Stringable;
use function array_pop;
use function count;
use function explode;
use function extension_loaded;
use function hexdec;
use function long2ip;
use function ltrim;
use function preg_match;
use function str_ends_with;
use function substr;
use const FILTER_FLAG_IPV4;
use const FILTER_FLAG_IPV6;
use const FILTER_VALIDATE_IP;
final class Converter
{
private const REGEXP_IPV4_HOST = '/
(?(DEFINE) # . is missing as it is used to separate labels
(?<hexadecimal>0x[[:xdigit:]]*)
(?<octal>0[0-7]*)
(?<decimal>\d+)
(?<ipv4_part>(?:(?&hexadecimal)|(?&octal)|(?&decimal))*)
)
^(?:(?&ipv4_part)\.){0,3}(?&ipv4_part)\.?$
/x';
private const REGEXP_IPV4_NUMBER_PER_BASE = [
'/^0x(?<number>[[:xdigit:]]*)$/' => 16,
'/^0(?<number>[0-7]*)$/' => 8,
'/^(?<number>\d+)$/' => 10,
];
private const IPV6_6TO4_PREFIX = '2002:';
private const IPV4_MAPPED_PREFIX = '::ffff:';
private readonly mixed $maxIPv4Number;
public function __construct(
private readonly Calculator $calculator
) {
$this->maxIPv4Number = $calculator->sub($calculator->pow(2, 32), 1);
}
/**
* Returns an instance using a GMP calculator.
*/
public static function fromGMP(): self
{
return new self(new GMPCalculator());
}
/**
* Returns an instance using a Bcmath calculator.
*/
public static function fromBCMath(): self
{
return new self(new BCMathCalculator());
}
/**
* Returns an instance using a PHP native calculator (requires 64bits PHP).
*/
public static function fromNative(): self
{
return new self(new NativeCalculator());
}
/**
* Returns an instance using a detected calculator depending on the PHP environment.
*
* @throws MissingFeature If no Calculator implementing object can be used on the platform
*
* @codeCoverageIgnore
*/
public static function fromEnvironment(): self
{
FeatureDetection::supportsIPv4Conversion();
return match (true) {
extension_loaded('gmp') => self::fromGMP(),
extension_loaded('bcmath') => self::fromBCMath(),
default => self::fromNative(),
};
}
public function isIpv4(BackedEnum|Stringable|string|null $host): bool
{
if ($host instanceof BackedEnum) {
$host = (string) $host->value;
}
if (null === $host) {
return false;
}
if (null !== $this->toDecimal($host)) {
return true;
}
$host = (string) $host;
if (false === filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return false;
}
$ipAddress = strtolower((string) inet_ntop((string) inet_pton($host)));
if (str_starts_with($ipAddress, self::IPV4_MAPPED_PREFIX)) {
return false !== filter_var(substr($ipAddress, 7), FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
}
if (!str_starts_with($ipAddress, self::IPV6_6TO4_PREFIX)) {
return false;
}
$hexParts = explode(':', substr($ipAddress, 5, 9));
if (count($hexParts) < 2) {
return false;
}
$ipAddress = long2ip((int) hexdec($hexParts[0]) * 65536 + (int) hexdec($hexParts[1]));
return '' !== ''.$ipAddress;
}
public function toIPv6Using6to4(BackedEnum|Stringable|string|null $host): ?string
{
$host = $this->toDecimal($host);
if (null === $host) {
return null;
}
/** @var array<string> $parts */
$parts = array_map(
fn (string $part): string => sprintf('%02x', $part),
explode('.', $host)
);
return '['.self::IPV6_6TO4_PREFIX.$parts[0].$parts[1].':'.$parts[2].$parts[3].'::]';
}
public function toIPv6UsingMapping(BackedEnum|Stringable|string|null $host): ?string
{
$host = $this->toDecimal($host);
if (null === $host) {
return null;
}
return '['.self::IPV4_MAPPED_PREFIX.$host.']';
}
public function toOctal(BackedEnum|Stringable|string|null $host): ?string
{
$host = $this->toDecimal($host);
return match (null) {
$host => null,
default => implode('.', array_map(
fn ($value) => str_pad(decoct((int) $value), 4, '0', STR_PAD_LEFT),
explode('.', $host)
)),
};
}
public function toHexadecimal(BackedEnum|Stringable|string|null $host): ?string
{
$host = $this->toDecimal($host);
return match (null) {
$host => null,
default => '0x'.implode('', array_map(
fn ($value) => dechex((int) $value),
explode('.', $host)
)),
};
}
/**
* Tries to convert a IPv4 hexadecimal or a IPv4 octal notation into a IPv4 dot-decimal notation if possible
* otherwise returns null.
*
* @see https://url.spec.whatwg.org/#concept-ipv4-parser
*/
public function toDecimal(BackedEnum|Stringable|string|null $host): ?string
{
if ($host instanceof BackedEnum) {
$host = $host->value;
}
$host = (string) $host;
if (str_starts_with($host, '[') && str_ends_with($host, ']')) {
$host = substr($host, 1, -1);
if (false === filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return null;
}
$ipAddress = strtolower((string) inet_ntop((string) inet_pton($host)));
if (str_starts_with($ipAddress, self::IPV4_MAPPED_PREFIX)) {
return substr($ipAddress, 7);
}
if (!str_starts_with($ipAddress, self::IPV6_6TO4_PREFIX)) {
return null;
}
$hexParts = explode(':', substr($ipAddress, 5, 9));
return (string) match (true) {
count($hexParts) < 2 => null,
default => long2ip((int) hexdec($hexParts[0]) * 65536 + (int) hexdec($hexParts[1])),
};
}
if (1 !== preg_match(self::REGEXP_IPV4_HOST, $host)) {
return null;
}
if (str_ends_with($host, '.')) {
$host = substr($host, 0, -1);
}
$numbers = [];
foreach (explode('.', $host) as $label) {
$number = $this->labelToNumber($label);
if (null === $number) {
return null;
}
$numbers[] = $number;
}
$ipv4 = array_pop($numbers);
$max = $this->calculator->pow(256, 6 - count($numbers));
if ($this->calculator->compare($ipv4, $max) > 0) {
return null;
}
foreach ($numbers as $offset => $number) {
if ($this->calculator->compare($number, 255) > 0) {
return null;
}
$ipv4 = $this->calculator->add($ipv4, $this->calculator->multiply(
$number,
$this->calculator->pow(256, 3 - $offset)
));
}
return $this->long2Ip($ipv4);
}
/**
* Converts a domain label into a IPv4 integer part.
*
* @see https://url.spec.whatwg.org/#ipv4-number-parser
*
* @return mixed returns null if it cannot correctly convert the label
*/
private function labelToNumber(string $label): mixed
{
foreach (self::REGEXP_IPV4_NUMBER_PER_BASE as $regexp => $base) {
if (1 !== preg_match($regexp, $label, $matches)) {
continue;
}
$number = ltrim($matches['number'], '0');
if ('' === $number) {
return 0;
}
$number = $this->calculator->baseConvert($number, $base);
if (0 <= $this->calculator->compare($number, 0) && 0 >= $this->calculator->compare($number, $this->maxIPv4Number)) {
return $number;
}
}
return null;
}
/**
* Generates the dot-decimal notation for IPv4.
*
* @see https://url.spec.whatwg.org/#concept-ipv4-parser
*
* @param mixed $ipAddress the number representation of the IPV4address
*/
private function long2Ip(mixed $ipAddress): string
{
$output = '';
for ($offset = 0; $offset < 4; $offset++) {
$output = $this->calculator->mod($ipAddress, 256).$output;
if ($offset < 3) {
$output = '.'.$output;
}
$ipAddress = $this->calculator->div($ipAddress, 256);
}
return $output;
}
}
+70
View File
@@ -0,0 +1,70 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\IPv4;
use GMP;
use function gmp_add;
use function gmp_cmp;
use function gmp_div_q;
use function gmp_init;
use function gmp_mod;
use function gmp_mul;
use function gmp_pow;
use function gmp_sub;
use const GMP_ROUND_MINUSINF;
final class GMPCalculator implements Calculator
{
public function baseConvert(mixed $value, int $base): GMP
{
return gmp_init($value, $base);
}
public function pow(mixed $value, int $exponent): GMP
{
return gmp_pow($value, $exponent);
}
public function compare(mixed $value1, mixed $value2): int
{
return gmp_cmp($value1, $value2);
}
public function multiply(mixed $value1, mixed $value2): GMP
{
return gmp_mul($value1, $value2);
}
public function div(mixed $value, mixed $base): GMP
{
return gmp_div_q($value, $base, GMP_ROUND_MINUSINF);
}
public function mod(mixed $value, mixed $base): GMP
{
return gmp_mod($value, $base);
}
public function add(mixed $value1, mixed $value2): GMP
{
return gmp_add($value1, $value2);
}
public function sub(mixed $value1, mixed $value2): GMP
{
return gmp_sub($value1, $value2);
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\IPv4;
use function floor;
use function intval;
final class NativeCalculator implements Calculator
{
public function baseConvert(mixed $value, int $base): int
{
return intval((string) $value, $base);
}
public function pow(mixed $value, int $exponent)
{
return $value ** $exponent;
}
public function compare(mixed $value1, mixed $value2): int
{
return $value1 <=> $value2;
}
public function multiply(mixed $value1, mixed $value2): int
{
return $value1 * $value2;
}
public function div(mixed $value, mixed $base): int
{
return (int) floor($value / $base);
}
public function mod(mixed $value, mixed $base): int
{
return $value % $base;
}
public function add(mixed $value1, mixed $value2): int
{
return $value1 + $value2;
}
public function sub(mixed $value1, mixed $value2): int
{
return $value1 - $value2;
}
}
+178
View File
@@ -0,0 +1,178 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\IPv6;
use BackedEnum;
use Stringable;
use ValueError;
use function filter_var;
use function implode;
use function inet_pton;
use function str_split;
use function strtolower;
use function unpack;
use const FILTER_FLAG_IPV6;
use const FILTER_VALIDATE_IP;
final class Converter
{
/**
* Significant 10 bits of IP to detect Zone ID regular expression pattern.
*
* @var string
*/
private const HOST_ADDRESS_BLOCK = "\xfe\x80";
public static function compressIp(BackedEnum|string $ipAddress): string
{
if ($ipAddress instanceof BackedEnum) {
$ipAddress = (string) $ipAddress->value;
}
return match (filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
false => throw new ValueError('The submitted IP is not a valid IPv6 address.'),
default => strtolower((string) inet_ntop((string) inet_pton($ipAddress))),
};
}
public static function expandIp(BackedEnum|string $ipAddress): string
{
if ($ipAddress instanceof BackedEnum) {
$ipAddress = (string) $ipAddress->value;
}
if (false === filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
throw new ValueError('The submitted IP is not a valid IPv6 address.');
}
$hex = (array) unpack('H*hex', (string) inet_pton($ipAddress));
return implode(':', str_split(strtolower($hex['hex'] ?? ''), 4));
}
public static function compress(BackedEnum|Stringable|string|null $host): ?string
{
$components = self::parse($host);
if (null === $components['ipAddress']) {
return match (true) {
null === $host => $host,
$host instanceof BackedEnum => (string) $host->value,
default => (string) $host,
};
}
$components['ipAddress'] = self::compressIp($components['ipAddress']);
return self::build($components);
}
public static function expand(Stringable|string|null $host): ?string
{
$components = self::parse($host);
if (null === $components['ipAddress']) {
return match ($host) {
null => $host,
default => (string) $host,
};
}
$components['ipAddress'] = self::expandIp($components['ipAddress']);
return self::build($components);
}
public static function build(array $components): string
{
$components['ipAddress'] ??= null;
$components['zoneIdentifier'] ??= null;
if (null === $components['ipAddress']) {
return '';
}
return '['.$components['ipAddress'].match ($components['zoneIdentifier']) {
null => '',
default => '%'.$components['zoneIdentifier'],
}.']';
}
/**
* @return array{ipAddress:string|null, zoneIdentifier:string|null}
*/
private static function parse(BackedEnum|Stringable|string|null $host): array
{
if (null === $host) {
return ['ipAddress' => null, 'zoneIdentifier' => null];
}
if ($host instanceof BackedEnum) {
$host = $host->value;
}
$host = (string) $host;
if ('' === $host) {
return ['ipAddress' => null, 'zoneIdentifier' => null];
}
if (!str_starts_with($host, '[')) {
return ['ipAddress' => null, 'zoneIdentifier' => null];
}
if (!str_ends_with($host, ']')) {
return ['ipAddress' => null, 'zoneIdentifier' => null];
}
[$ipv6, $zoneIdentifier] = explode('%', substr($host, 1, -1), 2) + [1 => null];
if (false === filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return ['ipAddress' => null, 'zoneIdentifier' => null];
}
return match (true) {
null === $zoneIdentifier,
is_string($ipv6) && str_starts_with((string)inet_pton($ipv6), self::HOST_ADDRESS_BLOCK) => ['ipAddress' => $ipv6, 'zoneIdentifier' => $zoneIdentifier],
default => ['ipAddress' => null, 'zoneIdentifier' => null],
};
}
/**
* Tells whether the host is an IPv6.
*/
public static function isIpv6(BackedEnum|Stringable|string|null $host): bool
{
return null !== self::parse($host)['ipAddress'];
}
public static function normalize(BackedEnum|Stringable|string|null $host): ?string
{
if ($host instanceof BackedEnum) {
$host = $host->value;
}
if (null === $host || '' === $host) {
return $host;
}
$host = (string) $host;
$components = self::parse($host);
if (null === $components['ipAddress']) {
return strtolower($host);
}
$components['ipAddress'] = strtolower($components['ipAddress']);
return self::build($components);
}
}
+231
View File
@@ -0,0 +1,231 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Idna;
use BackedEnum;
use League\Uri\Exceptions\ConversionFailed;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\FeatureDetection;
use Stringable;
use function idn_to_ascii;
use function idn_to_utf8;
use function rawurldecode;
use function strtolower;
use const INTL_IDNA_VARIANT_UTS46;
/**
* @see https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/uidna_8h.html
*/
final class Converter
{
private const REGEXP_IDNA_PATTERN = '/[^\x20-\x7f]/';
private const MAX_DOMAIN_LENGTH = 253;
private const MAX_LABEL_LENGTH = 63;
/**
* General registered name regular expression.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
* @see https://regex101.com/r/fptU8V/1
*/
private const REGEXP_REGISTERED_NAME = '/
(?(DEFINE)
(?<unreserved>[a-z0-9_~\-]) # . is missing as it is used to separate labels
(?<sub_delims>[!$&\'()*+,;=])
(?<encoded>%[A-F0-9]{2})
(?<reg_name>(?:(?&unreserved)|(?&sub_delims)|(?&encoded))*)
)
^(?:(?&reg_name)\.)*(?&reg_name)\.?$
/ix';
/**
* Converts the input to its IDNA ASCII form or throw on failure.
*
* @see Converter::toAscii()
*
* @throws SyntaxError if the string cannot be converted to UNICODE using IDN UTS46 algorithm
* @throws ConversionFailed if the conversion returns error
*/
public static function toAsciiOrFail(BackedEnum|Stringable|string $domain, Option|int|null $options = null): string
{
$result = self::toAscii($domain, $options);
return match (true) {
$result->hasErrors() => throw ConversionFailed::dueToIdnError($domain, $result),
default => $result->domain(),
};
}
/**
* Converts the input to its IDNA ASCII form.
*
* This method returns the string converted to IDN ASCII form
*
* @throws SyntaxError if the string cannot be converted to ASCII using IDN UTS46 algorithm
*/
public static function toAscii(BackedEnum|Stringable|string $domain, Option|int|null $options = null): Result
{
if ($domain instanceof BackedEnum) {
$domain = $domain->value;
}
$domain = rawurldecode((string) $domain);
if (1 === preg_match(self::REGEXP_IDNA_PATTERN, $domain)) {
FeatureDetection::supportsIdn();
$flags = match (true) {
null === $options => Option::forIDNA2008Ascii(),
$options instanceof Option => $options,
default => Option::new($options),
};
idn_to_ascii($domain, $flags->toBytes(), INTL_IDNA_VARIANT_UTS46, $idnaInfo);
if ([] === $idnaInfo) {
return Result::fromIntl([
'result' => strtolower($domain),
'isTransitionalDifferent' => false,
'errors' => self::validateDomainAndLabelLength($domain),
]);
}
return Result::fromIntl($idnaInfo);
}
$error = Error::NONE->value;
if (1 !== preg_match(self::REGEXP_REGISTERED_NAME, $domain)) {
$error |= Error::DISALLOWED->value;
}
return Result::fromIntl([
'result' => strtolower($domain),
'isTransitionalDifferent' => false,
'errors' => self::validateDomainAndLabelLength($domain) | $error,
]);
}
/**
* Converts the input to its IDNA UNICODE form or throw on failure.
*
* @see Converter::toUnicode()
*
* @throws ConversionFailed if the conversion returns error
*/
public static function toUnicodeOrFail(BackedEnum|Stringable|string $domain, Option|int|null $options = null): string
{
$result = self::toUnicode($domain, $options);
return match (true) {
$result->hasErrors() => throw ConversionFailed::dueToIdnError($domain, $result),
default => $result->domain(),
};
}
/**
* Converts the input to its IDNA UNICODE form.
*
* This method returns the string converted to IDN UNICODE form
*
* @throws SyntaxError if the string cannot be converted to UNICODE using IDN UTS46 algorithm
*/
public static function toUnicode(BackedEnum|Stringable|string $domain, Option|int|null $options = null): Result
{
if ($domain instanceof BackedEnum) {
$domain = $domain->value;
}
$domain = rawurldecode((string) $domain);
if (false === stripos($domain, 'xn--')) {
return Result::fromIntl(['result' => strtolower($domain), 'isTransitionalDifferent' => false, 'errors' => Error::NONE->value]);
}
FeatureDetection::supportsIdn();
$flags = match (true) {
null === $options => Option::forIDNA2008Unicode(),
$options instanceof Option => $options,
default => Option::new($options),
};
idn_to_utf8($domain, $flags->toBytes(), INTL_IDNA_VARIANT_UTS46, $idnaInfo);
if ([] === $idnaInfo) {
return Result::fromIntl(['result' => strtolower($domain), 'isTransitionalDifferent' => false, 'errors' => Error::NONE->value]);
}
return Result::fromIntl($idnaInfo);
}
/**
* Tells whether the submitted host is a valid IDN regardless of its format.
*
* Returns false if the host is invalid or if its conversion yields the same result
*/
public static function isIdn(BackedEnum|Stringable|string|null $domain): bool
{
if ($domain instanceof BackedEnum) {
$domain = $domain->value;
}
$domain = strtolower(rawurldecode((string) $domain));
$result = match (1) {
preg_match(self::REGEXP_IDNA_PATTERN, $domain) => self::toAscii($domain),
default => self::toUnicode($domain),
};
return match (true) {
$result->hasErrors() => false,
default => $result->domain() !== $domain,
};
}
/**
* Adapted from https://github.com/TRowbotham/idna.
*
* @see https://github.com/TRowbotham/idna/blob/master/src/Idna.php#L236
*/
private static function validateDomainAndLabelLength(string $domain): int
{
$error = Error::NONE->value;
$labels = explode('.', $domain);
$maxDomainSize = self::MAX_DOMAIN_LENGTH;
$length = count($labels);
// If the last label is empty, and it is not the first label, then it is the root label.
// Increase the max size by 1, making it 254, to account for the root label's "."
// delimiter. This also means we don't need to check the last label's length for being too
// long.
if ($length > 1 && '' === $labels[$length - 1]) {
++$maxDomainSize;
array_pop($labels);
}
if (strlen($domain) > $maxDomainSize) {
$error |= Error::DOMAIN_NAME_TOO_LONG->value;
}
foreach ($labels as $label) {
if (strlen($label) > self::MAX_LABEL_LENGTH) {
$error |= Error::LABEL_TOO_LONG->value;
break;
}
}
return $error;
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\Uri\Idna;
enum Error: int
{
case NONE = 0;
case EMPTY_LABEL = 1;
case LABEL_TOO_LONG = 2;
case DOMAIN_NAME_TOO_LONG = 4;
case LEADING_HYPHEN = 8;
case TRAILING_HYPHEN = 0x10;
case HYPHEN_3_4 = 0x20;
case LEADING_COMBINING_MARK = 0x40;
case DISALLOWED = 0x80;
case PUNYCODE = 0x100;
case LABEL_HAS_DOT = 0x200;
case INVALID_ACE_LABEL = 0x400;
case BIDI = 0x800;
case CONTEXTJ = 0x1000;
case CONTEXTO_PUNCTUATION = 0x2000;
case CONTEXTO_DIGITS = 0x4000;
public function description(): string
{
return match ($this) {
self::NONE => 'No error has occurred',
self::EMPTY_LABEL => 'a non-final domain name label (or the whole domain name) is empty',
self::LABEL_TOO_LONG => 'a domain name label is longer than 63 bytes',
self::DOMAIN_NAME_TOO_LONG => 'a domain name is longer than 255 bytes in its storage form',
self::LEADING_HYPHEN => 'a label starts with a hyphen-minus ("-")',
self::TRAILING_HYPHEN => 'a label ends with a hyphen-minus ("-")',
self::HYPHEN_3_4 => 'a label contains hyphen-minus ("-") in the third and fourth positions',
self::LEADING_COMBINING_MARK => 'a label starts with a combining mark',
self::DISALLOWED => 'a label or domain name contains disallowed characters',
self::PUNYCODE => 'a label starts with "xn--" but does not contain valid Punycode',
self::LABEL_HAS_DOT => 'a label contains a dot=full stop',
self::INVALID_ACE_LABEL => 'An ACE label does not contain a valid label string',
self::BIDI => 'a label does not meet the IDNA BiDi requirements (for right-to-left characters)',
self::CONTEXTJ => 'a label does not meet the IDNA CONTEXTJ requirements',
self::CONTEXTO_DIGITS => 'a label does not meet the IDNA CONTEXTO requirements for digits',
self::CONTEXTO_PUNCTUATION => 'a label does not meet the IDNA CONTEXTO requirements for punctuation characters. Some punctuation characters "Would otherwise have been DISALLOWED" but are allowed in certain contexts',
};
}
public static function filterByErrorBytes(int $errors): array
{
return array_values(
array_filter(
self::cases(),
fn (self $error): bool => 0 !== ($error->value & $errors)
)
);
}
}
+179
View File
@@ -0,0 +1,179 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Idna;
use ReflectionClass;
use ReflectionClassConstant;
/**
* @see https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/uidna_8h.html
*/
final class Option
{
private const DEFAULT = 0;
private const ALLOW_UNASSIGNED = 1;
private const USE_STD3_RULES = 2;
private const CHECK_BIDI = 4;
private const CHECK_CONTEXTJ = 8;
private const NONTRANSITIONAL_TO_ASCII = 0x10;
private const NONTRANSITIONAL_TO_UNICODE = 0x20;
private const CHECK_CONTEXTO = 0x40;
private function __construct(private readonly int $value)
{
}
private static function cases(): array
{
static $assoc;
if (null === $assoc) {
$assoc = [];
$fooClass = new ReflectionClass(self::class);
foreach ($fooClass->getConstants(ReflectionClassConstant::IS_PRIVATE) as $name => $value) {
$assoc[$name] = $value;
}
}
return $assoc;
}
public static function new(int $bytes = self::DEFAULT): self
{
return new self(array_reduce(
self::cases(),
fn (int $value, int $option) => 0 !== ($option & $bytes) ? ($value | $option) : $value,
self::DEFAULT
));
}
public static function forIDNA2008Ascii(): self
{
return self::new()
->nonTransitionalToAscii()
->checkBidi()
->useSTD3Rules()
->checkContextJ();
}
public static function forIDNA2008Unicode(): self
{
return self::new()
->nonTransitionalToUnicode()
->checkBidi()
->useSTD3Rules()
->checkContextJ();
}
public function toBytes(): int
{
return $this->value;
}
/** array<string, int> */
public function list(): array
{
return array_keys(array_filter(
self::cases(),
fn (int $value) => 0 !== ($value & $this->value)
));
}
public function allowUnassigned(): self
{
return $this->add(self::ALLOW_UNASSIGNED);
}
public function disallowUnassigned(): self
{
return $this->remove(self::ALLOW_UNASSIGNED);
}
public function useSTD3Rules(): self
{
return $this->add(self::USE_STD3_RULES);
}
public function prohibitSTD3Rules(): self
{
return $this->remove(self::USE_STD3_RULES);
}
public function checkBidi(): self
{
return $this->add(self::CHECK_BIDI);
}
public function ignoreBidi(): self
{
return $this->remove(self::CHECK_BIDI);
}
public function checkContextJ(): self
{
return $this->add(self::CHECK_CONTEXTJ);
}
public function ignoreContextJ(): self
{
return $this->remove(self::CHECK_CONTEXTJ);
}
public function checkContextO(): self
{
return $this->add(self::CHECK_CONTEXTO);
}
public function ignoreContextO(): self
{
return $this->remove(self::CHECK_CONTEXTO);
}
public function nonTransitionalToAscii(): self
{
return $this->add(self::NONTRANSITIONAL_TO_ASCII);
}
public function transitionalToAscii(): self
{
return $this->remove(self::NONTRANSITIONAL_TO_ASCII);
}
public function nonTransitionalToUnicode(): self
{
return $this->add(self::NONTRANSITIONAL_TO_UNICODE);
}
public function transitionalToUnicode(): self
{
return $this->remove(self::NONTRANSITIONAL_TO_UNICODE);
}
public function add(Option|int|null $option = null): self
{
return match (true) {
null === $option => $this,
$option instanceof self => self::new($this->value | $option->value),
default => self::new($this->value | $option),
};
}
public function remove(Option|int|null $option = null): self
{
return match (true) {
null === $option => $this,
$option instanceof self => self::new($this->value & ~$option->value),
default => self::new($this->value & ~$option),
};
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\Idna;
/**
* @see https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/uidna_8h.html
*/
final class Result
{
private function __construct(
private readonly string $domain,
private readonly bool $isTransitionalDifferent,
/** @var array<Error> */
private readonly array $errors
) {
}
/**
* @param array{result:string, isTransitionalDifferent:bool, errors:int} $infos
*/
public static function fromIntl(array $infos): self
{
return new self($infos['result'], $infos['isTransitionalDifferent'], Error::filterByErrorBytes($infos['errors']));
}
public function domain(): string
{
return $this->domain;
}
public function isTransitionalDifferent(): bool
{
return $this->isTransitionalDifferent;
}
/**
* @return array<Error>
*/
public function errors(): array
{
return $this->errors;
}
public function hasErrors(): bool
{
return [] !== $this->errors;
}
public function hasError(Error $error): bool
{
return in_array($error, $this->errors, true);
}
}
+188
View File
@@ -0,0 +1,188 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri\KeyValuePair;
use BackedEnum;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\StringCoercionMode;
use Stringable;
use function array_combine;
use function explode;
use function implode;
use function is_string;
use function preg_match;
use function str_replace;
use const PHP_QUERY_RFC1738;
use const PHP_QUERY_RFC3986;
final class Converter
{
private const REGEXP_INVALID_CHARS = '/[\x00-\x1f\x7f]/';
/**
* @param non-empty-string $separator the query string separator
* @param array<string> $fromRfc3986 contains all the RFC3986 encoded characters to be converted
* @param array<string> $toEncoding contains all the expected encoded characters
*/
private function __construct(
private readonly string $separator,
private readonly array $fromRfc3986 = [],
private readonly array $toEncoding = [],
) {
if ('' === $this->separator) {
throw new SyntaxError('The separator character must be a non empty string.');
}
}
/**
* @param non-empty-string $separator
*/
public static function new(string $separator): self
{
return new self($separator);
}
/**
* @param non-empty-string $separator
*/
public static function fromRFC3986(string $separator = '&'): self
{
return self::new($separator);
}
/**
* @param non-empty-string $separator
*/
public static function fromRFC1738(string $separator = '&'): self
{
return self::new($separator)
->withEncodingMap(['%20' => '+']);
}
/**
* @param non-empty-string $separator
*
* @see https://url.spec.whatwg.org/#application/x-www-form-urlencoded
*/
public static function fromFormData(string $separator = '&'): self
{
return self::new($separator)
->withEncodingMap(['%20' => '+', '%2A' => '*']);
}
public static function fromEncodingType(int $encType): self
{
return match ($encType) {
PHP_QUERY_RFC3986 => self::fromRFC3986(),
PHP_QUERY_RFC1738 => self::fromRFC1738(),
default => throw new SyntaxError('Unknown or Unsupported encoding.'),
};
}
/**
* @return non-empty-string
*/
public function separator(): string
{
return $this->separator;
}
/**
* @return array<string, string>
*/
public function encodingMap(): array
{
return array_combine($this->fromRfc3986, $this->toEncoding);
}
/**
* @return array<non-empty-list<string|null>>
*/
public function toPairs(BackedEnum|Stringable|string|int|float|bool|null $value): array
{
$value = StringCoercionMode::Native->coerce($value);
if (null === $value) {
return [];
}
$value = match (1) {
preg_match(self::REGEXP_INVALID_CHARS, $value) => throw new SyntaxError('Invalid query string: `'.$value.'`.'),
default => str_replace($this->toEncoding, $this->fromRfc3986, $value),
};
return array_map(
fn (string $pair): array => explode('=', $pair, 2) + [1 => null],
explode($this->separator, $value)
);
}
/**
* @param iterable<array{0:string|null, 1:BackedEnum|Stringable|string|bool|int|float|null}> $pairs
*/
public function toValue(iterable $pairs): ?string
{
$filteredPairs = [];
foreach ($pairs as $pair) {
$filteredPairs[] = match (true) {
!is_string($pair[0]) => throw new SyntaxError('the pair key MUST be a string;, `'.gettype($pair[0]).'` given.'),
null === $pair[1] => StringCoercionMode::Native->coerce($pair[0]),
default => StringCoercionMode::Native->coerce($pair[0]).'='.StringCoercionMode::Native->coerce($pair[1]),
};
}
return match ([]) {
$filteredPairs => null,
default => str_replace($this->fromRfc3986, $this->toEncoding, implode($this->separator, $filteredPairs)),
};
}
/**
* @param non-empty-string $separator
*/
public function withSeparator(string $separator): self
{
return match ($this->separator) {
$separator => $this,
default => new self($separator, $this->fromRfc3986, $this->toEncoding),
};
}
/**
* Sets the conversion map.
*
* Each key from the iterable structure represents the RFC3986 encoded characters as string,
* while each value represents the expected output encoded characters
*/
public function withEncodingMap(iterable $encodingMap): self
{
$fromRfc3986 = [];
$toEncoding = [];
foreach ($encodingMap as $from => $to) {
[$fromRfc3986[], $toEncoding[]] = match (true) {
!is_string($from) => throw new SyntaxError('The encoding output must be a string; `'.gettype($from).'` given.'),
$to instanceof Stringable,
is_string($to) => [$from, (string) $to],
default => throw new SyntaxError('The encoding output must be a string; `'.gettype($to).'` given.'),
};
}
return match (true) {
$fromRfc3986 !== $this->fromRfc3986,
$toEncoding !== $this->toEncoding => new self($this->separator, $fromRfc3986, $toEncoding),
default => $this,
};
}
}
+20
View File
@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2015 ignace nyamagana butera
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+88
View File
@@ -0,0 +1,88 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
enum QueryComposeMode
{
/**
* Pre-PHP 8.4 Mode.
*
* Strictly uses get_object_vars on objects (Enum included)
* If the value can not be serialized the entry is skipped.
*
* ie http_build_query behavior before PHP8.4
*/
case Compatible;
/**
* PHP 8.4+ enum-compatible lenient mode.
*
* Provides stable support for BackedEnum values.
* UnitEnum values are skipped.
* Uses get_object_vars() for non-enum objects.
* Unserializable values are skipped.
*
* Behaves like {@see QueryComposeMode::EnumCompatible}
* but does not throw for UnitEnum values.
*
* Mirrors http_build_query behavior in PHP 8.4+,
* except that error cases are silently ignored
* instead of throwing.
*
* This mode is tolerant by design and skips entries that would otherwise
* result in an exception in {@see QueryComposeMode::EnumCompatible}.
*/
case EnumLenient;
/**
* PHP 8.4+ mode.
*
* Provides stable support for BackedEnum values.
* Throws for UnitEnum.
* Uses get_object_vars() for non-enum objects.
* Unserializable values are skipped.
*
* http_build_query behavior in PHP 8.4+.
*/
case EnumCompatible;
/**
* Use PHP version http_build_query algorithm.
*
* In pre-PHP8.4 you get the same results as `Compatible`
* In PHP PHP8.4+ you get the same results as `EnumCompatible`
*/
case Native;
/**
* Validation-first mode.
*
* Guarantees that only scalar values, BackedEnum, and null are accepted.
* Any object, UnitEnum, resource, or recursive structure
* results in an exception.
*
* - null: the key name is used but the separator and its content are omitted
* - string: used as-is
* - bool: converted to string “0” (false) or “1” (true)
* - int: converted to numeric string (123 -> “123”)
* - float: converted to decimal string (3.14 -> “3.14”)
* - Backed Enum: converted to their backing value and then stringify see int and string
* - array: empty array: An empty array has zero items, therefore empty arrays are omitted from the query parameter list.
* - lists: Becomes a repeated name suffixed with empty brackets (ie "a" with ["foo", false, 1.23] will result in a[]=foo&a[]=0&a[]=1.23)
* - maps: Becomes a repeated name suffixed with brackets containing the key (ie "a" with ["b" => "foo", "c" => false, "d" => 1.23] will result in a[b]=foo&a[c]=0&a[d]=1.23)
*
* This contract is stable and independent of PHP's http_build_query implementation.
*/
case Safe;
}
+50
View File
@@ -0,0 +1,50 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
enum QueryExtractMode
{
/**
* Parses the query string using parse_str algorithm.
*/
case Native;
/**
* Parses the query string like parse_str without mangling result keys.
*
* The result is similar to PHP parse_str when used with its second argument,
* with the difference that variable names are not mangled.
*
* Behavior details:
* - Empty names are ignored
* - If a name is duplicated, the last value overwrites the previous one
* - If no "[" is detected, the value is added using the name as the array key
* - If "[" is detected but no matching "]" exists, the value is added using the name as the array key
* - If bracket usage is malformed, the remaining part is dropped
* - "." and " " are NOT converted to "_"
* - If no "]" exists, the first "[" is not converted to "_"
* - No whitespace trimming is performed on keys
*
* @see https://www.php.net/parse_str
* @see https://wiki.php.net/rfc/on_demand_name_mangling
*/
case Unmangled;
/**
* Same as QueryParsingMode::Unmangled and additionally
* preserves null values instead of converting them
* to empty strings.
*/
case LossLess;
}
+495
View File
@@ -0,0 +1,495 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
use BackedEnum;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\KeyValuePair\Converter;
use ReflectionEnum;
use ReflectionException;
use SplObjectStorage;
use Stringable;
use TypeError;
use UnitEnum;
use ValueError;
use function array_is_list;
use function array_key_exists;
use function array_keys;
use function get_debug_type;
use function get_object_vars;
use function http_build_query;
use function implode;
use function is_array;
use function is_object;
use function is_resource;
use function is_scalar;
use function rawurldecode;
use function str_replace;
use function strpos;
use function substr;
use const PHP_QUERY_RFC1738;
use const PHP_QUERY_RFC3986;
/**
* A class to parse the URI query string.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.4
*/
final class QueryString
{
private const PAIR_VALUE_DECODED = 1;
private const PAIR_VALUE_PRESERVED = 2;
private const RECURSION_MARKER = "\0__RECURSION_INTERNAL_MARKER__\0";
/**
* @codeCoverageIgnore
*/
private function __construct()
{
}
/**
* Build a query string from a list of pairs.
*
* @see QueryString::buildFromPairs()
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.2
*
* @param iterable<array{0:string, 1:mixed}> $pairs
* @param non-empty-string $separator
*
* @throws SyntaxError If the encoding type is invalid
* @throws SyntaxError If a pair is invalid
*/
public static function build(iterable $pairs, string $separator = '&', int $encType = PHP_QUERY_RFC3986, StringCoercionMode $coercionMode = StringCoercionMode::Native): ?string
{
return self::buildFromPairs($pairs, Converter::fromEncodingType($encType)->withSeparator($separator), $coercionMode);
}
/**
* Build a query string from a list of pairs.
*
* The method expects the return value from Query::parse to build
* a valid query string. This method differs from PHP http_build_query as
* it does not modify parameters keys.
*
* If a reserved character is found in a URI component and
* no delimiting role is known for that character, then it must be
* interpreted as representing the data octet corresponding to that
* character's encoding in US-ASCII.
*
* @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.2
*
* @param iterable<array{0:string, 1:mixed}> $pairs
*
* @throws SyntaxError If the encoding type is invalid
* @throws SyntaxError If a pair is invalid
*/
public static function buildFromPairs(iterable $pairs, ?Converter $converter = null, StringCoercionMode $coercionMode = StringCoercionMode::Native): ?string
{
$keyValuePairs = [];
foreach ($pairs as $pair) {
if (!is_array($pair) || [0, 1] !== array_keys($pair)) {
throw new SyntaxError('A pair must be a sequential array starting at `0` and containing two elements.');
}
[$key, $value] = $pair;
$coercionMode->isCoercible($value) || throw new SyntaxError('Converting a type `'.get_debug_type($value).'` into a string is not supported by the '.(StringCoercionMode::Native === $coercionMode ? 'PHP Native' : 'Ecmascript').' coercion mode.');
try {
$key = $coercionMode->coerce($key);
$value = $coercionMode->coerce($value);
} catch (TypeError $typeError) {
throw new SyntaxError('The pair can not be converted to build a query string.', previous: $typeError);
}
$keyValuePairs[] = [(string) Encoder::encodeQueryKeyValue($key), null === $value ? null : Encoder::encodeQueryKeyValue($value)];
}
return ($converter ?? Converter::fromRFC3986())->toValue($keyValuePairs);
}
/**
* Build a query string from an object or an array like http_build_query without discarding values.
* The method differs from http_build_query for the following behavior:
*
* - if a resource is used, a TypeError is thrown.
* - if a recursion is detected a ValueError is thrown
* - the method preserves value with `null` value (http_build_query) skip the key.
* - the method does not handle prefix usage
*
* @param array<array-key, mixed> $data
* @param non-empty-string $separator
*
* @throws TypeError if a resource is found it the input array
* @throws ValueError if a recursion is detected
*/
public static function compose(
array|object $data,
string $separator = '&',
int $encType = PHP_QUERY_RFC1738,
QueryComposeMode $composeMode = QueryComposeMode::Native
): ?string {
if (QueryComposeMode::Native === $composeMode) {
return http_build_query(data: $data, arg_separator: $separator, encoding_type: $encType);
}
$query = self::composeFromValue($data, Converter::fromEncodingType($encType)->withSeparator($separator), $composeMode);
return QueryComposeMode::Safe !== $composeMode ? (string) $query : $query;
}
public static function composeFromValue(
array|object $data,
?Converter $converter = null,
QueryComposeMode $composeMode = QueryComposeMode::Native,
): ?string {
if (QueryComposeMode::EnumLenient === $composeMode && $data instanceof UnitEnum && !$data instanceof BackedEnum) {
return '';
}
QueryComposeMode::Safe !== $composeMode || is_array($data) || throw new TypeError('In safe mode only arrays are supported.');
$converter ??= Converter::fromRFC3986();
$pairs = QueryComposeMode::Native !== $composeMode
? self::composeRecursive($composeMode, $data)
: self::parseFromValue(http_build_query(data: $data, arg_separator: '&'), Converter::fromRFC1738());
return self::buildFromPairs($pairs, $converter);
}
/**
* @param array<array-key, mixed>|object $data
* @param SplObjectStorage<object, null> $seenObjects
*
* @throws TypeError if a resource is found it the input array
* @throws ValueError if a recursion is detected
* @throws ReflectionException if reflection is not possible on the Enum
*
* @return iterable<array{0: array-key, 1: string|int|float|bool|null}>
*/
private static function composeRecursive(
QueryComposeMode $composeMode,
array|object $data,
string|int $prefix = '',
SplObjectStorage $seenObjects = new SplObjectStorage(),
): iterable {
QueryComposeMode::Safe !== $composeMode || is_array($data) || throw new TypeError('In safe mode only arrays are supported.');
in_array($composeMode, [QueryComposeMode::EnumCompatible, QueryComposeMode::EnumLenient], true) || !$data instanceof UnitEnum || throw new TypeError('Argument #1 ($data) must not be an enum, '.((new ReflectionEnum($data::class))->isBacked() ? 'Backed' : 'Pure').' given') ;
if (is_object($data)) {
if ($seenObjects->contains($data)) {
QueryComposeMode::Safe !== $composeMode || throw new ValueError('composition failed; circular reference detected.');
return;
}
$seenObjects->attach($data);
$data = get_object_vars($data);
}
if (self::hasCircularReference($data)) {
QueryComposeMode::Safe !== $composeMode || throw new ValueError('composition failed; circular reference detected.');
return;
}
$stripIndices = QueryComposeMode::Safe === $composeMode && array_is_list($data);
foreach ($data as $name => $value) {
$name = $stripIndices ? '' : $name;
if ('' !== $prefix) {
$name = $prefix.'['.$name.']';
}
if (is_resource($value)) {
QueryComposeMode::Safe !== $composeMode || throw new TypeError('composition failed; a resource has been detected and can not be converted.');
continue;
}
if (is_scalar($value)) {
yield [$name, $value];
continue;
}
if (null === $value) {
if (QueryComposeMode::Safe === $composeMode) {
yield [$name, $value];
}
continue;
}
if ($value instanceof BackedEnum) {
if (QueryComposeMode::Compatible !== $composeMode) {
yield [$name, $value->value];
continue;
}
$value = get_object_vars($value);
}
if ($value instanceof UnitEnum) {
if (QueryComposeMode::EnumLenient === $composeMode) {
continue;
}
QueryComposeMode::Compatible === $composeMode || throw new TypeError('Unbacked enum '.$value::class.' cannot be converted to a string');
$value = get_object_vars($value);
}
if (QueryComposeMode::Safe === $composeMode && is_object($value)) {
throw new ValueError('In conservative mode only arrays, scalar value or null are supported.');
}
yield from self::composeRecursive($composeMode, $value, $name, $seenObjects);
}
}
/**
* Array recursion detection.
* @see https://stackoverflow.com/questions/9042142/detecting-infinite-array-recursion-in-php
*/
private static function hasCircularReference(array &$arr): bool
{
if (isset($arr[self::RECURSION_MARKER])) {
return true;
}
try {
$arr[self::RECURSION_MARKER] = true;
foreach ($arr as $key => &$value) {
if (self::RECURSION_MARKER !== $key && is_array($value) && self::hasCircularReference($value)) {
return true;
}
}
return false;
} finally {
unset($arr[self::RECURSION_MARKER]);
}
}
/**
* Parses the query string.
*
* The result depends on the query parsing mode
*
* @see QueryString::extractFromValue()
*
* @param non-empty-string $separator
*
* @throws SyntaxError
*/
public static function extract(
BackedEnum|Stringable|string|bool|null $query,
string $separator = '&',
int $encType = PHP_QUERY_RFC3986,
QueryExtractMode $extractMode = QueryExtractMode::Unmangled,
): array {
return self::extractFromValue(
$query,
Converter::fromEncodingType($encType)->withSeparator($separator),
$extractMode,
);
}
/**
* Parses the query string.
*
* The result depends on the query parsing mode
*
* @throws SyntaxError
*/
public static function extractFromValue(
BackedEnum|Stringable|string|bool|null $query,
?Converter $converter = null,
QueryExtractMode $extractMode = QueryExtractMode::Unmangled,
): array {
$pairs = ($converter ?? Converter::fromRFC3986())->toPairs($query);
if (QueryExtractMode::Native === $extractMode) {
if ([] === $pairs) {
return [];
}
$data = [];
foreach ($pairs as [$key, $value]) {
$key = str_replace('&', '%26', (string) $key);
$data[] = null === $value ? $key : $key.'='.str_replace('&', '%26', $value);
}
parse_str(implode('&', $data), $result);
return $result;
}
return self::convert(
self::decodePairs($pairs, self::PAIR_VALUE_PRESERVED),
$extractMode
);
}
/**
* Parses a query string into a collection of key/value pairs.
*
* @param non-empty-string $separator
*
* @throws SyntaxError
*
* @return array<int, array{0:string, 1:string|null}>
*/
public static function parse(BackedEnum|Stringable|string|bool|null $query, string $separator = '&', int $encType = PHP_QUERY_RFC3986): array
{
return self::parseFromValue($query, Converter::fromEncodingType($encType)->withSeparator($separator));
}
/**
* Parses a query string into a collection of key/value pairs.
*
* @throws SyntaxError
*
* @return array<int, array{0:string, 1:string|null}>
*/
public static function parseFromValue(BackedEnum|Stringable|string|bool|null $query, ?Converter $converter = null): array
{
return self::decodePairs(
($converter ?? Converter::fromRFC3986())->toPairs($query),
self::PAIR_VALUE_DECODED
);
}
/**
* @param array<non-empty-list<string|null>> $pairs
*
* @return array<int, array{0:string, 1:string|null}>
*/
private static function decodePairs(array $pairs, int $pairValueState): array
{
$decodePair = static function (array $pair, int $pairValueState): array {
[$key, $value] = $pair;
return match ($pairValueState) {
self::PAIR_VALUE_PRESERVED => [(string) Encoder::decodeAll($key), $value],
default => [(string) Encoder::decodeAll($key), Encoder::decodeAll($value)],
};
};
return array_reduce(
$pairs,
fn (array $carry, array $pair) => [...$carry, $decodePair($pair, $pairValueState)],
[]
);
}
/**
* Converts a collection of key/value pairs and returns
* the store PHP variables as elements of an array.
*/
public static function convert(iterable $pairs, QueryExtractMode $extractMode = QueryExtractMode::Unmangled): array
{
$returnedValue = [];
foreach ($pairs as $pair) {
$returnedValue = self::extractPhpVariable($returnedValue, $pair, extractMode: $extractMode);
}
return $returnedValue;
}
/**
* Parses a query pair like parse_str without mangling the results array keys.
*
* <ul>
* <li>empty name are not saved</li>
* <li>If the value from name is duplicated its corresponding value will be overwritten</li>
* <li>if no "[" is detected the value is added to the return array with the name as index</li>
* <li>if no "]" is detected after detecting a "[" the value is added to the return array with the name as index</li>
* <li>if there's a mismatch in bracket usage the remaining part is dropped</li>
* <li>“.” and “ ” are not converted to “_”</li>
* <li>If there is no “]”, then the first “[” is not converted to becomes an “_”</li>
* <li>no whitespace trimming is done on the key value</li>
* </ul>
*
* @see https://php.net/parse_str
* @see https://wiki.php.net/rfc/on_demand_name_mangling
* @see https://github.com/php/php-src/blob/master/ext/standard/tests/strings/parse_str_basic1.phpt
* @see https://github.com/php/php-src/blob/master/ext/standard/tests/strings/parse_str_basic2.phpt
* @see https://github.com/php/php-src/blob/master/ext/standard/tests/strings/parse_str_basic3.phpt
* @see https://github.com/php/php-src/blob/master/ext/standard/tests/strings/parse_str_basic4.phpt
*
* @param array $data the submitted array
* @param array|string $name the pair key
* @param string $value the pair value
*/
private static function extractPhpVariable(
array $data,
array|string $name,
?string $value = '',
QueryExtractMode $extractMode = QueryExtractMode::Unmangled
): array {
if (is_array($name)) {
[$name, $value] = $name;
if (null !== $value || QueryExtractMode::LossLess !== $extractMode) {
$value = rawurldecode((string) $value);
}
}
if ('' === $name) {
return $data;
}
$leftBracketPosition = strpos($name, '[');
if (false === $leftBracketPosition) {
$data[$name] = $value;
return $data;
}
$rightBracketPosition = strpos($name, ']', $leftBracketPosition);
if (false === $rightBracketPosition) {
$data[$name] = $value;
return $data;
}
$key = substr($name, 0, $leftBracketPosition);
if ('' === $key) {
$key = '0';
}
if (!array_key_exists($key, $data) || !is_array($data[$key])) {
$data[$key] = [];
}
$remaining = substr($name, $rightBracketPosition + 1);
if (!str_starts_with($remaining, '[') || !str_contains($remaining, ']')) {
$remaining = '';
}
$name = substr($name, $leftBracketPosition + 1, $rightBracketPosition - $leftBracketPosition - 1).$remaining;
if ('' === $name) {
$data[$key][] = $value;
return $data;
}
$data[$key] = self::extractPhpVariable($data[$key], $name, $value, $extractMode);
return $data;
}
}
+160
View File
@@ -0,0 +1,160 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
use BackedEnum;
use DateTimeInterface;
use League\Uri\Contracts\UriComponentInterface;
use Stringable;
use TypeError;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use ValueError;
use function array_is_list;
use function array_map;
use function get_debug_type;
use function implode;
use function is_array;
use function is_float;
use function is_infinite;
use function is_nan;
use function is_object;
use function is_resource;
use function is_scalar;
use function json_encode;
use const JSON_PRESERVE_ZERO_FRACTION;
enum StringCoercionMode
{
/**
* PHP conversion mode.
*
* Guarantees that only scalar values, BackedEnum, and null are accepted.
* Any object, Non-backed enums, resource, or recursive structure results in an error.
*
* - null: is not converted and stays the `null` value
* - string: used as-is
* - bool: converted to string “0” (false) or “1” (true)
* - int: converted to numeric string (123 -> “123”)
* - float: converted to decimal string (3.14 -> “3.14”)
* - Backed Enum: converted to their backing value and then stringify see int and string
*/
case Native;
/**
* Ecmascript conversion mode.
*
* Guarantees that only scalar values, BackedEnum, and null are accepted.
* Any resource, or recursive structure results in an error.
*
* - null: converted to string “null”
* - string: used as-is
* - bool: converted to string “false” (false) or “true” (true)
* - int: converted to numeric string (123 -> “123”)
* - float: converted to decimal string (3.14 -> “3.14”), "NaN", "-Infinity" or "Infinity"
* - Backed Enum: converted to their backing value and then stringify see int and string
* - Array as list are flatten into a string list using the "," character as separator
* - Associative array, Non-backed enums, any object without stringification semantics is coerced to "[object Object]".
* - DateTimeInterface implementing object are coerce to their string representation using DateTimeInterface::RFC2822 format
*/
case Ecmascript;
private const RECURSION_MARKER = "\0__RECURSION_INTERNAL_MARKER_WHATWG__\0";
public function isCoercible(mixed $value): bool
{
return self::Ecmascript === $this
? !is_resource($value)
: match (true) {
$value instanceof Rfc3986Uri,
$value instanceof WhatWgUrl,
$value instanceof BackedEnum,
$value instanceof Stringable,
is_scalar($value),
null === $value => true,
default => false,
};
}
/**
* @throws TypeError if the type is not supported by the specific case
* @throws ValueError if circular reference is detected
*/
public function coerce(mixed $value): ?string
{
return match ($this) {
self::Ecmascript => match (true) {
$value instanceof Rfc3986Uri => $value->toString(),
$value instanceof WhatWgUrl => $value->toAsciiString(),
$value instanceof DateTimeInterface => $value->format(DateTimeInterface::RFC2822),
$value instanceof BackedEnum => (string) $value->value,
$value instanceof Stringable => $value->__toString(),
is_object($value) => '[object Object]',
is_array($value) => match (true) {
self::hasCircularReference($value) => throw new ValueError('Recursive array structure detected; unable to coerce value.'),
array_is_list($value) => implode(',', array_map($this->coerce(...), $value)),
default => '[object Object]',
},
true === $value => 'true',
false === $value => 'false',
null === $value => 'null',
is_float($value) => match (true) {
is_nan($value) => 'NaN',
is_infinite($value) => 0 < $value ? 'Infinity' : '-Infinity',
default => (string) json_encode($value, JSON_PRESERVE_ZERO_FRACTION),
},
is_scalar($value) => (string) $value,
default => throw new TypeError('Unable to coerce value of type "'.get_debug_type($value).'" with "'.$this->name.'" coercion.'),
},
self::Native => match (true) {
$value instanceof UriComponentInterface => $value->value(),
$value instanceof WhatWgUrl => $value->toAsciiString(),
$value instanceof Rfc3986Uri => $value->toString(),
$value instanceof BackedEnum => (string) $value->value,
$value instanceof Stringable => $value->__toString(),
false === $value => '0',
true === $value => '1',
null === $value => null,
is_scalar($value) => (string) $value,
default => throw new TypeError('Unable to coerce value of type "'.get_debug_type($value).'" with "'.$this->name.'" coercion.'),
},
};
}
/**
* Array recursion detection.
* @see https://stackoverflow.com/questions/9042142/detecting-infinite-array-recursion-in-php
*/
private static function hasCircularReference(array &$arr): bool
{
if (isset($arr[self::RECURSION_MARKER])) {
return true;
}
try {
$arr[self::RECURSION_MARKER] = true;
foreach ($arr as $key => &$value) {
if (self::RECURSION_MARKER !== $key && is_array($value) && self::hasCircularReference($value)) {
return true;
}
}
return false;
} finally {
unset($arr[self::RECURSION_MARKER]);
}
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
enum UriComparisonMode
{
case IncludeFragment;
case ExcludeFragment;
}
+744
View File
@@ -0,0 +1,744 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
use BackedEnum;
use Deprecated;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\Idna\Converter as IdnaConverter;
use Stringable;
use Throwable;
use function array_map;
use function array_merge;
use function array_pop;
use function array_reduce;
use function defined;
use function explode;
use function filter_var;
use function function_exists;
use function implode;
use function preg_match;
use function sprintf;
use function str_replace;
use function strpos;
use function strtolower;
use function substr;
use const FILTER_FLAG_IPV4;
use const FILTER_VALIDATE_IP;
/**
* A class to parse a URI string according to RFC3986.
*
* @link https://tools.ietf.org/html/rfc3986
* @package League\Uri
* @author Ignace Nyamagana Butera <nyamsprod@gmail.com>
* @since 6.0.0
*
* @phpstan-type AuthorityMap array{user: ?string, pass: ?string, host: ?string, port: ?int}
* @phpstan-type ComponentMap array{scheme: ?string, user: ?string, pass: ?string, host: ?string, port: ?int, path: string, query: ?string, fragment: ?string}
* @phpstan-type InputComponentMap array{scheme? : ?string, user? : ?string, pass? : ?string, host? : ?string, port? : ?int, path? : ?string, query? : ?string, fragment? : ?string}
*/
final class UriString
{
/**
* Default URI component values.
*
* @var ComponentMap
*/
private const URI_COMPONENTS = [
'scheme' => null, 'user' => null, 'pass' => null, 'host' => null,
'port' => null, 'path' => '', 'query' => null, 'fragment' => null,
];
/**
* Simple URI which do not need any parsing.
*
* @var array<string, array<string>>
*/
private const URI_SHORTCUTS = [
'' => ['path' => ''],
'#' => ['fragment' => ''],
'?' => ['query' => ''],
'?#' => ['query' => '', 'fragment' => ''],
'/' => ['path' => '/'],
'//' => ['host' => ''],
'///' => ['host' => '', 'path' => '/'],
];
/**
* Range of invalid characters in URI 3986 string.
*
* @var string
*/
private const REGEXP_VALID_URI_RFC3986_CHARS = '/^(?:[A-Za-z0-9\-._~:\/?#[\]@!$&\'()*+,;=%]|%[0-9A-Fa-f]{2})*$/';
/**
* Range of invalid characters in URI 3987 string.
*
* @var string
*/
private const REGEXP_INVALID_URI_RFC3987_CHARS = '/[\x00-\x1f\x7f\s]/';
/**
* RFC3986 regular expression URI splitter.
*
* @link https://tools.ietf.org/html/rfc3986#appendix-B
* @var string
*/
private const REGEXP_URI_PARTS = ',^
(?<scheme>(?<scontent>[^:/?\#]+):)? # URI scheme component
(?<authority>//(?<acontent>[^/?\#]*))? # URI authority part
(?<path>[^?\#]*) # URI path component
(?<query>\?(?<qcontent>[^\#]*))? # URI query component
(?<fragment>\#(?<fcontent>.*))? # URI fragment component
,x';
/**
* URI scheme regular expression.
*
* @link https://tools.ietf.org/html/rfc3986#section-3.1
* @var string
*/
private const REGEXP_URI_SCHEME = '/^([a-z][a-z\d+.-]*)?$/i';
/**
* Invalid path for URI without scheme and authority regular expression.
*
* @link https://tools.ietf.org/html/rfc3986#section-3.3
* @var string
*/
private const REGEXP_INVALID_PATH = ',^(([^/]*):)(.*)?/,';
/**
* Host and Port splitter regular expression.
*
* @var string
*/
private const REGEXP_HOST_PORT = ',^(?<host>\[.*\]|[^:]*)(:(?<port>.*))?$,';
/** @var array<string,int> */
private const DOT_SEGMENTS = ['.' => 1, '..' => 1];
/**
* Generate an IRI string representation (RFC3987) from its parsed representation
* returned by League\UriString::parse() or PHP's parse_url.
*
* If you supply your own array, you are responsible for providing
* valid components without their URI delimiters.
*
* @link https://tools.ietf.org/html/rfc3986#section-5.3
* @link https://tools.ietf.org/html/rfc3986#section-7.5
*/
public static function toIriString(BackedEnum|Stringable|string $uri): string
{
$components = self::parse($uri);
$port = null;
if (isset($components['port'])) {
$port = (int) $components['port'];
unset($components['port']);
}
if (null !== $components['host']) {
$components['host'] = IdnaConverter::toUnicode($components['host'])->domain();
}
$components['path'] = Encoder::decodePath($components['path']);
$components['user'] = Encoder::decodeNecessary($components['user']);
$components['pass'] = Encoder::decodeNecessary($components['pass']);
$components['query'] = Encoder::decodeQuery($components['query']);
$components['fragment'] = Encoder::decodeFragment($components['fragment']);
return self::build([
...array_map(fn (?string $value) => match (true) {
null === $value,
!str_contains($value, '%20') => $value,
default => str_replace('%20', ' ', $value),
}, $components),
...['port' => $port],
]);
}
/**
* Generate a URI string representation from its parsed representation
* returned by League\UriString::parse() or PHP's parse_url.
*
* If you supply your own array, you are responsible for providing
* valid components without their URI delimiters.
*
* @link https://tools.ietf.org/html/rfc3986#section-5.3
* @link https://tools.ietf.org/html/rfc3986#section-7.5
*
* @param InputComponentMap $components
*/
public static function build(array $components): string
{
return self::buildUri(
$components['scheme'] ?? null,
self::buildAuthority($components),
$components['path'] ?? null,
$components['query'] ?? null,
$components['fragment'] ?? null,
);
}
/**
* Generates a URI string representation based on RFC3986 algorithm.
*
* Valid URI component MUST be provided without their URI delimiters
* but properly encoded.
*
* @link https://tools.ietf.org/html/rfc3986#section-5.3
* @link https://tools.ietf.org/html/rfc3986#section-7.5§
*/
public static function buildUri(
?string $scheme = null,
?string $authority = null,
?string $path = null,
?string $query = null,
?string $fragment = null,
): string {
self::validateComponents($scheme, $authority, $path);
$uri = '';
if (null !== $scheme) {
$uri .= $scheme.':';
}
if (null !== $authority) {
$uri .= '//'.$authority;
}
$uri .= $path;
if (null !== $query) {
$uri .= '?'.$query;
}
if (null !== $fragment) {
$uri .= '#'.$fragment;
}
return $uri;
}
/**
* Generate a URI authority representation from its parsed representation.
*
* @param InputComponentMap $components
*/
public static function buildAuthority(array $components): ?string
{
if (!isset($components['host'])) {
(!isset($components['user']) && !isset($components['pass'])) || throw new SyntaxError('The user info component must not be set if the host is not defined.');
!isset($components['port']) || throw new SyntaxError('The port component must not be set if the host is not defined.');
return null;
}
$userInfo = $components['user'] ?? null;
if (isset($components['pass'])) {
$userInfo .= ':'.$components['pass'];
}
$authority = '';
if (isset($userInfo)) {
$authority .= $userInfo.'@';
}
$authority .= $components['host'];
if (isset($components['port'])) {
$authority .= ':'.$components['port'];
}
return $authority;
}
/**
* Parses and normalizes the URI following RFC3986 destructive and non-destructive constraints.
*
* @throws SyntaxError if the URI is not parsable
*
* @return ComponentMap
*/
public static function parseNormalized(Stringable|string $uri): array
{
$components = self::parse($uri);
if (null !== $components['scheme']) {
$components['scheme'] = strtolower($components['scheme']);
}
$components['host'] = self::normalizeHost($components['host']);
$path = $components['path'];
$authority = self::buildAuthority($components);
//dot segment only happens when:
// - the path is absolute
// - the scheme and/or the authority are defined
if ('/' === ($path[0] ?? '') || '' !== $components['scheme'].$authority) {
$path = self::removeDotSegments($path);
}
// if there is an authority, the path must be absolute
if ('' !== $path && '/' !== $path[0]) {
if (null !== $authority) {
$path = '/'.$path;
}
}
$components['path'] = (string) Encoder::normalizePath($path);
$components['query'] = Encoder::normalizeQuery($components['query']);
$components['fragment'] = Encoder::normalizeFragment($components['fragment']);
$components['user'] = Encoder::normalizeUser($components['user']);
$components['pass'] = Encoder::normalizePassword($components['pass']);
return $components;
}
/**
* Parses and normalizes the URI following RFC3986 destructive and non-destructive constraints.
*
* @throws SyntaxError if the URI is not parsable
*/
public static function normalize(Stringable|string $uri): string
{
return self::build(self::parseNormalized($uri));
}
/**
* Parses and normalizes the URI following RFC3986 destructive and non-destructive constraints.
*
* @throws SyntaxError if the URI is not parsable
*/
public static function normalizeAuthority(Stringable|string|null $authority): ?string
{
if (null === $authority) {
return null;
}
$components = self::parseAuthority($authority);
$components['host'] = self::normalizeHost($components['host'] ?? null);
$components['user'] = Encoder::normalizeUser($components['user']);
$components['pass'] = Encoder::normalizePassword($components['pass']);
return (string) self::buildAuthority($components);
}
/**
* Resolves a URI against a base URI using RFC3986 rules.
*
* This method MUST retain the state of the submitted URI instance, and return
* a URI instance of the same type that contains the applied modifications.
*
* This method MUST be transparent when dealing with error and exceptions.
* It MUST not alter or silence them apart from validating its own parameters.
*
* @see https://www.rfc-editor.org/rfc/rfc3986.html#section-5
*
* @throws SyntaxError if the BaseUri is not absolute or in absence of a BaseUri if the uri is not absolute
*/
public static function resolve(BackedEnum|Stringable|string $uri, BackedEnum|Stringable|string|null $baseUri = null): string
{
if ($uri instanceof BackedEnum) {
$uri = (string) $uri->value;
}
if ($baseUri instanceof BackedEnum) {
$baseUri = (string) $baseUri->value;
}
$uri = (string) $uri;
if ('' === $uri) {
$uri = $baseUri ?? throw new SyntaxError("The uri can not be the empty string when there's no base URI.");
}
$uriComponents = self::parse($uri);
$baseUriComponents = $uriComponents;
if (null !== $baseUri && $uri !== (string) $baseUri) {
$baseUriComponents = self::parse($baseUri);
}
null !== $baseUriComponents['scheme'] || throw new SyntaxError('The base URI must be an absolute URI or null; If the base URI is null the URI must be an absolute URI.');
$authority = self::buildAuthority($uriComponents);
$path = self::removeDotSegments($uriComponents['path']);
if ('' !== $path && '/' !== $path[0] && (null !== $authority || $uriComponents['path'] !== $path)) {
$path = '/'.$path;
}
if (null !== $uriComponents['scheme'] && '' !== $uriComponents['scheme']) {
return self::buildUri($uriComponents['scheme'], $authority, $path, $uriComponents['query'], $uriComponents['fragment']);
}
if (null !== $authority) {
return self::buildUri($baseUriComponents['scheme'], $authority, $path, $uriComponents['query'], $uriComponents['fragment']);
}
[$resolvedPath, $query] = self::resolvePathAndQuery($uriComponents, $baseUriComponents);
$baseAuthority = self::buildAuthority($baseUriComponents);
$path = self::removeDotSegments($resolvedPath);
if ('' !== $path && '/' !== $path[0] && (null !== $baseAuthority || $resolvedPath !== $path)) {
$path = '/'.$path;
}
return self::buildUri($baseUriComponents['scheme'], $baseAuthority, $path, $query, $uriComponents['fragment']);
}
/**
* Filter Dot segment according to RFC3986.
*
* @see http://tools.ietf.org/html/rfc3986#section-5.2.4
*/
public static function removeDotSegments(Stringable|string $path): string
{
$path = (string) $path;
if (!str_contains($path, '.')) {
return $path;
}
$reducer = function (array $carry, string $segment): array {
if ('..' === $segment) {
array_pop($carry);
return $carry;
}
if (!isset(self::DOT_SEGMENTS[$segment])) {
$carry[] = $segment;
}
return $carry;
};
$oldSegments = explode('/', $path);
$newPath = implode('/', array_reduce($oldSegments, $reducer(...), []));
if (isset(self::DOT_SEGMENTS[$oldSegments[array_key_last($oldSegments)]])) {
$newPath .= '/';
}
return $newPath;
}
/**
* Resolves an URI path and query component.
*
* @param ComponentMap $uri
* @param ComponentMap $baseUri
*
* @return array{0:string, 1:string|null}
*/
private static function resolvePathAndQuery(array $uri, array $baseUri): array
{
if (str_starts_with($uri['path'], '/')) {
return [$uri['path'], $uri['query']];
}
if ('' === $uri['path']) {
return [$baseUri['path'], $uri['query'] ?? $baseUri['query']];
}
$targetPath = $uri['path'];
if (null !== self::buildAuthority($baseUri) && '' === $baseUri['path']) {
$targetPath = '/'.$targetPath;
}
if ('' !== $baseUri['path']) {
$segments = explode('/', $baseUri['path']);
array_pop($segments);
if ([] !== $segments) {
$targetPath = implode('/', $segments).'/'.$targetPath;
}
}
return [$targetPath, $uri['query']];
}
public static function containsRfc3986Chars(Stringable|string $uri): bool
{
return 1 === preg_match(self::REGEXP_VALID_URI_RFC3986_CHARS, (string) $uri);
}
public static function containsRfc3987Chars(Stringable|string $uri): bool
{
return 1 !== preg_match(self::REGEXP_INVALID_URI_RFC3987_CHARS, (string) $uri);
}
/**
* Parse a URI string into its components.
*
* This method parses a URI and returns an associative array containing any
* of the various components of the URI that are present.
*
* <code>
* $components = UriString::parse('http://foo@test.example.com:42?query#');
* var_export($components);
* //will display
* array(
* 'scheme' => 'http', // the URI scheme component
* 'user' => 'foo', // the URI user component
* 'pass' => null, // the URI pass component
* 'host' => 'test.example.com', // the URI host component
* 'port' => 42, // the URI port component
* 'path' => '', // the URI path component
* 'query' => 'query', // the URI query component
* 'fragment' => '', // the URI fragment component
* );
* </code>
*
* The returned array is similar to PHP's parse_url return value with the following
* differences:
*
* <ul>
* <li>All components are always present in the returned array</li>
* <li>Empty and undefined component are treated differently. And empty component is
* set to the empty string while an undefined component is set to the `null` value.</li>
* <li>The path component is never undefined</li>
* <li>The method parses the URI following the RFC3986 rules, but you are still
* required to validate the returned components against its related scheme specific rules.</li>
* </ul>
*
* @link https://tools.ietf.org/html/rfc3986
*
* @throws SyntaxError if the URI contains invalid characters
* @throws SyntaxError if the URI contains an invalid scheme
* @throws SyntaxError if the URI contains an invalid path
*
* @return ComponentMap
*/
public static function parse(BackedEnum|Stringable|string|int $uri): array
{
if ($uri instanceof BackedEnum) {
$uri = $uri->value;
}
$uri = (string) $uri;
if (isset(self::URI_SHORTCUTS[$uri])) {
/** @var ComponentMap $components */
$components = [...self::URI_COMPONENTS, ...self::URI_SHORTCUTS[$uri]];
return $components;
}
self::containsRfc3987Chars($uri) || throw new SyntaxError(sprintf('The uri `%s` contains invalid characters', $uri));
//if the first character is a known URI delimiter, parsing can be simplified
$first_char = $uri[0];
//The URI is made of the fragment only
if ('#' === $first_char) {
[, $fragment] = explode('#', $uri, 2);
$components = self::URI_COMPONENTS;
$components['fragment'] = $fragment;
return $components;
}
//The URI is made of the query and fragment
if ('?' === $first_char) {
[, $partial] = explode('?', $uri, 2);
[$query, $fragment] = explode('#', $partial, 2) + [1 => null];
$components = self::URI_COMPONENTS;
$components['query'] = $query;
$components['fragment'] = $fragment;
return $components;
}
//use RFC3986 URI regexp to split the URI
preg_match(self::REGEXP_URI_PARTS, $uri, $parts);
$parts += ['query' => '', 'fragment' => ''];
if (':' === ($parts['scheme'] ?? null) || 1 !== preg_match(self::REGEXP_URI_SCHEME, $parts['scontent'] ?? '')) {
throw new SyntaxError(sprintf('The uri `%s` contains an invalid scheme', $uri));
}
if ('' === ($parts['scheme'] ?? '').($parts['authority'] ?? '') && 1 === preg_match(self::REGEXP_INVALID_PATH, $parts['path'] ?? '')) {
throw new SyntaxError(sprintf('The uri `%s` contains an invalid path.', $uri));
}
/** @var ComponentMap $components */
$components = array_merge(
self::URI_COMPONENTS,
'' === ($parts['authority'] ?? null) ? [] : self::parseAuthority($parts['acontent'] ?? null),
[
'path' => $parts['path'] ?? '',
'scheme' => '' === ($parts['scheme'] ?? null) ? null : ($parts['scontent'] ?? null),
'query' => '' === $parts['query'] ? null : ($parts['qcontent'] ?? null),
'fragment' => '' === $parts['fragment'] ? null : ($parts['fcontent'] ?? null),
]
);
return $components;
}
/**
* Assert the URI internal state is valid.
*
* @link https://tools.ietf.org/html/rfc3986#section-3
* @link https://tools.ietf.org/html/rfc3986#section-3.3
*
* @throws SyntaxError
*/
private static function validateComponents(?string $scheme, ?string $authority, ?string $path): void
{
if (null !== $authority) {
if (null !== $path && '' !== $path && '/' !== $path[0]) {
throw new SyntaxError('If an authority is present the path must be empty or start with a `/`.');
}
return;
}
if (null === $path || '' === $path) {
return;
}
if (str_starts_with($path, '//')) {
throw new SyntaxError('If there is no authority the path `'.$path.'` cannot start with a `//`.');
}
if (null !== $scheme || false === ($pos = strpos($path, ':'))) {
return;
}
if (!str_contains(substr($path, 0, $pos), '/')) {
throw new SyntaxError('In absence of a scheme and an authority the first path segment cannot contain a colon (":") character.');
}
}
/**
* Parses the URI authority part.
*
* @link https://tools.ietf.org/html/rfc3986#section-3.2
*
* @throws SyntaxError If the port component is invalid
*
* @return AuthorityMap
*/
public static function parseAuthority(BackedEnum|Stringable|string|null $authority): array
{
$components = ['user' => null, 'pass' => null, 'host' => null, 'port' => null];
if (null === $authority) {
return $components;
}
if ($authority instanceof BackedEnum) {
$authority = $authority->value;
}
$authority = (string) $authority;
$components['host'] = '';
if ('' === $authority) {
return $components;
}
$parts = explode('@', $authority, 2);
if (isset($parts[1])) {
[$components['user'], $components['pass']] = explode(':', $parts[0], 2) + [1 => null];
}
preg_match(self::REGEXP_HOST_PORT, $parts[1] ?? $parts[0], $matches);
$matches += ['port' => ''];
$components['port'] = self::filterPort($matches['port']);
$components['host'] = self::filterHost($matches['host'] ?? '');
return $components;
}
/**
* Filter and format the port component.
*
* @link https://tools.ietf.org/html/rfc3986#section-3.2.2
*
* @throws SyntaxError if the registered name is invalid
*/
private static function filterPort(string $port): ?int
{
return match (true) {
'' === $port => null,
1 === preg_match('/^\d*$/', $port) => (int) $port,
default => throw new SyntaxError(sprintf('The port `%s` is invalid', $port)),
};
}
/**
* Returns whether a hostname is valid.
*
* @link https://tools.ietf.org/html/rfc3986#section-3.2.2
*
* @throws SyntaxError if the registered name is invalid
*/
private static function filterHost(Stringable|string|null $host): ?string
{
try {
return HostRecord::from($host)->value;
} catch (Throwable) {
throw new SyntaxError(sprintf('Host `%s` is invalid : the IP host is malformed', $host));
}
}
/**
* Tells whether the scheme component is valid.
*/
public static function isValidScheme(BackedEnum|Stringable|string|null $scheme): bool
{
if ($scheme instanceof BackedEnum) {
$scheme = $scheme->value;
}
return null === $scheme || 1 === preg_match('/^[A-Za-z]([-A-Za-z\d+.]+)?$/', (string) $scheme);
}
private static function normalizeHost(BackedEnum|Stringable|string|null $host): ?string
{
if ($host instanceof BackedEnum) {
$host = $host->value;
}
if (null !== $host) {
$host = (string) $host;
}
if (null === $host || false !== filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return $host;
}
$host = (string) Encoder::normalizeHost($host);
static $isSupported = null;
$isSupported ??= (function_exists('\idn_to_ascii') && defined('\INTL_IDNA_VARIANT_UTS46'));
if (! $isSupported) {
return $host;
}
$idnaHost = IdnaConverter::toAscii($host);
if (!$idnaHost->hasErrors()) {
return $idnaHost->domain();
}
return $host;
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.6.0
* @codeCoverageIgnore
* @see HostRecoord::validate()
*
* Create a new instance from the environment.
*/
#[Deprecated(message:'use League\Uri\HostRecord::validate() instead', since:'league/uri:7.6.0')]
public static function isValidHost(Stringable|string|null $host): bool
{
return HostRecord::isValid($host);
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
enum UrnComparisonMode
{
case IncludeComponents;
case ExcludeComponents;
}
+70
View File
@@ -0,0 +1,70 @@
{
"name": "league/uri-interfaces",
"type": "library",
"description" : "Common tools for parsing and resolving RFC3987/RFC3986 URI",
"keywords": [
"url",
"uri",
"rfc3986",
"rfc3987",
"rfc6570",
"psr-7",
"parse_url",
"http",
"https",
"ws",
"ftp",
"data-uri",
"file-uri",
"parse_str",
"query-string",
"querystring",
"hostname"
],
"license": "MIT",
"homepage": "https://uri.thephpleague.com",
"authors": [
{
"name" : "Ignace Nyamagana Butera",
"email" : "nyamsprod@gmail.com",
"homepage" : "https://nyamsprod.com"
}
],
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/nyamsprod"
}
],
"require": {
"php" : "^8.1",
"ext-filter": "*",
"psr/http-message": "^1.1 || ^2.0"
},
"autoload": {
"psr-4": {
"League\\Uri\\": ""
}
},
"suggest": {
"ext-bcmath": "to improve IPV4 host parsing",
"ext-gmp": "to improve IPV4 host parsing",
"ext-intl": "to handle IDN host with the best performance",
"php-64bit": "to improve IPV4 host parsing",
"symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present",
"rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification"
},
"extra": {
"branch-alias": {
"dev-master": "7.x-dev"
}
},
"support": {
"forum": "https://thephpleague.slack.com",
"docs": "https://uri.thephpleague.com",
"issues": "https://github.com/thephpleague/uri-src/issues"
},
"config": {
"sort-packages": true
}
}
+646
View File
@@ -0,0 +1,646 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
use Deprecated;
use JsonSerializable;
use League\Uri\Contracts\UriAccess;
use League\Uri\Contracts\UriInterface;
use League\Uri\Exceptions\MissingFeature;
use League\Uri\Idna\Converter as IdnaConverter;
use League\Uri\IPv4\Converter as IPv4Converter;
use League\Uri\IPv6\Converter as IPv6Converter;
use Psr\Http\Message\UriFactoryInterface;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use function array_pop;
use function array_reduce;
use function count;
use function explode;
use function implode;
use function in_array;
use function preg_match;
use function rawurldecode;
use function sort;
use function str_contains;
use function str_repeat;
use function str_replace;
use function strpos;
use function substr;
/**
* @phpstan-import-type ComponentMap from UriInterface
* @deprecated since version 7.6.0
*
* @see Modifier
* @see Uri
*/
class BaseUri implements Stringable, JsonSerializable, UriAccess
{
/** @var array<string,int> */
final protected const WHATWG_SPECIAL_SCHEMES = ['ftp' => 1, 'http' => 1, 'https' => 1, 'ws' => 1, 'wss' => 1];
/** @var array<string,int> */
final protected const DOT_SEGMENTS = ['.' => 1, '..' => 1];
protected readonly Psr7UriInterface|UriInterface|null $origin;
protected readonly ?string $nullValue;
/**
* @param UriFactoryInterface|null $uriFactory Deprecated, will be removed in the next major release
*/
final protected function __construct(
protected readonly Psr7UriInterface|UriInterface $uri,
protected readonly ?UriFactoryInterface $uriFactory
) {
$this->nullValue = $this->uri instanceof Psr7UriInterface ? '' : null;
$this->origin = $this->computeOrigin($this->uri, $this->nullValue);
}
public static function from(Stringable|string $uri, ?UriFactoryInterface $uriFactory = null): static
{
$uri = static::formatHost(static::filterUri($uri, $uriFactory));
return new static($uri, $uriFactory);
}
public function withUriFactory(UriFactoryInterface $uriFactory): static
{
return new static($this->uri, $uriFactory);
}
public function withoutUriFactory(): static
{
return new static($this->uri, null);
}
public function getUri(): Psr7UriInterface|UriInterface
{
return $this->uri;
}
public function getUriString(): string
{
return $this->uri->__toString();
}
public function jsonSerialize(): string
{
return $this->uri->__toString();
}
public function __toString(): string
{
return $this->uri->__toString();
}
public function origin(): ?self
{
return match (null) {
$this->origin => null,
default => new self($this->origin, $this->uriFactory),
};
}
/**
* Returns the Unix filesystem path.
*
* The method will return null if a scheme is present and is not the `file` scheme
*/
public function unixPath(): ?string
{
return match ($this->uri->getScheme()) {
'file', $this->nullValue => rawurldecode($this->uri->getPath()),
default => null,
};
}
/**
* Returns the Windows filesystem path.
*
* The method will return null if a scheme is present and is not the `file` scheme
*/
public function windowsPath(): ?string
{
static $regexpWindowsPath = ',^(?<root>[a-zA-Z]:),';
if (!in_array($this->uri->getScheme(), ['file', $this->nullValue], true)) {
return null;
}
$originalPath = $this->uri->getPath();
$path = $originalPath;
if ('/' === ($path[0] ?? '')) {
$path = substr($path, 1);
}
if (1 === preg_match($regexpWindowsPath, $path, $matches)) {
$root = $matches['root'];
$path = substr($path, strlen($root));
return $root.str_replace('/', '\\', rawurldecode($path));
}
$host = $this->uri->getHost();
return match ($this->nullValue) {
$host => str_replace('/', '\\', rawurldecode($originalPath)),
default => '\\\\'.$host.'\\'.str_replace('/', '\\', rawurldecode($path)),
};
}
/**
* Returns a string representation of a File URI according to RFC8089.
*
* The method will return null if the URI scheme is not the `file` scheme
*/
public function toRfc8089(): ?string
{
$path = $this->uri->getPath();
return match (true) {
'file' !== $this->uri->getScheme() => null,
in_array($this->uri->getAuthority(), ['', null, 'localhost'], true) => 'file:'.match (true) {
'' === $path,
'/' === $path[0] => $path,
default => '/'.$path,
},
default => (string) $this->uri,
};
}
/**
* Tells whether the `file` scheme base URI represents a local file.
*/
public function isLocalFile(): bool
{
return match (true) {
'file' !== $this->uri->getScheme() => false,
in_array($this->uri->getAuthority(), ['', null, 'localhost'], true) => true,
default => false,
};
}
/**
* Tells whether the URI is opaque or not.
*
* A URI is opaque if and only if it is absolute
* and does not have an authority path.
*/
public function isOpaque(): bool
{
return $this->nullValue === $this->uri->getAuthority()
&& $this->isAbsolute();
}
/**
* Tells whether two URI do not share the same origin.
*/
public function isCrossOrigin(Stringable|string $uri): bool
{
if (null === $this->origin) {
return true;
}
$uri = static::filterUri($uri);
$uriOrigin = $this->computeOrigin($uri, $uri instanceof Psr7UriInterface ? '' : null);
return match(true) {
null === $uriOrigin,
$uriOrigin->__toString() !== $this->origin->__toString() => true,
default => false,
};
}
/**
* Tells whether the URI is absolute.
*/
public function isAbsolute(): bool
{
return $this->nullValue !== $this->uri->getScheme();
}
/**
* Tells whether the URI is a network path.
*/
public function isNetworkPath(): bool
{
return $this->nullValue === $this->uri->getScheme()
&& $this->nullValue !== $this->uri->getAuthority();
}
/**
* Tells whether the URI is an absolute path.
*/
public function isAbsolutePath(): bool
{
return $this->nullValue === $this->uri->getScheme()
&& $this->nullValue === $this->uri->getAuthority()
&& '/' === ($this->uri->getPath()[0] ?? '');
}
/**
* Tells whether the URI is a relative path.
*/
public function isRelativePath(): bool
{
return $this->nullValue === $this->uri->getScheme()
&& $this->nullValue === $this->uri->getAuthority()
&& '/' !== ($this->uri->getPath()[0] ?? '');
}
/**
* Tells whether both URI refers to the same document.
*/
public function isSameDocument(Stringable|string $uri): bool
{
return self::normalizedUri($this->uri)->equals(self::normalizedUri($uri));
}
private static function normalizedUri(Stringable|string $uri): Uri
{
// Normalize the URI according to RFC3986
$uri = ($uri instanceof Uri ? $uri : Uri::new($uri))->normalize();
return $uri
//Normalization as per WHATWG URL standard
//only meaningful for WHATWG Special URI scheme protocol
->when(
condition: '' === $uri->getPath() && null !== $uri->getAuthority(),
onSuccess: fn (Uri $uri) => $uri->withPath('/'),
)
//Sorting as per WHATWG URLSearchParams class
//not included on any equivalence algorithm
->when(
condition: null !== ($query = $uri->getQuery()) && str_contains($query, '&'),
onSuccess: function (Uri $uri) use ($query) {
$pairs = explode('&', (string) $query);
sort($pairs);
return $uri->withQuery(implode('&', $pairs));
}
);
}
/**
* Tells whether the URI contains an Internationalized Domain Name (IDN).
*/
public function hasIdn(): bool
{
return IdnaConverter::isIdn($this->uri->getHost());
}
/**
* Tells whether the URI contains an IPv4 regardless if it is mapped or native.
*/
public function hasIPv4(): bool
{
return IPv4Converter::fromEnvironment()->isIpv4($this->uri->getHost());
}
/**
* Resolves a URI against a base URI using RFC3986 rules.
*
* This method MUST retain the state of the submitted URI instance, and return
* a URI instance of the same type that contains the applied modifications.
*
* This method MUST be transparent when dealing with error and exceptions.
* It MUST not alter or silence them apart from validating its own parameters.
*/
public function resolve(Stringable|string $uri): static
{
$resolved = UriString::resolve($uri, $this->uri);
return new static(match ($this->uriFactory) {
null => Uri::new($resolved),
default => $this->uriFactory->createUri($resolved),
}, $this->uriFactory);
}
/**
* Relativize a URI according to a base URI.
*
* This method MUST retain the state of the submitted URI instance, and return
* a URI instance of the same type that contains the applied modifications.
*
* This method MUST be transparent when dealing with error and exceptions.
* It MUST not alter of silence them apart from validating its own parameters.
*/
public function relativize(Stringable|string $uri): static
{
$uri = static::formatHost(static::filterUri($uri, $this->uriFactory));
if ($this->canNotBeRelativize($uri)) {
return new static($uri, $this->uriFactory);
}
$null = $uri instanceof Psr7UriInterface ? '' : null;
$uri = $uri->withScheme($null)->withPort(null)->withUserInfo($null)->withHost($null);
$targetPath = $uri->getPath();
$basePath = $this->uri->getPath();
return new static(
match (true) {
$targetPath !== $basePath => $uri->withPath(static::relativizePath($targetPath, $basePath)),
static::componentEquals('query', $uri) => $uri->withPath('')->withQuery($null),
$null === $uri->getQuery() => $uri->withPath(static::formatPathWithEmptyBaseQuery($targetPath)),
default => $uri->withPath(''),
},
$this->uriFactory
);
}
final protected function computeOrigin(Psr7UriInterface|UriInterface $uri, ?string $nullValue): Psr7UriInterface|UriInterface|null
{
if ($uri instanceof Uri) {
$origin = $uri->getOrigin();
if (null === $origin) {
return null;
}
return Uri::tryNew($origin);
}
$origin = Uri::tryNew($uri)?->getOrigin();
if (null === $origin) {
return null;
}
$components = UriString::parse($origin);
return $uri
->withFragment($nullValue)
->withQuery($nullValue)
->withPath('')
->withScheme('localhost')
->withHost((string) $components['host'])
->withPort($components['port'])
->withScheme((string) $components['scheme'])
->withUserInfo($nullValue);
}
/**
* Input URI normalization to allow Stringable and string URI.
*/
final protected static function filterUri(Stringable|string $uri, UriFactoryInterface|null $uriFactory = null): Psr7UriInterface|UriInterface
{
return match (true) {
$uri instanceof UriAccess => $uri->getUri(),
$uri instanceof Psr7UriInterface,
$uri instanceof UriInterface => $uri,
$uriFactory instanceof UriFactoryInterface => $uriFactory->createUri((string) $uri),
default => Uri::new($uri),
};
}
/**
* Tells whether the component value from both URI object equals.
*
* @pqram 'query'|'authority'|'scheme' $property
*/
final protected function componentEquals(string $property, Psr7UriInterface|UriInterface $uri): bool
{
$getComponent = function (string $property, Psr7UriInterface|UriInterface $uri): ?string {
$component = match ($property) {
'query' => $uri->getQuery(),
'authority' => $uri->getAuthority(),
default => $uri->getScheme(),
};
return match (true) {
$uri instanceof UriInterface, '' !== $component => $component,
default => null,
};
};
return $getComponent($property, $uri) === $getComponent($property, $this->uri);
}
/**
* Filter the URI object.
*/
final protected static function formatHost(Psr7UriInterface|UriInterface $uri): Psr7UriInterface|UriInterface
{
$host = $uri->getHost();
try {
$converted = IPv4Converter::fromEnvironment()->toDecimal($host);
} catch (MissingFeature) {
$converted = null;
}
if (false === filter_var($converted, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
$converted = IPv6Converter::compress($host);
}
return match (true) {
null !== $converted => $uri->withHost($converted),
'' === $host,
$uri instanceof UriInterface => $uri,
default => $uri->withHost((string) Uri::fromComponents(['host' => $host])->getHost()),
};
}
/**
* Tells whether the submitted URI object can be relativized.
*/
final protected function canNotBeRelativize(Psr7UriInterface|UriInterface $uri): bool
{
return !static::componentEquals('scheme', $uri)
|| !static::componentEquals('authority', $uri)
|| static::from($uri)->isRelativePath();
}
/**
* Relatives the URI for an authority-less target URI.
*/
final protected static function relativizePath(string $path, string $basePath): string
{
$baseSegments = static::getSegments($basePath);
$targetSegments = static::getSegments($path);
$targetBasename = array_pop($targetSegments);
array_pop($baseSegments);
foreach ($baseSegments as $offset => $segment) {
if (!isset($targetSegments[$offset]) || $segment !== $targetSegments[$offset]) {
break;
}
unset($baseSegments[$offset], $targetSegments[$offset]);
}
$targetSegments[] = $targetBasename;
return static::formatPath(
str_repeat('../', count($baseSegments)).implode('/', $targetSegments),
$basePath
);
}
/**
* returns the path segments.
*
* @return string[]
*/
final protected static function getSegments(string $path): array
{
return explode('/', match (true) {
'' === $path,
'/' !== $path[0] => $path,
default => substr($path, 1),
});
}
/**
* Formatting the path to keep a valid URI.
*/
final protected static function formatPath(string $path, string $basePath): string
{
$colonPosition = strpos($path, ':');
$slashPosition = strpos($path, '/');
return match (true) {
'' === $path => match (true) {
'' === $basePath,
'/' === $basePath => $basePath,
default => './',
},
false === $colonPosition => $path,
false === $slashPosition,
$colonPosition < $slashPosition => "./$path",
default => $path,
};
}
/**
* Formatting the path to keep a resolvable URI.
*/
final protected static function formatPathWithEmptyBaseQuery(string $path): string
{
$targetSegments = static::getSegments($path);
$basename = $targetSegments[array_key_last($targetSegments)];
return '' === $basename ? './' : $basename;
}
/**
* Normalizes a URI for comparison; this URI string representation is not suitable for usage as per RFC guidelines.
*
* @deprecated since version 7.6.0
*
* @codeCoverageIgnore
*/
#[Deprecated(message:'no longer used by the isSameDocument method', since:'league/uri-interfaces:7.6.0')]
final protected function normalize(Psr7UriInterface|UriInterface $uri): string
{
$newUri = $uri->withScheme($uri instanceof Psr7UriInterface ? '' : null);
if ('' === $newUri->__toString()) {
return '';
}
return UriString::normalize($newUri);
}
/**
* Remove dot segments from the URI path as per RFC specification.
*
* @deprecated since version 7.6.0
*
* @codeCoverageIgnore
*/
#[Deprecated(message:'no longer used by the isSameDocument method', since:'league/uri-interfaces:7.6.0')]
final protected function removeDotSegments(string $path): string
{
if (!str_contains($path, '.')) {
return $path;
}
$reducer = function (array $carry, string $segment): array {
if ('..' === $segment) {
array_pop($carry);
return $carry;
}
if (!isset(static::DOT_SEGMENTS[$segment])) {
$carry[] = $segment;
}
return $carry;
};
$oldSegments = explode('/', $path);
$newPath = implode('/', array_reduce($oldSegments, $reducer(...), []));
if (isset(static::DOT_SEGMENTS[$oldSegments[array_key_last($oldSegments)]])) {
$newPath .= '/';
}
// @codeCoverageIgnoreStart
// added because some PSR-7 implementations do not respect RFC3986
if (str_starts_with($path, '/') && !str_starts_with($newPath, '/')) {
return '/'.$newPath;
}
// @codeCoverageIgnoreEnd
return $newPath;
}
/**
* Resolves an URI path and query component.
*
* @return array{0:string, 1:string|null}
*
* @deprecated since version 7.6.0
*
* @codeCoverageIgnore
*/
#[Deprecated(message:'no longer used by the isSameDocument method', since:'league/uri-interfaces:7.6.0')]
final protected function resolvePathAndQuery(Psr7UriInterface|UriInterface $uri): array
{
$targetPath = $uri->getPath();
$null = $uri instanceof Psr7UriInterface ? '' : null;
if (str_starts_with($targetPath, '/')) {
return [$targetPath, $uri->getQuery()];
}
if ('' === $targetPath) {
$targetQuery = $uri->getQuery();
if ($null === $targetQuery) {
$targetQuery = $this->uri->getQuery();
}
$targetPath = $this->uri->getPath();
//@codeCoverageIgnoreStart
//because some PSR-7 Uri implementations allow this RFC3986 forbidden construction
if (null !== $this->uri->getAuthority() && !str_starts_with($targetPath, '/')) {
$targetPath = '/'.$targetPath;
}
//@codeCoverageIgnoreEnd
return [$targetPath, $targetQuery];
}
$basePath = $this->uri->getPath();
if (null !== $this->uri->getAuthority() && '' === $basePath) {
$targetPath = '/'.$targetPath;
}
if ('' !== $basePath) {
$segments = explode('/', $basePath);
array_pop($segments);
if ([] !== $segments) {
$targetPath = implode('/', $segments).'/'.$targetPath;
}
}
return [$targetPath, $uri->getQuery()];
}
}
+358
View File
@@ -0,0 +1,358 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
use BackedEnum;
use League\Uri\Contracts\Conditionable;
use League\Uri\Contracts\FragmentDirective;
use League\Uri\Contracts\Transformable;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Exceptions\SyntaxError;
use SensitiveParameter;
use Stringable;
use Throwable;
use TypeError;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function is_bool;
use function str_replace;
use function strpos;
final class Builder implements Conditionable, Transformable
{
private ?string $scheme = null;
private ?string $username = null;
private ?string $password = null;
private ?string $host = null;
private ?int $port = null;
private ?string $path = null;
private ?string $query = null;
private ?string $fragment = null;
public function __construct(
BackedEnum|Stringable|string|null $scheme = null,
BackedEnum|Stringable|string|null $username = null,
#[SensitiveParameter] BackedEnum|Stringable|string|null $password = null,
BackedEnum|Stringable|string|null $host = null,
BackedEnum|int|null $port = null,
BackedEnum|Stringable|string|null $path = null,
BackedEnum|Stringable|string|null $query = null,
BackedEnum|Stringable|string|null $fragment = null,
) {
$this
->scheme($scheme)
->userInfo($username, $password)
->host($host)
->port($port)
->path($path)
->query($query)
->fragment($fragment);
}
/**
* @throws SyntaxError
*/
public function scheme(BackedEnum|Stringable|string|null $scheme): self
{
$scheme = $this->filterString($scheme);
if ($scheme !== $this->scheme) {
UriString::isValidScheme($scheme) || throw new SyntaxError('The scheme `'.$scheme.'` is invalid.');
$this->scheme = $scheme;
}
return $this;
}
/**
* @throws SyntaxError
*/
public function userInfo(
BackedEnum|Stringable|string|null $user,
#[SensitiveParameter] BackedEnum|Stringable|string|null $password = null
): static {
$username = Encoder::encodeUser($this->filterString($user));
$password = Encoder::encodePassword($this->filterString($password));
if ($username !== $this->username || $password !== $this->password) {
$this->username = $username;
$this->password = $password;
}
return $this;
}
/**
* @throws SyntaxError
*/
public function host(BackedEnum|Stringable|string|null $host): self
{
$host = $this->filterString($host);
if ($host !== $this->host) {
null === $host
|| HostRecord::isValid($host)
|| throw new SyntaxError('The host `'.$host.'` is invalid.');
$this->host = $host;
}
return $this;
}
/**
* @throws SyntaxError
* @throws TypeError
*/
public function port(BackedEnum|int|null $port): self
{
if ($port instanceof BackedEnum) {
1 === preg_match('/^\d+$/', (string) $port->value)
|| throw new TypeError('The port must be a valid BackedEnum containing a number.');
$port = (int) $port->value;
}
if ($port !== $this->port) {
null === $port
|| ($port >= 0 && $port < 65535)
|| throw new SyntaxError('The port value must be null or an integer between 0 and 65535.');
$this->port = $port;
}
return $this;
}
/**
* @throws SyntaxError
*/
public function authority(BackedEnum|Stringable|string|null $authority): self
{
['user' => $user, 'pass' => $pass, 'host' => $host, 'port' => $port] = UriString::parseAuthority($authority);
return $this
->userInfo($user, $pass)
->host($host)
->port($port);
}
/**
* @throws SyntaxError
*/
public function path(BackedEnum|Stringable|string|null $path): self
{
$path = $this->filterString($path);
if ($path !== $this->path) {
$this->path = null !== $path ? Encoder::encodePath($path) : null;
}
return $this;
}
/**
* @throws SyntaxError
*/
public function query(BackedEnum|Stringable|string|null $query): self
{
$query = $this->filterString($query);
if ($query !== $this->query) {
$this->query = Encoder::encodeQueryOrFragment($query);
}
return $this;
}
/**
* @throws SyntaxError
*/
public function fragment(BackedEnum|Stringable|string|null $fragment): self
{
$fragment = $this->filterString($fragment);
if ($fragment !== $this->fragment) {
$this->fragment = Encoder::encodeQueryOrFragment($fragment);
}
return $this;
}
/**
* Puts back the Builder in a freshly created state.
*/
public function reset(): self
{
$this->scheme = null;
$this->username = null;
$this->password = null;
$this->host = null;
$this->port = null;
$this->path = null;
$this->query = null;
$this->fragment = null;
return $this;
}
/**
* Executes the given callback with the current instance
* and returns the current instance.
*
* @param callable(self): self $callback
*/
public function transform(callable $callback): static
{
return $callback($this);
}
public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): static
{
if (!is_bool($condition)) {
$condition = $condition($this);
}
return match (true) {
$condition => $onSuccess($this),
null !== $onFail => $onFail($this),
default => $this,
} ?? $this;
}
/**
* @throws SyntaxError if the URI can not be build with the current Builder state
*/
public function guard(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUri = null): self
{
try {
$this->build($baseUri);
return $this;
} catch (Throwable $exception) {
throw new SyntaxError('The current builder cannot generate a valid URI.', previous: $exception);
}
}
/**
* Tells whether the URI can be built with the current Builder state.
*/
public function validate(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUri = null): bool
{
try {
$this->build($baseUri);
return true;
} catch (Throwable) {
return false;
}
}
public function build(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUri = null): Uri
{
$authority = $this->buildAuthority();
$path = $this->buildPath($authority);
$uriString = UriString::buildUri(
$this->scheme,
$authority,
$path,
Encoder::encodeQueryOrFragment($this->query),
Encoder::encodeQueryOrFragment($this->fragment)
);
return Uri::new(null === $baseUri ? $uriString : UriString::resolve($uriString, match (true) {
$baseUri instanceof Rfc3986Uri => $baseUri->toString(),
$baseUri instanceof WhatWgUrl => $baseUri->toAsciiString(),
default => $baseUri,
}));
}
/**
* @throws SyntaxError
*/
private function buildAuthority(): ?string
{
if (null === $this->host) {
(null === $this->username && null === $this->password && null === $this->port)
|| throw new SyntaxError('The User Information and/or the Port component(s) are set without a Host component being present.');
return null;
}
$authority = $this->host;
if (null !== $this->username || null !== $this->password) {
$userInfo = Encoder::encodeUser($this->username);
if (null !== $this->password) {
$userInfo .= ':'.Encoder::encodePassword($this->password);
}
$authority = $userInfo.'@'.$authority;
}
if (null !== $this->port) {
return $authority.':'.$this->port;
}
return $authority;
}
/**
* @throws SyntaxError
*/
private function buildPath(?string $authority): ?string
{
if (null === $this->path || '' === $this->path) {
return $this->path;
}
$path = Encoder::encodePath($this->path);
if (null !== $authority) {
return str_starts_with($path, '/') ? $path : '/'.$path;
}
if (str_starts_with($path, '//')) {
return '/.'.$path;
}
$colonPos = strpos($path, ':');
if (false !== $colonPos && null === $this->scheme) {
$slashPos = strpos($path, '/');
(false !== $slashPos && $colonPos > $slashPos) || throw new SyntaxError('In absence of the scheme and authority components, the first path segment cannot contain a colon (":") character.');
}
return $path;
}
/**
* Filter a string.
*
* @throws SyntaxError if the submitted data cannot be converted to string
*/
private function filterString(BackedEnum|Stringable|string|null $str): ?string
{
$str = match (true) {
$str instanceof FragmentDirective => $str->toFragmentValue(),
$str instanceof UriComponentInterface => $str->value(),
$str instanceof BackedEnum => (string) $str->value,
null === $str => null,
default => (string) $str,
};
if (null === $str) {
return null;
}
$str = str_replace(' ', '%20', $str);
return UriString::containsRfc3987Chars($str)
? $str
: throw new SyntaxError('The component value `'.$str.'` contains invalid characters.');
}
}
+386
View File
@@ -0,0 +1,386 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
use Deprecated;
use JsonSerializable;
use League\Uri\Contracts\Conditionable;
use League\Uri\Contracts\Transformable;
use League\Uri\Contracts\UriException;
use League\Uri\Contracts\UriInterface;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\UriTemplate\TemplateCanNotBeExpanded;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function is_bool;
use function ltrim;
/**
* @phpstan-import-type InputComponentMap from UriString
*/
final class Http implements Stringable, Psr7UriInterface, JsonSerializable, Conditionable, Transformable
{
private readonly UriInterface $uri;
private function __construct(UriInterface $uri)
{
if (null === $uri->getScheme() && '' === $uri->getHost()) {
throw new SyntaxError('An URI without scheme cannot contain an empty host string according to PSR-7: '.$uri);
}
$port = $uri->getPort();
if (null !== $port && ($port < 0 || $port > 65535)) {
throw new SyntaxError('The URI port is outside the established TCP and UDP port ranges: '.$uri);
}
$this->uri = $this->normalizePsr7Uri($uri);
}
/**
* PSR-7 UriInterface makes the following normalization.
*
* Safely stringify input when possible for League UriInterface compatibility.
*
* Query, Fragment and User Info when undefined are normalized to the empty string
*/
private function normalizePsr7Uri(UriInterface $uri): UriInterface
{
$components = [];
if ('' === $uri->getFragment()) {
$components['fragment'] = null;
}
if ('' === $uri->getQuery()) {
$components['query'] = null;
}
if ('' === $uri->getUserInfo()) {
$components['user'] = null;
$components['pass'] = null;
}
return match ($components) {
[] => $uri,
default => Uri::fromComponents([...$uri->toComponents(), ...$components]),
};
}
/**
* Create a new instance from a string or a stringable object.
*/
public static function new(Rfc3986Uri|WhatwgUrl|Stringable|string $uri = ''): self
{
return new self(Uri::new($uri));
}
/**
* Create a new instance from a string or a stringable structure or returns null on failure.
*/
public static function tryNew(Rfc3986Uri|WhatwgUrl|Stringable|string $uri = ''): ?self
{
try {
return self::new($uri);
} catch (UriException) {
return null;
}
}
/**
* Create a new instance from a hash of parse_url parts.
*
* @param InputComponentMap $components a hash representation of the URI similar
* to PHP parse_url function result
*/
public static function fromComponents(array $components): self
{
$components += [
'scheme' => null, 'user' => null, 'pass' => null, 'host' => null,
'port' => null, 'path' => '', 'query' => null, 'fragment' => null,
];
if ('' === $components['user']) {
$components['user'] = null;
}
if ('' === $components['pass']) {
$components['pass'] = null;
}
if ('' === $components['query']) {
$components['query'] = null;
}
if ('' === $components['fragment']) {
$components['fragment'] = null;
}
return new self(Uri::fromComponents($components));
}
/**
* Create a new instance from the environment.
*/
public static function fromServer(array $server): self
{
return new self(Uri::fromServer($server));
}
/**
* Creates a new instance from a template.
*
* @throws TemplateCanNotBeExpanded if the variables are invalid or missing
* @throws UriException if the variables are invalid or missing
*/
public static function fromTemplate(Stringable|string $template, iterable $variables = []): self
{
return new self(Uri::fromTemplate($template, $variables));
}
/**
* Returns a new instance from a URI and a Base URI.or null on failure.
*
* The returned URI must be absolute if a base URI is provided
*/
public static function parse(WhatWgUrl|Rfc3986Uri|Stringable|string $uri, WhatWgUrl|Rfc3986Uri|Stringable|string|null $baseUri = null): ?self
{
return null !== ($uri = Uri::parse($uri, $baseUri)) ? new self($uri) : null;
}
public function getScheme(): string
{
return $this->uri->getScheme() ?? '';
}
public function getAuthority(): string
{
return $this->uri->getAuthority() ?? '';
}
public function getUserInfo(): string
{
return $this->uri->getUserInfo() ?? '';
}
public function getHost(): string
{
return $this->uri->getHost() ?? '';
}
public function getPort(): ?int
{
return $this->uri->getPort();
}
public function getPath(): string
{
$path = $this->uri->getPath();
return match (true) {
str_starts_with($path, '//') => '/'.ltrim($path, '/'),
default => $path,
};
}
public function getQuery(): string
{
return $this->uri->getQuery() ?? '';
}
public function getFragment(): string
{
return $this->uri->getFragment() ?? '';
}
public function __toString(): string
{
return $this->uri->toString();
}
public function jsonSerialize(): string
{
return $this->uri->toString();
}
/**
* Safely stringify input when possible for League UriInterface compatibility.
*/
private function filterInput(string $str): ?string
{
return match ('') {
$str => null,
default => $str,
};
}
private function newInstance(UriInterface $uri): self
{
return match ($this->uri->toString()) {
$uri->toString() => $this,
default => new self($uri),
};
}
public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): static
{
if (!is_bool($condition)) {
$condition = $condition($this);
}
return match (true) {
$condition => $onSuccess($this),
null !== $onFail => $onFail($this),
default => $this,
} ?? $this;
}
public function transform(callable $callback): static
{
return $callback($this);
}
public function withScheme(string $scheme): self
{
return $this->newInstance($this->uri->withScheme($this->filterInput($scheme)));
}
public function withUserInfo(string $user, ?string $password = null): self
{
return $this->newInstance($this->uri->withUserInfo($this->filterInput($user), $password));
}
public function withHost(string $host): self
{
return $this->newInstance($this->uri->withHost($this->filterInput($host)));
}
public function withPort(?int $port): self
{
return $this->newInstance($this->uri->withPort($port));
}
public function withPath(string $path): self
{
return $this->newInstance($this->uri->withPath($path));
}
public function withQuery(string $query): self
{
return $this->newInstance($this->uri->withQuery($this->filterInput($query)));
}
public function withFragment(string $fragment): self
{
return $this->newInstance($this->uri->withFragment($this->filterInput($fragment)));
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.6.0
* @codeCoverageIgnore
* @see Http::parse()
*
* Create a new instance from a URI and a Base URI.
*
* The returned URI must be absolute.
*/
#[Deprecated(message:'use League\Uri\Http::parse() instead', since:'league/uri:7.6.0')]
public static function fromBaseUri(Rfc3986Uri|WhatwgUrl|Stringable|string $uri, Rfc3986Uri|WhatwgUrl|Stringable|string|null $baseUri = null): self
{
return new self(Uri::fromBaseUri($uri, $baseUri));
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @codeCoverageIgnore
* @see Http::new()
*
* Create a new instance from a string.
*/
#[Deprecated(message:'use League\Uri\Http::new() instead', since:'league/uri:7.0.0')]
public static function createFromString(Stringable|string $uri = ''): self
{
return self::new($uri);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @codeCoverageIgnore
* @see Http::fromComponents()
*
* Create a new instance from a hash of parse_url parts.
*
* @param InputComponentMap $components a hash representation of the URI similar
* to PHP parse_url function result
*/
#[Deprecated(message:'use League\Uri\Http::fromComponents() instead', since:'league/uri:7.0.0')]
public static function createFromComponents(array $components): self
{
return self::fromComponents($components);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @codeCoverageIgnore
* @see Http::fromServer()
*
* Create a new instance from the environment.
*/
#[Deprecated(message:'use League\Uri\Http::fromServer() instead', since:'league/uri:7.0.0')]
public static function createFromServer(array $server): self
{
return self::fromServer($server);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @codeCoverageIgnore
* @see Http::new()
*
* Create a new instance from a URI object.
*/
#[Deprecated(message:'use League\Uri\Http::new() instead', since:'league/uri:7.0.0')]
public static function createFromUri(Psr7UriInterface|UriInterface $uri): self
{
return self::new($uri);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @codeCoverageIgnore
* @see Http::fromBaseUri()
*
* Create a new instance from a URI and a Base URI.
*
* The returned URI must be absolute.
*/
#[Deprecated(message:'use League\Uri\Http::fromBaseUri() instead', since:'league/uri:7.0.0')]
public static function createFromBaseUri(Stringable|string $uri, Stringable|string|null $baseUri = null): self
{
return self::fromBaseUri($uri, $baseUri);
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
use Psr\Http\Message\UriFactoryInterface;
use Psr\Http\Message\UriInterface;
final class HttpFactory implements UriFactoryInterface
{
public function createUri(string $uri = ''): UriInterface
{
return Http::new($uri);
}
}
+20
View File
@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2015 ignace nyamagana butera
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+36
View File
@@ -0,0 +1,36 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
enum SchemeType
{
case Opaque;
case Hierarchical;
case Unknown;
public function isOpaque(): bool
{
return self::Opaque === $this;
}
public function isHierarchical(): bool
{
return self::Hierarchical === $this;
}
public function isUnknown(): bool
{
return self::Unknown === $this;
}
}

Some files were not shown because too many files have changed in this diff Show More