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:
Vendored
+6
@@ -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',
|
||||
);
|
||||
|
||||
Vendored
+13
@@ -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'),
|
||||
);
|
||||
|
||||
Vendored
+82
@@ -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 (
|
||||
|
||||
Vendored
+19
@@ -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.
|
||||
Vendored
+85
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
+2910
File diff suppressed because it is too large
Load Diff
+322
@@ -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;
|
||||
}
|
||||
}
|
||||
Vendored
+1027
File diff suppressed because it is too large
Load Diff
Vendored
+128
-2
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Vendored
+2
-2
@@ -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) {
|
||||
|
||||
Vendored
+323
@@ -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.
|
||||
Vendored
+202
@@ -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.
|
||||
Vendored
+442
@@ -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.
|
||||
Vendored
+32
@@ -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
@@ -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
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GeoIp2\Exception;
|
||||
|
||||
/**
|
||||
* This class represents a generic error.
|
||||
*/
|
||||
class GeoIp2Exception extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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'];
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
Vendored
+36
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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))*)
|
||||
)
|
||||
^(?:(?®_name)\.)*(?®_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
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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))*)
|
||||
)
|
||||
^(?:(?®_name)\.)*(?®_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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Vendored
+20
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
Vendored
+646
@@ -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()];
|
||||
}
|
||||
}
|
||||
Vendored
+358
@@ -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.');
|
||||
}
|
||||
}
|
||||
Vendored
+386
@@ -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);
|
||||
}
|
||||
}
|
||||
Vendored
+25
@@ -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);
|
||||
}
|
||||
}
|
||||
Vendored
+20
@@ -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.
|
||||
Vendored
+36
@@ -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
Reference in New Issue
Block a user