<?php

/**
 * CodeIgniter
 *
 * An open source application development framework for PHP
 *
 * This content is released under the MIT License (MIT)
 *
 * Copyright (c) 2014-2019 British Columbia Institute of Technology
 *
 * 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.
 *
 * @package    CodeIgniter
 * @author     CodeIgniter Dev Team
 * @copyright  2014-2019 British Columbia Institute of Technology (https://bcit.ca/)
 * @license    https://opensource.org/licenses/MIT	MIT License
 * @link       https://codeigniter.com
 * @since      Version 3.0.0
 * @filesource
 */

namespace CodeIgniter\Router;

use CodeIgniter\Autoloader\FileLocator;
use CodeIgniter\Router\Exceptions\RouterException;

/**
 * Class RouteCollection
 *
 * @todo Implement nested resource routing (See CakePHP)
 *
 * @package CodeIgniter\Router
 */
class RouteCollection implements RouteCollectionInterface
{

	/**
	 * The namespace to be added to any Controllers.
	 * Defaults to the global namespaces (\)
	 *
	 * @var string
	 */
	protected $defaultNamespace = '\\';

	/**
	 * The name of the default controller to use
	 * when no other controller is specified.
	 *
	 * Not used here. Pass-thru value for Router class.
	 *
	 * @var string
	 */
	protected $defaultController = 'Home';

	/**
	 * The name of the default method to use
	 * when no other method has been specified.
	 *
	 * Not used here. Pass-thru value for Router class.
	 *
	 * @var string
	 */
	protected $defaultMethod = 'index';

	/**
	 * The placeholder used when routing 'resources'
	 * when no other placeholder has been specified.
	 *
	 * @var string
	 */
	protected $defaultPlaceholder = 'any';

	/**
	 * Whether to convert dashes to underscores in URI.
	 *
	 * Not used here. Pass-thru value for Router class.
	 *
	 * @var boolean
	 */
	protected $translateURIDashes = false;

	/**
	 * Whether to match URI against Controllers
	 * when it doesn't match defined routes.
	 *
	 * Not used here. Pass-thru value for Router class.
	 *
	 * @var boolean
	 */
	protected $autoRoute = true;

	/**
	 * A callable that will be shown
	 * when the route cannot be matched.
	 *
	 * @var string|\Closure
	 */
	protected $override404;

	/**
	 * Defined placeholders that can be used
	 * within the
	 *
	 * @var array
	 */
	protected $placeholders = [
		'any'      => '.*',
		'segment'  => '[^/]+',
		'alphanum' => '[a-zA-Z0-9]+',
		'num'      => '[0-9]+',
		'alpha'    => '[a-zA-Z]+',
		'hash'     => '[^/]+',
	];

	/**
	 * An array of all routes and their mappings.
	 *
	 * @var array
	 */
	protected $routes = [
		'*'       => [],
		'options' => [],
		'get'     => [],
		'head'    => [],
		'post'    => [],
		'put'     => [],
		'delete'  => [],
		'trace'   => [],
		'connect' => [],
		'cli'     => [],
	];

	/**
	 * Array of routes options
	 *
	 * @var array
	 */
	protected $routesOptions = [];

	/**
	 * The current method that the script is being called by.
	 *
	 * @var string
	 */
	protected $HTTPVerb;

	/**
	 * The default list of HTTP methods (and CLI for command line usage)
	 * that is allowed if no other method is provided.
	 *
	 * @var array
	 */
	protected $defaultHTTPMethods = [
		'options',
		'get',
		'head',
		'post',
		'put',
		'delete',
		'trace',
		'connect',
		'cli',
	];

	/**
	 * The name of the current group, if any.
	 *
	 * @var string
	 */
	protected $group = null;

	/**
	 * The current subdomain.
	 *
	 * @var string
	 */
	protected $currentSubdomain = null;

	/**
	 * Stores copy of current options being
	 * applied during creation.
	 *
	 * @var null
	 */
	protected $currentOptions = null;

	/**
	 * A little performance booster.
	 *
	 * @var boolean
	 */
	protected $didDiscover = false;

	/**
	 * @var \CodeIgniter\Autoloader\FileLocator
	 */
	protected $fileLocator;

	/**
	 * @var \Config\Modules
	 */
	protected $moduleConfig;

	//--------------------------------------------------------------------

	/**
	 * Constructor
	 *
	 * @param FileLocator     $locator
	 * @param \Config\Modules $moduleConfig
	 */
	public function __construct(FileLocator $locator, $moduleConfig)
	{
		// Get HTTP verb
		$this->HTTPVerb = strtolower($_SERVER['REQUEST_METHOD'] ?? 'cli');

		$this->fileLocator = $locator;

		$this->moduleConfig = $moduleConfig;
	}

	//--------------------------------------------------------------------

	/**
	 * Registers a new constraint with the system. Constraints are used
	 * by the routes as placeholders for regular expressions to make defining
	 * the routes more human-friendly.
	 *
	 * You can pass an associative array as $placeholder, and have
	 * multiple placeholders added at once.
	 *
	 * @param string|array $placeholder
	 * @param string       $pattern
	 *
	 * @return \CodeIgniter\Router\RouteCollectionInterface
	 */
	public function addPlaceholder($placeholder, string $pattern = null): RouteCollectionInterface
	{
		if (! is_array($placeholder))
		{
			$placeholder = [$placeholder => $pattern];
		}

		$this->placeholders = array_merge($this->placeholders, $placeholder);

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Sets the default namespace to use for Controllers when no other
	 * namespace has been specified.
	 *
	 * @param $value
	 *
	 * @return \CodeIgniter\Router\RouteCollectionInterface
	 */
	public function setDefaultNamespace(string $value): RouteCollectionInterface
	{
		$this->defaultNamespace = filter_var($value, FILTER_SANITIZE_STRING);
		$this->defaultNamespace = rtrim($this->defaultNamespace, '\\') . '\\';

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Sets the default controller to use when no other controller has been
	 * specified.
	 *
	 * @param $value
	 *
	 * @return \CodeIgniter\Router\RouteCollectionInterface
	 */
	public function setDefaultController(string $value): RouteCollectionInterface
	{
		$this->defaultController = filter_var($value, FILTER_SANITIZE_STRING);

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Sets the default method to call on the controller when no other
	 * method has been set in the route.
	 *
	 * @param $value
	 *
	 * @return \CodeIgniter\Router\RouteCollectionInterface
	 */
	public function setDefaultMethod(string $value): RouteCollectionInterface
	{
		$this->defaultMethod = filter_var($value, FILTER_SANITIZE_STRING);

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Tells the system whether to convert dashes in URI strings into
	 * underscores. In some search engines, including Google, dashes
	 * create more meaning and make it easier for the search engine to
	 * find words and meaning in the URI for better SEO. But it
	 * doesn't work well with PHP method names....
	 *
	 * @param boolean $value
	 *
	 * @return \CodeIgniter\Router\RouteCollectionInterface
	 */
	public function setTranslateURIDashes(bool $value): RouteCollectionInterface
	{
		$this->translateURIDashes = $value;

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * If TRUE, the system will attempt to match the URI against
	 * Controllers by matching each segment against folders/files
	 * in APPPATH/Controllers, when a match wasn't found against
	 * defined routes.
	 *
	 * If FALSE, will stop searching and do NO automatic routing.
	 *
	 * @param boolean $value
	 *
	 * @return RouteCollectionInterface
	 */
	public function setAutoRoute(bool $value): RouteCollectionInterface
	{
		$this->autoRoute = $value;

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Sets the class/method that should be called if routing doesn't
	 * find a match. It can be either a closure or the controller/method
	 * name exactly like a route is defined: Users::index
	 *
	 * This setting is passed to the Router class and handled there.
	 *
	 * @param callable|null $callable
	 *
	 * @return RouteCollectionInterface
	 */
	public function set404Override($callable = null): RouteCollectionInterface
	{
		$this->override404 = $callable;

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Returns the 404 Override setting, which can be null, a closure
	 * or the controller/string.
	 *
	 * @return string|\Closure|null
	 */
	public function get404Override()
	{
		return $this->override404;
	}

	//--------------------------------------------------------------------

	/**
	 * Will attempt to discover any additional routes, either through
	 * the local PSR4 namespaces, or through selected Composer packages.
	 */
	protected function discoverRoutes()
	{
		if ($this->didDiscover)
		{
			return;
		}

		// We need this var in local scope
		// so route files can access it.
		$routes = $this;

		if ($this->moduleConfig->shouldDiscover('routes'))
		{
			$files = $this->fileLocator->search('Config/Routes.php');

			foreach ($files as $file)
			{
				// Don't include our main file again...
				if ($file === APPPATH . 'Config/Routes.php')
				{
					continue;
				}

				include $file;
			}
		}

		$this->didDiscover = true;
	}

	//--------------------------------------------------------------------

	/**
	 * Sets the default constraint to be used in the system. Typically
	 * for use with the 'resource' method.
	 *
	 * @param string $placeholder
	 *
	 * @return RouteCollectionInterface
	 */
	public function setDefaultConstraint(string $placeholder): RouteCollectionInterface
	{
		if (array_key_exists($placeholder, $this->placeholders))
		{
			$this->defaultPlaceholder = $placeholder;
		}

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Returns the name of the default controller. With Namespace.
	 *
	 * @return string
	 */
	public function getDefaultController(): string
	{
		return $this->defaultController;
	}

	//--------------------------------------------------------------------

	/**
	 * Returns the name of the default method to use within the controller.
	 *
	 * @return string
	 */
	public function getDefaultMethod(): string
	{
		return $this->defaultMethod;
	}

	//--------------------------------------------------------------------

	/**
	 * Returns the default namespace as set in the Routes config file.
	 *
	 * @return string
	 */
	public function getDefaultNamespace(): string
	{
		return $this->defaultNamespace;
	}

	//--------------------------------------------------------------------

	/**
	 * Returns the current value of the translateURIDashes setting.
	 *
	 * @return boolean
	 */
	public function shouldTranslateURIDashes(): bool
	{
		return $this->translateURIDashes;
	}

	//--------------------------------------------------------------------

	/**
	 * Returns the flag that tells whether to autoRoute URI against Controllers.
	 *
	 * @return boolean
	 */
	public function shouldAutoRoute(): bool
	{
		return $this->autoRoute;
	}

	//--------------------------------------------------------------------

	/**
	 * Returns the raw array of available routes.
	 *
	 * @param mixed $verb
	 *
	 * @return array
	 */
	public function getRoutes($verb = null): array
	{
		if (empty($verb))
		{
			$verb = $this->getHTTPVerb();
		}

		// Since this is the entry point for the Router,
		// take a moment to do any route discovery
		// we might need to do.
		$this->discoverRoutes();

		$routes = [];

		if (isset($this->routes[$verb]))
		{
			// Keep current verb's routes at the beginning so they're matched
			// before any of the generic, "add" routes.
			if (isset($this->routes['*']))
			{
				$extraRules = array_diff_key($this->routes['*'], $this->routes[$verb]);
				$collection = array_merge($this->routes[$verb], $extraRules);
			}
			foreach ($collection as $r)
			{
				$key          = key($r['route']);
				$routes[$key] = $r['route'][$key];
			}
		}

		return $routes;
	}

	//--------------------------------------------------------------------

	/**
	 * Returns one or all routes options
	 *
	 * @param string $from
	 *
	 * @return array
	 */
	public function getRoutesOptions(string $from = null): array
	{
		return $from ? $this->routesOptions[$from] ?? [] : $this->routesOptions;
	}

	//--------------------------------------------------------------------

	/**
	 * Returns the current HTTP Verb being used.
	 *
	 * @return string
	 */
	public function getHTTPVerb(): string
	{
		return $this->HTTPVerb;
	}

	//--------------------------------------------------------------------

	/**
	 * Sets the current HTTP verb.
	 * Used primarily for testing.
	 *
	 * @param string $verb
	 *
	 * @return $this
	 */
	public function setHTTPVerb(string $verb)
	{
		$this->HTTPVerb = $verb;

		return $this;
	}

	/**
	 * A shortcut method to add a number of routes at a single time.
	 * It does not allow any options to be set on the route, or to
	 * define the method used.
	 *
	 * @param array $routes
	 * @param array $options
	 *
	 * @return RouteCollectionInterface
	 */
	public function map(array $routes = [], array $options = null): RouteCollectionInterface
	{
		foreach ($routes as $from => $to)
		{
			$this->add($from, $to, $options);
		}

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Adds a single route to the collection.
	 *
	 * Example:
	 *      $routes->add('news', 'Posts::index');
	 *
	 * @param string       $from
	 * @param array|string $to
	 * @param array        $options
	 *
	 * @return RouteCollectionInterface
	 */
	public function add(string $from, $to, array $options = null): RouteCollectionInterface
	{
		$this->create('*', $from, $to, $options);

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Adds a temporary redirect from one route to another. Used for
	 * redirecting traffic from old, non-existing routes to the new
	 * moved routes.
	 *
	 * @param string  $from   The pattern to match against
	 * @param string  $to     Either a route name or a URI to redirect to
	 * @param integer $status The HTTP status code that should be returned with this redirect
	 *
	 * @return RouteCollection
	 */
	public function addRedirect(string $from, string $to, int $status = 302)
	{
		// Use the named route's pattern if this is a named route.
		if (array_key_exists($to, $this->routes['*']))
		{
			$to = $this->routes['*'][$to]['route'];
		}

		$this->create('*', $from, $to, ['redirect' => $status]);

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Determines if the route is a redirecting route.
	 *
	 * @param string $from
	 *
	 * @return boolean
	 */
	public function isRedirect(string $from): bool
	{
		foreach ($this->routes['*'] as $name => $route)
		{
			// Named route?
			if ($name === $from || key($route['route']) === $from)
			{
				return isset($route['redirect']) && is_numeric($route['redirect']);
			}
		}

		return false;
	}

	//--------------------------------------------------------------------

	/**
	 * Grabs the HTTP status code from a redirecting Route.
	 *
	 * @param string $from
	 *
	 * @return integer
	 */
	public function getRedirectCode(string $from): int
	{
		foreach ($this->routes['*'] as $name => $route)
		{
			// Named route?
			if ($name === $from || key($route['route']) === $from)
			{
				return $route['redirect'] ?? 0;
			}
		}

		return 0;
	}

	//--------------------------------------------------------------------
	//--------------------------------------------------------------------
	// Grouping Routes
	//--------------------------------------------------------------------

	/**
	 * Group a series of routes under a single URL segment. This is handy
	 * for grouping items into an admin area, like:
	 *
	 * Example:
	 *     // Creates route: admin/users
	 *     $route->group('admin', function() {
	 *            $route->resource('users');
	 *     });
	 *
	 * @param string $name The name to group/prefix the routes with.
	 * @param $params
	 *
	 * @return void
	 */
	public function group(string $name, ...$params)
	{
		$oldGroup   = $this->group;
		$oldOptions = $this->currentOptions;

		// To register a route, we'll set a flag so that our router
		// so it will see the group name.
		$this->group = ltrim($oldGroup . '/' . $name, '/');

		$callback = array_pop($params);

		if ($params && is_array($params[0]))
		{
			$this->currentOptions = array_shift($params);
		}

		if (is_callable($callback))
		{
			$callback($this);
		}

		$this->group          = $oldGroup;
		$this->currentOptions = $oldOptions;
	}

	//--------------------------------------------------------------------
	//--------------------------------------------------------------------
	// HTTP Verb-based routing
	//--------------------------------------------------------------------
	// Routing works here because, as the routes Config file is read in,
	// the various HTTP verb-based routes will only be added to the in-memory
	// routes if it is a call that should respond to that verb.
	//
	// The options array is typically used to pass in an 'as' or var, but may
	// be expanded in the future. See the docblock for 'add' method above for
	// current list of globally available options.

	//

	/**
	 * Creates a collections of HTTP-verb based routes for a controller.
	 *
	 * Possible Options:
	 *      'controller'    - Customize the name of the controller used in the 'to' route
	 *      'placeholder'   - The regex used by the Router. Defaults to '(:any)'
	 *
	 * Example:
	 *      $route->resource('photos');
	 *
	 *      // Generates the following routes:
	 *      HTTP Verb | Path        | Action        | Used for...
	 *      ----------+-------------+---------------+-----------------
	 *      GET         /photos             index           display a list of photos
	 *      GET         /photos/new         new             new a specific photo
	 *      GET         /photos/{id}/edit   edit            edit a specific photo
	 *      GET         /photos/{id}        show            display a specific photo
	 *      POST        /photos             create          create a new photo
	 *      PUT/PATCH   /photos/{id}        update          update an existing photo
	 *      DELETE      /photos/{id}        delete          delete an existing photo
	 *
	 *  If 'websafe' option is present, the following paths are also available:
	 *
	 *      POST        /photos/{id}        update
	 *      DELETE      /photos/{id}/delete delete
	 *
	 * @param string $name    The name of the controller to route to.
	 * @param array  $options An list of possible ways to customize the routing.
	 *
	 * @return RouteCollectionInterface
	 */
	public function resource(string $name, array $options = null): RouteCollectionInterface
	{
		// In order to allow customization of the route the
		// resources are sent to, we need to have a new name
		// to store the values in.
		$new_name = ucfirst($name);

		// If a new controller is specified, then we replace the
		// $name value with the name of the new controller.
		if (isset($options['controller']))
		{
			$new_name = ucfirst(filter_var($options['controller'], FILTER_SANITIZE_STRING));
		}

		// In order to allow customization of allowed id values
		// we need someplace to store them.
		$id = $this->placeholders[$this->defaultPlaceholder] ?? '(:segment)';

		if (isset($options['placeholder']))
		{
			$id = $options['placeholder'];
		}

		// Make sure we capture back-references
		$id = '(' . trim($id, '()') . ')';

		$methods = isset($options['only']) ? is_string($options['only']) ? explode(',', $options['only']) : $options['only'] : ['index', 'show', 'create', 'update', 'delete', 'new', 'edit'];

		if (isset($options['except']))
		{
			$options['except'] = is_array($options['except']) ? $options['except'] : explode(',', $options['except']);
			$c                 = count($methods);
			for ($i = 0; $i < $c; $i ++)
			{
				if (in_array($methods[$i], $options['except']))
				{
					unset($methods[$i]);
				}
			}
		}

		if (in_array('index', $methods))
		{
			$this->get($name, $new_name . '::index', $options);
		}
		if (in_array('new', $methods))
		{
			$this->get($name . '/new', $new_name . '::new', $options);
		}
		if (in_array('edit', $methods))
		{
			$this->get($name . '/' . $id . '/edit', $new_name . '::edit/$1', $options);
		}
		if (in_array('show', $methods))
		{
			$this->get($name . '/' . $id, $new_name . '::show/$1', $options);
		}
		if (in_array('create', $methods))
		{
			$this->post($name, $new_name . '::create', $options);
		}
		if (in_array('update', $methods))
		{
			$this->put($name . '/' . $id, $new_name . '::update/$1', $options);
			$this->patch($name . '/' . $id, $new_name . '::update/$1', $options);
		}
		if (in_array('delete', $methods))
		{
			$this->delete($name . '/' . $id, $new_name . '::delete/$1', $options);
		}

		// Web Safe? delete needs checking before update because of method name
		if (isset($options['websafe']))
		{
			if (in_array('delete', $methods))
			{
				$this->post($name . '/' . $id . '/delete', $new_name . '::delete/$1', $options);
			}
			if (in_array('update', $methods))
			{
				$this->post($name . '/' . $id, $new_name . '::update/$1', $options);
			}
		}

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Specifies a single route to match for multiple HTTP Verbs.
	 *
	 * Example:
	 *  $route->match( ['get', 'post'], 'users/(:num)', 'users/$1);
	 *
	 * @param array        $verbs
	 * @param string       $from
	 * @param string|array $to
	 * @param array        $options
	 *
	 * @return \CodeIgniter\Router\RouteCollectionInterface
	 */
	public function match(array $verbs = [], string $from, $to, array $options = null): RouteCollectionInterface
	{
		foreach ($verbs as $verb)
		{
			$verb = strtolower($verb);

			$this->{$verb}($from, $to, $options);
		}

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Specifies a route that is only available to GET requests.
	 *
	 * @param string       $from
	 * @param string|array $to
	 * @param array        $options
	 *
	 * @return \CodeIgniter\Router\RouteCollectionInterface
	 */
	public function get(string $from, $to, array $options = null): RouteCollectionInterface
	{
		$this->create('get', $from, $to, $options);

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Specifies a route that is only available to POST requests.
	 *
	 * @param string       $from
	 * @param string|array $to
	 * @param array        $options
	 *
	 * @return \CodeIgniter\Router\RouteCollectionInterface
	 */
	public function post(string $from, $to, array $options = null): RouteCollectionInterface
	{
		$this->create('post', $from, $to, $options);

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Specifies a route that is only available to PUT requests.
	 *
	 * @param string       $from
	 * @param string|array $to
	 * @param array        $options
	 *
	 * @return \CodeIgniter\Router\RouteCollectionInterface
	 */
	public function put(string $from, $to, array $options = null): RouteCollectionInterface
	{
		$this->create('put', $from, $to, $options);

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Specifies a route that is only available to DELETE requests.
	 *
	 * @param string       $from
	 * @param string|array $to
	 * @param array        $options
	 *
	 * @return \CodeIgniter\Router\RouteCollectionInterface
	 */
	public function delete(string $from, $to, array $options = null): RouteCollectionInterface
	{
		$this->create('delete', $from, $to, $options);

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Specifies a route that is only available to HEAD requests.
	 *
	 * @param string       $from
	 * @param string|array $to
	 * @param array        $options
	 *
	 * @return \CodeIgniter\Router\RouteCollectionInterface
	 */
	public function head(string $from, $to, array $options = null): RouteCollectionInterface
	{
		$this->create('head', $from, $to, $options);

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Specifies a route that is only available to PATCH requests.
	 *
	 * @param string       $from
	 * @param string|array $to
	 * @param array        $options
	 *
	 * @return \CodeIgniter\Router\RouteCollectionInterface
	 */
	public function patch(string $from, $to, array $options = null): RouteCollectionInterface
	{
		$this->create('patch', $from, $to, $options);

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Specifies a route that is only available to OPTIONS requests.
	 *
	 * @param string       $from
	 * @param string|array $to
	 * @param array        $options
	 *
	 * @return \CodeIgniter\Router\RouteCollectionInterface
	 */
	public function options(string $from, $to, array $options = null): RouteCollectionInterface
	{
		$this->create('options', $from, $to, $options);

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Specifies a route that is only available to command-line requests.
	 *
	 * @param string       $from
	 * @param string|array $to
	 * @param array        $options
	 *
	 * @return \CodeIgniter\Router\RouteCollectionInterface
	 */
	public function cli(string $from, $to, array $options = null): RouteCollectionInterface
	{
		$this->create('cli', $from, $to, $options);

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Limits the routes to a specified ENVIRONMENT or they won't run.
	 *
	 * @param string   $env
	 * @param \Closure $callback
	 *
	 * @return RouteCollectionInterface
	 */
	public function environment(string $env, \Closure $callback): RouteCollectionInterface
	{
		if (ENVIRONMENT === $env)
		{
			$callback($this);
		}

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Attempts to look up a route based on it's destination.
	 *
	 * If a route exists:
	 *
	 *      'path/(:any)/(:any)' => 'Controller::method/$1/$2'
	 *
	 * This method allows you to know the Controller and method
	 * and get the route that leads to it.
	 *
	 *      // Equals 'path/$param1/$param2'
	 *      reverseRoute('Controller::method', $param1, $param2);
	 *
	 * @param string $search
	 * @param array  ...$params
	 *
	 * @return string|false
	 */
	public function reverseRoute(string $search, ...$params)
	{
		// Named routes get higher priority.
		foreach ($this->routes as $verb => $collection)
		{
			if (array_key_exists($search, $collection))
			{
				return $this->fillRouteParams(key($collection[$search]['route']), $params);
			}
		}

		// If it's not a named route, then loop over
		// all routes to find a match.
		foreach ($this->routes as $verb => $collection)
		{
			foreach ($collection as $route)
			{
				$from = key($route['route']);
				$to   = $route['route'][$from];

				// Lose any namespace slash at beginning of strings
				// to ensure more consistent match.
				$to     = ltrim($to, '\\');
				$search = ltrim($search, '\\');

				// If there's any chance of a match, then it will
				// be with $search at the beginning of the $to string.
				if (strpos($to, $search) !== 0)
				{
					continue;
				}

				// Ensure that the number of $params given here
				// matches the number of back-references in the route
				if (substr_count($to, '$') !== count($params))
				{
					continue;
				}

				return $this->fillRouteParams($from, $params);
			}
		}

		// If we're still here, then we did not find a match.
		return false;
	}

	//--------------------------------------------------------------------

	/**
	 * Checks a route (using the "from") to see if it's filtered or not.
	 *
	 * @param string $search
	 *
	 * @return boolean
	 */
	public function isFiltered(string $search): bool
	{
		return isset($this->routesOptions[$search]['filter']);
	}

	//--------------------------------------------------------------------

	/**
	 * Returns the filter that should be applied for a single route, along
	 * with any parameters it might have. Parameters are found by splitting
	 * the parameter name on a colon to separate the filter name from the parameter list,
	 * and the splitting the result on commas. So:
	 *
	 *    'role:admin,manager'
	 *
	 * has a filter of "role", with parameters of ['admin', 'manager'].
	 *
	 * @param string $search
	 *
	 * @return string
	 */
	public function getFilterForRoute(string $search): string
	{
		if (! $this->isFiltered($search))
		{
			return '';
		}

		return $this->routesOptions[$search]['filter'];
	}

	//--------------------------------------------------------------------

	/**
	 * Given a
	 *
	 * @param string     $from
	 * @param array|null $params
	 *
	 * @return string
	 */
	protected function fillRouteParams(string $from, array $params = null): string
	{
		// Find all of our back-references in the original route
		preg_match_all('/\(([^)]+)\)/', $from, $matches);

		if (empty($matches[0]))
		{
			return '/' . ltrim($from, '/');
		}

		// Build our resulting string, inserting the $params in
		// the appropriate places.
		foreach ($matches[0] as $index => $pattern)
		{
			// Ensure that the param we're inserting matches
			// the expected param type.
			$pos = strpos($from, $pattern);

			if (preg_match("|{$pattern}|", $params[$index]))
			{
				$from = substr_replace($from, $params[$index], $pos, strlen($pattern));
			}
			else
			{
				throw RouterException::forInvalidParameterType();
			}
		}

		return '/' . ltrim($from, '/');
	}

	//--------------------------------------------------------------------

	/**
	 * Does the heavy lifting of creating an actual route. You must specify
	 * the request method(s) that this route will work for. They can be separated
	 * by a pipe character "|" if there is more than one.
	 *
	 * @param string       $verb
	 * @param string       $from
	 * @param string|array $to
	 * @param array        $options
	 */
	protected function create(string $verb, string $from, $to, array $options = null)
	{
		$overwrite = false;
		$prefix    = is_null($this->group) ? '' : $this->group . '/';

		$from = filter_var($prefix . $from, FILTER_SANITIZE_STRING);

		// While we want to add a route within a group of '/',
		// it doesn't work with matching, so remove them...
		if ($from !== '/')
		{
			$from = trim($from, '/');
		}

		$options = array_merge((array) $this->currentOptions, (array) $options);

		// Hostname limiting?
		if (! empty($options['hostname']))
		{
			// @todo determine if there's a way to whitelist hosts?
			if (isset($_SERVER['HTTP_HOST']) && strtolower($_SERVER['HTTP_HOST']) !== strtolower($options['hostname']))
			{
				return;
			}

			$overwrite = true;
		}

		// Limiting to subdomains?
		else if (isset($options['subdomain']) && ! empty($options['subdomain']))
		{
			// If we don't match the current subdomain, then
			// we don't need to add the route.
			if (! $this->checkSubdomains($options['subdomain']))
			{
				return;
			}

			$overwrite = true;
		}

		// Are we offsetting the binds?
		// If so, take care of them here in one
		// fell swoop.
		if (isset($options['offset']) && is_string($to))
		{
			// Get a constant string to work with.
			$to = preg_replace('/(\$\d+)/', '$X', $to);

			for ($i = (int) $options['offset'] + 1; $i < (int) $options['offset'] + 7; $i ++)
			{
				$to = preg_replace_callback(
						'/\$X/', function ($m) use ($i) {
							return '$' . $i;
						}, $to, 1
				);
			}
		}

		// Replace our regex pattern placeholders with the actual thing
		// so that the Router doesn't need to know about any of this.
		foreach ($this->placeholders as $tag => $pattern)
		{
			$from = str_ireplace(':' . $tag, $pattern, $from);
		}

		// If no namespace found, add the default namespace
		if (is_string($to) && strpos($to, '\\') === false)
		{
			$namespace = $options['namespace'] ?? $this->defaultNamespace;
			$to        = trim($namespace, '\\') . '\\' . $to;
		}

		// Always ensure that we escape our namespace so we're not pointing to
		// \CodeIgniter\Routes\Controller::method.
		if (is_string($to))
		{
			$to = '\\' . ltrim($to, '\\');
		}

		$name = $options['as'] ?? $from;

		// Don't overwrite any existing 'froms' so that auto-discovered routes
		// do not overwrite any app/Config/Routes settings. The app
		// routes should always be the "source of truth".
		// this works only because discovered routes are added just prior
		// to attempting to route the request.
		if (isset($this->routes[$verb][$name]) && ! $overwrite)
		{
			return;
		}

		$this->routes[$verb][$name] = [
			'route' => [$from => $to],
		];

		$this->routesOptions[$from] = $options;

		// Is this a redirect?
		if (isset($options['redirect']) && is_numeric($options['redirect']))
		{
			$this->routes['*'][$name]['redirect'] = $options['redirect'];
		}
	}

	//--------------------------------------------------------------------

	/**
	 * Compares the subdomain(s) passed in against the current subdomain
	 * on this page request.
	 *
	 * @param mixed $subdomains
	 *
	 * @return boolean
	 */
	private function checkSubdomains($subdomains): bool
	{
		// CLI calls can't be on subdomain.
		if (! isset($_SERVER['HTTP_HOST']))
		{
			return false;
		}

		if (is_null($this->currentSubdomain))
		{
			$this->currentSubdomain = $this->determineCurrentSubdomain();
		}

		if (! is_array($subdomains))
		{
			$subdomains = [$subdomains];
		}

		// Routes can be limited to any sub-domain. In that case, though,
		// it does require a sub-domain to be present.
		if (! empty($this->currentSubdomain) && in_array('*', $subdomains))
		{
			return true;
		}

		foreach ($subdomains as $subdomain)
		{
			if ($subdomain === $this->currentSubdomain)
			{
				return true;
			}
		}

		return false;
	}

	//--------------------------------------------------------------------

	/**
	 * Examines the HTTP_HOST to get a best match for the subdomain. It
	 * won't be perfect, but should work for our needs.
	 *
	 * It's especially not perfect since it's possible to register a domain
	 * with a period (.) as part of the domain name.
	 */
	private function determineCurrentSubdomain()
	{
		// We have to ensure that a scheme exists
		// on the URL else parse_url will mis-interpret
		// 'host' as the 'path'.
		$url = $_SERVER['HTTP_HOST'];
		if (strpos($url, 'http') !== 0)
		{
			$url = 'http://' . $url;
		}

		$parsedUrl = parse_url($url);

		$host = explode('.', $parsedUrl['host']);

		if ($host[0] === 'www')
		{
			unset($host[0]);
		}

		// Get rid of any domains, which will be the last
		unset($host[count($host)]);

		// Account for .co.uk, .co.nz, etc. domains
		if (end($host) === 'co')
		{
			$host = array_slice($host, 0, -1);
		}

		// If we only have 1 part left, then we don't have a sub-domain.
		if (count($host) === 1)
		{
			// Set it to false so we don't make it back here again.
			return false;
		}

		return array_shift($host);
	}

	//--------------------------------------------------------------------

	/**
	 * Reset the routes, so that a FeatureTestCase can provide the
	 * explicit ones needed for it.
	 */
	public function resetRoutes()
	{
		$this->routes = ['*' => []];
		foreach ($this->defaultHTTPMethods as $verb)
		{
			$this->routes[$verb] = [];
		}
	}

}


<?php

/**
 * CodeIgniter
 *
 * An open source application development framework for PHP
 *
 * This content is released under the MIT License (MIT)
 *
 * Copyright (c) 2014-2019 British Columbia Institute of Technology
 *
 * 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.
 *
 * @package    CodeIgniter
 * @author     CodeIgniter Dev Team
 * @copyright  2014-2019 British Columbia Institute of Technology (https://bcit.ca/)
 * @license    https://opensource.org/licenses/MIT	MIT License
 * @link       https://codeigniter.com
 * @since      Version 3.0.0
 * @filesource
 */

namespace CodeIgniter\Debug;

use CodeIgniter\API\ResponseTrait;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\Response;
use Config\Paths;
use function error_reporting;
use ErrorException;
use Throwable;

/**
 * Exceptions manager
 */
class Exceptions
{
	use ResponseTrait;

	/**
	 * Nesting level of the output buffering mechanism
	 *
	 * @var integer
	 */
	public $ob_level;

	/**
	 * The path to the directory containing the
	 * cli and html error view directories.
	 *
	 * @var string
	 */
	protected $viewPath;

	/**
	 * @var \Config\Exceptions
	 */
	protected $config;

	/**
	 * @var \CodeIgniter\HTTP\IncomingRequest
	 */
	protected $request;

	/**
	 * @var \CodeIgniter\HTTP\Response
	 */
	protected $response;

	//--------------------------------------------------------------------

	/**
	 * Constructor.
	 *
	 * @param \Config\Exceptions                $config
	 * @param \CodeIgniter\HTTP\IncomingRequest $request
	 * @param \CodeIgniter\HTTP\Response        $response
	 */
	public function __construct(\Config\Exceptions $config, IncomingRequest $request, Response $response)
	{
		$this->ob_level = ob_get_level();

		$this->viewPath = rtrim($config->errorViewPath, '/ ') . '/';

		$this->config = $config;

		$this->request  = $request;
		$this->response = $response;
	}

	//--------------------------------------------------------------------

	/**
	 * Responsible for registering the error, exception and shutdown
	 * handling of our application.
	 */
	public function initialize()
	{
		//Set the Exception Handler
		set_exception_handler([$this, 'exceptionHandler']);

		// Set the Error Handler
		set_error_handler([$this, 'errorHandler']);

		// Set the handler for shutdown to catch Parse errors
		// Do we need this in PHP7?
		register_shutdown_function([$this, 'shutdownHandler']);
	}

	//--------------------------------------------------------------------

	/**
	 * Catches any uncaught errors and exceptions, including most Fatal errors
	 * (Yay PHP7!). Will log the error, display it if display_errors is on,
	 * and fire an event that allows custom actions to be taken at this point.
	 *
	 * @param \Throwable $exception
	 */
	public function exceptionHandler(Throwable $exception)
	{
		$codes      = $this->determineCodes($exception);
		$statusCode = $codes[0];
		$exitCode   = $codes[1];

		// Log it
		if ($this->config->log === true && ! in_array($statusCode, $this->config->ignoreCodes))
		{
			log_message('critical', $exception->getMessage() . "\n{trace}", [
							'trace' => $exception->getTraceAsString(),
						]);
		}

		if (! is_cli())
		{
			$this->response->setStatusCode($statusCode);
			$header = "HTTP/{$this->request->getProtocolVersion()} {$this->response->getStatusCode()} {$this->response->getReason()}";
			header($header, true, $statusCode);

			if (strpos($this->request->getHeaderLine('accept'), 'text/html') === false)
			{
				$this->respond(ENVIRONMENT === 'development' ? $this->collectVars($exception, $statusCode) : '', $statusCode)->send();

				exit($exitCode);
			}
		}

		$this->render($exception, $statusCode);

		exit($exitCode);
	}

	//--------------------------------------------------------------------

	/**
	 * Even in PHP7, some errors make it through to the errorHandler, so
	 * convert these to Exceptions and let the exception handler log it and
	 * display it.
	 *
	 * This seems to be primarily when a user triggers it with trigger_error().
	 *
	 * @param integer      $severity
	 * @param string       $message
	 * @param string|null  $file
	 * @param integer|null $line
	 * @param null         $context
	 *
	 * @throws \ErrorException
	 */
	public function errorHandler(int $severity, string $message, string $file = null, int $line = null, $context = null)
	{
		if (! (error_reporting() & $severity))
		{
			return;
		}

		// Convert it to an exception and pass it along.
		throw new ErrorException($message, 0, $severity, $file, $line);
	}

	//--------------------------------------------------------------------

	/**
	 * Checks to see if any errors have happened during shutdown that
	 * need to be caught and handle them.
	 */
	public function shutdownHandler()
	{
		$error = error_get_last();

		// If we've got an error that hasn't been displayed, then convert
		// it to an Exception and use the Exception handler to display it
		// to the user.
		if (! is_null($error))
		{
			// Fatal Error?
			if (in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE]))
			{
				$this->exceptionHandler(new ErrorException($error['message'], $error['type'], 0, $error['file'], $error['line']));
			}
		}
	}

	//--------------------------------------------------------------------

	/**
	 * Determines the view to display based on the exception thrown,
	 * whether an HTTP or CLI request, etc.
	 *
	 * @param \Throwable $exception
	 * @param string     $template_path
	 *
	 * @return string       The path and filename of the view file to use
	 */
	protected function determineView(Throwable $exception, string $template_path): string
	{
		// Production environments should have a custom exception file.
		$view          = 'production.php';
		$template_path = rtrim($template_path, '/ ') . '/';

		if (str_ireplace(['off', 'none', 'no', 'false', 'null'], '', ini_get('display_errors')))
		{
			$view = 'error_exception.php';
		}

		// 404 Errors
		if ($exception instanceof PageNotFoundException)
		{
			return 'error_404.php';
		}

		// Allow for custom views based upon the status code
		else if (is_file($template_path . 'error_' . $exception->getCode() . '.php'))
		{
			return 'error_' . $exception->getCode() . '.php';
		}

		return $view;
	}

	//--------------------------------------------------------------------

	/**
	 * Given an exception and status code will display the error to the client.
	 *
	 * @param \Throwable $exception
	 * @param integer    $statusCode
	 */
	protected function render(Throwable $exception, int $statusCode)
	{
		// Determine directory with views
		$path = $this->viewPath;
		if (empty($path))
		{
			$paths = new Paths();
			$path  = $paths->viewDirectory . '/errors/';
		}

		$path = is_cli()
			? $path . 'cli/'
			: $path . 'html/';

		// Determine the vew
		$view = $this->determineView($exception, $path);

		// Prepare the vars
		$vars = $this->collectVars($exception, $statusCode);
		extract($vars);

		// Render it
		if (ob_get_level() > $this->ob_level + 1)
		{
			ob_end_clean();
		}

		ob_start();
		include($path . $view);
		$buffer = ob_get_contents();
		ob_end_clean();
		echo $buffer;
	}

	//--------------------------------------------------------------------

	/**
	 * Gathers the variables that will be made available to the view.
	 *
	 * @param \Throwable $exception
	 * @param integer    $statusCode
	 *
	 * @return array
	 */
	protected function collectVars(Throwable $exception, int $statusCode): array
	{
		return [
			'title'   => get_class($exception),
			'type'    => get_class($exception),
			'code'    => $statusCode,
			'message' => $exception->getMessage() ?? '(null)',
			'file'    => $exception->getFile(),
			'line'    => $exception->getLine(),
			'trace'   => $exception->getTrace(),
		];
	}

	/**
	 * Determines the HTTP status code and the exit status code for this request.
	 *
	 * @param \Throwable $exception
	 *
	 * @return array
	 */
	protected function determineCodes(Throwable $exception): array
	{
		$statusCode = abs($exception->getCode());

		if ($statusCode < 100 || $statusCode > 599)
		{
			$exitStatus = $statusCode + EXIT__AUTO_MIN; // 9 is EXIT__AUTO_MIN
			if ($exitStatus > EXIT__AUTO_MAX) // 125 is EXIT__AUTO_MAX
			{
				$exitStatus = EXIT_ERROR; // EXIT_ERROR
			}
			$statusCode = 500;
		}
		else
		{
			$exitStatus = 1; // EXIT_ERROR
		}

		return [
			$statusCode ?? 500,
			$exitStatus,
		];
	}

	//--------------------------------------------------------------------
	//--------------------------------------------------------------------
	// Display Methods
	//--------------------------------------------------------------------

	/**
	 * Clean Path
	 *
	 * This makes nicer looking paths for the error output.
	 *
	 * @param string $file
	 *
	 * @return string
	 */
	public static function cleanPath(string $file): string
	{
		if (strpos($file, APPPATH) === 0)
		{
			$file = 'APPPATH/' . substr($file, strlen(APPPATH));
		}
		elseif (strpos($file, SYSTEMPATH) === 0)
		{
			$file = 'SYSTEMPATH/' . substr($file, strlen(SYSTEMPATH));
		}
		elseif (strpos($file, FCPATH) === 0)
		{
			$file = 'FCPATH/' . substr($file, strlen(FCPATH));
		}

		return $file;
	}

	//--------------------------------------------------------------------

	/**
	 * Describes memory usage in real-world units. Intended for use
	 * with memory_get_usage, etc.
	 *
	 * @param $bytes
	 *
	 * @return string
	 */
	public static function describeMemory(int $bytes): string
	{
		if ($bytes < 1024)
		{
			return $bytes . 'B';
		}
		else if ($bytes < 1048576)
		{
			return round($bytes / 1024, 2) . 'KB';
		}

		return round($bytes / 1048576, 2) . 'MB';
	}

	//--------------------------------------------------------------------

	/**
	 * Creates a syntax-highlighted version of a PHP file.
	 *
	 * @param string  $file
	 * @param integer $lineNumber
	 * @param integer $lines
	 *
	 * @return boolean|string
	 */
	public static function highlightFile(string $file, int $lineNumber, int $lines = 15)
	{
		if (empty($file) || ! is_readable($file))
		{
			return false;
		}

		// Set our highlight colors:
		if (function_exists('ini_set'))
		{
			ini_set('highlight.comment', '#767a7e; font-style: italic');
			ini_set('highlight.default', '#c7c7c7');
			ini_set('highlight.html', '#06B');
			ini_set('highlight.keyword', '#f1ce61;');
			ini_set('highlight.string', '#869d6a');
		}

		try
		{
			$source = file_get_contents($file);
		}
		catch (Throwable $e)
		{
			return false;
		}

		$source = str_replace(["\r\n", "\r"], "\n", $source);
		$source = explode("\n", highlight_string($source, true));
		$source = str_replace('
', "\n", $source[1]);

		$source = explode("\n", str_replace("\r\n", "\n", $source));

		// Get just the part to show
		$start = $lineNumber - (int) round($lines / 2);
		$start = $start < 0 ? 0 : $start;

		// Get just the lines we need to display, while keeping line numbers...
		$source = array_splice($source, $start, $lines, true);

		// Used to format the line number in the source
		$format = '% ' . strlen($start + $lines) . 'd';

		$out = '';
		// Because the highlighting may have an uneven number
		// of open and close span tags on one line, we need
		// to ensure we can close them all to get the lines
		// showing correctly.
		$spans = 1;

		foreach ($source as $n => $row)
		{
			$spans += substr_count($row, '<span') - substr_count($row, '</span');
			$row    = str_replace(["\r", "\n"], ['', ''], $row);

			if (($n + $start + 1) === $lineNumber)
			{
				preg_match_all('#<[^>]+>#', $row, $tags);
				$out .= sprintf("{$format} %s\n%s", $n + $start + 1, strip_tags($row), implode('', $tags[0])
				);
			}
			else
			{
				$out .= sprintf('' . $format . ' %s', $n + $start + 1, $row) . "\n";
			}
		}

		if ($spans > 0)
		{
			$out .= str_repeat('', $spans);
		}

		return '
' . $out . '
';
	}

	//--------------------------------------------------------------------
}


<?php
/**
 * CodeIgniter
 *
 * An open source application development framework for PHP
 *
 * This content is released under the MIT License (MIT)
 *
 * Copyright (c) 2014-2019 British Columbia Institute of Technology
 *
 * 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.
 *
 * @package    CodeIgniter
 * @author     CodeIgniter Dev Team
 * @copyright  2014-2019 British Columbia Institute of Technology (https://bcit.ca/)
 * @license    https://opensource.org/licenses/MIT	MIT License
 * @link       https://codeigniter.com
 * @since      Version 3.0.0
 * @filesource
 */

namespace CodeIgniter\Session\Handlers;

use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Session\Exceptions\SessionException;

/**
 * Session handler using Memcache for persistence
 */
class MemcachedHandler extends BaseHandler implements \SessionHandlerInterface
{

	/**
	 * Memcached instance
	 *
	 * @var \Memcached
	 */
	protected $memcached;

	/**
	 * Key prefix
	 *
	 * @var string
	 */
	protected $keyPrefix = 'ci_session:';

	/**
	 * Lock key
	 *
	 * @var string
	 */
	protected $lockKey;

	/**
	 * Number of seconds until the session ends.
	 *
	 * @var integer
	 */
	protected $sessionExpiration = 7200;

	//--------------------------------------------------------------------

	/**
	 * Constructor
	 *
	 * @param  BaseConfig $config
	 * @throws \CodeIgniter\Session\Exceptions\SessionException
	 */
	public function __construct(BaseConfig $config, string $ipAddress)
	{
		parent::__construct($config, $ipAddress);

		if (empty($this->savePath))
		{
			throw SessionException::forEmptySavepath();
		}

		if ($this->matchIP === true)
		{
			$this->keyPrefix .= $this->ipAddress . ':';
		}

		if (! empty($this->keyPrefix))
		{
			ini_set('memcached.sess_prefix', $this->keyPrefix);
		}

		$this->sessionExpiration = $config->sessionExpiration;
	}

	//--------------------------------------------------------------------

	/**
	 * Open
	 *
	 * Sanitizes save_path and initializes connections.
	 *
	 * @param string $save_path Server path(s)
	 * @param string $name      Session cookie name, unused
	 *
	 * @return boolean
	 */
	public function open($save_path, $name): bool
	{
		$this->memcached = new \Memcached();
		$this->memcached->setOption(\Memcached::OPT_BINARY_PROTOCOL, true); // required for touch() usage

		$server_list = [];

		foreach ($this->memcached->getServerList() as $server)
		{
			$server_list[] = $server['host'] . ':' . $server['port'];
		}

		if (! preg_match_all('#,?([^,:]+)\:(\d{1,5})(?:\:(\d+))?#', $this->savePath, $matches, PREG_SET_ORDER)
		)
		{
			$this->memcached = null;
			$this->logger->error('Session: Invalid Memcached save path format: ' . $this->savePath);

			return false;
		}

		foreach ($matches as $match)
		{
			// If Memcached already has this server (or if the port is invalid), skip it
			if (in_array($match[1] . ':' . $match[2], $server_list, true))
			{
				$this->logger->debug('Session: Memcached server pool already has ' . $match[1] . ':' . $match[2]);
				continue;
			}

			if (! $this->memcached->addServer($match[1], $match[2], $match[3] ?? 0))
			{
				$this->logger->error('Could not add ' . $match[1] . ':' . $match[2] . ' to Memcached server pool.');
			}
			else
			{
				$server_list[] = $match[1] . ':' . $match[2];
			}
		}

		if (empty($server_list))
		{
			$this->logger->error('Session: Memcached server pool is empty.');

			return false;
		}

		return true;
	}

	//--------------------------------------------------------------------

	/**
	 * Read
	 *
	 * Reads session data and acquires a lock
	 *
	 * @param string $sessionID Session ID
	 *
	 * @return string    Serialized session data
	 */
	public function read($sessionID): string
	{
		if (isset($this->memcached) && $this->lockSession($sessionID))
		{
			// Needed by write() to detect session_regenerate_id() calls
			$this->sessionID = $sessionID;

			$session_data      = (string) $this->memcached->get($this->keyPrefix . $sessionID);
			$this->fingerprint = md5($session_data);

			return $session_data;
		}

		return '';
	}

	//--------------------------------------------------------------------

	/**
	 * Write
	 *
	 * Writes (create / update) session data
	 *
	 * @param string $sessionID   Session ID
	 * @param string $sessionData Serialized session data
	 *
	 * @return boolean
	 */
	public function write($sessionID, $sessionData): bool
	{
		if (! isset($this->memcached))
		{
			return false;
		}
		// Was the ID regenerated?
		elseif ($sessionID !== $this->sessionID)
		{
			if (! $this->releaseLock() || ! $this->lockSession($sessionID))
			{
				return false;
			}

			$this->fingerprint = md5('');
			$this->sessionID   = $sessionID;
		}

		if (isset($this->lockKey))
		{
			$this->memcached->replace($this->lockKey, time(), 300);

			if ($this->fingerprint !== ($fingerprint = md5($sessionData)))
			{
				if ($this->memcached->set($this->keyPrefix . $sessionID, $sessionData, $this->sessionExpiration))
				{
					$this->fingerprint = $fingerprint;

					return true;
				}

				return false;
			}

			return $this->memcached->touch($this->keyPrefix . $sessionID, $this->sessionExpiration);
		}

		return false;
	}

	//--------------------------------------------------------------------

	/**
	 * Close
	 *
	 * Releases locks and closes connection.
	 *
	 * @return boolean
	 */
	public function close(): bool
	{
		if (isset($this->memcached))
		{
			isset($this->lockKey) && $this->memcached->delete($this->lockKey);

			if (! $this->memcached->quit())
			{
				return false;
			}

			$this->memcached = null;

			return true;
		}

		return false;
	}

	//--------------------------------------------------------------------

	/**
	 * Destroy
	 *
	 * Destroys the current session.
	 *
	 * @param string $session_id Session ID
	 *
	 * @return boolean
	 */
	public function destroy($session_id): bool
	{
		if (isset($this->memcached, $this->lockKey))
		{
			$this->memcached->delete($this->keyPrefix . $session_id);

			return $this->destroyCookie();
		}

		return false;
	}

	//--------------------------------------------------------------------

	/**
	 * Garbage Collector
	 *
	 * Deletes expired sessions
	 *
	 * @param integer $maxlifetime Maximum lifetime of sessions
	 *
	 * @return boolean
	 */
	public function gc($maxlifetime): bool
	{
		// Not necessary, Memcached takes care of that.
		return true;
	}

	//--------------------------------------------------------------------

	/**
	 * Get lock
	 *
	 * Acquires an (emulated) lock.
	 *
	 * @param string $sessionID Session ID
	 *
	 * @return boolean
	 */
	protected function lockSession(string $sessionID): bool
	{
		if (isset($this->lockKey))
		{
			return $this->memcached->replace($this->lockKey, time(), 300);
		}

		// 30 attempts to obtain a lock, in case another request already has it
		$lock_key = $this->keyPrefix . $sessionID . ':lock';
		$attempt  = 0;

		do
		{
			if ($this->memcached->get($lock_key))
			{
				sleep(1);
				continue;
			}

			if (! $this->memcached->set($lock_key, time(), 300))
			{
				$this->logger->error('Session: Error while trying to obtain lock for ' . $this->keyPrefix . $sessionID);

				return false;
			}

			$this->lockKey = $lock_key;
			break;
		}
		while (++ $attempt < 30);

		if ($attempt === 30)
		{
			$this->logger->error('Session: Unable to obtain lock for ' . $this->keyPrefix . $sessionID . ' after 30 attempts, aborting.');

			return false;
		}

		$this->lock = true;

		return true;
	}

	//--------------------------------------------------------------------

	/**
	 * Release lock
	 *
	 * Releases a previously acquired lock
	 *
	 * @return boolean
	 */
	protected function releaseLock(): bool
	{
		if (isset($this->memcached, $this->lockKey) && $this->lock)
		{
			if (! $this->memcached->delete($this->lockKey) &&
					$this->memcached->getResultCode() !== \Memcached::RES_NOTFOUND
			)
			{
				$this->logger->error('Session: Error while trying to free lock for ' . $this->lockKey);

				return false;
			}

			$this->lockKey = null;
			$this->lock    = false;
		}

		return true;
	}

	//--------------------------------------------------------------------
}


<?php

/**
 * CodeIgniter
 *
 * An open source application development framework for PHP
 *
 * This content is released under the MIT License (MIT)
 *
 * Copyright (c) 2014-2019 British Columbia Institute of Technology
 *
 * 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.
 *
 * @package    CodeIgniter
 * @author     CodeIgniter Dev Team
 * @copyright  2014-2019 British Columbia Institute of Technology (https://bcit.ca/)
 * @license    https://opensource.org/licenses/MIT	MIT License
 * @link       https://codeigniter.com
 * @since      Version 3.0.0
 * @filesource
 */

namespace CodeIgniter\Validation;

/**
 * Format validation Rules.
 *
 * @package CodeIgniter\Validation
 */
class FormatRules
{

	/**
	 * Alpha
	 *
	 * @param string
	 *
	 * @return boolean
	 */
	public function alpha(string $str = null): bool
	{
		return ctype_alpha($str);
	}

	//--------------------------------------------------------------------

	/**
	 * Alpha with spaces.
	 *
	 * @param string $value Value.
	 *
	 * @return boolean True if alpha with spaces, else false.
	 */
	public function alpha_space(string $value = null): bool
	{
		if ($value === null)
		{
			return true;
		}

		return (bool) preg_match('/^[A-Z ]+$/i', $value);
	}

	//--------------------------------------------------------------------

	/**
	 * Alpha-numeric with underscores and dashes
	 *
	 * @param string
	 *
	 * @return boolean
	 */
	public function alpha_dash(string $str = null): bool
	{
		return (bool) preg_match('/^[a-z0-9_-]+$/i', $str);
	}

	//--------------------------------------------------------------------

	/**
	 * Alpha-numeric
	 *
	 * @param string
	 *
	 * @return boolean
	 */
	public function alpha_numeric(string $str = null): bool
	{
		return ctype_alnum($str);
	}

	//--------------------------------------------------------------------

	/**
	 * Alpha-numeric w/ spaces
	 *
	 * @param string
	 *
	 * @return boolean
	 */
	public function alpha_numeric_space(string $str = null): bool
	{
		return (bool) preg_match('/^[A-Z0-9 ]+$/i', $str);
	}

	//--------------------------------------------------------------------

	/**
	 * Any type of string
	 *
	 * Note: we specifically do NOT type hint $str here so that
	 * it doesn't convert numbers into strings.
	 *
	 * @param string|null $str
	 *
	 * @return boolean
	 */
	public function string($str = null): bool
	{
		return is_string($str);
	}

	//--------------------------------------------------------------------

	/**
	 * Decimal number
	 *
	 * @param string
	 *
	 * @return boolean
	 */
	public function decimal(string $str = null): bool
	{
		return (bool) preg_match('/^[\-+]?[0-9]+(|\.[0-9]+)$/', $str);
	}

	//--------------------------------------------------------------------

	/**
	 * Integer
	 *
	 * @param string
	 *
	 * @return boolean
	 */
	public function integer(string $str = null): bool
	{
		return (bool) preg_match('/^[\-+]?[0-9]+$/', $str);
	}

	//--------------------------------------------------------------------

	/**
	 * Is a Natural number  (0,1,2,3, etc.)
	 *
	 * @param  string
	 * @return boolean
	 */
	public function is_natural(string $str = null): bool
	{
		return ctype_digit($str);
	}

	//--------------------------------------------------------------------

	/**
	 * Is a Natural number, but not a zero  (1,2,3, etc.)
	 *
	 * @param  string
	 * @return boolean
	 */
	public function is_natural_no_zero(string $str = null): bool
	{
		return ($str !== '0' && ctype_digit($str));
	}

	//--------------------------------------------------------------------

	/**
	 * Numeric
	 *
	 * @param string
	 *
	 * @return boolean
	 */
	public function numeric(string $str = null): bool
	{
		return (bool) preg_match('/^[\-+]?[0-9]*\.?[0-9]+$/', $str);
	}

	//--------------------------------------------------------------------

	/**
	 * Compares value against a regular expression pattern.
	 *
	 * @param string $str
	 * @param string $pattern
	 * @param array  $data    Other field/value pairs
	 *
	 * @return boolean
	 */
	public function regex_match(string $str = null, string $pattern, array $data): bool
	{
		if (strpos($pattern, '/') !== 0)
		{
			$pattern = "/{$pattern}/";
		}

		return (bool) preg_match($pattern, $str);
	}

	//--------------------------------------------------------------------

	/**
	 * Validates that the string is a valid timezone as per the
	 * timezone_identifiers_list function.
	 *
	 * @see http://php.net/manual/en/datetimezone.listidentifiers.php
	 *
	 * @param string $str
	 *
	 * @return boolean
	 */
	public function timezone(string $str = null): bool
	{
		return in_array($str, timezone_identifiers_list());
	}

	//--------------------------------------------------------------------

	/**
	 * Valid Base64
	 *
	 * Tests a string for characters outside of the Base64 alphabet
	 * as defined by RFC 2045 http://www.faqs.org/rfcs/rfc2045
	 *
	 * @param  string
	 * @return boolean
	 */
	public function valid_base64(string $str = null): bool
	{
		return (base64_encode(base64_decode($str)) === $str);
	}

	//--------------------------------------------------------------------

	/**
	 * Valid JSON
	 *
	 * @param string
	 *
	 * @return boolean
	 */
	public function valid_json(string $str = null): bool
	{
		json_decode($str);
		return json_last_error() === JSON_ERROR_NONE;
	}

	//--------------------------------------------------------------------

	/**
	 * Checks for a correctly formatted email address
	 *
	 * @param string
	 *
	 * @return boolean
	 */
	public function valid_email(string $str = null): bool
	{
		if (function_exists('idn_to_ascii') && defined('INTL_IDNA_VARIANT_UTS46') && preg_match('#\A([^@]+)@(.+)\z#', $str, $matches))
		{
			$str = $matches[1] . '@' . idn_to_ascii($matches[2], 0, INTL_IDNA_VARIANT_UTS46);
		}

		return (bool) filter_var($str, FILTER_VALIDATE_EMAIL);
	}

	//--------------------------------------------------------------------

	/**
	 * Validate a comma-separated list of email addresses.
	 *
	 * Example:
	 *     valid_emails[one@example.com,two@example.com]
	 *
	 * @param string
	 *
	 * @return boolean
	 */
	public function valid_emails(string $str = null): bool
	{
		foreach (explode(',', $str) as $email)
		{
			$email = trim($email);
			if ($email === '')
			{
				return false;
			}

			if ($this->valid_email($email) === false)
			{
				return false;
			}
		}

		return true;
	}

	//--------------------------------------------------------------------

	/**
	 * Validate an IP address
	 *
	 * @param string $ip    IP Address
	 * @param string $which IP protocol: 'ipv4' or 'ipv6'
	 * @param array  $data
	 *
	 * @return boolean
	 */
	public function valid_ip(string $ip = null, string $which = null, array $data): bool
	{
		switch (strtolower($which))
		{
			case 'ipv4':
				$which = FILTER_FLAG_IPV4;
				break;
			case 'ipv6':
				$which = FILTER_FLAG_IPV6;
				break;
			default:
				$which = null;
				break;
		}

		return (bool) filter_var($ip, FILTER_VALIDATE_IP, $which);
	}

	//--------------------------------------------------------------------

	/**
	 * Checks a URL to ensure it's formed correctly.
	 *
	 * @param string $str
	 *
	 * @return boolean
	 */
	public function valid_url(string $str = null): bool
	{
		if (empty($str))
		{
			return false;
		}
		elseif (preg_match('/^(?:([^:]*)\:)?\/\/(.+)$/', $str, $matches))
		{
			if (! in_array($matches[1], ['http', 'https'], true))
			{
				return false;
			}

			$str = $matches[2];
		}

		$str = 'http://' . $str;

		return (filter_var($str, FILTER_VALIDATE_URL) !== false);
	}

	//--------------------------------------------------------------------

	/**
	 * Checks for a valid date and matches a given date format
	 *
	 * @param string $str
	 * @param string $format
	 *
	 * @return boolean
	 */
	public function valid_date(string $str = null, string $format = null): bool
	{
		if (empty($format))
		{
			return (bool) strtotime($str);
		}

		$date = \DateTime::createFromFormat($format, $str);

		return (bool) $date && \DateTime::getLastErrors()['warning_count'] === 0 && \DateTime::getLastErrors()['error_count'] === 0;
	}

	//--------------------------------------------------------------------
}


<?php

/**
 * CodeIgniter
 *
 * An open source application development framework for PHP
 *
 * This content is released under the MIT License (MIT)
 *
 * Copyright (c) 2014-2019 British Columbia Institute of Technology
 *
 * 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.
 *
 * @package    CodeIgniter
 * @author     CodeIgniter Dev Team
 * @copyright  2014-2019 British Columbia Institute of Technology (https://bcit.ca/)
 * @license    https://opensource.org/licenses/MIT	MIT License
 * @link       https://codeigniter.com
 * @since      Version 3.0.0
 * @filesource
 */

namespace CodeIgniter\Database\SQLite3;

use CodeIgniter\Database\ConnectionInterface;
use CodeIgniter\Database\Exceptions\DatabaseException;

/**
 * Forge for SQLite3
 */
class Forge extends \CodeIgniter\Database\Forge
{

	/**
	 * UNSIGNED support
	 *
	 * @var boolean|array
	 */
	protected $_unsigned = false;

	/**
	 * NULL value representation in CREATE/ALTER TABLE statements
	 *
	 * @var string
	 */
	protected $_null = 'NULL';

	//--------------------------------------------------------------------

	/**
	 * Constructor.
	 *
	 * @param $db ConnectionInterface
	 */
	public function __construct(ConnectionInterface $db)
	{
		parent::__construct($db);

		if (version_compare($this->db->getVersion(), '3.3', '<'))
		{
			$this->createTableIfStr = false;
			$this->dropTableIfStr   = false;
		}
	}

	//--------------------------------------------------------------------

	/**
	 * Create database
	 *
	 * @param string $db_name
	 *
	 * @return boolean
	 */
	public function createDatabase(string $db_name): bool
	{
		// In SQLite, a database is created when you connect to the database.
		// We'll return TRUE so that an error isn't generated.
		return true;
	}

	//--------------------------------------------------------------------

	/**
	 * Drop database
	 *
	 * @param string $db_name
	 *
	 * @return boolean
	 * @throws \CodeIgniter\Database\Exceptions\DatabaseException
	 */
	public function dropDatabase(string $db_name): bool
	{
		// In SQLite, a database is dropped when we delete a file
		if (! is_file($db_name))
		{
			if ($this->db->DBDebug)
			{
				throw new DatabaseException('Unable to drop the specified database.');
			}

			return false;
		}

		// We need to close the pseudo-connection first
		$this->db->close();
		if (! @unlink($db_name))
		{
			if ($this->db->DBDebug)
			{
				throw new DatabaseException('Unable to drop the specified database.');
			}

			return false;
		}

		if (! empty($this->db->dataCache['db_names']))
		{
			$key = array_search(strtolower($db_name), array_map('strtolower', $this->db->dataCache['db_names']), true);
			if ($key !== false)
			{
				unset($this->db->dataCache['db_names'][$key]);
			}
		}

		return true;
	}

	//--------------------------------------------------------------------

	/**
	 * ALTER TABLE
	 *
	 * @param string $alter_type ALTER type
	 * @param string $table      Table name
	 * @param mixed  $field      Column definition
	 *
	 * @return string|array
	 */
	protected function _alterTable(string $alter_type, string $table, $field)
	{
		switch ($alter_type)
		{
			case 'DROP':
				$sqlTable = new Table($this->db, $this);

				$sqlTable->fromTable($table)
					->dropColumn($field)
					->run();

				return '';
				break;
			case 'CHANGE':
				$sqlTable = new Table($this->db, $this);

				$sqlTable->fromTable($table)
						 ->modifyColumn($field)
						 ->run();

				return null;
				break;
			default:
				return parent::_alterTable($alter_type, $table, $field);
		}
	}

	//--------------------------------------------------------------------

	/**
	 * Process column
	 *
	 * @param array $field
	 *
	 * @return string
	 */
	protected function _processColumn(array $field): string
	{
		if ($field['type'] === 'TEXT' && strpos($field['length'], "('") === 0)
		{
			$field['type'] .= ' CHECK(' . $this->db->escapeIdentifiers($field['name'])
				. ' IN ' . $field['length'] . ')';
		}

		return $this->db->escapeIdentifiers($field['name'])
			   . ' ' . $field['type']
			   . $field['auto_increment']
			   . $field['null']
			   . $field['unique']
			   . $field['default'];
	}

	//--------------------------------------------------------------------

	/**
	 * Process indexes
	 *
	 * @param string $table
	 *
	 * @return array
	 */
	protected function _processIndexes(string $table): array
	{
		$sqls = [];

		for ($i = 0, $c = count($this->keys); $i < $c; $i++)
		{
			$this->keys[$i] = (array)$this->keys[$i];

			for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++)
			{
				if (! isset($this->fields[$this->keys[$i][$i2]]))
				{
					unset($this->keys[$i][$i2]);
				}
			}
			if (count($this->keys[$i]) <= 0)
			{
				continue;
			}

			if (in_array($i, $this->uniqueKeys))
			{
				$sqls[] = 'CREATE UNIQUE INDEX ' . $this->db->escapeIdentifiers($table . '_' . implode('_', $this->keys[$i]))
						  . ' ON ' . $this->db->escapeIdentifiers($table)
						  . ' (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i])) . ');';
				continue;
			}

			$sqls[] = 'CREATE INDEX ' . $this->db->escapeIdentifiers($table . '_' . implode('_', $this->keys[$i]))
					  . ' ON ' . $this->db->escapeIdentifiers($table)
					  . ' (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i])) . ');';
		}

		return $sqls;
	}

	//--------------------------------------------------------------------
	/**
	 * Field attribute TYPE
	 *
	 * Performs a data type mapping between different databases.
	 *
	 * @param array &$attributes
	 *
	 * @return void
	 */
	protected function _attributeType(array &$attributes)
	{
		switch (strtoupper($attributes['TYPE']))
		{
			case 'ENUM':
			case 'SET':
				$attributes['TYPE'] = 'TEXT';
				break;
			default:
				break;
		}
	}

	//--------------------------------------------------------------------

	/**
	 * Field attribute AUTO_INCREMENT
	 *
	 * @param array &$attributes
	 * @param array &$field
	 *
	 * @return void
	 */
	protected function _attributeAutoIncrement(array &$attributes, array &$field)
	{
		if (! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === true
			&& stripos($field['type'], 'int') !== false)
		{
			$field['type']           = 'INTEGER PRIMARY KEY';
			$field['default']        = '';
			$field['null']           = '';
			$field['unique']         = '';
			$field['auto_increment'] = ' AUTOINCREMENT';

			$this->primaryKeys = [];
		}
	}

	//--------------------------------------------------------------------

	/**
	 * Foreign Key Drop
	 *
	 * @param string $table        Table name
	 * @param string $foreign_name Foreign name
	 *
	 * @return boolean
	 * @throws \CodeIgniter\Database\Exceptions\DatabaseException
	 */
	public function dropForeignKey(string $table, string $foreign_name): bool
	{
		throw new DatabaseException(lang('Database.dropForeignKeyUnsupported'));
	}

	//--------------------------------------------------------------------

}


<?php

/**
 * CodeIgniter
 *
 * An open source application development framework for PHP
 *
 * This content is released under the MIT License (MIT)
 *
 * Copyright (c) 2014-2019 British Columbia Institute of Technology
 *
 * 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.
 *
 * @package    CodeIgniter
 * @author     CodeIgniter Dev Team
 * @copyright  2014-2019 British Columbia Institute of Technology (https://bcit.ca/)
 * @license    https://opensource.org/licenses/MIT	MIT License
 * @link       https://codeigniter.com
 * @since      Version 3.0.0
 * @filesource
 */

namespace CodeIgniter\Database\MySQLi;

use CodeIgniter\Database\PreparedQueryInterface;
use CodeIgniter\Database\BasePreparedQuery;

class PreparedQuery extends BasePreparedQuery implements PreparedQueryInterface
{

	/**
	 * Prepares the query against the database, and saves the connection
	 * info necessary to execute the query later.
	 *
	 * NOTE: This version is based on SQL code. Child classes should
	 * override this method.
	 *
	 * @param string $sql
	 * @param array  $options Passed to the connection's prepare statement.
	 *                        Unused in the MySQLi driver.
	 *
	 * @return mixed
	 */
	public function _prepare(string $sql, array $options = [])
	{
		// Mysqli driver doesn't like statements
		// with terminating semicolons.
		$sql = rtrim($sql, ';');

		if (! $this->statement = $this->db->mysqli->prepare($sql))
		{
			$this->errorCode   = $this->db->mysqli->errno;
			$this->errorString = $this->db->mysqli->error;
		}

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Takes a new set of data and runs it against the currently
	 * prepared query. Upon success, will return a Results object.
	 *
	 * @param array $data
	 *
	 * @return bool
	 */
	public function _execute(array $data): bool
	{
		if (is_null($this->statement))
		{
			throw new \BadMethodCallException('You must call prepare before trying to execute a prepared statement.');
		}

		// First off -bind the parameters
		$bindTypes = '';

		// Determine the type string
		foreach ($data as $item)
		{
			if (is_integer($item))
			{
				$bindTypes .= 'i';
			}
			elseif (is_numeric($item))
			{
				$bindTypes .= 'd';
			}
			else
			{
				$bindTypes .= 's';
			}
		}

		// Bind it
		$this->statement->bind_param($bindTypes, ...$data);

		$success = $this->statement->execute();

		return $success;
	}

	//--------------------------------------------------------------------

	/**
	 * Returns the result object for the prepared query.
	 *
	 * @return mixed
	 */
	public function _getResult()
	{
		return $this->statement->get_result();
	}

	//--------------------------------------------------------------------
}


<?php

/**
 * CodeIgniter
 *
 * An open source application development framework for PHP
 *
 * This content is released under the MIT License (MIT)
 *
 * Copyright (c) 2014-2019 British Columbia Institute of Technology
 *
 * 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.
 *
 * @package    CodeIgniter
 * @author     CodeIgniter Dev Team
 * @copyright  2014-2019 British Columbia Institute of Technology (https://bcit.ca/)
 * @license    https://opensource.org/licenses/MIT	MIT License
 * @link       https://codeigniter.com
 * @since      Version 3.0.0
 * @filesource
 */

namespace CodeIgniter\HTTP\Exceptions;

use CodeIgniter\Exceptions\ExceptionInterface;
use CodeIgniter\Exceptions\FrameworkException;

/**
 * Things that can go wrong with HTTP
 */
class HTTPException extends FrameworkException implements ExceptionInterface
{

	/**
	 * For CurlRequest
	 *
	 * @return             \CodeIgniter\HTTP\Exceptions\HTTPException
	 *
	 * Not testable with travis-ci
	 * @codeCoverageIgnore
	 */
	public static function forMissingCurl()
	{
		return new static(lang('HTTP.missingCurl'));
	}

	/**
	 * For CurlRequest
	 *
	 * @param string $cert
	 *
	 * @return \CodeIgniter\HTTP\Exceptions\HTTPException
	 */
	public static function forSSLCertNotFound(string $cert)
	{
		return new static(lang('HTTP.sslCertNotFound', [$cert]));
	}

	/**
	 * For CurlRequest
	 *
	 * @param string $key
	 *
	 * @return \CodeIgniter\HTTP\Exceptions\HTTPException
	 */
	public static function forInvalidSSLKey(string $key)
	{
		return new static(lang('HTTP.invalidSSLKey', [$key]));
	}

	/**
	 * For CurlRequest
	 *
	 * @param string $errorNum
	 * @param string $error
	 *
	 * @return             \CodeIgniter\HTTP\Exceptions\HTTPException
	 *
	 * Not testable with travis-ci; we over-ride the method which triggers it
	 * @codeCoverageIgnore
	 */
	public static function forCurlError(string $errorNum, string $error)
	{
		return new static(lang('HTTP.curlError', [$errorNum, $error]));
	}

	/**
	 * For IncomingRequest
	 *
	 * @param string $type
	 *
	 * @return \CodeIgniter\HTTP\Exceptions\HTTPException
	 */
	public static function forInvalidNegotiationType(string $type)
	{
		return new static(lang('HTTP.invalidNegotiationType', [$type]));
	}

	/**
	 * For Message
	 *
	 * @param string $protocols
	 *
	 * @return \CodeIgniter\HTTP\Exceptions\HTTPException
	 */
	public static function forInvalidHTTPProtocol(string $protocols)
	{
		return new static(lang('HTTP.invalidHTTPProtocol', [$protocols]));
	}

	/**
	 * For Negotiate
	 *
	 * @return \CodeIgniter\HTTP\Exceptions\HTTPException
	 */
	public static function forEmptySupportedNegotiations()
	{
		return new static(lang('HTTP.emptySupportedNegotiations'));
	}

	/**
	 * For RedirectResponse
	 *
	 * @param string $route
	 *
	 * @return \CodeIgniter\HTTP\Exceptions\HTTPException
	 */
	public static function forInvalidRedirectRoute(string $route)
	{
		return new static(lang('HTTP.invalidRoute', [$route]));
	}

	/**
	 * For Response
	 *
	 * @return \CodeIgniter\HTTP\Exceptions\HTTPException
	 */
	public static function forMissingResponseStatus()
	{
		return new static(lang('HTTP.missingResponseStatus'));
	}

	/**
	 * For Response
	 *
	 * @param integer $code
	 *
	 * @return \CodeIgniter\HTTP\Exceptions\HTTPException
	 */
	public static function forInvalidStatusCode(int $code)
	{
		return new static(lang('HTTP.invalidStatusCode', [$code]));
	}

	/**
	 * For Response
	 *
	 * @param integer $code
	 *
	 * @return \CodeIgniter\HTTP\Exceptions\HTTPException
	 */
	public static function forUnkownStatusCode(int $code)
	{
		return new static(lang('HTTP.unknownStatusCode', [$code]));
	}

	/**
	 * For URI
	 *
	 * @param string $uri
	 *
	 * @return \CodeIgniter\HTTP\Exceptions\HTTPException
	 */
	public static function forUnableToParseURI(string $uri)
	{
		return new static(lang('HTTP.cannotParseURI', [$uri]));
	}

	/**
	 * For URI
	 *
	 * @param integer $segment
	 *
	 * @return \CodeIgniter\HTTP\Exceptions\HTTPException
	 */
	public static function forURISegmentOutOfRange(int $segment)
	{
		return new static(lang('HTTP.segmentOutOfRange', [$segment]));
	}

	/**
	 * For URI
	 *
	 * @param integer $port
	 *
	 * @return \CodeIgniter\HTTP\Exceptions\HTTPException
	 */
	public static function forInvalidPort(int $port)
	{
		return new static(lang('HTTP.invalidPort', [$port]));
	}

	/**
	 * For URI
	 *
	 * @return \CodeIgniter\HTTP\Exceptions\HTTPException
	 */
	public static function forMalformedQueryString()
	{
		return new static(lang('HTTP.malformedQueryString'));
	}

	/**
	 * For Uploaded file move
	 *
	 * @return \CodeIgniter\HTTP\Exceptions\HTTPException
	 */
	public static function forAlreadyMoved()
	{
		return new static(lang('HTTP.alreadyMoved'));
	}

	/**
	 * For Uploaded file move
	 *
	 * @param string|null $path
	 *
	 * @return \CodeIgniter\HTTP\Exceptions\HTTPException
	 */
	public static function forInvalidFile(string $path = null)
	{
		return new static(lang('HTTP.invalidFile'));
	}

	/**
	 * For Uploaded file move
	 *
	 * @return \CodeIgniter\HTTP\Exceptions\HTTPException
	 */
	public static function forMoveFailed(string $source, string $target, string $error)
	{
		return new static(lang('HTTP.moveFailed', [$source, $target, $error]));
	}

}


<?php

/**
 * CodeIgniter
 *
 * An open source application development framework for PHP
 *
 * This content is released under the MIT License (MIT)
 *
 * Copyright (c) 2014-2019 British Columbia Institute of Technology
 *
 * 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.
 *
 * @package    CodeIgniter
 * @author     CodeIgniter Dev Team
 * @copyright  2014-2019 British Columbia Institute of Technology (https://bcit.ca/)
 * @license    https://opensource.org/licenses/MIT	MIT License
 * @link       https://codeigniter.com
 * @since      Version 3.0.0
 * @filesource
 */

namespace CodeIgniter\Validation;

/**
 * Class CreditCardRules
 *
 * Provides validation methods for common credit-card inputs.
 *
 * @see http://en.wikipedia.org/wiki/Credit_card_number
 *
 * @package CodeIgniter\Validation
 */
class CreditCardRules
{

	/**
	 * The cards that we support, with the defining details:
	 *
	 *  name        - The type of card as found in the form. Must match the user's value
	 *  length      - List of possible lengths for the card number
	 *  prefixes    - List of possible prefixes for the card
	 *  checkdigit  - Boolean on whether we should do a modulus10 check on the numbers.
	 *
	 * @var array
	 */
	protected $cards = [
		'American Express'                 => [
			'name'       => 'amex',
			'length'     => '15',
			'prefixes'   => '34,37',
			'checkdigit' => true,
		],
		'China UnionPay'                   => [
			'name'       => 'unionpay',
			'length'     => '16,17,18,19',
			'prefixes'   => '62',
			'checkdigit' => true,
		],
		'Dankort'                          => [
			'name'       => 'dankort',
			'length'     => '16',
			'prefixes'   => '5019,4175,4571,4',
			'checkdigit' => true,
		],
		'DinersClub'                       => [
			'name'       => 'dinersclub',
			'length'     => '14,16',
			'prefixes'   => '300,301,302,303,304,305,309,36,38,39,54,55',
			'checkdigit' => true,
		],
		'DinersClub CarteBlanche'          => [
			'name'       => 'carteblanche',
			'length'     => '14',
			'prefixes'   => '300,301,302,303,304,305',
			'checkdigit' => true,
		],
		'Discover Card'                    => [
			'name'       => 'discover',
			'length'     => '16,19',
			'prefixes'   => '6011,622,644,645,656,647,648,649,65',
			'checkdigit' => true,
		],
		'InterPayment'                     => [
			'name'       => 'interpayment',
			'length'     => '16,17,18,19',
			'prefixes'   => '4',
			'checkdigit' => true,
		],
		'JCB'                              => [
			'name'       => 'jcb',
			'length'     => '16,17,18,19',
			'prefixes'   => '352,353,354,355,356,357,358',
			'checkdigit' => true,
		],
		'Maestro'                          => [
			'name'       => 'maestro',
			'length'     => '12,13,14,15,16,18,19',
			'prefixes'   => '50,56,57,58,59,60,61,62,63,64,65,66,67,68,69',
			'checkdigit' => true,
		],
		'MasterCard'                       => [
			'name'       => 'mastercard',
			'length'     => '16',
			'prefixes'   => '51,52,53,54,55,22,23,24,25,26,27',
			'checkdigit' => true,
		],
		'NSPK MIR'                         => [
			'name'       => 'mir',
			'length'     => '16',
			'prefixes'   => '2200,2201,2202,2203,2204',
			'checkdigit' => true,
		],
		'Troy'                             => [
			'name'       => 'troy',
			'length'     => '16',
			'prefixes'   => '979200,979289',
			'checkdigit' => true,
		],
		'UATP'                             => [
			'name'       => 'uatp',
			'length'     => '15',
			'prefixes'   => '1',
			'checkdigit' => true,
		],
		'Verve'                            => [
			'name'       => 'verve',
			'length'     => '16,19',
			'prefixes'   => '506,650',
			'checkdigit' => true,
		],
		'Visa'                             => [
			'name'       => 'visa',
			'length'     => '13,16,19',
			'prefixes'   => '4',
			'checkdigit' => true,
		],
		// Canadian Cards
		'BMO ABM Card'                     => [
			'name'       => 'bmoabm',
			'length'     => '16',
			'prefixes'   => '500',
			'checkdigit' => false,
		],
		'CIBC Convenience Card'            => [
			'name'       => 'cibc',
			'length'     => '16',
			'prefixes'   => '4506',
			'checkdigit' => false,
		],
		'HSBC Canada Card'                 => [
			'name'       => 'hsbc',
			'length'     => '16',
			'prefixes'   => '56',
			'checkdigit' => false,
		],
		'Royal Bank of Canada Client Card' => [
			'name'       => 'rbc',
			'length'     => '16',
			'prefixes'   => '45',
			'checkdigit' => false,
		],
		'Scotiabank Scotia Card'           => [
			'name'       => 'scotia',
			'length'     => '16',
			'prefixes'   => '4536',
			'checkdigit' => false,
		],
		'TD Canada Trust Access Card'      => [
			'name'       => 'tdtrust',
			'length'     => '16',
			'prefixes'   => '589297',
			'checkdigit' => false,
		],
	];

	//--------------------------------------------------------------------

	/**
	 * Verifies that a credit card number is valid and matches the known
	 * formats for a wide number of credit card types. This does not verify
	 * that the card is a valid card, only that the number is formatted correctly.
	 *
	 * Example:
	 *  $rules = [
	 *      'cc_num' => 'valid_cc_number[visa]'
	 *  ];
	 *
	 * @param string $ccNumber
	 * @param string $type
	 * @param array  $data
	 *
	 * @return boolean
	 */
	public function valid_cc_number(string $ccNumber = null, string $type, array $data): bool
	{
		$type = strtolower($type);
		$info = null;

		// Get our card info based on provided name.
		foreach ($this->cards as $card)
		{
			if ($card['name'] === $type)
			{
				$info = $card;
				break;
			}
		}

		// If empty, it's not a card type we recognize, or invalid type.
		if (empty($info))
		{
			return false;
		}

		// Make sure we have a valid length
		if (strlen($ccNumber) === 0)
		{
			return false;
		}

		// Remove any spaces and dashes
		$ccNumber = str_replace([' ', '-'], '', $ccNumber);

		// Non-numeric values cannot be a number...duh
		if (! is_numeric($ccNumber))
		{
			return false;
		}

		// Make sure it's a valid length for this card
		$lengths = explode(',', $info['length']);

		if (! in_array(strlen($ccNumber), $lengths))
		{
			return false;
		}

		// Make sure it has a valid prefix
		$prefixes = explode(',', $info['prefixes']);

		$validPrefix = false;

		foreach ($prefixes as $prefix)
		{
			if (strpos($ccNumber, $prefix) === 0)
			{
							  $validPrefix = true;
							  break;
			}
		}

		if ($validPrefix === false)
		{
			return false;
		}

		// Still here? Then check the number against the Luhn algorithm, if required
		// @see https://en.wikipedia.org/wiki/Luhn_algorithm
		// @see https://gist.github.com/troelskn/1287893
		if ($info['checkdigit'] === true)
		{
			return $this->isValidLuhn($ccNumber);
		}

		return true;
	}

	//--------------------------------------------------------------------

	/**
	 * Checks the given number to see if the number passing a Luhn check.
	 *
	 * @param string $number
	 *
	 * @return boolean
	 */
	protected function isValidLuhn(string $number = null): bool
	{
		settype($number, 'string');

		$sumTable = [
			[
				0,
				1,
				2,
				3,
				4,
				5,
				6,
				7,
				8,
				9,
			],
			[
				0,
				2,
				4,
				6,
				8,
				1,
				3,
				5,
				7,
				9,
			],
		];

		$sum  = 0;
		$flip = 0;

		for ($i = strlen($number) - 1; $i >= 0; $i --)
		{
			$sum += $sumTable[$flip ++ & 0x1][$number[$i]];
		}

		return $sum % 10 === 0;
	}

	//--------------------------------------------------------------------
}


<?php

/**
 * CodeIgniter
 *
 * An open source application development framework for PHP
 *
 * This content is released under the MIT License (MIT)
 *
 * Copyright (c) 2014-2019 British Columbia Institute of Technology
 *
 * 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.
 *
 * @package    CodeIgniter
 * @author     CodeIgniter Dev Team
 * @copyright  2014-2019 British Columbia Institute of Technology (https://bcit.ca/)
 * @license    https://opensource.org/licenses/MIT	MIT License
 * @link       https://codeigniter.com
 * @since      Version 3.0.0
 * @filesource
 */

namespace CodeIgniter;

use ReflectionClass;

/**
 * ComposerScripts
 *
 * These scripts are used by Composer during installs and updates
 * to move files to locations within the system folder so that end-users
 * do not need to use Composer to install a package, but can simply
 * download
 *
 * @codeCoverageIgnore
 * @package            CodeIgniter
 */
class ComposerScripts
{
	protected static $basePath = 'ThirdParty/';

	/**
	 * After composer install/update, this is called to move
	 * the bare-minimum required files for our dependencies
	 * to appropriate locations.
	 *
	 * @throws \ReflectionException
	 */
	public static function postUpdate()
	{
		static::moveEscaper();
		static::moveKint();
	}

	//--------------------------------------------------------------------

	/**
	 * Move a file.
	 *
	 * @param string $source
	 * @param string $destination
	 *
	 * @return boolean
	 */
	protected static function moveFile(string $source, string $destination): bool
	{
		$source = realpath($source);

		if (empty($source))
		{
			die('Cannot move file. Source path invalid.');
		}

		if (! is_file($source))
		{
			return false;
		}

		return copy($source, $destination);
	}

	//--------------------------------------------------------------------

	/**
	 * Determine file path of a class.
	 *
	 * @param string $class
	 *
	 * @return string
	 * @throws \ReflectionException
	 */
	protected static function getClassFilePath(string $class)
	{
		$reflector = new ReflectionClass($class);

		return $reflector->getFileName();
	}

	//--------------------------------------------------------------------

	/**
	 * A recursive remove directory method.
	 *
	 * @param $dir
	 */
	protected static function removeDir($dir)
	{
		if (is_dir($dir))
		{
			$objects = scandir($dir);
			foreach ($objects as $object)
			{
				if ($object !== '.' && $object !== '..')
				{
					if (filetype($dir . '/' . $object) === 'dir')
					{
						static::removeDir($dir . '/' . $object);
					}
					else
					{
						unlink($dir . '/' . $object);
					}
				}
			}
			reset($objects);
			rmdir($dir);
		}
	}

	/**
	 * Moves the Zend Escaper files into our base repo so that it's
	 * available for packaged releases where the users don't user Composer.
	 *
	 * @throws \ReflectionException
	 */
	public static function moveEscaper()
	{
		if (class_exists('\\Zend\\Escaper\\Escaper') && is_file(static::getClassFilePath('\\Zend\\Escaper\\Escaper')))
		{
			$base = basename(__DIR__) . '/' . static::$basePath . 'ZendEscaper';

			foreach ([$base, $base . '/Exception'] as $path)
			{
				if (! is_dir($path))
				{
					mkdir($path, 0755);
				}
			}

			$files = [
				static::getClassFilePath('\\Zend\\Escaper\\Exception\\ExceptionInterface')       => $base . '/Exception/ExceptionInterface.php',
				static::getClassFilePath('\\Zend\\Escaper\\Exception\\InvalidArgumentException') => $base . '/Exception/InvalidArgumentException.php',
				static::getClassFilePath('\\Zend\\Escaper\\Exception\\RuntimeException')         => $base . '/Exception/RuntimeException.php',
				static::getClassFilePath('\\Zend\\Escaper\\Escaper')                             => $base . '/Escaper.php',
			];

			foreach ($files as $source => $dest)
			{
				if (! static::moveFile($source, $dest))
				{
					die('Error moving: ' . $source);
				}
			}
		}
	}

	//--------------------------------------------------------------------

	/**
	 * Moves the Kint file into our base repo so that it's
	 * available for packaged releases where the users don't user Composer.
	 */
	public static function moveKint()
	{
		$filename = 'vendor/kint-php/kint/build/kint-aante-light.php';

		if (is_file($filename))
		{
			$base = basename(__DIR__) . '/' . static::$basePath . 'Kint';

			// Remove the contents of the previous Kint folder, if any.
			if (is_dir($base))
			{
				static::removeDir($base);
			}

			// Create Kint if it doesn't exist already
			if (! is_dir($base))
			{
				mkdir($base, 0755);
			}

			if (! static::moveFile($filename, $base . '/kint.php'))
			{
				die('Error moving: ' . $filename);
			}
		}
	}
}