<?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;

use Psr\Log\LoggerAwareTrait;

/**
 * Implementation of CodeIgniter session container.
 *
 * Session configuration is done through session variables and cookie related
 * variables in app/config/App.php
 */
class Session implements SessionInterface
{

	use LoggerAwareTrait;

	/**
	 * Instance of the driver to use.
	 *
	 * @var \CodeIgniter\Log\Handlers\HandlerInterface
	 */
	protected $driver;

	/**
	 * The storage driver to use: files, database, redis, memcached
	 *
	 * @var string
	 */
	protected $sessionDriverName;

	/**
	 * The session cookie name, must contain only [0-9a-z_-] characters.
	 *
	 * @var string
	 */
	protected $sessionCookieName = 'ci_session';

	/**
	 * The number of SECONDS you want the session to last.
	 * Setting it to 0 (zero) means expire when the browser is closed.
	 *
	 * @var integer
	 */
	protected $sessionExpiration = 7200;

	/**
	 * The location to save sessions to, driver dependent..
	 *
	 * For the 'files' driver, it's a path to a writable directory.
	 * WARNING: Only absolute paths are supported!
	 *
	 * For the 'database' driver, it's a table name.
	 *
	 * TODO: address memcache & redis needs
	 *
	 * IMPORTANT: You are REQUIRED to set a valid save path!
	 *
	 * @var string
	 */
	protected $sessionSavePath = null;

	/**
	 * Whether to match the user's IP address when reading the session data.
	 *
	 * WARNING: If you're using the database driver, don't forget to update
	 * your session table's PRIMARY KEY when changing this setting.
	 *
	 * @var boolean
	 */
	protected $sessionMatchIP = false;

	/**
	 * How many seconds between CI regenerating the session ID.
	 *
	 * @var integer
	 */
	protected $sessionTimeToUpdate = 300;

	/**
	 * Whether to destroy session data associated with the old session ID
	 * when auto-regenerating the session ID. When set to FALSE, the data
	 * will be later deleted by the garbage collector.
	 *
	 * @var boolean
	 */
	protected $sessionRegenerateDestroy = false;

	/**
	 * The domain name to use for cookies.
	 * Set to .your-domain.com for site-wide cookies.
	 *
	 * @var string
	 */
	protected $cookieDomain = '';

	/**
	 * Path used for storing cookies.
	 * Typically will be a forward slash.
	 *
	 * @var string
	 */
	protected $cookiePath = '/';

	/**
	 * Cookie will only be set if a secure HTTPS connection exists.
	 *
	 * @var boolean
	 */
	protected $cookieSecure = false;

	/**
	 * sid regex expression
	 *
	 * @var string
	 */
	protected $sidRegexp;

	/**
	 * Logger instance to record error messages and warnings.
	 *
	 * @var \PSR\Log\LoggerInterface
	 */
	protected $logger;

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

	/**
	 * Constructor.
	 *
	 * Extract configuration settings and save them here.
	 *
	 * @param \SessionHandlerInterface $driver
	 * @param \Config\App              $config
	 */
	public function __construct(\SessionHandlerInterface $driver, $config)
	{
		$this->driver = $driver;

		$this->sessionDriverName        = $config->sessionDriver;
		$this->sessionCookieName        = $config->sessionCookieName;
		$this->sessionExpiration        = $config->sessionExpiration;
		$this->sessionSavePath          = $config->sessionSavePath;
		$this->sessionMatchIP           = $config->sessionMatchIP;
		$this->sessionTimeToUpdate      = $config->sessionTimeToUpdate;
		$this->sessionRegenerateDestroy = $config->sessionRegenerateDestroy;

		$this->cookieDomain = $config->cookieDomain;
		$this->cookiePath   = $config->cookiePath;
		$this->cookieSecure = $config->cookieSecure;

		helper('array');
	}

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

	/**
	 * Initialize the session container and starts up the session.
	 *
	 * @return mixed
	 */
	public function start()
	{
		if (is_cli() && ENVIRONMENT !== 'testing')
		{
			$this->logger->debug('Session: Initialization under CLI aborted.');

			return;
		}
		elseif ((bool) ini_get('session.auto_start'))
		{
			$this->logger->error('Session: session.auto_start is enabled in php.ini. Aborting.');

			return;
		}
		elseif (session_status() === PHP_SESSION_ACTIVE)
		{
			$this->logger->warning('Session: Sessions is enabled, and one exists.Please don\'t $session->start();');

			return;
		}

		if (! $this->driver instanceof \SessionHandlerInterface)
		{
			$this->logger->error("Session: Handler '" . $this->driver .
					"' doesn't implement SessionHandlerInterface. Aborting.");
		}

		$this->configure();

		$this->setSaveHandler();

		// Sanitize the cookie, because apparently PHP doesn't do that for userspace handlers
		if (isset($_COOKIE[$this->sessionCookieName]) && (
				! is_string($_COOKIE[$this->sessionCookieName]) || ! preg_match('#\A' . $this->sidRegexp . '\z#', $_COOKIE[$this->sessionCookieName])
				)
		)
		{
			unset($_COOKIE[$this->sessionCookieName]);
		}

		$this->startSession();

		// Is session ID auto-regeneration configured? (ignoring ajax requests)
		if ((empty($_SERVER['HTTP_X_REQUESTED_WITH']) ||
				strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) !== 'xmlhttprequest') && ($regenerate_time = $this->sessionTimeToUpdate) > 0
		)
		{
			if (! isset($_SESSION['__ci_last_regenerate']))
			{
				$_SESSION['__ci_last_regenerate'] = time();
			}
			elseif ($_SESSION['__ci_last_regenerate'] < (time() - $regenerate_time))
			{
				$this->regenerate((bool) $this->sessionRegenerateDestroy);
			}
		}
		// Another work-around ... PHP doesn't seem to send the session cookie
		// unless it is being currently created or regenerated
		elseif (isset($_COOKIE[$this->sessionCookieName]) && $_COOKIE[$this->sessionCookieName] === session_id())
		{
			$this->setCookie();
		}

		$this->initVars();

		$this->logger->info("Session: Class initialized using '" . $this->sessionDriverName . "' driver.");

		return $this;
	}

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

	/**
	 * Does a full stop of the session:
	 *
	 * - destroys the session
	 * - unsets the session id
	 * - destroys the session cookie
	 */
	public function stop()
	{
		setcookie(
				$this->sessionCookieName, session_id(), 1, $this->cookiePath, $this->cookieDomain, $this->cookieSecure, true
		);

		session_regenerate_id(true);
	}

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

	/**
	 * Configuration.
	 *
	 * Handle input binds and configuration defaults.
	 */
	protected function configure()
	{
		if (empty($this->sessionCookieName))
		{
			$this->sessionCookieName = ini_get('session.name');
		}
		else
		{
			ini_set('session.name', $this->sessionCookieName);
		}

		session_set_cookie_params(
				$this->sessionExpiration, $this->cookiePath, $this->cookieDomain, $this->cookieSecure, true // HTTP only; Yes, this is intentional and not configurable for security reasons.
		);

		if (empty($this->sessionExpiration))
		{
			$this->sessionExpiration = (int) ini_get('session.gc_maxlifetime');
		}
		else
		{
			ini_set('session.gc_maxlifetime', (int) $this->sessionExpiration);
		}

		if (! empty($this->sessionSavePath))
		{
			ini_set('session.save_path', $this->sessionSavePath);
		}

		// Security is king
		ini_set('session.use_trans_sid', 0);
		ini_set('session.use_strict_mode', 1);
		ini_set('session.use_cookies', 1);
		ini_set('session.use_only_cookies', 1);

		$this->configureSidLength();
	}

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

	/**
	 * Configure session ID length
	 *
	 * To make life easier, we used to force SHA-1 and 4 bits per
	 * character on everyone. And of course, someone was unhappy.
	 *
	 * Then PHP 7.1 broke backwards-compatibility because ext/session
	 * is such a mess that nobody wants to touch it with a pole stick,
	 * and the one guy who does, nobody has the energy to argue with.
	 *
	 * So we were forced to make changes, and OF COURSE something was
	 * going to break and now we have this pile of shit. -- Narf
	 *
	 * @return void
	 */
	protected function configureSidLength()
	{
		$bits_per_character = (int) (ini_get('session.sid_bits_per_character') !== false
			? ini_get('session.sid_bits_per_character')
			: 4);
		$sid_length         = (int) (ini_get('session.sid_length') !== false
			? ini_get('session.sid_length')
			: 40);
		if (($sid_length * $bits_per_character) < 160)
		{
			$bits = ($sid_length * $bits_per_character);
			// Add as many more characters as necessary to reach at least 160 bits
			$sid_length += (int) ceil((160 % $bits) / $bits_per_character);
			ini_set('session.sid_length', $sid_length);
		}

		// Yes, 4,5,6 are the only known possible values as of 2016-10-27
		switch ($bits_per_character)
		{
			case 4:
				$this->sidRegexp = '[0-9a-f]';
				break;
			case 5:
				$this->sidRegexp = '[0-9a-v]';
				break;
			case 6:
				$this->sidRegexp = '[0-9a-zA-Z,-]';
				break;
		}

		$this->sidRegexp .= '{' . $sid_length . '}';
	}

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

	/**
	 * Handle temporary variables
	 *
	 * Clears old "flash" data, marks the new one for deletion and handles
	 * "temp" data deletion.
	 */
	protected function initVars()
	{
		if (empty($_SESSION['__ci_vars']))
		{
			return;
		}

		$current_time = time();

		foreach ($_SESSION['__ci_vars'] as $key => &$value)
		{
			if ($value === 'new')
			{
				$_SESSION['__ci_vars'][$key] = 'old';
			}
			// Hacky, but 'old' will (implicitly) always be less than time() ;)
			// DO NOT move this above the 'new' check!
			elseif ($value < $current_time)
			{
				unset($_SESSION[$key], $_SESSION['__ci_vars'][$key]);
			}
		}

		if (empty($_SESSION['__ci_vars']))
		{
			unset($_SESSION['__ci_vars']);
		}
	}

	//--------------------------------------------------------------------
	//--------------------------------------------------------------------
	// Session Utility Methods
	//--------------------------------------------------------------------

	/**
	 * Regenerates the session ID.
	 *
	 * @param boolean $destroy Should old session data be destroyed?
	 */
	public function regenerate(bool $destroy = false)
	{
		$_SESSION['__ci_last_regenerate'] = time();
		session_regenerate_id($destroy);
	}

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

	/**
	 * Destroys the current session.
	 */
	public function destroy()
	{
		session_destroy();
	}

	//--------------------------------------------------------------------
	//--------------------------------------------------------------------
	// Basic Setters and Getters
	//--------------------------------------------------------------------

	/**
	 * Sets user data into the session.
	 *
	 * If $data is a string, then it is interpreted as a session property
	 * key, and  $value is expected to be non-null.
	 *
	 * If $data is an array, it is expected to be an array of key/value pairs
	 * to be set as session properties.
	 *
	 * @param string|array $data  Property name or associative array of properties
	 * @param string|array $value Property value if single key provided
	 */
	public function set($data, $value = null)
	{
		if (is_array($data))
		{
			foreach ($data as $key => &$value)
			{
				if (is_int($key))
				{
					$_SESSION[$value] = null;
				}
				else
				{
					$_SESSION[$key] = $value;
				}
			}

			return;
		}

		$_SESSION[$data] = $value;
	}

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

	/**
	 * Get user data that has been set in the session.
	 *
	 * If the property exists as "normal", returns it.
	 * Otherwise, returns an array of any temp or flash data values with the
	 * property key.
	 *
	 * Replaces the legacy method $session->userdata();
	 *
	 * @param  string $key Identifier of the session property to retrieve
	 * @return array|null	The property value(s)
	 */
	public function get(string $key = null)
	{
		if (! empty($key) && $value = dot_array_search($key, $_SESSION))
		{
			return $value;
		}
		elseif (empty($_SESSION))
		{
			return [];
		}

		if (! empty($key))
		{
			return null;
		}

		$userdata = [];
		$_exclude = array_merge(
			['__ci_vars'], $this->getFlashKeys(), $this->getTempKeys()
		);

		$keys = array_keys($_SESSION);
		foreach ($keys as $key)
		{
			if (! in_array($key, $_exclude, true))
			{
				$userdata[$key] = $_SESSION[$key];
			}
		}

		return $userdata;
	}

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

	/**
	 * Returns whether an index exists in the session array.
	 *
	 * @param string $key Identifier of the session property we are interested in.
	 *
	 * @return boolean
	 */
	public function has(string $key): bool
	{
		return isset($_SESSION[$key]);
	}

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

	  /**
	   * Push new value onto session value that is array.
	   *
	   * @param string $key  Identifier of the session property we are interested in.
	   * @param array  $data value to be pushed to existing session key.
	   *
	   * @return void
	   */
	public function push(string $key, array $data)
	{
		if ($this->has($key) && is_array($value = $this->get($key)))
			   {
			$this->set($key, array_merge($value, $data));
		}
	}

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

	/**
	 * Remove one or more session properties.
	 *
	 * If $key is an array, it is interpreted as an array of string property
	 * identifiers to remove. Otherwise, it is interpreted as the identifier
	 * of a specific session property to remove.
	 *
	 * @param string|array $key Identifier of the session property or properties to remove.
	 */
	public function remove($key)
	{
		if (is_array($key))
		{
			foreach ($key as $k)
			{
				unset($_SESSION[$k]);
			}

			return;
		}

		unset($_SESSION[$key]);
	}

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

	/**
	 * Magic method to set variables in the session by simply calling
	 *  $session->foo = bar;
	 *
	 * @param string        $key    Identifier of the session property to set.
	 * @param string|array  $value
	 */
	public function __set(string $key, $value)
	{
		$_SESSION[$key] = $value;
	}

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

	/**
	 * Magic method to get session variables by simply calling
	 *  $foo = $session->foo;
	 *
	 * @param string $key Identifier of the session property to remove.
	 *
	 * @return null|string
	 */
	public function __get(string $key)
	{
		// Note: Keep this order the same, just in case somebody wants to
		//       use 'session_id' as a session data key, for whatever reason
		if (isset($_SESSION[$key]))
		{
			return $_SESSION[$key];
		}
		elseif ($key === 'session_id')
		{
			return session_id();
		}

		return null;
	}

	//--------------------------------------------------------------------
	//--------------------------------------------------------------------
	// Flash Data Methods
	//--------------------------------------------------------------------

	/**
	 * Sets data into the session that will only last for a single request.
	 * Perfect for use with single-use status update messages.
	 *
	 * If $data is an array, it is interpreted as an associative array of
	 * key/value pairs for flashdata properties.
	 * Otherwise, it is interpreted as the identifier of a specific
	 * flashdata property, with $value containing the property value.
	 *
	 * @param array|string $data  Property identifier or associative array of properties
	 * @param string|array $value Property value if $data is a scalar
	 */
	public function setFlashdata($data, $value = null)
	{
		$this->set($data, $value);
		$this->markAsFlashdata(is_array($data) ? array_keys($data) : $data);
	}

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

	/**
	 * Retrieve one or more items of flash data from the session.
	 *
	 * If the item key is null, return all flashdata.
	 *
	 * @param  string $key Property identifier
	 * @return array|null	The requested property value, or an associative array  of them
	 */
	public function getFlashdata(string $key = null)
	{
		if (isset($key))
		{
			return (isset($_SESSION['__ci_vars'], $_SESSION['__ci_vars'][$key], $_SESSION[$key]) &&
					! is_int($_SESSION['__ci_vars'][$key])) ? $_SESSION[$key] : null;
		}

		$flashdata = [];

		if (! empty($_SESSION['__ci_vars']))
		{
			foreach ($_SESSION['__ci_vars'] as $key => &$value)
			{
				is_int($value) || $flashdata[$key] = $_SESSION[$key];
			}
		}

		return $flashdata;
	}

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

	/**
	 * Keeps a single piece of flash data alive for one more request.
	 *
	 * @param array|string $key Property identifier or array of them
	 */
	public function keepFlashdata($key)
	{
		$this->markAsFlashdata($key);
	}

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

	/**
	 * Mark a session property or properties as flashdata.
	 *
	 * @param array|string $key Property identifier or array of them
	 *
	 * @return boolean False if any of the properties are not already set
	 */
	public function markAsFlashdata($key): bool
	{
		if (is_array($key))
		{
			for ($i = 0, $c = count($key); $i < $c; $i ++)
			{
				if (! isset($_SESSION[$key[$i]]))
				{
					return false;
				}
			}

			$new = array_fill_keys($key, 'new');

			$_SESSION['__ci_vars'] = isset($_SESSION['__ci_vars']) ? array_merge($_SESSION['__ci_vars'], $new) : $new;

			return true;
		}

		if (! isset($_SESSION[$key]))
		{
			return false;
		}

		$_SESSION['__ci_vars'][$key] = 'new';

		return true;
	}

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

	/**
	 * Unmark data in the session as flashdata.
	 *
	 * @param mixed $key Property identifier or array of them
	 */
	public function unmarkFlashdata($key)
	{
		if (empty($_SESSION['__ci_vars']))
		{
			return;
		}

		is_array($key) || $key = [$key];

		foreach ($key as $k)
		{
			if (isset($_SESSION['__ci_vars'][$k]) && ! is_int($_SESSION['__ci_vars'][$k]))
			{
				unset($_SESSION['__ci_vars'][$k]);
			}
		}

		if (empty($_SESSION['__ci_vars']))
		{
			unset($_SESSION['__ci_vars']);
		}
	}

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

	/**
	 * Retrieve all of the keys for session data marked as flashdata.
	 *
	 * @return array	The property names of all flashdata
	 */
	public function getFlashKeys(): array
	{
		if (! isset($_SESSION['__ci_vars']))
		{
			return [];
		}

		$keys = [];
		foreach (array_keys($_SESSION['__ci_vars']) as $key)
		{
			is_int($_SESSION['__ci_vars'][$key]) || $keys[] = $key;
		}

		return $keys;
	}

	//--------------------------------------------------------------------
	//--------------------------------------------------------------------
	// Temp Data Methods
	//--------------------------------------------------------------------

	/**
	 * Sets new data into the session, and marks it as temporary data
	 * with a set lifespan.
	 *
	 * @param string|array $data  Session data key or associative array of items
	 * @param null         $value Value to store
	 * @param integer      $ttl   Time-to-live in seconds
	 */
	public function setTempdata($data, $value = null, int $ttl = 300)
	{
		$this->set($data, $value);
		$this->markAsTempdata($data, $ttl);
	}

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

	/**
	 * Returns either a single piece of tempdata, or all temp data currently
	 * in the session.
	 *
	 * @param  string   $key   Session data key
	 * @return mixed  Session data value or null if not found.
	 */
	public function getTempdata(string $key = null)
	{
		if (isset($key))
		{
			return (isset($_SESSION['__ci_vars'], $_SESSION['__ci_vars'][$key], $_SESSION[$key]) &&
					is_int($_SESSION['__ci_vars'][$key])) ? $_SESSION[$key] : null;
		}

		$tempdata = [];

		if (! empty($_SESSION['__ci_vars']))
		{
			foreach ($_SESSION['__ci_vars'] as $key => &$value)
			{
				is_int($value) && $tempdata[$key] = $_SESSION[$key];
			}
		}

		return $tempdata;
	}

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

	/**
	 * Removes a single piece of temporary data from the session.
	 *
	 * @param string $key Session data key
	 */
	public function removeTempdata(string $key)
	{
		$this->unmarkTempdata($key);
		unset($_SESSION[$key]);
	}

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

	/**
	 * Mark one of more pieces of data as being temporary, meaning that
	 * it has a set lifespan within the session.
	 *
	 * @param string|array $key Property identifier or array of them
	 * @param integer      $ttl Time to live, in seconds
	 *
	 * @return boolean    False if any of the properties were not set
	 */
	public function markAsTempdata($key, int $ttl = 300): bool
	{
		$ttl += time();

		if (is_array($key))
		{
			$temp = [];

			foreach ($key as $k => $v)
			{
				// Do we have a key => ttl pair, or just a key?
				if (is_int($k))
				{
					$k = $v;
					$v = $ttl;
				}
				elseif (is_string($v))
				{
					$v = time() + $ttl;
				}
				else
				{
					$v += time();
				}

				if (! array_key_exists($k, $_SESSION))
				{
					return false;
				}

				$temp[$k] = $v;
			}

			$_SESSION['__ci_vars'] = isset($_SESSION['__ci_vars']) ? array_merge($_SESSION['__ci_vars'], $temp) : $temp;

			return true;
		}

		if (! isset($_SESSION[$key]))
		{
			return false;
		}

		$_SESSION['__ci_vars'][$key] = $ttl;

		return true;
	}

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

	/**
	 * Unmarks temporary data in the session, effectively removing its
	 * lifespan and allowing it to live as long as the session does.
	 *
	 * @param string|array $key	Property identifier or array of them
	 */
	public function unmarkTempdata($key)
	{
		if (empty($_SESSION['__ci_vars']))
		{
			return;
		}

		is_array($key) || $key = [$key];

		foreach ($key as $k)
		{
			if (isset($_SESSION['__ci_vars'][$k]) && is_int($_SESSION['__ci_vars'][$k]))
			{
				unset($_SESSION['__ci_vars'][$k]);
			}
		}

		if (empty($_SESSION['__ci_vars']))
		{
			unset($_SESSION['__ci_vars']);
		}
	}

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

	/**
	 * Retrieve the keys of all session data that have been marked as temporary data.
	 *
	 * @return array
	 */
	public function getTempKeys(): array
	{
		if (! isset($_SESSION['__ci_vars']))
		{
			return [];
		}

		$keys = [];
		foreach (array_keys($_SESSION['__ci_vars']) as $key)
		{
			is_int($_SESSION['__ci_vars'][$key]) && $keys[] = $key;
		}

		return $keys;
	}

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

	/**
	 * Sets the driver as the session handler in PHP.
	 * Extracted for easier testing.
	 */
	protected function setSaveHandler()
	{
		session_set_save_handler($this->driver, true);
	}

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

	/**
	 * Starts the session.
	 * Extracted for testing reasons.
	 */
	protected function startSession()
	{
		if (ENVIRONMENT === 'testing')
		{
			$_SESSION = [];
			return;
		}

		session_start();
	}

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

	/**
	 * Takes care of setting the cookie on the client side.
	 * Extracted for testing reasons.
	 */
	protected function setCookie()
	{
		setcookie(
				$this->sessionCookieName, session_id(), (empty($this->sessionExpiration) ? 0 : time() + $this->sessionExpiration), $this->cookiePath, $this->cookieDomain, $this->cookieSecure, 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\Throttle;

use CodeIgniter\Cache\CacheInterface;

/**
 * Class Throttler
 *
 * Uses an implementation of the Token Bucket algorithm to implement a
 * "rolling window" type of throttling that can be used for rate limiting
 * an API or any other request.
 *
 * Each "token" in the "bucket" is equivalent to a single request
 * for the purposes of this implementation.
 *
 * @see https://en.wikipedia.org/wiki/Token_bucket
 *
 * @package CodeIgniter\Throttle
 */
class Throttler implements ThrottlerInterface
{

	/**
	 * @var \CodeIgniter\Cache\CacheInterface
	 */
	protected $cache;

	/**
	 * The number of seconds until the next token is available.
	 *
	 * @var integer
	 */
	protected $tokenTime = 0;

	/**
	 * The prefix applied to all keys to
	 * minimize potential conflicts.
	 *
	 * @var string
	 */
	protected $prefix = 'throttler_';

	/**
	 * Timestamp to use (during testing)
	 *
	 * @var integer
	 */
	protected $testTime;

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

	public function __construct(CacheInterface $cache)
	{
		$this->cache = $cache;
	}

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

	/**
	 * Returns the number of seconds until the next available token will
	 * be released for usage.
	 *
	 * @return integer
	 */
	public function getTokenTime(): int
	{
		return $this->tokenTime;
	}

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

	/**
	 * Restricts the number of requests made by a single IP address within
	 * a set number of seconds.
	 *
	 * Example:
	 *
	 *  if (! $throttler->check($request->ipAddress(), 60, MINUTE))
	 * {
	 *      die('You submitted over 60 requests within a minute.');
	 * }
	 *
	 * @param string  $key      The name to use as the "bucket" name.
	 * @param integer $capacity The number of requests the "bucket" can hold
	 * @param integer $seconds  The time it takes the "bucket" to completely refill
	 * @param integer $cost     The number of tokens this action uses.
	 *
	 * @return   boolean
	 * @internal param int $maxRequests
	 */
	public function check(string $key, int $capacity, int $seconds, int $cost = 1): bool
	{
		$tokenName = $this->prefix . $key;

		// Check to see if the bucket has even been created yet.
		if (($tokens = $this->cache->get($tokenName)) === false)
		{
			// If it hasn't been created, then we'll set it to the maximum
			// capacity - 1, and save it to the cache.
			$this->cache->save($tokenName, $capacity - $cost, $seconds);
			$this->cache->save($tokenName . 'Time', time());

			return true;
		}

		// If $tokens > 0, then we need to replenish the bucket
		// based on how long it's been since the last update.
		$throttleTime = $this->cache->get($tokenName . 'Time');
		$elapsed      = $this->time() - $throttleTime;
		// Number of tokens to add back per second
		$rate = $capacity / $seconds;

		// We must have a minimum wait of 1 second for a new token
		// Primarily stored to allow devs to report back to users.
		$this->tokenTime = max(1, $rate);

		// Add tokens based up on number per second that
		// should be refilled, then checked against capacity
		// to be sure the bucket didn't overflow.
		$tokens += $rate * $elapsed;
		$tokens  = $tokens > $capacity ? $capacity : $tokens;

		// If $tokens > 0, then we are save to perform the action, but
		// we need to decrement the number of available tokens.
		$response = false;

		if ($tokens > 0)
		{
			$response = true;

			$this->cache->save($tokenName, $tokens - $cost, $elapsed);
			$this->cache->save($tokenName . 'Time', time());
		}

		return $response;
	}

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

	/**
	 * Used during testing to set the current timestamp to use.
	 *
	 * @param integer $time
	 *
	 * @return $this
	 */
	public function setTestTime(int $time)
	{
		$this->testTime = $time;

		return $this;
	}

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

	/**
	 *
	 *
	 * @return integer
	 */
	public function time(): int
	{
		return $this->testTime ?? time();
	}

}


<?php namespace Config;

/*
| -------------------------------------------------------------------
| MIME TYPES
| -------------------------------------------------------------------
| This file contains an array of mime types.  It is used by the
| Upload class to help identify allowed file types.
|
| When more than one variation for an extension exist (like jpg, jpeg, etc)
| the most common one should be first in the array to aid the guess*
| methods. The same applies when more than one mime-type exists for a
| single extension.
|
*/
class Mimes
{
	/**
	 * Map of extensions to mime types.
	 *
	 * @var array
	 */
	public static $mimes = [
		'hqx'   => [
			'application/mac-binhex40',
			'application/mac-binhex',
			'application/x-binhex40',
			'application/x-mac-binhex40',
		],
		'cpt'   => 'application/mac-compactpro',
		'csv'   => [
			'text/csv',
			'text/x-comma-separated-values',
			'text/comma-separated-values',
			'application/octet-stream',
			'application/vnd.ms-excel',
			'application/x-csv',
			'text/x-csv',
			'application/csv',
			'application/excel',
			'application/vnd.msexcel',
			'text/plain',
		],
		'bin'   => [
			'application/macbinary',
			'application/mac-binary',
			'application/octet-stream',
			'application/x-binary',
			'application/x-macbinary',
		],
		'dms'   => 'application/octet-stream',
		'lha'   => 'application/octet-stream',
		'lzh'   => 'application/octet-stream',
		'exe'   => [
			'application/octet-stream',
			'application/x-msdownload',
		],
		'class' => 'application/octet-stream',
		'psd'   => [
			'application/x-photoshop',
			'image/vnd.adobe.photoshop',
		],
		'so'    => 'application/octet-stream',
		'sea'   => 'application/octet-stream',
		'dll'   => 'application/octet-stream',
		'oda'   => 'application/oda',
		'pdf'   => [
			'application/pdf',
			'application/force-download',
			'application/x-download',
			'binary/octet-stream',
		],
		'ai'    => [
			'application/pdf',
			'application/postscript',
		],
		'eps'   => 'application/postscript',
		'ps'    => 'application/postscript',
		'smi'   => 'application/smil',
		'smil'  => 'application/smil',
		'mif'   => 'application/vnd.mif',
		'xls'   => [
			'application/vnd.ms-excel',
			'application/msexcel',
			'application/x-msexcel',
			'application/x-ms-excel',
			'application/x-excel',
			'application/x-dos_ms_excel',
			'application/xls',
			'application/x-xls',
			'application/excel',
			'application/download',
			'application/vnd.ms-office',
			'application/msword',
		],
		'ppt'   => [
			'application/vnd.ms-powerpoint',
			'application/powerpoint',
			'application/vnd.ms-office',
			'application/msword',
		],
		'pptx'  => [
			'application/vnd.openxmlformats-officedocument.presentationml.presentation',
			'application/x-zip',
			'application/zip',
		],
		'wbxml' => 'application/wbxml',
		'wmlc'  => 'application/wmlc',
		'dcr'   => 'application/x-director',
		'dir'   => 'application/x-director',
		'dxr'   => 'application/x-director',
		'dvi'   => 'application/x-dvi',
		'gtar'  => 'application/x-gtar',
		'gz'    => 'application/x-gzip',
		'gzip'  => 'application/x-gzip',
		'php'   => [
			'application/x-php',
			'application/x-httpd-php',
			'application/php',
			'text/php',
			'text/x-php',
			'application/x-httpd-php-source',
		],
		'php4'  => 'application/x-httpd-php',
		'php3'  => 'application/x-httpd-php',
		'phtml' => 'application/x-httpd-php',
		'phps'  => 'application/x-httpd-php-source',
		'js'    => [
			'application/x-javascript',
			'text/plain',
		],
		'swf'   => 'application/x-shockwave-flash',
		'sit'   => 'application/x-stuffit',
		'tar'   => 'application/x-tar',
		'tgz'   => [
			'application/x-tar',
			'application/x-gzip-compressed',
		],
		'z'     => 'application/x-compress',
		'xhtml' => 'application/xhtml+xml',
		'xht'   => 'application/xhtml+xml',
		'zip'   => [
			'application/x-zip',
			'application/zip',
			'application/x-zip-compressed',
			'application/s-compressed',
			'multipart/x-zip',
		],
		'rar'   => [
			'application/x-rar',
			'application/rar',
			'application/x-rar-compressed',
		],
		'mid'   => 'audio/midi',
		'midi'  => 'audio/midi',
		'mpga'  => 'audio/mpeg',
		'mp2'   => 'audio/mpeg',
		'mp3'   => [
			'audio/mpeg',
			'audio/mpg',
			'audio/mpeg3',
			'audio/mp3',
		],
		'aif'   => [
			'audio/x-aiff',
			'audio/aiff',
		],
		'aiff'  => [
			'audio/x-aiff',
			'audio/aiff',
		],
		'aifc'  => 'audio/x-aiff',
		'ram'   => 'audio/x-pn-realaudio',
		'rm'    => 'audio/x-pn-realaudio',
		'rpm'   => 'audio/x-pn-realaudio-plugin',
		'ra'    => 'audio/x-realaudio',
		'rv'    => 'video/vnd.rn-realvideo',
		'wav'   => [
			'audio/x-wav',
			'audio/wave',
			'audio/wav',
		],
		'bmp'   => [
			'image/bmp',
			'image/x-bmp',
			'image/x-bitmap',
			'image/x-xbitmap',
			'image/x-win-bitmap',
			'image/x-windows-bmp',
			'image/ms-bmp',
			'image/x-ms-bmp',
			'application/bmp',
			'application/x-bmp',
			'application/x-win-bitmap',
		],
		'gif'   => 'image/gif',
		'jpg'   => [
			'image/jpeg',
			'image/pjpeg',
		],
		'jpeg'  => [
			'image/jpeg',
			'image/pjpeg',
		],
		'jpe'   => [
			'image/jpeg',
			'image/pjpeg',
		],
		'jp2'   => [
			'image/jp2',
			'video/mj2',
			'image/jpx',
			'image/jpm',
		],
		'j2k'   => [
			'image/jp2',
			'video/mj2',
			'image/jpx',
			'image/jpm',
		],
		'jpf'   => [
			'image/jp2',
			'video/mj2',
			'image/jpx',
			'image/jpm',
		],
		'jpg2'  => [
			'image/jp2',
			'video/mj2',
			'image/jpx',
			'image/jpm',
		],
		'jpx'   => [
			'image/jp2',
			'video/mj2',
			'image/jpx',
			'image/jpm',
		],
		'jpm'   => [
			'image/jp2',
			'video/mj2',
			'image/jpx',
			'image/jpm',
		],
		'mj2'   => [
			'image/jp2',
			'video/mj2',
			'image/jpx',
			'image/jpm',
		],
		'mjp2'  => [
			'image/jp2',
			'video/mj2',
			'image/jpx',
			'image/jpm',
		],
		'png'   => [
			'image/png',
			'image/x-png',
		],
		'tif'   => 'image/tiff',
		'tiff'  => 'image/tiff',
		'css'   => [
			'text/css',
			'text/plain',
		],
		'html'  => [
			'text/html',
			'text/plain',
		],
		'htm'   => [
			'text/html',
			'text/plain',
		],
		'shtml' => [
			'text/html',
			'text/plain',
		],
		'txt'   => 'text/plain',
		'text'  => 'text/plain',
		'log'   => [
			'text/plain',
			'text/x-log',
		],
		'rtx'   => 'text/richtext',
		'rtf'   => 'text/rtf',
		'xml'   => [
			'application/xml',
			'text/xml',
			'text/plain',
		],
		'xsl'   => [
			'application/xml',
			'text/xsl',
			'text/xml',
		],
		'mpeg'  => 'video/mpeg',
		'mpg'   => 'video/mpeg',
		'mpe'   => 'video/mpeg',
		'qt'    => 'video/quicktime',
		'mov'   => 'video/quicktime',
		'avi'   => [
			'video/x-msvideo',
			'video/msvideo',
			'video/avi',
			'application/x-troff-msvideo',
		],
		'movie' => 'video/x-sgi-movie',
		'doc'   => [
			'application/msword',
			'application/vnd.ms-office',
		],
		'docx'  => [
			'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
			'application/zip',
			'application/msword',
			'application/x-zip',
		],
		'dot'   => [
			'application/msword',
			'application/vnd.ms-office',
		],
		'dotx'  => [
			'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
			'application/zip',
			'application/msword',
		],
		'xlsx'  => [
			'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
			'application/zip',
			'application/vnd.ms-excel',
			'application/msword',
			'application/x-zip',
		],
		'word'  => [
			'application/msword',
			'application/octet-stream',
		],
		'xl'    => 'application/excel',
		'eml'   => 'message/rfc822',
		'json'  => [
			'application/json',
			'text/json',
		],
		'pem'   => [
			'application/x-x509-user-cert',
			'application/x-pem-file',
			'application/octet-stream',
		],
		'p10'   => [
			'application/x-pkcs10',
			'application/pkcs10',
		],
		'p12'   => 'application/x-pkcs12',
		'p7a'   => 'application/x-pkcs7-signature',
		'p7c'   => [
			'application/pkcs7-mime',
			'application/x-pkcs7-mime',
		],
		'p7m'   => [
			'application/pkcs7-mime',
			'application/x-pkcs7-mime',
		],
		'p7r'   => 'application/x-pkcs7-certreqresp',
		'p7s'   => 'application/pkcs7-signature',
		'crt'   => [
			'application/x-x509-ca-cert',
			'application/x-x509-user-cert',
			'application/pkix-cert',
		],
		'crl'   => [
			'application/pkix-crl',
			'application/pkcs-crl',
		],
		'der'   => 'application/x-x509-ca-cert',
		'kdb'   => 'application/octet-stream',
		'pgp'   => 'application/pgp',
		'gpg'   => 'application/gpg-keys',
		'sst'   => 'application/octet-stream',
		'csr'   => 'application/octet-stream',
		'rsa'   => 'application/x-pkcs7',
		'cer'   => [
			'application/pkix-cert',
			'application/x-x509-ca-cert',
		],
		'3g2'   => 'video/3gpp2',
		'3gp'   => [
			'video/3gp',
			'video/3gpp',
		],
		'mp4'   => 'video/mp4',
		'm4a'   => 'audio/x-m4a',
		'f4v'   => [
			'video/mp4',
			'video/x-f4v',
		],
		'flv'   => 'video/x-flv',
		'webm'  => 'video/webm',
		'aac'   => 'audio/x-acc',
		'm4u'   => 'application/vnd.mpegurl',
		'm3u'   => 'text/plain',
		'xspf'  => 'application/xspf+xml',
		'vlc'   => 'application/videolan',
		'wmv'   => [
			'video/x-ms-wmv',
			'video/x-ms-asf',
		],
		'au'    => 'audio/x-au',
		'ac3'   => 'audio/ac3',
		'flac'  => 'audio/x-flac',
		'ogg'   => [
			'audio/ogg',
			'video/ogg',
			'application/ogg',
		],
		'kmz'   => [
			'application/vnd.google-earth.kmz',
			'application/zip',
			'application/x-zip',
		],
		'kml'   => [
			'application/vnd.google-earth.kml+xml',
			'application/xml',
			'text/xml',
		],
		'ics'   => 'text/calendar',
		'ical'  => 'text/calendar',
		'zsh'   => 'text/x-scriptzsh',
		'7zip'  => [
			'application/x-compressed',
			'application/x-zip-compressed',
			'application/zip',
			'multipart/x-zip',
		],
		'cdr'   => [
			'application/cdr',
			'application/coreldraw',
			'application/x-cdr',
			'application/x-coreldraw',
			'image/cdr',
			'image/x-cdr',
			'zz-application/zz-winassoc-cdr',
		],
		'wma'   => [
			'audio/x-ms-wma',
			'video/x-ms-asf',
		],
		'jar'   => [
			'application/java-archive',
			'application/x-java-application',
			'application/x-jar',
			'application/x-compressed',
		],
		'svg'   => [
			'image/svg+xml',
			'application/xml',
			'text/xml',
		],
		'vcf'   => 'text/x-vcard',
		'srt'   => [
			'text/srt',
			'text/plain',
		],
		'vtt'   => [
			'text/vtt',
			'text/plain',
		],
		'ico'   => [
			'image/x-icon',
			'image/x-ico',
			'image/vnd.microsoft.icon',
		],
	];

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

	/**
	 * Attempts to determine the best mime type for the given file extension.
	 *
	 * @param string $extension
	 *
	 * @return string|null The mime type found, or none if unable to determine.
	 */
	public static function guessTypeFromExtension(string $extension)
	{
		$extension = trim(strtolower($extension), '. ');

		if (! array_key_exists($extension, static::$mimes))
		{
			return null;
		}

		return is_array(static::$mimes[$extension]) ? static::$mimes[$extension][0] : static::$mimes[$extension];
	}

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

	/**
	 * Attempts to determine the best file extension for a given mime type.
	 *
	 * @param string $type
	 * @param string $proposed_extension - default extension (in case there is more than one with the same mime type)
	 *
	 * @return string|null The extension determined, or null if unable to match.
	 */
	public static function guessExtensionFromType(string $type, ?string $proposed_extension = null)
	{
		$type = trim(strtolower($type), '. ');

		$proposed_extension = trim(strtolower($proposed_extension));

		if (! is_null($proposed_extension) && array_key_exists($proposed_extension, static::$mimes) && in_array($type, is_string(static::$mimes[$proposed_extension]) ? [static::$mimes[$proposed_extension]] : static::$mimes[$proposed_extension]))
		{
			return $proposed_extension;
		}

		foreach (static::$mimes as $ext => $types)
		{
			if (is_string($types) && $types === $type)
			{
				return $ext;
			}
			else if (is_array($types) && in_array($type, $types))
			{
				return $ext;
			}
		}

		return null;
	}

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

}


<?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;

use CodeIgniter\HTTP\Exceptions\HTTPException;
use Config\Services;

/**
 * Handle a redirect response
 */
class RedirectResponse extends Response
{
	/**
	 * Sets the URI to redirect to and, optionally, the HTTP status code to use.
	 * If no code is provided it will be automatically determined.
	 *
	 * @param string       $uri    The URI to redirect to
	 * @param integer|null $code   HTTP status code
	 * @param string       $method
	 *
	 * @return $this
	 */
	public function to(string $uri, int $code = null, string $method = 'auto')
	{
		// If it appears to be a relative URL, then convert to full URL
		// for better security.
		if (strpos($uri, 'http') !== 0)
		{
			$url = current_url(true)->resolveRelativeURI($uri);
			$uri = (string)$url;
		}

		return $this->redirect($uri, $method, $code);
	}

	/**
	 * Sets the URI to redirect to but as a reverse-routed or named route
	 * instead of a raw URI.
	 *
	 * @param string       $route
	 * @param array        $params
	 * @param integer|null $code
	 * @param string       $method
	 *
	 * @return $this
	 */
	public function route(string $route, array $params = [], int $code = 302, string $method = 'auto')
	{
		$routes = Services::routes(true);

		$route = $routes->reverseRoute($route, ...$params);

		if (! $route)
		{
			throw HTTPException::forInvalidRedirectRoute($route);
		}

		return $this->redirect(base_url($route), $method, $code);
	}

	/**
	 * Helper function to return to previous page.
	 *
	 * Example:
	 *  return redirect()->back();
	 *
	 * @param integer|null $code
	 * @param string       $method
	 *
	 * @return $this
	 */
	public function back(int $code = null, string $method = 'auto')
	{
		$this->ensureSession();

		return $this->redirect(previous_url(), $method, $code);
	}

	/**
	 * Specifies that the current $_GET and $_POST arrays should be
	 * packaged up with the response. It will then be available via
	 * the 'old()' helper function.
	 *
	 * @return $this
	 */
	public function withInput()
	{
		$session = $this->ensureSession();

		$input = [
			'get'  => $_GET ?? [],
			'post' => $_POST ?? [],
		];

		$session->setFlashdata('_ci_old_input', $input);

		// If the validator has any errors, transmit those back
		// so they can be displayed when the validation is
		// handled within a method different than displaying the form.
		$validator = Services::validation();
		if (! empty($validator->getErrors()))
		{
			$session->setFlashdata('_ci_validation_errors', serialize($validator->getErrors()));
		}

		return $this;
	}

	/**
	 * Adds a key and message to the session as Flashdata.
	 *
	 * @param string       $key
	 * @param string|array $message
	 *
	 * @return $this
	 */
	public function with(string $key, $message)
	{
		$session = $this->ensureSession();

		$session->setFlashdata($key, $message);

		return $this;
	}

	/**
	 * Ensures the session is loaded and started.
	 *
	 * @return \CodeIgniter\Session\Session
	 */
	protected function ensureSession()
	{
		return Services::session();
	}
}


<?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\Filters;

use CodeIgniter\Config\BaseConfig;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Filters\Exceptions\FilterException;

/**
 * Filters
 */
class Filters
{

	/**
	 * The processed filters that will
	 * be used to check against.
	 *
	 * @var array
	 */
	protected $filters = [
		'before' => [],
		'after'  => [],
	];

	/**
	 * The original config file
	 *
	 * @var BaseConfig
	 */
	protected $config;

	/**
	 * The active IncomingRequest or CLIRequest
	 *
	 * @var RequestInterface
	 */
	protected $request;

	/**
	 * The active Response instance
	 *
	 * @var ResponseInterface
	 */
	protected $response;

	/**
	 * Whether we've done initial processing
	 * on the filter lists.
	 *
	 * @var boolean
	 */
	protected $initialized = false;

	/**
	 * Any arguments to be passed to filters.
	 *
	 * @var array
	 */
	protected $arguments = [];

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

	public function __construct($config, RequestInterface $request, ResponseInterface $response)
	{
		$this->config  = $config;
		$this->request = & $request;
		$this->setResponse($response);
	}

	public function setResponse(ResponseInterface $response)
	{
		$this->response = & $response;
	}

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

	/**
	 * Runs through all of the filters for the specified
	 * uri and position.
	 *
	 * @param string $uri
	 * @param string $position
	 *
	 * @return \CodeIgniter\HTTP\RequestInterface|\CodeIgniter\HTTP\ResponseInterface|mixed
	 * @throws \CodeIgniter\Filters\Exceptions\FilterException
	 */
	public function run(string $uri, string $position = 'before')
	{
		$this->initialize(strtolower($uri));

		foreach ($this->filters[$position] as $alias => $rules)
		{
			if (is_numeric($alias) && is_string($rules))
			{
				$alias = $rules;
			}

			if (! array_key_exists($alias, $this->config->aliases))
			{
				throw FilterException::forNoAlias($alias);
			}

			$class = new $this->config->aliases[$alias]();

			if (! $class instanceof FilterInterface)
			{
				throw FilterException::forIncorrectInterface(get_class($class));
			}

			if ($position === 'before')
			{
				$result = $class->before($this->request, $this->arguments[$alias] ?? null);

				if ($result instanceof RequestInterface)
				{
					$this->request = $result;
					continue;
				}

				// If the response object was sent back,
				// then send it and quit.
				if ($result instanceof ResponseInterface)
				{
					// short circuit - bypass any other filters
					return $result;
				}

				// Ignore an empty result
				if (empty($result))
				{
					continue;
				}

				return $result;
			}
			elseif ($position === 'after')
			{
				$result = $class->after($this->request, $this->response);

				if ($result instanceof ResponseInterface)
				{
					$this->response = $result;
					continue;
				}
			}
		}

		return $position === 'before' ? $this->request : $this->response;
	}

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

	/**
	 * Runs through our list of filters provided by the configuration
	 * object to get them ready for use, including getting uri masks
	 * to proper regex, removing those we can from the possibilities
	 * based on HTTP method, etc.
	 *
	 * The resulting $this->filters is an array of only filters
	 * that should be applied to this request.
	 *
	 * We go ahead an process the entire tree because we'll need to
	 * run through both a before and after and don't want to double
	 * process the rows.
	 *
	 * @param string $uri
	 *
	 * @return Filters
	 */
	public function initialize(string $uri = null)
	{
		if ($this->initialized === true)
		{
			return $this;
		}

		$this->processGlobals($uri);
		$this->processMethods();
		$this->processFilters($uri);

		$this->initialized = true;

		return $this;
	}

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

	/**
	 * Returns the processed filters array.
	 *
	 * @return array
	 */
	public function getFilters(): array
	{
		return $this->filters;
	}

	/**
	 * Adds a new alias to the config file.
	 * MUST be called prior to initialize();
	 * Intended for use within routes files.
	 *
	 * @param string      $class
	 * @param string|null $alias
	 * @param string      $when
	 * @param string      $section
	 *
	 * @return $this
	 */
	public function addFilter(string $class, string $alias = null, string $when = 'before', string $section = 'globals')
	{
		$alias = $alias ?? md5($class);

		if (! isset($this->config->{$section}))
		{
			$this->config->{$section} = [];
		}

		if (! isset($this->config->{$section}[$when]))
		{
			$this->config->{$section}[$when] = [];
		}

		$this->config->aliases[$alias] = $class;

		$this->config->{$section}[$when][] = $alias;

		return $this;
	}

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

	/**
	 * Ensures that a specific filter is on and enabled for the current request.
	 *
	 * Filters can have "arguments". This is done by placing a colon immediately
	 * after the filter name, followed by a comma-separated list of arguments that
	 * are passed to the filter when executed.
	 *
	 * @param string $name
	 * @param string $when
	 *
	 * @return \CodeIgniter\Filters\Filters
	 */
	public function enableFilter(string $name, string $when = 'before')
	{
		// Get parameters and clean name
		if (strpos($name, ':') !== false)
		{
			list($name, $params) = explode(':', $name);

			$params = explode(',', $params);
			array_walk($params, function (&$item) {
				$item = trim($item);
			});

			$this->arguments[$name] = $params;
		}

		if (! array_key_exists($name, $this->config->aliases))
		{
			throw FilterException::forNoAlias($name);
		}

		if (! isset($this->filters[$when][$name]))
		{
			$this->filters[$when][] = $name;
		}

		return $this;
	}

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

	/**
	 * Returns the arguments for a specified key, or all.
	 *
	 * @return mixed
	 */
	public function getArguments(string $key = null)
	{
		return is_null($key)
			? $this->arguments
			: $this->arguments[$key];
	}

	//--------------------------------------------------------------------
	//--------------------------------------------------------------------
	// Processors
	//--------------------------------------------------------------------

	protected function processGlobals(string $uri = null)
	{
		if (! isset($this->config->globals) || ! is_array($this->config->globals))
		{
			return;
		}

		// Before
		if (isset($this->config->globals['before']))
		{
			// Take any 'except' routes into consideration
			foreach ($this->config->globals['before'] as $alias => $rules)
			{
				if (! is_array($rules) || ! array_key_exists('except', $rules))
				{
					continue;
				}

				$rules = $rules['except'];

				if (is_string($rules))
				{
					$rules = [$rules];
				}

				foreach ($rules as $path)
				{
					// Prep it for regex
					$path = strtolower(str_replace('/*', '*', $path));
					$path = trim(str_replace('*', '.+', $path), '/ ');

					// Path doesn't match the URI? continue on...
					if (preg_match('#' . $path . '#', $uri, $match) !== 1)
					{
						continue;
					}

					unset($this->config->globals['before'][$alias]);
					break;
				}
			}

			$this->filters['before'] = array_merge($this->filters['before'], $this->config->globals['before']);
		}

		// After
		if (isset($this->config->globals['after']))
		{
			// Take any 'except' routes into consideration
			foreach ($this->config->globals['after'] as $alias => $rules)
			{
				if (! is_array($rules) || ! array_key_exists('except', $rules))
				{
					continue;
				}

				$rules = $rules['except'];

				if (is_string($rules))
				{
					$rules = [$rules];
				}

				foreach ($rules as $path)
				{
					// Prep it for regex
					$path = strtolower(str_replace('/*', '*', $path));
					$path = trim(str_replace('*', '.+', $path), '/ ');

					// Path doesn't match the URI? continue on...
					if (preg_match('#' . $path . '#', $uri, $match) !== 1)
					{
						continue;
					}

					unset($this->config->globals['after'][$alias]);
					break;
				}
			}

			$this->filters['after'] = array_merge($this->filters['after'], $this->config->globals['after']);
		}
	}

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

	protected function processMethods()
	{
		if (! isset($this->config->methods) || ! is_array($this->config->methods))
		{
			return;
		}

		// Request method won't be set for CLI-based requests
		$method = strtolower($_SERVER['REQUEST_METHOD'] ?? 'cli');

		if (array_key_exists($method, $this->config->methods))
		{
			$this->filters['before'] = array_merge($this->filters['before'], $this->config->methods[$method]);
			return;
		}
	}

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

	protected function processFilters(string $uri = null)
	{
		if (! isset($this->config->filters) || ! $this->config->filters)
		{
			return;
		}

		$uri = strtolower(trim($uri, '/ '));

		$matches = [];

		foreach ($this->config->filters as $alias => $settings)
		{
			// Before
			if (isset($settings['before']))
			{
				foreach ($settings['before'] as $path)
				{
					// Prep it for regex
					$path = strtolower(str_replace('/*', '*', $path));
					$path = trim(str_replace('*', '.+', $path), '/ ');

					if (preg_match('#' . $path . '#', $uri) !== 1)
					{
						continue;
					}

					$matches[] = $alias;
				}

				$this->filters['before'] = array_merge($this->filters['before'], $matches);
				$matches                 = [];
			}

			// After
			if (isset($settings['after']))
			{
				foreach ($settings['after'] as $path)
				{
					// Prep it for regex
					$path = strtolower(str_replace('/*', '*', $path));
					$path = trim(str_replace('*', '.+', $path), '/ ');

					if (preg_match('#' . $path . '#', $uri) !== 1)
					{
						continue;
					}

					$matches[] = $alias;
				}

				$this->filters['after'] = array_merge($this->filters['after'], $matches);
				$matches                = [];
			}
		}
	}

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


<?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\Log\Handlers;

use CodeIgniter\Events\Events;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Config\Services;

/**
 * Class ChromeLoggerHandler
 *
 * Allows for logging items to the Chrome console for debugging.
 * Requires the ChromeLogger extension installed in your browser.
 *
 * @see https://craig.is/writing/chrome-logger
 *
 * @package CodeIgniter\Log\Handlers
 */
class ChromeLoggerHandler extends BaseHandler implements HandlerInterface
{

	/**
	 * Version of this library - for ChromeLogger use.
	 *
	 * @var float
	 */
	const VERSION = 1.0;

	/**
	 * The number of track frames returned from the backtrace.
	 *
	 * @var integer
	 */
	protected $backtraceLevel = 0;

	/**
	 * The final data that is sent to the browser.
	 *
	 * @var array
	 */
	protected $json = [
		'version' => self::VERSION,
		'columns' => [
			'log',
			'backtrace',
			'type',
		],
		'rows'    => [],
	];

	/**
	 * The header used to pass the data.
	 *
	 * @var string
	 */
	protected $header = 'X-ChromeLogger-Data';

	/**
	 * Maps the log levels to the ChromeLogger types.
	 *
	 * @var array
	 */
	protected $levels = [
		'emergency' => 'error',
		'alert'     => 'error',
		'critical'  => 'error',
		'error'     => 'error',
		'warning'   => 'warn',
		'notice'    => 'warn',
		'info'      => 'info',
		'debug'     => 'info',
	];

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

	/**
	 * Constructor
	 *
	 * @param array $config
	 */
	public function __construct(array $config = [])
	{
		parent::__construct($config);

		$request = Services::request(null, true);

		$this->json['request_uri'] = (string) $request->uri;

		Events::on('post_controller', [$this, 'sendLogs'], EVENT_PRIORITY_HIGH);
	}

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

	/**
	 * Handles logging the message.
	 * If the handler returns false, then execution of handlers
	 * will stop. Any handlers that have not run, yet, will not
	 * be run.
	 *
	 * @param $level
	 * @param $message
	 *
	 * @return boolean
	 */
	public function handle($level, $message): bool
	{
		// Format our message
		$message = $this->format($message);

		// Generate Backtrace info
		$backtrace = debug_backtrace(false, $this->backtraceLevel);
		$backtrace = end($backtrace);

		$backtraceMessage = 'unknown';
		if (isset($backtrace['file']) && isset($backtrace['line']))
		{
			$backtraceMessage = $backtrace['file'] . ':' . $backtrace['line'];
		}

		// Default to 'log' type.
		$type = '';

		if (array_key_exists($level, $this->levels))
		{
			$type = $this->levels[$level];
		}

		$this->json['rows'][] = [
			$message,
			$backtraceMessage,
			$type,
		];

		return true;
	}

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

	/**
	 * Converts the object to display nicely in the Chrome Logger UI.
	 *
	 * @param $object
	 *
	 * @return array
	 */
	protected function format($object)
	{
		if (! is_object($object))
		{
			return $object;
		}

		// @todo Modify formatting of objects once we can view them in browser.
		$objectArray = (array) $object;

		$objectArray['___class_name'] = get_class($object);

		return $objectArray;
	}

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

	/**
	 * Attaches the header and the content to the passed in request object.
	 *
	 * @param ResponseInterface $response
	 */
	public function sendLogs(ResponseInterface &$response = null)
	{
		if (is_null($response))
		{
			$response = Services::response(null, true);
		}

		$data = base64_encode(utf8_encode(json_encode($this->json)));

		$response->setHeader($this->header, $data);
	}

}


<?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;

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

	/**
	 * CREATE DATABASE statement
	 *
	 * @var string
	 */
	protected $createDatabaseStr = 'CREATE DATABASE %s CHARACTER SET %s COLLATE %s';

	/**
	 * DROP CONSTRAINT statement
	 *
	 * @var string
	 */
	protected $dropConstraintStr = 'ALTER TABLE %s DROP FOREIGN KEY %s';

	/**
	 * CREATE TABLE keys flag
	 *
	 * Whether table keys are created from within the
	 * CREATE TABLE statement.
	 *
	 * @var boolean
	 */
	protected $createTableKeys = true;

	/**
	 * UNSIGNED support
	 *
	 * @var array
	 */
	protected $_unsigned = [
		'TINYINT',
		'SMALLINT',
		'MEDIUMINT',
		'INT',
		'INTEGER',
		'BIGINT',
		'REAL',
		'DOUBLE',
		'DOUBLE PRECISION',
		'FLOAT',
		'DECIMAL',
		'NUMERIC',
	];

	/**
	 * Table Options list which required to be quoted
	 *
	 * @var array
	 */
	protected $_quoted_table_options = [
		'COMMENT',
		'COMPRESSION',
		'CONNECTION',
		'DATA DIRECTORY',
		'INDEX DIRECTORY',
		'ENCRYPTION',
		'PASSWORD',
	];

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

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

	/**
	 * CREATE TABLE attributes
	 *
	 * @param  array $attributes Associative array of table attributes
	 * @return string
	 */
	protected function _createTableAttributes(array $attributes): string
	{
		$sql = '';

		foreach (array_keys($attributes) as $key)
		{
			if (is_string($key))
			{
				$sql .= ' ' . strtoupper($key) . ' = ';

				if (in_array(strtoupper($key), $this->_quoted_table_options))
				{
					$sql .= $this->db->escape($attributes[$key]);
				}
				else
				{
					$sql .= $this->db->escapeString($attributes[$key]);
				}
			}
		}

		if (! empty($this->db->charset) && ! strpos($sql, 'CHARACTER SET') && ! strpos($sql, 'CHARSET'))
		{
			$sql .= ' DEFAULT CHARACTER SET = ' . $this->db->escapeString($this->db->charset);
		}

		if (! empty($this->db->DBCollat) && ! strpos($sql, 'COLLATE'))
		{
			$sql .= ' COLLATE = ' . $this->db->escapeString($this->db->DBCollat);
		}

		return $sql;
	}

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

	/**
	 * ALTER TABLE
	 *
	 * @param  string $alter_type ALTER type
	 * @param  string $table      Table name
	 * @param  mixed  $field      Column definition
	 * @return string|string[]
	 */
	protected function _alterTable(string $alter_type, string $table, $field)
	{
		if ($alter_type === 'DROP')
		{
			return parent::_alterTable($alter_type, $table, $field);
		}

		$sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table);
		foreach ($field as $i => $data)
		{
			if ($data['_literal'] !== false)
			{
				$field[$i] = ($alter_type === 'ADD') ? "\n\tADD " . $data['_literal'] : "\n\tMODIFY " . $data['_literal'];
			}
			else
			{
				if ($alter_type === 'ADD')
				{
					$field[$i]['_literal'] = "\n\tADD ";
				}
				else
				{
					$field[$i]['_literal'] = empty($data['new_name']) ? "\n\tMODIFY " : "\n\tCHANGE ";
				}

				$field[$i] = $field[$i]['_literal'] . $this->_processColumn($field[$i]);
			}
		}

		return [$sql . implode(',', $field)];
	}

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

	/**
	 * Process column
	 *
	 * @param  array $field
	 * @return string
	 */
	protected function _processColumn(array $field): string
	{
		$extra_clause = isset($field['after']) ? ' AFTER ' . $this->db->escapeIdentifiers($field['after']) : '';

		if (empty($extra_clause) && isset($field['first']) && $field['first'] === true)
		{
			$extra_clause = ' FIRST';
		}

		return $this->db->escapeIdentifiers($field['name'])
				. (empty($field['new_name']) ? '' : ' ' . $this->db->escapeIdentifiers($field['new_name']))
				. ' ' . $field['type'] . $field['length']
				. $field['unsigned']
				. $field['null']
				. $field['default']
				. $field['auto_increment']
				. $field['unique']
				. (empty($field['comment']) ? '' : ' COMMENT ' . $field['comment'])
				. $extra_clause;
	}

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

	/**
	 * Process indexes
	 *
	 * @param  string $table (ignored)
	 * @return string
	 */
	protected function _processIndexes(string $table): string
	{
		$sql = '';

		for ($i = 0, $c = count($this->keys); $i < $c; $i ++)
		{
			if (is_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]);
						continue;
					}
				}
			}
			elseif (! isset($this->fields[$this->keys[$i]]))
			{
				unset($this->keys[$i]);
				continue;
			}

			is_array($this->keys[$i]) || $this->keys[$i] = [$this->keys[$i]];

			$unique = in_array($i, $this->uniqueKeys) ? 'UNIQUE ' : '';

			$sql .= ",\n\t{$unique}KEY " . $this->db->escapeIdentifiers(implode('_', $this->keys[$i]))
				. ' (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i])) . ')';
		}

		$this->keys = [];

		return $sql;
	}

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


<?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;

use CodeIgniter\HTTP\Exceptions\HTTPException;
use CodeIgniter\HTTP\Files\FileCollection;
use CodeIgniter\HTTP\Files\UploadedFile;
use CodeIgniter\Config\Services;

/**
 * Class IncomingRequest
 *
 * Represents an incoming, getServer-side HTTP request.
 *
 * Per the HTTP specification, this interface includes properties for
 * each of the following:
 *
 * - Protocol version
 * - HTTP method
 * - URI
 * - Headers
 * - Message body
 *
 * Additionally, it encapsulates all data as it has arrived to the
 * application from the CGI and/or PHP environment, including:
 *
 * - The values represented in $_SERVER.
 * - Any cookies provided (generally via $_COOKIE)
 * - Query string arguments (generally via $_GET, or as parsed via parse_str())
 * - Upload files, if any (as represented by $_FILES)
 * - Deserialized body binds (generally from $_POST)
 *
 * @package CodeIgniter\HTTP
 */
class IncomingRequest extends Request
{

	/**
	 * Enable CSRF flag
	 *
	 * Enables a CSRF cookie token to be set.
	 * Set automatically based on Config setting.
	 *
	 * @var boolean
	 */
	protected $enableCSRF = false;

	/**
	 * A \CodeIgniter\HTTP\URI instance.
	 *
	 * @var URI
	 */
	public $uri;

	/**
	 * File collection
	 *
	 * @var Files\FileCollection
	 */
	protected $files;

	/**
	 * Negotiator
	 *
	 * @var \CodeIgniter\HTTP\Negotiate
	 */
	protected $negotiator;

	/**
	 * The default Locale this request
	 * should operate under.
	 *
	 * @var string
	 */
	protected $defaultLocale;

	/**
	 * The current locale of the application.
	 * Default value is set in Config\App.php
	 *
	 * @var string
	 */
	protected $locale;

	/**
	 * Stores the valid locale codes.
	 *
	 * @var array
	 */
	protected $validLocales = [];

	/**
	 * @var \Config\App
	 */
	public $config;

	/**
	 * Holds the old data from a redirect.
	 *
	 * @var array
	 */
	protected $oldInput = [];

	/**
	 * @var \CodeIgniter\HTTP\UserAgent
	 */
	protected $userAgent;

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

	/**
	 * Constructor
	 *
	 * @param object                      $config
	 * @param \CodeIgniter\HTTP\URI       $uri
	 * @param string|null                      $body
	 * @param \CodeIgniter\HTTP\UserAgent $userAgent
	 */
	public function __construct($config, URI $uri = null, $body = 'php://input', UserAgent $userAgent)
	{
		// Get our body from php://input
		if ($body === 'php://input')
		{
			$body = file_get_contents('php://input');
		}

		$this->body      = $body;
		$this->config    = $config;
		$this->userAgent = $userAgent;

		parent::__construct($config);

		$this->populateHeaders();

		$this->uri = $uri;

		$this->detectURI($config->uriProtocol, $config->baseURL);

		$this->validLocales = $config->supportedLocales;

		$this->detectLocale($config);
	}

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

	/**
	 * Handles setting up the locale, perhaps auto-detecting through
	 * content negotiation.
	 *
	 * @param $config
	 */
	public function detectLocale($config)
	{
		$this->locale = $this->defaultLocale = $config->defaultLocale;

		if (! $config->negotiateLocale)
		{
			return;
		}

		$this->setLocale($this->negotiate('language', $config->supportedLocales));
	}

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

	/**
	 * Returns the default locale as set in Config\App.php
	 *
	 * @return string
	 */
	public function getDefaultLocale(): string
	{
		return $this->defaultLocale;
	}

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

	/**
	 * Gets the current locale, with a fallback to the default
	 * locale if none is set.
	 *
	 * @return string
	 */
	public function getLocale(): string
	{
		return $this->locale ?? $this->defaultLocale;
	}

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

	/**
	 * Sets the locale string for this request.
	 *
	 * @param string $locale
	 *
	 * @return IncomingRequest
	 */
	public function setLocale(string $locale)
	{
		// If it's not a valid locale, set it
		// to the default locale for the site.
		if (! in_array($locale, $this->validLocales))
		{
			$locale = $this->defaultLocale;
		}

		$this->locale = $locale;

		// If the intl extension is loaded, make sure
		// that we set the locale for it... if not, though,
		// don't worry about it.
		// this should not block code coverage thru unit testing
		// @codeCoverageIgnoreStart
		try
		{
			if (class_exists('\Locale', false))
			{
				\Locale::setDefault($locale);
			}
		}
		catch (\Exception $e)
		{
		}
		// @codeCoverageIgnoreEnd

		return $this;
	}

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

	/**
	 * Determines if this request was made from the command line (CLI).
	 *
	 * @return boolean
	 */
	public function isCLI(): bool
	{
		return is_cli();
	}

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

	/**
	 * Test to see if a request contains the HTTP_X_REQUESTED_WITH header.
	 *
	 * @return boolean
	 */
	public function isAJAX(): bool
	{
		return ( ! empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
				strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest');
	}

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

	/**
	 * Attempts to detect if the current connection is secure through
	 * a few different methods.
	 *
	 * @return boolean
	 */
	public function isSecure(): bool
	{
		if (! empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off')
		{
			return true;
		}
		elseif (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https')
		{
			return true;
		}
		elseif (! empty($_SERVER['HTTP_FRONT_END_HTTPS']) && strtolower($_SERVER['HTTP_FRONT_END_HTTPS']) !== 'off')
		{
			return true;
		}

		return false;
	}

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

	/**
	 * Fetch an item from the $_REQUEST object. This is the simplest way
	 * to grab data from the request object and can be used in lieu of the
	 * other get* methods in most cases.
	 *
	 * @param string|array|null $index
	 * @param integer|null      $filter Filter constant
	 * @param mixed             $flags
	 *
	 * @return mixed
	 */
	public function getVar($index = null, $filter = null, $flags = null)
	{
		return $this->fetchGlobal('request', $index, $filter, $flags);
	}

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

	/**
	 * A convenience method that grabs the raw input stream and decodes
	 * the JSON into an array.
	 *
	 * If $assoc == true, then all objects in the response will be converted
	 * to associative arrays.
	 *
	 * @param boolean $assoc   Whether to return objects as associative arrays
	 * @param integer $depth   How many levels deep to decode
	 * @param integer $options Bitmask of options
	 *
	 * @see http://php.net/manual/en/function.json-decode.php
	 *
	 * @return mixed
	 */
	public function getJSON(bool $assoc = false, int $depth = 512, int $options = 0)
	{
		return json_decode($this->body, $assoc, $depth, $options);
	}

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

	/**
	 * A convenience method that grabs the raw input stream(send method in PUT, PATCH, DELETE) and decodes
	 * the String into an array.
	 *
	 * @return mixed
	 */
	public function getRawInput()
	{
		parse_str($this->body, $output);

		return $output;
	}

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

	/**
	 * Fetch an item from GET data.
	 *
	 * @param string|array|null $index  Index for item to fetch from $_GET.
	 * @param integer|null      $filter A filter name to apply.
	 * @param mixed|null        $flags
	 *
	 * @return mixed
	 */
	public function getGet($index = null, $filter = null, $flags = null)
	{
		return $this->fetchGlobal('get', $index, $filter, $flags);
	}

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

	/**
	 * Fetch an item from POST.
	 *
	 * @param string|array|null $index  Index for item to fetch from $_POST.
	 * @param integer|null      $filter A filter name to apply
	 * @param mixed             $flags
	 *
	 * @return mixed
	 */
	public function getPost($index = null, $filter = null, $flags = null)
	{
		return $this->fetchGlobal('post', $index, $filter, $flags);
	}

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

	/**
	 * Fetch an item from POST data with fallback to GET.
	 *
	 * @param string|array|null $index  Index for item to fetch from $_POST or $_GET
	 * @param integer|null      $filter A filter name to apply
	 * @param mixed             $flags
	 *
	 * @return mixed
	 */
	public function getPostGet($index = null, $filter = null, $flags = null)
	{
		// Use $_POST directly here, since filter_has_var only
		// checks the initial POST data, not anything that might
		// have been added since.
		return isset($_POST[$index]) ? $this->getPost($index, $filter, $flags) : $this->getGet($index, $filter, $flags);
	}

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

	/**
	 * Fetch an item from GET data with fallback to POST.
	 *
	 * @param string|array|null $index  Index for item to be fetched from $_GET or $_POST
	 * @param integer|null      $filter A filter name to apply
	 * @param mixed             $flags
	 *
	 * @return mixed
	 */
	public function getGetPost($index = null, $filter = null, $flags = null)
	{
		// Use $_GET directly here, since filter_has_var only
		// checks the initial GET data, not anything that might
		// have been added since.
		return isset($_GET[$index]) ? $this->getGet($index, $filter, $flags) : $this->getPost($index, $filter, $flags);
	}

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

	/**
	 * Fetch an item from the COOKIE array.
	 *
	 * @param string|array|null $index  Index for item to be fetched from $_COOKIE
	 * @param integer|null      $filter A filter name to be applied
	 * @param mixed             $flags
	 *
	 * @return mixed
	 */
	public function getCookie($index = null, $filter = null, $flags = null)
	{
		return $this->fetchGlobal('cookie', $index, $filter, $flags);
	}

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

	/**
	 * Fetch the user agent string
	 *
	 * @return \CodeIgniter\HTTP\UserAgent
	 */
	public function getUserAgent()
	{
		return $this->userAgent;
	}

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

	/**
	 * Attempts to get old Input data that has been flashed to the session
	 * with redirect_with_input(). It first checks for the data in the old
	 * POST data, then the old GET data and finally check for dot arrays
	 *
	 * @param string $key
	 *
	 * @return mixed
	 */
	public function getOldInput(string $key)
	{
		// If the session hasn't been started, or no
		// data was previously saved, we're done.
		if (empty($_SESSION['_ci_old_input']))
		{
			return;
		}

		// Check for the value in the POST array first.
		if (isset($_SESSION['_ci_old_input']['post'][$key]))
		{
			return $_SESSION['_ci_old_input']['post'][$key];
		}

		// Next check in the GET array.
		if (isset($_SESSION['_ci_old_input']['get'][$key]))
		{
			return $_SESSION['_ci_old_input']['get'][$key];
		}

		helper('array');

		// Check for an array value in POST.
		if (isset($_SESSION['_ci_old_input']['post']))
		{
			$value = dot_array_search($key, $_SESSION['_ci_old_input']['post']);
			if (! is_null($value))
			{
				return $value;
			}
		}

		// Check for an array value in GET.
		if (isset($_SESSION['_ci_old_input']['get']))
		{
			$value = dot_array_search($key, $_SESSION['_ci_old_input']['get']);
			if (! is_null($value))
			{
				return $value;
			}
		}

		//      // return null if requested session key not found
		//      return null;
	}

	/**
	 * Returns an array of all files that have been uploaded with this
	 * request. Each file is represented by an UploadedFile instance.
	 *
	 * @return array
	 */
	public function getFiles(): array
	{
		if (is_null($this->files))
		{
			$this->files = new FileCollection();
		}

		return $this->files->all(); // return all files
	}

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

	/**
	 * Retrieves a single file by the name of the input field used
	 * to upload it.
	 *
	 * @param string $fileID
	 *
	 * @return UploadedFile|null
	 */
	public function getFile(string $fileID)
	{
		if (is_null($this->files))
		{
			$this->files = new FileCollection();
		}

		return $this->files->getFile($fileID);
	}

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

	/**
	 * Sets up our URI object based on the information we have. This is
	 * either provided by the user in the baseURL Config setting, or
	 * determined from the environment as needed.
	 *
	 * @param string $protocol
	 * @param string $baseURL
	 */
	protected function detectURI(string $protocol, string $baseURL)
	{
		$this->uri->setPath($this->detectPath($protocol));

		// It's possible the user forgot a trailing slash on their
		// baseURL, so let's help them out.
		$baseURL = ! empty($baseURL) ? rtrim($baseURL, '/ ') . '/' : $baseURL;

		// Based on our baseURL provided by the developer (if set)
		// set our current domain name, scheme
		if (! empty($baseURL))
		{
			// We cannot add the path here, otherwise it's possible
			// that the routing will not work correctly if we are
			// within a sub-folder scheme. So it's modified in
			// the
			$this->uri->setScheme(parse_url($baseURL, PHP_URL_SCHEME));
			$this->uri->setHost(parse_url($baseURL, PHP_URL_HOST));
			$this->uri->setPort(parse_url($baseURL, PHP_URL_PORT));
			$this->uri->resolveRelativeURI(parse_url($baseURL, PHP_URL_PATH));
		}
		else
		{
			// @codeCoverageIgnoreStart
			if (! is_cli())
			{
				die('You have an empty or invalid base URL. The baseURL value must be set in Config\App.php, or through the .env file.');
			}
			// @codeCoverageIgnoreEnd
		}
	}

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

	/**
	 * Based on the URIProtocol Config setting, will attempt to
	 * detect the path portion of the current URI.
	 *
	 * @param string $protocol
	 *
	 * @return string
	 */
	public function detectPath(string $protocol = ''): string
	{
		if (empty($protocol))
		{
			$protocol = 'REQUEST_URI';
		}

		switch ($protocol)
		{
			case 'REQUEST_URI':
				$path = $this->parseRequestURI();
				break;
			case 'QUERY_STRING':
				$path = $this->parseQueryString();
				break;
			case 'PATH_INFO':
			default:
				$path = $this->fetchGlobal('server', $protocol) ?? $this->parseRequestURI();
				break;
		}

		return $path;
	}

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

	/**
	 * Provides a convenient way to work with the Negotiate class
	 * for content negotiation.
	 *
	 * @param string  $type
	 * @param array   $supported
	 * @param boolean $strictMatch
	 *
	 * @return string
	 */
	public function negotiate(string $type, array $supported, bool $strictMatch = false): string
	{
		if (is_null($this->negotiator))
		{
			$this->negotiator = Services::negotiator($this, true);
		}

		switch (strtolower($type))
		{
			case 'media':
				return $this->negotiator->media($supported, $strictMatch);
			case 'charset':
				return $this->negotiator->charset($supported);
			case 'encoding':
				return $this->negotiator->encoding($supported);
			case 'language':
				return $this->negotiator->language($supported);
		}

		throw HTTPException::forInvalidNegotiationType($type);
	}

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

	/**
	 * Will parse the REQUEST_URI and automatically detect the URI from it,
	 * fixing the query string if necessary.
	 *
	 * @return string The URI it found.
	 */
	protected function parseRequestURI(): string
	{
		if (! isset($_SERVER['REQUEST_URI'], $_SERVER['SCRIPT_NAME']))
		{
			return '';
		}

		// parse_url() returns false if no host is present, but the path or query string
		// contains a colon followed by a number
		$parts = parse_url('http://dummy' . $_SERVER['REQUEST_URI']);
		$query = $parts['query'] ?? '';
		$uri   = $parts['path'] ?? '';

		if (isset($_SERVER['SCRIPT_NAME'][0]))
		{
			// strip the script name from the beginning of the URI
			if (strpos($uri, $_SERVER['SCRIPT_NAME']) === 0)
			{
				$uri = (string) substr($uri, strlen($_SERVER['SCRIPT_NAME']));
			}
			// if the script is nested, strip the parent folder & script from the URI
			elseif (strpos($uri, $_SERVER['SCRIPT_NAME']) > 0)
			{
				$uri = (string) substr($uri, strpos($uri, $_SERVER['SCRIPT_NAME']) + strlen($_SERVER['SCRIPT_NAME']));
			}
			// or if index.php is implied
			elseif (strpos($uri, dirname($_SERVER['SCRIPT_NAME'])) === 0)
			{
				$uri = (string) substr($uri, strlen(dirname($_SERVER['SCRIPT_NAME'])));
			}
		}

		// This section ensures that even on servers that require the URI to contain the query string (Nginx) a correct
		// URI is found, and also fixes the QUERY_STRING getServer var and $_GET array.
		if (trim($uri, '/') === '' && strncmp($query, '/', 1) === 0)
		{
			$query                   = explode('?', $query, 2);
			$uri                     = $query[0];
			$_SERVER['QUERY_STRING'] = $query[1] ?? '';
		}
		else
		{
			$_SERVER['QUERY_STRING'] = $query;
		}

		parse_str($_SERVER['QUERY_STRING'], $_GET);

		if ($uri === '/' || $uri === '')
		{
			return '/';
		}

		return $this->removeRelativeDirectory($uri);
	}

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

	/**
	 * Parse QUERY_STRING
	 *
	 * Will parse QUERY_STRING and automatically detect the URI from it.
	 *
	 * @return string
	 */
	protected function parseQueryString(): string
	{
		$uri = $_SERVER['QUERY_STRING'] ?? @getenv('QUERY_STRING');

		if (trim($uri, '/') === '')
		{
			return '';
		}
		elseif (strncmp($uri, '/', 1) === 0)
		{
			$uri                     = explode('?', $uri, 2);
			$_SERVER['QUERY_STRING'] = $uri[1] ?? '';
			$uri                     = $uri[0];
		}

		parse_str($_SERVER['QUERY_STRING'], $_GET);

		return $this->removeRelativeDirectory($uri);
	}

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

	/**
	 * Remove relative directory (../) and multi slashes (///)
	 *
	 * Do some final cleaning of the URI and return it, currently only used in static::_parse_request_uri()
	 *
	 * @param string $uri
	 *
	 * @return string
	 */
	protected function removeRelativeDirectory(string $uri): string
	{
		$uris = [];
		$tok  = strtok($uri, '/');
		while ($tok !== false)
		{
			if (( ! empty($tok) || $tok === '0') && $tok !== '..')
			{
				$uris[] = $tok;
			}
			$tok = strtok('/');
		}

		return implode('/', $uris);
	}

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