<?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) 2008 - 2013 fusionLib. All rights reserved.
 * @link http://fusionlib.com
 */

/**
 * flUri Class
 *
 * Class to simplify working with URIs.
 *
 * @see ftp://ftp.isi.edu/in-notes/rfc3986.txt
 * @package fusionLib
 */
class flUri {

	/**
	 * The scheme of this Uri
	 *
	 * @var string
	 */
	protected $scheme;

	/**
	 * The host.
	 *
	 * @var string
	 */
	protected $host;

	/**
	 * The path.
	 *
	 * @var string
	 */
	protected $path;

	/**
	 * The query string.
	 *
	 * @var string
	 */
	protected $query;

	/**
	 * The fragment.
	 *
	 * @var string
	 */
	protected $fragment;

	/**
	 * The port.
	 *
	 * @var string
	 */
	protected $port;

	/**
	 * The username.
	 *
	 * @var string
	 */
	protected $username;

	/**
	 * The password.
	 *
	 * @var string
	 */
	protected $password;

	/**
	 * Construct the object.
	 *
	 * @param string $uri The Uri to construct the object for.
	 */
	function __construct($uri = '') {
		$this->setUri($uri);
	}

	/**
	 * Set the Uri parsing it into component parts.
	 *
	 * @param string $uri The URI to parse.
	 */
	function setUri($uri) {

		// Pattern from appendix B of http://www.ietf.org/rfc/rfc3986.txt
		$parts = array();
        preg_match('~^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?~', $uri, $parts);

		$this->scheme = isset($parts[2]) ? strtolower($parts[2]) : '';
		$this->host = isset($parts[4]) ? $parts[4] : '';
		$this->path = isset($parts[5]) ? $parts[5] : '';
		$this->query = isset($parts[7]) ? $parts[7] : '';
		$this->fragment = isset($parts[9]) ? $parts[9] : '';

		// Extract userinfo, port and host
		preg_match('~^(([^:@]*)(:([^@]*))?@)?([^:]+)(:(.*))?$~', $this->host, $parts);
		$this->username = isset($parts[2]) ? $parts[2] : '';
		$this->password = isset($parts[4]) ? $parts[4] : '';
		$this->host = isset($parts[5]) ? strtolower($parts[5]) : '';
		$this->port = isset($parts[7]) ? $parts[7] : '';
 	}

 	/**
 	 * Get the Uri.
 	 *
 	 * @return string The Uri as a string.
 	 */
 	function getUri() {
 		return $this->scheme . '://' .
 			(empty($this->username) ? '' : $this->username . (empty($this->password) ? '' : ":$this->password") . '@') .
 			$this->host . (empty($this->port) ? '' : ":$this->port") . (empty($this->path) ? '/' : $this->path) .
 			(empty($this->query) ? '' : "?$this->query") . (empty($this->fragment) ? '' : "#$this->fragment");
 	}

	/**
	 * Get the Uri as a string.
	 *
	 * @return string The URI.
	 */
	function __toString() {
		return $this->getUri();
	}

 	/**
 	 * Callback function to encode characters.
 	 *
 	 * @param array $m Matches
 	 * @return string Encoded string
 	 */
	private function _replaceChars($m) {
		return sprintf('%%%02X', ord($m[0]));
	}

 	/**
 	 * Normalize the URI.
 	 *
 	 * This resolves .. and mixed case encodings.
 	 */
 	function normalize() {

 		// Fix the scheme and host
 		$this->scheme = strtolower($this->scheme);
 		$this->host = strtolower($this->host);

 		// Fix the path
		$r = array();

		// Break into segments
 		$parts = explode('/', $this->path);

 		// Process each segment
		foreach($parts as $seg) {
			// If up 1 level remove segment from output
			if($seg == '..')
				@array_pop($r);
			// Else if not . keep
			else if($seg != '.') {
				// Decode the segment and recode it
				$r[]= preg_replace_callback('/[^a-zA-Z0-9:@!$&\'\(\)\*\+,;=\._~-]/',
											array($this, '_replaceChars'), urldecode($seg));
			}
		}

		// Join path back together
		$this->path = implode('/', $r);
		if(strlen($this->path) && $this->path[0] != '/')
			$this->path = '/' . $this->path;

		// Strip multiple /
		$this->path = preg_replace('#/+#', '/', $this->path);

		// scheme and port normalization
		$schemeNorm = array('http' => '80', 'https' => '443');
		foreach($schemeNorm as $s => $p) {
			if($this->scheme == $s && $this->port == $p)
				$this->port = '';
		}
 	}

 	/**
 	 * Tests if the Uri is valid.
 	 *
 	 * @return bool true if the Uri is valid; else false.
 	 */
 	function isValid() {
		return $this->validateScheme() && $this->validateUsername() && $this->validatePassword() &&
			$this->validateHost() && $this->validatePort() && $this->validatePath() && $this->validateQuery() && $this->validateFragment();
 	}

 	/**
 	 * Get the scheme.
 	 *
 	 * @return string The scheme or false.
 	 */
 	function getScheme() {
 		return empty($this->scheme) ? false : $this->scheme;
 	}

 	/**
 	 * Set the scheme.
 	 *
 	 * @param string $scheme The scheme.
 	 */
 	function setScheme($scheme) {
 		$this->scheme = $scheme;
 	}

 	/**
 	 * Validate the scheme.
 	 *
 	 * @return bool true if valid; else false.
 	 */
 	function validateScheme() {
 		return empty($this->scheme) || preg_match('/^[a-zA-Z]([a-zA-Z0-9+.-])*$/', $this->scheme) == 1;
 	}

 	/**
 	 * Get the username.
 	 *
 	 * @return string The username or false.
 	 */
 	function getUsername() {
 		return empty($this->username) ? false : $this->username;
 	}

 	/**
 	 * Set the username.
 	 *
 	 * @param string $username The username.
 	 */
 	function setUsername($username) {
 		$this->username = $username;
 	}

 	/**
 	 * Validate the username.
 	 *
 	 * @return bool true if valid; else false.
 	 */
 	function validateUsername() {
 		return empty($this->username) || preg_match('/^([a-zA-Z0-9\._~-]|(?:%[\da-fA-F]{2})|[!$&\'\(\)*+,;=])+$/', $this->username) == 1;
 	}

 	/**
 	 * Get the password.
 	 *
 	 * @return string The password or false.
 	 */
 	function getPassword() {
 		return empty($this->password) ? false : $this->password;
 	}

 	/**
 	 * Set the password.
 	 *
 	 * @param string $password The password.
 	 */
 	function setPassword($password) {
 		$this->password = $password;
 	}

 	/**
 	 * Validate the password.
 	 *
 	 * @return bool true if valid; else false.
 	 */
 	function validatePassword() {
 		return empty($this->password) || preg_match('/^([a-zA-Z0-9\._~-]|(?:%[\da-fA-F]{2})|[!$&\'\(\)*+,;=])+$/', $this->password) == 1;
 	}

 	/**
 	 * Get the host.
 	 *
 	 * @return string The host or false.
 	 */
 	function getHost() {
 		return empty($this->host) ? false : $this->host;
 	}

 	/**
 	 * Set the host.
 	 *
 	 * @param string $host The host.
 	 */
 	function setHost($host) {
 		$this->host = $host;
 	}

 	/**
 	 * Validate the host.
 	 *
 	 * @return bool true if valid; else false.
 	 */
 	function validateHost() {
 		return empty($this->host) || preg_match('/^([a-zA-Z0-9\._~-]|(?:%[\da-fA-F]{2})|[!$&\'\(\)*+,;=])+$/', $this->host) == 1;
 	}

 	/**
 	 * Get the port.
 	 *
 	 * @return string The port or false.
 	 */
 	function getPort() {
 		return empty($this->port) ? false : $this->port;
 	}

 	/**
 	 * Set the port.
 	 *
 	 * @param string $port The port.
 	 */
 	function setPort($port) {
 		$this->port = $port;
 	}

 	/**
 	 * Validate the port.
 	 *
 	 * @return bool true if valid; else false.
 	 */
 	function validatePort() {
 		return empty($this->port) || (ctype_digit($this->port) && $this->port >= 1 && $this->port <= 65535);
 	}

 	/**
 	 * Get the path.
 	 *
 	 * @return string The path or false.
 	 */
 	function getPath() {
 		return empty($this->path) ? false : $this->path;
 	}

 	/**
 	 * Set the path.
 	 *
 	 * @param string $path The path.
 	 */
 	function setPath($path) {
 		$this->path = $path;
 	}

 	/**
 	 * Validate the path.
 	 *
 	 * @return bool true if valid; else false.
 	 */
 	function validatePath() {
 		return empty($this->path) || preg_match('/^(?:\/([a-zA-Z0-9\._~-]|(?:%[\da-fA-F]{2})|[!$&\'\(\)*+,;=:@])*)+$/', $this->path) == 1;
 	}

 	/**
 	 * Get the query.
 	 *
 	 * @return string The query or false.
 	 */
 	function getQuery() {
 		return empty($this->query) ? false : $this->query;
 	}

 	/**
 	 * Set the query string.
 	 *
 	 * @param string $query The query string.
 	 */
 	function setQuery($query) {
 		$this->query = $query;
 	}

 	/**
 	 * Validate the query.
 	 *
 	 * @return bool true if valid; else false.
 	 */
 	function validateQuery() {
 		return empty($this->query) || preg_match('/^([a-zA-Z0-9\._~-]|(?:%[\da-fA-F]{2})|[!$&\'\(\)*+,;=:@\/\?])+$/', $this->query) == 1;
 	}

 	/**
 	 * Get the fragment.
 	 *
 	 * @return string The fragment or false.
 	 */
 	function getFragment() {
 		return empty($this->fragment) ? false : $this->fragment;
 	}

 	/**
 	 * Set the fragment.
 	 *
 	 * @param string $fragment The fragment.
 	 */
 	function setFragment($fragment) {
 		$this->fragment = $fragment;
 	}

 	/**
 	 * Validate the fragment.
 	 *
 	 * @return bool true if valid; else false.
 	 */
 	function validateFragment() {
 		return empty($this->fragment) || preg_match('/^([a-zA-Z0-9\._~-]|(?:%[\da-fA-F]{2})|[!$&\'\(\)*+,;=:@\/\?])+$/', $this->fragment) == 1;
 	}
}
