diff --git a/exam/composer.json b/exam/composer.json new file mode 100644 index 0000000..89253c3 --- /dev/null +++ b/exam/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "pecee/simple-router": "^5.4" + } +} diff --git a/exam/composer.lock b/exam/composer.lock new file mode 100644 index 0000000..7f8cb59 --- /dev/null +++ b/exam/composer.lock @@ -0,0 +1,82 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "d067465d9a99de373ba3093d61eda1d8", + "packages": [ + { + "name": "pecee/simple-router", + "version": "5.4.1.7", + "source": { + "type": "git", + "url": "https://github.com/skipperbent/simple-php-router.git", + "reference": "a2843d5b1e037f8b61cc99f27eab52a28bf41dfd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/skipperbent/simple-php-router/zipball/a2843d5b1e037f8b61cc99f27eab52a28bf41dfd", + "reference": "a2843d5b1e037f8b61cc99f27eab52a28bf41dfd", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=7.4" + }, + "require-dev": { + "mockery/mockery": "^1", + "phpstan/phpstan": "^1", + "phpstan/phpstan-deprecation-rules": "^1", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1", + "phpunit/phpunit": "^8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Pecee\\": "src/Pecee/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Simon Sessingø", + "email": "simon.sessingoe@gmail.com" + } + ], + "description": "Simple, fast PHP router that is easy to get integrated and in almost any project. Heavily inspired by the Laravel router.", + "keywords": [ + "framework", + "input-handler", + "laravel", + "pecee", + "php", + "request-handler", + "route", + "router", + "routing", + "routing-engine", + "simple-php-router", + "url-handling" + ], + "support": { + "issues": "https://github.com/skipperbent/simple-php-router/issues", + "source": "https://github.com/skipperbent/simple-php-router/issues" + }, + "time": "2023-12-11T21:48:25+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/exam/vendor/autoload.php b/exam/vendor/autoload.php new file mode 100644 index 0000000..3ae99ed --- /dev/null +++ b/exam/vendor/autoload.php @@ -0,0 +1,25 @@ + + * Jordi Boggiano + * + * 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 + * @author Jordi Boggiano + * @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> + */ + private $prefixLengthsPsr4 = array(); + /** + * @var array> + */ + private $prefixDirsPsr4 = array(); + /** + * @var list + */ + 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>> + */ + private $prefixesPsr0 = array(); + /** + * @var list + */ + private $fallbackDirsPsr0 = array(); + + /** @var bool */ + private $useIncludePath = false; + + /** + * @var array + */ + private $classMap = array(); + + /** @var bool */ + private $classMapAuthoritative = false; + + /** + * @var array + */ + private $missingClasses = array(); + + /** @var string|null */ + private $apcuPrefix; + + /** + * @var array + */ + private static $registeredLoaders = array(); + + /** + * @param string|null $vendorDir + */ + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + self::initializeIncludeClosure(); + } + + /** + * @return array> + */ + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + /** + * @return array> + */ + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + /** + * @return list + */ + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + /** + * @return list + */ + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + /** + * @return array Array of classname => path + */ + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $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 $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 $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 $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 $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 + */ + 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); + } +} diff --git a/exam/vendor/composer/InstalledVersions.php b/exam/vendor/composer/InstalledVersions.php new file mode 100644 index 0000000..51e734a --- /dev/null +++ b/exam/vendor/composer/InstalledVersions.php @@ -0,0 +1,359 @@ + + * Jordi Boggiano + * + * 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 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}|array{}|null + */ + private static $installed; + + /** + * @var bool|null + */ + private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array}> + */ + 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 + */ + 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 + */ + 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} + */ + 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}> + */ + 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} $data + */ + public static function reload($data) + { + self::$installed = $data; + self::$installedByVendor = array(); + } + + /** + * @return array[] + * @psalm-return list}> + */ + private static function getInstalled() + { + if (null === self::$canGetVendors) { + self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); + } + + $installed = array(); + + if (self::$canGetVendors) { + foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + 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} $required */ + $required = require $vendorDir.'/composer/installed.php'; + $installed[] = self::$installedByVendor[$vendorDir] = $required; + if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { + self::$installed = $installed[count($installed) - 1]; + } + } + } + } + + 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} $required */ + $required = require __DIR__ . '/installed.php'; + self::$installed = $required; + } else { + self::$installed = array(); + } + } + + if (self::$installed !== array()) { + $installed[] = self::$installed; + } + + return $installed; + } +} diff --git a/exam/vendor/composer/LICENSE b/exam/vendor/composer/LICENSE new file mode 100644 index 0000000..f27399a --- /dev/null +++ b/exam/vendor/composer/LICENSE @@ -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. + diff --git a/exam/vendor/composer/autoload_classmap.php b/exam/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000..0fb0a2c --- /dev/null +++ b/exam/vendor/composer/autoload_classmap.php @@ -0,0 +1,10 @@ + $vendorDir . '/composer/InstalledVersions.php', +); diff --git a/exam/vendor/composer/autoload_namespaces.php b/exam/vendor/composer/autoload_namespaces.php new file mode 100644 index 0000000..15a2ff3 --- /dev/null +++ b/exam/vendor/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ + array($vendorDir . '/pecee/simple-router/src/Pecee'), +); diff --git a/exam/vendor/composer/autoload_real.php b/exam/vendor/composer/autoload_real.php new file mode 100644 index 0000000..2b43424 --- /dev/null +++ b/exam/vendor/composer/autoload_real.php @@ -0,0 +1,38 @@ +register(true); + + return $loader; + } +} diff --git a/exam/vendor/composer/autoload_static.php b/exam/vendor/composer/autoload_static.php new file mode 100644 index 0000000..d798ace --- /dev/null +++ b/exam/vendor/composer/autoload_static.php @@ -0,0 +1,36 @@ + + array ( + 'Pecee\\' => 6, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'Pecee\\' => + array ( + 0 => __DIR__ . '/..' . '/pecee/simple-router/src/Pecee', + ), + ); + + public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInitd067465d9a99de373ba3093d61eda1d8::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInitd067465d9a99de373ba3093d61eda1d8::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInitd067465d9a99de373ba3093d61eda1d8::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/exam/vendor/composer/installed.json b/exam/vendor/composer/installed.json new file mode 100644 index 0000000..841f366 --- /dev/null +++ b/exam/vendor/composer/installed.json @@ -0,0 +1,72 @@ +{ + "packages": [ + { + "name": "pecee/simple-router", + "version": "5.4.1.7", + "version_normalized": "5.4.1.7", + "source": { + "type": "git", + "url": "https://github.com/skipperbent/simple-php-router.git", + "reference": "a2843d5b1e037f8b61cc99f27eab52a28bf41dfd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/skipperbent/simple-php-router/zipball/a2843d5b1e037f8b61cc99f27eab52a28bf41dfd", + "reference": "a2843d5b1e037f8b61cc99f27eab52a28bf41dfd", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=7.4" + }, + "require-dev": { + "mockery/mockery": "^1", + "phpstan/phpstan": "^1", + "phpstan/phpstan-deprecation-rules": "^1", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1", + "phpunit/phpunit": "^8" + }, + "time": "2023-12-11T21:48:25+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Pecee\\": "src/Pecee/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Simon Sessingø", + "email": "simon.sessingoe@gmail.com" + } + ], + "description": "Simple, fast PHP router that is easy to get integrated and in almost any project. Heavily inspired by the Laravel router.", + "keywords": [ + "framework", + "input-handler", + "laravel", + "pecee", + "php", + "request-handler", + "route", + "router", + "routing", + "routing-engine", + "simple-php-router", + "url-handling" + ], + "support": { + "issues": "https://github.com/skipperbent/simple-php-router/issues", + "source": "https://github.com/skipperbent/simple-php-router/issues" + }, + "install-path": "../pecee/simple-router" + } + ], + "dev": true, + "dev-package-names": [] +} diff --git a/exam/vendor/composer/installed.php b/exam/vendor/composer/installed.php new file mode 100644 index 0000000..ab447e5 --- /dev/null +++ b/exam/vendor/composer/installed.php @@ -0,0 +1,32 @@ + array( + 'name' => '__root__', + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'reference' => '156d277e775d31b796bdb4cdd123800f41b0a1cb', + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev' => true, + ), + 'versions' => array( + '__root__' => array( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'reference' => '156d277e775d31b796bdb4cdd123800f41b0a1cb', + 'type' => 'library', + 'install_path' => __DIR__ . '/../../', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'pecee/simple-router' => array( + 'pretty_version' => '5.4.1.7', + 'version' => '5.4.1.7', + 'reference' => 'a2843d5b1e037f8b61cc99f27eab52a28bf41dfd', + 'type' => 'library', + 'install_path' => __DIR__ . '/../pecee/simple-router', + 'aliases' => array(), + 'dev_requirement' => false, + ), + ), +); diff --git a/exam/vendor/composer/platform_check.php b/exam/vendor/composer/platform_check.php new file mode 100644 index 0000000..580fa96 --- /dev/null +++ b/exam/vendor/composer/platform_check.php @@ -0,0 +1,26 @@ += 70400)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 7.4.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; + } + } + trigger_error( + 'Composer detected issues in your platform: ' . implode(' ', $issues), + E_USER_ERROR + ); +} diff --git a/exam/vendor/pecee/simple-router/.github/workflows/ci.yml b/exam/vendor/pecee/simple-router/.github/workflows/ci.yml new file mode 100644 index 0000000..f6a9566 --- /dev/null +++ b/exam/vendor/pecee/simple-router/.github/workflows/ci.yml @@ -0,0 +1,59 @@ +name: CI + +on: [push, pull_request] + +jobs: + build-test: + runs-on: ${{ matrix.os }} + + env: + PHP_EXTENSIONS: json + PHP_INI_VALUES: assert.exception=1, zend.assertions=1 + + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - windows-latest + php-version: + - 7.4 + - 8.0 + phpunit-version: + - 8.5.32 + dependencies: + - lowest + - highest + name: PHPUnit Tests + steps: + - name: Configure git to avoid issues with line endings + if: matrix.os == 'windows-latest' + run: git config --global core.autocrlf false + - name: Checkout + uses: actions/checkout@v2 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + tools: composer:v5, phpunit:${{ matrix.phpunit-versions }} + coverage: xdebug + extensions: ${{ env.PHP_EXTENSIONS }} + ini-values: ${{ env.PHP_INI_VALUES }} + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: php${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('**/composer.json') }} + restore-keys: | + php${{ matrix.php-version }}-composer-${{ matrix.dependencies }}- + - name: Install lowest dependencies with composer + if: matrix.dependencies == 'lowest' + run: composer update --no-ansi --no-interaction --no-progress --prefer-lowest + - name: Install highest dependencies with composer + if: matrix.dependencies == 'highest' + run: composer update --no-ansi --no-interaction --no-progress + - name: Run tests with phpunit + run: composer test \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/.gitignore b/exam/vendor/pecee/simple-router/.gitignore new file mode 100644 index 0000000..152cb91 --- /dev/null +++ b/exam/vendor/pecee/simple-router/.gitignore @@ -0,0 +1,5 @@ +composer.lock +vendor/ +.idea/ +.phpunit.result.cache +tests/tmp \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/README.md b/exam/vendor/pecee/simple-router/README.md new file mode 100644 index 0000000..3dbbb21 --- /dev/null +++ b/exam/vendor/pecee/simple-router/README.md @@ -0,0 +1,2105 @@ +# simple-router + +Simple, fast and yet powerful PHP router that is easy to get integrated and in any project. Heavily inspired by the way Laravel handles routing, with both simplicity and expand-ability in mind. + +With simple-router you can create a new project fast, without depending on a framework. + +**It only takes a few lines of code to get started:** + +```php +SimpleRouter::get('/', function() { + return 'Hello world'; +}); +``` + +### Support the project + +If you like simple-router and wish to see the continued development and maintenance of the project, please consider showing your support by buying me a coffee. Supporters will be listed under the credits section of this documentation. + +You can donate any amount of your choice by [clicking here](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=NNX4D2RUSALCN). + +## Table of Contents + +- [Getting started](#getting-started) + - [Notes](#notes-1) + - [Requirements](#requirements) + - [Features](#features) + - [Installation](#installation) + - [Setting up Apache](#setting-up-apache) + - [Setting up Nginx](#setting-up-nginx) + - [Setting up IIS](#setting-up-iis) + - [Configuration](#configuration) + - [Helper functions](#helper-functions) +- [Routes](#routes) + - [Basic routing](#basic-routing) + - [Class hinting](#class-hinting) + - [Available methods](#available-methods) + - [Multiple HTTP-verbs](#multiple-http-verbs) + - [Route parameters](#route-parameters) + - [Required parameters](#required-parameters) + - [Optional parameters](#optional-parameters) + - [Including slash in parameters](#including-slash-in-parameters) + - [Regular expression constraints](#regular-expression-constraints) + - [Regular expression route-match](#regular-expression-route-match) + - [Custom regex for matching parameters](#custom-regex-for-matching-parameters) + - [Named routes](#named-routes) + - [Generating URLs To Named Routes](#generating-urls-to-named-routes) + - [Router groups](#router-groups) + - [Middleware](#middleware) + - [Namespaces](#namespaces) + - [Subdomain-routing](#subdomain-routing) + - [Route prefixes](#route-prefixes) + - [Partial groups](#partial-groups) + - [Form Method Spoofing](#form-method-spoofing) + - [Accessing The Current Route](#accessing-the-current-route) + - [Other examples](#other-examples) +- [CSRF-protection](#csrf-protection) + - [Adding CSRF-verifier](#adding-csrf-verifier) + - [Getting CSRF-token](#getting-csrf-token) + - [Custom CSRF-verifier](#custom-csrf-verifier) + - [Custom Token-provider](#custom-token-provider) +- [Middlewares](#middlewares) + - [Example](#example-1) +- [ExceptionHandlers](#exceptionhandlers) + - [Handling 404, 403 and other errors](#handling-404-403-and-other-errors) + - [Using custom exception handlers](#using-custom-exception-handlers) + - [Prevent merge of parent exception-handlers](#prevent-merge-of-parent-exception-handlers) +- [Urls](#urls) + - [Get the current url](#get-the-current-url) + - [Get by name (single route)](#get-by-name-single-route) + - [Get by name (controller route)](#get-by-name-controller-route) + - [Get by class](#get-by-class) + - [Using custom names for methods on a controller/resource route](#using-custom-names-for-methods-on-a-controllerresource-route) + - [Getting REST/resource controller urls](#getting-restresource-controller-urls) + - [Manipulating url](#manipulating-url) + - [Useful url tricks](#useful-url-tricks) +- [Input & parameters](#input--parameters) + - [Using the Input class to manage parameters](#using-the-input-class-to-manage-parameters) + - [Get single parameter value](#get-single-parameter-value) + - [Get parameter object](#get-parameter-object) + - [Managing files](#managing-files) + - [Get all parameters](#get-all-parameters) + - [Check if parameters exists](#check-if-parameters-exists) +- [Events](#events) + - [Available events](#available-events) + - [Registering new event](#registering-new-event) + - [Custom EventHandlers](#custom-eventhandlers) +- [Advanced](#advanced) + - [Multiple route rendering](#multiple-route-rendering) + - [Restrict access to IP](#restrict-access-to-ip) + - [Setting custom base path](#setting-custom-base-path) + - [Url rewriting](#url-rewriting) + - [Changing current route](#changing-current-route) + - [Bootmanager: loading routes dynamically](#bootmanager-loading-routes-dynamically) + - [Adding routes manually](#adding-routes-manually) + - [Custom class-loader](#custom-class-loader) + - [Integrating with php-di](#Integrating-with-php-di) + - [Parameters](#parameters) + - [Extending](#extending) +- [Help and support](#help-and-support) + - [Common issues and fixes](#common-issues-and-fixes) + - [Multiple routes matches? Which one has the priority?](#multiple-routes-matches-which-one-has-the-priority) + - [Parameters won't match or route not working with special characters](#parameters-wont-match-or-route-not-working-with-special-characters) + - [Using the router on sub-paths](#using-the-router-on-sub-paths) + - [Debugging](#debugging) + - [Creating unit-tests](#creating-unit-tests) + - [Debug information](#debug-information) + - [Benchmark and log-info](#benchmark-and-log-info) + - [Reporting a new issue](#reporting-a-new-issue) + - [Procedure for reporting a new issue](#procedure-for-reporting-a-new-issue) + - [Issue template](#issue-template) + - [Feedback and development](#feedback-and-development) + - [Contribution development guidelines](#contribution-development-guidelines) +- [Credits](#credits) + - [Sites](#sites) + - [License](#license) + +___ + +# Getting started + +Add the latest version of the simple-router project running this command. + +``` +composer require pecee/simple-router +``` + +## Notes + +The goal of this project is to create a router that is more or less 100% compatible with the Laravel documentation, while remaining as simple as possible, and as easy to integrate and change without compromising either speed or complexity. Being lightweight is the #1 priority. + +We've included a simple demo project for the router which can be found [here](https://github.com/skipperbent/simple-router-demo). This project should give you a basic understanding of how to setup and use simple-php-router project. + +Please note that the demo-project only covers how to integrate the `simple-php-router` in a project without an existing framework. If you are using a framework in your project, the implementation might vary. + +You can find the demo-project here: [https://github.com/skipperbent/simple-router-demo](https://github.com/skipperbent/simple-router-demo) + +**What we won't cover:** + +- How to setup a solution that fits your need. This is a basic demo to help you get started. +- Understanding of MVC; including Controllers, Middlewares or ExceptionHandlers. +- How to integrate into third party frameworks. + +**What we cover:** + +- How to get up and running fast - from scratch. +- How to get ExceptionHandlers, Middlewares and Controllers working. +- How to setup your webservers. + +## Requirements + +- PHP 7.1 or greater (version 3.x and below supports PHP 5.5+) +- PHP JSON extension enabled. + +## Features + +- Basic routing (`GET`, `POST`, `PUT`, `PATCH`, `UPDATE`, `DELETE`) with support for custom multiple verbs. +- Regular Expression Constraints for parameters. +- Named routes. +- Generating url to routes. +- Route groups. +- Middleware (classes that intercepts before the route is rendered). +- Namespaces. +- Route prefixes. +- CSRF protection. +- Optional parameters +- Sub-domain routing +- Custom boot managers to rewrite urls to "nicer" ones. +- Input manager; easily manage `GET`, `POST` and `FILE` values. +- IP based restrictions. +- Easily extendable. + +## Installation + +1. Navigate to your project folder in terminal and run the following command: + +```php +composer require pecee/simple-router +``` + +### Setting up Nginx + +If you are using Nginx please make sure that url-rewriting is enabled. + +You can easily enable url-rewriting by adding the following configuration for the Nginx configuration-file for the demo-project. + +``` +location / { + try_files $uri $uri/ /index.php?$query_string; +} +``` + +### Setting up Apache + +Nothing special is required for Apache to work. We've include the `.htaccess` file in the `public` folder. If rewriting is not working for you, please check that the `mod_rewrite` module (htaccess support) is enabled in the Apache configuration. + +#### .htaccess example + +Below is an example of an working `.htaccess` file used by simple-php-router. + +Simply create a new `.htaccess` file in your projects `public` directory and paste the contents below in your newly created file. This will redirect all requests to your `index.php` file (see Configuration section below). + +``` +RewriteEngine on +RewriteCond %{SCRIPT_FILENAME} !-f +RewriteCond %{SCRIPT_FILENAME} !-d +RewriteCond %{SCRIPT_FILENAME} !-l +RewriteRule ^(.*)$ index.php/$1 +``` + +### Setting up IIS + +On IIS you have to add some lines your `web.config` file in the `public` folder or create a new one. If rewriting is not working for you, please check that your IIS version have included the `url rewrite` module or download and install them from Microsoft web site. + +#### web.config example + +Below is an example of an working `web.config` file used by simple-php-router. + +Simply create a new `web.config` file in your projects `public` directory and paste the contents below in your newly created file. This will redirect all requests to your `index.php` file (see Configuration section below). If the `web.config` file already exists, add the `` section inside the `` branch. + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +#### Troubleshooting + +If you do not have a `favicon.ico` file in your project, you can get a `NotFoundHttpException` (404 - not found). + +To add `favicon.ico` to the IIS ignore-list, add the following line to the `` group: + +``` + +``` + +You can also make one exception for files with some extensions: + +``` + +``` + +If you are using `$_SERVER['ORIG_PATH_INFO']`, you will get `\index.php\` as part of the returned value. + +**Example:** + +``` +/index.php/test/mypage.php +``` + +### Configuration + +Create a new file, name it `routes.php` and place it in your library folder. This will be the file where you define all the routes for your project. + +**WARNING: NEVER PLACE YOUR ROUTES.PHP IN YOUR PUBLIC FOLDER!** + +In your ```index.php``` require your newly-created ```routes.php``` and call the ```SimpleRouter::start()``` method. This will trigger and do the actual routing of the requests. + +It's not required, but you can set `SimpleRouter::setDefaultNamespace('\Demo\Controllers');` to prefix all routes with the namespace to your controllers. This will simplify things a bit, as you won't have to specify the namespace for your controllers on each route. + +**This is an example of a basic ```index.php``` file:** + +```php +getInputHandler()->value($index, $defaultValue, ...$methods); + } + + return request()->getInputHandler(); +} + +/** + * @param string $url + * @param int|null $code + */ +function redirect(string $url, ?int $code = null): void +{ + if ($code !== null) { + response()->httpCode($code); + } + + response()->redirect($url); +} + +/** + * Get current csrf-token + * @return string|null + */ +function csrf_token(): ?string +{ + $baseVerifier = Router::router()->getCsrfVerifier(); + if ($baseVerifier !== null) { + return $baseVerifier->getTokenProvider()->getToken(); + } + + return null; +} +``` + +--- + +# Routes + +Remember the ```routes.php``` file you required in your ```index.php```? This file be where you place all your custom rules for routing. + +## Basic routing + +Below is a very basic example of setting up a route. First parameter is the url which the route should match - next parameter is a `Closure` or callback function that will be triggered once the route matches. + +```php +SimpleRouter::get('/', function() { + return 'Hello world'; +}); +``` + +### Class hinting + +You can use class hinting to load a class & method like this: + +```php +SimpleRouter::get('/', [MyClass::class, 'myMethod']); +``` + +### Available methods + +Here you can see a list over all available routes: + +```php +SimpleRouter::get($url, $callback, $settings); +SimpleRouter::post($url, $callback, $settings); +SimpleRouter::put($url, $callback, $settings); +SimpleRouter::patch($url, $callback, $settings); +SimpleRouter::delete($url, $callback, $settings); +SimpleRouter::options($url, $callback, $settings); +``` + +### Multiple HTTP-verbs + +Sometimes you might need to create a route that accepts multiple HTTP-verbs. If you need to match all HTTP-verbs you can use the `any` method. + +```php +SimpleRouter::match(['get', 'post'], '/', function() { + // ... +}); + +SimpleRouter::any('foo', function() { + // ... +}); +``` + +We've created a simple method which matches `GET` and `POST` which is most commonly used: + +```php +SimpleRouter::form('foo', function() { + // ... +}); +``` + +## Route parameters + +### Required parameters + +You'll properly wondering by know how you parse parameters from your urls. For example, you might want to capture the users id from an url. You can do so by defining route-parameters. + +```php +SimpleRouter::get('/user/{id}', function ($userId) { + return 'User with id: ' . $userId; +}); +``` + +You may define as many route parameters as required by your route: + +```php +SimpleRouter::get('/posts/{post}/comments/{comment}', function ($postId, $commentId) { + // ... +}); +``` + +**Note:** Route parameters are always encased within `{` `}` braces and should consist of alphabetic characters. Route parameters can only contain certain characters like `A-Z`, `a-z`, `0-9`, `-` and `_`. +If your route contain other characters, please see [Custom regex for matching parameters](#custom-regex-for-matching-parameters). + +### Optional parameters + +Occasionally you may need to specify a route parameter, but make the presence of that route parameter optional. You may do so by placing a ? mark after the parameter name. Make sure to give the route's corresponding variable a default value: + +```php +SimpleRouter::get('/user/{name?}', function ($name = null) { + return $name; +}); + +SimpleRouter::get('/user/{name?}', function ($name = 'Simon') { + return $name; +}); +``` + +### Including slash in parameters + +If you're working with WebDAV services the url could mean the difference between a file and a folder. + +For instance `/path` will be considered a file - whereas `/path/` will be considered a folder. + +The router can add the ending slash for the last parameter in your route based on the path. So if `/path/` is requested the parameter will contain the value of `path/` and visa versa. + +To ensure compatibility with older versions, this feature is disabled by default and has to be enabled by setting +the `setSettings(['includeSlash' => true])` or by using setting `setSlashParameterEnabled(true)` for your route. + +**Example** + +```php +SimpleRouter::get('/path/{fileOrFolder}', function ($fileOrFolder) { + return $fileOrFolder; +})->setSettings(['includeSlash' => true]); +``` + +- Requesting `/path/file` will return the `$fileOrFolder` value: `file`. +- Requesting `/path/folder/` will return the `$fileOrFolder` value: `folder/`. + +### Regular expression constraints + +You may constrain the format of your route parameters using the where method on a route instance. The where method accepts the name of the parameter and a regular expression defining how the parameter should be constrained: + +```php +SimpleRouter::get('/user/{name}', function ($name) { + + // ... do stuff + +})->where([ 'name' => '[A-Za-z]+' ]); + +SimpleRouter::get('/user/{id}', function ($id) { + + // ... do stuff + +})->where([ 'id' => '[0-9]+' ]); + +SimpleRouter::get('/user/{id}/{name}', function ($id, $name) { + + // ... do stuff + +})->where(['id' => '[0-9]+', 'name' => '[a-z]+']); +``` + +### Regular expression route-match + +You can define a regular-expression match for the entire route if you wish. + +This is useful if you for example are creating a model-box which loads urls from ajax. + +The example below is using the following regular expression: `/ajax/([\w]+)/?([0-9]+)?/?` which basically just matches `/ajax/` and exspects the next parameter to be a string - and the next to be a number (but optional). + +**Matches:** `/ajax/abc/`, `/ajax/abc/123/` + +**Won't match:** `/ajax/` + +Match groups specified in the regex will be passed on as parameters: + +```php +SimpleRouter::all('/ajax/abc/123', function($param1, $param2) { + // param1 = abc + // param2 = 123 +})->setMatch('/\/ajax\/([\w]+)\/?([0-9]+)?\/?/is'); +``` + +### Custom regex for matching parameters + +By default simple-php-router uses the `[\w\-]+` regular expression. It will match `A-Z`, `a-z`, `0-9`, `-` and `_` characters in parameters. +This decision was made with speed and reliability in mind, as this match will match both letters, number and most of the used symbols on the internet. + +However, sometimes it can be necessary to add a custom regular expression to match more advanced characters like foreign letters `æ ø å` etc. + +You can test your custom regular expression by using on the site [Regex101.com](https://www.regex101.com). + +Instead of adding a custom regular expression to all your parameters, you can simply add a global regular expression which will be used on all the parameters on the route. + +**Note:** If you the regular expression to be available across, we recommend using the global parameter on a group as demonstrated in the examples below. + +#### Example + +This example will ensure that all parameters use the `[\w\-\æ\ø\å]+` (`a-z`, `A-Z`, `-`, `_`, `0-9`, `æ`, `ø`, `å`) regular expression when parsing. + +```php +SimpleRouter::get('/path/{parameter}', 'VideoController@home', ['defaultParameterRegex' => '[\w\-\æ\ø\å]+']); +``` + +You can also apply this setting to a group if you need multiple routes to use your custom regular expression when parsing parameters. + +```php +SimpleRouter::group(['defaultParameterRegex' => '[\w\-\æ\ø\å]+'], function() { + + SimpleRouter::get('/path/{parameter}', 'VideoController@home'); + +}); +``` + +## Named routes + +Named routes allow the convenient generation of URLs or redirects for specific routes. You may specify a name for a route by chaining the name method onto the route definition: + +```php +SimpleRouter::get('/user/profile', function () { + // Your code here +})->name('profile'); +``` + +You can also specify names for Controller-actions: + +```php +SimpleRouter::get('/user/profile', 'UserController@profile')->name('profile'); +``` + +### Generating URLs To Named Routes + +Once you have assigned a name to a given route, you may use the route's name when generating URLs or redirects via the global `url` helper-function (see helpers section): + +```php +// Generating URLs... +$url = url('profile'); +``` + +If the named route defines parameters, you may pass the parameters as the second argument to the `url` function. The given parameters will automatically be inserted into the URL in their correct positions: + +```php +SimpleRouter::get('/user/{id}/profile', function ($id) { + // +})->name('profile'); + +$url = url('profile', ['id' => 1]); +``` + +For more information on urls, please see the [Urls](#urls) section. + +## Router groups + +Route groups allow you to share route attributes, such as middleware or namespaces, across a large number of routes without needing to define those attributes on each individual route. Shared attributes are specified in an array format as the first parameter to the `SimpleRouter::group` method. + +### Middleware + +To assign middleware to all routes within a group, you may use the middleware key in the group attribute array. Middleware are executed in the order they are listed in the array: + +```php +SimpleRouter::group(['middleware' => \Demo\Middleware\Auth::class], function () { + SimpleRouter::get('/', function () { + // Uses Auth Middleware + }); + + SimpleRouter::get('/user/profile', function () { + // Uses Auth Middleware + }); +}); +``` + +### Namespaces + +Another common use-case for route groups is assigning the same PHP namespace to a group of controllers using the `namespace` parameter in the group array: + +#### Note +Group namespaces will only be added to routes with relative callbacks. +For example if your route has an absolute callback like `\Demo\Controller\DefaultController@home`, the namespace from the route will not be prepended. +To fix this you can make the callback relative by removing the `\` in the beginning of the callback. + +```php +SimpleRouter::group(['namespace' => 'Admin'], function () { + // Controllers Within The "App\Http\Controllers\Admin" Namespace +}); +``` + +You can add parameters to the prefixes of your routes. + +Parameters from your previous routes will be injected +into your routes after any route-required parameters, starting from oldest to newest. + +```php +SimpleRouter::group(['prefix' => '/lang/{lang}'], function ($language) { + + SimpleRouter::get('/about', function($language) { + + // Will match /lang/da/about + + }); + +}); +``` + +### Subdomain-routing + +Route groups may also be used to handle sub-domain routing. Sub-domains may be assigned route parameters just like route urls, allowing you to capture a portion of the sub-domain for usage in your route or controller. The sub-domain may be specified using the `domain` key on the group attribute array: + +```php +SimpleRouter::group(['domain' => '{account}.myapp.com'], function () { + SimpleRouter::get('/user/{id}', function ($account, $id) { + // + }); +}); +``` + +### Route prefixes + +The `prefix` group attribute may be used to prefix each route in the group with a given url. For example, you may want to prefix all route urls within the group with `admin`: + +```php +SimpleRouter::group(['prefix' => '/admin'], function () { + SimpleRouter::get('/users', function () { + // Matches The "/admin/users" URL + }); +}); +``` + +You can also use parameters in your groups: + +```php +SimpleRouter::group(['prefix' => '/lang/{language}'], function ($language) { + SimpleRouter::get('/users', function ($language) { + // Matches The "/lang/da/users" URL + }); +}); +``` + +## Partial groups + +Partial router groups has the same benefits as a normal group, but **are only rendered once the url has matched** +in contrast to a normal group which are always rendered in order to retrieve it's child routes. +Partial groups are therefore more like a hybrid of a traditional route with the benefits of a group. + +This can be extremely useful in situations where you only want special routes to be added, but only when a certain criteria or logic has been met. + +**NOTE:** Use partial groups with caution as routes added within are only rendered and available once the url of the partial-group has matched. +This can cause `url()` not to find urls for the routes added within before the partial-group has been matched and is rendered. + +**Example:** + +```php +SimpleRouter::partialGroup('/plugin/{name}', function ($plugin) { + + // Add routes from plugin + +}); +``` + +## Form Method Spoofing + +HTML forms do not support `PUT`, `PATCH` or `DELETE` actions. So, when defining `PUT`, `PATCH` or `DELETE` routes that are called from an HTML form, you will need to add a hidden `_method` field to the form. The value sent with the `_method` field will be used as the HTTP request method: + +```php + +``` + +## Accessing The Current Route + +You can access information about the current route loaded by using the following method: + +```php +SimpleRouter::request()->getLoadedRoute(); +request()->getLoadedRoute(); +``` + +## Other examples + +You can find many more examples in the `routes.php` example-file below: + +```php + \Demo\Middlewares\Site::class, 'exceptionHandler' => \Demo\Handlers\CustomExceptionHandler::class], function() { + + + SimpleRouter::get('/answers/{id}', 'ControllerAnswers@show', ['where' => ['id' => '[0-9]+']]); + + /** + * Class hinting is supported too + */ + + SimpleRouter::get('/answers/{id}', [ControllerAnswers::class, 'show'], ['where' => ['id' => '[0-9]+']]); + + /** + * Restful resource (see IRestController interface for available methods) + */ + + SimpleRouter::resource('/rest', ControllerResource::class); + + + /** + * Load the entire controller (where url matches method names - getIndex(), postIndex(), putIndex()). + * The url paths will determine which method to render. + * + * For example: + * + * GET /animals => getIndex() + * GET /animals/view => getView() + * POST /animals/save => postSave() + * + * etc. + */ + + SimpleRouter::controller('/animals', ControllerAnimals::class); + +}); + +SimpleRouter::get('/page/404', 'ControllerPage@notFound', ['as' => 'page.notfound']); +``` + +--- + +# CSRF Protection + +Any forms posting to `POST`, `PUT` or `DELETE` routes should include the CSRF-token. We strongly recommend that you enable CSRF-verification on your site to maximize security. + +You can use the `BaseCsrfVerifier` to enable CSRF-validation on all request. If you need to disable verification for specific urls, please refer to the "Custom CSRF-verifier" section below. + +By default simple-php-router will use the `CookieTokenProvider` class. This provider will store the security-token in a cookie on the clients machine. +If you want to store the token elsewhere, please refer to the "Creating custom Token Provider" section below. + +## Adding CSRF-verifier + +When you've created your CSRF-verifier you need to tell simple-php-router that it should use it. You can do this by adding the following line in your `routes.php` file: + +```php +SimpleRouter::csrfVerifier(new \Demo\Middlewares\CsrfVerifier()); +``` + +## Getting CSRF-token + +When posting to any of the urls that has CSRF-verification enabled, you need post your CSRF-token or else the request will get rejected. + +You can get the CSRF-token by calling the helper method: + +```php +csrf_token(); +``` + +You can also get the token directly: + +```php +return SimpleRouter::router()->getCsrfVerifier()->getTokenProvider()->getToken(); +``` + +The default name/key for the input-field is `csrf_token` and is defined in the `POST_KEY` constant in the `BaseCsrfVerifier` class. +You can change the key by overwriting the constant in your own CSRF-verifier class. + +**Example:** + +The example below will post to the current url with a hidden field "`csrf_token`". + +```html +
+ + +
+``` + +## Custom CSRF-verifier + +Create a new class and extend the `BaseCsrfVerifier` middleware class provided by default with the simple-php-router library. + +Add the property `except` with an array of the urls to the routes you want to exclude/whitelist from the CSRF validation. +Using ```*``` at the end for the url will match the entire url. + +**Here's a basic example on a CSRF-verifier class:** + +```php +namespace Demo\Middlewares; + +use Pecee\Http\Middleware\BaseCsrfVerifier; + +class CsrfVerifier extends BaseCsrfVerifier +{ + /** + * CSRF validation will be ignored on the following urls. + */ + protected $except = ['/api/*']; +} +``` + +## Custom Token Provider + +By default the `BaseCsrfVerifier` will use the `CookieTokenProvider` to store the token in a cookie on the clients machine. + +If you need to store the token elsewhere, you can do that by creating your own class and implementing the `ITokenProvider` class. + +```php +class SessionTokenProvider implements ITokenProvider +{ + + /** + * Refresh existing token + */ + public function refresh(): void + { + // Implement your own functionality here... + } + + /** + * Validate valid CSRF token + * + * @param string $token + * @return bool + */ + public function validate($token): bool + { + // Implement your own functionality here... + } + + /** + * Get token token + * + * @param string|null $defaultValue + * @return string|null + */ + public function getToken(?string $defaultValue = null): ?string + { + // Implement your own functionality here... + } + +} +``` + +Next you need to set your custom `ITokenProvider` implementation on your `BaseCsrfVerifier` class in your routes file: + +```php +$verifier = new \Demo\Middlewares\CsrfVerifier(); +$verifier->setTokenProvider(new SessionTokenProvider()); + +SimpleRouter::csrfVerifier($verifier); +``` + +--- + +# Middlewares + +Middlewares are classes that loads before the route is rendered. A middleware can be used to verify that a user is logged in - or to set parameters specific for the current request/route. Middlewares must implement the `IMiddleware` interface. + +## Example + +```php +namespace Demo\Middlewares; + +use Pecee\Http\Middleware\IMiddleware; +use Pecee\Http\Request; + +class CustomMiddleware implements IMiddleware { + + public function handle(Request $request): void + { + + // Authenticate user, will be available using request()->user + $request->user = User::authenticate(); + + // If authentication failed, redirect request to user-login page. + if($request->user === null) { + $request->setRewriteUrl(url('user.login')); + } + + } +} +``` + +--- + +# ExceptionHandlers + +ExceptionHandler are classes that handles all exceptions. ExceptionsHandlers must implement the `IExceptionHandler` interface. + +## Handling 404, 403 and other errors + +If you simply want to catch a 404 (page not found) etc. you can use the `SimpleRouter::error($callback)` static helper method. + +This will add a callback method which is fired whenever an error occurs on all routes. + +The basic example below simply redirect the page to `/not-found` if an `NotFoundHttpException` (404) occurred. +The code should be placed in the file that contains your routes. + +```php +SimpleRouter::get('/not-found', 'PageController@notFound'); +SimpleRouter::get('/forbidden', 'PageController@notFound'); + +SimpleRouter::error(function(Request $request, \Exception $exception) { + + switch($exception->getCode()) { + // Page not found + case 404: + response()->redirect('/not-found'); + // Forbidden + case 403: + response()->redirect('/forbidden'); + } + +}); +``` + +The example above will redirect all errors with http-code `404` (page not found) to `/not-found` and `403` (forbidden) to `/forbidden`. + +If you do not want a redirect, but want the error-page rendered on the current-url, you can tell the router to execute a rewrite callback like so: + +```php +$request->setRewriteCallback('ErrorController@notFound'); +``` + +If you will set the correct status for the browser error use: + +```php +SimpleRouter::response()->httpCode(404); +``` + +## Using custom exception handlers + +This is a basic example of an ExceptionHandler implementation (please see "[Easily overwrite route about to be loaded](#easily-overwrite-route-about-to-be-loaded)" for examples on how to change callback). + +```php +namespace Demo\Handlers; + +use Pecee\Http\Request; +use Pecee\SimpleRouter\Handlers\IExceptionHandler; +use Pecee\SimpleRouter\Exceptions\NotFoundHttpException; + +class CustomExceptionHandler implements IExceptionHandler +{ + public function handleError(Request $request, \Exception $error): void + { + + /* You can use the exception handler to format errors depending on the request and type. */ + + if ($request->getUrl()->contains('/api')) { + + response()->json([ + 'error' => $error->getMessage(), + 'code' => $error->getCode(), + ]); + + } + + /* The router will throw the NotFoundHttpException on 404 */ + if($error instanceof NotFoundHttpException) { + + // Render custom 404-page + $request->setRewriteCallback('Demo\Controllers\PageController@notFound'); + return; + + } + + /* Other error */ + if($error instanceof MyCustomException) { + + $request->setRewriteRoute( + // Add new route based on current url (minus query-string) and add custom parameters. + (new RouteUrl(url(null, null, []), 'PageController@error'))->setParameters(['exception' => $error]) + ); + return; + + } + + throw $error; + + } + +} +``` + +You can add your custom exception-handler class to your group by using the `exceptionHandler` settings-attribute. +`exceptionHandler` can be either class-name or array of class-names. + +```php +SimpleRouter::group(['exceptionHandler' => \Demo\Handlers\CustomExceptionHandler::class], function() { + + // Your routes here + +}); +``` + +### Prevent merge of parent exception-handlers + +By default the router will merge exception-handlers to any handlers provided by parent groups, and will be executed in the order of newest to oldest. + +If you want your groups exception handler to be executed independently, you can add the `mergeExceptionHandlers` attribute and set it to `false`. + +```php +SimpleRouter::group(['prefix' => '/', 'exceptionHandler' => \Demo\Handlers\FirstExceptionHandler::class, 'mergeExceptionHandlers' => false], function() { + + SimpleRouter::group(['prefix' => '/admin', 'exceptionHandler' => \Demo\Handlers\SecondExceptionHandler::class], function() { + + // Both SecondExceptionHandler and FirstExceptionHandler will trigger (in that order). + + }); + + SimpleRouter::group(['prefix' => '/user', 'exceptionHandler' => \Demo\Handlers\SecondExceptionHandler::class, 'mergeExceptionHandlers' => false], function() { + + // Only SecondExceptionHandler will trigger. + + }); + +}); +``` + +--- + +# Urls + +By default all controller and resource routes will use a simplified version of their url as name. + +You easily use the `url()` shortcut helper function to retrieve urls for your routes or manipulate the current url. + +`url()` will return a `Url` object which will return a `string` when rendered, so it can be used safely in templates etc. but +contains all the useful helpers methods in the `Url` class like `contains`, `indexOf` etc. +Check the [Useful url tricks](#useful-url-tricks) below. + +### Get the current url + +It has never been easier to get and/or manipulate the current url. + +The example below shows you how to get the current url: + +```php +# output: /current-url +url(); +``` + +### Get by name (single route) + +```php +SimpleRouter::get('/product-view/{id}', 'ProductsController@show', ['as' => 'product']); + +# output: /product-view/22/?category=shoes +url('product', ['id' => 22], ['category' => 'shoes']); + +# output: /product-view/?category=shoes +url('product', null, ['category' => 'shoes']); +``` + +### Get by name (controller route) + +```php +SimpleRouter::controller('/images', ImagesController::class, ['as' => 'picture']); + +# output: /images/view/?category=shows +url('picture@getView', null, ['category' => 'shoes']); + +# output: /images/view/?category=shows +url('picture', 'getView', ['category' => 'shoes']); + +# output: /images/view/ +url('picture', 'view'); +``` + +### Get by class + +```php +SimpleRouter::get('/product-view/{id}', 'ProductsController@show', ['as' => 'product']); +SimpleRouter::controller('/images', 'ImagesController'); + +# output: /product-view/22/?category=shoes +url('ProductsController@show', ['id' => 22], ['category' => 'shoes']); + +# output: /images/image/?id=22 +url('ImagesController@getImage', null, ['id' => 22]); +``` + +### Using custom names for methods on a controller/resource route + +```php +SimpleRouter::controller('gadgets', GadgetsController::class, ['names' => ['getIphoneInfo' => 'iphone']]); + +url('gadgets.iphone'); + +# output +# /gadgets/iphoneinfo/ +``` + +### Getting REST/resource controller urls + +```php +SimpleRouter::resource('/phones', PhonesController::class); + +# output: /phones/ +url('phones'); + +# output: /phones/ +url('phones.index'); + +# output: /phones/create/ +url('phones.create'); + +# output: /phones/edit/ +url('phones.edit'); +``` + +### Manipulating url + +You can easily manipulate the query-strings, by adding your get param arguments. + +```php +# output: /current-url?q=cars + +url(null, null, ['q' => 'cars']); +``` + +You can remove a query-string parameter by setting the value to `null`. + +The example below will remove any query-string parameter named `q` from the url but keep all others query-string parameters: + +```php +$url = url()->removeParam('q'); +``` + +For more information please check the [Useful url tricks](#useful-url-tricks) section of the documentation. + +### Useful url tricks + +Calling `url` will always return a `Url` object. Upon rendered it will return a `string` of the relative `url`, so it's safe to use in templates etc. + +However this allow us to use the useful methods on the `Url` object like `indexOf` and `contains` or retrieve specific parts of the url like the path, querystring parameters, host etc. You can also manipulate the url like removing- or adding parameters, changing host and more. + +In the example below, we check if the current url contains the `/api` part. + +```php +if(url()->contains('/api')) { + // ... do stuff +} +``` + +As mentioned earlier, you can also use the `Url` object to show specific parts of the url or control what part of the url you want. + +```php +# Grab the query-string parameter id from the current-url. +$id = url()->getParam('id'); + +# Get the absolute url for the current url. +$absoluteUrl = url()->getAbsoluteUrl(); +``` + +For more available methods please check the `Pecee\Http\Url` class. + +# Input & parameters + +simple-router offers libraries and helpers that makes it easy to manage and manipulate input-parameters like `$_POST`, `$_GET` and `$_FILE`. + +## Using the Input class to manage parameters + +You can use the `InputHandler` class to easily access and manage parameters from your request. The `InputHandler` class offers extended features such as copying/moving uploaded files directly on the object, getting file-extension, mime-type etc. + +### Get single parameter value + +```input($index, $defaultValue, ...$methods);``` + +To quickly get a value from a parameter, you can use the `input` helper function. + +This will automatically trim the value and ensure that it's not empty. If it's empty the `$defaultValue` will be returned instead. + +**Note:** +This function returns a `string` unless the parameters are grouped together, in that case it will return an `array` of values. + +**Example:** + +This example matches both POST and GET request-methods and if name is empty the default-value "Guest" will be returned. + +```php +$name = input('name', 'Guest', 'post', 'get'); +``` + +### Get parameter object + +When dealing with file-uploads it can be useful to retrieve the raw parameter object. + +**Search for object with default-value across multiple or specific request-methods:** + +The example below will return an `InputItem` object if the parameter was found or return the `$defaultValue`. If parameters are grouped, it will return an array of `InputItem` objects. + +```php +$object = input()->find($index, $defaultValue = null, ...$methods); +``` + +**Getting specific `$_GET` parameter as `InputItem` object:** + +The example below will return an `InputItem` object if the parameter was found or return the `$defaultValue`. If parameters are grouped, it will return an array of `InputItem` objects. + +```php +$object = input()->get($index, $defaultValue = null); +``` + +**Getting specific `$_POST` parameter as `InputItem` object:** + +The example below will return an `InputItem` object if the parameter was found or return the `$defaultValue`. If parameters are grouped, it will return an array of `InputItem` objects. + +```php +$object = input()->post($index, $defaultValue = null); +``` + +**Getting specific `$_FILE` parameter as `InputFile` object:** + +The example below will return an `InputFile` object if the parameter was found or return the `$defaultValue`. If parameters are grouped, it will return an array of `InputFile` objects. + +```php +$object = input()->file($index, $defaultValue = null); +``` + +### Managing files + +```php +/** + * Loop through a collection of files uploaded from a form on the page like this + * + */ + +/* @var $image \Pecee\Http\Input\InputFile */ +foreach(input()->file('images', []) as $image) +{ + if($image->getMime() === 'image/jpeg') + { + $destinationFilname = sprintf('%s.%s', uniqid(), $image->getExtension()); + $image->move(sprintf('/uploads/%s', $destinationFilename)); + } +} + +``` + +### Get all parameters + +```php +# Get all +$values = input()->all(); + +# Only match specific keys +$values = input()->all([ + 'company_name', + 'user_id' +]); +``` + +All object implements the `IInputItem` interface and will always contain these methods: + +- `getIndex()` - returns the index/key of the input. +- `setIndex()` - set the index/key of the input. +- `getName()` - returns a human friendly name for the input (company_name will be Company Name etc). +- `setName()` - sets a human friendly name for the input (company_name will be Company Name etc). +- `getValue()` - returns the value of the input. +- `setValue()` - sets the value of the input. + +`InputFile` has the same methods as above along with some other file-specific methods like: + +- `getFilename` - get the filename. +- `getTmpName()` - get file temporary name. +- `getSize()` - get file size. +- `move($destination)` - move file to destination. +- `getContents()` - get file content. +- `getType()` - get mime-type for file. +- `getError()` - get file upload error. +- `hasError()` - returns `bool` if an error occurred while uploading (if `getError` is not 0). +- `toArray()` - returns raw array + +--- + +### Check if parameters exists + +You can easily if multiple items exists by using the `exists` method. It's simular to `value` as it can be used +to filter on request-methods and supports both `string` and `array` as parameter value. + +**Example:** + +```php +if(input()->exists(['name', 'lastname'])) { + // Do stuff +} + +/* Similar to code above */ +if(input()->exists('name') && input()->exists('lastname')) { + // Do stuff +} +``` + +# Events + +This section will help you understand how to register your own callbacks to events in the router. +It will also cover the basics of event-handlers; how to use the handlers provided with the router and how to create your own custom event-handlers. + +## Available events + +This section contains all available events that can be registered using the `EventHandler`. + +All event callbacks will retrieve a `EventArgument` object as parameter. This object contains easy access to event-name, router- and request instance and any special event-arguments related to the given event. You can see what special event arguments each event returns in the list below. + +| Name | Special arguments | Description | +| ------------- |----------- | ---- | +| `EVENT_ALL` | - | Fires when a event is triggered. | +| `EVENT_INIT` | - | Fires when router is initializing and before routes are loaded. | +| `EVENT_LOAD` | `loadedRoutes` | Fires when all routes has been loaded and rendered, just before the output is returned. | +| `EVENT_ADD_ROUTE` | `route`
`isSubRoute` | Fires when route is added to the router. `isSubRoute` is true when sub-route is rendered. | +| `EVENT_REWRITE` | `rewriteUrl`
`rewriteRoute` | Fires when a url-rewrite is and just before the routes are re-initialized. | +| `EVENT_BOOT` | `bootmanagers` | Fires when the router is booting. This happens just before boot-managers are rendered and before any routes has been loaded. | +| `EVENT_RENDER_BOOTMANAGER` | `bootmanagers`
`bootmanager` | Fires before a boot-manager is rendered. | +| `EVENT_LOAD_ROUTES` | `routes` | Fires when the router is about to load all routes. | +| `EVENT_FIND_ROUTE` | `name` | Fires whenever the `findRoute` method is called within the `Router`. This usually happens when the router tries to find routes that contains a certain url, usually after the `EventHandler::EVENT_GET_URL` event. | +| `EVENT_GET_URL` | `name`
`parameters`
`getParams` | Fires whenever the `SimpleRouter::getUrl` method or `url`-helper function is called and the router tries to find the route. | +| `EVENT_MATCH_ROUTE` | `route` | Fires when a route is matched and valid (correct request-type etc). and before the route is rendered. | +| `EVENT_RENDER_ROUTE` | `route` | Fires before a route is rendered. | +| `EVENT_LOAD_EXCEPTIONS` | `exception`
`exceptionHandlers` | Fires when the router is loading exception-handlers. | +| `EVENT_RENDER_EXCEPTION` | `exception`
`exceptionHandler`
`exceptionHandlers` | Fires before the router is rendering a exception-handler. | +| `EVENT_RENDER_MIDDLEWARES` | `route`
`middlewares` | Fires before middlewares for a route is rendered. | +| `EVENT_RENDER_CSRF` | `csrfVerifier` | Fires before the CSRF-verifier is rendered. | + +## Registering new event + +To register a new event you need to create a new instance of the `EventHandler` object. On this object you can add as many callbacks as you like by calling the `registerEvent` method. + +When you've registered events, make sure to add it to the router by calling +`SimpleRouter::addEventHandler()`. We recommend that you add your event-handlers within your `routes.php`. + +**Example:** + +```php +use Pecee\SimpleRouter\Handlers\EventHandler; +use Pecee\SimpleRouter\Event\EventArgument; + +// --- your routes goes here --- + +$eventHandler = new EventHandler(); + +// Add event that fires when a route is rendered +$eventHandler->register(EventHandler::EVENT_RENDER_ROUTE, function(EventArgument $argument) { + + // Get the route by using the special argument for this event. + $route = $argument->route; + + // DO STUFF... + +}); + +SimpleRouter::addEventHandler($eventHandler); + +``` + +## Custom EventHandlers + +`EventHandler` is the class that manages events and must inherit from the `IEventHandler` interface. The handler knows how to handle events for the given handler-type. + +Most of the time the basic `\Pecee\SimpleRouter\Handler\EventHandler` class will be more than enough for most people as you simply register an event which fires when triggered. + +Let's go over how to create your very own event-handler class. + +Below is a basic example of a custom event-handler called `DatabaseDebugHandler`. The idea of the sample below is to logs all events to the database when triggered. Hopefully it will be enough to give you an idea on how the event-handlers work. + +```php +namespace Demo\Handlers; + +use Pecee\SimpleRouter\Event\EventArgument; +use Pecee\SimpleRouter\Router; + +class DatabaseDebugHandler implements IEventHandler +{ + + /** + * Debug callback + * @var \Closure + */ + protected $callback; + + public function __construct() + { + $this->callback = function (EventArgument $argument) { + // todo: store log in database + }; + } + + /** + * Get events. + * + * @param string|null $name Filter events by name. + * @return array + */ + public function getEvents(?string $name): array + { + return [ + $name => [ + $this->callback, + ], + ]; + } + + /** + * Fires any events registered with given event-name + * + * @param Router $router Router instance + * @param string $name Event name + * @param array ...$eventArgs Event arguments + */ + public function fireEvents(Router $router, string $name, ...$eventArgs): void + { + $callback = $this->callback; + $callback(new EventArgument($router, $eventArgs)); + } + + /** + * Set debug callback + * + * @param \Closure $event + */ + public function setCallback(\Closure $event): void + { + $this->callback = $event; + } + +} +``` + +--- + +# Advanced + +## Multiple route rendering + +If you need multiple routes to be executed on the same url, you can enable this feature by setting `SimpleRouter::enableMultiRouteRendering(true)` +in your `routes.php` file. + +This is most commonly used in advanced cases, for example in CMS systems where multiple routes needs to be rendered. + +## Restrict access to IP + +You can white and/or blacklist access to IP's using the build in `IpRestrictAccess` middleware. + +Create your own custom Middleware and extend the `IpRestrictAccess` class. + +The `IpRestrictAccess` class contains two properties `ipBlacklist` and `ipWhitelist` that can be added to your middleware to change which IP's that have access to your routes. + +You can use `*` to restrict access to a range of ips. + +```php +use \Pecee\Http\Middleware\IpRestrictAccess; + +class IpBlockerMiddleware extends IpRestrictAccess +{ + + protected $ipBlacklist = [ + '5.5.5.5', + '8.8.*', + ]; + + protected $ipWhitelist = [ + '8.8.2.2', + ]; + +} +``` + +You can add the middleware to multiple routes by adding your [middleware to a group](#middleware). + +## Setting custom base path + +Sometimes it can be useful to add a custom base path to all of the routes added. + +This can easily be done by taking advantage of the [Event Handlers](#events) support of the project. + +```php +$basePath = '/basepath'; + +$eventHandler = new EventHandler(); +$eventHandler->register(EventHandler::EVENT_ADD_ROUTE, function(EventArgument $event) use($basePath) { + + $route = $event->route; + + // Skip routes added by group as these will inherit the url + if(!$event->isSubRoute) { + return; + } + + switch (true) { + case $route instanceof ILoadableRoute: + $route->prependUrl($basePath); + break; + case $route instanceof IGroupRoute: + $route->prependPrefix($basePath); + break; + + } + +}); + +SimpleRouter::addEventHandler($eventHandler); +``` + +In the example shown above, we create a new `EVENT_ADD_ROUTE` event that triggers, when a new route is added. +We skip all subroutes as these will inherit the url from their parent. Then, if the route is a group, we change the prefix +otherwise we change the url. + +## Url rewriting + +### Changing current route + +Sometimes it can be useful to manipulate the route about to be loaded. +simple-php-router allows you to easily manipulate and change the routes which are about to be rendered. +All information about the current route is stored in the `\Pecee\SimpleRouter\Router` instance's `loadedRoute` property. + +For easy access you can use the shortcut helper function `request()` instead of calling the class directly `\Pecee\SimpleRouter\SimpleRouter::router()`. + + +```php +request()->setRewriteCallback('Example\MyCustomClass@hello'); + +// -- or you can rewrite by url -- + +request()->setRewriteUrl('/my-rewrite-url'); +``` + +### Bootmanager: loading routes dynamically + +Sometimes it can be necessary to keep urls stored in the database, file or similar. In this example, we want the url ```/my-cat-is-beatiful``` to load the route ```/article/view/1``` which the router knows, because it's defined in the ```routes.php``` file. + +To interfere with the router, we create a class that implements the ```IRouterBootManager``` interface. This class will be loaded before any other rules in ```routes.php``` and allow us to "change" the current route, if any of our criteria are fulfilled (like coming from the url ```/my-cat-is-beatiful```). + +```php +use Pecee\Http\Request; +use Pecee\SimpleRouter\IRouterBootManager; +use Pecee\SimpleRouter\Router; + +class CustomRouterRules implement IRouterBootManager +{ + + /** + * Called when router is booting and before the routes is loaded. + * + * @param \Pecee\SimpleRouter\Router $router + * @param \Pecee\Http\Request $request + */ + public function boot(\Pecee\SimpleRouter\Router $router, \Pecee\Http\Request $request): void + { + + $rewriteRules = [ + '/my-cat-is-beatiful' => '/article/view/1', + '/horses-are-great' => '/article/view/2', + ]; + + foreach($rewriteRules as $url => $rule) { + + // If the current url matches the rewrite url, we use our custom route + + if($request->getUrl()->contains($url)) { + $request->setRewriteUrl($rule); + } + } + + } + +} +``` + +The above should be pretty self-explanatory and can easily be changed to loop through urls store in the database, file or cache. + +What happens is that if the current route matches the route defined in the index of our ```$rewriteRules``` array, we set the route to the array value instead. + +By doing this the route will now load the url ```/article/view/1``` instead of ```/my-cat-is-beatiful```. + +The last thing we need to do, is to add our custom boot-manager to the ```routes.php``` file. You can create as many bootmanagers as you like and easily add them in your ```routes.php``` file. + +```php +SimpleRouter::addBootManager(new CustomRouterRules()); +``` + +### Adding routes manually + +The ```SimpleRouter``` class referenced in the previous example, is just a simple helper class that knows how to communicate with the ```Router``` class. +If you are up for a challenge, want the full control or simply just want to create your own ```Router``` helper class, this example is for you. + +```php +use \Pecee\SimpleRouter\Router; +use \Pecee\SimpleRouter\Route\RouteUrl; + +/* Create new Router instance */ +$router = new Router(); + +$route = new RouteUrl('/answer/1', function() { + + die('this callback will match /answer/1'); + +}); + +$route->addMiddleware(\Demo\Middlewares\AuthMiddleware::class); +$route->setNamespace('\Demo\Controllers'); +$route->setPrefix('v1'); + +/* Add the route to the router */ +$router->addRoute($route); +``` + +## Custom class loader + +You can easily extend simple-router to support custom injection frameworks like php-di by taking advantage of the ability to add your custom class-loader. + +Class-loaders must inherit the `IClassLoader` interface. + +**Example:** + +```php +class MyCustomClassLoader implements IClassLoader +{ + /** + * Load class + * + * @param string $class + * @return object + * @throws NotFoundHttpException + */ + public function loadClass(string $class) + { + if (\class_exists($class) === false) { + throw new NotFoundHttpException(sprintf('Class "%s" does not exist', $class), 404); + } + + return new $class(); + } + + /** + * Called when loading class method + * @param object $class + * @param string $method + * @param array $parameters + * @return object + */ + public function loadClassMethod($class, string $method, array $parameters) + { + return call_user_func_array([$class, $method], array_values($parameters)); + } + + /** + * Load closure + * + * @param Callable $closure + * @param array $parameters + * @return mixed + */ + public function loadClosure(Callable $closure, array $parameters) + { + return \call_user_func_array($closure, array_values($parameters)); + } + +} +``` + +Next, we need to configure our `routes.php` so the router uses our `MyCustomClassLoader` class for loading classes. This can be done by adding the following line to your `routes.php` file. + +```php +SimpleRouter::setCustomClassLoader(new MyCustomClassLoader()); +``` + +### Integrating with php-di + +php-di support was discontinued by version 4.3, however you can easily add it again by creating your own class-loader like the example below: + +```php +use Pecee\SimpleRouter\ClassLoader\IClassLoader; +use Pecee\SimpleRouter\Exceptions\ClassNotFoundHttpException; + +class MyCustomClassLoader implements IClassLoader +{ + + protected $container; + + public function __construct() + { + // Create our new php-di container + $this->container = (new \DI\ContainerBuilder()) + ->useAutowiring(true) + ->build(); + } + + /** + * Load class + * + * @param string $class + * @return object + * @throws ClassNotFoundHttpException + */ + public function loadClass(string $class) + { + if ($this->container->has($class) === false) { + throw new ClassNotFoundHttpException($class, null, sprintf('Class "%s" does not exist', $class), 404, null); + } + return $this->container->get($class); + } + + /** + * Called when loading class method + * @param object $class + * @param string $method + * @param array $parameters + * @return string + */ + public function loadClassMethod($class, string $method, array $parameters) + { + return (string)$this->container->call([$class, $method], $parameters); + } + + /** + * Load closure + * + * @param Callable $closure + * @param array $parameters + * @return string + */ + public function loadClosure(callable $closure, array $parameters) + { + return (string)$this->container->call($closure, $parameters); + } +} +``` + +## Parameters + +This section contains advanced tips & tricks on extending the usage for parameters. + +## Extending + +This is a simple example of an integration into a framework. + +The framework has it's own ```Router``` class which inherits from the ```SimpleRouter``` class. This allows the framework to add custom functionality like loading a custom `routes.php` file or add debugging information etc. + +```php +namespace Demo; + +use Pecee\SimpleRouter\SimpleRouter; + +class Router extends SimpleRouter { + + public static function start() { + + // change this to whatever makes sense in your project + require_once 'routes.php'; + + // change default namespace for all routes + parent::setDefaultNamespace('\Demo\Controllers'); + + // Do initial stuff + parent::start(); + + } + +} +``` + +--- + +# Help and support + +This section will go into details on how to debug the router and answer some of the commonly asked questions- and issues. + +## Common issues and fixes + +This section will go over common issues and how to resolve them. + +### Parameters won't match or route not working with special characters + +Often people experience this issue when one or more parameters contains special characters. The router uses a sparse regular-expression that matches letters from a-z along with numbers when matching parameters, to improve performance. + +All other characters has to be defined via the `defaultParameterRegex` option on your route. + +You can read more about adding your own custom regular expression for matching parameters by [clicking here](#custom-regex-for-matching-parameters). + +### Multiple routes matches? Which one has the priority? + +The router will match routes in the order they're added and will render multiple routes, if they match. + +If you want the router to stop when a route is matched, you simply return a value in your callback or stop the execution manually (using `response()->json()` etc.) or simply by returning a result. + +Any returned objects that implements the `__toString()` magic method will also prevent other routes from being rendered. + +If you want the router only to execute one route per request, you can [disabling multiple route rendering](#disable-multiple-route-rendering). + +### Using the router on sub-paths + +Please refer to [Setting custom base path](#setting-custom-base-path) part of the documentation. + +## Debugging + +This section will show you how to write unit-tests for the router, view useful debugging information and answer some of the frequently asked questions. + +It will also covers how to report any issue you might encounter. + +### Creating unit-tests + +The easiest and fastest way to debug any issues with the router, is to create a unit-test that represents the issue you are experiencing. + +Unit-tests use a special `TestRouter` class, which simulates a request-method and requested url of a browser. + +The `TestRouter` class can return the output directly or render a route silently. + +```php +public function testUnicodeCharacters() +{ + // Add route containing two optional paramters with special spanish characters like "í". + TestRouter::get('/cursos/listado/{listado?}/{category?}', 'DummyController@method1', ['defaultParameterRegex' => '[\w\p{L}\s-]+']); + + // Start the routing and simulate the url "/cursos/listado/especialidad/cirugía local". + TestRouter::debugNoReset('/cursos/listado/especialidad/cirugía local', 'GET'); + + // Verify that the url for the loaded route matches the expected route. + $this->assertEquals('/cursos/listado/{listado?}/{category?}/', TestRouter::router()->getRequest()->getLoadedRoute()->getUrl()); + + // Start the routing and simulate the url "/test/Dermatología" using "GET" as request-method. + TestRouter::debugNoReset('/test/Dermatología', 'GET'); + + // Another route containing one parameter with special spanish characters like "í". + TestRouter::get('/test/{param}', 'DummyController@method1', ['defaultParameterRegex' => '[\w\p{L}\s-\í]+']); + + // Get all parameters parsed by the loaded route. + $parameters = TestRouter::request()->getLoadedRoute()->getParameters(); + + // Check that the parameter named "param" matches the exspected value. + $this->assertEquals('Dermatología', $parameters['param']); + + // Add route testing danish special characters like "ø". + TestRouter::get('/category/økse', 'DummyController@method1', ['defaultParameterRegex' => '[\w\ø]+']); + + // Start the routing and simulate the url "/kategory/økse" using "GET" as request-method. + TestRouter::debugNoReset('/category/økse', 'GET'); + + // Validate that the URL of the loaded-route matches the expected url. + $this->assertEquals('/category/økse/', TestRouter::router()->getRequest()->getLoadedRoute()->getUrl()); + + // Reset the router, so other tests wont inherit settings or the routes we've added. + TestRouter::router()->reset(); +} +``` + +#### Using the TestRouter helper + +Depending on your test, you can use the methods below when rendering routes in your unit-tests. + + +| Method | Description | +| ------------- |-------------| +| ```TestRouter::debug($url, $method)``` | Will render the route without returning anything. Exceptions will be thrown and the router will be reset automatically. | +| ```TestRouter::debugOutput($url, $method)``` | Will render the route and return any value that the route might output. Manual reset required by calling `TestRouter::router()->reset()`. | +| ```TestRouter::debugNoReset($url, $method);``` | Will render the route without resetting the router. Useful if you need to get loaded route, parameters etc. from the router. Manual reset required by calling `TestRouter::router()->reset()`. | + +### Debug information + +The library can output debug-information, which contains information like loaded routes, the parsed request-url etc. It also contains info which are important when reporting a new issue like PHP-version, library version, server-variables, router debug log etc. + +You can activate the debug-information by calling the alternative start-method. + +The example below will start the routing an return array with debugging-information + +**Example:** + +```php +$debugInfo = SimpleRouter::startDebug(); +echo sprintf('
%s
', var_export($debugInfo)); +exit; +``` + +**The example above will provide you with an output containing:** + +| Key | Description | +| ------------- |------------- | +| `url` | The parsed request-uri. This url should match the url in the browser.| +| `method` | The browsers request method (example: `GET`, `POST`, `PUT`, `PATCH`, `DELETE` etc).| +| `host` | The website host (example: `domain.com`).| +| `loaded_routes` | List of all the routes that matched the `url` and that has been rendered/loaded. | +| `all_routes` | All available routes | +| `boot_managers` | All available BootManagers | +| `csrf_verifier` | CsrfVerifier class | +| `log` | List of debug messages/log from the router. | +| `router_output` | The rendered callback output from the router. | +| `library_version` | The version of simple-php-router you are using. | +| `php_version` | The version of PHP you are using. | +| `server_params` | List of all `$_SERVER` variables/headers. | + +#### Benchmark and logging + +You can activate benchmark debugging/logging by calling `setDebugEnabled` method on the `Router` instance. + +You have to enable debugging BEFORE starting the routing. + +**Example:** + +```php +SimpleRouter::router()->setDebugEnabled(true); +SimpleRouter::start(); +``` + +When the routing is complete, you can get the debug-log by calling the `getDebugLog()` on the `Router` instance. This will return an `array` of log-messages each containing execution time, trace info and debug-message. + +**Example:** + +```php +$messages = SimpleRouter::router()->getDebugLog(); +``` + +## Reporting a new issue + +**Before reporting your issue, make sure that the issue you are experiencing aren't already answered in the [Common errors](#common-errors) section or by searching the [closed issues](https://github.com/skipperbent/simple-php-router/issues?q=is%3Aissue+is%3Aclosed) page on GitHub.** + +To avoid confusion and to help you resolve your issue as quickly as possible, you should provide a detailed explanation of the problem you are experiencing. + +### Procedure for reporting a new issue + +1. Go to [this page](https://github.com/skipperbent/simple-php-router/issues/new) to create a new issue. +2. Add a title that describes your problems in as few words as possible. +3. Copy and paste the template below in the description of your issue and replace each step with your own information. If the step is not relevant for your issue you can delete it. + +### Issue template + +Copy and paste the template below into the description of your new issue and replace it with your own information. + +You can check the [Debug information](#debug-information) section to see how to generate the debug-info. + +
+### Description
+
+The library fails to render the route `/user/æsel` which contains one parameter using a custom regular expression for matching special foreign characters. Routes without special characters like `/user/tom` renders correctly.
+
+### Steps to reproduce the error
+
+1. Add the following route:
+
+```php
+SimpleRouter::get('/user/{name}', 'UserController@show')->where(['name' => '[\w]+']);
+```
+
+2. Navigate to `/user/æsel` in browser.
+
+3. `NotFoundHttpException` is thrown by library.
+
+### Route and/or callback for failing route
+
+*Route:*
+
+```php
+SimpleRouter::get('/user/{name}', 'UserController@show')->where(['name' => '[\w]+']);
+```
+
+*Callback:*
+
+```php
+public function show($username) {
+    return sprintf('Username is: %s', $username);
+}
+```
+
+### Debug info
+
+```php
+
+[PASTE YOUR DEBUG-INFO HERE]
+
+```
+
+ +Remember that a more detailed issue- description and debug-info might suck to write, but it will help others understand- and resolve your issue without asking for the information. + +**Note:** please be as detailed as possible in the description when creating a new issue. This will help others to more easily understand- and solve your issue. Providing the necessary steps to reproduce the error within your description, adding useful debugging info etc. will help others quickly resolve the issue you are reporting. + +## Feedback and development + +If the library is missing a feature that you need in your project or if you have feedback, we'd love to hear from you. +Feel free to leave us feedback by [creating a new issue](https://github.com/skipperbent/simple-php-router/issues/new). + +**Experiencing an issue?** + +Please refer to our [Help and support](#help-and-support) section in the documentation before reporting a new issue. + +### Contribution development guidelines + +- Please try to follow the PSR-2 codestyle guidelines. + +- Please create your pull requests to the development base that matches the version number you want to change. +For example when pushing changes to version 3, the pull request should use the `v3-development` base/branch. + +- Create detailed descriptions for your commits, as these will be used in the changelog for new releases. + +- When changing existing functionality, please ensure that the unit-tests working. + +- When adding new stuff, please remember to add new unit-tests for the functionality. + +--- + +# Credits + +## Sites + +This is some sites that uses the simple-router project in production. + +- [holla.dk](http://www.holla.dk) +- [ninjaimg.com](http://ninjaimg.com) +- [bookandbegin.com](https://bookandbegin.com) +- [dscuz.com](https://www.dscuz.com) + +## License + +### The MIT License (MIT) + +Copyright (c) 2016 Simon Sessingø / simple-php-router + +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. diff --git a/exam/vendor/pecee/simple-router/composer.json b/exam/vendor/pecee/simple-router/composer.json new file mode 100644 index 0000000..3208bfa --- /dev/null +++ b/exam/vendor/pecee/simple-router/composer.json @@ -0,0 +1,56 @@ +{ + "name": "pecee/simple-router", + "description": "Simple, fast PHP router that is easy to get integrated and in almost any project. Heavily inspired by the Laravel router.", + "keywords": [ + "router", + "router", + "routing", + "route", + "simple-php-router", + "laravel", + "pecee", + "php", + "framework", + "url-handling", + "input-handler", + "routing-engine", + "request-handler" + ], + "license": "MIT", + "support": { + "source": "https://github.com/skipperbent/simple-php-router/issues" + }, + "authors": [ + { + "name": "Simon Sessingø", + "email": "simon.sessingoe@gmail.com" + } + ], + "require": { + "php": ">=7.4", + "ext-json": "*" + }, + "require-dev": { + "phpunit/phpunit": "^8", + "mockery/mockery": "^1", + "phpstan/phpstan": "^1", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-deprecation-rules": "^1", + "phpstan/phpstan-strict-rules": "^1" + }, + "scripts": { + "test": [ + "phpunit tests" + ] + }, + "autoload": { + "psr-4": { + "Pecee\\": "src/Pecee/" + } + }, + "config": { + "allow-plugins": { + "ocramius/package-versions": true + } + } +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/helpers.php b/exam/vendor/pecee/simple-router/helpers.php new file mode 100644 index 0000000..81d836f --- /dev/null +++ b/exam/vendor/pecee/simple-router/helpers.php @@ -0,0 +1,88 @@ +getInputHandler()->value($index, $defaultValue, ...$methods); + } + + return request()->getInputHandler(); +} + +/** + * @param string $url + * @param int|null $code + */ +function redirect(string $url, ?int $code = null): void +{ + if ($code !== null) { + response()->httpCode($code); + } + + response()->redirect($url); +} + +/** + * Get current csrf-token + * @return string|null + */ +function csrf_token(): ?string +{ + $baseVerifier = Router::router()->getCsrfVerifier(); + if ($baseVerifier !== null) { + return $baseVerifier->getTokenProvider()->getToken(); + } + + return null; +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/phpstan.neon b/exam/vendor/pecee/simple-router/phpstan.neon new file mode 100644 index 0000000..3719f4a --- /dev/null +++ b/exam/vendor/pecee/simple-router/phpstan.neon @@ -0,0 +1,22 @@ +parameters: + level: 6 + paths: + - src + fileExtensions: + - php + bootstrapFiles: + - ./vendor/autoload.php + ignoreErrors: + reportUnmatchedIgnoredErrors: true + checkMissingIterableValueType: false + checkGenericClassInNonGenericObjectType: false + parallel: + processTimeout: 300.0 + jobSize: 10 + maximumNumberOfProcesses: 4 + minimumNumberOfJobsPerProcess: 4 +includes: + - vendor/phpstan/phpstan-strict-rules/rules.neon + - vendor/phpstan/phpstan-phpunit/extension.neon + - vendor/phpstan/phpstan-phpunit/rules.neon + - vendor/phpstan/phpstan-deprecation-rules/rules.neon \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/phpunit.xml b/exam/vendor/pecee/simple-router/phpunit.xml new file mode 100644 index 0000000..49b5e9e --- /dev/null +++ b/exam/vendor/pecee/simple-router/phpunit.xml @@ -0,0 +1,24 @@ + + + + + + tests/Pecee/SimpleRouter/ + + + + + src + + + diff --git a/exam/vendor/pecee/simple-router/src/Pecee/Controllers/IResourceController.php b/exam/vendor/pecee/simple-router/src/Pecee/Controllers/IResourceController.php new file mode 100644 index 0000000..8c062fe --- /dev/null +++ b/exam/vendor/pecee/simple-router/src/Pecee/Controllers/IResourceController.php @@ -0,0 +1,48 @@ +index = $index; + + $this->errors = 0; + + // Make the name human friendly, by replace _ with space + $this->name = ucfirst(str_replace('_', ' ', strtolower($this->index))); + } + + /** + * Create from array + * + * @param array $values + * @throws InvalidArgumentException + * @return static + */ + public static function createFromArray(array $values): self + { + if (isset($values['index']) === false) { + throw new InvalidArgumentException('Index key is required'); + } + + /* Easy way of ensuring that all indexes-are set and not filling the screen with isset() */ + + $values += [ + 'tmp_name' => null, + 'type' => null, + 'size' => null, + 'name' => null, + 'error' => null, + ]; + + return (new self($values['index'])) + ->setSize((int)$values['size']) + ->setError((int)$values['error']) + ->setType($values['type']) + ->setTmpName($values['tmp_name']) + ->setFilename($values['name']); + + } + + /** + * @return string + */ + public function getIndex(): string + { + return $this->index; + } + + /** + * Set input index + * @param string $index + * @return static + */ + public function setIndex(string $index): IInputItem + { + $this->index = $index; + + return $this; + } + + /** + * @return int + */ + public function getSize(): ?int + { + return $this->size; + } + + /** + * Set file size + * @param int $size + * @return static + */ + public function setSize(int $size): IInputItem + { + $this->size = $size; + + return $this; + } + + /** + * Get mime-type of file + * @return string + */ + public function getMime(): string + { + return $this->getType(); + } + + /** + * @return string + */ + public function getType(): string + { + return $this->type; + } + + /** + * Set type + * @param string $type + * @return static + */ + public function setType(string $type): IInputItem + { + $this->type = $type; + + return $this; + } + + /** + * Returns extension without "." + * + * @return string + */ + public function getExtension(): string + { + return pathinfo($this->getFilename(), PATHINFO_EXTENSION); + } + + /** + * Get human friendly name + * + * @return string + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * Set human friendly name. + * Useful for adding validation etc. + * + * @param string $name + * @return static + */ + public function setName(string $name): IInputItem + { + $this->name = $name; + + return $this; + } + + /** + * Set filename + * + * @param string $name + * @return static + */ + public function setFilename(string $name): IInputItem + { + $this->filename = $name; + + return $this; + } + + /** + * Get filename + * + * @return string mixed + */ + public function getFilename(): ?string + { + return $this->filename; + } + + /** + * Move the uploaded temporary file to it's new home + * + * @param string $destination + * @return bool + */ + public function move(string $destination): bool + { + return move_uploaded_file($this->tmpName, $destination); + } + + /** + * Get file contents + * + * @return string + */ + public function getContents(): string + { + return file_get_contents($this->tmpName); + } + + /** + * Return true if an upload error occurred. + * + * @return bool + */ + public function hasError(): bool + { + return ($this->getError() !== 0); + } + + /** + * Get upload-error code. + * + * @return int|null + */ + public function getError(): ?int + { + return $this->errors; + } + + /** + * Set error + * + * @param int|null $error + * @return static + */ + public function setError(?int $error): IInputItem + { + $this->errors = (int)$error; + + return $this; + } + + /** + * @return string + */ + public function getTmpName(): string + { + return $this->tmpName; + } + + /** + * Set file temp. name + * @param string $name + * @return static + */ + public function setTmpName(string $name): IInputItem + { + $this->tmpName = $name; + + return $this; + } + + public function __toString(): string + { + return $this->getTmpName(); + } + + public function getValue(): string + { + return $this->getFilename(); + } + + /** + * @param mixed $value + * @return static + */ + public function setValue($value): IInputItem + { + $this->filename = $value; + + return $this; + } + + public function toArray(): array + { + return [ + 'tmp_name' => $this->tmpName, + 'type' => $this->type, + 'size' => $this->size, + 'name' => $this->name, + 'error' => $this->errors, + 'filename' => $this->filename, + ]; + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/src/Pecee/Http/Input/InputHandler.php b/exam/vendor/pecee/simple-router/src/Pecee/Http/Input/InputHandler.php new file mode 100644 index 0000000..79bd672 --- /dev/null +++ b/exam/vendor/pecee/simple-router/src/Pecee/Http/Input/InputHandler.php @@ -0,0 +1,474 @@ +request = $request; + + $this->parseInputs(); + } + + /** + * Parse input values + * + */ + public function parseInputs(): void + { + /* Parse get requests */ + if (count($_GET) !== 0) { + $this->originalParams = $_GET; + $this->get = $this->parseInputItem($this->originalParams); + } + + /* Parse post requests */ + $this->originalPost = $_POST; + + if ($this->request->isPostBack() === true) { + + $contents = file_get_contents('php://input'); + + // Append any PHP-input json + if (strpos(trim($contents), '{') === 0) { + $post = json_decode($contents, true); + + if ($post !== false) { + $this->originalPost += $post; + } + } else { + $post = []; + parse_str($contents, $post); + $this->originalPost += $post; + } + } + + if (count($this->originalPost) !== 0) { + $this->post = $this->parseInputItem($this->originalPost); + } + + /* Parse get requests */ + if (count($_FILES) !== 0) { + $this->originalFile = $_FILES; + $this->file = $this->parseFiles($this->originalFile); + } + } + + /** + * @param array $files Array with files to parse + * @param string|null $parentKey Key from parent (used when parsing nested array). + * @return array + */ + public function parseFiles(array $files, ?string $parentKey = null): array + { + $list = []; + + foreach ($files as $key => $value) { + + // Parse multi dept file array + if (isset($value['name']) === false && is_array($value) === true) { + $list[$key] = $this->parseFiles($value, $key); + continue; + } + + // Handle array input + if (is_array($value['name']) === false) { + $values = ['index' => $parentKey ?? $key]; + + try { + $list[$key] = InputFile::createFromArray($values + $value); + } catch (InvalidArgumentException $e) { + + } + continue; + } + + $keys = [$key]; + $files = $this->rearrangeFile($value['name'], $keys, $value); + + if (isset($list[$key]) === true) { + $list[$key][] = $files; + } else { + $list[$key] = $files; + } + + } + + return $list; + } + + /** + * Rearrange multi-dimensional file object created by PHP. + * + * @param array $values + * @param array $index + * @param array|null $original + * @return array + */ + protected function rearrangeFile(array $values, array &$index, ?array $original): array + { + $originalIndex = $index[0]; + array_shift($index); + + $output = []; + + foreach ($values as $key => $value) { + + if (is_array($original['name'][$key]) === false) { + + try { + + $file = InputFile::createFromArray([ + 'index' => ($key === '' && $originalIndex !== '') ? $originalIndex : $key, + 'name' => $original['name'][$key], + 'error' => $original['error'][$key], + 'tmp_name' => $original['tmp_name'][$key], + 'type' => $original['type'][$key], + 'size' => $original['size'][$key], + ]); + + if (isset($output[$key]) === true) { + $output[$key][] = $file; + continue; + } + + $output[$key] = $file; + continue; + + } catch (InvalidArgumentException $e) { + + } + } + + $index[] = $key; + + $files = $this->rearrangeFile($value, $index, $original); + + if (isset($output[$key]) === true) { + $output[$key][] = $files; + } else { + $output[$key] = $files; + } + + } + + return $output; + } + + /** + * Parse input item from array + * + * @param array $array + * @return array + */ + protected function parseInputItem(array $array): array + { + $list = []; + + foreach ($array as $key => $value) { + + // Handle array input + if (is_array($value) === true) { + $value = $this->parseInputItem($value); + } + + $list[$key] = new InputItem($key, $value); + } + + return $list; + } + + /** + * Find input object + * + * @param string $index + * @param array ...$methods + * @return IInputItem|array|null + */ + public function find(string $index, ...$methods) + { + $element = null; + + if (count($methods) > 0) { + $methods = is_array(...$methods) ? array_values(...$methods) : $methods; + } + + if (count($methods) === 0 || in_array(Request::REQUEST_TYPE_GET, $methods, true) === true) { + $element = $this->get($index); + } + + if (($element === null && count($methods) === 0) || (count($methods) !== 0 && in_array(Request::REQUEST_TYPE_POST, $methods, true) === true)) { + $element = $this->post($index); + } + + if (($element === null && count($methods) === 0) || (count($methods) !== 0 && in_array('file', $methods, true) === true)) { + $element = $this->file($index); + } + + return $element; + } + + protected function getValueFromArray(array $array): array + { + $output = []; + /* @var $item InputItem */ + foreach ($array as $key => $item) { + + if ($item instanceof IInputItem) { + $item = $item->getValue(); + } + + $output[$key] = is_array($item) ? $this->getValueFromArray($item) : $item; + } + + return $output; + } + + /** + * Get input element value matching index + * + * @param string $index + * @param string|mixed|null $defaultValue + * @param array ...$methods + * @return string|array + */ + public function value(string $index, $defaultValue = null, ...$methods) + { + $input = $this->find($index, ...$methods); + + if ($input instanceof IInputItem) { + $input = $input->getValue(); + } + + /* Handle collection */ + if (is_array($input) === true) { + $output = $this->getValueFromArray($input); + + return (count($output) === 0) ? $defaultValue : $output; + } + + return ($input === null || (is_string($input) && trim($input) === '')) ? $defaultValue : $input; + } + + /** + * Check if a input-item exist. + * If an array is as $index parameter the method returns true if all elements exist. + * + * @param string|array $index + * @param array ...$methods + * @return bool + */ + public function exists($index, ...$methods): bool + { + // Check array + if (is_array($index) === true) { + foreach ($index as $key) { + if ($this->value($key, null, ...$methods) === null) { + return false; + } + } + + return true; + } + + return $this->value($index, null, ...$methods) !== null; + } + + /** + * Find post-value by index or return default value. + * + * @param string $index + * @param mixed|null $defaultValue + * @return InputItem|array|string|null + */ + public function post(string $index, $defaultValue = null) + { + return $this->post[$index] ?? $defaultValue; + } + + /** + * Find file by index or return default value. + * + * @param string $index + * @param mixed|null $defaultValue + * @return InputFile|array|string|null + */ + public function file(string $index, $defaultValue = null) + { + return $this->file[$index] ?? $defaultValue; + } + + /** + * Find parameter/query-string by index or return default value. + * + * @param string $index + * @param mixed|null $defaultValue + * @return InputItem|array|string|null + */ + public function get(string $index, $defaultValue = null) + { + return $this->get[$index] ?? $defaultValue; + } + + /** + * Get all get/post items + * @param array $filter Only take items in filter + * @return array + */ + public function all(array $filter = []): array + { + $output = $this->originalParams + $this->originalPost + $this->originalFile; + $output = (count($filter) > 0) ? array_intersect_key($output, array_flip($filter)) : $output; + + foreach ($filter as $filterKey) { + if (array_key_exists($filterKey, $output) === false) { + $output[$filterKey] = null; + } + } + + return $output; + } + + /** + * Add GET parameter + * + * @param string $key + * @param InputItem $item + */ + public function addGet(string $key, InputItem $item): void + { + $this->get[$key] = $item; + } + + /** + * Add POST parameter + * + * @param string $key + * @param InputItem $item + */ + public function addPost(string $key, InputItem $item): void + { + $this->post[$key] = $item; + } + + /** + * Add FILE parameter + * + * @param string $key + * @param InputFile $item + */ + public function addFile(string $key, InputFile $item): void + { + $this->file[$key] = $item; + } + + /** + * Get original post variables + * @return array + */ + public function getOriginalPost(): array + { + return $this->originalPost; + } + + /** + * Set original post variables + * @param array $post + * @return static $this + */ + public function setOriginalPost(array $post): self + { + $this->originalPost = $post; + + return $this; + } + + /** + * Get original get variables + * @return array + */ + public function getOriginalParams(): array + { + return $this->originalParams; + } + + /** + * Set original get-variables + * @param array $params + * @return static $this + */ + public function setOriginalParams(array $params): self + { + $this->originalParams = $params; + + return $this; + } + + /** + * Get original file variables + * @return array + */ + public function getOriginalFile(): array + { + return $this->originalFile; + } + + /** + * Set original file posts variables + * @param array $file + * @return static $this + */ + public function setOriginalFile(array $file): self + { + $this->originalFile = $file; + + return $this; + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/src/Pecee/Http/Input/InputItem.php b/exam/vendor/pecee/simple-router/src/Pecee/Http/Input/InputItem.php new file mode 100644 index 0000000..4d574a8 --- /dev/null +++ b/exam/vendor/pecee/simple-router/src/Pecee/Http/Input/InputItem.php @@ -0,0 +1,123 @@ +index = $index; + $this->value = $value; + + // Make the name human friendly, by replace _ with space + $this->name = ucfirst(str_replace('_', ' ', strtolower($this->index))); + } + + /** + * @return string + */ + public function getIndex(): string + { + return $this->index; + } + + public function setIndex(string $index): IInputItem + { + $this->index = $index; + + return $this; + } + + /** + * @return string + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * Set input name + * @param string $name + * @return static + */ + public function setName(string $name): IInputItem + { + $this->name = $name; + + return $this; + } + + /** + * @return mixed + */ + public function getValue() + { + return $this->value; + } + + /** + * Set input value + * @param mixed $value + * @return static + */ + public function setValue($value): IInputItem + { + $this->value = $value; + + return $this; + } + + public function offsetExists($offset): bool + { + return isset($this->value[$offset]); + } + + #[\ReturnTypeWillChange] + public function offsetGet($offset): ?self + { + if ($this->offsetExists($offset) === true) { + return $this->value[$offset]; + } + + return null; + } + + public function offsetSet($offset, $value): void + { + $this->value[$offset] = $value; + } + + public function offsetUnset($offset): void + { + unset($this->value[$offset]); + } + + public function __toString(): string + { + $value = $this->getValue(); + + return (is_array($value) === true) ? json_encode($value) : $value; + } + + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->getValue()); + } +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/src/Pecee/Http/Middleware/BaseCsrfVerifier.php b/exam/vendor/pecee/simple-router/src/Pecee/Http/Middleware/BaseCsrfVerifier.php new file mode 100644 index 0000000..f9ce3fe --- /dev/null +++ b/exam/vendor/pecee/simple-router/src/Pecee/Http/Middleware/BaseCsrfVerifier.php @@ -0,0 +1,132 @@ +tokenProvider = new CookieTokenProvider(); + } + + protected function isIncluded(Request $request): bool + { + if (count($this->include) > 0) { + foreach ($this->include as $includeUrl) { + $includeUrl = rtrim($includeUrl, '/'); + if ($includeUrl[strlen($includeUrl) - 1] === '*') { + $includeUrl = rtrim($includeUrl, '*'); + return $request->getUrl()->contains($includeUrl); + } + + return ($includeUrl === rtrim($request->getUrl()->getRelativeUrl(false), '/')); + } + } + + return false; + } + + /** + * Check if the url matches the urls in the except property + * @param Request $request + * @return bool + */ + protected function skip(Request $request): bool + { + if (count($this->except) === 0) { + return false; + } + + foreach ($this->except as $url) { + $url = rtrim($url, '/'); + if ($url[strlen($url) - 1] === '*') { + $url = rtrim($url, '*'); + $skip = $request->getUrl()->contains($url); + } else { + $skip = ($url === rtrim($request->getUrl()->getRelativeUrl(false), '/')); + } + + if ($skip === true) { + + $skip = !$this->isIncluded($request); + + if ($skip === false) { + continue; + } + + return true; + } + } + + return false; + } + + /** + * Handle request + * + * @param Request $request + * @throws TokenMismatchException + */ + public function handle(Request $request): void + { + if ($this->skip($request) === false && ($request->isPostBack() === true || $request->isPostBack() === true && $this->isIncluded($request) === true)) { + + $token = $request->getInputHandler()->value( + static::POST_KEY, + $request->getHeader(static::HEADER_KEY), + ); + + if ($this->tokenProvider->validate((string)$token) === false) { + throw new TokenMismatchException('Invalid CSRF-token.'); + } + + } + + // Refresh existing token + $this->tokenProvider->refresh(); + } + + public function getTokenProvider(): ITokenProvider + { + return $this->tokenProvider; + } + + /** + * Set token provider + * @param ITokenProvider $provider + */ + public function setTokenProvider(ITokenProvider $provider): void + { + $this->tokenProvider = $provider; + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/src/Pecee/Http/Middleware/Exceptions/TokenMismatchException.php b/exam/vendor/pecee/simple-router/src/Pecee/Http/Middleware/Exceptions/TokenMismatchException.php new file mode 100644 index 0000000..78f24cc --- /dev/null +++ b/exam/vendor/pecee/simple-router/src/Pecee/Http/Middleware/Exceptions/TokenMismatchException.php @@ -0,0 +1,10 @@ +ipWhitelist, true) === true) { + return true; + } + + foreach ($this->ipBlacklist as $blackIp) { + + // Blocks range (8.8.*) + if ($blackIp[strlen($blackIp) - 1] === '*' && strpos($ip, trim($blackIp, '*')) === 0) { + return false; + } + + // Blocks exact match + if ($blackIp === $ip) { + return false; + } + + } + + return true; + } + + /** + * @param Request $request + * @throws HttpException + */ + public function handle(Request $request): void + { + if($this->validate((string)$request->getIp()) === false) { + throw new HttpException(sprintf('Restricted ip. Access to %s has been blocked', $request->getIp()), 403); + } + } +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/src/Pecee/Http/Request.php b/exam/vendor/pecee/simple-router/src/Pecee/Http/Request.php new file mode 100644 index 0000000..e9f9742 --- /dev/null +++ b/exam/vendor/pecee/simple-router/src/Pecee/Http/Request.php @@ -0,0 +1,564 @@ + $value) { + $this->headers[strtolower($key)] = $value; + $this->headers[str_replace('_', '-', strtolower($key))] = $value; + } + + $this->setHost($this->getHeader('http-host')); + + // Check if special IIS header exist, otherwise use default. + $url = $this->getHeader('unencoded-url'); + if ($url !== null) { + $this->setUrl(new Url($url)); + } else { + $this->setUrl(new Url(urldecode((string)$this->getHeader('request-uri')))); + } + $this->setContentType((string)$this->getHeader('content-type')); + $this->setMethod((string)($_POST[static::FORCE_METHOD_KEY] ?? $this->getHeader('request-method'))); + $this->inputHandler = new InputHandler($this); + } + + public function isSecure(): bool + { + return $this->getHeader('http-x-forwarded-proto') === 'https' || $this->getHeader('https') !== null || (int)$this->getHeader('server-port') === 443; + } + + /** + * @return Url + */ + public function getUrl(): Url + { + return $this->url; + } + + /** + * Copy url object + * + * @return Url + */ + public function getUrlCopy(): Url + { + return clone $this->url; + } + + /** + * @return string|null + */ + public function getHost(): ?string + { + return $this->host; + } + + /** + * @return string|null + */ + public function getMethod(): ?string + { + return $this->method; + } + + /** + * Get http basic auth user + * @return string|null + */ + public function getUser(): ?string + { + return $this->getHeader('php-auth-user'); + } + + /** + * Get http basic auth password + * @return string|null + */ + public function getPassword(): ?string + { + return $this->getHeader('php-auth-pw'); + } + + /** + * Get the csrf token + * @return string|null + */ + public function getCsrfToken(): ?string + { + return $this->getHeader(BaseCsrfVerifier::HEADER_KEY); + } + + /** + * Get all headers + * @return array + */ + public function getHeaders(): array + { + return $this->headers; + } + + /** + * Get id address + * If $safe is false, this function will detect Proxys. But the user can edit this header to whatever he wants! + * https://stackoverflow.com/questions/3003145/how-to-get-the-client-ip-address-in-php#comment-25086804 + * @param bool $safeMode When enabled, only safe non-spoofable headers will be returned. Note this can cause issues when using proxy. + * @return string|null + */ + public function getIp(bool $safeMode = false): ?string + { + $headers = []; + if ($safeMode === false) { + $headers = [ + 'http-cf-connecting-ip', + 'http-client-ip', + 'http-x-forwarded-for', + ]; + } + + $headers[] = 'remote-addr'; + + return $this->getFirstHeader($headers); + } + + /** + * Get remote address/ip + * + * @alias static::getIp + * @return string|null + */ + public function getRemoteAddr(): ?string + { + return $this->getIp(); + } + + /** + * Get referer + * @return string|null + */ + public function getReferer(): ?string + { + return $this->getHeader('http-referer'); + } + + /** + * Get user agent + * @return string|null + */ + public function getUserAgent(): ?string + { + return $this->getHeader('http-user-agent'); + } + + /** + * Get header value by name + * + * @param string $name Name of the header. + * @param string|mixed|null $defaultValue Value to be returned if header is not found. + * @param bool $tryParse When enabled the method will try to find the header from both from client (http) and server-side variants, if the header is not found. + * + * @return string|null + */ + public function getHeader(string $name, $defaultValue = null, bool $tryParse = true): ?string + { + $name = strtolower($name); + $header = $this->headers[$name] ?? null; + + if ($tryParse === true && $header === null) { + if (strpos($name, 'http-') === 0) { + // Trying to find client header variant which was not found, searching for header variant without http- prefix. + $header = $this->headers[str_replace('http-', '', $name)] ?? null; + } else { + // Trying to find server variant which was not found, searching for client variant with http- prefix. + $header = $this->headers['http-' . $name] ?? null; + } + } + + return $header ?? $defaultValue; + } + + /** + * Will try to find first header from list of headers. + * + * @param array $headers + * @param mixed|null $defaultValue + * @return mixed|null + */ + public function getFirstHeader(array $headers, $defaultValue = null) + { + foreach ($headers as $header) { + $header = $this->getHeader($header); + if ($header !== null) { + return $header; + } + } + + return $defaultValue; + } + + /** + * Get request content-type + * @return string|null + */ + public function getContentType(): ?string + { + return $this->contentType; + } + + /** + * Set request content-type + * @param string $contentType + * @return $this + */ + protected function setContentType(string $contentType): self + { + if (strpos($contentType, ';') > 0) { + $this->contentType = strtolower(substr($contentType, 0, strpos($contentType, ';'))); + } else { + $this->contentType = strtolower($contentType); + } + + return $this; + } + + /** + * Get input class + * @return InputHandler + */ + public function getInputHandler(): InputHandler + { + return $this->inputHandler; + } + + /** + * Is format accepted + * + * @param string $format + * + * @return bool + */ + public function isFormatAccepted(string $format): bool + { + return ($this->getHeader('http-accept') !== null && stripos($this->getHeader('http-accept'), $format) !== false); + } + + /** + * Returns true if the request is made through Ajax + * + * @return bool + */ + public function isAjax(): bool + { + return (strtolower((string)$this->getHeader('http-x-requested-with')) === 'xmlhttprequest'); + } + + /** + * Returns true when request-method is type that could contain data in the page body. + * + * @return bool + */ + public function isPostBack(): bool + { + return in_array($this->getMethod(), static::$requestTypesPost, true); + } + + /** + * Get accept formats + * @return array + */ + public function getAcceptFormats(): array + { + return explode(',', $this->getHeader('http-accept')); + } + + /** + * @param Url $url + */ + public function setUrl(Url $url): void + { + $this->url = $url; + + if ($this->isSecure() === true) { + $this->url->setScheme('https'); + } + } + + /** + * @param string|null $host + */ + public function setHost(?string $host): void + { + // Strip any potential ports from hostname + if (strpos((string)$host, ':') !== false) { + $host = strstr($host, strrchr($host, ':'), true); + } + + $this->host = $host; + } + + /** + * @param string $method + */ + public function setMethod(string $method): void + { + $this->method = strtolower($method); + } + + /** + * Set rewrite route + * + * @param ILoadableRoute $route + * @return static + */ + public function setRewriteRoute(ILoadableRoute $route): self + { + $this->hasPendingRewrite = true; + $this->rewriteRoute = SimpleRouter::addDefaultNamespace($route); + + return $this; + } + + /** + * Get rewrite route + * + * @return ILoadableRoute|null + */ + public function getRewriteRoute(): ?ILoadableRoute + { + return $this->rewriteRoute; + } + + /** + * Get rewrite url + * + * @return string|null + */ + public function getRewriteUrl(): ?string + { + return $this->rewriteUrl; + } + + /** + * Set rewrite url + * + * @param string $rewriteUrl + * @return static + */ + public function setRewriteUrl(string $rewriteUrl): self + { + $this->hasPendingRewrite = true; + $this->rewriteUrl = rtrim($rewriteUrl, '/') . '/'; + + return $this; + } + + /** + * Set rewrite callback + * @param string|\Closure $callback + * @return static + */ + public function setRewriteCallback($callback): self + { + $this->hasPendingRewrite = true; + + return $this->setRewriteRoute(new RouteUrl($this->getUrl()->getPath(), $callback)); + } + + /** + * Get loaded route + * @return ILoadableRoute|null + */ + public function getLoadedRoute(): ?ILoadableRoute + { + return (count($this->loadedRoutes) > 0) ? end($this->loadedRoutes) : null; + } + + /** + * Get all loaded routes + * + * @return array + */ + public function getLoadedRoutes(): array + { + return $this->loadedRoutes; + } + + /** + * Set loaded routes + * + * @param array $routes + * @return static + */ + public function setLoadedRoutes(array $routes): self + { + $this->loadedRoutes = $routes; + return $this; + } + + /** + * Added loaded route + * + * @param ILoadableRoute $route + * @return static + */ + public function addLoadedRoute(ILoadableRoute $route): self + { + $this->loadedRoutes[] = $route; + return $this; + } + + /** + * Returns true if the request contains a rewrite + * + * @return bool + */ + public function hasPendingRewrite(): bool + { + return $this->hasPendingRewrite; + } + + /** + * Defines if the current request contains a rewrite. + * + * @param bool $boolean + * @return Request + */ + public function setHasPendingRewrite(bool $boolean): self + { + $this->hasPendingRewrite = $boolean; + return $this; + } + + public function __isset($name): bool + { + return array_key_exists($name, $this->data) === true; + } + + public function __set($name, $value = null) + { + $this->data[$name] = $value; + } + + public function __get($name) + { + return $this->data[$name] ?? null; + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/src/Pecee/Http/Response.php b/exam/vendor/pecee/simple-router/src/Pecee/Http/Response.php new file mode 100644 index 0000000..5e6ca47 --- /dev/null +++ b/exam/vendor/pecee/simple-router/src/Pecee/Http/Response.php @@ -0,0 +1,132 @@ +request = $request; + } + + /** + * Set the http status code + * + * @param int $code + * @return static + */ + public function httpCode(int $code): self + { + http_response_code($code); + + return $this; + } + + /** + * Redirect the response + * + * @param string $url + * @param ?int $httpCode + * + * @return never + */ + public function redirect(string $url, ?int $httpCode = null): void + { + if ($httpCode !== null) { + $this->httpCode($httpCode); + } + + $this->header('location: ' . $url); + exit(0); + } + + public function refresh(): void + { + $this->redirect($this->request->getUrl()->getOriginalUrl()); + } + + /** + * Add http authorisation + * @param string $name + * @return static + */ + public function auth(string $name = ''): self + { + $this->headers([ + 'WWW-Authenticate: Basic realm="' . $name . '"', + 'HTTP/1.0 401 Unauthorized', + ]); + + return $this; + } + + public function cache(string $eTag, int $lastModifiedTime = 2592000): self + { + $this->headers([ + 'Cache-Control: public', + sprintf('Last-Modified: %s GMT', gmdate('D, d M Y H:i:s', $lastModifiedTime)), + sprintf('Etag: %s', $eTag), + ]); + + $httpModified = $this->request->getHeader('http-if-modified-since'); + $httpIfNoneMatch = $this->request->getHeader('http-if-none-match'); + + if (($httpIfNoneMatch !== null && $httpIfNoneMatch === $eTag) || ($httpModified !== null && strtotime($httpModified) === $lastModifiedTime)) { + + $this->header('HTTP/1.1 304 Not Modified'); + exit(0); + } + + return $this; + } + + /** + * Json encode + * @param array|JsonSerializable $value + * @param int $options JSON options Bitmask consisting of JSON_HEX_QUOT, JSON_HEX_TAG, JSON_HEX_AMP, JSON_HEX_APOS, JSON_NUMERIC_CHECK, JSON_PRETTY_PRINT, JSON_UNESCAPED_SLASHES, JSON_FORCE_OBJECT, JSON_PRESERVE_ZERO_FRACTION, JSON_UNESCAPED_UNICODE, JSON_PARTIAL_OUTPUT_ON_ERROR. + * @param int $dept JSON debt. + * @throws InvalidArgumentException + */ + public function json($value, int $options = 0, int $dept = 512): void + { + if (($value instanceof JsonSerializable) === false && is_array($value) === false) { + throw new InvalidArgumentException('Invalid type for parameter "value". Must be of type array or object implementing the \JsonSerializable interface.'); + } + + $this->header('Content-Type: application/json; charset=utf-8'); + echo json_encode($value, $options, $dept); + exit(0); + } + + /** + * Add header to response + * @param string $value + * @return static + */ + public function header(string $value): self + { + header($value); + + return $this; + } + + /** + * Add multiple headers to response + * @param array $headers + * @return static + */ + public function headers(array $headers): self + { + foreach ($headers as $header) { + $this->header($header); + } + + return $this; + } + +} diff --git a/exam/vendor/pecee/simple-router/src/Pecee/Http/Security/CookieTokenProvider.php b/exam/vendor/pecee/simple-router/src/Pecee/Http/Security/CookieTokenProvider.php new file mode 100644 index 0000000..b748953 --- /dev/null +++ b/exam/vendor/pecee/simple-router/src/Pecee/Http/Security/CookieTokenProvider.php @@ -0,0 +1,124 @@ +token = ($this->hasToken() === true) ? $_COOKIE[static::CSRF_KEY] : null; + + if ($this->token === null) { + $this->token = $this->generateToken(); + } + } + + /** + * Generate random identifier for CSRF token + * + * @return string + * @throws SecurityException + */ + public function generateToken(): string + { + try { + return bin2hex(random_bytes(32)); + } catch (Exception $e) { + throw new SecurityException($e->getMessage(), (int)$e->getCode(), $e->getPrevious()); + } + } + + /** + * Validate valid CSRF token + * + * @param string $token + * @return bool + */ + public function validate(string $token): bool + { + if ($this->getToken() !== null) { + return hash_equals($token, $this->getToken()); + } + + return false; + } + + /** + * Set csrf token cookie + * Overwrite this method to save the token to another storage like session etc. + * + * @param string $token + */ + public function setToken(string $token): void + { + $this->token = $token; + setcookie(static::CSRF_KEY, $token, time() + (60 * $this->cookieTimeoutMinutes), '/', ini_get('session.cookie_domain'), ini_get('session.cookie_secure'), ini_get('session.cookie_httponly')); + } + + /** + * Get csrf token + * @param string|null $defaultValue + * @return string|null + */ + public function getToken(?string $defaultValue = null): ?string + { + return $this->token ?? $defaultValue; + } + + /** + * Refresh existing token + */ + public function refresh(): void + { + if ($this->token !== null) { + $this->setToken($this->token); + } + } + + /** + * Returns whether the csrf token has been defined + * @return bool + */ + public function hasToken(): bool + { + return isset($_COOKIE[static::CSRF_KEY]); + } + + /** + * Get timeout for cookie in minutes + * @return int + */ + public function getCookieTimeoutMinutes(): int + { + return $this->cookieTimeoutMinutes; + } + + /** + * Set cookie timeout in minutes + * @param int $minutes + */ + public function setCookieTimeoutMinutes(int $minutes): void + { + $this->cookieTimeoutMinutes = $minutes; + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/src/Pecee/Http/Security/Exceptions/SecurityException.php b/exam/vendor/pecee/simple-router/src/Pecee/Http/Security/Exceptions/SecurityException.php new file mode 100644 index 0000000..0588d72 --- /dev/null +++ b/exam/vendor/pecee/simple-router/src/Pecee/Http/Security/Exceptions/SecurityException.php @@ -0,0 +1,10 @@ +originalUrl = $url; + $this->parse($url, true); + } + + public function parse(?string $url, bool $setOriginalPath = false): self + { + if ($url !== null) { + $data = $this->parseUrl($url); + + $this->scheme = $data['scheme'] ?? null; + $this->host = $data['host'] ?? null; + $this->port = $data['port'] ?? null; + $this->username = $data['user'] ?? null; + $this->password = $data['pass'] ?? null; + + if (isset($data['path']) === true) { + $this->setPath($data['path']); + + if ($setOriginalPath === true) { + $this->originalPath = $data['path']; + } + } + + $this->fragment = $data['fragment'] ?? null; + + if (isset($data['query']) === true) { + $this->setQueryString($data['query']); + } + } + + return $this; + } + + /** + * Check if url is using a secure protocol like https + * + * @return bool + */ + public function isSecure(): bool + { + return (strtolower($this->getScheme()) === 'https'); + } + + /** + * Checks if url is relative + * + * @return bool + */ + public function isRelative(): bool + { + return ($this->getHost() === null); + } + + /** + * Get url scheme + * + * @return string|null + */ + public function getScheme(): ?string + { + return $this->scheme; + } + + /** + * Set the scheme of the url + * + * @param string $scheme + * @return static + */ + public function setScheme(string $scheme): self + { + $this->scheme = $scheme; + + return $this; + } + + /** + * Get url host + * + * @param bool $includeTrails Prepend // in front of hostname + * @return string|null + */ + public function getHost(bool $includeTrails = false): ?string + { + if ((string)$this->host !== '' && $includeTrails === true) { + return '//' . $this->host; + } + + return $this->host; + } + + /** + * Set the host of the url + * + * @param string $host + * @return static + */ + public function setHost(string $host): self + { + $this->host = $host; + + return $this; + } + + /** + * Get url port + * + * @return int|null + */ + public function getPort(): ?int + { + return ($this->port !== null) ? (int)$this->port : null; + } + + /** + * Set the port of the url + * + * @param int $port + * @return static + */ + public function setPort(int $port): self + { + $this->port = $port; + + return $this; + } + + /** + * Parse username from url + * + * @return string|null + */ + public function getUsername(): ?string + { + return $this->username; + } + + /** + * Set the username of the url + * + * @param string $username + * @return static + */ + public function setUsername(string $username): self + { + $this->username = $username; + + return $this; + } + + /** + * Parse password from url + * @return string|null + */ + public function getPassword(): ?string + { + return $this->password; + } + + /** + * Set the url password + * + * @param string $password + * @return static + */ + public function setPassword(string $password): self + { + $this->password = $password; + + return $this; + } + + /** + * Get path from url + * @return string + */ + public function getPath(): ?string + { + return $this->path ?? '/'; + } + + /** + * Get original path with no sanitization of ending trail/slash. + * @return string|null + */ + public function getOriginalPath(): ?string + { + return $this->originalPath; + } + + /** + * Set the url path + * + * @param string $path + * @return static + */ + public function setPath(string $path): self + { + $this->path = rtrim($path, '/') . '/'; + + return $this; + } + + /** + * Get query-string from url + * + * @return array + */ + public function getParams(): array + { + return $this->params; + } + + /** + * Merge parameters array + * + * @param array $params + * @return static + */ + public function mergeParams(array $params): self + { + return $this->setParams(array_merge($this->getParams(), $params)); + } + + /** + * Set the url params + * + * @param array $params + * @return static + */ + public function setParams(array $params): self + { + $this->params = $params; + + return $this; + } + + /** + * Set raw query-string parameters as string + * + * @param string $queryString + * @return static + */ + public function setQueryString(string $queryString): self + { + $params = []; + parse_str($queryString, $params); + + if (count($params) > 0) { + return $this->setParams($params); + } + + return $this; + } + + /** + * Get query-string params as string + * + * @return string + */ + public function getQueryString(): string + { + return static::arrayToParams($this->getParams()); + } + + /** + * Get fragment from url (everything after #) + * + * @return string|null + */ + public function getFragment(): ?string + { + return $this->fragment; + } + + /** + * Set url fragment + * + * @param string $fragment + * @return static + */ + public function setFragment(string $fragment): self + { + $this->fragment = $fragment; + + return $this; + } + + /** + * @return string + */ + public function getOriginalUrl(): string + { + return $this->originalUrl; + } + + /** + * Get position of value. + * Returns -1 on failure. + * + * @param string $value + * @return int + */ + public function indexOf(string $value): int + { + $index = stripos($this->getOriginalUrl(), $value); + + return ($index === false) ? -1 : $index; + } + + /** + * Check if url contains value. + * + * @param string $value + * @return bool + */ + public function contains(string $value): bool + { + return (stripos($this->getOriginalUrl(), $value) !== false); + } + + /** + * Check if url contains parameter/query string. + * + * @param string $name + * @return bool + */ + public function hasParam(string $name): bool + { + return array_key_exists($name, $this->getParams()); + } + + /** + * Removes multiple parameters from the query-string + * + * @param array ...$names + * @return static + */ + public function removeParams(...$names): self + { + $params = array_diff_key($this->getParams(), array_flip(...$names)); + $this->setParams($params); + + return $this; + } + + /** + * Removes parameter from the query-string + * + * @param string $name + * @return static + */ + public function removeParam(string $name): self + { + $params = $this->getParams(); + unset($params[$name]); + $this->setParams($params); + + return $this; + } + + /** + * Get parameter by name. + * Returns parameter value or default value. + * + * @param string $name + * @param string|null $defaultValue + * @return string|null + */ + public function getParam(string $name, ?string $defaultValue = null): ?string + { + return (isset($this->getParams()[$name]) === true) ? $this->getParams()[$name] : $defaultValue; + } + + /** + * UTF-8 aware parse_url() replacement. + * @param string $url + * @param int $component + * @return array + * @throws MalformedUrlException + */ + public function parseUrl(string $url, int $component = -1): array + { + $encodedUrl = preg_replace_callback( + '/[^:\/@?&=#]+/u', + static function ($matches): string { + return urlencode($matches[0]); + }, + $url + ); + + $parts = parse_url($encodedUrl, $component); + + if ($parts === false) { + throw new MalformedUrlException(sprintf('Failed to parse url: "%s"', $url)); + } + + return array_map('urldecode', $parts); + } + + /** + * Convert array to query-string params + * + * @param array $getParams + * @param bool $includeEmpty + * @return string + */ + public static function arrayToParams(array $getParams = [], bool $includeEmpty = true): string + { + if (count($getParams) !== 0) { + + if ($includeEmpty === false) { + $getParams = array_filter($getParams, static function ($item): bool { + return (trim($item) !== ''); + }); + } + + return http_build_query($getParams); + } + + return ''; + } + + /** + * Returns the relative url + * + * @param bool $includeParams + * @return string + */ + public function getRelativeUrl(bool $includeParams = true): string + { + $path = $this->path ?? '/'; + + if ($includeParams === false) { + return $path; + } + + $query = $this->getQueryString() !== '' ? '?' . $this->getQueryString() : ''; + $fragment = $this->fragment !== null ? '#' . $this->fragment : ''; + + return $path . $query . $fragment; + } + + /** + * Returns the absolute url + * + * @param bool $includeParams + * @return string + */ + public function getAbsoluteUrl(bool $includeParams = true): string + { + $scheme = $this->scheme !== null ? $this->scheme . '://' : ''; + $host = $this->host ?? ''; + $port = $this->port !== null ? ':' . $this->port : ''; + $user = $this->username ?? ''; + $pass = $this->password !== null ? ':' . $this->password : ''; + $pass = ($user !== '' || $pass !== '') ? $pass . '@' : ''; + + return $scheme . $user . $pass . $host . $port . $this->getRelativeUrl($includeParams); + } + + /** + * Specify data which should be serialized to JSON + * @link http://php.net/manual/en/jsonserializable.jsonserialize.php + * @return string data which can be serialized by json_encode, + * which is a value of any type other than a resource. + * @since 5.4.0 + */ + public function jsonSerialize(): string + { + return $this->getHost(true) . $this->getRelativeUrl(); + } + + public function __toString(): string + { + return $this->getHost(true) . $this->getRelativeUrl(); + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/ClassLoader/ClassLoader.php b/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/ClassLoader/ClassLoader.php new file mode 100644 index 0000000..82e18ee --- /dev/null +++ b/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/ClassLoader/ClassLoader.php @@ -0,0 +1,49 @@ +eventName = $eventName; + $this->router = $router; + $this->arguments = $arguments; + } + + /** + * Get event name + * + * @return string + */ + public function getEventName(): string + { + return $this->eventName; + } + + /** + * Set the event name + * + * @param string $name + */ + public function setEventName(string $name): void + { + $this->eventName = $name; + } + + /** + * Get the router instance + * + * @return Router + */ + public function getRouter(): Router + { + return $this->router; + } + + /** + * Get the request instance + * + * @return Request + */ + public function getRequest(): Request + { + return $this->getRouter()->getRequest(); + } + + /** + * @param string $name + * @return mixed + */ + public function __get(string $name) + { + return $this->arguments[$name] ?? null; + } + + /** + * @param string $name + * @return bool + */ + public function __isset(string $name): bool + { + return array_key_exists($name, $this->arguments); + } + + /** + * @param string $name + * @param mixed $value + * @throws InvalidArgumentException + */ + public function __set(string $name, $value): void + { + throw new InvalidArgumentException('Not supported'); + } + + /** + * Get arguments + * + * @return array + */ + public function getArguments(): array + { + return $this->arguments; + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Event/IEventArgument.php b/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Event/IEventArgument.php new file mode 100644 index 0000000..a8a6d7e --- /dev/null +++ b/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Event/IEventArgument.php @@ -0,0 +1,46 @@ +class = $class; + $this->method = $method; + } + + /** + * Get class name + * @return string + */ + public function getClass(): string + { + return $this->class; + } + + /** + * Get method + * @return string|null + */ + public function getMethod(): ?string + { + return $this->method; + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Exceptions/HttpException.php b/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Exceptions/HttpException.php new file mode 100644 index 0000000..4a63af8 --- /dev/null +++ b/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Exceptions/HttpException.php @@ -0,0 +1,10 @@ +callback = $callback; + } + + /** + * @param Request $request + * @param Exception $error + */ + public function handleError(Request $request, Exception $error): void + { + /* Fire exceptions */ + call_user_func($this->callback, + $request, + $error + ); + } +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Handlers/DebugEventHandler.php b/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Handlers/DebugEventHandler.php new file mode 100644 index 0000000..6674fc9 --- /dev/null +++ b/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Handlers/DebugEventHandler.php @@ -0,0 +1,63 @@ +callback = static function (EventArgument $argument): void { + // todo: log in database + }; + } + + /** + * Get events. + * + * @param string|null $name Filter events by name. + * @return array + */ + public function getEvents(?string $name): array + { + return [ + $name => [ + $this->callback, + ], + ]; + } + + /** + * Fires any events registered with given event-name + * + * @param Router $router Router instance + * @param string $name Event name + * @param array $eventArgs Event arguments + */ + public function fireEvents(Router $router, string $name, array $eventArgs = []): void + { + $callback = $this->callback; + $callback(new EventArgument($name, $router, $eventArgs)); + } + + /** + * Set debug callback + * + * @param Closure $event + */ + public function setCallback(Closure $event): void + { + $this->callback = $event; + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Handlers/EventHandler.php b/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Handlers/EventHandler.php new file mode 100644 index 0000000..3f276b4 --- /dev/null +++ b/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Handlers/EventHandler.php @@ -0,0 +1,185 @@ +registeredEvents[$name]) === true) { + $this->registeredEvents[$name][] = $callback; + } else { + $this->registeredEvents[$name] = [$callback]; + } + + return $this; + } + + /** + * Get events. + * + * @param string|null $name Filter events by name. + * @param array|string ...$names Add multiple names... + * @return array + */ + public function getEvents(?string $name, ...$names): array + { + if ($name === null) { + return $this->registeredEvents; + } + + $names[] = $name; + $events = []; + + foreach ($names as $eventName) { + if (isset($this->registeredEvents[$eventName]) === true) { + $events += $this->registeredEvents[$eventName]; + } + } + + return $events; + } + + /** + * Fires any events registered with given event-name + * + * @param Router $router Router instance + * @param string $name Event name + * @param array $eventArgs Event arguments + */ + public function fireEvents(Router $router, string $name, array $eventArgs = []): void + { + $events = $this->getEvents(static::EVENT_ALL, $name); + + /* @var $event Closure */ + foreach ($events as $event) { + $event(new EventArgument($name, $router, $eventArgs)); + } + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Handlers/IEventHandler.php b/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Handlers/IEventHandler.php new file mode 100644 index 0000000..bf4e49a --- /dev/null +++ b/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Handlers/IEventHandler.php @@ -0,0 +1,27 @@ +debug('Loading middlewares'); + + foreach ($this->getMiddlewares() as $middleware) { + + if (is_object($middleware) === false) { + $middleware = $router->getClassLoader()->loadClass($middleware); + } + + if (($middleware instanceof IMiddleware) === false) { + throw new HttpException($middleware . ' must be inherit the IMiddleware interface'); + } + + $className = get_class($middleware); + + $router->debug('Loading middleware "%s"', $className); + $middleware->handle($request); + $router->debug('Finished loading middleware "%s"', $className); + } + + $router->debug('Finished loading middlewares'); + } + + public function matchRegex(Request $request, $url): ?bool + { + /* Match on custom defined regular expression */ + if ($this->regex === null) { + return null; + } + + $parameters = []; + if ((bool)preg_match($this->regex, $url, $parameters) !== false) { + $this->setParameters($parameters); + + return true; + } + + return false; + } + + /** + * Set url + * + * @param string $url + * @return static + */ + public function setUrl(string $url): ILoadableRoute + { + $this->url = ($url === '/') ? '/' : '/' . trim($url, '/') . '/'; + + $parameters = []; + if (strpos($this->url, $this->paramModifiers[0]) !== false) { + + $regex = sprintf(static::PARAMETERS_REGEX_FORMAT, $this->paramModifiers[0], $this->paramOptionalSymbol, $this->paramModifiers[1]); + + if ((bool)preg_match_all('/' . $regex . '/u', $this->url, $matches) !== false) { + $parameters = array_fill_keys($matches[1], null); + } + } + + $this->parameters = $parameters; + + return $this; + } + + /** + * Prepends url while ensuring that the url has the correct formatting. + * + * @param string $url + * @return ILoadableRoute + */ + public function prependUrl(string $url): ILoadableRoute + { + return $this->setUrl(rtrim($url, '/') . $this->url); + } + + public function getUrl(): string + { + return $this->url; + } + + /** + * Returns true if group is defined and matches the given url. + * + * @param string $url + * @param Request $request + * @return bool + */ + protected function matchGroup(string $url, Request $request): bool + { + return ($this->getGroup() === null || $this->getGroup()->matchRoute($url, $request) === true); + } + + /** + * Find url that matches method, parameters or name. + * Used when calling the url() helper. + * + * @param string|null $method + * @param string|array|null $parameters + * @param string|null $name + * @return string + */ + public function findUrl(?string $method = null, $parameters = null, ?string $name = null): string + { + $url = $this->getUrl(); + + /* Create the param string - {parameter} */ + $param1 = $this->paramModifiers[0] . '%s' . $this->paramModifiers[1]; + + /* Create the param string with the optional symbol - {parameter?} */ + $param2 = $this->paramModifiers[0] . '%s' . $this->paramOptionalSymbol . $this->paramModifiers[1]; + + /* Replace any {parameter} in the url with the correct value */ + + $params = $this->getParameters(); + + foreach (array_keys($params) as $param) { + + if ($parameters === '' || (is_array($parameters) === true && count($parameters) === 0)) { + $value = ''; + } else { + $p = (array)$parameters; + $value = array_key_exists($param, $p) ? $p[$param] : $params[$param]; + + /* If parameter is specifically set to null - use the original-defined value */ + if ($value === null && isset($this->originalParameters[$param]) === true) { + $value = $this->originalParameters[$param]; + } + } + + if (stripos($url, $param1) !== false || stripos($url, $param) !== false) { + /* Add parameter to the correct position */ + $url = str_ireplace([sprintf($param1, $param), sprintf($param2, $param)], (string)$value, $url); + } else { + /* Parameter aren't recognized and will be appended at the end of the url */ + $url .= $value . '/'; + } + } + + $url = rtrim('/' . ltrim($url, '/'), '/') . '/'; + + $group = $this->getGroup(); + + if ($group !== null && count($group->getDomains()) !== 0 && SimpleRouter::request()->getHost() !== $group->getDomains()[0]) { + $url = '//' . $group->getDomains()[0] . $url; + } + + return $url; + } + + /** + * Returns the provided name for the router. + * + * @return string + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * Check if route has given name. + * + * @param string $name + * @return bool + */ + public function hasName(string $name): bool + { + return strtolower((string)$this->name) === strtolower($name); + } + + /** + * Add regular expression match for the entire route. + * + * @param string $regex + * @return static + */ + public function setMatch(string $regex): ILoadableRoute + { + $this->regex = $regex; + + return $this; + } + + /** + * Get regular expression match used for matching route (if defined). + * + * @return string + */ + public function getMatch(): string + { + return $this->regex; + } + + /** + * Sets the router name, which makes it easier to obtain the url or router at a later point. + * Alias for LoadableRoute::setName(). + * + * @param string|array $name + * @return static + * @see LoadableRoute::setName() + */ + public function name($name): ILoadableRoute + { + return $this->setName($name); + } + + /** + * Sets the router name, which makes it easier to obtain the url or router at a later point. + * + * @param string $name + * @return static + */ + public function setName(string $name): ILoadableRoute + { + $this->name = $name; + + return $this; + } + + /** + * Merge with information from another route. + * + * @param array $settings + * @param bool $merge + * @return static + */ + public function setSettings(array $settings, bool $merge = false): IRoute + { + if (isset($settings['as']) === true) { + + $name = $settings['as']; + + if ($this->name !== null && $merge !== false) { + $name .= '.' . $this->name; + } + + $this->setName($name); + } + + if (isset($settings['prefix']) === true) { + $this->prependUrl($settings['prefix']); + } + + return parent::setSettings($settings, $merge); + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Route/Route.php b/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Route/Route.php new file mode 100644 index 0000000..3ddf03f --- /dev/null +++ b/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Route/Route.php @@ -0,0 +1,654 @@ +debug('Starting rendering route "%s"', get_class($this)); + + $callback = $this->getCallback(); + + if ($callback === null) { + return null; + } + + $router->debug('Parsing parameters'); + + $parameters = $this->getParameters(); + + $router->debug('Finished parsing parameters'); + + /* Filter parameters with null-value */ + if ($this->filterEmptyParams === true) { + $parameters = array_filter($parameters, static function ($var): bool { + return ($var !== null); + }); + } + + /* Render callback function */ + if (is_callable($callback) === true) { + $router->debug('Executing callback'); + + /* Load class from type hinting */ + if (is_array($callback) === true && isset($callback[0], $callback[1]) === true) { + $callback[0] = $router->getClassLoader()->loadClass($callback[0]); + } + + /* When the callback is a function */ + + return $router->getClassLoader()->loadClosure($callback, $parameters); + } + + $controller = $this->getClass(); + $method = $this->getMethod(); + + $namespace = $this->getNamespace(); + $className = ($namespace !== null && $controller[0] !== '\\') ? $namespace . '\\' . $controller : $controller; + + $router->debug('Loading class %s', $className); + $class = $router->getClassLoader()->loadClass($className); + + if ($method === null) { + $controller[1] = '__invoke'; + } + + if (method_exists($class, $method) === false) { + throw new ClassNotFoundHttpException($className, $method, sprintf('Method "%s" does not exist in class "%s"', $method, $className), 404, null); + } + + $router->debug('Executing callback %s -> %s', $className, $method); + + return $router->getClassLoader()->loadClassMethod($class, $method, $parameters); + } + + protected function parseParameters($route, $url, Request $request, $parameterRegex = null): ?array + { + $regex = (strpos($route, $this->paramModifiers[0]) === false) ? null : + sprintf + ( + static::PARAMETERS_REGEX_FORMAT, + $this->paramModifiers[0], + $this->paramOptionalSymbol, + $this->paramModifiers[1] + ); + + // Ensures that host names/domains will work with parameters + if ($route[0] === $this->paramModifiers[0]) { + $url = '/' . ltrim($url, '/'); + } + + $urlRegex = ''; + $parameters = []; + + if ($regex === null || (bool)preg_match_all('/' . $regex . '/u', $route, $parameters) === false) { + $urlRegex = preg_quote($route, '/'); + } else { + + foreach (preg_split('/((\.?-?\/?){[^' . $this->paramModifiers[1] . ']+' . $this->paramModifiers[1] . ')/', $route) as $key => $t) { + + $regex = ''; + + if ($key < count($parameters[1])) { + + $name = $parameters[1][$key]; + + /* If custom regex is defined, use that */ + if (isset($this->where[$name]) === true) { + $regex = $this->where[$name]; + } else { + $regex = $parameterRegex ?? $this->defaultParameterRegex ?? static::PARAMETERS_DEFAULT_REGEX; + } + + $regex = sprintf('((\/|-|\.)(?P<%2$s>%3$s))%1$s', $parameters[2][$key], $name, $regex); + } + + $urlRegex .= preg_quote($t, '/') . $regex; + } + } + + // Get name of last param + if (trim($urlRegex) === '' || (bool)preg_match(sprintf($this->urlRegex, $urlRegex), $url, $matches) === false) { + return null; + } + + $values = []; + + if (isset($parameters[1]) === true) { + + $groupParameters = $this->getGroup() !== null ? $this->getGroup()->getParameters() : []; + + $lastParams = []; + + /* Only take matched parameters with name */ + $originalPath = $request->getUrl()->getOriginalPath(); + foreach ((array)$parameters[1] as $i => $name) { + + // Ignore parent parameters + if (isset($groupParameters[$name]) === true) { + $lastParams[$name] = $matches[$name]; + continue; + } + + // If last parameter and slash parameter is enabled, use slash according to original path (non sanitized version) + $lastParameter = $this->paramModifiers[0] . $name . $this->paramModifiers[1] . '/'; + if ($this->slashParameterEnabled && ($i === count($parameters[1]) - 1) && (substr_compare($route, $lastParameter, -strlen($lastParameter)) === 0) && $originalPath[strlen($originalPath) - 1] === '/') { + $matches[$name] .= '/'; + } + + $values[$name] = (isset($matches[$name]) === true && $matches[$name] !== '') ? $matches[$name] : null; + } + + $values += $lastParams; + } + + $this->originalParameters = $values; + + return $values; + } + + /** + * Returns callback name/identifier for the current route based on the callback. + * Useful if you need to get a unique identifier for the loaded route, for instance + * when using translations etc. + * + * @return string + */ + public function getIdentifier(): string + { + if (is_string($this->callback) === true && strpos($this->callback, '@') !== false) { + return $this->callback; + } + + return 'function:' . md5($this->callback); + } + + /** + * Set allowed request methods + * + * @param array $methods + * @return static + */ + public function setRequestMethods(array $methods): IRoute + { + $this->requestMethods = $methods; + + return $this; + } + + /** + * Get allowed request methods + * + * @return array + */ + public function getRequestMethods(): array + { + return $this->requestMethods; + } + + /** + * @return IRoute|null + */ + public function getParent(): ?IRoute + { + return $this->parent; + } + + /** + * Get the group for the route. + * + * @return IGroupRoute|null + */ + public function getGroup(): ?IGroupRoute + { + return $this->group; + } + + /** + * Set group + * + * @param IGroupRoute $group + * @return static + */ + public function setGroup(IGroupRoute $group): IRoute + { + $this->group = $group; + + /* Add/merge parent settings with child */ + + return $this->setSettings($group->toArray(), true); + } + + /** + * Set parent route + * + * @param IRoute $parent + * @return static + */ + public function setParent(IRoute $parent): IRoute + { + $this->parent = $parent; + + return $this; + } + + /** + * Set callback + * + * @param string|array|\Closure $callback + * @return static + */ + public function setCallback($callback): IRoute + { + $this->callback = $callback; + + return $this; + } + + /** + * @return string|callable|null + */ + public function getCallback() + { + return $this->callback; + } + + public function getMethod(): ?string + { + if (is_array($this->callback) === true && count($this->callback) > 1) { + return $this->callback[1]; + } + + if (is_string($this->callback) === true && strpos($this->callback, '@') !== false) { + $tmp = explode('@', $this->callback); + + return $tmp[1]; + } + + return null; + } + + public function getClass(): ?string + { + if (is_array($this->callback) === true && count($this->callback) > 0) { + return $this->callback[0]; + } + + if (is_string($this->callback) === true && strpos($this->callback, '@') !== false) { + $tmp = explode('@', $this->callback); + + return $tmp[0]; + } + + return null; + } + + public function setMethod(string $method): IRoute + { + $this->callback = [$this->getClass(), $method]; + + return $this; + } + + public function setClass(string $class): IRoute + { + $this->callback = [$class, $this->getMethod()]; + + return $this; + } + + /** + * @param string $namespace + * @return static + */ + public function setNamespace(string $namespace): IRoute + { + // Do not set namespace when class-hinting is used + if (is_array($this->callback) === true) { + return $this; + } + + $ns = $this->getNamespace(); + + if ($ns !== null) { + // Don't overwrite namespaces that starts with \ + if ($ns[0] !== '\\') { + $namespace .= '\\' . $ns; + } else { + $namespace = $ns; + } + } + + $this->namespace = $namespace; + + return $this; + } + + /** + * @param string $namespace + * @return static + */ + public function setDefaultNamespace(string $namespace): IRoute + { + $this->defaultNamespace = $namespace; + + return $this; + } + + public function getDefaultNamespace(): ?string + { + return $this->defaultNamespace; + } + + /** + * @return string|null + */ + public function getNamespace(): ?string + { + return $this->namespace ?? $this->defaultNamespace; + } + + public function setSlashParameterEnabled(bool $enabled): self + { + $this->slashParameterEnabled = $enabled; + return $this; + } + + public function getSlashParameterEnabled(): bool + { + return $this->slashParameterEnabled; + } + + /** + * Export route settings to array so they can be merged with another route. + * + * @return array + */ + public function toArray(): array + { + $values = []; + + if ($this->namespace !== null) { + $values['namespace'] = $this->namespace; + } + + if (count($this->requestMethods) !== 0) { + $values['method'] = $this->requestMethods; + } + + if (count($this->where) !== 0) { + $values['where'] = $this->where; + } + + if (count($this->middlewares) !== 0) { + $values['middleware'] = $this->middlewares; + } + + if ($this->defaultParameterRegex !== null) { + $values['defaultParameterRegex'] = $this->defaultParameterRegex; + } + + if ($this->slashParameterEnabled === true) { + $values['includeSlash'] = $this->slashParameterEnabled; + } + + return $values; + } + + /** + * Merge with information from another route. + * + * @param array $settings + * @param bool $merge + * @return static + */ + public function setSettings(array $settings, bool $merge = false): IRoute + { + if (isset($settings['namespace']) === true) { + $this->setNamespace($settings['namespace']); + } + + if (isset($settings['method']) === true) { + $this->setRequestMethods(array_merge($this->requestMethods, (array)$settings['method'])); + } + + if (isset($settings['where']) === true) { + $this->setWhere(array_merge($this->where, (array)$settings['where'])); + } + + if (isset($settings['parameters']) === true) { + $this->setParameters(array_merge($this->parameters, (array)$settings['parameters'])); + } + + // Push middleware if multiple + if (isset($settings['middleware']) === true) { + $this->setMiddlewares(array_merge((array)$settings['middleware'], $this->middlewares)); + } + + if (isset($settings['defaultParameterRegex']) === true) { + $this->setDefaultParameterRegex($settings['defaultParameterRegex']); + } + + if (isset($settings['includeSlash']) === true) { + $this->setSlashParameterEnabled($settings['includeSlash']); + } + + return $this; + } + + /** + * Get parameter names. + * + * @return array + */ + public function getWhere(): array + { + return $this->where; + } + + /** + * Set parameter names. + * + * @param array $options + * @return static + */ + public function setWhere(array $options): IRoute + { + $this->where = $options; + + return $this; + } + + /** + * Add regular expression parameter match. + * Alias for LoadableRoute::where() + * + * @param array $options + * @return static + * @see LoadableRoute::where() + */ + public function where(array $options) + { + return $this->setWhere($options); + } + + /** + * Get parameters + * + * @return array + */ + public function getParameters(): array + { + /* Sort the parameters after the user-defined param order, if any */ + $parameters = []; + + if (count($this->originalParameters) !== 0) { + $parameters = $this->originalParameters; + } + + return array_merge($parameters, $this->parameters); + } + + /** + * Get parameters + * + * @param array $parameters + * @return static + */ + public function setParameters(array $parameters): IRoute + { + $this->parameters = array_merge($this->parameters, $parameters); + + return $this; + } + + /** + * Add middleware class-name + * + * @param string $middleware + * @return static + * @deprecated This method is deprecated and will be removed in the near future. + */ + public function setMiddleware(string $middleware): self + { + $this->middlewares[] = $middleware; + + return $this; + } + + /** + * Add middleware class-name + * + * @param string $middleware + * @return static + */ + public function addMiddleware(string $middleware): IRoute + { + $this->middlewares[] = $middleware; + + return $this; + } + + /** + * Set middlewares array + * + * @param array $middlewares + * @return static + */ + public function setMiddlewares(array $middlewares): IRoute + { + $this->middlewares = $middlewares; + + return $this; + } + + /** + * @return array + */ + public function getMiddlewares(): array + { + return $this->middlewares; + } + + /** + * Set default regular expression used when matching parameters. + * This is used when no custom parameter regex is found. + * + * @param string $regex + * @return static + */ + public function setDefaultParameterRegex(string $regex): self + { + $this->defaultParameterRegex = $regex; + + return $this; + } + + /** + * Get default regular expression used when matching parameters. + * + * @return string + */ + public function getDefaultParameterRegex(): string + { + return $this->defaultParameterRegex; + } + + /** + * If enabled parameters containing null-value will not be passed along to the callback. + * + * @param bool $enabled + * @return static $this + */ + public function setFilterEmptyParams(bool $enabled): IRoute + { + $this->filterEmptyParams = $enabled; + + return $this; + } + + /** + * Status if filtering of empty params is enabled or disabled + * @return bool + */ + public function getFilterEmptyParams(): bool + { + return $this->filterEmptyParams; + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Route/RouteController.php b/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Route/RouteController.php new file mode 100644 index 0000000..ca48a92 --- /dev/null +++ b/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Route/RouteController.php @@ -0,0 +1,186 @@ +setUrl($url); + $this->setName(trim(str_replace('/', '.', $url), '/')); + $this->controller = $controller; + } + + /** + * Check if route has given name. + * + * @param string $name + * @return bool + */ + public function hasName(string $name): bool + { + if ($this->name === null) { + return false; + } + + /* Remove method/type */ + if (strpos($name, '.') !== false) { + $method = substr($name, strrpos($name, '.') + 1); + $newName = substr($name, 0, strrpos($name, '.')); + + if (in_array($method, $this->names, true) === true && strtolower($this->name) === strtolower($newName)) { + return true; + } + } + + return parent::hasName($name); + } + + /** + * @param string|null $method + * @param string|array|null $parameters + * @param string|null $name + * @return string + */ + public function findUrl(?string $method = null, $parameters = null, ?string $name = null): string + { + if (strpos($name, '.') !== false) { + $found = array_search(substr($name, strrpos($name, '.') + 1), $this->names, true); + if ($found !== false) { + $method = (string)$found; + } + } + + $url = ''; + $parameters = (array)$parameters; + + if ($method !== null) { + + /* Remove requestType from method-name, if it exists */ + foreach (Request::$requestTypes as $requestType) { + + if (stripos($method, $requestType) === 0) { + $method = substr($method, strlen($requestType)); + break; + } + } + + $method .= '/'; + } + + $group = $this->getGroup(); + + $url .= '/' . trim($this->getUrl(), '/') . '/' . strtolower((string)$method) . implode('/', $parameters); + + $url = '/' . trim($url, '/') . '/'; + + if ($group !== null && count($group->getDomains()) !== 0 && SimpleRouter::request()->getHost() !== $group->getDomains()[0]) { + $url = '//' . $group->getDomains()[0] . $url; + } + + return $url; + } + + public function matchRoute(string $url, Request $request): bool + { + if ($this->matchGroup($url, $request) === false) { + return false; + } + + /* Match global regular-expression for route */ + $regexMatch = $this->matchRegex($request, $url); + + if ($regexMatch === false || (stripos($url, $this->url) !== 0 && strtoupper($url) !== strtoupper($this->url))) { + return false; + } + + $strippedUrl = trim(str_ireplace($this->url, '/', $url), '/'); + $path = explode('/', $strippedUrl); + + if (count($path) !== 0) { + + $method = (isset($path[0]) === false || trim($path[0]) === '') ? $this->defaultMethod : $path[0]; + $this->method = $request->getMethod() . ucfirst($method); + + $this->parameters = array_slice($path, 1); + + // Set callback + $this->setCallback([$this->controller, $this->method]); + + return true; + } + + return false; + } + + /** + * Get controller class-name. + * + * @return string + */ + public function getController(): string + { + return $this->controller; + } + + /** + * Get controller class-name. + * + * @param string $controller + * @return static + */ + public function setController(string $controller): IControllerRoute + { + $this->controller = $controller; + + return $this; + } + + /** + * Return active method + * + * @return string|null + */ + public function getMethod(): ?string + { + return $this->method; + } + + /** + * Set active method + * + * @param string $method + * @return static + */ + public function setMethod(string $method): IRoute + { + $this->method = $method; + + return $this; + } + + /** + * Merge with information from another route. + * + * @param array $settings + * @param bool $merge + * @return static + */ + public function setSettings(array $settings, bool $merge = false): IRoute + { + if (isset($settings['names']) === true) { + $this->names = $settings['names']; + } + + return parent::setSettings($settings, $merge); + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Route/RouteGroup.php b/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Route/RouteGroup.php new file mode 100644 index 0000000..bcba62f --- /dev/null +++ b/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Route/RouteGroup.php @@ -0,0 +1,265 @@ +domains) === 0) { + return true; + } + + foreach ($this->domains as $domain) { + + // If domain has no parameters but matches + if ($domain === $request->getHost()) { + return true; + } + + $parameters = $this->parseParameters($domain, $request->getHost(), $request, '.*'); + + if ($parameters !== null && count($parameters) !== 0) { + $this->parameters = $parameters; + + return true; + } + } + + return false; + } + + /** + * Method called to check if route matches + * + * @param string $url + * @param Request $request + * @return bool + */ + public function matchRoute(string $url, Request $request): bool + { + if ($this->getGroup() !== null && $this->getGroup()->matchRoute($url, $request) === false) { + return false; + } + + if ($this->prefix !== null) { + /* Parse parameters from current route */ + $parameters = $this->parseParameters($this->prefix, $url, $request); + + /* If no custom regular expression or parameters was found on this route, we stop */ + if ($parameters === null) { + return false; + } + + /* Set the parameters */ + $this->setParameters($parameters); + } + + $parsedPrefix = $this->prefix; + + foreach ($this->getParameters() as $parameter => $value) { + $parsedPrefix = str_ireplace('{' . $parameter . '}', (string)$value, (string)$parsedPrefix); + } + + /* Skip if prefix doesn't match */ + if ($this->prefix !== null && stripos($url, rtrim($parsedPrefix, '/') . '/') === false) { + return false; + } + + return $this->matchDomain($request); + } + + /** + * Add exception handler + * + * @param IExceptionHandler|string $handler + * @return static + */ + public function addExceptionHandler($handler): IGroupRoute + { + $this->exceptionHandlers[] = $handler; + + return $this; + } + + /** + * Set exception-handlers for group + * + * @param array $handlers + * @return static + */ + public function setExceptionHandlers(array $handlers): IGroupRoute + { + $this->exceptionHandlers = $handlers; + + return $this; + } + + /** + * Get exception-handlers for group + * + * @return array + */ + public function getExceptionHandlers(): array + { + return $this->exceptionHandlers; + } + + /** + * Get allowed domains for domain. + * + * @return array + */ + public function getDomains(): array + { + return $this->domains; + } + + /** + * Set allowed domains for group. + * + * @param array $domains + * @return static + */ + public function setDomains(array $domains): IGroupRoute + { + $this->domains = $domains; + + return $this; + } + + /** + * @param string $prefix + * @return static + */ + public function setPrefix(string $prefix): IGroupRoute + { + $this->prefix = '/' . trim($prefix, '/'); + + return $this; + } + + /** + * Prepends prefix while ensuring that the url has the correct formatting. + * + * @param string $url + * @return static + */ + public function prependPrefix(string $url): IGroupRoute + { + return $this->setPrefix(rtrim($url, '/') . $this->prefix); + } + + /** + * Set prefix that child-routes will inherit. + * + * @return string|null + */ + public function getPrefix(): ?string + { + return $this->prefix; + } + + /** + * When enabled group will overwrite any existing exception-handlers. + * + * @param bool $merge + * @return static + */ + public function setMergeExceptionHandlers(bool $merge): IGroupRoute + { + $this->mergeExceptionHandlers = $merge; + + return $this; + } + + /** + * Returns true if group should overwrite existing exception-handlers. + * + * @return bool + */ + public function getMergeExceptionHandlers(): bool + { + return $this->mergeExceptionHandlers; + } + + /** + * Merge with information from another route. + * + * @param array $settings + * @param bool $merge + * @return static + */ + public function setSettings(array $settings, bool $merge = false): IRoute + { + if (isset($settings['prefix']) === true) { + $this->setPrefix($settings['prefix'] . $this->prefix); + } + + if (isset($settings['mergeExceptionHandlers']) === true) { + $this->setMergeExceptionHandlers($settings['mergeExceptionHandlers']); + } + + if ($merge === false && isset($settings['exceptionHandler']) === true) { + $this->setExceptionHandlers((array)$settings['exceptionHandler']); + } + + if (isset($settings['domain']) === true) { + $this->setDomains((array)$settings['domain']); + } + + if (isset($settings['as']) === true) { + + $name = $settings['as']; + + if ($this->name !== null && $merge !== false) { + $name .= '.' . $this->name; + } + + $this->name = $name; + } + + return parent::setSettings($settings, $merge); + } + + /** + * Export route settings to array so they can be merged with another route. + * + * @return array + */ + public function toArray(): array + { + $values = []; + + if ($this->prefix !== null) { + $values['prefix'] = $this->getPrefix(); + } + + if ($this->name !== null) { + $values['as'] = $this->name; + } + + if (count($this->parameters) !== 0) { + $values['parameters'] = $this->parameters; + } + + return array_merge($values, parent::toArray()); + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Route/RoutePartialGroup.php b/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Route/RoutePartialGroup.php new file mode 100644 index 0000000..b59abaa --- /dev/null +++ b/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Route/RoutePartialGroup.php @@ -0,0 +1,7 @@ + '', + 'create' => 'create', + 'store' => '', + 'show' => '', + 'edit' => 'edit', + 'update' => '', + 'destroy' => '', + ]; + + protected array $methodNames = [ + 'index' => 'index', + 'create' => 'create', + 'store' => 'store', + 'show' => 'show', + 'edit' => 'edit', + 'update' => 'update', + 'destroy' => 'destroy', + ]; + + protected array $names = []; + protected string $controller; + + public function __construct($url, $controller) + { + $this->setUrl($url); + $this->controller = $controller; + $this->setName(trim(str_replace('/', '.', $url), '/')); + } + + /** + * Check if route has given name. + * + * @param string $name + * @return bool + */ + public function hasName(string $name): bool + { + if ($this->name === null) { + return false; + } + + if (strtolower($this->name) === strtolower($name)) { + return true; + } + + /* Remove method/type */ + if (strpos($name, '.') !== false) { + $name = substr($name, 0, strrpos($name, '.')); + } + + return (strtolower($this->name) === strtolower($name)); + } + + /** + * @param string|null $method + * @param array|string|null $parameters + * @param string|null $name + * @return string + */ + public function findUrl(?string $method = null, $parameters = null, ?string $name = null): string + { + $url = array_search($name, $this->names, true); + + $parametersUrl = ''; + + if ($parameters !== null && count($parameters) > 0) { + $parametersUrl = join('/', $parameters) . '/'; + } + + if ($url !== false) { + return rtrim($this->url . $parametersUrl . $this->urls[$url], '/') . '/'; + } + + $url = $this->url . $parametersUrl; + + $group = $this->getGroup(); + if ($group !== null && count($group->getDomains()) !== 0 && SimpleRouter::request()->getHost() !== $group->getDomains()[0]) { + $url = '//' . $group->getDomains()[0] . $url; + } + + return $url; + } + + protected function call($method): bool + { + $this->setCallback([$this->controller, $method]); + + return true; + } + + public function matchRoute(string $url, Request $request): bool + { + if ($this->matchGroup($url, $request) === false) { + return false; + } + + /* Match global regular-expression for route */ + $regexMatch = $this->matchRegex($request, $url); + + if ($regexMatch === false || (stripos($url, $this->url) !== 0 && strtoupper($url) !== strtoupper($this->url))) { + return false; + } + + $route = rtrim($this->url, '/') . '/{id?}/{action?}'; + + /* Parse parameters from current route */ + $this->parameters = $this->parseParameters($route, $url, $request); + + /* If no custom regular expression or parameters was found on this route, we stop */ + if ($regexMatch === null && $this->parameters === null) { + return false; + } + + $action = strtolower(trim((string)$this->parameters['action'])); + $id = $this->parameters['id']; + + // Remove action parameter + unset($this->parameters['action']); + + $method = $request->getMethod(); + + // Delete + if ($method === Request::REQUEST_TYPE_DELETE && $id !== null) { + return $this->call($this->methodNames['destroy']); + } + + // Update + if ($id !== null && in_array($method, [Request::REQUEST_TYPE_PATCH, Request::REQUEST_TYPE_PUT], true) === true) { + return $this->call($this->methodNames['update']); + } + + // Edit + if ($method === Request::REQUEST_TYPE_GET && $id !== null && $action === 'edit') { + return $this->call($this->methodNames['edit']); + } + + // Create + if ($method === Request::REQUEST_TYPE_GET && $id === 'create') { + return $this->call($this->methodNames['create']); + } + + // Save + if ($method === Request::REQUEST_TYPE_POST) { + return $this->call($this->methodNames['store']); + } + + // Show + if ($method === Request::REQUEST_TYPE_GET && $id !== null) { + return $this->call($this->methodNames['show']); + } + + // Index + return $this->call($this->methodNames['index']); + } + + /** + * @return string + */ + public function getController(): string + { + return $this->controller; + } + + /** + * @param string $controller + * @return static + */ + public function setController(string $controller): IControllerRoute + { + $this->controller = $controller; + + return $this; + } + + public function setName(string $name): ILoadableRoute + { + $this->name = $name; + + $this->names = [ + 'index' => $this->name . '.index', + 'create' => $this->name . '.create', + 'store' => $this->name . '.store', + 'show' => $this->name . '.show', + 'edit' => $this->name . '.edit', + 'update' => $this->name . '.update', + 'destroy' => $this->name . '.destroy', + ]; + + return $this; + } + + /** + * Define custom method name for resource controller + * + * @param array $names + * @return static $this + */ + public function setMethodNames(array $names): RouteResource + { + $this->methodNames = $names; + + return $this; + } + + /** + * Get method names + * + * @return array + */ + public function getMethodNames(): array + { + return $this->methodNames; + } + + /** + * Merge with information from another route. + * + * @param array $settings + * @param bool $merge + * @return static + */ + public function setSettings(array $settings, bool $merge = false): IRoute + { + if (isset($settings['names']) === true) { + $this->names = $settings['names']; + } + + if (isset($settings['methods']) === true) { + $this->methodNames = $settings['methods']; + } + + return parent::setSettings($settings, $merge); + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Route/RouteUrl.php b/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Route/RouteUrl.php new file mode 100644 index 0000000..f0e2b8a --- /dev/null +++ b/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Route/RouteUrl.php @@ -0,0 +1,47 @@ +setUrl($url); + $this->setCallback($callback); + } + + public function matchRoute(string $url, Request $request): bool + { + if ($this->getGroup() !== null && $this->getGroup()->matchRoute($url, $request) === false) { + return false; + } + + /* Match global regular-expression for route */ + $regexMatch = $this->matchRegex($request, $url); + + if ($regexMatch === false) { + return false; + } + + /* Parse parameters from current route */ + $parameters = $this->parseParameters($this->url, $url, $request); + + /* If no custom regular expression or parameters was found on this route, we stop */ + if ($regexMatch === null && $parameters === null) { + return false; + } + + /* Set the parameters */ + $this->setParameters((array)$parameters); + + return true; + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Router.php b/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Router.php new file mode 100644 index 0000000..3e1964a --- /dev/null +++ b/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/Router.php @@ -0,0 +1,961 @@ +reset(); + } + + /** + * Resets the router by reloading request and clearing all routes and data. + */ + public function reset(): void + { + $this->debugStartTime = microtime(true); + $this->isProcessingRoute = false; + + try { + $this->request = new Request(); + } catch (MalformedUrlException $e) { + $this->debug(sprintf('Invalid request-uri url: %s', $e->getMessage())); + } + + $this->routes = []; + $this->bootManagers = []; + $this->routeStack = []; + $this->processedRoutes = []; + $this->exceptionHandlers = []; + $this->loadedExceptionHandlers = []; + $this->eventHandlers = []; + $this->debugList = []; + $this->csrfVerifier = null; + $this->classLoader = new ClassLoader(); + } + + /** + * Add route + * @param IRoute $route + * @return IRoute + */ + public function addRoute(IRoute $route): IRoute + { + $this->fireEvents(EventHandler::EVENT_ADD_ROUTE, [ + 'route' => $route, + 'isSubRoute' => $this->isProcessingRoute, + ]); + + /* + * If a route is currently being processed, that means that the route being added are rendered from the parent + * routes callback, so we add them to the stack instead. + */ + if ($this->isProcessingRoute === true) { + $this->routeStack[] = $route; + } else { + $this->routes[] = $route; + } + + return $route; + } + + /** + * Render and process any new routes added. + * + * @param IRoute $route + * @throws NotFoundHttpException + */ + protected function renderAndProcess(IRoute $route): void + { + $this->isProcessingRoute = true; + $route->renderRoute($this->request, $this); + $this->isProcessingRoute = false; + + if (count($this->routeStack) !== 0) { + + /* Pop and grab the routes added when executing group callback earlier */ + $stack = $this->routeStack; + $this->routeStack = []; + + /* Route any routes added to the stack */ + $this->processRoutes($stack, ($route instanceof IGroupRoute) ? $route : null); + } + } + + /** + * Process added routes. + * + * @param array|IRoute[] $routes + * @param IGroupRoute|null $group + * @throws NotFoundHttpException + */ + protected function processRoutes(array $routes, ?IGroupRoute $group = null): void + { + $this->debug('Processing routes'); + + // Stop processing routes if no valid route is found. + if ($this->request->getRewriteRoute() === null && $this->request->getUrl()->getOriginalUrl() === '') { + $this->debug('Halted route-processing as no valid route was found'); + + return; + } + + $url = $this->request->getRewriteUrl() ?? $this->request->getUrl()->getPath(); + + // Loop through each route-request + foreach ($routes as $route) { + + $this->debug('Processing route "%s"', get_class($route)); + + if ($group !== null) { + /* Add the parent group */ + $route->setGroup($group); + } + + /* @var $route IGroupRoute */ + if ($route instanceof IGroupRoute) { + + if ($route->matchRoute($url, $this->request) === true) { + + /* Add exception handlers */ + if (count($route->getExceptionHandlers()) !== 0) { + + if ($route->getMergeExceptionHandlers() === true) { + + foreach ($route->getExceptionHandlers() as $handler) { + $this->exceptionHandlers[] = $handler; + } + + } else { + $this->exceptionHandlers = $route->getExceptionHandlers(); + } + } + + /* Only render partial group if it matches */ + if ($route instanceof IPartialGroupRoute === true) { + $this->renderAndProcess($route); + continue; + } + + } + + if ($route instanceof IPartialGroupRoute === false) { + $this->renderAndProcess($route); + } + + continue; + } + + if ($route instanceof ILoadableRoute === true) { + + /* Add the route to the map, so we can find the active one when all routes has been loaded */ + $this->processedRoutes[] = $route; + } + } + } + + /** + * Load routes + * @return void + * @throws NotFoundHttpException + */ + public function loadRoutes(): void + { + $this->debug('Loading routes'); + + $this->fireEvents(EventHandler::EVENT_LOAD_ROUTES, [ + 'routes' => $this->routes, + ]); + + /* Loop through each route-request */ + $this->processRoutes($this->routes); + + $this->fireEvents(EventHandler::EVENT_BOOT, [ + 'bootmanagers' => $this->bootManagers, + ]); + + /* Initialize boot-managers */ + + /* @var $manager IRouterBootManager */ + foreach ($this->bootManagers as $manager) { + + $className = get_class($manager); + $this->debug('Rendering bootmanager "%s"', $className); + $this->fireEvents(EventHandler::EVENT_RENDER_BOOTMANAGER, [ + 'bootmanagers' => $this->bootManagers, + 'bootmanager' => $manager, + ]); + + /* Render bootmanager */ + $manager->boot($this, $this->request); + + $this->debug('Finished rendering bootmanager "%s"', $className); + } + + $this->debug('Finished loading routes'); + } + + /** + * Start the routing + * + * @return string|null + * @throws NotFoundHttpException + * @throws \Pecee\Http\Middleware\Exceptions\TokenMismatchException + * @throws HttpException + * @throws Exception + */ + public function start(): ?string + { + $this->debug('Router starting'); + + $this->fireEvents(EventHandler::EVENT_INIT); + + $this->loadRoutes(); + + if ($this->csrfVerifier !== null) { + + $this->fireEvents(EventHandler::EVENT_RENDER_CSRF, [ + 'csrfVerifier' => $this->csrfVerifier, + ]); + + try { + /* Verify csrf token for request */ + $this->csrfVerifier->handle($this->request); + } catch (Exception $e) { + return $this->handleException($e); + } + } + + $output = $this->routeRequest(); + + $this->fireEvents(EventHandler::EVENT_LOAD, [ + 'loadedRoutes' => $this->getRequest()->getLoadedRoutes(), + ]); + + $this->debug('Routing complete'); + + return $output; + } + + /** + * Routes the request + * + * @return string|null + * @throws HttpException + * @throws Exception + */ + public function routeRequest(): ?string + { + $this->debug('Routing request'); + + $methodNotAllowed = null; + + try { + $url = $this->request->getRewriteUrl() ?? $this->request->getUrl()->getPath(); + + /* @var $route ILoadableRoute */ + foreach ($this->processedRoutes as $key => $route) { + + $this->debug('Matching route "%s"', get_class($route)); + + /* Add current processing route to constants */ + $this->currentProcessingRoute = $route; + + /* If the route matches */ + if ($route->matchRoute($url, $this->request) === true) { + + $this->fireEvents(EventHandler::EVENT_MATCH_ROUTE, [ + 'route' => $route, + ]); + + /* Check if request method matches */ + if (count($route->getRequestMethods()) !== 0 && in_array($this->request->getMethod(), $route->getRequestMethods(), true) === false) { + $this->debug('Method "%s" not allowed', $this->request->getMethod()); + + // Only set method not allowed is not already set + if ($methodNotAllowed === null) { + $methodNotAllowed = true; + } + + continue; + } + + $this->fireEvents(EventHandler::EVENT_RENDER_MIDDLEWARES, [ + 'route' => $route, + 'middlewares' => $route->getMiddlewares(), + ]); + + $route->loadMiddleware($this->request, $this); + + $output = $this->handleRouteRewrite($key, $url); + if ($output !== null) { + return $output; + } + + $methodNotAllowed = false; + + $this->request->addLoadedRoute($route); + + $this->fireEvents(EventHandler::EVENT_RENDER_ROUTE, [ + 'route' => $route, + ]); + + $routeOutput = $route->renderRoute($this->request, $this); + + if ($this->renderMultipleRoutes === true) { + if ($routeOutput !== '') { + return $routeOutput; + } + + $output = $this->handleRouteRewrite($key, $url); + if ($output !== null) { + return $output; + } + } else { + $output = $this->handleRouteRewrite($key, $url); + + return $output ?? $routeOutput; + } + } + } + + } catch (Exception $e) { + return $this->handleException($e); + } + + if ($methodNotAllowed === true) { + $message = sprintf('Route "%s" or method "%s" not allowed.', $this->request->getUrl()->getPath(), $this->request->getMethod()); + return $this->handleException(new NotFoundHttpException($message, 403)); + } + + if (count($this->request->getLoadedRoutes()) === 0) { + + $rewriteUrl = $this->request->getRewriteUrl(); + + if ($rewriteUrl !== null) { + $message = sprintf('Route not found: "%s" (rewrite from: "%s")', $rewriteUrl, $this->request->getUrl()->getPath()); + } else { + $message = sprintf('Route not found: "%s"', $this->request->getUrl()->getPath()); + } + + $this->debug($message); + + return $this->handleException(new NotFoundHttpException($message, 404)); + } + + return null; + } + + /** + * Handle route-rewrite + * + * @param string $key + * @param string $url + * @return string|null + * @throws HttpException + * @throws Exception + */ + protected function handleRouteRewrite(string $key, string $url): ?string + { + /* If the request has changed */ + if ($this->request->hasPendingRewrite() === false) { + return null; + } + + $route = $this->request->getRewriteRoute(); + + if ($route !== null) { + /* Add rewrite route */ + $this->processedRoutes[] = $route; + } + + if ($this->request->getRewriteUrl() !== $url) { + + unset($this->processedRoutes[$key]); + + $this->request->setHasPendingRewrite(false); + + $this->fireEvents(EventHandler::EVENT_REWRITE, [ + 'rewriteUrl' => $this->request->getRewriteUrl(), + 'rewriteRoute' => $this->request->getRewriteRoute(), + ]); + + return $this->routeRequest(); + } + + return null; + } + + /** + * @param Exception $e + * @return string|null + * @throws Exception + * @throws HttpException + */ + protected function handleException(Exception $e): ?string + { + $this->debug('Starting exception handling for "%s"', get_class($e)); + + $this->fireEvents(EventHandler::EVENT_LOAD_EXCEPTIONS, [ + 'exception' => $e, + 'exceptionHandlers' => $this->exceptionHandlers, + ]); + + /* @var $handler IExceptionHandler */ + foreach (array_reverse($this->exceptionHandlers) as $key => $handler) { + + if (is_object($handler) === false) { + $handler = new $handler(); + } + + $this->fireEvents(EventHandler::EVENT_RENDER_EXCEPTION, [ + 'exception' => $e, + 'exceptionHandler' => $handler, + 'exceptionHandlers' => $this->exceptionHandlers, + ]); + + $this->debug('Processing exception-handler "%s"', get_class($handler)); + + if (($handler instanceof IExceptionHandler) === false) { + throw new HttpException('Exception handler must implement the IExceptionHandler interface.', 500); + } + + try { + $this->debug('Start rendering exception handler'); + $handler->handleError($this->request, $e); + $this->debug('Finished rendering exception-handler'); + + if (isset($this->loadedExceptionHandlers[$key]) === false && $this->request->hasPendingRewrite() === true) { + + $this->loadedExceptionHandlers[$key] = $handler; + + $this->debug('Exception handler contains rewrite, reloading routes'); + + $this->fireEvents(EventHandler::EVENT_REWRITE, [ + 'rewriteUrl' => $this->request->getRewriteUrl(), + 'rewriteRoute' => $this->request->getRewriteRoute(), + ]); + + if ($this->request->getRewriteRoute() !== null) { + $this->processedRoutes[] = $this->request->getRewriteRoute(); + } + + return $this->routeRequest(); + } + + } catch (Exception $e) { + + } + + $this->debug('Finished processing'); + } + + $this->debug('Finished exception handling - exception not handled, throwing'); + throw $e; + } + + /** + * Find route by alias, class, callback or method. + * + * @param string $name + * @return ILoadableRoute|null + */ + public function findRoute(string $name): ?ILoadableRoute + { + $this->debug('Finding route by name "%s"', $name); + + $this->fireEvents(EventHandler::EVENT_FIND_ROUTE, [ + 'name' => $name, + ]); + + foreach ($this->processedRoutes as $route) { + + /* Check if the name matches with a name on the route. Should match either router alias or controller alias. */ + if ($route->hasName($name) === true) { + $this->debug('Found route "%s" by name "%s"', $route->getUrl(), $name); + + return $route; + } + + /* Direct match to controller */ + if ($route instanceof IControllerRoute && strtoupper($route->getController()) === strtoupper($name)) { + $this->debug('Found route "%s" by controller "%s"', $route->getUrl(), $name); + + return $route; + } + + /* Using @ is most definitely a controller@method or alias@method */ + if (strpos($name, '@') !== false) { + [$controller, $method] = array_map('strtolower', explode('@', $name)); + + if ($controller === strtolower((string)$route->getClass()) && $method === strtolower((string)$route->getMethod())) { + $this->debug('Found route "%s" by controller "%s" and method "%s"', $route->getUrl(), $controller, $method); + + return $route; + } + } + + /* Check if callback matches (if it's not a function) */ + $callback = $route->getCallback(); + if (is_string($callback) === true && is_callable($callback) === false && strpos($name, '@') !== false && strpos($callback, '@') !== false) { + + /* Check if the entire callback is matching */ + if (strpos($callback, $name) === 0 || strtolower($callback) === strtolower($name)) { + $this->debug('Found route "%s" by callback "%s"', $route->getUrl(), $name); + + return $route; + } + + /* Check if the class part of the callback matches (class@method) */ + if (strtolower($name) === strtolower($route->getClass())) { + $this->debug('Found route "%s" by class "%s"', $route->getUrl(), $name); + + return $route; + } + } + } + + $this->debug('Route not found'); + + return null; + } + + /** + * Get url for a route by using either name/alias, class or method name. + * + * The name parameter supports the following values: + * - Route name + * - Controller/resource name (with or without method) + * - Controller class name + * + * When searching for controller/resource by name, you can use this syntax "route.name@method". + * You can also use the same syntax when searching for a specific controller-class "MyController@home". + * If no arguments is specified, it will return the url for the current loaded route. + * + * @param string|null $name + * @param string|array|null $parameters + * @param array|null $getParams + * @return Url + * @throws InvalidArgumentException + */ + public function getUrl(?string $name = null, $parameters = null, ?array $getParams = null): Url + { + $this->debug('Finding url', func_get_args()); + + $this->fireEvents(EventHandler::EVENT_GET_URL, [ + 'name' => $name, + 'parameters' => $parameters, + 'getParams' => $getParams, + ]); + + if ($name === '' && $parameters === '') { + return new Url('/'); + } + + /* Only merge $_GET when all parameters are null */ + $getParams = ($name === null && $parameters === null && $getParams === null) ? $_GET : (array)$getParams; + + /* Return current route if no options has been specified */ + if ($name === null && $parameters === null) { + return $this->request + ->getUrlCopy() + ->setParams($getParams); + } + + $loadedRoute = $this->request->getLoadedRoute(); + + /* If nothing is defined and a route is loaded we use that */ + if ($name === null && $loadedRoute !== null) { + return $this->request->getUrlCopy()->parse($loadedRoute->findUrl($loadedRoute->getMethod(), $parameters, $name))->setParams($getParams); + } + + if ($name !== null) { + /* We try to find a match on the given name */ + $route = $this->findRoute($name); + + if ($route !== null) { + return $this->request->getUrlCopy()->parse($route->findUrl($route->getMethod(), $parameters, $name))->setParams($getParams); + } + } + + /* Using @ is most definitely a controller@method or alias@method */ + if (is_string($name) === true && strpos($name, '@') !== false) { + [$controller, $method] = explode('@', $name); + + /* Loop through all the routes to see if we can find a match */ + + /* @var $route ILoadableRoute */ + foreach ($this->processedRoutes as $processedRoute) { + + /* Check if the route contains the name/alias */ + if ($processedRoute->hasName($controller) === true) { + return $this->request->getUrlCopy()->parse($processedRoute->findUrl($method, $parameters, $name))->setParams($getParams); + } + + /* Check if the route controller is equal to the name */ + if ($processedRoute instanceof IControllerRoute && strtolower($processedRoute->getController()) === strtolower($controller)) { + return $this->request->getUrlCopy()->parse($processedRoute->findUrl($method, $parameters, $name))->setParams($getParams); + } + + } + } + + /* No result so we assume that someone is using a hardcoded url and join everything together. */ + $url = trim(implode('/', array_merge((array)$name, (array)$parameters)), '/'); + $url = (($url === '') ? '/' : '/' . $url . '/'); + + return $this->request->getUrlCopy()->parse($url)->setParams($getParams); + } + + /** + * Get BootManagers + * @return array + */ + public function getBootManagers(): array + { + return $this->bootManagers; + } + + /** + * Set BootManagers + * + * @param array $bootManagers + * @return static + */ + public function setBootManagers(array $bootManagers): self + { + $this->bootManagers = $bootManagers; + + return $this; + } + + /** + * Add BootManager + * + * @param IRouterBootManager $bootManager + * @return static + */ + public function addBootManager(IRouterBootManager $bootManager): self + { + $this->bootManagers[] = $bootManager; + + return $this; + } + + /** + * Get routes that has been processed. + * + * @return array + */ + public function getProcessedRoutes(): array + { + return $this->processedRoutes; + } + + /** + * @return array + */ + public function getRoutes(): array + { + return $this->routes; + } + + /** + * Set routes + * + * @param array $routes + * @return static + */ + public function setRoutes(array $routes): self + { + $this->routes = $routes; + + return $this; + } + + /** + * Get current request + * + * @return Request + */ + public function getRequest(): Request + { + return $this->request; + } + + /** + * Get csrf verifier class + * @return BaseCsrfVerifier + */ + public function getCsrfVerifier(): ?BaseCsrfVerifier + { + return $this->csrfVerifier; + } + + /** + * Set csrf verifier class + * + * @param BaseCsrfVerifier $csrfVerifier + */ + public function setCsrfVerifier(BaseCsrfVerifier $csrfVerifier): void + { + $this->csrfVerifier = $csrfVerifier; + } + + /** + * Set class loader + * + * @param IClassLoader $loader + */ + public function setClassLoader(IClassLoader $loader): void + { + $this->classLoader = $loader; + } + + /** + * Get class loader + * + * @return IClassLoader + */ + public function getClassLoader(): IClassLoader + { + return $this->classLoader; + } + + /** + * Register event handler + * + * @param IEventHandler $handler + */ + public function addEventHandler(IEventHandler $handler): void + { + $this->eventHandlers[] = $handler; + } + + /** + * Get registered event-handler. + * + * @return array + */ + public function getEventHandlers(): array + { + return $this->eventHandlers; + } + + /** + * Fire event in event-handler. + * + * @param string $name + * @param array $arguments + */ + protected function fireEvents(string $name, array $arguments = []): void + { + if (count($this->eventHandlers) === 0) { + return; + } + + /* @var IEventHandler $eventHandler */ + foreach ($this->eventHandlers as $eventHandler) { + $eventHandler->fireEvents($this, $name, $arguments); + } + } + + /** + * Add new debug message + * @param string $message + * @param array $args + */ + public function debug(string $message, ...$args): void + { + if ($this->debugEnabled === false) { + return; + } + + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); + $this->debugList[] = [ + 'message' => vsprintf($message, $args), + 'time' => number_format(microtime(true) - $this->debugStartTime, 10), + 'trace' => end($trace), + ]; + } + + /** + * Enable or disables debugging + * + * @param bool $enabled + * @return static + */ + public function setDebugEnabled(bool $enabled): self + { + $this->debugEnabled = $enabled; + + return $this; + } + + /** + * Get the list containing all debug messages. + * + * @return array + */ + public function getDebugLog(): array + { + return $this->debugList; + } + + /** + * Get the current processing route details. + * + * @return ILoadableRoute + */ + public function getCurrentProcessingRoute(): ILoadableRoute + { + return $this->currentProcessingRoute; + } + + /** + * Changes the rendering behavior of the router. + * When enabled the router will render all routes that matches. + * When disabled the router will stop rendering at the first route that matches. + * + * @param bool $bool + * @return $this + */ + public function setRenderMultipleRoutes(bool $bool): self + { + $this->renderMultipleRoutes = $bool; + + return $this; + } + + public function addExceptionHandler(IExceptionHandler $handler): self + { + $this->exceptionHandlers[] = $handler; + + return $this; + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/SimpleRouter.php b/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/SimpleRouter.php new file mode 100644 index 0000000..2117cf6 --- /dev/null +++ b/exam/vendor/pecee/simple-router/src/Pecee/SimpleRouter/SimpleRouter.php @@ -0,0 +1,537 @@ +getRoutes() as $route) { + static::addDefaultNamespace($route); + } + + echo static::router()->start(); + } + + /** + * Start the routing an return array with debugging-information + * + * @return array + */ + public static function startDebug(): array + { + $routerOutput = null; + + try { + ob_start(); + static::router()->setDebugEnabled(true)->start(); + $routerOutput = ob_get_clean(); + } catch (Exception $e) { + + } + + // Try to parse library version + $composerFile = dirname(__DIR__, 3) . '/composer.lock'; + $version = false; + + if (is_file($composerFile) === true) { + $composerInfo = json_decode(file_get_contents($composerFile), true); + + if (isset($composerInfo['packages']) === true && is_array($composerInfo['packages']) === true) { + foreach ($composerInfo['packages'] as $package) { + if (isset($package['name']) === true && strtolower($package['name']) === 'pecee/simple-router') { + $version = $package['version']; + break; + } + } + } + } + + $request = static::request(); + $router = static::router(); + + return [ + 'url' => $request->getUrl(), + 'method' => $request->getMethod(), + 'host' => $request->getHost(), + 'loaded_routes' => $request->getLoadedRoutes(), + 'all_routes' => $router->getRoutes(), + 'boot_managers' => $router->getBootManagers(), + 'csrf_verifier' => $router->getCsrfVerifier(), + 'log' => $router->getDebugLog(), + 'event_handlers' => $router->getEventHandlers(), + 'router_output' => $routerOutput, + 'library_version' => $version, + 'php_version' => PHP_VERSION, + 'server_params' => $request->getHeaders(), + ]; + } + + /** + * Set default namespace which will be prepended to all routes. + * + * @param string $defaultNamespace + */ + public static function setDefaultNamespace(string $defaultNamespace): void + { + static::$defaultNamespace = $defaultNamespace; + } + + /** + * Base CSRF verifier + * + * @param BaseCsrfVerifier $baseCsrfVerifier + */ + public static function csrfVerifier(BaseCsrfVerifier $baseCsrfVerifier): void + { + static::router()->setCsrfVerifier($baseCsrfVerifier); + } + + /** + * Add new event handler to the router + * + * @param IEventHandler $eventHandler + */ + public static function addEventHandler(IEventHandler $eventHandler): void + { + static::router()->addEventHandler($eventHandler); + } + + /** + * Boot managers allows you to alter the routes before the routing occurs. + * Perfect if you want to load pretty-urls from a file or database. + * + * @param IRouterBootManager $bootManager + */ + public static function addBootManager(IRouterBootManager $bootManager): void + { + static::router()->addBootManager($bootManager); + } + + /** + * Redirect to when route matches. + * + * @param string $where + * @param string $to + * @param int $httpCode + * @return IRoute + */ + public static function redirect(string $where, string $to, int $httpCode = 301): IRoute + { + return static::get($where, static function () use ($to, $httpCode): void { + static::response()->redirect($to, $httpCode); + }); + } + + /** + * Route the given url to your callback on GET request method. + * + * @param string $url + * @param string|array|Closure $callback + * @param array|null $settings + * + * @return RouteUrl|IRoute + */ + public static function get(string $url, $callback, array $settings = null): IRoute + { + return static::match([Request::REQUEST_TYPE_GET], $url, $callback, $settings); + } + + /** + * Route the given url to your callback on POST request method. + * + * @param string $url + * @param string|array|Closure $callback + * @param array|null $settings + * @return RouteUrl|IRoute + */ + public static function post(string $url, $callback, array $settings = null): IRoute + { + return static::match([Request::REQUEST_TYPE_POST], $url, $callback, $settings); + } + + /** + * Route the given url to your callback on PUT request method. + * + * @param string $url + * @param string|array|Closure $callback + * @param array|null $settings + * @return RouteUrl|IRoute + */ + public static function put(string $url, $callback, array $settings = null): IRoute + { + return static::match([Request::REQUEST_TYPE_PUT], $url, $callback, $settings); + } + + /** + * Route the given url to your callback on PATCH request method. + * + * @param string $url + * @param string|array|Closure $callback + * @param array|null $settings + * @return RouteUrl|IRoute + */ + public static function patch(string $url, $callback, array $settings = null): IRoute + { + return static::match([Request::REQUEST_TYPE_PATCH], $url, $callback, $settings); + } + + /** + * Route the given url to your callback on OPTIONS request method. + * + * @param string $url + * @param string|array|Closure $callback + * @param array|null $settings + * @return RouteUrl|IRoute + */ + public static function options(string $url, $callback, array $settings = null): IRoute + { + return static::match([Request::REQUEST_TYPE_OPTIONS], $url, $callback, $settings); + } + + /** + * Route the given url to your callback on DELETE request method. + * + * @param string $url + * @param string|array|Closure $callback + * @param array|null $settings + * @return RouteUrl|IRoute + */ + public static function delete(string $url, $callback, array $settings = null): IRoute + { + return static::match([Request::REQUEST_TYPE_DELETE], $url, $callback, $settings); + } + + /** + * Groups allows for encapsulating routes with special settings. + * + * @param array $settings + * @param Closure $callback + * @return RouteGroup|IGroupRoute + * @throws InvalidArgumentException + */ + public static function group(array $settings, Closure $callback): IGroupRoute + { + $group = new RouteGroup(); + $group->setCallback($callback); + $group->setSettings($settings); + + static::router()->addRoute($group); + + return $group; + } + + /** + * Special group that has the same benefits as group but supports + * parameters and which are only rendered when the url matches. + * + * @param string $url + * @param Closure $callback + * @param array $settings + * @return RoutePartialGroup|IPartialGroupRoute + * @throws InvalidArgumentException + */ + public static function partialGroup(string $url, Closure $callback, array $settings = []): IPartialGroupRoute + { + $settings['prefix'] = $url; + + $group = new RoutePartialGroup(); + $group->setSettings($settings); + $group->setCallback($callback); + + static::router()->addRoute($group); + + return $group; + } + + /** + * Alias for the form method + * + * @param string $url + * @param string|array|Closure $callback + * @param array|null $settings + * @return RouteUrl|IRoute + * @see SimpleRouter::form + */ + public static function basic(string $url, $callback, array $settings = null): IRoute + { + return static::form($url, $callback, $settings); + } + + /** + * This type will route the given url to your callback on the provided request methods. + * Route the given url to your callback on POST and GET request method. + * + * @param string $url + * @param string|array|Closure $callback + * @param array|null $settings + * @return RouteUrl|IRoute + * @see SimpleRouter::form + */ + public static function form(string $url, $callback, array $settings = null): IRoute + { + return static::match([ + Request::REQUEST_TYPE_GET, + Request::REQUEST_TYPE_POST, + ], $url, $callback, $settings); + } + + /** + * This type will route the given url to your callback on the provided request methods. + * + * @param array $requestMethods + * @param string $url + * @param string|array|Closure $callback + * @param array|null $settings + * @return RouteUrl|IRoute + */ + public static function match(array $requestMethods, string $url, $callback, array $settings = null): IRoute + { + $route = new RouteUrl($url, $callback); + $route->setRequestMethods($requestMethods); + + if ($settings !== null) { + $route->setSettings($settings); + } + + return static::router()->addRoute($route); + } + + /** + * This type will route the given url to your callback and allow any type of request method + * + * @param string $url + * @param string|array|Closure $callback + * @param array|null $settings + * @return RouteUrl|IRoute + */ + public static function all(string $url, $callback, array $settings = null): IRoute + { + $route = new RouteUrl($url, $callback); + + if ($settings !== null) { + $route->setSettings($settings); + } + + return static::router()->addRoute($route); + } + + /** + * This route will route request from the given url to the controller. + * + * @param string $url + * @param string $controller + * @param array|null $settings + * @return RouteController|IRoute + */ + public static function controller(string $url, string $controller, array $settings = null): IRoute + { + $route = new RouteController($url, $controller); + + if ($settings !== null) { + $route->setSettings($settings); + } + + return static::router()->addRoute($route); + } + + /** + * This type will route all REST-supported requests to different methods in the provided controller. + * + * @param string $url + * @param string $controller + * @param array|null $settings + * @return RouteResource|IRoute + */ + public static function resource(string $url, string $controller, array $settings = null): IRoute + { + $route = new RouteResource($url, $controller); + + if ($settings !== null) { + $route->setSettings($settings); + } + + return static::router()->addRoute($route); + } + + /** + * Add exception callback handler. + * + * @param Closure $callback + * @return CallbackExceptionHandler $callbackHandler + */ + public static function error(Closure $callback): CallbackExceptionHandler + { + $callbackHandler = new CallbackExceptionHandler($callback); + + static::router()->addExceptionHandler($callbackHandler); + + return $callbackHandler; + } + + /** + * Get url for a route by using either name/alias, class or method name. + * + * The name parameter supports the following values: + * - Route name + * - Controller/resource name (with or without method) + * - Controller class name + * + * When searching for controller/resource by name, you can use this syntax "route.name@method". + * You can also use the same syntax when searching for a specific controller-class "MyController@home". + * If no arguments is specified, it will return the url for the current loaded route. + * + * @param string|null $name + * @param string|array|null $parameters + * @param array|null $getParams + * @return Url + */ + public static function getUrl(?string $name = null, $parameters = null, ?array $getParams = null): Url + { + try { + return static::router()->getUrl($name, $parameters, $getParams); + } catch (Exception $e) { + return new Url('/'); + } + } + + /** + * Get the request + * + * @return Request + */ + public static function request(): Request + { + return static::router()->getRequest(); + } + + /** + * Get the response object + * + * @return Response + */ + public static function response(): Response + { + if (static::$response === null) { + static::$response = new Response(static::request()); + } + + return static::$response; + } + + /** + * Returns the router instance + * + * @return Router + */ + public static function router(): Router + { + if (static::$router === null) { + static::$router = new Router(); + } + + return static::$router; + } + + /** + * Prepends the default namespace to all new routes added. + * + * @param ILoadableRoute|IRoute $route + * @return IRoute|ILoadableRoute + */ + public static function addDefaultNamespace(IRoute $route): IRoute + { + if (static::$defaultNamespace !== null) { + $route->setNamespace(static::$defaultNamespace); + } + + return $route; + } + + /** + * Changes the rendering behavior of the router. + * When enabled the router will render all routes that matches. + * When disabled the router will stop rendering at the first route that matches. + * + * @param bool $bool + */ + public static function enableMultiRouteRendering(bool $bool): void + { + static::router()->setRenderMultipleRoutes($bool); + } + + /** + * Set custom class-loader class used. + * @param IClassLoader $classLoader + */ + public static function setCustomClassLoader(IClassLoader $classLoader): void + { + static::router()->setClassLoader($classLoader); + } + + /** + * Get default namespace + * @return string|null + */ + public static function getDefaultNamespace(): ?string + { + return static::$defaultNamespace; + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/BootManagerTest.php b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/BootManagerTest.php new file mode 100644 index 0000000..925ea97 --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/BootManagerTest.php @@ -0,0 +1,49 @@ + '/about', + '/contact' => '/', + ])); + + TestRouter::debug('/contact'); + + $this->assertTrue($result); + } + + public function testFindUrlFromBootManager() + { + TestRouter::get('/', 'DummyController@method1'); + TestRouter::get('/about', 'DummyController@method2')->name('about'); + TestRouter::get('/contact', 'DummyController@method3')->name('contact'); + + $result = false; + + // Add boot-manager + TestRouter::addBootManager(new FindUrlBootManager($result)); + + TestRouter::debug('/'); + + $this->assertTrue($result); + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/ClassLoaderTest.php b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/ClassLoaderTest.php new file mode 100644 index 0000000..79f6095 --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/ClassLoaderTest.php @@ -0,0 +1,30 @@ +assertEquals('method3', $classLoaderClass); + $this->assertTrue($result); + + TestRouter::router()->reset(); + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/CsrfVerifierTest.php b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/CsrfVerifierTest.php new file mode 100644 index 0000000..4d05b16 --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/CsrfVerifierTest.php @@ -0,0 +1,66 @@ +getToken(); + + TestRouter::router()->reset(); + + $router = TestRouter::router(); + $router->getRequest()->setMethod(\Pecee\Http\Request::REQUEST_TYPE_POST); + $router->getRequest()->setUrl(new \Pecee\Http\Url('/page')); + $csrf = new DummyCsrfVerifier(); + $csrf->setTokenProvider($tokenProvider); + + $csrf->handle($router->getRequest()); + + // If handle doesn't throw exception, the test has passed + $this->assertTrue(true); + } + + public function testTokenFail() + { + $this->expectException(\Pecee\Http\Middleware\Exceptions\TokenMismatchException::class); + + global $_POST; + + $tokenProvider = new SilentTokenProvider(); + + $router = TestRouter::router(); + $router->getRequest()->setMethod(\Pecee\Http\Request::REQUEST_TYPE_POST); + $router->getRequest()->setUrl(new \Pecee\Http\Url('/page')); + $csrf = new DummyCsrfVerifier(); + $csrf->setTokenProvider($tokenProvider); + + $csrf->handle($router->getRequest()); + } + + public function testExcludeInclude() + { + $router = TestRouter::router(); + $csrf = new DummyCsrfVerifier(); + $request = $router->getRequest(); + + $request->setUrl(new \Pecee\Http\Url('/exclude-page')); + $this->assertTrue($csrf->testSkip($router->getRequest())); + + $request->setUrl(new \Pecee\Http\Url('/exclude-all/page')); + $this->assertTrue($csrf->testSkip($router->getRequest())); + + $request->setUrl(new \Pecee\Http\Url('/exclude-all/include-page')); + $this->assertFalse($csrf->testSkip($router->getRequest())); + + $request->setUrl(new \Pecee\Http\Url('/include-page')); + $this->assertFalse($csrf->testSkip($router->getRequest())); + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/CustomMiddlewareTest.php b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/CustomMiddlewareTest.php new file mode 100644 index 0000000..7345986 --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/CustomMiddlewareTest.php @@ -0,0 +1,71 @@ +expectException(\Pecee\SimpleRouter\Exceptions\HttpException::class); + + global $_SERVER; + + // Test exact ip + + $_SERVER['remote-addr'] = '5.5.5.5'; + + TestRouter::group(['middleware' => IpRestrictMiddleware::class], function() { + TestRouter::get('/fail', 'DummyController@method1'); + }); + + TestRouter::debug('/fail'); + + // Test ip-range + + $_SERVER['remote-addr'] = '8.8.4.4'; + + TestRouter::router()->reset(); + + TestRouter::group(['middleware' => IpRestrictMiddleware::class], function() { + TestRouter::get('/fail', 'DummyController@method1'); + }); + + TestRouter::debug('/fail'); + + } + + public function testIpSuccess() { + + global $_SERVER; + + // Test ip that is not blocked + + $_SERVER['remote-addr'] = '6.6.6.6'; + + TestRouter::router()->reset(); + + TestRouter::group(['middleware' => IpRestrictMiddleware::class], function() { + TestRouter::get('/success', 'DummyController@method1'); + }); + + TestRouter::debug('/success'); + + // Test ip in whitelist + + $_SERVER['remote-addr'] = '8.8.2.2'; + + TestRouter::router()->reset(); + + TestRouter::group(['middleware' => IpRestrictMiddleware::class], function() { + TestRouter::get('/success', 'DummyController@method1'); + }); + + TestRouter::debug('/success'); + + $this->assertTrue(true); + + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/ClassLoader/CustomClassLoader.php b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/ClassLoader/CustomClassLoader.php new file mode 100644 index 0000000..90ccc91 --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/ClassLoader/CustomClassLoader.php @@ -0,0 +1,26 @@ +skip($request); + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/DummyController.php b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/DummyController.php new file mode 100644 index 0000000..3b25e5d --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/DummyController.php @@ -0,0 +1,46 @@ +response = $response; + parent::__construct('', 0); + } + + public function getResponse() + { + return $this->response; + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/Handler/ExceptionHandler.php b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/Handler/ExceptionHandler.php new file mode 100644 index 0000000..90dfc1a --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/Handler/ExceptionHandler.php @@ -0,0 +1,10 @@ +getMessage(); + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/Handler/ExceptionHandlerFirst.php b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/Handler/ExceptionHandlerFirst.php new file mode 100644 index 0000000..ca72071 --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/Handler/ExceptionHandlerFirst.php @@ -0,0 +1,13 @@ +setUrl(new \Pecee\Http\Url('/')); + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/Handler/ExceptionHandlerSecond.php b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/Handler/ExceptionHandlerSecond.php new file mode 100644 index 0000000..274bd3c --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/Handler/ExceptionHandlerSecond.php @@ -0,0 +1,13 @@ +setUrl(new \Pecee\Http\Url('/')); + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/Handler/ExceptionHandlerThird.php b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/Handler/ExceptionHandlerThird.php new file mode 100644 index 0000000..793746a --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/Handler/ExceptionHandlerThird.php @@ -0,0 +1,13 @@ +result = &$result; + } + + /** + * Called when router loads it's routes + * + * @param \Pecee\SimpleRouter\Router $router + * @param \Pecee\Http\Request $request + */ + public function boot(\Pecee\SimpleRouter\Router $router, \Pecee\Http\Request $request): void + { + $contact = $router->findRoute('contact'); + + if($contact !== null) { + $this->result = true; + } + } +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/Managers/TestBootManager.php b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/Managers/TestBootManager.php new file mode 100644 index 0000000..1617b46 --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/Managers/TestBootManager.php @@ -0,0 +1,30 @@ +rewrite = $rewrite; + } + + /** + * Called when router loads it's routes + * + * @param \Pecee\SimpleRouter\Router $router + * @param \Pecee\Http\Request $request + */ + public function boot(\Pecee\SimpleRouter\Router $router, \Pecee\Http\Request $request): void + { + foreach ($this->rewrite as $url => $rewrite) { + // If the current url matches the rewrite url, we use our custom route + + if ($request->getUrl()->contains($url) === true) { + $request->setRewriteUrl($rewrite); + } + + } + } +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/Middleware/IpRestrictMiddleware.php b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/Middleware/IpRestrictMiddleware.php new file mode 100644 index 0000000..3c21e93 --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/Middleware/IpRestrictMiddleware.php @@ -0,0 +1,14 @@ +setRewriteCallback(function() { + return 'ok'; + }); + + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/NSController.php b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/NSController.php new file mode 100644 index 0000000..fe8e33e --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/Dummy/NSController.php @@ -0,0 +1,11 @@ +refresh(); + } + + /** + * Refresh existing token + */ + public function refresh(): void + { + $this->token = uniqid('', false); + } + + /** + * Validate valid CSRF token + * + * @param string $token + * @return bool + */ + public function validate(string $token): bool + { + return ($token === $this->token); + } + + /** + * Get token token + * + * @param string|null $defaultValue + * @return string|null + */ + public function getToken(?string $defaultValue = null): ?string + { + return $this->token ?? $defaultValue; + } +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/EventHandlerTest.php b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/EventHandlerTest.php new file mode 100644 index 0000000..2473740 --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/EventHandlerTest.php @@ -0,0 +1,151 @@ +register(EventHandler::EVENT_ALL, function (EventArgument $arg) use (&$events) { + $key = \array_search($arg->getEventName(), $events, true); + unset($events[$key]); + }); + + TestRouter::addEventHandler($eventHandler); + + // Add rewrite + TestRouter::error(function (\Pecee\Http\Request $request, \Exception $error) { + + // Trigger rewrite + $request->setRewriteUrl('/'); + + }); + + TestRouter::get('/', 'DummyController@method1')->name('home'); + + // Trigger findRoute + TestRouter::router()->findRoute('home'); + + // Trigger getUrl + TestRouter::router()->getUrl('home'); + + // Add csrf-verifier + $csrfVerifier = new \Pecee\Http\Middleware\BaseCsrfVerifier(); + $csrfVerifier->setTokenProvider(new SilentTokenProvider()); + TestRouter::csrfVerifier($csrfVerifier); + + // Add boot-manager + TestRouter::addBootManager(new TestBootManager([ + '/test' => '/', + ])); + + // Start router + TestRouter::debug('/non-existing'); + + $this->assertEquals($events, []); + } + + public function testAllEvent() + { + $status = false; + + $eventHandler = new EventHandler(); + $eventHandler->register(EventHandler::EVENT_ALL, function (EventArgument $arg) use (&$status) { + $status = true; + }); + + TestRouter::addEventHandler($eventHandler); + + TestRouter::get('/', 'DummyController@method1'); + TestRouter::debug('/'); + + // All event should fire for each other event + $this->assertEquals(true, $status); + } + + public function testPrefixEvent() + { + + $eventHandler = new EventHandler(); + $eventHandler->register(EventHandler::EVENT_ADD_ROUTE, function (EventArgument $arg) use (&$status) { + + if ($arg->route instanceof \Pecee\SimpleRouter\Route\LoadableRoute) { + $arg->route->prependUrl('/local-path'); + } + + }); + + TestRouter::addEventHandler($eventHandler); + + $status = false; + + TestRouter::get('/', function () use (&$status) { + $status = true; + }); + + TestRouter::debug('/local-path'); + + $this->assertTrue($status); + + } + + public function testCustomBasePath() { + + $basePath = '/basepath/'; + + $eventHandler = new EventHandler(); + $eventHandler->register(EventHandler::EVENT_ADD_ROUTE, function(EventArgument $data) use($basePath) { + + // Skip routes added by group + if($data->isSubRoute === false) { + + switch (true) { + case $data->route instanceof \Pecee\SimpleRouter\Route\ILoadableRoute: + $data->route->prependUrl($basePath); + break; + case $data->route instanceof \Pecee\SimpleRouter\Route\IGroupRoute: + $data->route->prependPrefix($basePath); + break; + + } + } + + }); + + $results = []; + + TestRouter::addEventHandler($eventHandler); + + TestRouter::get('/about', function() use(&$results) { + $results[] = 'about'; + }); + + TestRouter::group(['prefix' => '/admin'], function() use(&$results) { + TestRouter::get('/', function() use(&$results) { + $results[] = 'admin'; + }); + }); + + TestRouter::router()->setRenderMultipleRoutes(false); + TestRouter::debugNoReset('/basepath/about'); + TestRouter::debugNoReset('/basepath/admin'); + + $this->assertEquals(['about', 'admin'], $results); + + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/InputHandlerTest.php b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/InputHandlerTest.php new file mode 100644 index 0000000..be50698 --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/InputHandlerTest.php @@ -0,0 +1,287 @@ + 'Pepsi', + 1 => 'Coca Cola', + 2 => 'Harboe', + 3 => 'Mountain Dew', + ]; + + protected $day = 'monday'; + + public function testPost() + { + global $_POST; + + $_POST = [ + 'names' => $this->names, + 'day' => $this->day, + 'sodas' => $this->sodas, + ]; + + $router = TestRouter::router(); + $router->reset(); + $router->getRequest()->setMethod('post'); + + $handler = TestRouter::request()->getInputHandler(); + + $this->assertEquals($this->names, $handler->value('names')); + $this->assertEquals($this->names, $handler->all(['names'])['names']); + $this->assertEquals($this->day, $handler->value('day')); + $this->assertInstanceOf(\Pecee\Http\Input\InputItem::class, $handler->find('day')); + $this->assertInstanceOf(\Pecee\Http\Input\InputItem::class, $handler->post('day')); + $this->assertInstanceOf(\Pecee\Http\Input\InputItem::class, $handler->find('day', 'post')); + + // Check non-existing and wrong request-type + $this->assertCount(1, $handler->all(['non-existing'])); + $this->assertEmpty($handler->all(['non-existing'])['non-existing']); + $this->assertNull($handler->value('non-existing')); + $this->assertNull($handler->find('non-existing')); + $this->assertNull($handler->value('names', null, 'get')); + $this->assertNull($handler->find('names', 'get')); + $this->assertEquals($this->sodas, $handler->value('sodas')); + + $objects = $handler->find('names'); + + $this->assertInstanceOf(\Pecee\Http\Input\InputItem::class, $objects); + $this->assertCount(4, $objects); + + /* @var $object \Pecee\Http\Input\InputItem */ + foreach($objects as $i => $object) { + $this->assertInstanceOf(\Pecee\Http\Input\InputItem::class, $object); + $this->assertEquals($this->names[$i], $object->getValue()); + } + + // Reset + $_POST = []; + } + + public function testGet() + { + global $_GET; + + $_GET = [ + 'names' => $this->names, + 'day' => $this->day, + ]; + + $router = TestRouter::router(); + $router->reset(); + $router->getRequest()->setMethod('get'); + + $handler = TestRouter::request()->getInputHandler(); + + $this->assertEquals($this->names, $handler->value('names')); + $this->assertEquals($this->names, $handler->all(['names'])['names']); + $this->assertEquals($this->day, $handler->value('day')); + $this->assertInstanceOf(\Pecee\Http\Input\InputItem::class, $handler->find('day')); + $this->assertInstanceOf(\Pecee\Http\Input\InputItem::class, $handler->get('day')); + + // Check non-existing and wrong request-type + $this->assertCount(1, $handler->all(['non-existing'])); + $this->assertEmpty($handler->all(['non-existing'])['non-existing']); + $this->assertNull($handler->value('non-existing')); + $this->assertNull($handler->find('non-existing')); + $this->assertNull($handler->value('names', null, 'post')); + $this->assertNull($handler->find('names', 'post')); + + $objects = $handler->find('names'); + + $this->assertInstanceOf(\Pecee\Http\Input\InputItem::class, $objects); + $this->assertCount(4, $objects); + + /* @var $object \Pecee\Http\Input\InputItem */ + foreach($objects as $i => $object) { + $this->assertInstanceOf(\Pecee\Http\Input\InputItem::class, $object); + $this->assertEquals($this->names[$i], $object->getValue()); + } + + // Reset + $_GET = []; + } + + public function testFindInput() { + + global $_POST; + $_POST['hello'] = 'motto'; + + $router = TestRouter::router(); + $router->reset(); + $router->getRequest()->setMethod('post'); + $inputHandler = TestRouter::request()->getInputHandler(); + + $value = $inputHandler->value('hello', null, \Pecee\Http\Request::$requestTypesPost); + + $this->assertEquals($_POST['hello'], $value); + } + + public function testFile() + { + global $_FILES; + + $testFile = $this->generateFile(); + + $_FILES = [ + 'test_input' => $testFile, + ]; + + $router = TestRouter::router(); + $router->reset(); + $router->getRequest()->setMethod('post'); + $inputHandler = TestRouter::request()->getInputHandler(); + + $testFileContent = md5(uniqid('test', false)); + + $file = $inputHandler->file('test_input'); + + $this->assertInstanceOf(InputFile::class, $file); + $this->assertEquals($testFile['name'], $file->getFilename()); + $this->assertEquals($testFile['type'], $file->getType()); + $this->assertEquals($testFile['tmp_name'], $file->getTmpName()); + $this->assertEquals($testFile['error'], $file->getError()); + $this->assertEquals($testFile['size'], $file->getSize()); + $this->assertEquals(pathinfo($testFile['name'], PATHINFO_EXTENSION), $file->getExtension()); + + file_put_contents($testFile['tmp_name'], $testFileContent); + $this->assertEquals($testFileContent, $file->getContents()); + + // Cleanup + unlink($testFile['tmp_name']); + } + + public function testFilesArray() + { + global $_FILES; + + $testFiles = [ + $file = $this->generateFile(), + $file = $this->generateFile(), + $file = $this->generateFile(), + $file = $this->generateFile(), + $file = $this->generateFile(), + ]; + + $_FILES = [ + 'my_files' => $testFiles, + ]; + + $router = TestRouter::router(); + $router->reset(); + $router->getRequest()->setMethod('post'); + $inputHandler = TestRouter::request()->getInputHandler(); + + $files = $inputHandler->file('my_files'); + $this->assertCount(5, $files); + + /* @var $file InputFile */ + foreach ($files as $key => $file) { + + $testFileContent = md5(uniqid('test', false)); + + $this->assertInstanceOf(InputFile::class, $file); + $this->assertEquals($testFiles[$key]['name'], $file->getFilename()); + $this->assertEquals($testFiles[$key]['type'], $file->getType()); + $this->assertEquals($testFiles[$key]['tmp_name'], $file->getTmpName()); + $this->assertEquals($testFiles[$key]['error'], $file->getError()); + $this->assertEquals($testFiles[$key]['size'], $file->getSize()); + $this->assertEquals(pathinfo($testFiles[$key]['name'], PATHINFO_EXTENSION), $file->getExtension()); + + file_put_contents($testFiles[$key]['tmp_name'], $testFileContent); + + $this->assertEquals($testFileContent, $file->getContents()); + + // Cleanup + unlink($testFiles[$key]['tmp_name']); + } + + } + + public function testAll() + { + global $_POST; + global $_GET; + + $_POST = [ + 'names' => $this->names, + 'is_sad' => true, + ]; + + $_GET = [ + 'brands' => $this->brands, + 'is_happy' => true, + ]; + + $router = TestRouter::router(); + $router->reset(); + $router->getRequest()->setMethod('post'); + + $handler = TestRouter::request()->getInputHandler(); + + // GET + $brandsFound = $handler->all(['brands', 'nothing']); + + $this->assertArrayHasKey('brands', $brandsFound); + $this->assertArrayHasKey('nothing', $brandsFound); + $this->assertEquals($this->brands, $brandsFound['brands']); + $this->assertNull($brandsFound['nothing']); + + // POST + $namesFound = $handler->all(['names', 'nothing']); + + $this->assertArrayHasKey('names', $namesFound); + $this->assertArrayHasKey('nothing', $namesFound); + $this->assertEquals($this->names, $namesFound['names']); + $this->assertNull($namesFound['nothing']); + + // DEFAULT VALUE + $nonExisting = $handler->all([ + 'non-existing' + ]); + + $this->assertArrayHasKey('non-existing', $nonExisting); + $this->assertNull($nonExisting['non-existing']); + + // Reset + $_GET = []; + $_POST = []; + } + + protected function generateFile() + { + return [ + 'name' => uniqid('', false) . '.txt', + 'type' => 'text/plain', + 'tmp_name' => sys_get_temp_dir() . '/phpYfWUiw', + 'error' => 0, + 'size' => rand(3, 40), + ]; + } + + protected function generateFileContent() + { + return md5(uniqid('', false)); + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/LoadableRouteTest.php b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/LoadableRouteTest.php new file mode 100644 index 0000000..00300ec --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/LoadableRouteTest.php @@ -0,0 +1,27 @@ +assertEmpty($route->getParameters()); + + $route->setUrl('/'); + $this->assertEmpty($route->getParameters()); + + $expected = ['param' => null, 'optionalParam' => null]; + $route->setUrl('/{param}/{optionalParam?}'); + $this->assertEquals($expected, $route->getParameters()); + + $expected = ['otherParam' => null]; + $route->setUrl('/{otherParam}'); + $this->assertEquals($expected, $route->getParameters()); + + $expected = []; + $route->setUrl('/'); + $this->assertEquals($expected, $route->getParameters()); + } +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/MiddlewareTest.php b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/MiddlewareTest.php new file mode 100644 index 0000000..0183c59 --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/MiddlewareTest.php @@ -0,0 +1,35 @@ +expectException(MiddlewareLoadedException::class); + + TestRouter::group(['exceptionHandler' => 'ExceptionHandler'], function () { + TestRouter::get('/my/test/url', 'DummyController@method1', ['middleware' => 'DummyMiddleware']); + }); + + TestRouter::debug('/my/test/url', 'get'); + + } + + public function testNestedMiddlewareDontLoad() + { + + TestRouter::group(['exceptionHandler' => 'ExceptionHandler', 'middleware' => 'DummyMiddleware'], function () { + TestRouter::get('/middleware', 'DummyController@method1'); + }); + + TestRouter::get('/my/test/url', 'DummyController@method1'); + + TestRouter::debug('/my/test/url', 'get'); + + $this->assertTrue(true); + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RequestTest.php b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RequestTest.php new file mode 100644 index 0000000..b17256a --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RequestTest.php @@ -0,0 +1,84 @@ +reset(); + + $request = $router->getRequest(); + + $callback($request); + + // Reset everything + $_SERVER[$name] = null; + $router->reset(); + } + + public function testContentTypeParse() + { + global $_SERVER; + + // Test normal content-type + + $contentType = 'application/x-www-form-urlencoded'; + + $this->processHeader('content_type', $contentType, function(\Pecee\Http\Request $request) use($contentType) { + $this->assertEquals($contentType, $request->getContentType()); + }); + + // Test special content-type with encoding + + $contentTypeWithEncoding = 'application/x-www-form-urlencoded; charset=UTF-8'; + + $this->processHeader('content_type', $contentTypeWithEncoding, function(\Pecee\Http\Request $request) use($contentType) { + $this->assertEquals($contentType, $request->getContentType()); + }); + } + + public function testGetIp() + { + $ip = '1.1.1.1'; + $this->processHeader('remote_addr', $ip, function(\Pecee\Http\Request $request) use($ip) { + $this->assertEquals($ip, $request->getIp()); + }); + + $ip = '2.2.2.2'; + $this->processHeader('http-cf-connecting-ip', $ip, function(\Pecee\Http\Request $request) use($ip) { + $this->assertEquals($ip, $request->getIp()); + }); + + $ip = '3.3.3.3'; + $this->processHeader('http-client-ip', $ip, function(\Pecee\Http\Request $request) use($ip) { + $this->assertEquals($ip, $request->getIp()); + }); + + $ip = '4.4.4.4'; + $this->processHeader('http-x-forwarded-for', $ip, function(\Pecee\Http\Request $request) use($ip) { + $this->assertEquals($ip, $request->getIp()); + }); + + // Test safe + + $ip = '5.5.5.5'; + $this->processHeader('http-x-forwarded-for', $ip, function(\Pecee\Http\Request $request) { + $this->assertEquals(null, $request->getIp(true)); + }); + + } + + // TODO: implement more test-cases + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RouterCallbackExceptionHandlerTest.php b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RouterCallbackExceptionHandlerTest.php new file mode 100644 index 0000000..f86d121 --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RouterCallbackExceptionHandlerTest.php @@ -0,0 +1,47 @@ +expectException(ExceptionHandlerException::class); + + // Match normal route on alias + TestRouter::get('/my-new-url', 'DummyController@method2'); + TestRouter::get('/my-url', 'DummyController@method1'); + + TestRouter::error(function (\Pecee\Http\Request $request, \Exception $exception) { + throw new ExceptionHandlerException(); + }); + + TestRouter::debug('/404-url'); + } + + public function testExceptionHandlerCallback() { + + TestRouter::group(['prefix' => null], function() { + TestRouter::get('/', function() { + return 'Hello world'; + }); + + TestRouter::get('/not-found', 'DummyController@method1'); + TestRouter::error(function(\Pecee\Http\Request $request, \Exception $exception) { + + if($exception instanceof \Pecee\SimpleRouter\Exceptions\NotFoundHttpException && $exception->getCode() === 404) { + return $request->setRewriteCallback(static function() { + return 'success'; + }); + } + }); + }); + + $result = TestRouter::debugOutput('/thisdoes-not/existssss', 'get'); + $this->assertEquals('success', $result); + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RouterControllerTest.php b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RouterControllerTest.php new file mode 100644 index 0000000..fff6566 --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RouterControllerTest.php @@ -0,0 +1,41 @@ +assertEquals('getTest', $response); + + } + + public function testPost() + { + // Match normal route on alias + TestRouter::controller('/url', 'DummyController'); + + $response = TestRouter::debugOutput('/url/test', 'post'); + + $this->assertEquals('postTest', $response); + + } + + public function testPut() + { + // Match normal route on alias + TestRouter::controller('/url', 'DummyController'); + + $response = TestRouter::debugOutput('/url/test', 'put'); + + $this->assertEquals('putTest', $response); + + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RouterGroupTest.php b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RouterGroupTest.php new file mode 100644 index 0000000..d922d96 --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RouterGroupTest.php @@ -0,0 +1,120 @@ + '/group'], function () use (&$result) { + $result = true; + }); + + try { + TestRouter::debug('/', 'get'); + } catch (\Exception $e) { + + } + $this->assertTrue($result); + } + + public function testNestedGroup() + { + + TestRouter::group(['prefix' => '/api'], function () { + + TestRouter::group(['prefix' => '/v1'], function () { + TestRouter::get('/test', 'DummyController@method1'); + }); + + }); + + TestRouter::debug('/api/v1/test', 'get'); + + $this->assertTrue(true); + } + + public function testMultipleRoutes() + { + + TestRouter::group(['prefix' => '/api'], function () { + + TestRouter::group(['prefix' => '/v1'], function () { + TestRouter::get('/test', 'DummyController@method1'); + }); + + }); + + TestRouter::get('/my/match', 'DummyController@method1'); + + TestRouter::group(['prefix' => '/service'], function () { + + TestRouter::group(['prefix' => '/v1'], function () { + TestRouter::get('/no-match', 'DummyController@method1'); + }); + + }); + + TestRouter::debug('/my/match', 'get'); + + $this->assertTrue(true); + } + + public function testUrls() + { + // Test array name + TestRouter::get('/my/fancy/url/1', 'DummyController@method1', ['as' => 'fancy1']); + + // Test method name + TestRouter::get('/my/fancy/url/2', 'DummyController@method1')->setName('fancy2'); + + TestRouter::debugNoReset('/my/fancy/url/1'); + + $this->assertEquals('/my/fancy/url/1/', TestRouter::getUrl('fancy1')); + $this->assertEquals('/my/fancy/url/2/', TestRouter::getUrl('fancy2')); + + TestRouter::router()->reset(); + + } + + public function testNamespaceExtend() + { + TestRouter::group(['namespace' => '\My\Namespace'], function () use (&$result) { + + TestRouter::group(['namespace' => 'Service'], function () use (&$result) { + + TestRouter::get('/test', function () use (&$result) { + return \Pecee\SimpleRouter\SimpleRouter::router()->getRequest()->getLoadedRoute()->getNamespace(); + }); + + }); + + }); + + $namespace = TestRouter::debugOutput('/test'); + $this->assertEquals('\My\Namespace\Service', $namespace); + } + + public function testNamespaceOverwrite() + { + TestRouter::group(['namespace' => '\My\Namespace'], function () use (&$result) { + + TestRouter::group(['namespace' => '\Service'], function () use (&$result) { + + TestRouter::get('/test', function () use (&$result) { + return \Pecee\SimpleRouter\SimpleRouter::router()->getRequest()->getLoadedRoute()->getNamespace(); + }); + + }); + + }); + + $namespace = TestRouter::debugOutput('/test'); + $this->assertEquals('\Service', $namespace); + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RouterPartialGroupTest.php b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RouterPartialGroupTest.php new file mode 100644 index 0000000..df78d9f --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RouterPartialGroupTest.php @@ -0,0 +1,116 @@ +assertEquals('param1', $result1); + $this->assertEquals('param2', $result2); + } + + /** + * Fixed issue with partial routes not loading child groups. + * Reported in issue: #456 + */ + public function testPartialGroupWithGroup() { + + $lang = null; + + $route1 = '/lang/da/test/'; + $route2 = '/lang/da/auth'; + $route3 = '/lang/da/auth/test'; + + TestRouter::partialGroup( + '/lang/{test}/', + function ($lang = 'en') use($route1, $route2, $route3) { + + TestRouter::get('/test/', function () use($route1) { + return $route1; + }); + + TestRouter::group(['prefix' => '/auth/'], function () use($route2, $route3) { + + TestRouter::get('/', function() use($route2) { + return $route2; + }); + + TestRouter::get('/test', function () use($route3){ + return $route3; + }); + + }); + + } + ); + + $test1 = TestRouter::debugOutput('/lang/da/test', 'get', false); + $test2 = TestRouter::debugOutput('/lang/da/auth', 'get', false); + $test3 = TestRouter::debugOutput('/lang/da/auth/test', 'get', false); + + $this->assertEquals($test1, $route1); + $this->assertEquals($test2, $route2); + $this->assertEquals($test3, $route3); + + } + + public function testPhp8CallUserFunc() { + + TestRouter::router()->reset(); + + $result = false; + $lang = 'de'; + + TestRouter::group(['prefix' => '/lang'], function() use(&$result) { + TestRouter::get('/{lang}', function ($lang) use(&$result) { + $result = $lang; + }); + }); + + TestRouter::debug("/lang/$lang"); + + $this->assertEquals($lang, $result); + + // Test partial group + + $lang = 'de'; + $userId = 22; + + $result1 = false; + $result2 = false; + + TestRouter::partialGroup( + '/lang/{lang}/', + function ($lang) use(&$result1, &$result2) { + + $result1 = $lang; + + TestRouter::get('/user/{userId}', function ($userId) use(&$result2) { + $result2 = $userId; + }); + }); + + TestRouter::debug("/lang/$lang/user/$userId"); + + $this->assertEquals($lang, $result1); + $this->assertEquals($userId, $result2); + + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RouterResourceTest.php b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RouterResourceTest.php new file mode 100644 index 0000000..052ae91 --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RouterResourceTest.php @@ -0,0 +1,84 @@ +assertEquals('store', $response); + } + + public function testResourceCreate() + { + TestRouter::resource('/resource', 'ResourceController'); + $response = TestRouter::debugOutput('/resource/create', 'get'); + + $this->assertEquals('create', $response); + + } + + public function testResourceIndex() + { + TestRouter::resource('/resource', 'ResourceController'); + $response = TestRouter::debugOutput('/resource', 'get'); + + $this->assertEquals('index', $response); + } + + public function testResourceDestroy() + { + TestRouter::resource('/resource', 'ResourceController'); + $response = TestRouter::debugOutput('/resource/38', 'delete'); + + $this->assertEquals('destroy 38', $response); + } + + + public function testResourceEdit() + { + TestRouter::resource('/resource', 'ResourceController'); + $response = TestRouter::debugOutput('/resource/38/edit', 'get'); + + $this->assertEquals('edit 38', $response); + + } + + public function testResourceUpdate() + { + TestRouter::resource('/resource', 'ResourceController'); + $response = TestRouter::debugOutput('/resource/38', 'put'); + + $this->assertEquals('update 38', $response); + + } + + public function testResourceGet() + { + TestRouter::resource('/resource', 'ResourceController'); + $response = TestRouter::debugOutput('/resource/38', 'get'); + + $this->assertEquals('show 38', $response); + } + + public function testResourceUrls() + { + TestRouter::resource('/resource', 'ResourceController')->name('resource'); + + TestRouter::debugNoReset('/resource'); + + $this->assertEquals('/resource/3/create/', TestRouter::router()->getUrl('resource.create', ['id' => 3])); + $this->assertEquals('/resource/5/edit/', TestRouter::router()->getUrl('resource.edit', ['id' => 5])); + $this->assertEquals('/resource/6/', TestRouter::router()->getUrl('resource.update', ['id' => 6])); + $this->assertEquals('/resource/9/', TestRouter::router()->getUrl('resource.destroy', ['id' => 9])); + $this->assertEquals('/resource/12/', TestRouter::router()->getUrl('resource.delete', ['id' => 12])); + $this->assertEquals('/resource/', TestRouter::router()->getUrl('resource')); + + TestRouter::router()->reset(); + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RouterRewriteTest.php b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RouterRewriteTest.php new file mode 100644 index 0000000..a75e1d2 --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RouterRewriteTest.php @@ -0,0 +1,203 @@ + [ExceptionHandlerFirst::class, ExceptionHandlerSecond::class]], function () { + + TestRouter::group(['prefix' => '/test', 'exceptionHandler' => ExceptionHandlerThird::class], function () { + + TestRouter::get('/my-path', 'DummyController@method1'); + + }); + }); + + try { + TestRouter::debug('/test/non-existing', 'get'); + } catch (\ResponseException $e) { + + } + + $expectedStack = [ + ExceptionHandlerThird::class, + ExceptionHandlerSecond::class, + ExceptionHandlerFirst::class, + ]; + + $this->assertEquals($expectedStack, $stack); + + } + + public function testStopMergeExceptionHandlers() + { + global $stack; + $stack = []; + + TestRouter::group(['prefix' => '/', 'exceptionHandler' => ExceptionHandlerFirst::class], function () { + + TestRouter::group(['prefix' => '/admin', 'exceptionHandler' => ExceptionHandlerSecond::class, 'mergeExceptionHandlers' => false], function () { + + TestRouter::get('/my-path', 'DummyController@method1'); + + }); + }); + + try { + TestRouter::debug('/admin/my-path-test', 'get'); + } catch (\Pecee\SimpleRouter\Exceptions\NotFoundHttpException $e) { + + } + + $expectedStack = [ + ExceptionHandlerSecond::class, + ]; + + $this->assertEquals($expectedStack, $stack); + } + + public function testRewriteExceptionMessage() + { + $this->expectException(\Pecee\SimpleRouter\Exceptions\NotFoundHttpException::class); + + TestRouter::error(function (\Pecee\Http\Request $request, \Exception $error) { + + if (strtolower($request->getUrl()->getPath()) === '/my/test/') { + $request->setRewriteUrl('/another-non-existing'); + } + + }); + + TestRouter::debug('/my/test', 'get'); + } + + public function testRewriteUrlFromRoute() + { + + TestRouter::get('/old', function () { + TestRouter::request()->setRewriteUrl('/new'); + }); + + TestRouter::get('/new', function () { + echo 'ok'; + }); + + TestRouter::get('/new1', function () { + echo 'ok'; + }); + + TestRouter::get('/new2', function () { + echo 'ok'; + }); + + $output = TestRouter::debugOutput('/old'); + + $this->assertEquals('ok', $output); + + } + + public function testRewriteCallbackFromRoute() + { + + TestRouter::get('/old', function () { + TestRouter::request()->setRewriteUrl('/new'); + }); + + TestRouter::get('/new', function () { + return 'ok'; + }); + + TestRouter::get('/new1', function () { + return 'fail'; + }); + + TestRouter::get('/new/2', function () { + return 'fail'; + }); + + $output = TestRouter::debugOutput('/old'); + + TestRouter::router()->reset(); + + $this->assertEquals('ok', $output); + + } + + public function testRewriteRouteFromRoute() + { + + TestRouter::get('/match', function () { + TestRouter::request()->setRewriteRoute(new \Pecee\SimpleRouter\Route\RouteUrl('/match', function () { + return 'ok'; + })); + }); + + TestRouter::get('/old1', function () { + return 'fail'; + }); + + TestRouter::get('/old/2', function () { + return 'fail'; + }); + + TestRouter::get('/new2', function () { + return 'fail'; + }); + + $output = TestRouter::debugOutput('/match'); + + TestRouter::router()->reset(); + + $this->assertEquals('ok', $output); + + } + + public function testMiddlewareRewrite() + { + + TestRouter::group(['middleware' => 'RewriteMiddleware'], function () { + TestRouter::get('/', function () { + return 'fail'; + }); + + TestRouter::get('no/match', function () { + return 'fail'; + }); + }); + + $output = TestRouter::debugOutput('/'); + + $this->assertEquals('ok', $output); + + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RouterRouteTest.php b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RouterRouteTest.php new file mode 100644 index 0000000..18ab4d9 --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RouterRouteTest.php @@ -0,0 +1,335 @@ +assertTrue($result); + } + + public function testMultiParam() + { + $result = false; + TestRouter::get('/test-{param1}-{param2}', function ($param1, $param2) use (&$result) { + + if ($param1 === 'param1' && $param2 === 'param2') { + $result = true; + } + + }); + + TestRouter::debug('/test-param1-param2', 'get'); + + $this->assertTrue($result); + + } + + public function testNotFound() + { + $this->expectException('\Pecee\SimpleRouter\Exceptions\NotFoundHttpException'); + TestRouter::get('/non-existing-path', 'DummyController@method1'); + TestRouter::debug('/test-param1-param2', 'post'); + } + + public function testGet() + { + TestRouter::get('/my/test/url', 'DummyController@method1'); + TestRouter::debug('/my/test/url', 'get'); + + $this->assertTrue(true); + } + + public function testPost() + { + TestRouter::post('/my/test/url', 'DummyController@method1'); + TestRouter::debug('/my/test/url', 'post'); + + $this->assertTrue(true); + } + + public function testPut() + { + TestRouter::put('/my/test/url', 'DummyController@method1'); + TestRouter::debug('/my/test/url', 'put'); + + $this->assertTrue(true); + } + + public function testDelete() + { + TestRouter::delete('/my/test/url', 'DummyController@method1'); + TestRouter::debug('/my/test/url', 'delete'); + + $this->assertTrue(true); + } + + public function testMethodNotAllowed() + { + TestRouter::get('/my/test/url', 'DummyController@method1'); + + try { + TestRouter::debug('/my/test/url', 'post'); + } catch (\Exception $e) { + $this->assertEquals(403, $e->getCode()); + } + } + + public function testSimpleParam() + { + TestRouter::get('/test-{param1}', 'DummyController@param'); + $response = TestRouter::debugOutput('/test-param1', 'get'); + + $this->assertEquals('param1', $response); + } + + public function testPathParamRegex() + { + TestRouter::get('/{lang}/productscategories/{name}', 'DummyController@param', ['where' => ['lang' => '[a-z]+', 'name' => '[A-Za-z0-9-]+']]); + $response = TestRouter::debugOutput('/it/productscategories/system', 'get'); + + $this->assertEquals('it, system', $response); + } + + public function testFixedDomain() + { + $result = false; + TestRouter::request()->setHost('admin.world.com'); + + TestRouter::group(['domain' => 'admin.world.com'], function () use (&$result) { + TestRouter::get('/test', function ($subdomain = null) use (&$result) { + $result = true; + }); + }); + + TestRouter::debug('/test', 'get'); + + $this->assertTrue($result); + } + + public function testFixedNotAllowedDomain() + { + $result = false; + TestRouter::request()->setHost('other.world.com'); + + TestRouter::group(['domain' => 'admin.world.com'], function () use (&$result) { + TestRouter::get('/', function ($subdomain = null) use (&$result) { + $result = true; + }); + }); + + try { + TestRouter::debug('/', 'get'); + } catch(\Exception $e) { + + } + + $this->assertFalse($result); + } + + public function testDomainAllowedRoute() + { + $result = false; + TestRouter::request()->setHost('hello.world.com'); + + TestRouter::group(['domain' => '{subdomain}.world.com'], function () use (&$result) { + TestRouter::get('/test', function ($subdomain = null) use (&$result) { + $result = ($subdomain === 'hello'); + }); + }); + + TestRouter::debug('/test', 'get'); + + $this->assertTrue($result); + + } + + public function testDomainNotAllowedRoute() + { + TestRouter::request()->setHost('other.world.com'); + + $result = false; + + TestRouter::group(['domain' => '{subdomain}.world.com'], function () use (&$result) { + TestRouter::get('/test', function ($subdomain = null) use (&$result) { + $result = ($subdomain === 'hello'); + }); + }); + + TestRouter::debug('/test', 'get'); + + $this->assertFalse($result); + + } + + public function testFixedSubdomainDynamicDomain() + { + TestRouter::request()->setHost('other.world.com'); + + $result = false; + + TestRouter::group(['domain' => 'other.{domain}'], function () use (&$result) { + TestRouter::get('/test', function ($domain = null) use (&$result) { + + $result = true; + }); + }); + + TestRouter::debug('/test', 'get'); + + $this->assertTrue($result); + + } + + public function testFixedSubdomainDynamicDomainParameter() + { + TestRouter::request()->setHost('other.world.com'); + + $result = false; + + TestRouter::group(['domain' => 'other.{domain}'], function () use (&$result) { + TestRouter::get('/test', 'DummyController@param'); + TestRouter::get('/test/{key}', 'DummyController@param'); + }); + + $response = TestRouter::debugOutputNoReset('/test', 'get'); + + $this->assertEquals('world.com', $response); + + $response = TestRouter::debugOutput('/test/unittest', 'get'); + + $this->assertEquals('unittest, world.com', $response); + + } + + public function testWrongFixedSubdomainDynamicDomain() + { + TestRouter::request()->setHost('wrong.world.com'); + + $result = false; + + TestRouter::group(['domain' => 'other.{domain}'], function () use (&$result) { + TestRouter::get('/test', function ($domain = null) use (&$result) { + + $result = true; + }); + }); + + try { + TestRouter::debug('/test', 'get'); + } catch(\Exception $e) { + + } + + + $this->assertFalse($result); + + } + + public function testRegEx() + { + TestRouter::get('/my/{path}', 'DummyController@method1')->where(['path' => '[a-zA-Z-]+']); + TestRouter::debug('/my/custom-path', 'get'); + + $this->assertTrue(true); + } + + public function testParametersWithDashes() + { + + $defaultVariable = null; + + TestRouter::get('/my/{path}', function ($path = 'working') use (&$defaultVariable) { + $defaultVariable = $path; + }); + + TestRouter::debug('/my/hello-motto-man'); + + $this->assertEquals('hello-motto-man', $defaultVariable); + + } + + public function testParameterDefaultValue() + { + + $defaultVariable = null; + + TestRouter::get('/my/{path?}', function ($path = 'working') use (&$defaultVariable) { + $defaultVariable = $path; + }); + + TestRouter::debug('/my/'); + + $this->assertEquals('working', $defaultVariable); + + } + + public function testDefaultParameterRegex() + { + TestRouter::get('/my/{path}', 'DummyController@param', ['defaultParameterRegex' => '[\w-]+']); + $output = TestRouter::debugOutput('/my/custom-regex', 'get'); + + $this->assertEquals('custom-regex', $output); + } + + public function testDefaultParameterRegexGroup() + { + TestRouter::group(['defaultParameterRegex' => '[\w-]+'], function () { + TestRouter::get('/my/{path}', 'DummyController@param'); + }); + + $output = TestRouter::debugOutput('/my/custom-regex', 'get'); + + $this->assertEquals('custom-regex', $output); + } + + public function testClassHint() + { + TestRouter::get('/my/test/url', ['DummyController', 'method1']); + TestRouter::all('/my/test/url', ['DummyController', 'method1']); + TestRouter::match(['put', 'get', 'post'], '/my/test/url', ['DummyController', 'method1']); + + TestRouter::debug('/my/test/url', 'get'); + + $this->assertTrue(true); + } + + public function testDefaultNameSpaceOverload() + { + TestRouter::setDefaultNamespace('DefaultNamespace\\Controllers'); + TestRouter::get('/test', [\MyNamespace\NSController::class, 'method']); + + $result = TestRouter::debugOutput('/test'); + + $this->assertTrue( (bool)$result); + } + + public function testSameRoutes() + { + TestRouter::get('/recipe', 'DummyController@method1')->name('add'); + TestRouter::post('/recipe', 'DummyController@method2')->name('edit'); + + TestRouter::debugNoReset('/recipe', 'post'); + TestRouter::debug('/recipe', 'get'); + + $this->assertTrue(true); + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RouterUrlTest.php b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RouterUrlTest.php new file mode 100644 index 0000000..9ed9a55 --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/Pecee/SimpleRouter/RouterUrlTest.php @@ -0,0 +1,389 @@ +assertEquals('/page/{id?}/', TestRouter::router()->getRequest()->getLoadedRoute()->getUrl()); + + TestRouter::debugNoReset('/', 'get'); + $this->assertEquals('/', TestRouter::router()->getRequest()->getLoadedRoute()->getUrl()); + + $output = TestRouter::debugOutput('/test-output', 'get'); + $this->assertEquals('return value', $output); + + TestRouter::router()->reset(); + } + + public function testLastParameterSlash() + { + TestRouter::get('/test/{param}', function ($param) { + return $param; + })->setSettings(['includeSlash' => true]); + + // Test with ending / + $output = TestRouter::debugOutputNoReset('/test/param/'); + $this->assertEquals($output, 'param/'); + + // Test without ending / + $output = TestRouter::debugOutputNoReset('/test/param'); + $this->assertEquals($output, 'param'); + + TestRouter::router()->reset(); + } + + public function testUnicodeCharacters() + { + // Test spanish characters + TestRouter::get('/cursos/listado/{listado?}/{category?}', 'DummyController@method1', ['defaultParameterRegex' => '[\w\p{L}\s\-]+']); + TestRouter::get('/test/{param}', 'DummyController@method1', ['defaultParameterRegex' => '[\w\p{L}\s\-\í]+']); + TestRouter::debugNoReset('/cursos/listado/especialidad/cirugía local', 'get'); + + $this->assertEquals('/cursos/listado/{listado?}/{category?}/', TestRouter::router()->getRequest()->getLoadedRoute()->getUrl()); + + TestRouter::debugNoReset('/test/Dermatología'); + $parameters = TestRouter::request()->getLoadedRoute()->getParameters(); + + $this->assertEquals('Dermatología', $parameters['param']); + + // Test danish characters + TestRouter::get('/kategori/økse', 'DummyController@method1', ['defaultParameterRegex' => '[\w\ø]+']); + TestRouter::debugNoReset('/kategori/økse', 'get'); + + $this->assertEquals('/kategori/økse/', TestRouter::router()->getRequest()->getLoadedRoute()->getUrl()); + + TestRouter::router()->reset(); + } + + public function testOptionalParameters() + { + TestRouter::get('/aviso/legal', 'DummyController@method1'); + TestRouter::get('/aviso/{aviso}', 'DummyController@method1'); + TestRouter::get('/pagina/{pagina}', 'DummyController@method1'); + TestRouter::get('/{pagina?}', 'DummyController@method1'); + + TestRouter::debugNoReset('/aviso/optional', 'get'); + $this->assertEquals('/aviso/{aviso}/', TestRouter::router()->getRequest()->getLoadedRoute()->getUrl()); + + TestRouter::debugNoReset('/pagina/optional', 'get'); + $this->assertEquals('/pagina/{pagina}/', TestRouter::router()->getRequest()->getLoadedRoute()->getUrl()); + + TestRouter::debugNoReset('/optional', 'get'); + $this->assertEquals('/{pagina?}/', TestRouter::router()->getRequest()->getLoadedRoute()->getUrl()); + + TestRouter::debugNoReset('/avisolegal', 'get'); + $this->assertNotEquals('/aviso/{aviso}/', TestRouter::router()->getRequest()->getLoadedRoute()->getUrl()); + + TestRouter::debugNoReset('/avisolegal', 'get'); + $this->assertEquals('/{pagina?}/', TestRouter::router()->getRequest()->getLoadedRoute()->getUrl()); + + TestRouter::router()->reset(); + } + + public function testSimilarUrls() + { + TestRouter::reset(); + // Match normal route on alias + TestRouter::get('/url11', 'DummyController@method1'); + TestRouter::get('/url22', 'DummyController@method2'); + TestRouter::get('/url33', 'DummyController@method2')->name('match'); + + + TestRouter::debugNoReset('/url33', 'get'); + + $this->assertEquals(TestRouter::getUrl('match'), TestRouter::getUrl()); + + TestRouter::router()->reset(); + } + + public function testUrls() + { + // Match normal route on alias + TestRouter::get('/', 'DummyController@method1', ['as' => 'home']); + + TestRouter::get('/about', 'DummyController@about'); + + TestRouter::group(['prefix' => '/admin', 'as' => 'admin'], function () { + + // Match route with prefix on alias + TestRouter::get('/{id?}', 'DummyController@method2', ['as' => 'home']); + + // Match controller with prefix and alias + TestRouter::controller('/users', 'DummyController', ['as' => 'users']); + + // Match controller with prefix and NO alias + TestRouter::controller('/pages', 'DummyController'); + + }); + + TestRouter::group(['prefix' => 'api', 'as' => 'api'], function () { + + // Match resource controller + TestRouter::resource('phones', 'DummyController'); + + }); + + TestRouter::controller('gadgets', 'DummyController', ['names' => ['getIphoneInfo' => 'iphone']]); + + // Match controller with no prefix and no alias + TestRouter::controller('/cats', 'CatsController'); + + // Pretend to load page + TestRouter::debugNoReset('/', 'get'); + + $this->assertEquals('/gadgets/iphoneinfo/', TestRouter::getUrl('gadgets.iphone')); + + $this->assertEquals('/api/phones/create/', TestRouter::getUrl('api.phones.create')); + + // Should match / + $this->assertEquals('/', TestRouter::getUrl('home')); + + // Should match /about/ + $this->assertEquals('/about/', TestRouter::getUrl('DummyController@about')); + + // Should match /admin/ + $this->assertEquals('/admin/', TestRouter::getUrl('DummyController@method2')); + + // Should match /admin/ + $this->assertEquals('/admin/', TestRouter::getUrl('admin.home')); + + // Should match /admin/2/ + $this->assertEquals('/admin/2/', TestRouter::getUrl('admin.home', ['id' => 2])); + + // Should match /admin/users/ + $this->assertEquals('/admin/users/', TestRouter::getUrl('admin.users')); + + // Should match /admin/users/home/ + $this->assertEquals('/admin/users/home/', TestRouter::getUrl('admin.users@home')); + + // Should match /cats/ + $this->assertEquals('/cats/', TestRouter::getUrl('CatsController')); + + // Should match /cats/view/ + $this->assertEquals('/cats/view/', TestRouter::getUrl('CatsController', 'view')); + + // Should match /cats/view/ + //$this->assertEquals('/cats/view/', TestRouter::getUrl('CatsController', ['view'])); + + // Should match /cats/view/666 + $this->assertEquals('/cats/view/666/', TestRouter::getUrl('CatsController@getView', ['666'])); + + // Should match /funny/man/ + $this->assertEquals('/funny/man/', TestRouter::getUrl('/funny/man')); + + // Should match /?jackdaniels=true&cola=yeah + $this->assertEquals('/?jackdaniels=true&cola=yeah', TestRouter::getUrl('home', null, ['jackdaniels' => 'true', 'cola' => 'yeah'])); + + TestRouter::reset(); + + } + + public function testCustomRegex() + { + TestRouter::request()->setHost('google.com'); + + TestRouter::get('/admin/', function () { + return 'match'; + })->setMatch('/^\/admin\/?(.*)/i'); + + $output = TestRouter::debugOutput('/admin/asd/bec/123', 'get'); + $this->assertEquals('match', $output); + + TestRouter::router()->reset(); + } + + public function testCustomRegexWithParameter() + { + TestRouter::request()->setHost('google.com'); + + $results = ''; + + TestRouter::get('/tester/{param}', function ($param = null) use ($results) { + return $results = $param; + })->setMatch('/(.*)/i'); + + $output = TestRouter::debugOutput('/tester/abepik/ko'); + $this->assertEquals('/tester/abepik/ko/', $output); + } + + public function testRenderMultipleRoutesDisabled() + { + TestRouter::router()->setRenderMultipleRoutes(false); + + $result = false; + + TestRouter::get('/', function () use (&$result) { + $result = true; + }); + + TestRouter::get('/', function () use (&$result) { + $result = false; + }); + + TestRouter::debug('/'); + + $this->assertTrue($result); + } + + public function testRenderMultipleRoutesEnabled() + { + TestRouter::router()->setRenderMultipleRoutes(true); + + $result = []; + + TestRouter::get('/', function () use (&$result) { + $result[] = 'route1'; + }); + + TestRouter::get('/', function () use (&$result) { + $result[] = 'route2'; + }); + + TestRouter::debug('/'); + + $this->assertCount(2, $result); + } + + public function testDefaultNamespace() + { + TestRouter::setDefaultNamespace('\\Default\\Namespace'); + + TestRouter::get('/', 'DummyController@method1', ['as' => 'home']); + + TestRouter::group([ + 'namespace' => 'Appended\Namespace', + 'prefix' => '/horses', + ], function () { + + TestRouter::get('/', 'DummyController@method1'); + + TestRouter::group([ + 'namespace' => '\\New\\Namespace', + 'prefix' => '/race', + ], function () { + + TestRouter::get('/', 'DummyController@method1'); + + }); + }); + + // Test appended namespace + + $class = null; + + try { + TestRouter::debugNoReset('/horses/'); + } catch (\Pecee\SimpleRouter\Exceptions\ClassNotFoundHttpException $e) { + $class = $e->getClass(); + } + + $this->assertEquals('\\Default\\Namespace\\Appended\Namespace\\DummyController', $class); + + // Test overwritten namespace + + $class = null; + + try { + TestRouter::debugNoReset('/horses/race'); + } catch (\Pecee\SimpleRouter\Exceptions\ClassNotFoundHttpException $e) { + $class = $e->getClass(); + } + + $this->assertEquals('\\New\\Namespace\\DummyController', $class); + + TestRouter::router()->reset(); + } + + public function testGroupPrefix() + { + + $result = false; + + TestRouter::group(['prefix' => '/lang/{lang}'], function () use (&$result) { + + TestRouter::get('/test', function () use (&$result) { + $result = true; + }); + }); + + TestRouter::debug('/lang/da/test'); + + $this->assertTrue($result); + + // Test group prefix sub-route + + $result = null; + $expectedResult = 28; + + TestRouter::group(['prefix' => '/lang/{lang}'], function () use (&$result) { + + TestRouter::get('/horse/{horseType}', function ($horseType) use (&$result) { + $result = false; + }); + + TestRouter::get('/user/{userId}', function ($userId) use (&$result) { + $result = $userId; + }); + }); + + TestRouter::debug("/lang/da/user/$expectedResult"); + + $this->assertEquals($expectedResult, $result); + + } + + public function testPassParameter() + { + + $result = false; + $expectedLanguage = 'da'; + + TestRouter::group(['prefix' => '/lang/{lang}'], function ($language) use (&$result) { + + TestRouter::get('/test', function ($language) use (&$result) { + $result = $language; + }); + + }); + + TestRouter::debug("/lang/$expectedLanguage/test"); + + $this->assertEquals($expectedLanguage, $result); + + } + + public function testPassParameterDeep() + { + + $result = false; + $expectedLanguage = 'da'; + + TestRouter::group(['prefix' => '/lang/{lang}'], function ($language) use (&$result) { + + TestRouter::group(['prefix' => '/admin'], function ($language) use (&$result) { + TestRouter::get('/test', function ($language) use (&$result) { + $result = $language; + }); + }); + + }); + + TestRouter::debug("/lang/$expectedLanguage/admin/test"); + + $this->assertEquals($expectedLanguage, $result); + + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/TestRouter.php b/exam/vendor/pecee/simple-router/tests/TestRouter.php new file mode 100644 index 0000000..0ebbff5 --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/TestRouter.php @@ -0,0 +1,69 @@ +setHost('testhost.com'); + } + + public static function reset(): void + { + static::$router = null; + } + + public static function debugNoReset(string $testUrl, string $testMethod = 'get'): void + { + $request = static::request(); + + $request->setUrl((new \Pecee\Http\Url($testUrl))); + $request->setMethod($testMethod); + + static::start(); + } + + public static function debug(string $testUrl, string $testMethod = 'get', bool $reset = true): void + { + try { + static::debugNoReset($testUrl, $testMethod); + } catch (\Exception $e) { + static::$defaultNamespace = null; + static::router()->reset(); + throw $e; + } + + if ($reset === true) { + static::$defaultNamespace = null; + static::router()->reset(); + } + + } + + public static function debugOutput(string $testUrl, string $testMethod = 'get', bool $reset = true): string + { + $response = null; + + // Route request + ob_start(); + static::debug($testUrl, $testMethod, $reset); + $response = ob_get_clean(); + + // Return response + return $response; + } + + public static function debugOutputNoReset(string $testUrl, string $testMethod = 'get', bool $reset = true): string + { + $response = null; + + // Route request + ob_start(); + static::debugNoReset($testUrl, $testMethod, $reset); + $response = ob_get_clean(); + + // Return response + return $response; + } + +} \ No newline at end of file diff --git a/exam/vendor/pecee/simple-router/tests/bootstrap.php b/exam/vendor/pecee/simple-router/tests/bootstrap.php new file mode 100644 index 0000000..22f8cd2 --- /dev/null +++ b/exam/vendor/pecee/simple-router/tests/bootstrap.php @@ -0,0 +1,4 @@ +where(['name' => '[\w]+']); +$debugInfo = SimpleRouter::startDebug(); +echo sprintf('
%s
', var_export($debugInfo, true)); +exit; \ No newline at end of file diff --git a/exam/.htaccess b/exam2/.htaccess similarity index 100% rename from exam/.htaccess rename to exam2/.htaccess diff --git a/exam/api/docs/.htaccess b/exam2/api/docs/.htaccess similarity index 100% rename from exam/api/docs/.htaccess rename to exam2/api/docs/.htaccess diff --git a/exam/api/docs/api.yaml b/exam2/api/docs/api.yaml similarity index 100% rename from exam/api/docs/api.yaml rename to exam2/api/docs/api.yaml diff --git a/exam/api/docs/index.css b/exam2/api/docs/index.css similarity index 100% rename from exam/api/docs/index.css rename to exam2/api/docs/index.css diff --git a/exam/api/docs/index.html b/exam2/api/docs/index.html similarity index 100% rename from exam/api/docs/index.html rename to exam2/api/docs/index.html diff --git a/exam/api/index.php b/exam2/api/index.php similarity index 100% rename from exam/api/index.php rename to exam2/api/index.php diff --git a/exam/api/posts/index.php b/exam2/api/posts/index.php similarity index 100% rename from exam/api/posts/index.php rename to exam2/api/posts/index.php diff --git a/exam/api/user/index.php b/exam2/api/user/index.php similarity index 100% rename from exam/api/user/index.php rename to exam2/api/user/index.php diff --git a/exam/api/users/index.php b/exam2/api/users/index.php similarity index 100% rename from exam/api/users/index.php rename to exam2/api/users/index.php diff --git a/exam/app/.htaccess b/exam2/app/.htaccess similarity index 100% rename from exam/app/.htaccess rename to exam2/app/.htaccess diff --git a/exam/app/app.php b/exam2/app/app.php similarity index 100% rename from exam/app/app.php rename to exam2/app/app.php diff --git a/exam/config/.htaccess b/exam2/config/.htaccess similarity index 100% rename from exam/config/.htaccess rename to exam2/config/.htaccess diff --git a/exam/config/app.php b/exam2/config/app.php similarity index 100% rename from exam/config/app.php rename to exam2/config/app.php diff --git a/exam/config/database.php b/exam2/config/database.php similarity index 100% rename from exam/config/database.php rename to exam2/config/database.php diff --git a/exam/index.php b/exam2/index.php similarity index 100% rename from exam/index.php rename to exam2/index.php diff --git a/exam/pages/404.php b/exam2/pages/404.php similarity index 100% rename from exam/pages/404.php rename to exam2/pages/404.php diff --git a/exam/pages/500.php b/exam2/pages/500.php similarity index 100% rename from exam/pages/500.php rename to exam2/pages/500.php diff --git a/exam/pages/assets/index-D81sf-ye.js b/exam2/pages/assets/index-D81sf-ye.js similarity index 100% rename from exam/pages/assets/index-D81sf-ye.js rename to exam2/pages/assets/index-D81sf-ye.js diff --git a/exam/pages/assets/index-DiwrgTda.css b/exam2/pages/assets/index-DiwrgTda.css similarity index 100% rename from exam/pages/assets/index-DiwrgTda.css rename to exam2/pages/assets/index-DiwrgTda.css diff --git a/exam/pages/assets/react-CHdo91hT.svg b/exam2/pages/assets/react-CHdo91hT.svg similarity index 100% rename from exam/pages/assets/react-CHdo91hT.svg rename to exam2/pages/assets/react-CHdo91hT.svg diff --git a/exam/pages/index.html b/exam2/pages/index.html similarity index 100% rename from exam/pages/index.html rename to exam2/pages/index.html diff --git a/exam/pages/vite.svg b/exam2/pages/vite.svg similarity index 100% rename from exam/pages/vite.svg rename to exam2/pages/vite.svg diff --git a/exam/react/.eslintrc.cjs b/exam2/react/.eslintrc.cjs similarity index 100% rename from exam/react/.eslintrc.cjs rename to exam2/react/.eslintrc.cjs diff --git a/exam/react/.gitignore b/exam2/react/.gitignore similarity index 100% rename from exam/react/.gitignore rename to exam2/react/.gitignore diff --git a/exam/react/.prettierrc b/exam2/react/.prettierrc similarity index 100% rename from exam/react/.prettierrc rename to exam2/react/.prettierrc diff --git a/exam/react/README.md b/exam2/react/README.md similarity index 100% rename from exam/react/README.md rename to exam2/react/README.md diff --git a/exam/react/index.html b/exam2/react/index.html similarity index 100% rename from exam/react/index.html rename to exam2/react/index.html diff --git a/exam/react/package.json b/exam2/react/package.json similarity index 100% rename from exam/react/package.json rename to exam2/react/package.json diff --git a/exam/react/pnpm-lock.yaml b/exam2/react/pnpm-lock.yaml similarity index 100% rename from exam/react/pnpm-lock.yaml rename to exam2/react/pnpm-lock.yaml diff --git a/exam/react/public/404.php b/exam2/react/public/404.php similarity index 100% rename from exam/react/public/404.php rename to exam2/react/public/404.php diff --git a/exam/react/public/500.php b/exam2/react/public/500.php similarity index 100% rename from exam/react/public/500.php rename to exam2/react/public/500.php diff --git a/exam/react/public/vite.svg b/exam2/react/public/vite.svg similarity index 100% rename from exam/react/public/vite.svg rename to exam2/react/public/vite.svg diff --git a/exam/react/src/App.css b/exam2/react/src/App.css similarity index 100% rename from exam/react/src/App.css rename to exam2/react/src/App.css diff --git a/exam/react/src/App.tsx b/exam2/react/src/App.tsx similarity index 100% rename from exam/react/src/App.tsx rename to exam2/react/src/App.tsx diff --git a/exam/react/src/assets/react.svg b/exam2/react/src/assets/react.svg similarity index 100% rename from exam/react/src/assets/react.svg rename to exam2/react/src/assets/react.svg diff --git a/exam/react/src/index.css b/exam2/react/src/index.css similarity index 100% rename from exam/react/src/index.css rename to exam2/react/src/index.css diff --git a/exam/react/src/main.tsx b/exam2/react/src/main.tsx similarity index 100% rename from exam/react/src/main.tsx rename to exam2/react/src/main.tsx diff --git a/exam/react/src/vite-env.d.ts b/exam2/react/src/vite-env.d.ts similarity index 100% rename from exam/react/src/vite-env.d.ts rename to exam2/react/src/vite-env.d.ts diff --git a/exam/react/tsconfig.app.json b/exam2/react/tsconfig.app.json similarity index 100% rename from exam/react/tsconfig.app.json rename to exam2/react/tsconfig.app.json diff --git a/exam/react/tsconfig.json b/exam2/react/tsconfig.json similarity index 100% rename from exam/react/tsconfig.json rename to exam2/react/tsconfig.json diff --git a/exam/react/tsconfig.node.json b/exam2/react/tsconfig.node.json similarity index 100% rename from exam/react/tsconfig.node.json rename to exam2/react/tsconfig.node.json diff --git a/exam/react/vite.config.ts b/exam2/react/vite.config.ts similarity index 100% rename from exam/react/vite.config.ts rename to exam2/react/vite.config.ts diff --git a/exam/routes/.htaccess b/exam2/routes/.htaccess similarity index 100% rename from exam/routes/.htaccess rename to exam2/routes/.htaccess diff --git a/exam/routes/routes.php b/exam2/routes/routes.php similarity index 100% rename from exam/routes/routes.php rename to exam2/routes/routes.php diff --git a/exam/vendor/.htaccess b/exam2/vendor/.htaccess similarity index 100% rename from exam/vendor/.htaccess rename to exam2/vendor/.htaccess diff --git a/exam/vendor/auth/auth.php b/exam2/vendor/auth/auth.php similarity index 100% rename from exam/vendor/auth/auth.php rename to exam2/vendor/auth/auth.php diff --git a/exam/vendor/autoloader.php b/exam2/vendor/autoloader.php similarity index 100% rename from exam/vendor/autoloader.php rename to exam2/vendor/autoloader.php diff --git a/exam/vendor/config/config.php b/exam2/vendor/config/config.php similarity index 100% rename from exam/vendor/config/config.php rename to exam2/vendor/config/config.php diff --git a/exam/vendor/database/database.php b/exam2/vendor/database/database.php similarity index 100% rename from exam/vendor/database/database.php rename to exam2/vendor/database/database.php diff --git a/exam/vendor/headers/headers.php b/exam2/vendor/headers/headers.php similarity index 100% rename from exam/vendor/headers/headers.php rename to exam2/vendor/headers/headers.php diff --git a/exam/vendor/pathParams/pathParams.php b/exam2/vendor/pathParams/pathParams.php similarity index 100% rename from exam/vendor/pathParams/pathParams.php rename to exam2/vendor/pathParams/pathParams.php diff --git a/exam/vendor/response/response.php b/exam2/vendor/response/response.php similarity index 100% rename from exam/vendor/response/response.php rename to exam2/vendor/response/response.php