feat(demo): add story 1 — Sorano: Rock and Time

This commit is contained in:
2026-06-20 21:19:57 +02:00
parent 42ed59a6b3
commit 8f87155c1d
5508 changed files with 1595740 additions and 124 deletions
+22
View File
@@ -0,0 +1,22 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
throw new RuntimeException($err);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit98c98c1c3d67f21a128f935fe4a74897::getLoader();
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../p3k/picofeed/picofeed)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/p3k/picofeed/picofeed');
}
}
return include __DIR__ . '/..'.'/p3k/picofeed/picofeed';
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../scssphp/scssphp/bin/pscss)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/scssphp/scssphp/bin/pscss');
}
}
return include __DIR__ . '/..'.'/scssphp/scssphp/bin/pscss';
+579
View File
@@ -0,0 +1,579 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var string|null */
private $apcuPrefix;
/**
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return list<string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders keyed by their corresponding vendor directories.
*
* @return array<string, self>
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}
+396
View File
@@ -0,0 +1,396 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
* @internal
*/
private static $selfDir = null;
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool
*/
private static $installedIsLocalDir;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
// so we have to assume it does not, and that may result in duplicate data being returned when listing
// all installed packages for example
self::$installedIsLocalDir = false;
}
/**
* @return string
*/
private static function getSelfDir()
{
if (self::$selfDir === null) {
self::$selfDir = strtr(__DIR__, '\\', '/');
}
return self::$selfDir;
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
$copiedLocalDir = false;
if (self::$canGetVendors) {
$selfDir = self::getSelfDir();
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
self::$installedByVendor[$vendorDir] = $required;
$installed[] = $required;
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
self::$installed = $required;
self::$installedIsLocalDir = true;
}
}
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
$copiedLocalDir = true;
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array() && !$copiedLocalDir) {
$installed[] = self::$installed;
}
return $installed;
}
}
+21
View File
@@ -0,0 +1,21 @@
Copyright (c) Nils Adermann, Jordi Boggiano
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.
+11
View File
@@ -0,0 +1,11 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'Grav\\Plugin\\AdminPlugin' => $baseDir . '/admin.php',
);
+11
View File
@@ -0,0 +1,11 @@
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
);
+10
View File
@@ -0,0 +1,10 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'PicoFeed' => array($vendorDir . '/p3k/picofeed/lib'),
);
+18
View File
@@ -0,0 +1,18 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'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\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
'League\\Uri\\' => array($vendorDir . '/league/uri', $vendorDir . '/league/uri-interfaces'),
'Laminas\\Xml\\' => array($vendorDir . '/laminas/laminas-xml/src'),
'Grav\\Plugin\\Admin\\' => array($baseDir . '/classes/plugin'),
);
+50
View File
@@ -0,0 +1,50 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit98c98c1c3d67f21a128f935fe4a74897
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit98c98c1c3d67f21a128f935fe4a74897', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit98c98c1c3d67f21a128f935fe4a74897', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit98c98c1c3d67f21a128f935fe4a74897::getInitializer($loader));
$loader->register(true);
$filesToLoad = \Composer\Autoload\ComposerStaticInit98c98c1c3d67f21a128f935fe4a74897::$files;
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}, null, null);
foreach ($filesToLoad as $fileIdentifier => $file) {
$requireFile($fileIdentifier, $file);
}
return $loader;
}
}
+104
View File
@@ -0,0 +1,104 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit98c98c1c3d67f21a128f935fe4a74897
{
public static $files = array (
'320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
);
public static $prefixLengthsPsr4 = array (
'S' =>
array (
'Symfony\\Polyfill\\Mbstring\\' => 26,
'Symfony\\Polyfill\\Ctype\\' => 23,
'Symfony\\Component\\Filesystem\\' => 29,
'SourceSpan\\' => 11,
'ScssPhp\\ScssPhp\\' => 16,
),
'P' =>
array (
'Psr\\Http\\Message\\' => 17,
),
'L' =>
array (
'League\\Uri\\' => 11,
'Laminas\\Xml\\' => 12,
),
'G' =>
array (
'Grav\\Plugin\\Admin\\' => 18,
),
);
public static $prefixDirsPsr4 = array (
'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\\Http\\Message\\' =>
array (
0 => __DIR__ . '/..' . '/psr/http-factory/src',
1 => __DIR__ . '/..' . '/psr/http-message/src',
),
'League\\Uri\\' =>
array (
0 => __DIR__ . '/..' . '/league/uri',
1 => __DIR__ . '/..' . '/league/uri-interfaces',
),
'Laminas\\Xml\\' =>
array (
0 => __DIR__ . '/..' . '/laminas/laminas-xml/src',
),
'Grav\\Plugin\\Admin\\' =>
array (
0 => __DIR__ . '/../..' . '/classes/plugin',
),
);
public static $prefixesPsr0 = array (
'P' =>
array (
'PicoFeed' =>
array (
0 => __DIR__ . '/..' . '/p3k/picofeed/lib',
),
),
);
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'Grav\\Plugin\\AdminPlugin' => __DIR__ . '/../..' . '/admin.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit98c98c1c3d67f21a128f935fe4a74897::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit98c98c1c3d67f21a128f935fe4a74897::$prefixDirsPsr4;
$loader->prefixesPsr0 = ComposerStaticInit98c98c1c3d67f21a128f935fe4a74897::$prefixesPsr0;
$loader->classMap = ComposerStaticInit98c98c1c3d67f21a128f935fe4a74897::$classMap;
}, null, ClassLoader::class);
}
}
+822
View File
@@ -0,0 +1,822 @@
{
"packages": [
{
"name": "laminas/laminas-xml",
"version": "1.7.0",
"version_normalized": "1.7.0.0",
"source": {
"type": "git",
"url": "https://github.com/laminas/laminas-xml.git",
"reference": "3a7850dec668a89807accfa4826a2ff11497fe74"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laminas/laminas-xml/zipball/3a7850dec668a89807accfa4826a2ff11497fe74",
"reference": "3a7850dec668a89807accfa4826a2ff11497fe74",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-simplexml": "*",
"php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0"
},
"conflict": {
"zendframework/zendxml": "*"
},
"require-dev": {
"ext-iconv": "*",
"laminas/laminas-coding-standard": "~1.0.0",
"phpunit/phpunit": "^10.5.35 || ^11.4",
"squizlabs/php_codesniffer": "3.10.3 as 2.9999999.9999999"
},
"time": "2024-10-11T08:45:59+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Laminas\\Xml\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"description": "Utility library for XML usage, best practices, and security in PHP",
"homepage": "https://laminas.dev",
"keywords": [
"laminas",
"security",
"xml"
],
"support": {
"chat": "https://laminas.dev/chat",
"forum": "https://discourse.laminas.dev",
"issues": "https://github.com/laminas/laminas-xml/issues",
"rss": "https://github.com/laminas/laminas-xml/releases.atom",
"source": "https://github.com/laminas/laminas-xml"
},
"funding": [
{
"url": "https://funding.communitybridge.org/projects/laminas-project",
"type": "community_bridge"
}
],
"install-path": "../laminas/laminas-xml"
},
{
"name": "league/uri",
"version": "7.7.0",
"version_normalized": "7.7.0.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/uri.git",
"reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/uri/zipball/8d587cddee53490f9b82bf203d3a9aa7ea4f9807",
"reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807",
"shasum": ""
},
"require": {
"league/uri-interfaces": "^7.7",
"php": "^8.1",
"psr/http-factory": "^1"
},
"conflict": {
"league/uri-schemes": "^1.0"
},
"suggest": {
"ext-bcmath": "to improve IPV4 host parsing",
"ext-dom": "to convert the URI into an HTML anchor tag",
"ext-fileinfo": "to create Data URI from file contennts",
"ext-gmp": "to improve IPV4 host parsing",
"ext-intl": "to handle IDN host with the best performance",
"ext-uri": "to use the PHP native URI class",
"jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain",
"league/uri-components": "Needed to easily manipulate URI objects components",
"league/uri-polyfill": "Needed to backport the PHP URI extension for older versions of PHP",
"php-64bit": "to improve IPV4 host parsing",
"rowbot/url": "to handle WHATWG URL",
"symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present"
},
"time": "2025-12-07T16:02:06+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "7.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"League\\Uri\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ignace Nyamagana Butera",
"email": "nyamsprod@gmail.com",
"homepage": "https://nyamsprod.com"
}
],
"description": "URI manipulation library",
"homepage": "https://uri.thephpleague.com",
"keywords": [
"URN",
"data-uri",
"file-uri",
"ftp",
"hostname",
"http",
"https",
"middleware",
"parse_str",
"parse_url",
"psr-7",
"query-string",
"querystring",
"rfc2141",
"rfc3986",
"rfc3987",
"rfc6570",
"rfc8141",
"uri",
"uri-template",
"url",
"ws"
],
"support": {
"docs": "https://uri.thephpleague.com",
"forum": "https://thephpleague.slack.com",
"issues": "https://github.com/thephpleague/uri-src/issues",
"source": "https://github.com/thephpleague/uri/tree/7.7.0"
},
"funding": [
{
"url": "https://github.com/sponsors/nyamsprod",
"type": "github"
}
],
"install-path": "../league/uri"
},
{
"name": "league/uri-interfaces",
"version": "7.7.0",
"version_normalized": "7.7.0.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/uri-interfaces.git",
"reference": "62ccc1a0435e1c54e10ee6022df28d6c04c2946c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/62ccc1a0435e1c54e10ee6022df28d6c04c2946c",
"reference": "62ccc1a0435e1c54e10ee6022df28d6c04c2946c",
"shasum": ""
},
"require": {
"ext-filter": "*",
"php": "^8.1",
"psr/http-message": "^1.1 || ^2.0"
},
"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",
"rowbot/url": "to handle WHATWG URL",
"symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present"
},
"time": "2025-12-07T16:03:21+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "7.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"League\\Uri\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ignace Nyamagana Butera",
"email": "nyamsprod@gmail.com",
"homepage": "https://nyamsprod.com"
}
],
"description": "Common tools for parsing and resolving RFC3987/RFC3986 URI",
"homepage": "https://uri.thephpleague.com",
"keywords": [
"data-uri",
"file-uri",
"ftp",
"hostname",
"http",
"https",
"parse_str",
"parse_url",
"psr-7",
"query-string",
"querystring",
"rfc3986",
"rfc3987",
"rfc6570",
"uri",
"url",
"ws"
],
"support": {
"docs": "https://uri.thephpleague.com",
"forum": "https://thephpleague.slack.com",
"issues": "https://github.com/thephpleague/uri-src/issues",
"source": "https://github.com/thephpleague/uri-interfaces/tree/7.7.0"
},
"funding": [
{
"url": "https://github.com/sponsors/nyamsprod",
"type": "github"
}
],
"install-path": "../league/uri-interfaces"
},
{
"name": "p3k/picofeed",
"version": "1.1.0",
"version_normalized": "1.1.0.0",
"source": {
"type": "git",
"url": "https://github.com/rhukster/picofeed.git",
"reference": "b567fe98836e8700dba8bd4363af47234086c600"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/rhukster/picofeed/zipball/b567fe98836e8700dba8bd4363af47234086c600",
"reference": "b567fe98836e8700dba8bd4363af47234086c600",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-iconv": "*",
"ext-libxml": "*",
"ext-simplexml": "*",
"ext-xml": "*",
"laminas/laminas-xml": "^1.7",
"php": ">=5.3.0"
},
"replace": {
"miniflux/picofeed": "0.1.35"
},
"require-dev": {
"phpdocumentor/reflection-docblock": "2.0.4",
"phpunit/phpunit": "4.8.26",
"rector/rector": "^1.2",
"symfony/yaml": "2.8.7"
},
"suggest": {
"ext-curl": "PicoFeed will use cURL if present"
},
"time": "2024-12-09T23:53:27+00:00",
"bin": [
"picofeed"
],
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-0": {
"PicoFeed": "lib/"
}
},
"license": [
"MIT"
],
"authors": [
{
"name": "Frédéric Guillot"
}
],
"description": "Modern library to handle RSS/Atom feeds",
"homepage": "https://github.com/aaronpk/picoFeed",
"support": {
"source": "https://github.com/rhukster/picofeed/tree/1.1.0"
},
"install-path": "../p3k/picofeed"
},
{
"name": "psr/http-factory",
"version": "1.1.0",
"version_normalized": "1.1.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-factory.git",
"reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
"reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
"shasum": ""
},
"require": {
"php": ">=7.1",
"psr/http-message": "^1.0 || ^2.0"
},
"time": "2024-04-15T12:06:14+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
"keywords": [
"factory",
"http",
"message",
"psr",
"psr-17",
"psr-7",
"request",
"response"
],
"support": {
"source": "https://github.com/php-fig/http-factory"
},
"install-path": "../psr/http-factory"
},
{
"name": "psr/http-message",
"version": "2.0",
"version_normalized": "2.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message.git",
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"time": "2023-04-04T09:54:51+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP messages",
"homepage": "https://github.com/php-fig/http-message",
"keywords": [
"http",
"http-message",
"psr",
"psr-7",
"request",
"response"
],
"support": {
"source": "https://github.com/php-fig/http-message/tree/2.0"
},
"install-path": "../psr/http-message"
},
{
"name": "scssphp/scssphp",
"version": "v2.1.0",
"version_normalized": "2.1.0.0",
"source": {
"type": "git",
"url": "https://github.com/scssphp/scssphp.git",
"reference": "d8450c2baf5fb07d00374999d0ea51276974d1b6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/scssphp/scssphp/zipball/d8450c2baf5fb07d00374999d0ea51276974d1b6",
"reference": "d8450c2baf5fb07d00374999d0ea51276974d1b6",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-json": "*",
"ext-mbstring": "*",
"league/uri": "^7.6",
"league/uri-interfaces": "^7.6",
"php": ">=8.1",
"scssphp/source-span": "^1.1",
"symfony/filesystem": "^5.4 || ^6.0 || ^7.0 || ^8.0"
},
"require-dev": {
"jgthms/bulma": "~0.9.4",
"jiripudil/phpstan-sealed-classes": "^1.3",
"phpstan/phpstan": "^2.1.31",
"phpstan/phpstan-deprecation-rules": "^2.0",
"phpunit/phpunit": "^9.5.6",
"sass/sass-spec": "*",
"squizlabs/php_codesniffer": "^3.13",
"symfony/phpunit-bridge": "^7.3 || ^8.0",
"symfony/polyfill-php84": "^1.33",
"symfony/var-dumper": "^6.4 || ^7.3 || ^8.0",
"thoughtbot/bourbon": "^7.0",
"twbs/bootstrap": "^5.3",
"twbs/bootstrap4": "4.6.1",
"zurb/foundation": "~6.7.0"
},
"time": "2025-11-21T17:27:59+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"ScssPhp\\ScssPhp\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Anthon Pang",
"email": "apang@softwaredevelopment.ca",
"homepage": "https://github.com/robocoder"
},
{
"name": "Cédric Morin",
"email": "cedric@yterium.com",
"homepage": "https://github.com/Cerdic"
}
],
"description": "scssphp is a compiler for SCSS written in PHP.",
"homepage": "https://scssphp.github.io/scssphp/",
"keywords": [
"css",
"less",
"sass",
"scss",
"stylesheet"
],
"support": {
"issues": "https://github.com/scssphp/scssphp/issues",
"source": "https://github.com/scssphp/scssphp/tree/v2.1.0"
},
"install-path": "../scssphp/scssphp"
},
{
"name": "scssphp/source-span",
"version": "v1.1.0",
"version_normalized": "1.1.0.0",
"source": {
"type": "git",
"url": "https://github.com/scssphp/source-span.git",
"reference": "37d653206daf11da1ee60b333984101bc4c27ba2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/scssphp/source-span/zipball/37d653206daf11da1ee60b333984101bc4c27ba2",
"reference": "37d653206daf11da1ee60b333984101bc4c27ba2",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"league/uri": "^7.6",
"league/uri-interfaces": "^7.6",
"php": ">=8.1"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpstan/phpstan-deprecation-rules": "^2.0",
"phpunit/phpunit": "^9.5.6",
"squizlabs/php_codesniffer": "~3.5",
"symfony/phpunit-bridge": "^6.4 || ^7.3 || ^8.0",
"symfony/var-dumper": "^6.4 || ^7.3 || ^8.0"
},
"time": "2025-11-21T16:28:19+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"SourceSpan\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christophe Coevoet",
"homepage": "https://github.com/stof"
}
],
"description": "Provides a representation for source code locations and spans.",
"keywords": [
"parsing"
],
"support": {
"issues": "https://github.com/scssphp/source-span/issues",
"source": "https://github.com/scssphp/source-span/tree/v1.1.0"
},
"install-path": "../scssphp/source-span"
},
{
"name": "symfony/filesystem",
"version": "v7.4.0",
"version_normalized": "7.4.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "d551b38811096d0be9c4691d406991b47c0c630a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a",
"reference": "d551b38811096d0be9c4691d406991b47c0c630a",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-mbstring": "~1.8"
},
"require-dev": {
"symfony/process": "^6.4|^7.0|^8.0"
},
"time": "2025-11-27T13:27:24+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Symfony\\Component\\Filesystem\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/filesystem/tree/v7.4.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"install-path": "../symfony/filesystem"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.33.0",
"version_normalized": "1.33.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"time": "2024-09-09T11:45:10+00:00",
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"installation-source": "dist",
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"install-path": "../symfony/polyfill-ctype"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.33.0",
"version_normalized": "1.33.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
"reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
"shasum": ""
},
"require": {
"ext-iconv": "*",
"php": ">=7.2"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"time": "2024-12-23T08:48:59+00:00",
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"installation-source": "dist",
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"install-path": "../symfony/polyfill-mbstring"
}
],
"dev": false,
"dev-package-names": []
}
+128
View File
@@ -0,0 +1,128 @@
<?php return array(
'root' => array(
'name' => 'getgrav/grav-plugin-admin',
'pretty_version' => '1.11.x-dev',
'version' => '1.11.9999999.9999999-dev',
'reference' => '4631f81dbe4a3e12144abe5a77d507d6a346f75c',
'type' => 'grav-plugin',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => false,
),
'versions' => array(
'getgrav/grav-plugin-admin' => array(
'pretty_version' => '1.11.x-dev',
'version' => '1.11.9999999.9999999-dev',
'reference' => '4631f81dbe4a3e12144abe5a77d507d6a346f75c',
'type' => 'grav-plugin',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'laminas/laminas-xml' => array(
'pretty_version' => '1.7.0',
'version' => '1.7.0.0',
'reference' => '3a7850dec668a89807accfa4826a2ff11497fe74',
'type' => 'library',
'install_path' => __DIR__ . '/../laminas/laminas-xml',
'aliases' => array(),
'dev_requirement' => false,
),
'league/uri' => array(
'pretty_version' => '7.7.0',
'version' => '7.7.0.0',
'reference' => '8d587cddee53490f9b82bf203d3a9aa7ea4f9807',
'type' => 'library',
'install_path' => __DIR__ . '/../league/uri',
'aliases' => array(),
'dev_requirement' => false,
),
'league/uri-interfaces' => array(
'pretty_version' => '7.7.0',
'version' => '7.7.0.0',
'reference' => '62ccc1a0435e1c54e10ee6022df28d6c04c2946c',
'type' => 'library',
'install_path' => __DIR__ . '/../league/uri-interfaces',
'aliases' => array(),
'dev_requirement' => false,
),
'miniflux/picofeed' => array(
'dev_requirement' => false,
'replaced' => array(
0 => '0.1.35',
),
),
'p3k/picofeed' => array(
'pretty_version' => '1.1.0',
'version' => '1.1.0.0',
'reference' => 'b567fe98836e8700dba8bd4363af47234086c600',
'type' => 'library',
'install_path' => __DIR__ . '/../p3k/picofeed',
'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,
),
'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/filesystem' => array(
'pretty_version' => 'v7.4.0',
'version' => '7.4.0.0',
'reference' => 'd551b38811096d0be9c4691d406991b47c0c630a',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/filesystem',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-ctype' => array(
'pretty_version' => 'v1.33.0',
'version' => '1.33.0.0',
'reference' => 'a3cc8b044a6ea513310cbd48ef7333b384945638',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-ctype',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-mbstring' => array(
'pretty_version' => 'v1.33.0',
'version' => '1.33.0.0',
'reference' => '6d857f4d76bd4b343eac26d6b539585d2bc56493',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-mbstring',
'aliases' => array(),
'dev_requirement' => false,
),
),
);
+25
View File
@@ -0,0 +1,25 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 80300)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.3.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
throw new \RuntimeException(
'Composer detected issues in your platform: ' . implode(' ', $issues)
);
}
@@ -0,0 +1,22 @@
name: Autocloser
on: [issues, pull_request]
jobs:
autoclose:
runs-on: ubuntu-latest
steps:
- name: Autoclose new issues and PRs
uses: roots/issue-closer@v1.1
with:
repo-token: ${{ secrets.ORGANIZATION_ADMIN_TOKEN }}
issue-pattern: "^exact-string-will-never-match$"
pr-pattern: "^exact-string-will-never-match$"
issue-close-message: |
This package is considered feature-complete, and is now in **security-only** maintenance mode, following a [decision by the Technical Steering Committee](https://github.com/laminas/technical-steering-committee/blob/2b55453e172a1b8c9c4c212be7cf7e7a58b9352c/meetings/minutes/2020-08-03-TSC-Minutes.md#vote-on-components-to-mark-as-security-only).
If you have a security issue, please [follow our security reporting guidelines](https://getlaminas.org/security/).
If you wish to take on the role of maintainer, please [nominate yourself](https://github.com/laminas/technical-steering-committee/issues/new?assignees=&labels=Nomination&template=Maintainer_Nomination.md&title=%5BNOMINATION%5D%5BMAINTAINER%5D%3A+%7Bname+of+person+being+nominated%7D)
pr-close-message: |
This package is considered feature-complete, and is now in **security-only** maintenance mode, following a [decision by the Technical Steering Committee](https://github.com/laminas/technical-steering-committee/blob/2b55453e172a1b8c9c4c212be7cf7e7a58b9352c/meetings/minutes/2020-08-03-TSC-Minutes.md#vote-on-components-to-mark-as-security-only).
If you have a security issue, please [follow our security reporting guidelines](https://getlaminas.org/security/).
If you wish to take on the role of maintainer, please [nominate yourself](https://github.com/laminas/technical-steering-committee/issues/new?assignees=&labels=Nomination&template=Maintainer_Nomination.md&title=%5BNOMINATION%5D%5BMAINTAINER%5D%3A+%7Bname+of+person+being+nominated%7D)
@@ -0,0 +1,33 @@
name: "Continuous Integration"
on:
pull_request:
push:
branches:
- '[0-9]+.[0-9]+.x'
- 'refs/pull/*'
tags:
jobs:
matrix:
name: Generate job matrix
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.matrix.outputs.matrix }}
steps:
- name: Gather CI configuration
id: matrix
uses: laminas/laminas-ci-matrix-action@v1
qa:
name: QA Checks
needs: [matrix]
runs-on: ${{ matrix.operatingSystem }}
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.matrix.outputs.matrix) }}
steps:
- name: ${{ matrix.name }}
uses: laminas/laminas-continuous-integration-action@v1
with:
job: ${{ matrix.job }}
@@ -0,0 +1,71 @@
# Alternate workflow example.
# This one is identical to the one in release-on-milestone.yml, with one change:
# the Release step uses the ORGANIZATION_ADMIN_TOKEN instead, to allow it to
# trigger a release workflow event. This is useful if you have other actions
# that intercept that event.
name: "Automatic Releases"
on:
milestone:
types:
- "closed"
jobs:
release:
name: "GIT tag, release & create merge-up PR"
runs-on: ubuntu-latest
steps:
- name: "Checkout"
uses: "actions/checkout@v2"
- name: "Release"
uses: "laminas/automatic-releases@v1"
with:
command-name: "laminas:automatic-releases:release"
env:
"GITHUB_TOKEN": ${{ secrets.ORGANIZATION_ADMIN_TOKEN }}
"SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
"GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
"GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
- name: "Create Merge-Up Pull Request"
uses: "laminas/automatic-releases@v1"
with:
command-name: "laminas:automatic-releases:create-merge-up-pull-request"
env:
"GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }}
"SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
"GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
"GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
- name: "Create and/or Switch to new Release Branch"
uses: "laminas/automatic-releases@v1"
with:
command-name: "laminas:automatic-releases:switch-default-branch-to-next-minor"
env:
"GITHUB_TOKEN": ${{ secrets.ORGANIZATION_ADMIN_TOKEN }}
"SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
"GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
"GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
- name: "Bump Changelog Version On Originating Release Branch"
uses: "laminas/automatic-releases@v1"
with:
command-name: "laminas:automatic-releases:bump-changelog"
env:
"GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }}
"SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
"GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
"GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
- name: "Create new milestones"
uses: "laminas/automatic-releases@v1"
with:
command-name: "laminas:automatic-releases:create-milestones"
env:
"GITHUB_TOKEN": ${{ secrets.GITHUB_TOKEN }}
"SIGNING_SECRET_KEY": ${{ secrets.SIGNING_SECRET_KEY }}
"GIT_AUTHOR_NAME": ${{ secrets.GIT_AUTHOR_NAME }}
"GIT_AUTHOR_EMAIL": ${{ secrets.GIT_AUTHOR_EMAIL }}
+1
View File
@@ -0,0 +1 @@
Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. (https://getlaminas.org/)
+26
View File
@@ -0,0 +1,26 @@
Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
- Neither the name of Laminas Foundation nor the names of its contributors may
be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 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.
+66
View File
@@ -0,0 +1,66 @@
# laminas-xml
> ## 🇷🇺 Русским гражданам
>
> Мы, участники Laminas, родились и живем в разных странах. У многих из нас есть друзья, родственники и коллеги как в России, так и в Украине. Некоторые из нас родились в России. Некоторые из нас живут в России. У некоторых бабушки и дедушки сражались с фашистами во Второй мировой войне. Здесь никто не поддерживает фашизм.
>
> У одного из нас есть украинская родственница, которая спаслась из дома вместе с сыном. Поезд задержался из-за бомбежки на дороге впереди. У нас есть друзья, которые прячутся в бомбоубежищах. Мы с тревогой ждем весточки от них после воздушных налетов, которые беспорядочно наносят удары и попадают по больницам, школам, детским садам и домам. Мы не берем это из каких-либо СМИ. Мы наблюдаем это напрямую.
>
> Вы доверяете нам достаточно, чтоб использовать наши программы, и мы просим вас довериться нам вновь. Мы нуждаемся в помощи. Выходите и протестуйте против этой бесполезной войны. Остановите кровопролитие. Скажите "Нет войне!"
>
> ## 🇺🇸 To Citizens of Russia
>
> We at Laminas come from all over the world. Many of us have friends, family and colleagues in both Russia and Ukraine. Some of us were born in Russia. Some of us currently live in Russia. Some have grandparents who fought Nazis in World War II. Nobody here supports fascism.
>
> One team member has a Ukrainian relative who fled her home with her son. The train was delayed due to bombing on the road ahead. We have friends who are hiding in bomb shelters. We anxiously follow up on them after the air raids, which indiscriminately fire at hospitals, schools, kindergartens and houses. We're not taking this from any media. These are our actual experiences.
>
> You trust us enough to use our software. We ask that you trust us to say the truth on this. We need your help. Go out and protest this unnecessary war. Stop the bloodshed. Say "stop the war!"
> This package is considered feature-complete, and is now in **security-only** maintenance mode, following a [decision by the Technical Steering Committee](https://github.com/laminas/technical-steering-committee/blob/2b55453e172a1b8c9c4c212be7cf7e7a58b9352c/meetings/minutes/2020-08-03-TSC-Minutes.md#vote-on-components-to-mark-as-security-only).
> If you have a security issue, please [follow our security reporting guidelines](https://getlaminas.org/security/).
> If you wish to take on the role of maintainer, please [nominate yourself](https://github.com/laminas/technical-steering-committee/issues/new?assignees=&labels=Nomination&template=Maintainer_Nomination.md&title=%5BNOMINATION%5D%5BMAINTAINER%5D%3A+%7Bname+of+person+being+nominated%7D)
[![Build Status](https://github.com/laminas/laminas-xml/workflows/Continuous%20Integration/badge.svg)](https://github.com/laminas/laminas-xml/actions?query=workflow%3A"Continuous+Integration")
An utility component for XML usage and best practices in PHP
## Installation
You can install using:
```bash
$ curl -s https://getcomposer.org/installer | php
$ php composer.phar install
```
Notice that this library doesn't have any external dependencies, the usage of composer is for autoloading and standard purpose.
## Laminas\Xml\Security
This is a security component to prevent [XML eXternal Entity](https://www.owasp.org/index.php/XML_External_Entity_%28XXE%29_Processing) (XXE) and [XML Entity Expansion](http://projects.webappsec.org/w/page/13247002/XML%20Entity%20Expansion) (XEE) attacks on XML documents.
The XXE attack is prevented disabling the load of external entities in the libxml library used by PHP, using the function [libxml_disable_entity_loader](http://www.php.net/manual/en/function.libxml-disable-entity-loader.php).
The XEE attack is prevented looking inside the XML document for ENTITY usage. If the XML document uses ENTITY the library throw an Exception.
We have two static methods to scan and load XML document from a string (scan) and from a file (scanFile). You can decide to get a SimpleXMLElement or DOMDocument as result, using the following use cases:
```php
use Laminas\Xml\Security as XmlSecurity;
$xml = <<<XML
<?xml version="1.0"?>
<results>
<result>test</result>
</results>
XML;
// SimpleXML use case
$simplexml = XmlSecurity::scan($xml);
printf ("SimpleXMLElement: %s\n", ($simplexml instanceof \SimpleXMLElement) ? 'yes' : 'no');
// DOMDocument use case
$dom = new \DOMDocument('1.0');
$dom = XmlSecurity::scan($xml, $dom);
printf ("DOMDocument: %s\n", ($dom instanceof \DOMDocument) ? 'yes' : 'no');
```
+58
View File
@@ -0,0 +1,58 @@
{
"name": "laminas/laminas-xml",
"description": "Utility library for XML usage, best practices, and security in PHP",
"license": "BSD-3-Clause",
"keywords": [
"laminas",
"xml",
"security"
],
"homepage": "https://laminas.dev",
"support": {
"issues": "https://github.com/laminas/laminas-xml/issues",
"source": "https://github.com/laminas/laminas-xml",
"rss": "https://github.com/laminas/laminas-xml/releases.atom",
"chat": "https://laminas.dev/chat",
"forum": "https://discourse.laminas.dev"
},
"config": {
"sort-packages": true,
"platform": {
"php": "8.1.99"
}
},
"require": {
"php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0",
"ext-dom": "*",
"ext-simplexml": "*"
},
"require-dev": {
"ext-iconv": "*",
"laminas/laminas-coding-standard": "~1.0.0",
"phpunit/phpunit": "^10.5.35 || ^11.4",
"squizlabs/php_codesniffer": "3.10.3 as 2.9999999.9999999"
},
"autoload": {
"psr-4": {
"Laminas\\Xml\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"LaminasTest\\Xml\\": "test/"
}
},
"scripts": {
"check": [
"@cs-check",
"@test"
],
"cs-check": "phpcs",
"cs-fix": "phpcbf",
"test": "phpunit --colors=always",
"test-coverage": "phpunit --colors=always --coverage-clover clover.xml"
},
"conflict": {
"zendframework/zendxml": "*"
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"local>laminas/.github:renovate-config-security-updates-only"
]
}
@@ -0,0 +1,7 @@
<?php
namespace Laminas\Xml\Exception;
interface ExceptionInterface
{
}
@@ -0,0 +1,10 @@
<?php
namespace Laminas\Xml\Exception;
/**
* Invalid argument exception
*/
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{
}
@@ -0,0 +1,10 @@
<?php
namespace Laminas\Xml\Exception;
/**
* Runtime exception
*/
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}
@@ -0,0 +1,407 @@
<?php
namespace Laminas\Xml;
use DOMDocument;
use SimpleXMLElement;
class Security
{
const ENTITY_DETECT = 'Detected use of ENTITY in XML, disabled to prevent XXE/XEE attacks';
/**
* Heuristic scan to detect entity in XML
*
* @param string $xml
* @throws Exception\RuntimeException If entity expansion or external entity declaration was discovered.
*/
protected static function heuristicScan($xml)
{
foreach (self::getEntityComparison($xml) as $compare) {
if (strpos($xml, $compare) !== false) {
throw new Exception\RuntimeException(self::ENTITY_DETECT);
}
}
}
/**
* Scan XML string for potential XXE and XEE attacks
*
* @param string $xml
* @param int $libXmlConstants additional libxml constants to pass in
* @param callable $callback the callback to use to create the dom element
* @param DomDocument|null $dom
* @return SimpleXMLElement|DomDocument|boolean
* @throws Exception\RuntimeException
*/
private static function scanString($xml, $libXmlConstants, callable $callback, DOMDocument|null $dom = null)
{
// If running with PHP-FPM we perform an heuristic scan
// We cannot use libxml_disable_entity_loader because of this bug
// @see https://bugs.php.net/bug.php?id=64938
if (self::isPhpFpm()) {
self::heuristicScan($xml);
}
if (null === $dom) {
$simpleXml = true;
$dom = new DOMDocument();
}
if (! self::isPhpFpm()) {
if (\PHP_VERSION_ID < 80000) {
$loadEntities = libxml_disable_entity_loader(true);
}
$useInternalXmlErrors = libxml_use_internal_errors(true);
}
// Load XML with network access disabled (LIBXML_NONET)
// error disabled with @ for PHP-FPM scenario
set_error_handler(function ($errno, $errstr) {
if (substr_count($errstr, 'DOMDocument::loadXML()') > 0) {
return true;
}
return false;
}, E_WARNING);
$result = $callback($xml, $dom, LIBXML_NONET | $libXmlConstants);
restore_error_handler();
if (! $result) {
// Entity load to previous setting
if (! self::isPhpFpm()) {
if (\PHP_VERSION_ID < 80000) {
libxml_disable_entity_loader($loadEntities);
}
libxml_use_internal_errors($useInternalXmlErrors);
}
return false;
}
// Scan for potential XEE attacks using ENTITY, if not PHP-FPM
if (! self::isPhpFpm()) {
foreach ($dom->childNodes as $child) {
if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) {
if ($child->entities->length > 0) {
throw new Exception\RuntimeException(self::ENTITY_DETECT);
}
}
}
}
// Entity load to previous setting
if (! self::isPhpFpm()) {
if (\PHP_VERSION_ID < 80000) {
libxml_disable_entity_loader($loadEntities);
}
libxml_use_internal_errors($useInternalXmlErrors);
}
if (isset($simpleXml)) {
$result = simplexml_import_dom($dom);
if (! $result instanceof SimpleXMLElement) {
return false;
}
return $result;
}
return $dom;
}
/**
* Scan XML string for potential XXE and XEE attacks
*
* @param string $xml
* @param DomDocument|null $dom
* @param int $libXmlConstants additional libxml constants to pass in
* @throws Exception\RuntimeException
* @return SimpleXMLElement|DomDocument|boolean
*/
public static function scan($xml, DOMDocument|null $dom = null, $libXmlConstants = 0)
{
$callback = function ($xml, $dom, $constants) {
return $dom->loadXml($xml, $constants);
};
return self::scanString($xml, $libXmlConstants, $callback, $dom);
}
/**
* Scan HTML string for potential XXE and XEE attacks
*
* @param string $xml
* @param DomDocument|null $dom
* @param int $libXmlConstants additional libxml constants to pass in
* @throws Exception\RuntimeException
* @return SimpleXMLElement|DomDocument|boolean
*/
public static function scanHtml($html, DOMDocument|null $dom = null, $libXmlConstants = 0)
{
$callback = function ($html, $dom, $constants) {
return $dom->loadHtml($html, $constants);
};
return self::scanString($html, $libXmlConstants, $callback, $dom);
}
/**
* Scan XML file for potential XXE/XEE attacks
*
* @param string $file
* @param DOMDocument|null $dom
* @throws Exception\InvalidArgumentException
* @return SimpleXMLElement|DomDocument
*/
public static function scanFile($file, DOMDocument|null $dom = null)
{
if (! file_exists($file)) {
throw new Exception\InvalidArgumentException(
"The file $file specified doesn't exist"
);
}
return self::scan(file_get_contents($file), $dom);
}
/**
* Return true if PHP is running with PHP-FPM
*
* This method is mainly used to determine whether or not heuristic checks
* (vs libxml checks) should be made, due to threading issues in libxml;
* under php-fpm, threading becomes a concern.
*
* However, PHP versions 5.6.6+ contain a patch to the
* libxml support in PHP that makes the libxml checks viable; in such
* versions, this method will return false to enforce those checks, which
* are more strict and accurate than the heuristic checks.
*
* @return boolean
*/
public static function isPhpFpm()
{
$isVulnerableVersion = version_compare(PHP_VERSION, '5.6', 'ge')
&& version_compare(PHP_VERSION, '5.6.6', 'lt');
if (0 === strpos(php_sapi_name(), 'fpm') && $isVulnerableVersion) {
return true;
}
return false;
}
/**
* Determine and return the string(s) to use for the <!ENTITY comparison.
*
* @param string $xml
* @return string[]
*/
protected static function getEntityComparison($xml)
{
$encodingMap = self::getAsciiEncodingMap();
return array_map(function ($encoding) use ($encodingMap) {
$generator = isset($encodingMap[$encoding]) ? $encodingMap[$encoding] : $encodingMap['UTF-8'];
return $generator('<!ENTITY');
}, self::detectXmlEncoding($xml, self::detectStringEncoding($xml)));
}
/**
* Determine the string encoding.
*
* Determines string encoding from either a detected BOM or a
* heuristic.
*
* @param string $xml
* @return string File encoding
*/
protected static function detectStringEncoding($xml)
{
return self::detectBom($xml) ?: self::detectXmlStringEncoding($xml);
}
/**
* Attempt to match a known BOM.
*
* Iterates through the return of getBomMap(), comparing the initial bytes
* of the provided string to the BOM of each; if a match is determined,
* it returns the encoding.
*
* @param string $string
* @return false|string Returns encoding on success.
*/
protected static function detectBom($string)
{
foreach (self::getBomMap() as $criteria) {
if (0 === strncmp($string, $criteria['bom'], $criteria['length'])) {
return $criteria['encoding'];
}
}
return false;
}
/**
* Attempt to detect the string encoding of an XML string.
*
* @param string $xml
* @return string Encoding
*/
protected static function detectXmlStringEncoding($xml)
{
foreach (self::getAsciiEncodingMap() as $encoding => $generator) {
$prefix = $generator('<' . '?xml');
if (0 === strncmp($xml, $prefix, strlen($prefix))) {
return $encoding;
}
}
// Fallback
return 'UTF-8';
}
/**
* Attempt to detect the specified XML encoding.
*
* Using the file's encoding, determines if an "encoding" attribute is
* present and well-formed in the XML declaration; if so, it returns a
* list with both the ASCII representation of that declaration and the
* original file encoding.
*
* If not, a list containing only the provided file encoding is returned.
*
* @param string $xml
* @param string $fileEncoding
* @return string[] Potential XML encodings
*/
protected static function detectXmlEncoding($xml, $fileEncoding)
{
$encodingMap = self::getAsciiEncodingMap();
$generator = $encodingMap[$fileEncoding];
$encAttr = $generator('encoding="');
$quote = $generator('"');
$close = $generator('>');
$closePos = strpos($xml, $close);
if (false === $closePos) {
return [$fileEncoding];
}
$encPos = strpos($xml, $encAttr);
if (false === $encPos
|| $encPos > $closePos
) {
return [$fileEncoding];
}
$encPos += strlen($encAttr);
$quotePos = strpos($xml, $quote, $encPos);
if (false === $quotePos) {
return [$fileEncoding];
}
$encoding = self::substr($xml, $encPos, $quotePos);
return [
// Following line works because we're only supporting 8-bit safe encodings at this time.
str_replace('\0', '', $encoding), // detected encoding
$fileEncoding, // file encoding
];
}
/**
* Return a list of BOM maps.
*
* Returns a list of common encoding -> BOM maps, along with the character
* length to compare against.
*
* @link https://en.wikipedia.org/wiki/Byte_order_mark
* @return array
*/
protected static function getBomMap()
{
return [
[
'encoding' => 'UTF-32BE',
'bom' => pack('CCCC', 0x00, 0x00, 0xfe, 0xff),
'length' => 4,
],
[
'encoding' => 'UTF-32LE',
'bom' => pack('CCCC', 0xff, 0xfe, 0x00, 0x00),
'length' => 4,
],
[
'encoding' => 'GB-18030',
'bom' => pack('CCCC', 0x84, 0x31, 0x95, 0x33),
'length' => 4,
],
[
'encoding' => 'UTF-16BE',
'bom' => pack('CC', 0xfe, 0xff),
'length' => 2,
],
[
'encoding' => 'UTF-16LE',
'bom' => pack('CC', 0xff, 0xfe),
'length' => 2,
],
[
'encoding' => 'UTF-8',
'bom' => pack('CCC', 0xef, 0xbb, 0xbf),
'length' => 3,
],
];
}
/**
* Return a map of encoding => generator pairs.
*
* Returns a map of encoding => generator pairs, where the generator is a
* callable that accepts a string and returns the appropriate byte order
* sequence of that string for the encoding.
*
* @return array
*/
protected static function getAsciiEncodingMap()
{
return [
'UTF-32BE' => function ($ascii) {
return preg_replace('/(.)/', "\0\0\0\\1", $ascii);
},
'UTF-32LE' => function ($ascii) {
return preg_replace('/(.)/', "\\1\0\0\0", $ascii);
},
'UTF-32odd1' => function ($ascii) {
return preg_replace('/(.)/', "\0\\1\0\0", $ascii);
},
'UTF-32odd2' => function ($ascii) {
return preg_replace('/(.)/', "\0\0\\1\0", $ascii);
},
'UTF-16BE' => function ($ascii) {
return preg_replace('/(.)/', "\0\\1", $ascii);
},
'UTF-16LE' => function ($ascii) {
return preg_replace('/(.)/', "\\1\0", $ascii);
},
'UTF-8' => function ($ascii) {
return $ascii;
},
'GB-18030' => function ($ascii) {
return $ascii;
},
];
}
/**
* Binary-safe substr.
*
* substr() is not binary-safe; this method loops by character to ensure
* multi-byte characters are aggregated correctly.
*
* @param string $string
* @param int $start
* @param int $end
* @return string
*/
protected static function substr($string, $start, $end)
{
$substr = '';
for ($i = $start; $i < $end; $i += 1) {
$substr .= $string[$i];
}
return $substr;
}
}
@@ -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,117 @@
<?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 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,53 @@
<?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
*/
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 fragment.
*/
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,254 @@
<?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 Deprecated;
use Iterator;
use IteratorAggregate;
use Stringable;
/**
* @extends IteratorAggregate<array{0:string, 1:string|null}>
*
* @method self withoutPairByKey(string ...$keys) Returns an instance without pairs with the specified keys.
* @method self withoutPairByValue(Stringable|string|int|bool|null ...$values) Returns an instance without pairs with the specified values.
* @method self withoutPairByKeyValue(string $key, Stringable|string|int|bool|null $value) 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 ?string toFormData() Returns the string representation using the applicat/www-form-urlencoded rules
* @method ?string toRFC3986() Returns the string representation using RFC3986 rules
* @method self normalize() returns the normalized string representation of the component
*/
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,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;
}
+468
View File
@@ -0,0 +1,468 @@
<?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 Closure;
use Deprecated;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\IPv6\Converter as IPv6Converter;
use SensitiveParameter;
use Stringable;
use function explode;
use function filter_var;
use function gettype;
use function in_array;
use function is_scalar;
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[1-9A-Fa-f]|[3-7][0-9A-Fa-f]|61|62|64|65|66|7[AB]|5F),';
/**
* Tell whether the user component is correctly encoded.
*/
public static function isUserEncoded(Stringable|string|null $encoded): bool
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.']+|'.self::REGEXP_PART_ENCODED.'/';
return null === $encoded || 1 !== preg_match($pattern, (string) $encoded);
}
/**
* Encode User.
*
* All generic delimiters MUST be encoded
*/
public static function encodeUser(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 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(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] Stringable|string|null $encoded): bool
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':]+|'.self::REGEXP_PART_ENCODED.'/';
return null === $encoded || 1 !== preg_match($pattern, (string) $encoded);
}
/**
* Encode Password.
*
* Generic delimiters ":" MUST NOT be encoded
*/
public static function encodePassword(#[SensitiveParameter] 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] 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] Stringable|string|null $userInfo): bool
{
if (null === $userInfo) {
return true;
}
[$user, $password] = explode(':', (string) $userInfo, 2) + [1 => null];
return self::isUserEncoded($user)
&& self::isPasswordEncoded($password);
}
public static function encodeUserInfo(#[SensitiveParameter] Stringable|string|null $userInfo): ?string
{
if (null === $userInfo) {
return null;
}
[$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] Stringable|string|null $userInfo): ?string
{
if (null === $userInfo) {
return null;
}
[$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(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(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(Stringable|string|null $str): ?string
{
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(Stringable|string|null $encoded): bool
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':@\/]+|'.self::REGEXP_PART_ENCODED.'/';
return null === $encoded || 1 !== preg_match($pattern, (string) $encoded);
}
/**
* Encode Path.
*
* Generic delimiters ":", "@", and "/" MUST NOT be encoded
*/
public static function encodePath(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(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(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(Stringable|string|null $encoded): bool
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.'\/?%]+|'.self::REGEXP_PART_ENCODED.'/';
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(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(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(Stringable|string|null $encoded): bool
{
static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':@\/?%]|'.self::REGEXP_PART_ENCODED.'/';
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(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(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(Stringable|string|null $host): ?string
{
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(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
{
return match (true) {
true === $component => '1',
false === $component => '0',
$component instanceof UriComponentInterface => $component->value(),
$component instanceof Stringable,
is_scalar($component) => (string) $component,
null === $component => null,
default => throw new SyntaxError(sprintf('The component must be a scalar value `%s` given.', gettype($component))),
};
}
/**
* Encodes the URI component characters using a regular expression to find which characters need encoding.
*/
private static function encode(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(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(Stringable|string|int|null $component): ?string
{
return self::decodeNecessary($component);
}
}
@@ -0,0 +1,46 @@
<?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\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(Stringable|string $host, Result $result): self
{
$reasons = array_map(fn (Error $error): string => $error->description(), $result->errors());
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,65 @@
<?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.');
}
}
@@ -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;
}
@@ -0,0 +1,441 @@
<?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 Exception;
use JsonSerializable;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\Idna\Converter as IdnConverter;
use Stringable;
use Throwable;
use function array_key_first;
use function count;
use function explode;
use function filter_var;
use function get_object_vars;
use function in_array;
use function inet_pton;
use function is_object;
use function preg_match;
use function rawurldecode;
use function strpos;
use function strtolower;
use function substr;
use const FILTER_FLAG_IPV4;
use const FILTER_FLAG_IPV6;
use const FILTER_VALIDATE_IP;
/**
* @phpstan-type HostRecordSerializedShape array{0: array{host: ?string}, 1: array{}}
*/
final class HostRecord implements JsonSerializable
{
/**
* Maximum number of host cached.
*
* @var int
*/
private const MAXIMUM_HOST_CACHED = 100;
private const REGEXP_NON_ASCII_PATTERN = '/[^\x20-\x7f]/';
/**
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
*
* invalid characters in host regular expression
*/
private const REGEXP_INVALID_HOST_CHARS = '/
[:\/?#\[\]@ ] # gen-delims characters as well as the space character
/ix';
/**
* General registered name regular expression.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
* @see https://regex101.com/r/fptU8V/1
*/
private const REGEXP_REGISTERED_NAME = '/
(?(DEFINE)
(?<unreserved>[a-z0-9_~\-]) # . is missing as it is used to separate labels
(?<sub_delims>[!$&\'()*+,;=])
(?<encoded>%[A-F0-9]{2})
(?<reg_name>(?:(?&unreserved)|(?&sub_delims)|(?&encoded))*)
)
^(?:(?&reg_name)\.)*(?&reg_name)\.?$
/ix';
/**
* Domain name regular expression.
*
* Everything but the domain name length is validated
*
* @see https://tools.ietf.org/html/rfc1034#section-3.5
* @see https://tools.ietf.org/html/rfc1123#section-2.1
* @see https://regex101.com/r/71j6rt/1
*/
private const REGEXP_DOMAIN_NAME = '/
(?(DEFINE)
(?<let_dig> [a-z0-9]) # alpha digit
(?<let_dig_hyp> [a-z0-9-]) # alpha digit and hyphen
(?<ldh_str> (?&let_dig_hyp){0,61}(?&let_dig)) # domain label end
(?<label> (?&let_dig)((?&ldh_str))?) # domain label
(?<domain> (?&label)(\.(?&label)){0,126}\.?) # domain name
)
^(?&domain)$
/ix';
/**
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
*
* IPvFuture regular expression
*/
private const REGEXP_IP_FUTURE = '/^
v(?<version>[A-F\d])+\.
(?:
(?<unreserved>[a-z\d_~\-\.])|
(?<sub_delims>[!$&\'()*+,;=:]) # also include the : character
)+
$/ix';
private const REGEXP_GEN_DELIMS = '/[:\/?#\[\]@ ]/';
private const ADDRESS_BLOCK = "\xfe\x80";
private ?bool $isDomainName = null;
private ?bool $hasZoneIdentifier = null;
private bool $asciiIsLoaded = false;
private ?string $hostAsAscii = null;
private bool $unicodeIsLoaded = false;
private ?string $hostAsUnicode = null;
private bool $isIpVersionLoaded = false;
private ?string $ipVersion = null;
private bool $isIpValueLoaded = false;
private ?string $ipValue = null;
private function __construct(
public readonly ?string $value,
public readonly HostType $type,
public readonly HostFormat $format
) {
}
public function hasZoneIdentifier(): bool
{
return $this->hasZoneIdentifier ??= HostType::Ipv6 === $this->type && str_contains((string) $this->value, '%');
}
public function toAscii(): ?string
{
if (!$this->asciiIsLoaded) {
$this->asciiIsLoaded = true;
$this->hostAsAscii = (function (): ?string {
if (HostType::RegisteredName !== $this->type || null === $this->value) {
return $this->value;
}
$formattedHost = rawurldecode($this->value);
if ($formattedHost === $this->value) {
return $this->isDomainType() ? IdnConverter::toAscii($this->value)->domain() : strtolower($formattedHost);
}
return Encoder::normalizeHost($this->value);
})();
}
return $this->hostAsAscii;
}
public function toUnicode(): ?string
{
if (!$this->unicodeIsLoaded) {
$this->unicodeIsLoaded = true;
$this->hostAsUnicode = $this->isDomainType() && null !== $this->value ? IdnConverter::toUnicode($this->value)->domain() : $this->value;
}
return $this->hostAsUnicode;
}
public function isDomainType(): bool
{
return $this->isDomainName ??= match (true) {
HostType::RegisteredName !== $this->type, '' === $this->value => false,
null === $this->value => true,
default => is_object($result = IdnConverter::toAscii($this->value))
&& !$result->hasErrors()
&& self::isValidDomain($result->domain()),
};
}
public function ipVersion(): ?string
{
if (!$this->isIpVersionLoaded) {
$this->isIpVersionLoaded = true;
$this->ipVersion = match (true) {
HostType::Ipv4 === $this->type => '4',
HostType::Ipv6 === $this->type => '6',
1 === preg_match(self::REGEXP_IP_FUTURE, substr((string) $this->value, 1, -1), $matches) => $matches['version'],
default => null,
};
}
return $this->ipVersion;
}
public function ipValue(): ?string
{
if (!$this->isIpValueLoaded) {
$this->isIpValueLoaded = true;
$this->ipValue = (function (): ?string {
if (HostType::RegisteredName === $this->type) {
return null;
}
if (HostType::Ipv4 === $this->type) {
return $this->value;
}
$ip = substr((string) $this->value, 1, -1);
if (HostType::Ipv6 !== $this->type) {
return substr($ip, (int) strpos($ip, '.') + 1);
}
$pos = strpos($ip, '%');
if (false === $pos) {
return $ip;
}
return substr($ip, 0, $pos).'%'.rawurldecode(substr($ip, $pos + 3));
})();
}
return $this->ipValue;
}
public static function isValid(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::isRegisteredName($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(Stringable|string|null $host): self
{
if ($host instanceof UriComponentInterface) {
$host = $host->value();
}
if (null === $host) {
return new self(
value: null,
type: HostType::RegisteredName,
format: HostFormat::Ascii,
);
}
$host = (string) $host;
if ('' === $host) {
return new self(
value: '',
type: HostType::RegisteredName,
format: HostFormat::Ascii,
);
}
static $inMemoryCache = [];
if (isset($inMemoryCache[$host])) {
return $inMemoryCache[$host];
}
if (self::MAXIMUM_HOST_CACHED < count($inMemoryCache)) {
unset($inMemoryCache[array_key_first($inMemoryCache)]);
}
if ($host === filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return $inMemoryCache[$host] = new self(
value: $host,
type: HostType::Ipv4,
format: HostFormat::Ascii,
);
}
if (str_starts_with($host, '[')) {
str_ends_with($host, ']') || throw new SyntaxError('The host '.$host.' is not a valid IPv6 host.');
$ipHost = substr($host, 1, -1);
if (1 === preg_match(self::REGEXP_IP_FUTURE, $ipHost, $matches)) {
return !in_array($matches['version'], ['4', '6'], true) ? ($inMemoryCache[$host] = new self(
value: $host,
type: HostType::IpvFuture,
format: HostFormat::Ascii,
)) : throw new SyntaxError('The host '.$host.' is not a valid IPvFuture host.');
}
if (self::isValidIpv6Hostname($ipHost)) {
return $inMemoryCache[$host] = new self(
value: $host,
type: HostType::Ipv6,
format: HostFormat::Ascii,
);
}
throw new SyntaxError('The host '.$host.' is not a valid IPv6 host.');
}
$domainName = rawurldecode($host);
$format = HostFormat::Unicode;
if (1 !== preg_match(self::REGEXP_NON_ASCII_PATTERN, $domainName)) {
$domainName = strtolower($domainName);
$format = HostFormat::Ascii;
}
if (1 === preg_match(self::REGEXP_REGISTERED_NAME, $domainName)) {
return $inMemoryCache[$host] = new self(
value: $host,
type: HostType::RegisteredName,
format: $format,
);
}
(HostFormat::Ascii !== $format && 1 !== preg_match(self::REGEXP_INVALID_HOST_CHARS, $domainName)) || throw new SyntaxError('`'.$host.'` is an invalid domain name : the host contains invalid characters.');
IdnConverter::toAsciiOrFail($domainName);
return $inMemoryCache[$host] = new self(
value: $host,
type: HostType::RegisteredName,
format: $format,
);
}
/**
* Tells whether the registered name is a valid domain name according to RFC1123.
*
* @see http://man7.org/linux/man-pages/man7/hostname.7.html
* @see https://tools.ietf.org/html/rfc1123#section-2.1
*/
private static function isValidDomain(string $hostname): bool
{
$domainMaxLength = str_ends_with($hostname, '.') ? 254 : 253;
return !isset($hostname[$domainMaxLength])
&& 1 === preg_match(self::REGEXP_DOMAIN_NAME, $hostname);
}
/**
* Validates an Ipv6 as Host.
*
* @see http://tools.ietf.org/html/rfc6874#section-2
* @see http://tools.ietf.org/html/rfc6874#section-4
*/
private static function isValidIpv6Hostname(string $host): bool
{
[$ipv6, $scope] = explode('%', $host, 2) + [1 => null];
if (null === $scope) {
return (bool) filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
}
$scope = rawurldecode('%'.$scope);
return 1 !== preg_match(self::REGEXP_NON_ASCII_PATTERN, $scope)
&& 1 !== preg_match(self::REGEXP_GEN_DELIMS, $scope)
&& false !== filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)
&& str_starts_with((string)inet_pton((string)$ipv6), self::ADDRESS_BLOCK);
}
public function jsonSerialize(): ?string
{
return $this->value;
}
/**
* @return HostRecordSerializedShape
*/
public function __serialize(): array
{
return [['host' => $this->value], []];
}
/**
* @param HostRecordSerializedShape $data
*
* @throws Exception|SyntaxError
*/
public function __unserialize(array $data): void
{
[$properties] = $data;
$record = self::from($properties['host'] ?? throw new Exception('The `host` property is missing from the serialized object.'));
//if the Host computed value are already cache this avoid recomputing them
foreach (get_object_vars($record) as $prop => $value) {
/** @phpstan-ignore-next-line */
$this->{$prop} = $value;
}
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
enum HostType
{
case RegisteredName;
case Ipv4;
case Ipv6;
case IpvFuture;
}
@@ -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);
}
@@ -0,0 +1,309 @@
<?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 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(Stringable|string|null $host): bool
{
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(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(Stringable|string|null $host): ?string
{
$host = $this->toDecimal($host);
if (null === $host) {
return null;
}
return '['.self::IPV4_MAPPED_PREFIX.$host.']';
}
public function toOctal(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(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(Stringable|string|null $host): ?string
{
$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;
}
}
@@ -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\IPv6;
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(string $ipAddress): string
{
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(string $ipAddress): string
{
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(Stringable|string|null $host): ?string
{
$components = self::parse($host);
if (null === $components['ipAddress']) {
return match ($host) {
null => $host,
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(Stringable|string|null $host): array
{
if (null === $host) {
return ['ipAddress' => null, 'zoneIdentifier' => null];
}
$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(Stringable|string|null $host): bool
{
return null !== self::parse($host)['ipAddress'];
}
public static function normalize(Stringable|string|null $host): ?string
{
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);
}
}
@@ -0,0 +1,219 @@
<?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 League\Uri\Exceptions\ConversionFailed;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\FeatureDetection;
use Stringable;
use function idn_to_ascii;
use function idn_to_utf8;
use function rawurldecode;
use function strtolower;
use const INTL_IDNA_VARIANT_UTS46;
/**
* @see https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/uidna_8h.html
*/
final class Converter
{
private const REGEXP_IDNA_PATTERN = '/[^\x20-\x7f]/';
private const MAX_DOMAIN_LENGTH = 253;
private const MAX_LABEL_LENGTH = 63;
/**
* General registered name regular expression.
*
* @see https://tools.ietf.org/html/rfc3986#section-3.2.2
* @see https://regex101.com/r/fptU8V/1
*/
private const REGEXP_REGISTERED_NAME = '/
(?(DEFINE)
(?<unreserved>[a-z0-9_~\-]) # . is missing as it is used to separate labels
(?<sub_delims>[!$&\'()*+,;=])
(?<encoded>%[A-F0-9]{2})
(?<reg_name>(?:(?&unreserved)|(?&sub_delims)|(?&encoded))*)
)
^(?:(?&reg_name)\.)*(?&reg_name)\.?$
/ix';
/**
* Converts the input to its IDNA ASCII form or throw on failure.
*
* @see Converter::toAscii()
*
* @throws SyntaxError if the string cannot be converted to UNICODE using IDN UTS46 algorithm
* @throws ConversionFailed if the conversion returns error
*/
public static function toAsciiOrFail(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(Stringable|string $domain, Option|int|null $options = null): Result
{
$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(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(Stringable|string $domain, Option|int|null $options = null): Result
{
$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(Stringable|string|null $domain): bool
{
$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;
}
}
@@ -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)
)
);
}
}
@@ -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),
};
}
}
@@ -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,209 @@
<?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 League\Uri\Contracts\UriComponentInterface;
use League\Uri\Exceptions\SyntaxError;
use Stringable;
use function array_combine;
use function explode;
use function implode;
use function is_float;
use function is_int;
use function is_string;
use function json_encode;
use function preg_match;
use function str_replace;
use const JSON_PRESERVE_ZERO_FRACTION;
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(Stringable|string|int|float|bool|null $value): array
{
$value = match (true) {
$value instanceof UriComponentInterface => $value->value(),
$value instanceof Stringable, is_int($value) => (string) $value,
false === $value => '0',
true === $value => '1',
default => $value,
};
if (null === $value) {
return [];
}
$value = match (1) {
preg_match(self::REGEXP_INVALID_CHARS, (string) $value) => throw new SyntaxError('Invalid query string: `'.$value.'`.'),
default => str_replace($this->toEncoding, $this->fromRfc3986, (string) $value),
};
return array_map(
fn (string $pair): array => explode('=', $pair, 2) + [1 => null],
explode($this->separator, $value)
);
}
private static function vString(Stringable|string|bool|int|float|null $value): ?string
{
return match (true) {
$value => '1',
false === $value => '0',
null === $value => null,
is_float($value) => (string) json_encode($value, JSON_PRESERVE_ZERO_FRACTION),
default => (string) $value,
};
}
/**
* @param iterable<array{0:string|null, 1: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] => self::vString($pair[0]),
default => self::vString($pair[0]).'='.self::vString($pair[1]),
};
}
return match ([]) {
$filteredPairs => null,
default => str_replace($this->fromRfc3986, $this->toEncoding, implode($this->separator, $filteredPairs)),
};
}
/**
* @param non-empty-string $separator
*/
public function withSeparator(string $separator): self
{
return match ($this->separator) {
$separator => $this,
default => new self($separator, $this->fromRfc3986, $this->toEncoding),
};
}
/**
* Sets the conversion map.
*
* Each key from the iterable structure represents the RFC3986 encoded characters as string,
* while each value represents the expected output encoded characters
*/
public function withEncodingMap(iterable $encodingMap): self
{
$fromRfc3986 = [];
$toEncoding = [];
foreach ($encodingMap as $from => $to) {
[$fromRfc3986[], $toEncoding[]] = match (true) {
!is_string($from) => throw new SyntaxError('The encoding output must be a string; `'.gettype($from).'` given.'),
$to instanceof Stringable,
is_string($to) => [$from, (string) $to],
default => throw new SyntaxError('The encoding output must be a string; `'.gettype($to).'` given.'),
};
}
return match (true) {
$fromRfc3986 !== $this->fromRfc3986,
$toEncoding !== $this->toEncoding => new self($this->separator, $fromRfc3986, $toEncoding),
default => $this,
};
}
}
+20
View File
@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2015 ignace nyamagana butera
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,276 @@
<?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 League\Uri\Exceptions\SyntaxError;
use League\Uri\KeyValuePair\Converter;
use Stringable;
use function array_key_exists;
use function array_keys;
use function is_array;
use function rawurldecode;
use function strpos;
use function substr;
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;
/**
* @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:string|float|int|bool|null}> $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): ?string
{
return self::buildFromPairs($pairs, Converter::fromEncodingType($encType)->withSeparator($separator));
}
/**
* 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:string|float|int|bool|null}> $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): ?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.');
}
$keyValuePairs[] = [(string) Encoder::encodeQueryKeyValue($pair[0]), match(null) {
$pair[1] => null,
default => Encoder::encodeQueryKeyValue($pair[1]),
}];
}
return ($converter ?? Converter::fromRFC3986())->toValue($keyValuePairs);
}
/**
* Parses the query string like parse_str without mangling the results.
*
* @see QueryString::extractFromValue()
* @see http://php.net/parse_str
* @see https://wiki.php.net/rfc/on_demand_name_mangling
*
* @param non-empty-string $separator
*
* @throws SyntaxError
*/
public static function extract(Stringable|string|bool|null $query, string $separator = '&', int $encType = PHP_QUERY_RFC3986): array
{
return self::extractFromValue($query, Converter::fromEncodingType($encType)->withSeparator($separator));
}
/**
* Parses the query string like parse_str without mangling the results.
*
* 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
*
* @throws SyntaxError
*/
public static function extractFromValue(Stringable|string|bool|null $query, ?Converter $converter = null): array
{
return self::convert(self::decodePairs(
($converter ?? Converter::fromRFC3986())->toPairs($query),
self::PAIR_VALUE_PRESERVED
));
}
/**
* 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(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(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): array
{
$returnedValue = [];
foreach ($pairs as $pair) {
$returnedValue = self::extractPhpVariable($returnedValue, $pair);
}
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 = ''): array
{
if (is_array($name)) {
[$name, $value] = $name;
$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);
return $data;
}
}
@@ -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;
}
+721
View File
@@ -0,0 +1,721 @@
<?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 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(Stringable|string $uri): string
{
$components = UriString::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 = UriString::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(Stringable|string $uri, Stringable|string|null $baseUri = null): string
{
$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 && (string) $uri !== (string) $baseUri) {
$baseUriComponents = self::parse($baseUri);
}
$hasLeadingSlash = str_starts_with($baseUriComponents['path'], '/');
if (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.');
}
if (null !== $uriComponents['scheme'] && '' !== $uriComponents['scheme']) {
$uriComponents['path'] = self::removeDotSegments($uriComponents['path']);
return UriString::build($uriComponents);
}
if (null !== self::buildAuthority($uriComponents)) {
$uriComponents['scheme'] = $baseUriComponents['scheme'];
$uriComponents['path'] = self::removeDotSegments($uriComponents['path']);
return UriString::build($uriComponents);
}
[$path, $query] = self::resolvePathAndQuery($uriComponents, $baseUriComponents);
$path = UriString::removeDotSegments($path);
if ('' !== $path && '/' !== $path[0] && $hasLeadingSlash) {
$path = '/'.$path;
}
$baseUriComponents['path'] = $path;
$baseUriComponents['query'] = $query;
$baseUriComponents['fragment'] = $uriComponents['fragment'];
return UriString::build($baseUriComponents);
}
/**
* 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(Stringable|string|int $uri): array
{
$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(Stringable|string|null $authority): array
{
$components = ['user' => null, 'pass' => null, 'host' => null, 'port' => null];
if (null === $authority) {
return $components;
}
$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(Stringable|string|null $scheme): bool
{
return null === $scheme || 1 === preg_match('/^[A-Za-z]([-A-Za-z\d+.]+)?$/', (string) $scheme);
}
private static function normalizeHost(?string $host): ?string
{
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;
}
@@ -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 WHATWG URL"
},
"extra": {
"branch-alias": {
"dev-master": "7.x-dev"
}
},
"support": {
"forum": "https://thephpleague.slack.com",
"docs": "https://uri.thephpleague.com",
"issues": "https://github.com/thephpleague/uri-src/issues"
},
"config": {
"sort-packages": true
}
}
+646
View File
@@ -0,0 +1,646 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
use Deprecated;
use JsonSerializable;
use League\Uri\Contracts\UriAccess;
use League\Uri\Contracts\UriInterface;
use League\Uri\Exceptions\MissingFeature;
use League\Uri\Idna\Converter as IdnaConverter;
use League\Uri\IPv4\Converter as IPv4Converter;
use League\Uri\IPv6\Converter as IPv6Converter;
use Psr\Http\Message\UriFactoryInterface;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use function array_pop;
use function array_reduce;
use function count;
use function explode;
use function implode;
use function in_array;
use function preg_match;
use function rawurldecode;
use function sort;
use function str_contains;
use function str_repeat;
use function str_replace;
use function strpos;
use function substr;
/**
* @phpstan-import-type ComponentMap from UriInterface
* @deprecated since version 7.6.0
*
* @see Modifier
* @see Uri
*/
class BaseUri implements Stringable, JsonSerializable, UriAccess
{
/** @var array<string,int> */
final protected const WHATWG_SPECIAL_SCHEMES = ['ftp' => 1, 'http' => 1, 'https' => 1, 'ws' => 1, 'wss' => 1];
/** @var array<string,int> */
final protected const DOT_SEGMENTS = ['.' => 1, '..' => 1];
protected readonly Psr7UriInterface|UriInterface|null $origin;
protected readonly ?string $nullValue;
/**
* @param UriFactoryInterface|null $uriFactory Deprecated, will be removed in the next major release
*/
final protected function __construct(
protected readonly Psr7UriInterface|UriInterface $uri,
protected readonly ?UriFactoryInterface $uriFactory
) {
$this->nullValue = $this->uri instanceof Psr7UriInterface ? '' : null;
$this->origin = $this->computeOrigin($this->uri, $this->nullValue);
}
public static function from(Stringable|string $uri, ?UriFactoryInterface $uriFactory = null): static
{
$uri = static::formatHost(static::filterUri($uri, $uriFactory));
return new static($uri, $uriFactory);
}
public function withUriFactory(UriFactoryInterface $uriFactory): static
{
return new static($this->uri, $uriFactory);
}
public function withoutUriFactory(): static
{
return new static($this->uri, null);
}
public function getUri(): Psr7UriInterface|UriInterface
{
return $this->uri;
}
public function getUriString(): string
{
return $this->uri->__toString();
}
public function jsonSerialize(): string
{
return $this->uri->__toString();
}
public function __toString(): string
{
return $this->uri->__toString();
}
public function origin(): ?self
{
return match (null) {
$this->origin => null,
default => new self($this->origin, $this->uriFactory),
};
}
/**
* Returns the Unix filesystem path.
*
* The method will return null if a scheme is present and is not the `file` scheme
*/
public function unixPath(): ?string
{
return match ($this->uri->getScheme()) {
'file', $this->nullValue => rawurldecode($this->uri->getPath()),
default => null,
};
}
/**
* Returns the Windows filesystem path.
*
* The method will return null if a scheme is present and is not the `file` scheme
*/
public function windowsPath(): ?string
{
static $regexpWindowsPath = ',^(?<root>[a-zA-Z]:),';
if (!in_array($this->uri->getScheme(), ['file', $this->nullValue], true)) {
return null;
}
$originalPath = $this->uri->getPath();
$path = $originalPath;
if ('/' === ($path[0] ?? '')) {
$path = substr($path, 1);
}
if (1 === preg_match($regexpWindowsPath, $path, $matches)) {
$root = $matches['root'];
$path = substr($path, strlen($root));
return $root.str_replace('/', '\\', rawurldecode($path));
}
$host = $this->uri->getHost();
return match ($this->nullValue) {
$host => str_replace('/', '\\', rawurldecode($originalPath)),
default => '\\\\'.$host.'\\'.str_replace('/', '\\', rawurldecode($path)),
};
}
/**
* Returns a string representation of a File URI according to RFC8089.
*
* The method will return null if the URI scheme is not the `file` scheme
*/
public function toRfc8089(): ?string
{
$path = $this->uri->getPath();
return match (true) {
'file' !== $this->uri->getScheme() => null,
in_array($this->uri->getAuthority(), ['', null, 'localhost'], true) => 'file:'.match (true) {
'' === $path,
'/' === $path[0] => $path,
default => '/'.$path,
},
default => (string) $this->uri,
};
}
/**
* Tells whether the `file` scheme base URI represents a local file.
*/
public function isLocalFile(): bool
{
return match (true) {
'file' !== $this->uri->getScheme() => false,
in_array($this->uri->getAuthority(), ['', null, 'localhost'], true) => true,
default => false,
};
}
/**
* Tells whether the URI is opaque or not.
*
* A URI is opaque if and only if it is absolute
* and does not have an authority path.
*/
public function isOpaque(): bool
{
return $this->nullValue === $this->uri->getAuthority()
&& $this->isAbsolute();
}
/**
* Tells whether two URI do not share the same origin.
*/
public function isCrossOrigin(Stringable|string $uri): bool
{
if (null === $this->origin) {
return true;
}
$uri = static::filterUri($uri);
$uriOrigin = $this->computeOrigin($uri, $uri instanceof Psr7UriInterface ? '' : null);
return match(true) {
null === $uriOrigin,
$uriOrigin->__toString() !== $this->origin->__toString() => true,
default => false,
};
}
/**
* Tells whether the URI is absolute.
*/
public function isAbsolute(): bool
{
return $this->nullValue !== $this->uri->getScheme();
}
/**
* Tells whether the URI is a network path.
*/
public function isNetworkPath(): bool
{
return $this->nullValue === $this->uri->getScheme()
&& $this->nullValue !== $this->uri->getAuthority();
}
/**
* Tells whether the URI is an absolute path.
*/
public function isAbsolutePath(): bool
{
return $this->nullValue === $this->uri->getScheme()
&& $this->nullValue === $this->uri->getAuthority()
&& '/' === ($this->uri->getPath()[0] ?? '');
}
/**
* Tells whether the URI is a relative path.
*/
public function isRelativePath(): bool
{
return $this->nullValue === $this->uri->getScheme()
&& $this->nullValue === $this->uri->getAuthority()
&& '/' !== ($this->uri->getPath()[0] ?? '');
}
/**
* Tells whether both URI refers to the same document.
*/
public function isSameDocument(Stringable|string $uri): bool
{
return self::normalizedUri($this->uri)->equals(self::normalizedUri($uri));
}
private static function normalizedUri(Stringable|string $uri): Uri
{
// Normalize the URI according to RFC3986
$uri = ($uri instanceof Uri ? $uri : Uri::new($uri))->normalize();
return $uri
//Normalization as per WHATWG URL standard
//only meaningful for WHATWG Special URI scheme protocol
->when(
condition: '' === $uri->getPath() && null !== $uri->getAuthority(),
onSuccess: fn (Uri $uri) => $uri->withPath('/'),
)
//Sorting as per WHATWG URLSearchParams class
//not included on any equivalence algorithm
->when(
condition: null !== ($query = $uri->getQuery()) && str_contains($query, '&'),
onSuccess: function (Uri $uri) use ($query) {
$pairs = explode('&', (string) $query);
sort($pairs);
return $uri->withQuery(implode('&', $pairs));
}
);
}
/**
* Tells whether the URI contains an Internationalized Domain Name (IDN).
*/
public function hasIdn(): bool
{
return IdnaConverter::isIdn($this->uri->getHost());
}
/**
* Tells whether the URI contains an IPv4 regardless if it is mapped or native.
*/
public function hasIPv4(): bool
{
return IPv4Converter::fromEnvironment()->isIpv4($this->uri->getHost());
}
/**
* Resolves a URI against a base URI using RFC3986 rules.
*
* This method MUST retain the state of the submitted URI instance, and return
* a URI instance of the same type that contains the applied modifications.
*
* This method MUST be transparent when dealing with error and exceptions.
* It MUST not alter or silence them apart from validating its own parameters.
*/
public function resolve(Stringable|string $uri): static
{
$resolved = UriString::resolve($uri, $this->uri->__toString());
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()];
}
}
+380
View File
@@ -0,0 +1,380 @@
<?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\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
{
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 withScheme(string $scheme): self
{
return $this->newInstance($this->uri->withScheme($this->filterInput($scheme)));
}
public function withUserInfo(string $user, ?string $password = null): self
{
return $this->newInstance($this->uri->withUserInfo($this->filterInput($user), $password));
}
public function withHost(string $host): self
{
return $this->newInstance($this->uri->withHost($this->filterInput($host)));
}
public function withPort(?int $port): self
{
return $this->newInstance($this->uri->withPort($port));
}
public function withPath(string $path): self
{
return $this->newInstance($this->uri->withPath($path));
}
public function withQuery(string $query): self
{
return $this->newInstance($this->uri->withQuery($this->filterInput($query)));
}
public function withFragment(string $fragment): self
{
return $this->newInstance($this->uri->withFragment($this->filterInput($fragment)));
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.6.0
* @codeCoverageIgnore
* @see Http::parse()
*
* Create a new instance from a URI and a Base URI.
*
* The returned URI must be absolute.
*/
#[Deprecated(message:'use League\Uri\Http::parse() instead', since:'league/uri:7.6.0')]
public static function fromBaseUri(Rfc3986Uri|WhatwgUrl|Stringable|string $uri, Rfc3986Uri|WhatwgUrl|Stringable|string|null $baseUri = null): self
{
return new self(Uri::fromBaseUri($uri, $baseUri));
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @codeCoverageIgnore
* @see Http::new()
*
* Create a new instance from a string.
*/
#[Deprecated(message:'use League\Uri\Http::new() instead', since:'league/uri:7.0.0')]
public static function createFromString(Stringable|string $uri = ''): self
{
return self::new($uri);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @codeCoverageIgnore
* @see Http::fromComponents()
*
* Create a new instance from a hash of parse_url parts.
*
* @param InputComponentMap $components a hash representation of the URI similar
* to PHP parse_url function result
*/
#[Deprecated(message:'use League\Uri\Http::fromComponents() instead', since:'league/uri:7.0.0')]
public static function createFromComponents(array $components): self
{
return self::fromComponents($components);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @codeCoverageIgnore
* @see Http::fromServer()
*
* Create a new instance from the environment.
*/
#[Deprecated(message:'use League\Uri\Http::fromServer() instead', since:'league/uri:7.0.0')]
public static function createFromServer(array $server): self
{
return self::fromServer($server);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @codeCoverageIgnore
* @see Http::new()
*
* Create a new instance from a URI object.
*/
#[Deprecated(message:'use League\Uri\Http::new() instead', since:'league/uri:7.0.0')]
public static function createFromUri(Psr7UriInterface|UriInterface $uri): self
{
return self::new($uri);
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.0.0
* @codeCoverageIgnore
* @see Http::fromBaseUri()
*
* Create a new instance from a URI and a Base URI.
*
* The returned URI must be absolute.
*/
#[Deprecated(message:'use League\Uri\Http::fromBaseUri() instead', since:'league/uri:7.0.0')]
public static function createFromBaseUri(Stringable|string $uri, Stringable|string|null $baseUri = null): self
{
return self::fromBaseUri($uri, $baseUri);
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
use Psr\Http\Message\UriFactoryInterface;
use Psr\Http\Message\UriInterface;
final class HttpFactory implements UriFactoryInterface
{
public function createUri(string $uri = ''): UriInterface
{
return Http::new($uri);
}
}
+20
View File
@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2015 ignace nyamagana butera
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+36
View File
@@ -0,0 +1,36 @@
<?php
/**
* League.Uri (https://uri.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace League\Uri;
enum SchemeType
{
case Opaque;
case Hierarchical;
case Unknown;
public function isOpaque(): bool
{
return self::Opaque === $this;
}
public function isHierarchical(): bool
{
return self::Hierarchical === $this;
}
public function isUnknown(): bool
{
return self::Unknown === $this;
}
}
File diff suppressed because it is too large Load Diff
+105
View File
@@ -0,0 +1,105 @@
<?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 League\Uri\Contracts\UriInterface;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
/**
* @deprecated since version 7.0.0
* @codeCoverageIgnore
* @see BaseUri
*/
final class UriInfo
{
/**
* @codeCoverageIgnore
*/
private function __construct()
{
}
/**
* Tells whether the URI represents an absolute URI.
*/
#[Deprecated(message:'use League\Uri\BaseUri::isAbsolute() instead', since:'league/uri:7.0.0')]
public static function isAbsolute(Psr7UriInterface|UriInterface $uri): bool
{
return BaseUri::from($uri)->isAbsolute();
}
/**
* Tell whether the URI represents a network path.
*/
#[Deprecated(message:'use League\Uri\BaseUri::isNetworkPath() instead', since:'league/uri:7.0.0')]
public static function isNetworkPath(Psr7UriInterface|UriInterface $uri): bool
{
return BaseUri::from($uri)->isNetworkPath();
}
/**
* Tells whether the URI represents an absolute path.
*/
#[Deprecated(message:'use League\Uri\BaseUri::isAbsolutePath() instead', since:'league/uri:7.0.0')]
public static function isAbsolutePath(Psr7UriInterface|UriInterface $uri): bool
{
return BaseUri::from($uri)->isAbsolutePath();
}
/**
* Tell whether the URI represents a relative path.
*
*/
#[Deprecated(message:'use League\Uri\BaseUri::isRelativePath() instead', since:'league/uri:7.0.0')]
public static function isRelativePath(Psr7UriInterface|UriInterface $uri): bool
{
return BaseUri::from($uri)->isRelativePath();
}
/**
* Tells whether both URI refers to the same document.
*/
#[Deprecated(message:'use League\Uri\BaseUri::isSameDocument() instead', since:'league/uri:7.0.0')]
public static function isSameDocument(Psr7UriInterface|UriInterface $uri, Psr7UriInterface|UriInterface $baseUri): bool
{
return BaseUri::from($baseUri)->isSameDocument($uri);
}
/**
* Returns the URI origin property as defined by WHATWG URL living standard.
*
* {@see https://url.spec.whatwg.org/#origin}
*
* For URI without a special scheme the method returns null
* For URI with the file scheme the method will return null (as this is left to the implementation decision)
* For URI with a special scheme the method returns the scheme followed by its authority (without the userinfo part)
*/
#[Deprecated(message:'use League\Uri\BaseUri::origin() instead', since:'league/uri:7.0.0')]
public static function getOrigin(Psr7UriInterface|UriInterface $uri): ?string
{
return BaseUri::from($uri)->origin()?->__toString();
}
/**
* Tells whether two URI do not share the same origin.
*
* @see UriInfo::getOrigin()
*/
#[Deprecated(message:'use League\Uri\BaseUri::isCrossOrigin() instead', since:'league/uri:7.0.0')]
public static function isCrossOrigin(Psr7UriInterface|UriInterface $uri, Psr7UriInterface|UriInterface $baseUri): bool
{
return BaseUri::from($baseUri)->isCrossOrigin($uri);
}
}
+56
View File
@@ -0,0 +1,56 @@
<?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 League\Uri\Contracts\UriInterface;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
/**
* @deprecated since version 7.0.0
* @codeCoverageIgnore
* @see BaseUri
*/
final class UriResolver
{
/**
* 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.
*/
#[Deprecated(message:'use League\Uri\BaseUri::resolve() instead', since:'league/uri:7.0.0')]
public static function resolve(Psr7UriInterface|UriInterface $uri, Psr7UriInterface|UriInterface $baseUri): Psr7UriInterface|UriInterface
{
return BaseUri::from($baseUri)->resolve($uri)->getUri();
}
/**
* Relativizes 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 or silence them apart from validating its own parameters.
*/
#[Deprecated(message:'use League\Uri\BaseUri::relativize() instead', since:'league/uri:7.0.0')]
public static function relativize(Psr7UriInterface|UriInterface $uri, Psr7UriInterface|UriInterface $baseUri): Psr7UriInterface|UriInterface
{
return BaseUri::from($baseUri)->relativize($uri)->getUri();
}
}
+194
View File
@@ -0,0 +1,194 @@
<?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 ValueError;
/*
* Supported schemes and corresponding default port.
*
* @see https://github.com/python-hyper/hyperlink/blob/master/src/hyperlink/_url.py for the curating list definition
* @see https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
* @see https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
*/
enum UriScheme: string
{
case About = 'about';
case Acap = 'acap';
case Bitcoin = 'bitcoin';
case Geo = 'geo';
case Blob = 'blob';
case Afp = 'afp';
case Data = 'data';
case Dict = 'dict';
case Dns = 'dns';
case File = 'file';
case Ftp = 'ftp';
case Git = 'git';
case Gopher = 'gopher';
case Http = 'http';
case Https = 'https';
case Imap = 'imap';
case Imaps = 'imaps';
case Ipp = 'ipp';
case Ipps = 'ipps';
case Irc = 'irc';
case Ircs = 'ircs';
case Javascript = 'javascript';
case Ldap = 'ldap';
case Ldaps = 'ldaps';
case Magnet = 'magnet';
case Mailto = 'mailto';
case Mms = 'mms';
case Msrp = 'msrp';
case Msrps = 'msrps';
case Mtqp = 'mtqp';
case News = 'news';
case Nfs = 'nfs';
case Nntp = 'nntp';
case Nntps = 'nntps';
case Pkcs11 = 'pkcs11';
case Pop = 'pop';
case Prospero = 'prospero';
case Redis = 'redis';
case Rsync = 'rsync';
case Rtsp = 'rtsp';
case Rtsps = 'rtsps';
case Rtspu = 'rtspu';
case Sftp = 'sftp';
case Wss = 'wss';
case Ws = 'ws';
case Sip = 'sip';
case Sips = 'sips';
case Smb = 'smb';
case Smtp = 'smtp';
case Snmp = 'snmp';
case Ssh = 'ssh';
case Steam = 'steam';
case Svn = 'svn';
case Tel = 'tel';
case Telnet = 'telnet';
case Tn3270 = 'tn3270';
case Urn = 'urn';
case Ventrilo = 'ventrilo';
case Vnc = 'vnc';
case Wais = 'wais';
case Xmpp = 'xmpp';
public function port(): ?int
{
return match ($this) {
self::Acap => 674,
self::Afp => 548,
self::Dict => 2628,
self::Dns => 53,
self::Ftp => 21,
self::Http, self::Ws => 80,
self::Https, self::Wss => 443,
self::Git => 9418,
self::Gopher => 70,
self::Imap => 143,
self::Imaps => 993,
self::Ipp, self::Ipps => 631,
self::Irc => 194,
self::Ircs => 6697,
self::Ldap => 389,
self::Ldaps => 636,
self::Mms => 1755,
self::Msrp, self::Msrps => 2855,
self::Mtqp => 1038,
self::Nfs => 111,
self::Nntp => 119,
self::Nntps => 563,
self::Pop => 110,
self::Prospero => 1525,
self::Redis => 6379,
self::Rsync => 873,
self::Rtsp => 554,
self::Rtsps => 322,
self::Rtspu => 5005,
self::Sftp, self::Ssh => 22,
self::Smb => 445,
self::Smtp => 25,
self::Snmp => 161,
self::Svn => 3690,
self::Telnet, self::Tn3270 => 23,
self::Ventrilo => 3784,
self::Vnc => 5900,
self::Wais => 210,
self::Xmpp => 80,
default => null,
};
}
public function type(): SchemeType
{
return match ($this) {
self::Urn,
self::About,
self::Bitcoin,
self::Blob,
self::Data,
self::Geo,
self::Javascript,
self::Magnet,
self::Mailto,
self::Pkcs11,
self::Sip,
self::Sips,
self::Tel => SchemeType::Opaque,
self::File => SchemeType::Hierarchical,
self::News => SchemeType::Unknown,
default => match (true) {
null !== $this->port() => SchemeType::Hierarchical,
default => SchemeType::Unknown,
},
};
}
public function isWhatWgSpecial(): bool
{
return match ($this) {
self::Ftp,
self::Http,
self::Https,
self::Ws,
self::Wss => true,
default => false,
};
}
/**
* @return list<self>
*/
public static function fromPort(?int $port): array
{
null === $port || 0 <= $port || throw new ValueError('The submitted port cannot be negative.');
static $reverse = [];
if ([] === $reverse) {
foreach (self::cases() as $case) {
$defaultPort = $case->port();
if (null === $defaultPort) {
continue;
}
$reverse[$defaultPort] ??= [];
$reverse[$defaultPort][] = $case;
}
}
return $reverse[$port] ?? [];
}
}
+289
View File
@@ -0,0 +1,289 @@
<?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 League\Uri\Contracts\UriException;
use League\Uri\Contracts\UriInterface;
use League\Uri\Exceptions\MissingFeature;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\UriTemplate\Template;
use League\Uri\UriTemplate\TemplateCanNotBeExpanded;
use League\Uri\UriTemplate\VariableBag;
use Psr\Http\Message\UriFactoryInterface;
use Psr\Http\Message\UriInterface as Psr7UriInterface;
use Stringable;
use Uri\InvalidUriException;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\InvalidUrlException;
use Uri\WhatWg\Url as WhatWgUrl;
use function array_fill_keys;
use function array_key_exists;
use function class_exists;
/**
* Defines the URI Template syntax and the process for expanding a URI Template into a URI reference.
*
* @link https://tools.ietf.org/html/rfc6570
* @package League\Uri
* @author Ignace Nyamagana Butera <nyamsprod@gmail.com>
* @since 6.1.0
*
* @phpstan-import-type InputValue from VariableBag
*/
final class UriTemplate implements Stringable
{
private readonly Template $template;
private readonly VariableBag $defaultVariables;
/**
* @throws SyntaxError if the template syntax is invalid
* @throws TemplateCanNotBeExpanded if the template or the variables are invalid
*/
public function __construct(Stringable|string $template, iterable $defaultVariables = [])
{
$this->template = $template instanceof Template ? $template : Template::new($template);
$this->defaultVariables = $this->filterVariables($defaultVariables);
}
private function filterVariables(iterable $variables): VariableBag
{
if (!$variables instanceof VariableBag) {
$variables = new VariableBag($variables);
}
return $variables
->filter(fn ($value, string|int $name) => array_key_exists(
$name,
array_fill_keys($this->template->variableNames, 1)
));
}
/**
* Returns the string representation of the UriTemplate.
*/
public function __toString(): string
{
return $this->template->value;
}
/**
* Returns the distinct variables placeholders used in the template.
*
* @return array<string>
*/
public function getVariableNames(): array
{
return $this->template->variableNames;
}
/**
* @return array<string, InputValue>
*/
public function getDefaultVariables(): array
{
return iterator_to_array($this->defaultVariables);
}
/**
* Returns a new instance with the updated default variables.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the modified default variables.
*
* If present, variables whose name is not part of the current template
* possible variable names are removed.
*
* @throws TemplateCanNotBeExpanded if the variables are invalid
*/
public function withDefaultVariables(iterable $defaultVariables): self
{
$defaultVariables = $this->filterVariables($defaultVariables);
if ($this->defaultVariables->equals($defaultVariables)) {
return $this;
}
return new self($this->template, $defaultVariables);
}
private function templateExpanded(iterable $variables = []): string
{
return $this->template->expand($this->filterVariables($variables)->replace($this->defaultVariables));
}
private function templateExpandedOrFail(iterable $variables = []): string
{
return $this->template->expandOrFail($this->filterVariables($variables)->replace($this->defaultVariables));
}
/**
* @throws TemplateCanNotBeExpanded if the variables are invalid
* @throws UriException if the resulting expansion cannot be converted to a UriInterface instance
*/
public function expand(iterable $variables = [], Rfc3986Uri|WhatWgUrl|Stringable|string|null $baseUri = null): UriInterface
{
$expanded = $this->templateExpanded($variables);
return null === $baseUri ? Uri::new($expanded) : (Uri::parse($expanded, $baseUri) ?? throw new SyntaxError('Unable to expand URI'));
}
/**
* @throws MissingFeature if no Uri\Rfc3986\Uri class is found
* @throws TemplateCanNotBeExpanded if the variables are invalid
* @throws InvalidUriException if the base URI cannot be converted to a Uri\Rfc3986\Uri instance
* @throws InvalidUriException if the resulting expansion cannot be converted to a Uri\Rfc3986\Uri instance
*/
public function expandToUri(iterable $variables = [], Rfc3986Uri|WhatWgUrl|Stringable|string|null $baseUri = null): Rfc3986Uri
{
class_exists(Rfc3986Uri::class) || throw new MissingFeature('Support for '.Rfc3986Uri::class.' requires PHP8.5+ or a polyfill. Run "composer require league/uri-polyfill" or use you own polyfill.');
return new Rfc3986Uri($this->templateExpanded($variables), $this->newRfc3986Uri($baseUri));
}
/**
* @throws MissingFeature if no Uri\Whatwg\Url class is found
* @throws TemplateCanNotBeExpanded if the variables are invalid
* @throws InvalidUrlException if the base URI cannot be converted to a Uri\Whatwg\Url instance
* @throws InvalidUrlException if the resulting expansion cannot be converted to a Uri\Whatwg\Url instance
*/
public function expandToUrl(iterable $variables = [], Rfc3986Uri|WhatWgUrl|Stringable|string|null $baseUrl = null, array|null &$errors = []): WhatWgUrl
{
class_exists(WhatWgUrl::class) || throw new MissingFeature('Support for '.WhatWgUrl::class.' requires PHP8.5+ or a polyfill. Run "composer require league/uri-polyfill" or use you own polyfill.');
return new WhatWgUrl($this->templateExpanded($variables), $this->newWhatWgUrl($baseUrl), $errors);
}
/**
* @throws TemplateCanNotBeExpanded if the variables are invalid
* @throws UriException if the resulting expansion cannot be converted to a UriInterface instance
*/
public function expandToPsr7Uri(
iterable $variables = [],
Rfc3986Uri|WhatWgUrl|Stringable|string|null $baseUrl = null,
UriFactoryInterface $uriFactory = new HttpFactory()
): Psr7UriInterface {
$uriString = $this->templateExpandedOrFail($variables);
return $uriFactory->createUri(
null === $baseUrl
? $uriString
: UriString::resolve($uriString, match (true) {
$baseUrl instanceof Rfc3986Uri => $baseUrl->toRawString(),
$baseUrl instanceof WhatWgUrl => $baseUrl->toUnicodeString(),
default => $baseUrl,
})
);
}
/**
* @throws TemplateCanNotBeExpanded if the variables are invalid or missing
* @throws UriException if the resulting expansion cannot be converted to a UriInterface instance
*/
public function expandOrFail(iterable $variables = [], Rfc3986Uri|WhatWgUrl|Stringable|string|null $baseUri = null): UriInterface
{
$expanded = $this->templateExpandedOrFail($variables);
return null === $baseUri ? Uri::new($expanded) : (Uri::parse($expanded, $baseUri) ?? throw new SyntaxError('Unable to expand URI'));
}
/**
* @throws MissingFeature if no Uri\Rfc3986\Uri class is found
* @throws TemplateCanNotBeExpanded if the variables are invalid
* @throws InvalidUriException if the base URI cannot be converted to a Uri\Rfc3986\Uri instance
* @throws InvalidUriException if the resulting expansion cannot be converted to a Uri\Rfc3986\Uri instance
*/
public function expandToUriOrFail(iterable $variables = [], Rfc3986Uri|WhatWgUrl|Stringable|string|null $baseUri = null): Rfc3986Uri
{
class_exists(Rfc3986Uri::class) || throw new MissingFeature('Support for '.Rfc3986Uri::class.' requires PHP8.5+ or a polyfill. Run "composer require league/uri-polyfill" or use you own polyfill.');
return new Rfc3986Uri($this->templateExpandedOrFail($variables), $this->newRfc3986Uri($baseUri));
}
/**
* @throws MissingFeature if no Uri\Whatwg\Url class is found
* @throws TemplateCanNotBeExpanded if the variables are invalid
* @throws InvalidUrlException if the base URI cannot be converted to a Uri\Whatwg\Url instance
* @throws InvalidUrlException if the resulting expansion cannot be converted to a Uri\Whatwg\Url instance
*/
public function expandToUrlOrFail(iterable $variables = [], Rfc3986Uri|WhatWgUrl|Stringable|string|null $baseUrl = null, array|null &$errors = []): WhatWgUrl
{
class_exists(WhatWgUrl::class) || throw new MissingFeature('Support for '.WhatWgUrl::class.' requires PHP8.5+ or a polyfill. Run "composer require league/uri-polyfill" or use you own polyfill.');
return new WhatWgUrl($this->templateExpandedOrFail($variables), $this->newWhatWgUrl($baseUrl), $errors);
}
/**
* @throws TemplateCanNotBeExpanded if the variables are invalid
* @throws UriException if the resulting expansion cannot be converted to a UriInterface instance
*/
public function expandToPsr7UriOrFail(
iterable $variables = [],
Rfc3986Uri|WhatWgUrl|Stringable|string|null $baseUrl = null,
UriFactoryInterface $uriFactory = new HttpFactory()
): Psr7UriInterface {
$uriString = $this->templateExpandedOrFail($variables);
return $uriFactory->createUri(
null === $baseUrl
? $uriString
: UriString::resolve($uriString, match (true) {
$baseUrl instanceof Rfc3986Uri => $baseUrl->toRawString(),
$baseUrl instanceof WhatWgUrl => $baseUrl->toUnicodeString(),
default => $baseUrl,
})
);
}
/**
* @throws InvalidUrlException
*/
private function newWhatWgUrl(Rfc3986Uri|WhatWgUrl|Stringable|string|null $url = null): ?WhatWgUrl
{
return match (true) {
null === $url => null,
$url instanceof WhatWgUrl => $url,
$url instanceof Rfc3986Uri => new WhatWgUrl($url->toRawString()),
default => new WhatWgUrl((string) $url),
};
}
/**
* @throws InvalidUriException
*/
private function newRfc3986Uri(Rfc3986Uri|WhatWgUrl|Stringable|string|null $uri = null): ?Rfc3986Uri
{
return match (true) {
null === $uri => null,
$uri instanceof Rfc3986Uri => $uri,
$uri instanceof WhatWgUrl => new Rfc3986Uri($uri->toAsciiString()),
default => new Rfc3986Uri((string) $uri),
};
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated Since version 7.6.0
* @codeCoverageIgnore
* @see UriTemplate::toString()
*
* Create a new instance from the environment.
*/
#[Deprecated(message:'use League\Uri\UriTemplate::__toString() instead', since:'league/uri:7.6.0')]
public function getTemplate(): string
{
return $this->__toString();
}
}
@@ -0,0 +1,99 @@
<?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\UriTemplate;
use Deprecated;
use League\Uri\Exceptions\SyntaxError;
use Stringable;
use function array_filter;
use function array_map;
use function array_unique;
use function explode;
use function implode;
/**
* @internal The class exposes the internal representation of an Expression and its usage
* @link https://www.rfc-editor.org/rfc/rfc6570#section-2.2
*/
final class Expression
{
/** @var array<VarSpecifier> */
private readonly array $varSpecifiers;
/** @var array<string> */
public readonly array $variableNames;
public readonly string $value;
private function __construct(public readonly Operator $operator, VarSpecifier ...$varSpecifiers)
{
$this->varSpecifiers = $varSpecifiers;
$this->variableNames = array_unique(
array_map(
static fn (VarSpecifier $varSpecifier): string => $varSpecifier->name,
$varSpecifiers
)
);
$this->value = '{'.$operator->value.implode(',', array_map(
static fn (VarSpecifier $varSpecifier): string => $varSpecifier->toString(),
$varSpecifiers
)).'}';
}
/**
* @throws SyntaxError if the expression is invalid
*/
public static function new(Stringable|string $expression): self
{
$parts = Operator::parseExpression($expression);
return new Expression($parts['operator'], ...array_map(
static fn (string $varSpec): VarSpecifier => VarSpecifier::new($varSpec),
explode(',', $parts['variables'])
));
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @throws SyntaxError if the expression is invalid
* @see Expression::new()
*
* @deprecated Since version 7.0.0
* @codeCoverageIgnore
*/
#[Deprecated(message:'use League\Uri\UriTemplate\Exppression::new() instead', since:'league/uri:7.0.0')]
public static function createFromString(Stringable|string $expression): self
{
return self::new($expression);
}
public function expand(VariableBag $variables): string
{
$expanded = implode(
$this->operator->separator(),
array_filter(
array_map(
fn (VarSpecifier $varSpecifier): string => $this->operator->expand($varSpecifier, $variables),
$this->varSpecifiers
),
static fn ($value): bool => '' !== $value
)
);
return match ('') {
$expanded => '',
default => $this->operator->first().$expanded,
};
}
}
+225
View File
@@ -0,0 +1,225 @@
<?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\UriTemplate;
use League\Uri\Encoder;
use League\Uri\Exceptions\SyntaxError;
use Stringable;
use function implode;
use function is_array;
use function preg_match;
use function rawurlencode;
use function str_contains;
use function substr;
/**
* Processing behavior according to the expression type operator.
*
* @internal The class exposes the internal representation of an Operator and its usage
*
* @link https://www.rfc-editor.org/rfc/rfc6570#section-2.2
* @link https://tools.ietf.org/html/rfc6570#appendix-A
*/
enum Operator: string
{
/**
* Expression regular expression pattern.
*
* @link https://tools.ietf.org/html/rfc6570#section-2.2
*/
private const REGEXP_EXPRESSION = '/^\{(?:(?<operator>[\.\/;\?&\=,\!@\|\+#])?(?<variables>[^\}]*))\}$/';
/**
* Reserved Operator characters.
*
* @link https://tools.ietf.org/html/rfc6570#section-2.2
*/
private const RESERVED_OPERATOR = '=,!@|';
case None = '';
case ReservedChars = '+';
case Label = '.';
case Path = '/';
case PathParam = ';';
case Query = '?';
case QueryPair = '&';
case Fragment = '#';
public function first(): string
{
return match ($this) {
self::None, self::ReservedChars => '',
default => $this->value,
};
}
public function separator(): string
{
return match ($this) {
self::None, self::ReservedChars, self::Fragment => ',',
self::Query, self::QueryPair => '&',
default => $this->value,
};
}
public function isNamed(): bool
{
return match ($this) {
self::Query, self::PathParam, self::QueryPair => true,
default => false,
};
}
/**
* Removes percent encoding on reserved characters (used with + and # modifiers).
*/
public function decode(string $var): string
{
return match ($this) {
Operator::ReservedChars, Operator::Fragment => (string) Encoder::encodeQueryOrFragment($var),
default => rawurlencode($var),
};
}
/**
* @throws SyntaxError if the expression is invalid
* @throws SyntaxError if the operator used in the expression is invalid
* @throws SyntaxError if the contained variable specifiers are invalid
*
* @return array{operator:Operator, variables:string}
*/
public static function parseExpression(Stringable|string $expression): array
{
$expression = (string) $expression;
if (1 !== preg_match(self::REGEXP_EXPRESSION, $expression, $parts)) {
throw new SyntaxError('The expression "'.$expression.'" is invalid.');
}
/** @var array{operator:string, variables:string} $parts */
$parts = $parts + ['operator' => ''];
if ('' !== $parts['operator'] && str_contains(self::RESERVED_OPERATOR, $parts['operator'])) {
throw new SyntaxError('The operator used in the expression "'.$expression.'" is reserved.');
}
return [
'operator' => self::from($parts['operator']),
'variables' => $parts['variables'],
];
}
/**
* Replaces an expression with the given variables.
*
* @throws TemplateCanNotBeExpanded if the variables is an array and a ":" modifier needs to be applied
* @throws TemplateCanNotBeExpanded if the variables contains nested array values
*/
public function expand(VarSpecifier $varSpecifier, VariableBag $variables): string
{
$value = $variables->fetch($varSpecifier->name);
if (null === $value) {
return '';
}
[$expanded, $actualQuery] = $this->inject($value, $varSpecifier);
if (!$actualQuery) {
return $expanded;
}
if ('&' !== $this->separator() && '' === $expanded) {
return $varSpecifier->name;
}
return $varSpecifier->name.'='.$expanded;
}
/**
* @param string|array<string> $value
*
* @return array{0:string, 1:bool}
*/
private function inject(array|string $value, VarSpecifier $varSpec): array
{
if (is_array($value)) {
return $this->replaceList($value, $varSpec);
}
if (':' === $varSpec->modifier) {
$value = substr($value, 0, $varSpec->position);
}
return [$this->decode($value), $this->isNamed()];
}
/**
* Expands an expression using a list of values.
*
* @param array<string> $value
*
* @throws TemplateCanNotBeExpanded if the variables is an array and a ":" modifier needs to be applied
*
* @return array{0:string, 1:bool}
*/
private function replaceList(array $value, VarSpecifier $varSpec): array
{
if (':' === $varSpec->modifier) {
throw TemplateCanNotBeExpanded::dueToUnableToProcessValueListWithPrefix($varSpec->name);
}
if ([] === $value) {
return ['', false];
}
$pairs = [];
$isList = array_is_list($value);
$useQuery = $this->isNamed();
foreach ($value as $key => $var) {
if (!$isList) {
$key = rawurlencode((string) $key);
}
$var = $this->decode($var);
if ('*' === $varSpec->modifier) {
if (!$isList) {
$var = $key.'='.$var;
} elseif ($key > 0 && $useQuery) {
$var = $varSpec->name.'='.$var;
}
}
$pairs[$key] = $var;
}
if ('*' === $varSpec->modifier) {
if (!$isList) {
// Don't prepend the value name when using the `explode` modifier with an associative array.
$useQuery = false;
}
return [implode($this->separator(), $pairs), $useQuery];
}
if (!$isList) {
// When an associative array is encountered and the `explode` modifier is not set, then
// the result must be a comma separated list of keys followed by their respective values.
$retVal = [];
foreach ($pairs as $offset => $data) {
$retVal[$offset] = $offset.','.$data;
}
$pairs = $retVal;
}
return [implode(',', $pairs), $useQuery];
}
}
+143
View File
@@ -0,0 +1,143 @@
<?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\UriTemplate;
use Deprecated;
use League\Uri\Exceptions\SyntaxError;
use Stringable;
use function array_filter;
use function array_map;
use function array_reduce;
use function array_unique;
use function preg_match_all;
use function preg_replace;
use function str_replace;
use function strpbrk;
use const PREG_SET_ORDER;
/**
* @internal The class exposes the internal representation of a Template and its usage
*/
final class Template implements Stringable
{
/**
* Expression regular expression pattern.
*/
private const REGEXP_EXPRESSION_DETECTOR = '/(?<expression>\{[^}]*})/x';
/** @var array<Expression> */
private readonly array $expressions;
/** @var array<string> */
public readonly array $variableNames;
private function __construct(public readonly string $value, Expression ...$expressions)
{
$this->expressions = $expressions;
$this->variableNames = array_unique(
array_merge(
...array_map(
static fn (Expression $expression): array => $expression->variableNames,
$expressions
)
)
);
}
/**
* @throws SyntaxError if the template contains invalid expressions
* @throws SyntaxError if the template contains invalid variable specification
*/
public static function new(Stringable|string $template): self
{
$template = (string) $template;
/** @var string $remainder */
$remainder = preg_replace(self::REGEXP_EXPRESSION_DETECTOR, '', $template);
false === strpbrk($remainder, '{}') || throw new SyntaxError('The template "'.$template.'" contains invalid expressions.');
preg_match_all(self::REGEXP_EXPRESSION_DETECTOR, $template, $founds, PREG_SET_ORDER);
return new self($template, ...array_values(
array_reduce($founds, function (array $carry, array $found): array {
if (!isset($carry[$found['expression']])) {
$carry[$found['expression']] = Expression::new($found['expression']);
}
return $carry;
}, [])
));
}
/**
* @throws TemplateCanNotBeExpanded if the variables are invalid
*/
public function expand(iterable $variables = []): string
{
if (!$variables instanceof VariableBag) {
$variables = new VariableBag($variables);
}
return $this->expandAll($variables);
}
/**
* @throws TemplateCanNotBeExpanded if the variables are invalid or missing
*/
public function expandOrFail(iterable $variables = []): string
{
if (!$variables instanceof VariableBag) {
$variables = new VariableBag($variables);
}
$missing = array_filter($this->variableNames, fn (string $name): bool => !isset($variables[$name]));
if ([] !== $missing) {
throw TemplateCanNotBeExpanded::dueToMissingVariables(...$missing);
}
return $this->expandAll($variables);
}
private function expandAll(VariableBag $variables): string
{
return array_reduce(
$this->expressions,
fn (string $uri, Expression $expr): string => str_replace($expr->value, $expr->expand($variables), $uri),
$this->value
);
}
public function __toString(): string
{
return $this->value;
}
/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @throws SyntaxError if the template contains invalid expressions
* @throws SyntaxError if the template contains invalid variable specification
* @deprecated Since version 7.0.0
* @codeCoverageIgnore
* @see Template::new()
*
* Create a new instance from a string.
*
*/
#[Deprecated(message:'use League\Uri\UriTemplate\Template::new() instead', since:'league/uri:7.0.0')]
public static function createFromString(Stringable|string $template): self
{
return self::new($template);
}
}
@@ -0,0 +1,44 @@
<?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\UriTemplate;
use InvalidArgumentException;
use League\Uri\Contracts\UriException;
class TemplateCanNotBeExpanded extends InvalidArgumentException implements UriException
{
public readonly array $variablesNames;
public function __construct(string $message = '', string ...$variableNames)
{
parent::__construct($message, 0, null);
$this->variablesNames = $variableNames;
}
public static function dueToUnableToProcessValueListWithPrefix(string $variableName): self
{
return new self('The ":" modifier cannot be applied on "'.$variableName.'" since it is a list of values.', $variableName);
}
public static function dueToNestedListOfValue(string $variableName): self
{
return new self('The "'.$variableName.'" cannot be a nested list.', $variableName);
}
public static function dueToMissingVariables(string ...$variableNames): self
{
return new self('The following required variables are missing: `'.implode('`, `', $variableNames).'`.', ...$variableNames);
}
}
@@ -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\UriTemplate;
use League\Uri\Exceptions\SyntaxError;
use function preg_match;
/**
* @internal The class exposes the internal representation of a Var Specifier
* @link https://www.rfc-editor.org/rfc/rfc6570#section-2.3
*/
final class VarSpecifier
{
/**
* Variables specification regular expression pattern.
*
* @link https://tools.ietf.org/html/rfc6570#section-2.3
*/
private const REGEXP_VARSPEC = '/^(?<name>(?:[A-z0-9_\.]|%[0-9a-fA-F]{2})+)(?<modifier>\:(?<position>\d+)|\*)?$/';
private const MODIFIER_POSITION_MAX_POSITION = 10_000;
private function __construct(
public readonly string $name,
public readonly string $modifier,
public readonly int $position
) {
}
public static function new(string $specification): self
{
1 === preg_match(self::REGEXP_VARSPEC, $specification, $parsed) || throw new SyntaxError('The variable specification "'.$specification.'" is invalid.');
$properties = ['name' => $parsed['name'], 'modifier' => $parsed['modifier'] ?? '', 'position' => $parsed['position'] ?? ''];
if ('' !== $properties['position']) {
$properties['position'] = (int) $properties['position'];
$properties['modifier'] = ':';
}
if ('' === $properties['position']) {
$properties['position'] = 0;
}
if (self::MODIFIER_POSITION_MAX_POSITION <= $properties['position']) {
throw new SyntaxError('The variable specification "'.$specification.'" is invalid the position modifier must be lower than 10000.');
}
return new self($properties['name'], $properties['modifier'], $properties['position']);
}
public function toString(): string
{
return $this->name.$this->modifier.match (true) {
0 < $this->position => $this->position,
default => '',
};
}
}
@@ -0,0 +1,157 @@
<?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\UriTemplate;
use ArrayAccess;
use Closure;
use Countable;
use IteratorAggregate;
use Stringable;
use Traversable;
use function array_filter;
use function is_bool;
use function is_scalar;
use const ARRAY_FILTER_USE_BOTH;
/**
* @internal The class exposes the internal representation of variable bags
*
* @phpstan-type InputValue string|bool|int|float|array<string|bool|int|float>
*
* @implements ArrayAccess<string, InputValue>
* @implements IteratorAggregate<string, InputValue>
*/
final class VariableBag implements ArrayAccess, Countable, IteratorAggregate
{
/**
* @var array<string,string|array<string>>
*/
private array $variables = [];
/**
* @param iterable<array-key, InputValue> $variables
*/
public function __construct(iterable $variables = [])
{
foreach ($variables as $name => $value) {
$this->assign((string) $name, $value);
}
}
public function count(): int
{
return count($this->variables);
}
public function getIterator(): Traversable
{
yield from $this->variables;
}
public function offsetExists(mixed $offset): bool
{
return array_key_exists($offset, $this->variables);
}
public function offsetUnset(mixed $offset): void
{
unset($this->variables[$offset]);
}
public function offsetSet(mixed $offset, mixed $value): void
{
$this->assign($offset, $value); /* @phpstan-ignore-line */
}
public function offsetGet(mixed $offset): mixed
{
return $this->fetch($offset);
}
/**
* Tells whether the bag is empty or not.
*/
public function isEmpty(): bool
{
return [] === $this->variables;
}
/**
* Tells whether the bag is empty or not.
*/
public function isNotEmpty(): bool
{
return [] !== $this->variables;
}
public function equals(mixed $value): bool
{
return $value instanceof self
&& $this->variables === $value->variables;
}
/**
* Fetches the variable value if none found returns null.
*
* @return null|string|array<string>
*/
public function fetch(string $name): null|string|array
{
return $this->variables[$name] ?? null;
}
/**
* @param Stringable|InputValue $value
*/
public function assign(string $name, Stringable|string|bool|int|float|array|null $value): void
{
$this->variables[$name] = $this->normalizeValue($value, $name, true);
}
/**
* @param Stringable|InputValue $value
*
* @throws TemplateCanNotBeExpanded if the value contains nested list
*/
private function normalizeValue(
Stringable|string|float|int|bool|array|null $value,
string $name,
bool $isNestedListAllowed
): array|string {
return match (true) {
is_bool($value) => true === $value ? '1' : '0',
(null === $value || is_scalar($value) || $value instanceof Stringable) => (string) $value,
!$isNestedListAllowed => throw TemplateCanNotBeExpanded::dueToNestedListOfValue($name),
default => array_map(fn ($var): array|string => self::normalizeValue($var, $name, false), $value),
};
}
/**
* Replaces elements from passed variables into the current instance.
*/
public function replace(VariableBag $variables): self
{
return new self($this->variables + $variables->variables);
}
/**
* Filters elements using the closure.
*/
public function filter(Closure $fn): self
{
return new self(array_filter($this->variables, $fn, ARRAY_FILTER_USE_BOTH));
}
}
+578
View File
@@ -0,0 +1,578 @@
<?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 Closure;
use JsonSerializable;
use League\Uri\Contracts\Conditionable;
use League\Uri\Contracts\UriComponentInterface;
use League\Uri\Contracts\UriInterface;
use League\Uri\Exceptions\SyntaxError;
use League\Uri\UriTemplate\Template;
use Stringable;
use Uri\Rfc3986\Uri as Rfc3986Uri;
use Uri\WhatWg\Url as WhatWgUrl;
use function is_bool;
use function preg_match;
use function str_replace;
use function strtolower;
/**
* @phpstan-type UrnSerialize array{0: array{urn: non-empty-string}, 1: array{}}
* @phpstan-import-type InputComponentMap from UriString
* @phpstan-type UrnMap array{
* scheme: 'urn',
* nid: string,
* nss: string,
* r_component: ?string,
* q_component: ?string,
* f_component: ?string,
* }
*/
final class Urn implements Conditionable, Stringable, JsonSerializable
{
/**
* RFC8141 regular expression URN splitter.
*
* The regexp does not perform any look-ahead.
* Not all invalid URN are caught. Some
* post-regexp-validation checks
* are mandatory.
*
* @link https://datatracker.ietf.org/doc/html/rfc8141#section-2
*
* @var string
*/
private const REGEXP_URN_PARTS = '/^
urn:
(?<nid>[a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])?): # NID
(?<nss>.*?) # NSS
(?<frc>\?\+(?<rcomponent>.*?))? # r-component
(?<fqc>\?\=(?<qcomponent>.*?))? # q-component
(?:\#(?<fcomponent>.*))? # f-component
$/xi';
/**
* RFC8141 namespace identifier regular expression.
*
* @link https://datatracker.ietf.org/doc/html/rfc8141#section-2
*
* @var string
*/
private const REGEX_NID_SEQUENCE = '/^[a-z0-9]([a-z0-9-]{0,30})[a-z0-9]$/xi';
/** @var non-empty-string */
private readonly string $uriString;
/** @var non-empty-string */
private readonly string $nid;
/** @var non-empty-string */
private readonly string $nss;
/** @var non-empty-string|null */
private readonly ?string $rComponent;
/** @var non-empty-string|null */
private readonly ?string $qComponent;
/** @var non-empty-string|null */
private readonly ?string $fComponent;
/**
* @param Rfc3986Uri|WhatWgUrl|Stringable|string $urn the percent-encoded URN
*/
public static function parse(Rfc3986Uri|WhatWgUrl|Stringable|string $urn): ?Urn
{
try {
return self::fromString($urn);
} catch (SyntaxError) {
return null;
}
}
/**
* @param Rfc3986Uri|WhatWgUrl|Stringable|string $urn the percent-encoded URN
* @see self::fromString()
*
* @throws SyntaxError if the URN is invalid
*/
public static function new(Rfc3986Uri|WhatWgUrl|Stringable|string $urn): self
{
return self::fromString($urn);
}
/**
* @param Rfc3986Uri|WhatWgUrl|Stringable|string $urn the percent-encoded URN
*
* @throws SyntaxError if the URN is invalid
*/
public static function fromString(Rfc3986Uri|WhatWgUrl|Stringable|string $urn): self
{
$urn = match (true) {
$urn instanceof Rfc3986Uri => $urn->toRawString(),
$urn instanceof WhatWgUrl => $urn->toAsciiString(),
default => (string) $urn,
};
UriString::containsRfc3986Chars($urn) || throw new SyntaxError('The URN is malformed, it contains invalid characters.');
1 === preg_match(self::REGEXP_URN_PARTS, $urn, $matches) || throw new SyntaxError('The URN string is invalid.');
return new self(
nid: $matches['nid'],
nss: $matches['nss'],
rComponent: (isset($matches['frc']) && '' !== $matches['frc']) ? $matches['rcomponent'] : null,
qComponent: (isset($matches['fqc']) && '' !== $matches['fqc']) ? $matches['qcomponent'] : null,
fComponent: $matches['fcomponent'] ?? null,
);
}
/**
* Create a new instance from a hash representation of the URI similar
* to PHP parse_url function result.
*
* @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,
];
return self::fromString(UriString::build($components));
}
/**
* @param Stringable|string $nss the percent-encoded NSS
*
* @throws SyntaxError if the URN is invalid
*/
public static function fromRfc2141(Stringable|string $nid, Stringable|string $nss): self
{
return new self((string) $nid, (string) $nss);
}
/**
* @param string $nss the percent-encoded NSS
* @param ?string $rComponent the percent-encoded r-component
* @param ?string $qComponent the percent-encoded q-component
* @param ?string $fComponent the percent-encoded f-component
*
* @throws SyntaxError if one of the URN part is invalid
*/
private function __construct(
string $nid,
string $nss,
?string $rComponent = null,
?string $qComponent = null,
?string $fComponent = null,
) {
('' !== $nid && 1 === preg_match(self::REGEX_NID_SEQUENCE, $nid)) || throw new SyntaxError('The URN is malformed, the NID is invalid.');
('' !== $nss && Encoder::isPathEncoded($nss)) || throw new SyntaxError('The URN is malformed, the NSS is invalid.');
/** @param Closure(string): ?non-empty-string $closure */
$validateComponent = static fn (?string $value, Closure $closure, string $name): ?string => match (true) {
null === $value,
('' !== $value && 1 !== preg_match('/[#?]/', $value) && $closure($value)) => $value,
default => throw new SyntaxError('The URN is malformed, the `'.$name.'` component is invalid.'),
};
$this->nid = $nid;
$this->nss = $nss;
$this->rComponent = $validateComponent($rComponent, Encoder::isPathEncoded(...), 'r-component');
$this->qComponent = $validateComponent($qComponent, Encoder::isQueryEncoded(...), 'q-component');
$this->fComponent = $validateComponent($fComponent, Encoder::isFragmentEncoded(...), 'f-component');
$this->uriString = $this->setUriString();
}
/**
* @return non-empty-string
*/
private function setUriString(): string
{
$str = $this->toRfc2141();
if (null !== $this->rComponent) {
$str .= '?+'.$this->rComponent;
}
if (null !== $this->qComponent) {
$str .= '?='.$this->qComponent;
}
if (null !== $this->fComponent) {
$str .= '#'.$this->fComponent;
}
return $str;
}
/**
* Returns the NID.
*
* @return non-empty-string
*/
public function getNid(): string
{
return $this->nid;
}
/**
* Returns the percent-encoded NSS.
*
* @return non-empty-string
*/
public function getNss(): string
{
return $this->nss;
}
/**
* Returns the percent-encoded r-component string or null if it is not set.
*
* @return ?non-empty-string
*/
public function getRComponent(): ?string
{
return $this->rComponent;
}
/**
* Returns the percent-encoded q-component string or null if it is not set.
*
* @return ?non-empty-string
*/
public function getQComponent(): ?string
{
return $this->qComponent;
}
/**
* Returns the percent-encoded f-component string or null if it is not set.
*
* @return ?non-empty-string
*/
public function getFComponent(): ?string
{
return $this->fComponent;
}
/**
* Returns the RFC8141 URN string representation.
*
* @return non-empty-string
*/
public function toString(): string
{
return $this->uriString;
}
/**
* Returns the RFC2141 URN string representation.
*
* @return non-empty-string
*/
public function toRfc2141(): string
{
return 'urn:'.$this->nid.':'.$this->nss;
}
/**
* Returns the human-readable string representation of the URN as an IRI.
*
* @see https://datatracker.ietf.org/doc/html/rfc3987
*/
public function toDisplayString(): string
{
return UriString::toIriString($this->uriString);
}
/**
* Returns the RFC8141 URN string representation.
*
* @see self::toString()
*
* @return non-empty-string
*/
public function __toString(): string
{
return $this->toString();
}
/**
* Returns the RFC8141 URN string representation.
* @see self::toString()
*
* @return non-empty-string
*/
public function jsonSerialize(): string
{
return $this->toString();
}
/**
* Returns the RFC3986 representation of the current URN.
*
* If a template URI is used the following variables as present
* {nid} for the namespace identifier
* {nss} for the namespace specific string
* {r_component} for the r-component without its delimiter
* {q_component} for the q-component without its delimiter
* {f_component} for the f-component without its delimiter
*/
public function resolve(UriTemplate|Template|string|null $template = null): UriInterface
{
return null !== $template ? Uri::fromTemplate($template, $this->toComponents()) : Uri::new($this->uriString);
}
public function hasRComponent(): bool
{
return null !== $this->rComponent;
}
public function hasQComponent(): bool
{
return null !== $this->qComponent;
}
public function hasFComponent(): bool
{
return null !== $this->fComponent;
}
public function hasOptionalComponent(): bool
{
return null !== $this->rComponent
|| null !== $this->qComponent
|| null !== $this->fComponent;
}
/**
* Return an instance with the specified NID.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified NID.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withNid(Stringable|string $nid): self
{
$nid = (string) $nid;
return $this->nid === $nid ? $this : new self(
nid: $nid,
nss: $this->nss,
rComponent: $this->rComponent,
qComponent: $this->qComponent,
fComponent: $this->fComponent,
);
}
/**
* Return an instance with the specified NSS.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified NSS.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withNss(Stringable|string $nss): self
{
$nss = Encoder::encodePath($nss);
return $this->nss === $nss ? $this : new self(
nid: $this->nid,
nss: $nss,
rComponent: $this->rComponent,
qComponent: $this->qComponent,
fComponent: $this->fComponent,
);
}
/**
* Return an instance with the specified r-component.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified r-component.
*
* The component is removed if the value is null.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withRComponent(Stringable|string|null $component): self
{
if ($component instanceof UriComponentInterface) {
$component = $component->value();
}
if (null !== $component) {
$component = self::formatComponent(Encoder::encodePath($component));
}
return $this->rComponent === $component ? $this : new self(
nid: $this->nid,
nss: $this->nss,
rComponent: $component,
qComponent: $this->qComponent,
fComponent: $this->fComponent,
);
}
private static function formatComponent(?string $component): ?string
{
return null === $component ? null : str_replace(['?', '#'], ['%3F', '%23'], $component);
}
/**
* Return an instance with the specified q-component.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified q-component.
*
* The component is removed if the value is null.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withQComponent(Stringable|string|null $component): self
{
if ($component instanceof UriComponentInterface) {
$component = $component->value();
}
$component = self::formatComponent(Encoder::encodeQueryOrFragment($component));
return $this->qComponent === $component ? $this : new self(
nid: $this->nid,
nss: $this->nss,
rComponent: $this->rComponent,
qComponent: $component,
fComponent: $this->fComponent,
);
}
/**
* Return an instance with the specified f-component.
*
* This method MUST retain the state of the current instance, and return
* an instance that contains the specified f-component.
*
* The component is removed if the value is null.
*
* @throws SyntaxError for invalid component or transformations
* that would result in an object in invalid state.
*/
public function withFComponent(Stringable|string|null $component): self
{
if ($component instanceof UriComponentInterface) {
$component = $component->value();
}
$component = self::formatComponent(Encoder::encodeQueryOrFragment($component));
return $this->fComponent === $component ? $this : new self(
nid: $this->nid,
nss: $this->nss,
rComponent: $this->rComponent,
qComponent: $this->qComponent,
fComponent: $component,
);
}
public function normalize(): self
{
$copy = new self(
nid: strtolower($this->nid),
nss: (string) Encoder::normalizePath($this->nss),
rComponent: null === $this->rComponent ? $this->rComponent : Encoder::normalizePath($this->rComponent),
qComponent: Encoder::normalizeQuery($this->qComponent),
fComponent: Encoder::normalizeFragment($this->fComponent),
);
return $copy->uriString === $this->uriString ? $this : $copy;
}
public function equals(Urn|Rfc3986Uri|WhatWgUrl|Stringable|string $other, UrnComparisonMode $urnComparisonMode = UrnComparisonMode::ExcludeComponents): bool
{
if (!$other instanceof Urn) {
$other = self::parse($other);
}
return (null !== $other) && match ($urnComparisonMode) {
UrnComparisonMode::ExcludeComponents => $other->normalize()->toRfc2141() === $this->normalize()->toRfc2141(),
UrnComparisonMode::IncludeComponents => $other->normalize()->toString() === $this->normalize()->toString(),
};
}
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;
}
/**
* @return UrnSerialize
*/
public function __serialize(): array
{
return [['urn' => $this->toString()], []];
}
/**
* @param UrnSerialize $data
*
* @throws SyntaxError
*/
public function __unserialize(array $data): void
{
[$properties] = $data;
$uri = self::fromString($properties['urn'] ?? throw new SyntaxError('The `urn` property is missing from the serialized object.'));
$this->nid = $uri->nid;
$this->nss = $uri->nss;
$this->rComponent = $uri->rComponent;
$this->qComponent = $uri->qComponent;
$this->fComponent = $uri->fComponent;
$this->uriString = $uri->uriString;
}
/**
* @return UrnMap
*/
public function toComponents(): array
{
return [
'scheme' => 'urn',
'nid' => $this->nid,
'nss' => $this->nss,
'r_component' => $this->rComponent,
'q_component' => $this->qComponent,
'f_component' => $this->fComponent,
];
}
/**
* @return UrnMap
*/
public function __debugInfo(): array
{
return $this->toComponents();
}
}
+84
View File
@@ -0,0 +1,84 @@
{
"name": "league/uri",
"type": "library",
"description" : "URI manipulation library",
"keywords": [
"url",
"uri",
"urn",
"uri-template",
"rfc2141",
"rfc3986",
"rfc3987",
"rfc8141",
"rfc6570",
"psr-7",
"parse_url",
"http",
"https",
"ws",
"ftp",
"data-uri",
"file-uri",
"middleware",
"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"
}
],
"support": {
"forum": "https://thephpleague.slack.com",
"docs": "https://uri.thephpleague.com",
"issues": "https://github.com/thephpleague/uri-src/issues"
},
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/nyamsprod"
}
],
"require": {
"php": "^8.1",
"league/uri-interfaces": "^7.7",
"psr/http-factory": "^1"
},
"autoload": {
"psr-4": {
"League\\Uri\\": ""
}
},
"conflict": {
"league/uri-schemes": "^1.0"
},
"suggest": {
"ext-bcmath": "to improve IPV4 host parsing",
"ext-dom": "to convert the URI into an HTML anchor tag",
"ext-fileinfo": "to create Data URI from file contennts",
"ext-gmp": "to improve IPV4 host parsing",
"ext-intl": "to handle IDN host with the best performance",
"ext-uri": "to use the PHP native URI class",
"jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain",
"league/uri-components" : "Needed to easily manipulate URI objects components",
"league/uri-polyfill" : "Needed to backport the PHP URI extension for older versions of PHP",
"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 WHATWG URL"
},
"extra": {
"branch-alias": {
"dev-master": "7.x-dev"
}
},
"config": {
"sort-packages": true
}
}
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Frederic Guillot
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.
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace PicoFeed;
use PicoFeed\Config\Config;
use PicoFeed\Logging\Logger;
/**
* Base class
*
* @package PicoFeed
* @author Frederic Guillot
*/
abstract class Base
{
/**
* Config class instance
*
* @access protected
* @var \PicoFeed\Config\Config
*/
protected $config;
/**
* Constructor.
*
* @param \PicoFeed\Config\Config $config Config class instance
*/
public function __construct(?Config $config = null)
{
$this->config = $config ?: new Config();
Logger::setTimezone($this->config->getTimezone());
}
public function setConfig(Config $config) {
$this->config = $config;
}
}
@@ -0,0 +1,712 @@
<?php
namespace PicoFeed\Client;
use DateTime;
use Exception;
use LogicException;
use PicoFeed\Logging\Logger;
use PicoFeed\Config\Config;
/**
* Client class.
*
* @author Frederic Guillot
*/
abstract class Client
{
/**
* Flag that say if the resource have been modified.
*
* @var bool
*/
private $is_modified = true;
/**
* HTTP Content-Type.
*
* @var string
*/
private $content_type = '';
/**
* HTTP encoding.
*
* @var string
*/
private $encoding = '';
/**
* HTTP request headers.
*
* @var array
*/
protected $request_headers = array();
/**
* HTTP Etag header.
*
* @var string
*/
protected $etag = '';
/**
* HTTP Last-Modified header.
*
* @var string
*/
protected $last_modified = '';
/**
* Expiration DateTime
*
* @var DateTime
*/
protected $expiration = null;
/**
* Proxy hostname.
*
* @var string
*/
protected $proxy_hostname = '';
/**
* Proxy port.
*
* @var int
*/
protected $proxy_port = 3128;
/**
* Proxy username.
*
* @var string
*/
protected $proxy_username = '';
/**
* Proxy password.
*
* @var string
*/
protected $proxy_password = '';
/**
* Basic auth username.
*
* @var string
*/
protected $username = '';
/**
* Basic auth password.
*
* @var string
*/
protected $password = '';
/**
* CURL options.
*
* @var array
*/
protected $additional_curl_options = array();
/**
* Client connection timeout.
*
* @var int
*/
protected $timeout = 10;
/**
* User-agent.
*
* @var string
*/
protected $user_agent = 'PicoFeed (https://github.com/miniflux/picoFeed)';
/**
* Real URL used (can be changed after a HTTP redirect).
*
* @var string
*/
protected $url = '';
/**
* Page/Feed content.
*
* @var string
*/
protected $content = '';
/**
* Number maximum of HTTP redirections to avoid infinite loops.
*
* @var int
*/
protected $max_redirects = 5;
/**
* Maximum size of the HTTP body response.
*
* @var int
*/
protected $max_body_size = 2097152; // 2MB
/**
* HTTP response status code.
*
* @var int
*/
protected $status_code = 0;
/**
* Enables direct passthrough to requesting client.
*
* @var bool
*/
protected $passthrough = false;
/**
* Do the HTTP request.
*
* @abstract
*
* @return array
*/
abstract public function doRequest();
/**
* Get client instance: curl or stream driver.
*
* @static
*
* @return \PicoFeed\Client\Client
*/
public static function getInstance()
{
if (function_exists('curl_init')) {
return new Curl();
} elseif (ini_get('allow_url_fopen')) {
return new Stream();
}
throw new LogicException('You must have "allow_url_fopen=1" or curl extension installed');
}
/**
* Add HTTP Header to the request.
*
* @param array $headers
*/
public function setHeaders($headers)
{
$this->request_headers = $headers;
}
/**
* Perform the HTTP request.
*
* @param string $url URL
*
* @return Client
*/
public function execute($url = '')
{
if ($url !== '') {
$this->url = $url;
}
Logger::setMessage(get_called_class().' Fetch URL: '.$this->url);
Logger::setMessage(get_called_class().' Etag provided: '.$this->etag);
Logger::setMessage(get_called_class().' Last-Modified provided: '.$this->last_modified);
$response = $this->doRequest();
$this->status_code = $response['status'];
$this->handleNotModifiedResponse($response);
$this->handleErrorResponse($response);
$this->handleNormalResponse($response);
$this->expiration = $this->parseExpiration($response['headers']);
Logger::setMessage(get_called_class().' Expiration: '.$this->expiration->format(DATE_ISO8601));
return $this;
}
/**
* Handle not modified response.
*
* @param array $response Client response
*/
protected function handleNotModifiedResponse(array $response)
{
if ($response['status'] == 304) {
$this->is_modified = false;
} elseif ($response['status'] == 200) {
$this->is_modified = $this->hasBeenModified($response, $this->etag, $this->last_modified);
$this->etag = $this->getHeader($response, 'ETag');
$this->last_modified = $this->getHeader($response, 'Last-Modified');
}
if ($this->is_modified === false) {
Logger::setMessage(get_called_class().' Resource not modified');
}
}
/**
* Handle Http Error codes
*
* @param array $response Client response
* @throws ForbiddenException
* @throws InvalidUrlException
* @throws UnauthorizedException
*/
protected function handleErrorResponse(array $response)
{
$status = $response['status'];
if ($status == 401) {
throw new UnauthorizedException('Wrong or missing credentials');
} else if ($status == 403) {
throw new ForbiddenException('Not allowed to access resource');
} else if ($status == 404) {
throw new InvalidUrlException('Resource not found');
}
}
/**
* Handle normal response.
*
* @param array $response Client response
*/
protected function handleNormalResponse(array $response)
{
if ($response['status'] == 200) {
$this->content = $response['body'];
$this->content_type = $this->findContentType($response);
$this->encoding = $this->findCharset();
}
}
/**
* Check if a request has been modified according to the parameters.
*
* @param array $response
* @param string $etag
* @param string $lastModified
*
* @return bool
*/
private function hasBeenModified($response, $etag, $lastModified)
{
$headers = array(
'Etag' => $etag,
'Last-Modified' => $lastModified,
);
// Compare the values for each header that is present
$presentCacheHeaderCount = 0;
foreach ($headers as $key => $value) {
if (isset($response['headers'][$key])) {
if ($response['headers'][$key] !== $value) {
return true;
}
++$presentCacheHeaderCount;
}
}
// If at least one header is present and the values match, the response
// was not modified
if ($presentCacheHeaderCount > 0) {
return false;
}
return true;
}
/**
* Find content type from response headers.
*
* @param array $response Client response
* @return string
*/
public function findContentType(array $response)
{
return strtolower($this->getHeader($response, 'Content-Type'));
}
/**
* Find charset from response headers.
*
* @return string
*/
public function findCharset()
{
$result = explode('charset=', $this->content_type);
return isset($result[1]) ? $result[1] : '';
}
/**
* Get header value from a client response.
*
* @param array $response Client response
* @param string $header Header name
* @return string
*/
public function getHeader(array $response, $header)
{
return isset($response['headers'][$header]) ? $response['headers'][$header] : '';
}
/**
* Set the Last-Modified HTTP header.
*
* @param string $last_modified Header value
* @return $this
*/
public function setLastModified($last_modified)
{
$this->last_modified = $last_modified;
return $this;
}
/**
* Get the value of the Last-Modified HTTP header.
*
* @return string
*/
public function getLastModified()
{
return $this->last_modified;
}
/**
* Set the value of the Etag HTTP header.
*
* @param string $etag Etag HTTP header value
* @return $this
*/
public function setEtag($etag)
{
$this->etag = $etag;
return $this;
}
/**
* Get the Etag HTTP header value.
*
* @return string
*/
public function getEtag()
{
return $this->etag;
}
/**
* Get the final url value.
*
* @return string
*/
public function getUrl()
{
return $this->url;
}
/**
* Set the url.
*
* @param $url
* @return string
*/
public function setUrl($url)
{
$this->url = $url;
return $this;
}
/**
* Get the HTTP response status code.
*
* @return int
*/
public function getStatusCode()
{
return $this->status_code;
}
/**
* Get the body of the HTTP response.
*
* @return string
*/
public function getContent()
{
return $this->content;
}
/**
* Get the content type value from HTTP headers.
*
* @return string
*/
public function getContentType()
{
return $this->content_type;
}
/**
* Get the encoding value from HTTP headers.
*
* @return string
*/
public function getEncoding()
{
return $this->encoding;
}
/**
* Return true if the remote resource has changed.
*
* @return bool
*/
public function isModified()
{
return $this->is_modified;
}
/**
* return true if passthrough mode is enabled.
*
* @return bool
*/
public function isPassthroughEnabled()
{
return $this->passthrough;
}
/**
* Set connection timeout.
*
* @param int $timeout Connection timeout
* @return $this
*/
public function setTimeout($timeout)
{
$this->timeout = $timeout ?: $this->timeout;
return $this;
}
/**
* Set a custom user agent.
*
* @param string $user_agent User Agent
* @return $this
*/
public function setUserAgent($user_agent)
{
$this->user_agent = $user_agent ?: $this->user_agent;
return $this;
}
/**
* Set the maximum number of HTTP redirections.
*
* @param int $max Maximum
* @return $this
*/
public function setMaxRedirections($max)
{
$this->max_redirects = $max ?: $this->max_redirects;
return $this;
}
/**
* Set the maximum size of the HTTP body.
*
* @param int $max Maximum
* @return $this
*/
public function setMaxBodySize($max)
{
$this->max_body_size = $max ?: $this->max_body_size;
return $this;
}
/**
* Set the proxy hostname.
*
* @param string $hostname Proxy hostname
* @return $this
*/
public function setProxyHostname($hostname)
{
$this->proxy_hostname = $hostname ?: $this->proxy_hostname;
return $this;
}
/**
* Set the proxy port.
*
* @param int $port Proxy port
* @return $this
*/
public function setProxyPort($port)
{
$this->proxy_port = $port ?: $this->proxy_port;
return $this;
}
/**
* Set the proxy username.
*
* @param string $username Proxy username
* @return $this
*/
public function setProxyUsername($username)
{
$this->proxy_username = $username ?: $this->proxy_username;
return $this;
}
/**
* Set the proxy password.
*
* @param string $password Password
* @return $this
*/
public function setProxyPassword($password)
{
$this->proxy_password = $password ?: $this->proxy_password;
return $this;
}
/**
* Set the username.
*
* @param string $username Basic Auth username
*
* @return $this
*/
public function setUsername($username)
{
$this->username = $username ?: $this->username;
return $this;
}
/**
* Set the password.
*
* @param string $password Basic Auth Password
*
* @return $this
*/
public function setPassword($password)
{
$this->password = $password ?: $this->password;
return $this;
}
/**
* Set the CURL options.
*
* @param array $options
* @return $this
*/
public function setAdditionalCurlOptions(array $options)
{
$this->additional_curl_options = $options ?: $this->additional_curl_options;
return $this;
}
/**
* Enable the passthrough mode.
*
* @return $this
*/
public function enablePassthroughMode()
{
$this->passthrough = true;
return $this;
}
/**
* Disable the passthrough mode.
*
* @return $this
*/
public function disablePassthroughMode()
{
$this->passthrough = false;
return $this;
}
/**
* Set config object.
*
* @param \PicoFeed\Config\Config $config Config instance
* @return $this
*/
public function setConfig(Config $config)
{
if ($config !== null) {
$this->setTimeout($config->getClientTimeout());
$this->setUserAgent($config->getClientUserAgent());
$this->setMaxRedirections($config->getMaxRedirections());
$this->setMaxBodySize($config->getMaxBodySize());
$this->setProxyHostname($config->getProxyHostname());
$this->setProxyPort($config->getProxyPort());
$this->setProxyUsername($config->getProxyUsername());
$this->setProxyPassword($config->getProxyPassword());
$this->setAdditionalCurlOptions($config->getAdditionalCurlOptions() ?: array());
}
return $this;
}
/**
* Return true if the HTTP status code is a redirection
*
* @access protected
* @param integer $code
* @return boolean
*/
public function isRedirection($code)
{
return $code == 301 || $code == 302 || $code == 303 || $code == 307;
}
public function parseExpiration(HttpHeaders $headers)
{
try {
if (isset($headers['Cache-Control'])) {
if (preg_match('/s-maxage=(\d+)/', $headers['Cache-Control'], $matches)) {
return new DateTime('+' . $matches[1] . ' seconds');
} else if (preg_match('/max-age=(\d+)/', $headers['Cache-Control'], $matches)) {
return new DateTime('+' . $matches[1] . ' seconds');
}
}
if (! empty($headers['Expires'])) {
return new DateTime($headers['Expires']);
}
} catch (Exception $e) {
Logger::setMessage('Unable to parse expiration date: '.$e->getMessage());
}
return new DateTime();
}
/**
* Get expiration date time from "Expires" or "Cache-Control" headers
*
* @return DateTime
*/
public function getExpiration()
{
return $this->expiration ?: new DateTime();
}
}
@@ -0,0 +1,14 @@
<?php
namespace PicoFeed\Client;
use PicoFeed\PicoFeedException;
/**
* ClientException Exception.
*
* @author Frederic Guillot
*/
abstract class ClientException extends PicoFeedException
{
}
@@ -0,0 +1,412 @@
<?php
namespace PicoFeed\Client;
use PicoFeed\Logging\Logger;
/**
* cURL HTTP client.
*
* @author Frederic Guillot
*/
class Curl extends Client
{
protected $nbRedirects = 0;
/**
* HTTP response body.
*
* @var string
*/
private $body = '';
/**
* Body size.
*
* @var int
*/
private $body_length = 0;
/**
* HTTP response headers.
*
* @var array
*/
private $response_headers = array();
/**
* Counter on the number of header received.
*
* @var int
*/
private $response_headers_count = 0;
/**
* cURL callback to read the HTTP body.
*
* If the function return -1, curl stop to read the HTTP response
*
* @param resource $ch cURL handler
* @param string $buffer Chunk of data
*
* @return int Length of the buffer
*/
public function readBody($ch, $buffer)
{
$length = strlen($buffer);
$this->body_length += $length;
if ($this->body_length > $this->max_body_size) {
return -1;
}
$this->body .= $buffer;
return $length;
}
/**
* cURL callback to read HTTP headers.
*
* @param resource $ch cURL handler
* @param string $buffer Header line
*
* @return int Length of the buffer
*/
public function readHeaders($ch, $buffer)
{
$length = strlen($buffer);
if ($buffer === "\r\n" || $buffer === "\n") {
++$this->response_headers_count;
} else {
if (!isset($this->response_headers[$this->response_headers_count])) {
$this->response_headers[$this->response_headers_count] = '';
}
$this->response_headers[$this->response_headers_count] .= $buffer;
}
return $length;
}
/**
* cURL callback to passthrough the HTTP body to the client.
*
* If the function return -1, curl stop to read the HTTP response
*
* @param resource $ch cURL handler
* @param string $buffer Chunk of data
*
* @return int Length of the buffer
*/
public function passthroughBody($ch, $buffer)
{
// do it only at the beginning of a transmission
if ($this->body_length === 0) {
list($status, $headers) = HttpHeaders::parse(explode("\n", $this->response_headers[$this->response_headers_count - 1]));
if ($this->isRedirection($status)) {
return $this->handleRedirection($headers['Location']);
}
if (isset($headers['Content-Type'])) {
header('Content-Type:' .$headers['Content-Type']);
}
}
$length = strlen($buffer);
$this->body_length += $length;
echo $buffer;
return $length;
}
/**
* Prepare HTTP headers.
*
* @return string[]
*/
private function prepareHeaders()
{
$headers = array(
'Connection: close',
);
if ($this->etag) {
$headers[] = 'If-None-Match: '.$this->etag;
$headers[] = 'A-IM: feed';
}
if ($this->last_modified) {
$headers[] = 'If-Modified-Since: '.$this->last_modified;
}
$headers = array_merge($headers, $this->request_headers);
return $headers;
}
/**
* Prepare curl proxy context.
*
* @param resource $ch
*
* @return resource $ch
*/
private function prepareProxyContext($ch)
{
if ($this->proxy_hostname) {
Logger::setMessage(get_called_class().' Proxy: '.$this->proxy_hostname.':'.$this->proxy_port);
curl_setopt($ch, CURLOPT_PROXYPORT, $this->proxy_port);
curl_setopt($ch, CURLOPT_PROXYTYPE, 'HTTP');
curl_setopt($ch, CURLOPT_PROXY, $this->proxy_hostname);
if ($this->proxy_username) {
Logger::setMessage(get_called_class().' Proxy credentials: Yes');
curl_setopt($ch, CURLOPT_PROXYUSERPWD, $this->proxy_username.':'.$this->proxy_password);
} else {
Logger::setMessage(get_called_class().' Proxy credentials: No');
}
}
return $ch;
}
/**
* Prepare curl auth context.
*
* @param resource $ch
*
* @return resource $ch
*/
private function prepareAuthContext($ch)
{
if ($this->username && $this->password) {
curl_setopt($ch, CURLOPT_USERPWD, $this->username.':'.$this->password);
}
return $ch;
}
/**
* Set write/header functions.
*
* @param resource $ch
*
* @return resource $ch
*/
private function prepareDownloadMode($ch)
{
$this->body = '';
$this->response_headers = array();
$this->response_headers_count = 0;
$write_function = 'readBody';
$header_function = 'readHeaders';
if ($this->isPassthroughEnabled()) {
$write_function = 'passthroughBody';
}
curl_setopt($ch, CURLOPT_WRITEFUNCTION, array($this, $write_function));
curl_setopt($ch, CURLOPT_HEADERFUNCTION, array($this, $header_function));
return $ch;
}
/**
* Set additional CURL options.
*
* @param resource $ch
*
* @return resource $ch
*/
private function prepareAdditionalCurlOptions($ch){
foreach( $this->additional_curl_options as $c_op => $c_val ){
curl_setopt($ch, $c_op, $c_val);
}
return $ch;
}
/**
* Prepare curl context.
*
* @return resource
*/
private function prepareContext()
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->url);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout);
curl_setopt($ch, CURLOPT_USERAGENT, $this->user_agent);
curl_setopt($ch, CURLOPT_HTTPHEADER, $this->prepareHeaders());
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
curl_setopt($ch, CURLOPT_ENCODING, '');
curl_setopt($ch, CURLOPT_COOKIEJAR, 'php://memory');
curl_setopt($ch, CURLOPT_COOKIEFILE, 'php://memory');
// Disable SSLv3 by enforcing TLSv1.x for curl >= 7.34.0 and < 7.39.0.
// Versions prior to 7.34 and at least when compiled against openssl
// interpret this parameter as "limit to TLSv1.0" which fails for sites
// which enforce TLS 1.1+.
// Starting with curl 7.39.0 SSLv3 is disabled by default.
$version = curl_version();
if ($version['version_number'] >= 467456 && $version['version_number'] < 468736) {
curl_setopt($ch, CURLOPT_SSLVERSION, 1);
}
$ch = $this->prepareDownloadMode($ch);
$ch = $this->prepareProxyContext($ch);
$ch = $this->prepareAuthContext($ch);
$ch = $this->prepareAdditionalCurlOptions($ch);
return $ch;
}
/**
* Execute curl context.
*/
private function executeContext()
{
$ch = $this->prepareContext();
curl_exec($ch);
Logger::setMessage(get_called_class().' cURL total time: '.curl_getinfo($ch, CURLINFO_TOTAL_TIME));
Logger::setMessage(get_called_class().' cURL dns lookup time: '.curl_getinfo($ch, CURLINFO_NAMELOOKUP_TIME));
Logger::setMessage(get_called_class().' cURL connect time: '.curl_getinfo($ch, CURLINFO_CONNECT_TIME));
Logger::setMessage(get_called_class().' cURL speed download: '.curl_getinfo($ch, CURLINFO_SPEED_DOWNLOAD));
Logger::setMessage(get_called_class().' cURL effective url: '.curl_getinfo($ch, CURLINFO_EFFECTIVE_URL));
$curl_errno = curl_errno($ch);
if ($curl_errno) {
Logger::setMessage(get_called_class().' cURL error: '.curl_error($ch));
curl_close($ch);
$this->handleError($curl_errno);
}
// Update the url if there where redirects
$this->url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
curl_close($ch);
}
/**
* Do the HTTP request.
*
* @return array HTTP response ['body' => ..., 'status' => ..., 'headers' => ...]
*/
public function doRequest()
{
$this->executeContext();
list($status, $headers) = HttpHeaders::parse(explode("\n", $this->response_headers[$this->response_headers_count - 1]));
if ($this->isRedirection($status)) {
if (empty($headers['Location'])) {
$status = 200;
} else {
return $this->handleRedirection($headers['Location']);
}
}
return array(
'status' => $status,
'body' => $this->body,
'headers' => $headers,
);
}
/**
* Handle HTTP redirects
*
* @param string $location Redirected URL
* @return array
* @throws MaxRedirectException
*/
private function handleRedirection($location)
{
$result = array();
$this->url = Url::resolve($location, $this->url);
$this->body = '';
$this->body_length = 0;
$this->response_headers = array();
$this->response_headers_count = 0;
while (true) {
$this->nbRedirects++;
if ($this->nbRedirects >= $this->max_redirects) {
throw new MaxRedirectException('Maximum number of redirections reached');
}
$result = $this->doRequest();
if ($this->isRedirection($result['status'])) {
$this->url = Url::resolve($result['headers']['Location'], $this->url);
$this->body = '';
$this->body_length = 0;
$this->response_headers = array();
$this->response_headers_count = 0;
} else {
break;
}
}
return $result;
}
/**
* Handle cURL errors (throw individual exceptions).
*
* We don't use constants because they are not necessary always available
* (depends of the version of libcurl linked to php)
*
* @see http://curl.haxx.se/libcurl/c/libcurl-errors.html
*
* @param int $errno cURL error code
* @throws InvalidCertificateException
* @throws InvalidUrlException
* @throws MaxRedirectException
* @throws MaxSizeException
* @throws TimeoutException
*/
private function handleError($errno)
{
switch ($errno) {
case 78: // CURLE_REMOTE_FILE_NOT_FOUND
throw new InvalidUrlException('Resource not found', $errno);
case 6: // CURLE_COULDNT_RESOLVE_HOST
throw new InvalidUrlException('Unable to resolve hostname', $errno);
case 7: // CURLE_COULDNT_CONNECT
throw new InvalidUrlException('Unable to connect to the remote host', $errno);
case 23: // CURLE_WRITE_ERROR
throw new MaxSizeException('Maximum response size exceeded', $errno);
case 28: // CURLE_OPERATION_TIMEDOUT
throw new TimeoutException('Operation timeout', $errno);
case 35: // CURLE_SSL_CONNECT_ERROR
case 51: // CURLE_PEER_FAILED_VERIFICATION
case 58: // CURLE_SSL_CERTPROBLEM
case 60: // CURLE_SSL_CACERT
case 59: // CURLE_SSL_CIPHER
case 64: // CURLE_USE_SSL_FAILED
case 66: // CURLE_SSL_ENGINE_INITFAILED
case 77: // CURLE_SSL_CACERT_BADFILE
case 83: // CURLE_SSL_ISSUER_ERROR
$msg = 'Invalid SSL certificate caused by CURL error number ' . $errno;
throw new InvalidCertificateException($msg, $errno);
case 47: // CURLE_TOO_MANY_REDIRECTS
throw new MaxRedirectException('Maximum number of redirections reached', $errno);
case 63: // CURLE_FILESIZE_EXCEEDED
throw new MaxSizeException('Maximum response size exceeded', $errno);
default:
throw new InvalidUrlException('Unable to fetch the URL', $errno);
}
}
}
@@ -0,0 +1,10 @@
<?php
namespace PicoFeed\Client;
/**
* @author Bernhard Posselt
*/
class ForbiddenException extends ClientException
{
}
@@ -0,0 +1,79 @@
<?php
namespace PicoFeed\Client;
use ArrayAccess;
use PicoFeed\Logging\Logger;
/**
* Class to handle HTTP headers case insensitivity.
*
* @author Bernhard Posselt
* @author Frederic Guillot
*/
class HttpHeaders implements ArrayAccess
{
private $headers = array();
public function __construct(array $headers)
{
foreach ($headers as $key => $value) {
$this->headers[strtolower($key)] = $value;
}
}
public function offsetGet($offset)
{
return $this->offsetExists($offset) ? $this->headers[strtolower($offset)] : '';
}
public function offsetSet($offset, $value)
{
$this->headers[strtolower($offset)] = $value;
}
public function offsetExists($offset)
{
return isset($this->headers[strtolower($offset)]);
}
public function offsetUnset($offset)
{
unset($this->headers[strtolower($offset)]);
}
/**
* Parse HTTP headers.
*
* @static
*
* @param array $lines List of headers
*
* @return array
*/
public static function parse(array $lines)
{
$status = 0;
$headers = array();
foreach ($lines as $line) {
if (strpos($line, 'HTTP/1') === 0) {
$headers = array();
$status = (int) substr($line, 9, 3);
} elseif (strpos($line, ': ') !== false) {
list($name, $value) = explode(': ', $line);
if ($value) {
$headers[trim($name)] = trim($value);
}
}
}
Logger::setMessage(get_called_class().' HTTP status code: '.$status);
foreach ($headers as $name => $value) {
Logger::setMessage(get_called_class().' HTTP header: '.$name.' => '.$value);
}
return array($status, new self($headers));
}
}
@@ -0,0 +1,12 @@
<?php
namespace PicoFeed\Client;
/**
* InvalidCertificateException Exception.
*
* @author Frederic Guillot
*/
class InvalidCertificateException extends ClientException
{
}
@@ -0,0 +1,12 @@
<?php
namespace PicoFeed\Client;
/**
* InvalidUrlException Exception.
*
* @author Frederic Guillot
*/
class InvalidUrlException extends ClientException
{
}
@@ -0,0 +1,12 @@
<?php
namespace PicoFeed\Client;
/**
* MaxRedirectException Exception.
*
* @author Frederic Guillot
*/
class MaxRedirectException extends ClientException
{
}
@@ -0,0 +1,12 @@
<?php
namespace PicoFeed\Client;
/**
* MaxSizeException Exception.
*
* @author Frederic Guillot
*/
class MaxSizeException extends ClientException
{
}

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