<?php

/**
 * THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 *
 * @package fusionLib
 * @copyright Copyright (c) 2007 - 2016 fusionLib. All rights reserved.
 * @link http://fusionlib.com
 */

/**
 * flRequest Class
 *
 * Class to provide access to request information and aid in the processing of the request.
 *
 * For mod_rewrite support .htaccess needs to contain:
 * <code>
 * RewriteEngine on
 * # RewriteBase /
 * RewriteCond %{REQUEST_FILENAME} !-f
 * RewriteCond %{REQUEST_FILENAME} !-d
 * RewriteRule ^(.*) index.php
 * </code>
 *
 * @package fusionLib
 */
class flRequest {

	/**#@+
	 * File upload error codes.
	 */
	const UPLOAD_OK = 0;
	const UPLOADERR_SIZE = 1;
	const UPLOADERR_NOFILE = 2;
	const UPLOADERR_WRITE = 3;
	const UPLOADERR_OTHER = 4;
	/**#@-*/

	/**
	 * The base URI for the installation without domain name.
	 *
	 * @var string
	 */
	private $baseURI;

	/**
	 * The path of the site home page within the domain.
	 *
	 * @var string
	 */
	private $sitePath;

	/**
	 * Precalculated base part of absolute URIs, scheme is per request.
	 *
	 * @var string
	 */
	private $absBase;

	/**
	 * Flags if host using mod_rewrite or not.
	 *
	 * @var bool
	 */
	private $usingModRewrite;

	/**
	 * Flags if the request is over https or not.
	 *
	 * @var bool
	 */
	private $isSsl;

	/**
	 * The request path segments.
	 *
	 * @var array
	 */
	private $segments;

	/**
	 * Data read from PUT/DELETE.
	 *
	 * @var array
	 */
	private $putData = '';

	/**
	 * Array of parameters
	 *
	 * @var array
	 */
	private $params = array();

	/**
	 * Construct the request object setting the base URI.
	 *
	 * This is the URI excluding the domain name to the installation, e.g. if not using mod_rewrite
	 * and installed at http://www.example.com/testing/index.php then the base URI will be
	 * '/testing/index.php'. The same address using mod_rewrite would be http://www.example.com/testing/
	 * therefore the base URI should be set to '/testing/';
	 *
	 * @param string $uri The base URI.
	 */
	function __construct($uri) {

		// Save http or https
		$this->isSsl = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on';

		// Save if mod_rewrite or not, ends in / is rewrite
		$this->usingModRewrite = substr($uri, -1) == '/';

		// Save the base uri forcing trailing / & site path
		$this->baseURI = $this->usingModRewrite ? $uri : $uri . '/';
		$this->sitePath = $this->usingModRewrite ? $uri : dirname($uri) . '/';

		// Parse the request URI
		$this->_parseURI($uri);

		// If PUT or DELETE then read data in
		if($_SERVER['REQUEST_METHOD'] == 'PUT' || $_SERVER['REQUEST_METHOD'] == 'DELETE') {
			$fp = fopen('php://input', 'r');
			while($data = fread($fp, 1024))
				$this->putData .= $data;
			fclose($fp);
		}

		// If quoted then traverse data stripping slashes
		if(get_magic_quotes_gpc()) {
			array_walk($_GET, array($this, '_stripSlashes'));
			array_walk($_POST, array($this, '_stripSlashes'));
			array_walk($_COOKIE, array($this, '_stripSlashes'));
		}

		// Merge post and get into params
		$this->params = $_POST + $_GET;

		$this->absBase = ($this->isSsl ? 'https://' : 'http://') . $_SERVER['SERVER_NAME'];
	}

	/**
	 * Private function to parse the request URI.
	 *
	 * Process the request URI to extract the segments that are the path relative to the install.
	 *
	 * @param string $uri The base uri.
	 */
	protected function _parseURI($uri) {
		// If there's a null in the request stop dead, should only happen when under attack
		if(strpos(urldecode($_SERVER['REQUEST_URI']), chr(0)) !== false)
			die();

		// Get the URI and strip off trailing ? and #
		$requestURI = preg_replace('/[\?#].*$/', '', $_SERVER['REQUEST_URI']);

		// Strip the base uri
		$requestURI = preg_replace('|^' . preg_quote($uri) . '|', '', $requestURI);

		// Convert any \ to /
		$requestURI = str_replace('\\', '/', $requestURI);

		// If GET, not a file and no trailing / then do a redirect to / version to mimic apache
		if($this->isGet() && strlen($requestURI) && substr($requestURI, -1) != '/' && !preg_match('/\.[a-z0-9]{1,4}$/', $requestURI)) {
			// Get parameters
			if(get_magic_quotes_gpc())
				array_walk($_GET, array($this, '_stripSlashes'));
			$params = http_build_query($_GET);
			if(strlen($params)) $params = '?' . substr($params, 1);

			// Do redirect
			header('HTTP/1.1 301 Moved Permanently');
			header('Location: ' . ($this->isSsl ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'] . $this->baseURI . ltrim($requestURI, '/') . '/' . $params);
			exit;
		}

		// If empty URI or not mod_rewrite & request matches folder holding site done
		if($requestURI == '' || (!$this->usingModRewrite && ($requestURI == $this->sitePath || $requestURI == '/'))) {
			$this->segments = array();
			return;
		}

		// Trim off trailing slashes, decode & get segments
		$this->segments = explode('/', urldecode(str_replace('+', ' ', trim($requestURI, '/'))));
	}

	/**
	 * Private function to strip slashes from parameters.
	 *
	 * @param mixed $item The item to strip slashes from.
	 */
	private function _stripSlashes(& $item) {
		// Walk arrays and strip values
		if(is_array($item))
			array_walk($item, array($this, '_stripSlashes'));
		else
			$item = stripslashes($item);
	}

	/**
	 * Get the base URI for generating action URIs.
	 *
	 * @param bool $absolute true to create an absolute URI else false for relative
	 * @param bool $secure tue to use https, false for http or null for as per request.
	 * @return string The URI.
	 */
	function getBaseUri($absolute = false, $secure = null) {
		return !$absolute ? $this->baseURI : (($secure === null ? $this->absBase : ($secure ? 'https://' : 'http://') . $_SERVER['SERVER_NAME']) . $this->baseURI);
	}

	/**
	 * Get the base URI for generating resource URIs.
	 *
	 * @param bool $absolute true to create an absolute URI else false for relative
	 * @param bool $secure tue to use https, false for http or null for as per request.
	 * @return string The URI.
	 */
	function getBasePath($absolute = false, $secure = null) {
		return !$absolute ? $this->sitePath : (($secure === null ? $this->absBase : ($secure ? 'https://' : 'http://') . $_SERVER['SERVER_NAME']) . $this->sitePath);
	}

	/**
	 * Get the value of a query parameter.
	 *
	 * @param string $key The parameter to get.
	 * @param mixed $default The default value to use if the key is not present.
	 * @return mixed The value assigned to the key or the value of $default.
	 */
	function getQuery($key, $default = null) {
		return isset($_GET[$key]) ? $_GET[$key] : $default;
	}

	/**
	 * Get the address of the page.
	 *
	 * @param bool $absolute true to create an absolute URI else false for relative.
	 * @param bool $secure tue to use https, false for http or null for as per request.
	 * @return string The page address.
	 */
	function getSelf($absolute = false, $secure = null) {
		$uri = $this->getBaseUri($absolute, $secure);
		if(count($this->segments)) {
			$uri .= join('/', $this->segments);
			if(!preg_match('/\.[a-z0-9]{1,4}$/', $uri)) $uri .= '/';
		}
		return $uri;
	}

	/**
	 * Test if a query parameter is present.
	 *
	 * @param string $key The parameter to test.
	 * @return bool true if the parameter is present else false.
	 */
	function hasQuery($key) {
		return isset($_GET[$key]);
	}

	/**
	 * Get the value of a post parameter.
	 *
	 * @param string $key The parameter to get.
	 * @param mixed $default The default value to use if the key is not present.
	 * @return mixed The value assigned to the key or the value of $default.
	 */
	function getPost($key, $default = null) {
		return isset($_POST[$key]) ? $_POST[$key] : $default;
	}

	/**
	 * Test if a post parameter is present.
	 *
	 * @param string $key The parameter to test.
	 * @return bool true if the parameter is present else false.
	 */
	function hasPost($key) {
		return isset($_POST[$key]);
	}

	/**
	 * Get the PUT data.
	 *
	 * @return string The data sent via PUT.
	 */
	function getPutData() {
		return $this->putData;
	}

	/**
	 * Get the value of a parameter, checks in the order: internal, $_POST, $_GET
	 *
	 * @param string $key The parameter to get.
	 * @param mixed $default The default value to use if the key is not present.
	 * @return mixed The value assigned to the key or the value of $default.
	 */
	function getParam($key, $default = null) {
		return array_key_exists($key, $this->params) ? $this->params[$key] : $default;
	}

	/**
	 * Get all the parameters.
	 *
	 * @return array Associative array of parameters and their values.
	 */
	function getParams() {
		return $this->params;
	}

	/**
	 * Set the value of a parameter.
	 *
	 * @param string $key The parameter to set.
	 * @param mixed $value The value to set.
	 * @return flRequest This object.
	 */
	function setParam($key, $value) {
		$this->params[$key] = $value;
		return $this;
	}

	/**
	 * Test if a parameter is present.
	 *
	 * @param string $key The parameter to test.
	 * @return bool true if the parameter is present else false.
	 */
	function hasParam($key) {
		return array_key_exists($key, $this->params);
	}

	/**
	 * Get the value of a cookie.
	 *
	 * @param string $key The cookie to get.
	 * @param string $default The default value to return if the cookie isn't available.
	 * @return string The value of the cookie or $default.
	 */
	function getCookie($key, $default = null) {
		return isset($_COOKIE[$key]) ? $_COOKIE[$key] : $default;
	}

	/**
	 * Test if the cookie exists.
	 *
	 * @param string $key The cookie to test for.
	 * @return bool true if the cookie is present else false.
	 */
	function hasCookie($key) {
		return isset($_COOKIE[$key]);
	}

	/**
	 * Get the name of the server.
	 *
	 * This is the name of the virtual server that received the request, this may not be the name of the physical server.
	 *
	 * @return string The server name.
	 */
	function getServerName() {
		return $_SERVER['SERVER_NAME'];
	}

	/**
	 * Get the IP of the server.
	 *
	 * @return string The server IP.
	 */
	function getServerIp() {
		return $_SERVER['SERVER_ADDR'];
	}

	/**
	 * Get the IP address of the client.
	 *
	 * @return string The client IP address.
	 */
	function getClientIp() {
		$ip = '';

		if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
			$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
			$ip = trim(end($ips));
		}
		else if(isset($_SERVER['REMOTE_ADDR']))
			$ip = $_SERVER['REMOTE_ADDR'];
		else if(isset($_SERVER['HTTP_CLIENT_IP']))
			$ip = $_SERVER['HTTP_CLIENT_IP'];

		return $ip;
	}

	/**
	 * Get the browsers user agent string.
	 *
	 * @return string The user agent string.
	 */
	function getUserAgent() {
		return isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
	}

	/**
	 * Tests if the install is using mod_rewrite.
	 *
	 * @return bool true if using mod_rewrite; else false.
	 */
	function isModRewrite() {
		return $this->usingModRewrite;
	}

	/**
	 * Test if GET request.
	 *
	 * @return bool true if the request is GET else false.
	 */
	function isGet() {
		return $_SERVER['REQUEST_METHOD'] == 'GET';
	}

	/**
	 * Test if POST request.
	 *
	 * @return bool true if the request is POST else false.
	 */
	function isPost() {
		return $_SERVER['REQUEST_METHOD'] == 'POST';
	}

	/**
	 * Test if PUT request.
	 *
	 * @return bool true if the request is PUT else false.
	 */
	function isPut() {
		return $_SERVER['REQUEST_METHOD'] == 'PUT';
	}

	/**
	 * Test if DELETE request.
	 *
	 * @return bool true if the request is DELETE else false.
	 */
	function isDelete() {
		return $_SERVER['REQUEST_METHOD'] == 'DELETE';
	}

	/**
	 * Test if OPTIONS request.
	 *
	 * @return bool true if the request is OPTIONS else false.
	 */
	function isOptions() {
		return $_SERVER['REQUEST_METHOD'] == 'OPTIONS';
	}

	/**
	 * Test if HEAD request.
	 *
	 * @return bool true if the request is HEAD else false.
	 */
	function isHead() {
		return $_SERVER['REQUEST_METHOD'] == 'HEAD';
	}

	/**
	 * Test if the request is over https rather than http.
	 *
	 * @return bool true if the request is over HTTPS or false for HTTP.
	 */
	function isSecure() {
		return $this->isSsl;
	}

	/**
	 * Test if the request if via AJAX.
	 *
	 * @return bool true if AJAX request else false.
	 */
	function isXmlHttpRequest() {
		return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest';
	}

	/**
	 * Retrieve the referrer string.
	 *
	 * @return string The referrer string if any.
	 */
	function getReferrer() {
		return isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
	}

	/**
	 * Test if the request came from a mobile device.
	 *
	 * @return bool true if the request is from a mobile device; else false.
	 */
	function isMobile() {
		if(preg_match('#Windows|Mac|Linux|FreeBSD|Solaris|bot|slurp|spider|crawl#i', $this->getUserAgent())) {
			$isMobile = preg_match('#Mobile#i', $this->getUserAgent());
		}
		else
			$isMobile = true;

		return $isMobile;
	}

	/**
	 * Test if the named file has been uploaded.
	 *
	 * @param string $name The name of the file to test for.
	 * @return bool true if the file was uploaded or false if the file was not uploaded for any reason.
	 */
	function hasFile($name) {
		return isset($_FILES[$name]) && $_FILES[$name]['size'] && $_FILES[$name]['error'] == UPLOAD_ERR_OK && is_uploaded_file($_FILES[$name]['tmp_name']);
	}

	/**
	 * Get the error if any for an uploaded file.
	 *
	 * @param string $name The name of the file to return information for.
	 * @return int The error code one of flRequest::UPLOAD*
	 */
	function getFileError($name) {
		if(isset($_FILES[$name])) {
			switch($_FILES[$name]['error']) {
				case 0:
					return flRequest::UPLOAD_OK;
				case 1:
				case 2:
					return flRequest::UPLOADERR_SIZE;
				case 3:
				case 4:
					return flRequest::UPLOADERR_NOFILE;
				case 6:
				case 7:
					return flRequest::UPLOADERR_WRITE;
				default:
					return flRequest::UPLOADERR_OTHER;
			}
		}

		return flRequest::UPLOADERR_NOFILE;
	}

	/**
	 * Get the temporary file name an uploaded file.
	 *
	 * @param string $name The name of the file to return.
	 * @return string The name of the temporary file or false if the file was not uploaded.
	 */
	function getTmpFileName($name) {
		return $this->hasFile($name) ? $_FILES[$name]['tmp_name'] : false;
	}

	/**
	 * Get the file name an uploaded file.
	 *
	 * @param string $name The name of the file to return.
	 * @return string The name of the file or false if the file was not uploaded.
	 */
	function getFileName($name) {
		return $this->hasFile($name) ? $_FILES[$name]['name'] : false;
	}

	/**
	 * Move the uploaded file to a permanent location.
	 *
	 * @param string $name The name of the uploaded file to move.
	 * @param string $to The path and name to move the file to.
	 * @param string $default The file to copy as a default if the named file is not available.
	 * @return bool true if the file was moved or false if it failed.
	 */
	function moveUploadedFile($name, $to, $default = null) {
		if($this->hasFile($name))
			return move_uploaded_file($_FILES[$name]['tmp_name'], $to);
		else if($default !== null)
			return copy($default, $to);
		else
			return false;
	}

	/**
	 * Get the segment from the request URI.
	 *
	 * @param int $num The zero based index to the segment to return.
	 * @param string $default The value to return if the segment isn't present.
	 * @return string The requested segment or the default value.
	 */
	function getSegment($num, $default = null) {
		return isset($this->segments[$num]) ? $this->segments[$num] : $default;
	}

	/**
	 * Get the array of segments from the request URI.
	 *
	 * @return array Array of URI segments.
	 */
	function getSegments() {
		return $this->segments;
	}
}
