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

use CodeIgniter\I18n\Exceptions\I18nException;
use IntlCalendar;
use Locale;
use DateTime;
use DateInterval;
use DateTimeZone;
use IntlDateFormatter;

/**
 * Class Time
 *
 * A localized date/time package inspired
 * by Nesbot/Carbon and CakePHP/Chronos.
 *
 * Requires the intl PHP extension.
 *
 * @package CodeIgniter\I18n
 */
class Time extends DateTime
{

	/**
	 * @var string
	 */
	protected $timezone;

	/**
	 * @var string
	 */
	protected $locale;

	/**
	 * Format to use when displaying datetime through __toString
	 *
	 * @var string
	 */
	protected $toStringFormat = 'yyyy-MM-dd HH:mm:ss';

	/**
	 * Used to check time string to determine if it is relative time or not....
	 *
	 * @var string
	 */
	protected static $relativePattern = '/this|next|last|tomorrow|yesterday|midnight|today|[+-]|first|last|ago/i';

	/**
	 * @var \CodeIgniter\I18n\Time
	 */
	protected static $testNow;

	//--------------------------------------------------------------------
	// Constructors
	//--------------------------------------------------------------------

	/**
	 * Time constructor.
	 *
	 * @param string|null $time
	 * @param null        $timezone
	 * @param string|null $locale
	 *
	 * @throws \Exception
	 */
	public function __construct(string $time = null, $timezone = null, string $locale = null)
	{
		// If no locale was provided, grab it from Locale (set by IncomingRequest for web requests)
		$this->locale = ! empty($locale) ? $locale : Locale::getDefault();

		// If a test instance has been provided, use it instead.
		if (is_null($time) && static::$testNow instanceof Time)
		{
			if (empty($timezone))
			{
				$timezone = static::$testNow->getTimezone();
			}

			$time = static::$testNow->toDateTimeString();
		}

		$timezone       = ! empty($timezone) ? $timezone : date_default_timezone_get();
		$this->timezone = $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone);

		if (! empty($time))
		{
			// If the time string was a relative string (i.e. 'next Tuesday')
			// then we need to adjust the time going in so that we have a current
			// timezone to work with.
			if (is_string($time) && static::hasRelativeKeywords($time))
			{
				$instance = new DateTime('now', $this->timezone);
				$instance->modify($time);

				$time = $instance->format('Y-m-d H:i:s');
			}
		}

		return parent::__construct($time, $this->timezone);
	}

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

	/**
	 * Returns a new Time instance with the timezone set.
	 *
	 * @param string|DateTimeZone|null $timezone
	 * @param string|null              $locale
	 *
	 * @return \CodeIgniter\I18n\Time
	 * @throws \Exception
	 */
	public static function now($timezone = null, string $locale = null)
	{
		return new Time(null, $timezone, $locale);
	}

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

	/**
	 * Returns a new Time instance while parsing a datetime string.
	 *
	 * Example:
	 *  $time = Time::parse('first day of December 2008');
	 *
	 * @param string      $datetime
	 * @param string|null $timezone
	 * @param string|null $locale
	 *
	 * @return \CodeIgniter\I18n\Time
	 * @throws \Exception
	 */
	public static function parse(string $datetime, $timezone = null, string $locale = null)
	{
		return new Time($datetime, $timezone, $locale);
	}

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

	/**
	 * Return a new time with the time set to midnight.
	 *
	 * @param null        $timezone
	 * @param string|null $locale
	 *
	 * @return \CodeIgniter\I18n\Time
	 * @throws \Exception
	 */
	public static function today($timezone = null, string $locale = null)
	{
		return new Time(date('Y-m-d 00:00:00'), $timezone, $locale);
	}

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

	/**
	 * Returns an instance set to midnight yesterday morning.
	 *
	 * @param null        $timezone
	 * @param string|null $locale
	 *
	 * @return \CodeIgniter\I18n\Time
	 * @throws \Exception
	 */
	public static function yesterday($timezone = null, string $locale = null)
	{
		return new Time(date('Y-m-d 00:00:00', strtotime('-1 day')), $timezone, $locale);
	}

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

	/**
	 * Returns an instance set to midnight tomorrow morning.
	 *
	 * @param null        $timezone
	 * @param string|null $locale
	 *
	 * @return \CodeIgniter\I18n\Time
	 * @throws \Exception
	 */
	public static function tomorrow($timezone = null, string $locale = null)
	{
		return new Time(date('Y-m-d 00:00:00', strtotime('+1 day')), $timezone, $locale);
	}

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

	/**
	 * Returns a new instance based on the year, month and day. If any of those three
	 * are left empty, will default to the current value.
	 *
	 * @param integer|null $year
	 * @param integer|null $month
	 * @param integer|null $day
	 * @param null         $timezone
	 * @param string       $locale
	 *
	 * @return \CodeIgniter\I18n\Time
	 * @throws \Exception
	 */
	public static function createFromDate(int $year = null, int $month = null, int $day = null, $timezone = null, string $locale = null)
	{
		return static::create($year, $month, $day, null, null, null, $timezone, $locale);
	}

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

	/**
	 * Returns a new instance with the date set to today, and the time set to the values passed in.
	 *
	 * @param integer|null $hour
	 * @param integer|null $minutes
	 * @param integer|null $seconds
	 * @param null         $timezone
	 * @param string|null  $locale
	 *
	 * @return \CodeIgniter\I18n\Time
	 * @throws \Exception
	 */
	public static function createFromTime(int $hour = null, int $minutes = null, int $seconds = null, $timezone = null, string $locale = null)
	{
		return static::create(null, null, null, $hour, $minutes, $seconds, $timezone, $locale);
	}

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

	/**
	 * Returns a new instance with the date time values individually set.
	 *
	 * @param integer|null $year
	 * @param integer|null $month
	 * @param integer|null $day
	 * @param integer|null $hour
	 * @param integer|null $minutes
	 * @param integer|null $seconds
	 * @param null         $timezone
	 * @param string|null  $locale
	 *
	 * @return \CodeIgniter\I18n\Time
	 * @throws \Exception
	 */
	public static function create(int $year = null, int $month = null, int $day = null, int $hour = null, int $minutes = null, int $seconds = null, $timezone = null, string $locale = null)
	{
		$year    = is_null($year) ? date('Y') : $year;
		$month   = is_null($month) ? date('m') : $month;
		$day     = is_null($day) ? date('d') : $day;
		$hour    = empty($hour) ? 0 : $hour;
		$minutes = empty($minutes) ? 0 : $minutes;
		$seconds = empty($seconds) ? 0 : $seconds;

		return new Time(date('Y-m-d H:i:s', strtotime("{$year}-{$month}-{$day} {$hour}:{$minutes}:{$seconds}")), $timezone, $locale);
	}

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

	/**
	 * Provides a replacement for DateTime's own createFromFormat function, that provides
	 * more flexible timeZone handling
	 *
	 * @param string                $format
	 * @param string                $datetime
	 * @param DateTimeZone|string   $timeZone
	 *
	 * @return \CodeIgniter\I18n\Time
	 * @throws \Exception
	 */
	public static function createFromFormat($format, $datetime, $timeZone = null)
	{
		$date = parent::createFromFormat($format, $datetime);

		return new Time($date->format('Y-m-d H:i:s'), $timeZone);
	}

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

	/**
	 * Returns a new instance with the datetime set based on the provided UNIX timestamp.
	 *
	 * @param integer      $timestamp
	 * @param DateTimeZone $timeZone
	 * @param string|null  $locale
	 *
	 * @return \CodeIgniter\I18n\Time
	 * @throws \Exception
	 */
	public static function createFromTimestamp(int $timestamp, $timeZone = null, string $locale = null)
	{
		return new Time(date('Y-m-d H:i:s', $timestamp), $timeZone, $locale);
	}

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

	/**
	 * Takes an instance of DateTime and returns an instance of Time with it's same values.
	 *
	 * @param \DateTime   $dateTime
	 * @param string|null $locale
	 *
	 * @return \CodeIgniter\I18n\Time
	 * @throws \Exception
	 */
	public static function instance(DateTime $dateTime, string $locale = null)
	{
		$date     = $dateTime->format('Y-m-d H:i:s');
		$timezone = $dateTime->getTimezone();

		return new Time($date, $timezone, $locale);
	}

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

	/**
	 * Converts the current instance to a mutable DateTime object.
	 *
	 * @return \DateTime
	 * @throws \Exception
	 */
	public function toDateTime()
	{
		$dateTime = new DateTime(null, $this->getTimezone());
		$dateTime->setTimestamp(parent::getTimestamp());

		return $dateTime;
	}

	//--------------------------------------------------------------------
	// For Testing
	//--------------------------------------------------------------------

	/**
	 * Creates an instance of Time that will be returned during testing
	 * when calling 'Time::now' instead of the current time.
	 *
	 * @param \CodeIgniter\I18n\Time|string $datetime
	 * @param null                          $timezone
	 * @param string|null                   $locale
	 *
	 * @throws \Exception
	 */
	public static function setTestNow($datetime = null, $timezone = null, string $locale = null)
	{
		// Reset the test instance
		if (is_null($datetime))
		{
			static::$testNow = null;
			return;
		}

		// Convert to a Time instance
		if (is_string($datetime))
		{
			$datetime = new Time($datetime, $timezone, $locale);
		}
		else if ($datetime instanceof DateTime && ! $datetime instanceof Time)
		{
			$datetime = new Time($datetime->format('Y-m-d H:i:s'), $timezone);
		}

		static::$testNow = $datetime;
	}

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

	/**
	 * Returns whether we have a testNow instance saved.
	 *
	 * @return boolean
	 */
	public static function hasTestNow(): bool
	{
		return ! is_null(static::$testNow);
	}

	//--------------------------------------------------------------------
	//--------------------------------------------------------------------
	// Getters
	//--------------------------------------------------------------------

	/**
	 * Returns the localized Year
	 *
	 * @return string
	 * @throws \Exception
	 */
	public function getYear(): string
	{
		return $this->toLocalizedString('Y');
	}

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

	/**
	 * Returns the localized Month
	 *
	 * @return string
	 * @throws \Exception
	 */
	public function getMonth(): string
	{
		return $this->toLocalizedString('M');
	}

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

	/**
	 * Return the localized day of the month.
	 *
	 * @return string
	 * @throws \Exception
	 */
	public function getDay(): string
	{
		return $this->toLocalizedString('d');
	}

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

	/**
	 * Return the localized hour (in 24-hour format).
	 *
	 * @return string
	 * @throws \Exception
	 */
	public function getHour(): string
	{
		return $this->toLocalizedString('H');
	}

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

	/**
	 * Return the localized minutes in the hour.
	 *
	 * @return string
	 * @throws \Exception
	 */
	public function getMinute(): string
	{
		return $this->toLocalizedString('m');
	}

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

	/**
	 * Return the localized seconds
	 *
	 * @return string
	 * @throws \Exception
	 */
	public function getSecond(): string
	{
		return $this->toLocalizedString('s');
	}

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

	/**
	 * Return the index of the day of the week
	 *
	 * @return string
	 * @throws \Exception
	 */
	public function getDayOfWeek(): string
	{
		return $this->toLocalizedString('c');
	}

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

	/**
	 * Return the index of the day of the year
	 *
	 * @return string
	 * @throws \Exception
	 */
	public function getDayOfYear(): string
	{
		return $this->toLocalizedString('D');
	}

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

	/**
	 * Return the index of the week in the month
	 *
	 * @return string
	 * @throws \Exception
	 */
	public function getWeekOfMonth(): string
	{
		return $this->toLocalizedString('W');
	}

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

	/**
	 * Return the index of the week in the year
	 *
	 * @return string
	 * @throws \Exception
	 */
	public function getWeekOfYear(): string
	{
		return $this->toLocalizedString('w');
	}

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

	/**
	 * Returns the age in years from the "current" date and 'now'
	 * @throws \Exception
	 */
	public function getAge()
	{
		$now  = Time::now()->getTimestamp();
		$time = $this->getTimestamp();

		// future dates have no age
		return max(0, date('Y', $now) - date('Y', $time));
	}

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

	/**
	 * Returns the number of the current quarter for the year.
	 *
	 * @return string
	 * @throws \Exception
	 */
	public function getQuarter(): string
	{
		return $this->toLocalizedString('Q');
	}

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

	/**
	 * Are we in daylight savings time currently?
	 *
	 * @return boolean
	 */
	public function getDst(): bool
	{
		// grab the transactions that would affect today
		$start       = strtotime('-1 year', $this->getTimestamp());
		$end         = strtotime('+2 year', $start);
		$transitions = $this->timezone->getTransitions($start, $end);

		$daylightSaving = false;
		foreach ($transitions as $transition)
		{
			if ($transition['time'] > $this->format('U'))
			{
				$daylightSaving = (bool) $transition['isdst'] ?? $daylightSaving;
			}
		}
		return $daylightSaving;
	}

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

	/**
	 * Returns boolean whether the passed timezone is the same as
	 * the local timezone.
	 *
	 * @return boolean
	 */
	public function getLocal(): bool
	{
		$local = date_default_timezone_get();

		return $local === $this->timezone->getName();
	}

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

	/**
	 * Returns boolean whether object is in UTC.
	 *
	 * @return boolean
	 */
	public function getUtc(): bool
	{
		return $this->getOffset() === 0;
	}

	/**
	 * Returns the name of the current timezone.
	 *
	 * @return string
	 */
	public function getTimezoneName(): string
	{
		return $this->timezone->getName();
	}

	//--------------------------------------------------------------------
	// Setters
	//--------------------------------------------------------------------

	/**
	 * Sets the current year for this instance.
	 *
	 * @param $value
	 *
	 * @return \CodeIgniter\I18n\Time
	 * @throws \Exception
	 */
	public function setYear($value)
	{
		return $this->setValue('year', $value);
	}

	/**
	 * Sets the month of the year.
	 *
	 * @param $value
	 *
	 * @return \CodeIgniter\I18n\Time
	 * @throws \Exception
	 */
	public function setMonth($value)
	{
		if (is_numeric($value) && $value < 1 || $value > 12)
		{
			throw I18nException::forInvalidMonth($value);
		}

		if (is_string($value) && ! is_numeric($value))
		{
			$value = date('m', strtotime("{$value} 1 2017"));
		}

		return $this->setValue('month', $value);
	}

	/**
	 * Sets the day of the month.
	 *
	 * @param $value
	 *
	 * @return \CodeIgniter\I18n\Time
	 * @throws \Exception
	 */
	public function setDay($value)
	{
		if ($value < 1 || $value > 31)
		{
			throw I18nException::forInvalidDay($value);
		}

		$date    = $this->getYear() . '-' . $this->getMonth();
		$lastDay = date('t', strtotime($date));
		if ($value > $lastDay)
		{
			throw I18nException::forInvalidOverDay($lastDay, $value);
		}

		return $this->setValue('day', $value);
	}

	/**
	 * Sets the hour of the day (24 hour cycle)
	 *
	 * @param $value
	 *
	 * @return \CodeIgniter\I18n\Time
	 * @throws \Exception
	 */
	public function setHour($value)
	{
		if ($value < 0 || $value > 23)
		{
			throw I18nException::forInvalidHour($value);
		}

		return $this->setValue('hour', $value);
	}

	/**
	 * Sets the minute of the hour
	 *
	 * @param $value
	 *
	 * @return \CodeIgniter\I18n\Time
	 * @throws \Exception
	 */
	public function setMinute($value)
	{
		if ($value < 0 || $value > 59)
		{
			throw I18nException::forInvalidMinutes($value);
		}

		return $this->setValue('minute', $value);
	}

	/**
	 * Sets the second of the minute.
	 *
	 * @param $value
	 *
	 * @return \CodeIgniter\I18n\Time
	 * @throws \Exception
	 */
	public function setSecond($value)
	{
		if ($value < 0 || $value > 59)
		{
			throw I18nException::forInvalidSeconds($value);
		}

		return $this->setValue('second', $value);
	}

	/**
	 * Helper method to do the heavy lifting of the 'setX' methods.
	 *
	 * @param string $name
	 * @param $value
	 *
	 * @return \CodeIgniter\I18n\Time
	 * @throws \Exception
	 */
	protected function setValue(string $name, $value)
	{
		list($year, $month, $day, $hour, $minute, $second) = explode('-', $this->format('Y-n-j-G-i-s'));
		$$name                                             = $value;

		return Time::create($year, $month, $day, $hour, $minute, $second, $this->getTimezoneName(), $this->locale);
	}

	/**
	 * Returns a new instance with the revised timezone.
	 *
	 * @param \DateTimeZone $timezone
	 *
	 * @return \CodeIgniter\I18n\Time
	 * @throws \Exception
	 */
	public function setTimezone($timezone)
	{
		return Time::parse($this->toDateTimeString(), $timezone, $this->locale);
	}

	/**
	 * Returns a new instance with the date set to the new timestamp.
	 *
	 * @param integer $timestamp
	 *
	 * @return \CodeIgniter\I18n\Time
	 * @throws \Exception
	 */
	public function setTimestamp($timestamp)
	{
		$time = date('Y-m-d H:i:s', $timestamp);

		return Time::parse($time, $this->timezone, $this->locale);
	}

	//--------------------------------------------------------------------
	// Add/Subtract
	//--------------------------------------------------------------------

	/**
	 * Returns a new Time instance with $seconds added to the time.
	 *
	 * @param integer $seconds
	 *
	 * @return static
	 */
	public function addSeconds(int $seconds)
	{
		$time = clone($this);

		return $time->add(DateInterval::createFromDateString("{$seconds} seconds"));
	}

	/**
	 * Returns a new Time instance with $minutes added to the time.
	 *
	 * @param integer $minutes
	 *
	 * @return static
	 */
	public function addMinutes(int $minutes)
	{
		$time = clone($this);

		return $time->add(DateInterval::createFromDateString("{$minutes} minutes"));
	}

	/**
	 * Returns a new Time instance with $hours added to the time.
	 *
	 * @param integer $hours
	 *
	 * @return static
	 */
	public function addHours(int $hours)
	{
		$time = clone($this);

		return $time->add(DateInterval::createFromDateString("{$hours} hours"));
	}

	/**
	 * Returns a new Time instance with $days added to the time.
	 *
	 * @param integer $days
	 *
	 * @return static
	 */
	public function addDays(int $days)
	{
		$time = clone($this);

		return $time->add(DateInterval::createFromDateString("{$days} days"));
	}

	/**
	 * Returns a new Time instance with $months added to the time.
	 *
	 * @param integer $months
	 *
	 * @return static
	 */
	public function addMonths(int $months)
	{
		$time = clone($this);

		return $time->add(DateInterval::createFromDateString("{$months} months"));
	}

	/**
	 * Returns a new Time instance with $years added to the time.
	 *
	 * @param integer $years
	 *
	 * @return static
	 */
	public function addYears(int $years)
	{
		$time = clone($this);

		return $time->add(DateInterval::createFromDateString("{$years} years"));
	}

	/**
	 * Returns a new Time instance with $seconds subtracted from the time.
	 *
	 * @param integer $seconds
	 *
	 * @return static
	 */
	public function subSeconds(int $seconds)
	{
		$time = clone($this);

		return $time->sub(DateInterval::createFromDateString("{$seconds} seconds"));
	}

	/**
	 * Returns a new Time instance with $minutes subtracted from the time.
	 *
	 * @param integer $minutes
	 *
	 * @return static
	 */
	public function subMinutes(int $minutes)
	{
		$time = clone($this);

		return $time->sub(DateInterval::createFromDateString("{$minutes} minutes"));
	}

	/**
	 * Returns a new Time instance with $hours subtracted from the time.
	 *
	 * @param integer $hours
	 *
	 * @return static
	 */
	public function subHours(int $hours)
	{
		$time = clone($this);

		return $time->sub(DateInterval::createFromDateString("{$hours} hours"));
	}

	/**
	 * Returns a new Time instance with $days subtracted from the time.
	 *
	 * @param integer $days
	 *
	 * @return static
	 */
	public function subDays(int $days)
	{
		$time = clone($this);

		return $time->sub(DateInterval::createFromDateString("{$days} days"));
	}

	/**
	 * Returns a new Time instance with $months subtracted from the time.
	 *
	 * @param integer $months
	 *
	 * @return static
	 */
	public function subMonths(int $months)
	{
		$time = clone($this);

		return $time->sub(DateInterval::createFromDateString("{$months} months"));
	}

	/**
	 * Returns a new Time instance with $hours subtracted from the time.
	 *
	 * @param integer $years
	 *
	 * @return static
	 */
	public function subYears(int $years)
	{
		$time = clone($this);

		return $time->sub(DateInterval::createFromDateString("{$years} years"));
	}

	//--------------------------------------------------------------------
	// Formatters
	//--------------------------------------------------------------------

	/**
	 * Returns the localized value of the date in the format 'Y-m-d H:i:s'
	 *
	 * @throws \Exception
	 */
	public function toDateTimeString()
	{
		return $this->toLocalizedString('yyyy-MM-dd HH:mm:ss');
	}

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

	/**
	 * Returns a localized version of the date in Y-m-d format.
	 *
	 * @return string
	 * @throws \Exception
	 */
	public function toDateString()
	{
		return $this->toLocalizedString('yyyy-MM-dd');
	}

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

	/**
	 * Returns a localized version of the date in nicer date format:
	 *
	 *  i.e. Apr 1, 2017
	 *
	 * @return string
	 * @throws \Exception
	 */
	public function toFormattedDateString()
	{
		return $this->toLocalizedString('MMM d, yyyy');
	}

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

	/**
	 * Returns a localized version of the time in nicer date format:
	 *
	 *  i.e. 13:20:33
	 *
	 * @return string
	 * @throws \Exception
	 */
	public function toTimeString()
	{
		return $this->toLocalizedString('HH:mm:ss');
	}

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

	/**
	 * Returns the localized value of this instance in $format.
	 *
	 * @param string|null $format
	 *
	 * @return string|boolean
	 * @throws \Exception
	 */
	public function toLocalizedString(?string $format = null)
	{
		$format = $format ?? $this->toStringFormat;

		return IntlDateFormatter::formatObject($this->toDateTime(), $format, $this->locale);
	}

	//--------------------------------------------------------------------
	//--------------------------------------------------------------------
	// Comparison
	//--------------------------------------------------------------------

	/**
	 * Determines if the datetime passed in is equal to the current instance.
	 * Equal in this case means that they represent the same moment in time,
	 * and are not required to be in the same timezone, as both times are
	 * converted to UTC and compared that way.
	 *
	 * @param Time|DateTime|string $testTime
	 * @param string|null          $timezone
	 *
	 * @return boolean
	 * @throws \Exception
	 */
	public function equals($testTime, string $timezone = null): bool
	{
		$testTime = $this->getUTCObject($testTime, $timezone);

		$ourTime = $this->toDateTime()
				->setTimezone(new DateTimeZone('UTC'))
				->format('Y-m-d H:i:s');

		return $testTime->format('Y-m-d H:i:s') === $ourTime;
	}

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

	/**
	 * Ensures that the times are identical, taking timezone into account.
	 *
	 * @param Time|DateTime|string $testTime
	 * @param string|null          $timezone
	 *
	 * @return boolean
	 * @throws \Exception
	 */
	public function sameAs($testTime, string $timezone = null): bool
	{
		if ($testTime instanceof DateTime)
		{
			$testTime = $testTime->format('Y-m-d H:i:s');
		}
		else if (is_string($testTime))
		{
			$timezone = $timezone ?: $this->timezone;
			$timezone = $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone);
			$testTime = new DateTime($testTime, $timezone);
			$testTime = $testTime->format('Y-m-d H:i:s');
		}

		$ourTime = $this->toDateTimeString();

		return $testTime === $ourTime;
	}

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

	/**
	 * Determines if the current instance's time is before $testTime,
	 * after converting to UTC.
	 *
	 * @param $testTime
	 * @param string|null $timezone
	 *
	 * @return boolean
	 * @throws \Exception
	 */
	public function isBefore($testTime, string $timezone = null): bool
	{
		$testTime = $this->getUTCObject($testTime, $timezone)->getTimestamp();
		$ourTime  = $this->getTimestamp();

		return $ourTime < $testTime;
	}

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

	/**
	 * Determines if the current instance's time is after $testTime,
	 * after converting in UTC.
	 *
	 * @param             $testTime
	 * @param string|null $timezone
	 *
	 * @return boolean
	 * @throws \Exception
	 */
	public function isAfter($testTime, string $timezone = null): bool
	{
		$testTime = $this->getUTCObject($testTime, $timezone)->getTimestamp();
		$ourTime  = $this->getTimestamp();

		return $ourTime > $testTime;
	}

	//--------------------------------------------------------------------
	//--------------------------------------------------------------------
	// Differences
	//--------------------------------------------------------------------

	/**
	 * Returns a text string that is easily readable that describes
	 * how long ago, or how long from now, a date is, like:
	 *
	 *  - 3 weeks ago
	 *  - in 4 days
	 *  - 6 hours ago
	 *
	 * @return mixed
	 * @throws \Exception
	 */
	public function humanize()
	{
		$now  = IntlCalendar::fromDateTime(Time::now($this->timezone)->toDateTimeString());
		$time = $this->getCalendar()->getTime();

		$years   = $now->fieldDifference($time, IntlCalendar::FIELD_YEAR);
		$months  = $now->fieldDifference($time, IntlCalendar::FIELD_MONTH);
		$days    = $now->fieldDifference($time, IntlCalendar::FIELD_DAY_OF_YEAR);
		$hours   = $now->fieldDifference($time, IntlCalendar::FIELD_HOUR_OF_DAY);
		$minutes = $now->fieldDifference($time, IntlCalendar::FIELD_MINUTE);

		$phrase = null;

		if ($years !== 0)
		{
			$phrase = lang('Time.years', [abs($years)]);
			$before = $years < 0;
		}
		else if ($months !== 0)
		{
			$phrase = lang('Time.months', [abs($months)]);
			$before = $months < 0;
		}
		else if ($days !== 0 && (abs($days) >= 7))
		{
			$weeks  = ceil($days / 7);
			$phrase = lang('Time.weeks', [abs($weeks)]);
			$before = $days < 0;
		}
		else if ($days !== 0)
		{
			$before = $days < 0;

			// Yesterday/Tomorrow special cases
			if (abs($days) === 1)
			{
				return $before ? lang('Time.yesterday') : lang('Time.tomorrow');
			}

			$phrase = lang('Time.days', [abs($days)]);
		}
		else if ($hours !== 0)
		{
			// Display the actual time instead of a regular phrase.
			return $this->format('g:i a');
		}
		else if ($minutes !== 0)
		{
			$phrase = lang('Time.minutes', [abs($minutes)]);
			$before = $minutes < 0;
		}
		else
		{
			return lang('Time.now');
		}

		return $before ? lang('Time.ago', [$phrase]) : lang('Time.inFuture', [$phrase]);
	}

	/**
	 * @param $testTime
	 * @param string|null $timezone
	 *
	 * @return \CodeIgniter\I18n\TimeDifference
	 * @throws \Exception
	 */
	public function difference($testTime, string $timezone = null)
	{
		$testTime = $this->getUTCObject($testTime, $timezone);
		$ourTime  = $this->getUTCObject($this);

		return new TimeDifference($ourTime, $testTime);
	}

	//--------------------------------------------------------------------
	// Utilities
	//--------------------------------------------------------------------

	/**
	 * Returns a Time instance with the timezone converted to UTC.
	 *
	 * @param $time
	 * @param string|null $timezone
	 *
	 * @return \DateTime|static
	 * @throws \Exception
	 */
	public function getUTCObject($time, string $timezone = null)
	{
		if ($time instanceof Time)
		{
			$time = $time->toDateTime()
					->setTimezone(new DateTimeZone('UTC'));
		}
		else if ($time instanceof DateTime)
		{
			$time = $time->setTimezone(new DateTimeZone('UTC'));
		}
		else if (is_string($time))
		{
			$timezone = $timezone ?: $this->timezone;
			$timezone = $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone);
			$time     = new DateTime($time, $timezone);
			$time     = $time->setTimezone(new DateTimeZone('UTC'));
		}

		return $time;
	}

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

	/**
	 * Returns the IntlCalendar object used for this object,
	 * taking into account the locale, date, etc.
	 *
	 * Primarily used internally to provide the difference and comparison functions,
	 * but available for public consumption if they need it.
	 *
	 * @return \IntlCalendar
	 * @throws \Exception
	 */
	public function getCalendar()
	{
		return IntlCalendar::fromDateTime($this->toDateTimeString());
	}

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

	/**
	 * Check a time string to see if it includes a relative date (like 'next Tuesday').
	 *
	 * @param string $time
	 *
	 * @return boolean
	 */
	protected static function hasRelativeKeywords(string $time): bool
	{
		// skip common format with a '-' in it
		if (preg_match('/[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}/', $time) !== 1)
		{
			return preg_match(static::$relativePattern, $time) > 0;
		}

		return false;
	}

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

	/**
	 * Outputs a short format version of the datetime.
	 *
	 * @return string
	 * @throws \Exception
	 */
	public function __toString(): string
	{
		return IntlDateFormatter::formatObject($this->toDateTime(), $this->toStringFormat, $this->locale);
	}

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

	/**
	 * Allow for property-type access to any getX method...
	 *
	 * Note that we cannot use this for any of our setX methods,
	 * as they return new Time objects, but the __set ignores
	 * return values.
	 * See http://php.net/manual/en/language.oop5.overloading.php
	 *
	 * @param $name
	 *
	 * @return mixed
	 */
	public function __get($name)
	{
		$method = 'get' . ucfirst($name);

		if (method_exists($this, $method))
		{
			return $this->$method();
		}

		return null;
	}

}


<?php
/**
 * Zend Framework (http://framework.zend.com/)
 *
 * @link      http://github.com/zendframework/zf2 for the canonical source repository
 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
 * @license   http://framework.zend.com/license/new-bsd New BSD License
 */

namespace Zend\Escaper;

/**
 * Context specific methods for use in secure output escaping
 */
class Escaper
{
    /**
     * Entity Map mapping Unicode codepoints to any available named HTML entities.
     *
     * While HTML supports far more named entities, the lowest common denominator
     * has become HTML5's XML Serialisation which is restricted to the those named
     * entities that XML supports. Using HTML entities would result in this error:
     *     XML Parsing Error: undefined entity
     *
     * @var array
     */
    protected static $htmlNamedEntityMap = [
        34 => 'quot',         // quotation mark
        38 => 'amp',          // ampersand
        60 => 'lt',           // less-than sign
        62 => 'gt',           // greater-than sign
    ];

    /**
     * Current encoding for escaping. If not UTF-8, we convert strings from this encoding
     * pre-escaping and back to this encoding post-escaping.
     *
     * @var string
     */
    protected $encoding = 'utf-8';

    /**
     * Holds the value of the special flags passed as second parameter to
     * htmlspecialchars().
     *
     * @var int
     */
    protected $htmlSpecialCharsFlags;

    /**
     * Static Matcher which escapes characters for HTML Attribute contexts
     *
     * @var callable
     */
    protected $htmlAttrMatcher;

    /**
     * Static Matcher which escapes characters for Javascript contexts
     *
     * @var callable
     */
    protected $jsMatcher;

    /**
     * Static Matcher which escapes characters for CSS Attribute contexts
     *
     * @var callable
     */
    protected $cssMatcher;

    /**
     * List of all encoding supported by this class
     *
     * @var array
     */
    protected $supportedEncodings = [
        'iso-8859-1',   'iso8859-1',    'iso-8859-5',   'iso8859-5',
        'iso-8859-15',  'iso8859-15',   'utf-8',        'cp866',
        'ibm866',       '866',          'cp1251',       'windows-1251',
        'win-1251',     '1251',         'cp1252',       'windows-1252',
        '1252',         'koi8-r',       'koi8-ru',      'koi8r',
        'big5',         '950',          'gb2312',       '936',
        'big5-hkscs',   'shift_jis',    'sjis',         'sjis-win',
        'cp932',        '932',          'euc-jp',       'eucjp',
        'eucjp-win',    'macroman'
    ];

    /**
     * Constructor: Single parameter allows setting of global encoding for use by
     * the current object.
     *
     * @param string $encoding
     * @throws Exception\InvalidArgumentException
     */
    public function __construct($encoding = null)
    {
        if ($encoding !== null) {
            if (! is_string($encoding)) {
                throw new Exception\InvalidArgumentException(
                    get_class($this) . ' constructor parameter must be a string, received ' . gettype($encoding)
                );
            }
            if ($encoding === '') {
                throw new Exception\InvalidArgumentException(
                    get_class($this) . ' constructor parameter does not allow a blank value'
                );
            }

            $encoding = strtolower($encoding);
            if (! in_array($encoding, $this->supportedEncodings)) {
                throw new Exception\InvalidArgumentException(
                    'Value of \'' . $encoding . '\' passed to ' . get_class($this)
                    . ' constructor parameter is invalid. Provide an encoding supported by htmlspecialchars()'
                );
            }

            $this->encoding = $encoding;
        }

        // We take advantage of ENT_SUBSTITUTE flag to correctly deal with invalid UTF-8 sequences.
        $this->htmlSpecialCharsFlags = ENT_QUOTES | ENT_SUBSTITUTE;

        // set matcher callbacks
        $this->htmlAttrMatcher = [$this, 'htmlAttrMatcher'];
        $this->jsMatcher       = [$this, 'jsMatcher'];
        $this->cssMatcher      = [$this, 'cssMatcher'];
    }

    /**
     * Return the encoding that all output/input is expected to be encoded in.
     *
     * @return string
     */
    public function getEncoding()
    {
        return $this->encoding;
    }

    /**
     * Escape a string for the HTML Body context where there are very few characters
     * of special meaning. Internally this will use htmlspecialchars().
     *
     * @param string $string
     * @return string
     */
    public function escapeHtml($string)
    {
        return htmlspecialchars($string, $this->htmlSpecialCharsFlags, $this->encoding);
    }

    /**
     * Escape a string for the HTML Attribute context. We use an extended set of characters
     * to escape that are not covered by htmlspecialchars() to cover cases where an attribute
     * might be unquoted or quoted illegally (e.g. backticks are valid quotes for IE).
     *
     * @param string $string
     * @return string
     */
    public function escapeHtmlAttr($string)
    {
        $string = $this->toUtf8($string);
        if ($string === '' || ctype_digit($string)) {
            return $string;
        }

        $result = preg_replace_callback('/[^a-z0-9,\.\-_]/iSu', $this->htmlAttrMatcher, $string);
        return $this->fromUtf8($result);
    }

    /**
     * Escape a string for the Javascript context. This does not use json_encode(). An extended
     * set of characters are escaped beyond ECMAScript's rules for Javascript literal string
     * escaping in order to prevent misinterpretation of Javascript as HTML leading to the
     * injection of special characters and entities. The escaping used should be tolerant
     * of cases where HTML escaping was not applied on top of Javascript escaping correctly.
     * Backslash escaping is not used as it still leaves the escaped character as-is and so
     * is not useful in a HTML context.
     *
     * @param string $string
     * @return string
     */
    public function escapeJs($string)
    {
        $string = $this->toUtf8($string);
        if ($string === '' || ctype_digit($string)) {
            return $string;
        }

        $result = preg_replace_callback('/[^a-z0-9,\._]/iSu', $this->jsMatcher, $string);
        return $this->fromUtf8($result);
    }

    /**
     * Escape a string for the URI or Parameter contexts. This should not be used to escape
     * an entire URI - only a subcomponent being inserted. The function is a simple proxy
     * to rawurlencode() which now implements RFC 3986 since PHP 5.3 completely.
     *
     * @param string $string
     * @return string
     */
    public function escapeUrl($string)
    {
        return rawurlencode($string);
    }

    /**
     * Escape a string for the CSS context. CSS escaping can be applied to any string being
     * inserted into CSS and escapes everything except alphanumerics.
     *
     * @param string $string
     * @return string
     */
    public function escapeCss($string)
    {
        $string = $this->toUtf8($string);
        if ($string === '' || ctype_digit($string)) {
            return $string;
        }

        $result = preg_replace_callback('/[^a-z0-9]/iSu', $this->cssMatcher, $string);
        return $this->fromUtf8($result);
    }

    /**
     * Callback function for preg_replace_callback that applies HTML Attribute
     * escaping to all matches.
     *
     * @param array $matches
     * @return string
     */
    protected function htmlAttrMatcher($matches)
    {
        $chr = $matches[0];
        $ord = ord($chr);

        /**
         * The following replaces characters undefined in HTML with the
         * hex entity for the Unicode replacement character.
         */
        if (($ord <= 0x1f && $chr != "\t" && $chr != "\n" && $chr != "\r")
            || ($ord >= 0x7f && $ord <= 0x9f)
        ) {
            return '�';
        }

        /**
         * Check if the current character to escape has a name entity we should
         * replace it with while grabbing the integer value of the character.
         */
        if (strlen($chr) > 1) {
            $chr = $this->convertEncoding($chr, 'UTF-32BE', 'UTF-8');
        }

        $hex = bin2hex($chr);
        $ord = hexdec($hex);
        if (isset(static::$htmlNamedEntityMap[$ord])) {
            return '&' . static::$htmlNamedEntityMap[$ord] . ';';
        }

        /**
         * Per OWASP recommendations, we'll use upper hex entities
         * for any other characters where a named entity does not exist.
         */
        if ($ord > 255) {
            return sprintf('&#x%04X;', $ord);
        }
        return sprintf('&#x%02X;', $ord);
    }

    /**
     * Callback function for preg_replace_callback that applies Javascript
     * escaping to all matches.
     *
     * @param array $matches
     * @return string
     */
    protected function jsMatcher($matches)
    {
        $chr = $matches[0];
        if (strlen($chr) == 1) {
            return sprintf('\\x%02X', ord($chr));
        }
        $chr = $this->convertEncoding($chr, 'UTF-16BE', 'UTF-8');
        $hex = strtoupper(bin2hex($chr));
        if (strlen($hex) <= 4) {
            return sprintf('\\u%04s', $hex);
        }
        $highSurrogate = substr($hex, 0, 4);
        $lowSurrogate = substr($hex, 4, 4);
        return sprintf('\\u%04s\\u%04s', $highSurrogate, $lowSurrogate);
    }

    /**
     * Callback function for preg_replace_callback that applies CSS
     * escaping to all matches.
     *
     * @param array $matches
     * @return string
     */
    protected function cssMatcher($matches)
    {
        $chr = $matches[0];
        if (strlen($chr) == 1) {
            $ord = ord($chr);
        } else {
            $chr = $this->convertEncoding($chr, 'UTF-32BE', 'UTF-8');
            $ord = hexdec(bin2hex($chr));
        }
        return sprintf('\\%X ', $ord);
    }

    /**
     * Converts a string to UTF-8 from the base encoding. The base encoding is set via this
     * class' constructor.
     *
     * @param string $string
     * @throws Exception\RuntimeException
     * @return string
     */
    protected function toUtf8($string)
    {
        if ($this->getEncoding() === 'utf-8') {
            $result = $string;
        } else {
            $result = $this->convertEncoding($string, 'UTF-8', $this->getEncoding());
        }

        if (! $this->isUtf8($result)) {
            throw new Exception\RuntimeException(
                sprintf('String to be escaped was not valid UTF-8 or could not be converted: %s', $result)
            );
        }

        return $result;
    }

    /**
     * Converts a string from UTF-8 to the base encoding. The base encoding is set via this
     * class' constructor.
     * @param string $string
     * @return string
     */
    protected function fromUtf8($string)
    {
        if ($this->getEncoding() === 'utf-8') {
            return $string;
        }

        return $this->convertEncoding($string, $this->getEncoding(), 'UTF-8');
    }

    /**
     * Checks if a given string appears to be valid UTF-8 or not.
     *
     * @param string $string
     * @return bool
     */
    protected function isUtf8($string)
    {
        return ($string === '' || preg_match('/^./su', $string));
    }

    /**
     * Encoding conversion helper which wraps iconv and mbstring where they exist or throws
     * and exception where neither is available.
     *
     * @param string $string
     * @param string $to
     * @param array|string $from
     * @throws Exception\RuntimeException
     * @return string
     */
    protected function convertEncoding($string, $to, $from)
    {
        if (function_exists('iconv')) {
            $result = iconv($from, $to, $string);
        } elseif (function_exists('mb_convert_encoding')) {
            $result = mb_convert_encoding($string, $to, $from);
        } else {
            throw new Exception\RuntimeException(
                get_class($this)
                . ' requires either the iconv or mbstring extension to be installed'
                . ' when escaping for non UTF-8 strings.'
            );
        }

        if ($result === false) {
            return ''; // return non-fatal blank string on encoding errors from users
        }
        return $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\Cache\Handlers;

use CodeIgniter\Cache\CacheInterface;
use CodeIgniter\Exceptions\CriticalError;

/**
 * Predis cache handler
 */
class PredisHandler implements CacheInterface
{

	/**
	 * Prefixed to all cache names.
	 *
	 * @var string
	 */
	protected $prefix;

	/**
	 * Default config
	 *
	 * @static
	 * @var    array
	 */
	protected $config = [
		'scheme'   => 'tcp',
		'host'     => '127.0.0.1',
		'password' => null,
		'port'     => 6379,
		'timeout'  => 0,
	];

	/**
	 * Predis connection
	 *
	 * @var Predis
	 */
	protected $redis;

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

	public function __construct($config)
	{
		$this->prefix = $config->prefix ?: '';

		if (isset($config->redis))
		{
			$this->config = array_merge($this->config, $config->redis);
		}
	}

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

	/**
	 * Takes care of any handler-specific setup that must be done.
	 */
	public function initialize()
	{
		try
		{
			// Create a new instance of Predis\Client
			$this->redis = new \Predis\Client($this->config, ['prefix' => $this->prefix]);

			// Check if the connection is valid by trying to get the time.
			$this->redis->time();
		}
		catch (\Exception $e)
		{
			// thrown if can't connect to redis server.
			throw new CriticalError('Cache: Predis connection refused (' . $e->getMessage() . ')');
		}
	}

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

	/**
	 * Attempts to fetch an item from the cache store.
	 *
	 * @param string $key Cache item name
	 *
	 * @return mixed
	 */
	public function get(string $key)
	{
		$data = array_combine([
			'__ci_type',
			'__ci_value',
		], $this->redis->hmget($key, ['__ci_type', '__ci_value'])
		);

		if (! isset($data['__ci_type'], $data['__ci_value']) || $data['__ci_value'] === false)
		{
			return null;
		}

		switch ($data['__ci_type'])
		{
			case 'array':
			case 'object':
				return unserialize($data['__ci_value']);
			case 'boolean':
			case 'integer':
			case 'double': // Yes, 'double' is returned and NOT 'float'
			case 'string':
			case 'NULL':
				return settype($data['__ci_value'], $data['__ci_type']) ? $data['__ci_value'] : null;
			case 'resource':
			default:
				return null;
		}
	}

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

	/**
	 * Saves an item to the cache store.
	 *
	 * @param string  $key   Cache item name
	 * @param mixed   $value The data to save
	 * @param integer $ttl   Time To Live, in seconds (default 60)
	 *
	 * @return mixed
	 */
	public function save(string $key, $value, int $ttl = 60)
	{
		switch ($data_type = gettype($value))
		{
			case 'array':
			case 'object':
				$value = serialize($value);
				break;
			case 'boolean':
			case 'integer':
			case 'double': // Yes, 'double' is returned and NOT 'float'
			case 'string':
			case 'NULL':
				break;
			case 'resource':
			default:
				return false;
		}

		if (! $this->redis->hmset($key, ['__ci_type' => $data_type, '__ci_value' => $value]))
		{
			return false;
		}

		$this->redis->expireat($key, time() + $ttl);

		return true;
	}

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

	/**
	 * Deletes a specific item from the cache store.
	 *
	 * @param string $key Cache item name
	 *
	 * @return mixed
	 */
	public function delete(string $key)
	{
		return ($this->redis->del($key) === 1);
	}

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

	/**
	 * Performs atomic incrementation of a raw stored value.
	 *
	 * @param string  $key    Cache ID
	 * @param integer $offset Step/value to increase by
	 *
	 * @return mixed
	 */
	public function increment(string $key, int $offset = 1)
	{
		return $this->redis->hincrby($key, 'data', $offset);
	}

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

	/**
	 * Performs atomic decrementation of a raw stored value.
	 *
	 * @param string  $key    Cache ID
	 * @param integer $offset Step/value to increase by
	 *
	 * @return mixed
	 */
	public function decrement(string $key, int $offset = 1)
	{
		return $this->redis->hincrby($key, 'data', -$offset);
	}

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

	/**
	 * Will delete all items in the entire cache.
	 *
	 * @return mixed
	 */
	public function clean()
	{
		return $this->redis->flushdb();
	}

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

	/**
	 * Returns information on the entire cache.
	 *
	 * The information returned and the structure of the data
	 * varies depending on the handler.
	 *
	 * @return mixed
	 */
	public function getCacheInfo()
	{
		return $this->redis->info();
	}

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

	/**
	 * Returns detailed information about the specific item in the cache.
	 *
	 * @param string $key Cache item name.
	 *
	 * @return mixed
	 */
	public function getMetaData(string $key)
	{
		$data = array_combine(['__ci_value'], $this->redis->hmget($key, ['__ci_value']));

		if (isset($data['__ci_value']) && $data['__ci_value'] !== false)
		{
			return [
				'expire' => time() + $this->redis->ttl($key),
				'data'   => $data['__ci_value'],
			];
		}

		return false;
	}

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

	/**
	 * Determines if the driver is supported on this system.
	 *
	 * @return boolean
	 */
	public function isSupported(): bool
	{
		return class_exists('\Predis\Client');
	}

}


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

/**
 * Class FileCollection
 *
 * Provides easy access to uploaded files for a request.
 *
 * @package CodeIgniter\HTTP\Files
 */
class FileCollection
{

	/**
	 * An array of UploadedFile instances for any files
	 * uploaded as part of this request.
	 * Populated the first time either files(), file(), or hasFile()
	 * is called.
	 *
	 * @var array|null
	 */
	protected $files;

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

	/**
	 * Returns an array of all uploaded files that were found.
	 * Each element in the array will be an instance of UploadedFile.
	 * The key of each element will be the client filename.
	 *
	 * @return array|null
	 */
	public function all()
	{
		$this->populateFiles();

		return $this->files;
	}

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

	/**
	 * Attempts to get a single file from the collection of uploaded files.
	 *
	 * @param string $name
	 *
	 * @return UploadedFile|null
	 */
	public function getFile(string $name)
	{
		$this->populateFiles();

		if ($this->hasFile($name))
		{
			if (strpos($name, '.') !== false)
			{
				$name         = explode('.', $name);
				$uploadedFile = $this->getValueDotNotationSyntax($name, $this->files);
				return ($uploadedFile instanceof UploadedFile) ?
					 $uploadedFile : null;
			}

			if (array_key_exists($name, $this->files))
			{
				$uploadedFile = $this->files[$name];
				return  ($uploadedFile instanceof UploadedFile) ?
					$uploadedFile : null;
			}
		}

		return null;
	}

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

	/**
	 * Checks whether an uploaded file with name $fileID exists in
	 * this request.
	 *
	 * @param string $fileID The name of the uploaded file (from the input)
	 *
	 * @return boolean
	 */
	public function hasFile(string $fileID): bool
	{
		$this->populateFiles();

		if (strpos($fileID, '.') !== false)
		{
			$segments = explode('.', $fileID);

			$el = $this->files;

			foreach ($segments as $segment)
			{
				if (! array_key_exists($segment, $el))
				{
					return false;
				}

				$el = $el[$segment];
			}

			return true;
		}

		return isset($this->files[$fileID]);
	}

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

	/**
	 * Taking information from the $_FILES array, it creates an instance
	 * of UploadedFile for each one, saving the results to this->files.
	 *
	 * Called by files(), file(), and hasFile()
	 */
	protected function populateFiles()
	{
		if (is_array($this->files))
		{
			return;
		}

		$this->files = [];

		if (empty($_FILES))
		{
			return;
		}

		$files = $this->fixFilesArray($_FILES);

		foreach ($files as $name => $file)
		{
			$this->files[$name] = $this->createFileObject($file);
		}
	}

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

	/**
	 * Given a file array, will create UploadedFile instances. Will
	 * loop over an array and create objects for each.
	 *
	 * @param array $array
	 *
	 * @return array|UploadedFile
	 */
	protected function createFileObject(array $array)
	{
		if (! isset($array['name']))
		{
			$output = [];

			foreach ($array as $key => $values)
			{
				if (! is_array($values))
				{
					continue;
				}

				$output[$key] = $this->createFileObject($values);
			}

			return $output;
		}

		return new UploadedFile(
				$array['tmp_name'] ?? null, $array['name'] ?? null, $array['type'] ?? null, $array['size'] ?? null, $array['error'] ?? null
		);
	}

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

	/**
	 * Reformats the odd $_FILES array into something much more like
	 * we would expect, with each object having its own array.
	 *
	 * Thanks to Jack Sleight on the PHP Manual page for the basis
	 * of this method.
	 *
	 * @see http://php.net/manual/en/reserved.variables.files.php#118294
	 *
	 * @param array $data
	 *
	 * @return array
	 */
	protected function fixFilesArray(array $data): array
	{
		$output = [];

		foreach ($data as $name => $array)
		{
			foreach ($array as $field => $value)
			{
				$pointer = &$output[$name];

				if (! is_array($value))
				{
					$pointer[$field] = $value;
					continue;
				}

				$stack    = [&$pointer];
				$iterator = new \RecursiveIteratorIterator(
						new \RecursiveArrayIterator($value), \RecursiveIteratorIterator::SELF_FIRST
				);

				foreach ($iterator as $key => $val)
				{
					array_splice($stack, $iterator->getDepth() + 1);
					$pointer = &$stack[count($stack) - 1];
					$pointer = &$pointer[$key];
					$stack[] = &$pointer;
					if (! $iterator->hasChildren())
					{
						$pointer[$field] = $val;
					}
				}
			}
		}

		return $output;
	}

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

	/**
	 * Navigate through a array looking for a particular index
	 *
	 * @param array $index The index sequence we are navigating down
	 * @param array $value The portion of the array to process
	 *
	 * @return mixed
	 */
	protected function getValueDotNotationSyntax(array $index, array $value)
	{
		if (is_array($index) && ! empty($index))
		{
			$current_index = array_shift($index);
		}
		if (is_array($index) && $index && is_array($value[$current_index]) && $value[$current_index])
		{
			return $this->getValueDotNotationSyntax($index, $value[$current_index]);
		}

		return (isset($value[$current_index])) ? $value[$current_index] : 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\Commands\Utilities;

use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use Config\Services;

/**
 * Lists all of the user-defined routes. This will include any Routes files
 * that can be discovered, but will NOT include any routes that are not defined
 * in a routes file, but are instead discovered through auto-routing.
 *
 * @package CodeIgniter\Commands
 */
class Routes extends BaseCommand
{

	/**
	 * The group the command is lumped under
	 * when listing commands.
	 *
	 * @var string
	 */
	protected $group = 'CodeIgniter';

	/**
	 * The Command's name
	 *
	 * @var string
	 */
	protected $name = 'routes';

	/**
	 * the Command's short description
	 *
	 * @var string
	 */
	protected $description = 'Displays all of user-defined routes. Does NOT display auto-detected routes.';

	/**
	 * the Command's usage
	 *
	 * @var string
	 */
	protected $usage = 'routes';

	/**
	 * the Command's Arguments
	 *
	 * @var array
	 */
	protected $arguments = [];

	/**
	 * the Command's Options
	 *
	 * @var array
	 */
	protected $options = [];

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

	/**
	 * Displays the help for the spark cli script itself.
	 *
	 * @param array $params
	 */
	public function run(array $params)
	{
		$collection = Services::routes(true);
		$methods    = [
			'get',
			'head',
			'post',
			'put',
			'delete',
			'options',
			'trace',
			'connect',
			'cli',
		];

		$tbody = [];
		foreach ($methods as $method)
		{
			$routes = $collection->getRoutes($method);

			foreach ($routes as $from => $to)
			{
				// filter for strings, as callbacks aren't displayable
				if (is_string($to))
				{
					$tbody[] = [
						$from,
						$method,
						$to,
					];
				}
			}
		}

		$thead = [
			'Route',
			'Method',
			'Command',
		];

		CLI::table($tbody, $thead);
	}

}


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

use CodeIgniter\Cache\CacheInterface;

/**
 * Redis cache handler
 */
class RedisHandler implements CacheInterface
{

	/**
	 * Prefixed to all cache names.
	 *
	 * @var string
	 */
	protected $prefix;

	/**
	 * Default config
	 *
	 * @static
	 * @var    array
	 */
	protected $config = [
		'host'     => '127.0.0.1',
		'password' => null,
		'port'     => 6379,
		'timeout'  => 0,
		'database' => 0,
	];

	/**
	 * Redis connection
	 *
	 * @var Redis
	 */
	protected $redis;

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

	public function __construct($config)
	{
		$config       = (array)$config;
		$this->prefix = $config['prefix'] ?? '';

		if (! empty($config))
		{
			$this->config = array_merge($this->config, $config['redis']);
		}
	}

	/**
	 * Class destructor
	 *
	 * Closes the connection to Memcache(d) if present.
	 */
	public function __destruct()
	{
		if ($this->redis)
		{
			$this->redis->close();
		}
	}

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

	/**
	 * Takes care of any handler-specific setup that must be done.
	 */
	public function initialize()
	{
		$config = $this->config;

		$this->redis = new \Redis();
		if (! $this->redis->connect($config['host'], ($config['host'][0] === '/' ? 0 : $config['port']), $config['timeout']))
		{
			log_message('error', 'Cache: Redis connection failed. Check your configuration.');
		}

		if (isset($config['password']) && ! $this->redis->auth($config['password']))
		{
			log_message('error', 'Cache: Redis authentication failed.');
		}

		if (isset($config['database']) && ! $this->redis->select($config['database']))
		{
			log_message('error', 'Cache: Redis select database failed.');
		}
	}

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

	/**
	 * Attempts to fetch an item from the cache store.
	 *
	 * @param string $key Cache item name
	 *
	 * @return mixed
	 */
	public function get(string $key)
	{
		$key = $this->prefix . $key;

		$data = $this->redis->hMGet($key, ['__ci_type', '__ci_value']);

		if (! isset($data['__ci_type'], $data['__ci_value']) || $data['__ci_value'] === false)
		{
			return null;
		}

		switch ($data['__ci_type'])
		{
			case 'array':
			case 'object':
				return unserialize($data['__ci_value']);
			case 'boolean':
			case 'integer':
			case 'double': // Yes, 'double' is returned and NOT 'float'
			case 'string':
			case 'NULL':
				return settype($data['__ci_value'], $data['__ci_type']) ? $data['__ci_value'] : null;
			case 'resource':
			default:
				return null;
		}
	}

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

	/**
	 * Saves an item to the cache store.
	 *
	 * @param string  $key   Cache item name
	 * @param mixed   $value The data to save
	 * @param integer $ttl   Time To Live, in seconds (default 60)
	 *
	 * @return mixed
	 */
	public function save(string $key, $value, int $ttl = 60)
	{
		$key = $this->prefix . $key;

		switch ($data_type = gettype($value))
		{
			case 'array':
			case 'object':
				$value = serialize($value);
				break;
			case 'boolean':
			case 'integer':
			case 'double': // Yes, 'double' is returned and NOT 'float'
			case 'string':
			case 'NULL':
				break;
			case 'resource':
			default:
				return false;
		}

		if (! $this->redis->hMSet($key, ['__ci_type' => $data_type, '__ci_value' => $value]))
		{
			return false;
		}
		elseif ($ttl)
		{
			$this->redis->expireAt($key, time() + $ttl);
		}

		return true;
	}

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

	/**
	 * Deletes a specific item from the cache store.
	 *
	 * @param string $key Cache item name
	 *
	 * @return mixed
	 */
	public function delete(string $key)
	{
		$key = $this->prefix . $key;

		return ($this->redis->delete($key) === 1);
	}

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

	/**
	 * Performs atomic incrementation of a raw stored value.
	 *
	 * @param string  $key    Cache ID
	 * @param integer $offset Step/value to increase by
	 *
	 * @return mixed
	 */
	public function increment(string $key, int $offset = 1)
	{
		$key = $this->prefix . $key;

		return $this->redis->hIncrBy($key, 'data', $offset);
	}

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

	/**
	 * Performs atomic decrementation of a raw stored value.
	 *
	 * @param string  $key    Cache ID
	 * @param integer $offset Step/value to increase by
	 *
	 * @return mixed
	 */
	public function decrement(string $key, int $offset = 1)
	{
		$key = $this->prefix . $key;

		return $this->redis->hIncrBy($key, 'data', -$offset);
	}

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

	/**
	 * Will delete all items in the entire cache.
	 *
	 * @return mixed
	 */
	public function clean()
	{
		return $this->redis->flushDB();
	}

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

	/**
	 * Returns information on the entire cache.
	 *
	 * The information returned and the structure of the data
	 * varies depending on the handler.
	 *
	 * @return mixed
	 */
	public function getCacheInfo()
	{
		return $this->redis->info();
	}

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

	/**
	 * Returns detailed information about the specific item in the cache.
	 *
	 * @param string $key Cache item name.
	 *
	 * @return mixed
	 */
	public function getMetaData(string $key)
	{
		$key = $this->prefix . $key;

		$value = $this->get($key);

		if ($value !== null)
		{
			$time = time();
			return [
				'expire' => $time + $this->redis->ttl($key),
				'mtime'  => $time,
				'data'   => $value,
			];
		}

		return null;
	}

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

	/**
	 * Determines if the driver is supported on this system.
	 *
	 * @return boolean
	 */
	public function isSupported(): bool
	{
		return extension_loaded('redis');
	}

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


<?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 4.0.0
 * @filesource
 */

namespace CodeIgniter\HTTP;

use CodeIgniter\Exceptions\DownloadException;
use CodeIgniter\Files\File;
use Config\Mimes;

class DownloadResponse extends Message implements ResponseInterface
{
	/**
	 * Download file name
	 *
	 * @var string
	 */
	private $filename;

	/**
	 * Download for file
	 *
	 * @var File?
	 */
	private $file;

	/**
	 * mime set flag
	 *
	 * @var boolean
	 */
	private $setMime;

	/**
	 * Download for binary
	 *
	 * @var string
	 */
	private $binary;

	/**
	 * Download reason
	 *
	 * @var string
	 */
	private $reason = 'OK';

	/**
	 * Download charset
	 *
	 * @var string
	 */
	private $charset = 'UTF-8';

	/**
	 * pretend
	 *
	 * @var boolean
	 */
	private $pretend = false;

	public function __construct(string $filename, bool $setMime)
	{
		$this->filename = $filename;
		$this->setMime  = $setMime;
	}

	/**
	 * set download for binary string.
	 *
	 * @param string $binary
	 */
	public function setBinary(string $binary)
	{
		if ($this->file !== null)
		{
			throw DownloadException::forCannotSetBinary();
		}

		$this->binary = $binary;
	}

	/**
	 * set download for file.
	 *
	 * @param string $filepath
	 */
	public function setFilePath(string $filepath)
	{
		if ($this->binary !== null)
		{
			throw DownloadException::forCannotSetFilePath($filepath);
		}

		$this->file = new File($filepath, true);
	}

	/**
	 * get content length.
	 *
	 * @return integer
	 */
	public function getContentLength() : int
	{
		if (is_string($this->binary))
		{
			return strlen($this->binary);
		}
		elseif ($this->file instanceof File)
		{
			return $this->file->getSize();
		}

		return 0;
	}

	/**
	 * Set content type by guessing mime type from file extension
	 */
	private function setContentTypeByMimeType()
	{
		$mime    = null;
		$charset = '';

		if ($this->setMime === true)
		{
			if (($last_dot_position = strrpos($this->filename, '.')) !== false)
			{
				$mime    = Mimes::guessTypeFromExtension(substr($this->filename, $last_dot_position + 1));
				$charset = $this->charset;
			}
		}

		if (! is_string($mime))
		{
			// Set the default MIME type to send
			$mime    = 'application/octet-stream';
			$charset = '';
		}

		$this->setContentType($mime, $charset);
	}

	/**
	 * get download filename.
	 *
	 * @return string
	 */
	private function getDownloadFileName(): string
	{
		$filename  = $this->filename;
		$x         = explode('.', $this->filename);
		$extension = end($x);

		/* It was reported that browsers on Android 2.1 (and possibly older as well)
		 * need to have the filename extension upper-cased in order to be able to
		 * download it.
		 *
		 * Reference: http://digiblog.de/2011/04/19/android-and-the-download-file-headers/
		 */
		// @todo: depend super global
		if (count($x) !== 1 && isset($_SERVER['HTTP_USER_AGENT'])
				&& preg_match('/Android\s(1|2\.[01])/', $_SERVER['HTTP_USER_AGENT']))
		{
			$x[count($x) - 1] = strtoupper($extension);
			$filename         = implode('.', $x);
		}

		return $filename;
	}

	/**
	 * get Content-Disposition Header string.
	 *
	 * @return string
	 */
	private function getContentDisposition() : string
	{
		$download_filename = $this->getDownloadFileName();

		$utf8_filename = $download_filename;

		if (strtoupper($this->charset) !== 'UTF-8')
		{
			$utf8_filename = mb_convert_encoding($download_filename, 'UTF-8', $this->charset);
		}

		$result = sprintf('attachment; filename="%s"', $download_filename);

		if (isset($utf8_filename))
		{
			$result .= '; filename*=UTF-8\'\'' . rawurlencode($utf8_filename);
		}

		return $result;
	}

	/**
	 * {@inheritDoc}
	 */
	public function getStatusCode(): int
	{
		return 200;
	}

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

	/**
	 * {@inheritDoc}
	 *
	 * @throws DownloadException
	 */
	public function setStatusCode(int $code, string $reason = '')
	{
		throw DownloadException::forCannotSetStatusCode($code, $reason);
	}

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

	/**
	 * {@inheritDoc}
	 */
	public function getReason(): string
	{
		return $this->reason;
	}

	//--------------------------------------------------------------------
	//--------------------------------------------------------------------
	// Convenience Methods
	//--------------------------------------------------------------------

	/**
	 * {@inheritDoc}
	 */
	public function setDate(\DateTime $date)
	{
		$date->setTimezone(new \DateTimeZone('UTC'));

		$this->setHeader('Date', $date->format('D, d M Y H:i:s') . ' GMT');

		return $this;
	}

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

	/**
	 * {@inheritDoc}
	 */
	public function setContentType(string $mime, string $charset = 'UTF-8')
	{
		// add charset attribute if not already there and provided as parm
		if ((strpos($mime, 'charset=') < 1) && ! empty($charset))
		{
			$mime .= '; charset=' . $charset;
		}

		$this->removeHeader('Content-Type'); // replace existing content type
		$this->setHeader('Content-Type', $mime);
		if (! empty($charset))
		{
			$this->charset = $charset;
		}

		return $this;
	}

	/**
	 * {@inheritDoc}
	 */
	public function noCache(): self
	{
		$this->removeHeader('Cache-control');

		$this->setHeader('Cache-control', ['private', 'no-transform', 'no-store', 'must-revalidate']);

		return $this;
	}

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

	/**
	 * {@inheritDoc}
	 *
	 * @throws DownloadException
	 */
	public function setCache(array $options = [])
	{
		throw DownloadException::forCannotSetCache();
	}

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

	/**
	 * {@inheritDoc}
	 */
	public function setLastModified($date)
	{
		if ($date instanceof \DateTime)
		{
			$date->setTimezone(new \DateTimeZone('UTC'));
			$this->setHeader('Last-Modified', $date->format('D, d M Y H:i:s') . ' GMT');
		}
		elseif (is_string($date))
		{
			$this->setHeader('Last-Modified', $date);
		}

		return $this;
	}

	//--------------------------------------------------------------------
	//--------------------------------------------------------------------
	// Output Methods
	//--------------------------------------------------------------------

	public function pretend(bool $pretend = true)
	{
		$this->pretend = $pretend;

		return $this;
	}

	/**
	 * {@inheritDoc}
	 */
	public function send()
	{
		$this->buildHeaders();
		$this->sendHeaders();
		$this->sendBody();

		return $this;
	}

	/**
	 * set header for file download.
	 */
	public function buildHeaders()
	{
		if (! $this->hasHeader('Content-Type'))
		{
			$this->setContentTypeByMimeType();
		}

		$this->setHeader('Content-Disposition', $this->getContentDisposition());
		$this->setHeader('Expires-Disposition', '0');
		$this->setHeader('Content-Transfer-Encoding', 'binary');
		$this->setHeader('Content-Length', (string)$this->getContentLength());
		$this->noCache();
	}

	/**
	 * Sends the headers of this HTTP request to the browser.
	 *
	 * @return DownloadResponse
	 */
	public function sendHeaders()
	{
		// Have the headers already been sent?
		if ($this->pretend || headers_sent())
		{
			return $this;
		}

		// Per spec, MUST be sent with each request, if possible.
		// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
		if (! isset($this->headers['Date']))
		{
			$this->setDate(\DateTime::createFromFormat('U', time()));
		}

		// HTTP Status
		header(sprintf('HTTP/%s %s %s', $this->protocolVersion, $this->getStatusCode(), $this->getReason()), true,
				$this->getStatusCode());

		// Send all of our headers
		foreach ($this->getHeaders() as $name => $values)
		{
			header($name . ': ' . $this->getHeaderLine($name), false, $this->getStatusCode());
		}

		return $this;
	}

	/**
	 * output download file text.
	 *
	 * @throws DownloadException
	 *
	 * @return DownloadResponse
	 */
	public function sendBody()
	{
		if ($this->binary !== null)
		{
			return $this->sendBodyByBinary();
		}
		elseif ($this->file !== null)
		{
			return $this->sendBodyByFilePath();
		}

		throw DownloadException::forNotFoundDownloadSource();
	}

	/**
	 * output download text by file.
	 *
	 * @return DownloadResponse
	 */
	private function sendBodyByFilePath()
	{
		$spl_file_object = $this->file->openFile('rb');

		// Flush 1MB chunks of data
		while (! $spl_file_object->eof() && ($data = $spl_file_object->fread(1048576)) !== false)
		{
			echo $data;
		}

		return $this;
	}

	/**
	 * output download text by binary
	 *
	 * @return DownloadResponse
	 */
	private function sendBodyByBinary()
	{
		echo $this->binary;

		return $this;
	}
}


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

use CodeIgniter\Config\Services;
use CodeIgniter\Controller;

/**
 * Command runner
 */
class CommandRunner extends Controller
{

	/**
	 * Stores the info about found Commands.
	 *
	 * @var array
	 */
	protected $commands = [];

	/**
	 * @var \CodeIgniter\Log\Logger
	 */
	protected $logger;

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

	/**
	 * We map all un-routed CLI methods through this function
	 * so we have the chance to look for a Command first.
	 *
	 * @param string $method
	 * @param array  ...$params
	 *
	 * @throws \ReflectionException
	 */
	public function _remap($method, ...$params)
	{
		// The first param is usually empty, so scrap it.
		if (empty($params[0]))
		{
			array_shift($params);
		}

		$this->index($params);
	}

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

	/**
	 * @param array $params
	 *
	 * @return mixed
	 * @throws \ReflectionException
	 */
	public function index(array $params)
	{
		$command = array_shift($params);

		$this->createCommandList();

		if (is_null($command))
		{
			$command = 'help';
		}

		return $this->runCommand($command, $params);
	}

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

	/**
	 * Actually runs the command.
	 *
	 * @param string $command
	 * @param array  $params
	 *
	 * @return mixed
	 */
	protected function runCommand(string $command, array $params)
	{
		if (! isset($this->commands[$command]))
		{
			CLI::error(lang('CLI.commandNotFound', [$command]));
			CLI::newLine();
			return;
		}

		// The file would have already been loaded during the
		// createCommandList function...
		$className = $this->commands[$command]['class'];
		$class     = new $className($this->logger, $this);

		return $class->run($params);
	}

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

	/**
	 * Scans all Commands directories and prepares a list
	 * of each command with it's group and file.
	 *
	 * @throws \ReflectionException
	 */
	protected function createCommandList()
	{
		$files = Services::locator()->listFiles('Commands/');

		// If no matching command files were found, bail
		if (empty($files))
		{
			// This should never happen in unit testing.
			// if it does, we have far bigger problems!
			// @codeCoverageIgnoreStart
			return;
			// @codeCoverageIgnoreEnd
		}

		// Loop over each file checking to see if a command with that
		// alias exists in the class. If so, return it. Otherwise, try the next.
		foreach ($files as $file)
		{
			$className = Services::locator()->findQualifiedNameFromPath($file);
			if (empty($className) || ! class_exists($className))
			{
				continue;
			}

			$class = new \ReflectionClass($className);

			if (! $class->isInstantiable() || ! $class->isSubclassOf(BaseCommand::class))
			{
				continue;
			}

			$class = new $className($this->logger, $this);

			// Store it!
			if ($class->group !== null)
			{
				$this->commands[$class->name] = [
					'class'       => $className,
					'file'        => $file,
					'group'       => $class->group,
					'description' => $class->description,
				];
			}

			$class = null;
			unset($class);
		}

		asort($this->commands);
	}

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

	/**
	 * Allows access to the current commands that have been found.
	 *
	 * @return array
	 */
	public function getCommands(): array
	{
		return $this->commands;
	}

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


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

use CodeIgniter\Config\BaseConfig;

/**
 * Class Config
 */
class Config extends BaseConfig
{

	/**
	 * Cache for instance of any connections that
	 * have been requested as a "shared" instance.
	 *
	 * @var array
	 */
	static protected $instances = [];

	/**
	 * The main instance used to manage all of
	 * our open database connections.
	 *
	 * @var \CodeIgniter\Database\Database
	 */
	static protected $factory;

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

	/**
	 * Creates the default
	 *
	 * @param string|array $group     The name of the connection group to use,
	 *                                or an array of configuration settings.
	 * @param boolean      $getShared Whether to return a shared instance of the connection.
	 *
	 * @return BaseConnection
	 */
	public static function connect($group = null, bool $getShared = true)
	{
		// If a DB connection is passed in, just pass it back
		if ($group instanceof BaseConnection)
		{
			return $group;
		}

		if (is_array($group))
		{
			$config = $group;
			$group  = 'custom-' . md5(json_encode($config));
		}

		$config = $config ?? new \Config\Database();

		if (empty($group))
		{
			$group = ENVIRONMENT === 'testing' ? 'tests' : $config->defaultGroup;
		}

		if (is_string($group) && ! isset($config->$group) && strpos($group, 'custom-') !== 0)
		{
			throw new \InvalidArgumentException($group . ' is not a valid database connection group.');
		}

		if ($getShared && isset(static::$instances[$group]))
		{
			return static::$instances[$group];
		}

		static::ensureFactory();

		if (isset($config->$group))
		{
			$config = $config->$group;
		}

		$connection = static::$factory->load($config, $group);

		static::$instances[$group] = & $connection;

		return $connection;
	}

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

	/**
	 * Returns an array of all db connections currently made.
	 *
	 * @return array
	 */
	public static function getConnections(): array
	{
		return static::$instances;
	}

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

	/**
	 * Loads and returns an instance of the Forge for the specified
	 * database group, and loads the group if it hasn't been loaded yet.
	 *
	 * @param string|array|null $group
	 *
	 * @return Forge
	 */
	public static function forge($group = null)
	{
		$db = static::connect($group);

		return static::$factory->loadForge($db);
	}

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

	/**
	 * Returns a new instance of the Database Utilities class.
	 *
	 * @param string|array|null $group
	 *
	 * @return BaseUtils
	 */
	public static function utils($group = null)
	{
		$db = static::connect($group);

		return static::$factory->loadUtils($db);
	}

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

	/**
	 * Returns a new instance of the Database Seeder.
	 *
	 * @param string|null $group
	 *
	 * @return Seeder
	 */
	public static function seeder(string $group = null)
	{
		$config = config('Database');

		return new Seeder($config, static::connect($group));
	}

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

	/**
	 * Ensures the database Connection Manager/Factory is loaded and ready to use.
	 */
	protected static function ensureFactory()
	{
		if (static::$factory instanceof Database)
		{
			return;
		}

		static::$factory = new Database();
	}

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


<?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\Commands\Database;

use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use Config\Services;
use Config\Autoload;

/**
 * Runs all of the migrations in reverse order, until they have
 * all been un-applied.
 *
 * @package CodeIgniter\Commands
 */
class MigrateRollback extends BaseCommand
{

	/**
	 * The group the command is lumped under
	 * when listing commands.
	 *
	 * @var string
	 */
	protected $group = 'Database';

	/**
	 * The Command's name
	 *
	 * @var string
	 */
	protected $name = 'migrate:rollback';

	/**
	 * the Command's short description
	 *
	 * @var string
	 */
	protected $description = 'Runs all of the migrations in reverse order, until they have all been un-applied.';

	/**
	 * the Command's usage
	 *
	 * @var string
	 */
	protected $usage = 'migrate:rollback [Options]';

	/**
	 * the Command's Arguments
	 *
	 * @var array
	 */
	protected $arguments = [];

	/**
	 * the Command's Options
	 *
	 * @var array
	 */
	protected $options = [
		'-n'   => 'Set migration namespace',
		'-g'   => 'Set database group',
		'-all' => 'Set latest for all namespace, will ignore (-n) option',
	];

	/**
	 * Runs all of the migrations in reverse order, until they have
	 * all been un-applied.
	 *
	 * @param array $params
	 */
	public function run(array $params = [])
	{
		$runner = Services::migrations();

		CLI::write(lang('Migrations.rollingBack'), 'yellow');

		$group = $params['-g'] ?? CLI::getOption('g');

		if (! is_null($group))
		{
			$runner->setGroup($group);
		}
		try
		{
			if (! $this->isAllNamespace($params))
			{
				$namespace = $params['-n'] ?? CLI::getOption('n');
				$runner->version(0, $namespace);
			}
			else
			{
				// Get all namespaces form  PSR4 paths.
				$config     = new Autoload();
				$namespaces = $config->psr4;
				foreach ($namespaces as $namespace => $path)
				{
					$runner->setNamespace($namespace);
					$migrations = $runner->findMigrations();
					if (empty($migrations))
					{
						continue;
					}
					$runner->version(0, $namespace, $group);
				}
			}
			$messages = $runner->getCliMessages();
			foreach ($messages as $message)
			{
				CLI::write($message);
			}

			CLI::write('Done');
		}
		catch (\Exception $e)
		{
			$this->showError($e);
		}
	}

	/**
	 * To migrate all namespaces to the latest migration
	 *
	 * Demo:
	 *  1. command line: php spark migrate:latest -all
	 *  2. command file: $this->call('migrate:latest', ['-g' => 'test','-all']);
	 *
	 * @param  array $params
	 * @return boolean
	 */
	private function isAllNamespace(array $params): bool
	{
		if (array_search('-all', $params) !== false)
		{
			return true;
		}

		return ! is_null(CLI::getOption('all'));
	}

}


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

/**
 * Iterator for debugging.
 */
class Iterator
{

	/**
	 * Stores the tests that we are to run.
	 *
	 * @var array
	 */
	protected $tests = [];

	/**
	 * Stores the results of each of the tests.
	 *
	 * @var array
	 */
	protected $results = [];

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

	/**
	 * Adds a test to run.
	 *
	 * Tests are simply closures that the user can define any sequence of
	 * things to happen during the test.
	 *
	 * @param string    $name
	 * @param \Closure  $closure
	 *
	 * @return $this
	 */
	public function add(string $name, \Closure $closure)
	{
		$name = strtolower($name);

		$this->tests[$name] = $closure;

		return $this;
	}

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

	/**
	 * Runs through all of the tests that have been added, recording
	 * time to execute the desired number of iterations, and the approximate
	 * memory usage used during those iterations.
	 *
	 * @param integer $iterations
	 * @param boolean $output
	 *
	 * @return string|null
	 */
	public function run(int $iterations = 1000, bool $output = true)
	{
		foreach ($this->tests as $name => $test)
		{
			// clear memory before start
			gc_collect_cycles();

			$start     = microtime(true);
			$start_mem = $max_memory = memory_get_usage(true);

			for ($i = 0; $i < $iterations; $i ++)
			{
				$result = $test();

				$max_memory = max($max_memory, memory_get_usage(true));

				unset($result);
			}

			$this->results[$name] = [
				'time'   => microtime(true) - $start,
				'memory' => $max_memory - $start_mem,
				'n'      => $iterations,
			];
		}

		if ($output)
		{
			return $this->getReport();
		}

		return null;
	}

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

	/**
	 * Get results.
	 *
	 * @return string
	 */
	public function getReport(): string
	{
		if (empty($this->results))
		{
			return 'No results to display.';
		}

		helper('number');

		// Template
		$tpl = '

					Test
					Time
					Memory

				{rows}

';

		$rows = '';

		foreach ($this->results as $name => $result)
		{
			$memory = number_to_size($result['memory'], 4);

			$rows .= "

				{$name}
				" . number_format($result['time'], 4) . "
				{$memory}

";
		}

		$tpl = str_replace('{rows}', $rows, $tpl);

		return $tpl . '
';
	}

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

}