Show:

File: platform/classes/Q/Uri.php

<?php

/**
 * @module Q
 */
class Q_Uri
{
	/**
	 * Represents an internal URI
	 * @class Q_Uri
	 * @constructor
	 */
	protected function __construct()
	{
		
	}
	
	/**
	 * Constructs a URI object from something
	 * @method from
	 * @static
	 * @param {string} $source An absolute URL, or an array, or a URI in string form.
	 * @param {string} [$route=null] The pattern of the route in the routes config.
	 *  If not specified, then Qbix searches all the route patterns in order, until it finds one that fits.
	 *  If you set this to false, then $source is treated as an absolute URL, regardless of its format.
	 * @return {Q_Uri|false} Returns false if no route patterns match.
	 *  Otherwise, returns the URI.
	 */
	static function from(
	 $source,
	 $route = null)
	{

		if (empty($source)) {
			return null;
		}
		
		if ($route === false) {
			$u = new Q_Uri();
			$u->Q_url = $source;
			return $u;
		}
			
		if (is_array($source)) {
			return self::fromArray($source);
		}
		
		if (is_string($source)) {
			if (Q_Valid::url($source)) {
				return self::fromUrl($source, $route);
			} else {
				return self::fromString($source);
			}
		}
		
		if ($source instanceof Q_Uri) {
			$source2 = clone $source;
			return $source2;
		}
	}
	
	/**
	 * @method __toString
	 * @return {string}
	 */
	function __toString()
	{
		if (!empty($this->Q_url)) {
			return $this->Q_url;
		}
		
		// returns it as a string
		$module = isset($this->fields['module']) ? $this->fields['module'] : null;
		$action = isset($this->fields['action']) ? $this->fields['action'] : null;
		$result = "$module/$action";
		if ($this->querystring) {
			$result .= '?'.$this->querystring;
		}
		if ($this->anchorstring) {
			$result .= '#'.$this->anchorstring;
		}
		$other_fields = array();
		foreach ($this->fields as $name => $value) {
			if (is_numeric($name) 
			 or $name == 'action' 
			 or $name == 'module') {
				continue;
			}
			$other_fields[$name] = $value;
		}
		if (!empty($other_fields)) {
			$result .= " " . self::encode($other_fields);
		}
		return $result;
	}
	
	/**
	 * @method toArray
	 * @return {array}
	 */
	function toArray()
	{
		// returns it as an array
		$result = $this->fields;
		if ($this->querystring) {
			$result['?'] = $this->querystring;
		}
		if ($this->anchorstring) {
			$result['#'] = $this->anchorstring;
		}
		return $result;
	}
	
	/**
	 * Takes some input and returns the corresponding URL
	 * @method url
	 * @static
	 * @param {string|boolean|array|Q_Uri} $source This can be a Q_Uri, an array or string representing a uri,
	 *  an absolute url, or "true". If you pass "true", then the Q_Request::baseUrl(true) is used as input.
	 * @param {string|null} [$route=null] If you know which route pattern to use, then specify it here. Otherwise, leave it null.
	 * @param {boolean} [$noProxy=false] If set to true, Q_Uri::proxySource($url) is not called before returning the result.
	 * @param {string} [$controller=true] The controller to pass to `Q_Request::baseUrl($controller)` when forming the URL.
	 * @return {string}
	 */
	static function url(
	 $source,
	 $route = null,
	 $noProxy = false,
	 $controller = true)
	{
		if (empty($source)) {
			return $source;
		}
		if ($source === true) {
			$source = Q_Request::baseUrl($controller);
		}

		if (($source instanceof Q_Uri) and $source->Q_url) {
			return Q_Uri::fixUrl($source->Q_url);
		}
		
		static $cache = array();
		$cache_key = $noProxy ? $source . "\tnoProxy" : $source;
		if (is_string($source) and isset($cache[$cache_key])) {
			return Q_Uri::fixUrl($cache[$cache_key]);
		}
		
		if (is_string($source) and isset($source[0]) and $source[0] == '#') {
			// $source is a fragment reference
			return Q_Uri::fixUrl($source);
		}
		
		if (Q_Valid::url($source)) {
			// $source is already a URL
			$result = $noProxy ? $source : self::proxySource($source);
			if (is_string($source)) {
				$cache[$source] = $result;
			}
			return Q_Uri::fixUrl($result);
		}
		
		$uri = self::from($source);
		if (!$uri) {
			$url = null;
		} else { 
			if ($controller === true) {
				// If developer set a custom controller, calculate it.
				$cs = Q_Config::get('Q', 'web', 'controllerSuffix', null);
				if (isset($cs)) {
					$controller = $cs;
				}
			}
			$url = $uri->toUrl($route, $controller);
		}
		if (!isset($url)) {
			$hash = Q_Uri::unreachableUri();
			if ($hash) {
				$result = $hash;
				if (is_string($source)) {
					$cache[$source] = $result;
				}
				return $result;
			}
		}
		if ($noProxy) {
			$result = $url;
			if (is_string($source)) {
				$cache[$source] = $result;
			}
			return $result;
		}
		$result = self::proxySource($url);
		if (is_string($source)) {
			$cache[$source] = $result;
		}
		return Q_Uri::fixUrl($result);
	}

	static function unreachableUri()
	{
		return Q_Config::get('Q', 'uri', 'unreachableUri', '#_noRouteToUri');
	}
	
	/**
	 * Adds cache busting to a url
	 * @param {string} $url A string to replace the default base url
	 * @param {integer} $milliseconds Number of milliseconds before a new cachebuster code is appended
	 */
	static function cacheBust($url, $milliseconds)
	{
		return Q_Uri::url("$url?Q.cacheBust=".floor(microtime(true)*1000/$milliseconds));
	}
	
	/**
	 * Set a suffix for all URLs that will be generated with this class.
	 * @method suffix
	 * @static
	 * @param {array|string} [$suffix=null] If no arguments are passed, just returns the current suffix.
	 *  Pass an array here. For each entry, the key is tested and if it
	 *  begins the URL, then the value is appended.
	 *  Suffixes are applied when URLs are generated.
	 *  You can also pass a string here, in which case the array('' => $suffix) is used.
	 * @return {array} Returns the suffix at the time the function was called.
	 */
	static function suffix($suffix = null)
	{
		if (is_string($suffix)) {
			$suffix = array('' => $suffix);
		}
		if (!isset($suffix)) {
			return isset(self::$suffix) ? self::$suffix : array();
		}
		$prev_suffix = self::$suffix;
		self::$suffix = $suffix;
		return $prev_suffix;
	}
	
	/**
	 * Set cache base url, relative to which this particular client may store cached
	 * versions of files.
	 * @method cacheBaseUrl
	 * @static
	 * @param {array} [$base_url=null] If no arguments are passed, just returns the current cache base url.
	 * @return {array} Returns the cache base url at the time the function was called.
	 */
	static function cacheBaseUrl($base_url = null)
	{
		if (!isset($base_url)) {
			return isset(self::$cacheBaseUrl) ? self::$cacheBaseUrl : array();
		}
		$prev_base_url = self::$cacheBaseUrl;
		self::$cacheBaseUrl = $base_url;
		return $prev_base_url;
	}
	
	/**
	 * Get the base url of a plugin
	 * @method pluginBaseUrl
	 * @static
	 * @param string $plugin The name of the plugin, with first letter uppercase.
	 * @return {string} Returns an absolute or relative URL
	 */
	static function pluginBaseUrl($plugin)
	{
		/**
		 * Hook for custom logic modifying the urls for a plugin
		 * @event Q/Uri/pluginUrl {before}
		 * @param {string} plugin
		 * @return {string}
		 */
		if ($url = Q::event('Q/Uri/pluginUrl', compact('url'), 'before')) {
			return;
		}
		return "Q/plugins/$plugin";
	}
	
	/**
	 * Returns the value of the specified URI field, or null
	 * if it is not present.
	 * @method __get
	 * @param {string} $field_name The name of the field.
	 * @return {string|null} Returns the value of the field, or null if not there.
	 */
	function __get($field_name)
	{
		if (isset($this->fields[$field_name])) {
			return $this->fields[$field_name];
		}
		return null;
	}
	
	/**
	 * Sets the value of the specified URI field
	 * @method __set
	 * @param {string} $field_name The name of the field.
	 * @param {string|array} $value The value of the field
	 */
	function __set($field_name, $value)
	{
		if (is_array($value)) {
			$this->fields[$field_name] = $value;
		} else {
			$this->fields[$field_name] = (string)$value;
		}
	}
	
	/**
	 * Returns whether the specified URI field is set
	 * @method __isset
	 * @param {string} $field_name The name of the field.
	 */
	function __isset($field_name)
	{
		return isset($this->fields[$field_name]);
	}
	
	//
	// Internal
	//
	
	/**
	 * @method fromUrl
	 * @static
	 * @protected
	 * @param {string} $url
	 * @param {string} [$route=null]
	 * @return {Q_Uri}
	 * @throws {Q_Exception_BadUrl}
	 * @throws {Q_Exception_MissingRoute}
	 */
	protected static function fromUrl(
	 $url,
	 $route = null)
	{
		if (empty($url)) {
			return null;
		}
		$url = Q_Uri::interpolateUrl($url);
			
		static $routed_cache = array();
		if (isset($routed_cache[$url])) {
			return $routed_cache[$url];
		}

		/**
		 * Hook for custom logic modifying routing from URLs to internal URIs
		 * @event Q/Uri/fromUrl {before}
		 * @param {string} url
		 * @return {Q_Uri}
		 */
		$uri = Q::event('Q/Uri/fromUrl', compact('url'), 'before');
		if (isset($uri)) {
			$routed_cache[$url] = $uri;
			return $uri;
		}
		$routes = Q_Config::get('Q', 'routes', array());
		if (empty($routes)) {
			return self::fromArray(array(
				'module' => 'Q', 
				'action' => 'welcome'
			));
		}
		$base_url = Q_Request::baseUrl(true);

		$len = strlen($base_url);
		$head = substr($url, 0, $len);
		if ($head != $base_url) {
			// try applying proxies before giving up
			$dest_url = self::proxyDestination($url);
			$head = substr($dest_url, 0, $len);
			if ($head != $base_url) {
				// even the proxy destination doesn't match.
				throw new Q_Exception_BadUrl(compact('base_url', 'url'));
			}
			$result = self::fromUrl($dest_url, $route);
			if (!empty($result)) {
				return $result;
			} else {
		    	throw new Q_Exception_BadUrl(compact('base_url', 'url'));
			}
		}

		// Get the path within our app
		$tail = substr($url, strlen($head) + 1);
		$p = explode('#', $tail);
		$p2 = explode('?', $p[0]);
		$path = $p2[0];

		// Break it up into segments and try the routes
		$segments = $path ? explode('/', $path) : array();
		$uri_fields = null;

		if ($route) {
			if (! array_key_exists($route, $routes))
				throw new Q_Exception_MissingRoute(compact('route'));
			$uri_fields = self::matchSegments($route, $segments);
		} else {
			foreach ($routes as $pattern => $fields) {
				if (!isset($fields))
					continue; // this provides a way to disable a route via config
				$pattern2 = Q_Uri::interpolateUrl($pattern);
				$uri_fields = self::matchSegments($pattern2, $segments);
				if ($uri_fields !== false) {
					$matched = true;
					foreach ((array)$uri_fields as $k => $v) {
						if (isset($fields[$k])) {
							if (!preg_match($fields[$k], $v)) {
								$matched = false;
								break;
							}
						}
					}
					// If this route has a special condition, test it
					if (!empty($fields[''])) {
						$params = array(
							'fields' => $uri_fields, 
							'pattern' => $pattern,
							'fromUrl' => $url
						);
						if (false === Q::event($fields[''], $params)) {
							$matched = false;
						}
					}
					if ($matched) {
						// If we are here, then the route has matched!
						$route = $pattern;
						break;
					}
				}
			}
		}
 
		if (!is_array($uri_fields)) {
			// No route has matched
			return self::fromArray(array());
		}
		
		// Now, fill in any extra fields, if present
		if (is_array($routes[$route])) {
			$uri_fields = array_merge($routes[$route], $uri_fields);
		}

		$uri = self::fromArray($uri_fields);
		if (isset($route)) {
			$uri->route = $route;
		}
		$routed_cache[$url] = $uri;
		return $uri;
	}

	/**
	 * Maps this URI into an external URL.
	 * @method toUrl
	 * @param {string} [$route=null] If you name the route to use for unrouting,
	 *  it will be used as much as possible.
	 *  Otherwise, Qbix will go through the routes one by one in order,
	 *  until it finds one that can route a URL to the full URI
	 *  contained in this object.
	 * @param {string} [$controller=true] You can supply a different controller name, like 'tool.php'
	 * @return {string} If a $route is specified, the router uses this route 
	 *  and replaces as many variables as it can to match the $internal_destination. 
	 *  If not, the router tries to find a route and use it to 
	 *  make an external URL that maps to the internal destination
	 *  exactly, but if none of the routes can do this, it returns 
	 *  an empty string.
	 *  You may want to use Q_Uri::proxySource() on the returned url to get
	 *  the proxy url corresponding to it.
	 */
	function toUrl(
	 $route = null,
	 $controller = true)
	{
		if (!empty($this->Q_url)) {
			return $this->Q_url;
		}
		
		if (empty($this->fields)) {
			return null;
		}
		
		$routes = Q_Config::get('Q', 'routes', array());
		if (empty($routes)) {
			$url = Q_Request::baseUrl($controller);
		} else if ($route) {
			if (!isset($routes[$route])) {
				$url = null;
			} else {
				return self::matchRoute($route, $routes[$route], $controller);
			}
		} else {
			foreach ($routes as $pattern => $fields) {
				if (!isset($fields))
					continue;
				$url = $this->matchRoute($pattern, $fields, $controller);
				if ($url) {
					if ($this->querystring) {
						$url .= '?'.$this->querystring;
					}
					if ($this->anchorstring) {
						$url .= '#'.$this->anchorstring;
					}
					$suffix = self::suffix();
					if (is_string($suffix)) {
						$url .= self::suffix();
					} else {
						// aggregate suffixes
						foreach ($suffix as $k => $v) {
							$k_len = strlen($k);
							if (substr($url, 0, $k_len) === $k) {
								$url .= $v;
							}
						}
					}
					$route = $pattern;
					break;
				}
			}
		}
		
		/**
		 * @event Q/Uri/toUrl {before}
		 * @param {Q_Uri} uri This is the uri being turned into a URL
		 * @param {string|null} route The route that is going to be used (from config)
		 * @param {string|null} controller The controller that is being used for the baseUrl
		 * @param {string|null} url The computed url, that will be returned. You can modify it.
		 * @return {string}
		 */
		$uri = $this;
		$params = compact('uri', 'route', 'pattern', 'controller', 'url');
		$params['url'] = &$url;
		Q::event('Q/Uri/toUrl', $params, 'before', false, $url);
		
		if ($url) {
			return self::fixUrl($url);
		}
				
		return null;
	}
	
	/**
	 * Get the route that was used to obtain this URI from a URL
	 * @method route
	 * @return {string}
	 */
	function route()
	{
		return $this->route;
	}
	
	/**
	 * @method matchSegments
	 * @static
	 * @protected
	 * @param {string} $pattern The pattern (of the rule) to match
	 * @param {string} $segments The segments extracted from the URL
	 * @return {array|false} Returns false if one of the literal values doesn't match up,
	 *  Otherwise, returns array of field => name pairs
	 *  where fields were filled.
	 */
	protected static function matchSegments($pattern, $segments)
	{
		$route_segments = $pattern ? explode('/', $pattern) : array();
		$tail_array = false;
		if (substr($pattern, -2) === '[]') {
			$tail_array = true;
			$last_rs = end($route_segments);
			if (!in_array($last_rs[0], self::$variablePrefixes)) {
				return false;
			}
			$route_segments = array_slice($route_segments, 0, -1);
		}
		$count = count($route_segments);
		$segments_count = count($segments);
		if ($tail_array) {
			if ($count >= $segments_count) {
				return false; // rule does not match
			}
		} else {
			if ($count != $segments_count) {
				return false; // rule does not match
			}
		}
		// Segments matching test
		$args = array();
		for ($i = 0; $i < $count; ++ $i) {
			$rs = $route_segments[$i];
			$rs_parts = explode('.', $rs);
			$rs_parts_count = count($rs_parts);
			$segment = urldecode($segments[$i]);
			$s_parts = explode('.', $segment, $rs_parts_count);
			$s_parts_count = count($s_parts);
			if ($s_parts_count < $rs_parts_count) {
				return false;
			}
			for ($j = 0; $j < $rs_parts_count; ++$j) {
				if (!isset($rs_parts[$j][0]) or !in_array($rs_parts[$j][0], self::$variablePrefixes)) {
					// literal value
					if ($s_parts[$j] !== str_replace(self::$escapedVariablePrefixes, self::$variablePrefixes, $rs_parts[$j])) {
						return false;
					}
					continue;
				}
				// otherwise, $variable
				$field_name = substr($rs_parts[$j], 1);
				$args[$field_name] = $s_parts[$j];
			}
		}
		
		if (!empty($last_rs)) {
			// Put the rest of the segments into an array
			$field_name = substr($last_rs, 1, -2);
			$args[$field_name] = array();
			while ($i < $segments_count) {
				$args[$field_name][] = urldecode($segments[$i]);
				++$i;
			}
		}

		return $args;
	}
	
	/**
	 * @method matchRoute
	 * @static
	 * @protected
	 * @param {string} $pattern
	 * @param {array} $fields
	 * @param {string} [$controller=true]
	 * @return {string|false} Returns false if even one field doesn't match.
	 *  Otherwise, returns the URL that would be routed to this uri.
	 */
	protected function matchRoute(
	 $pattern, 
	 $fields,
	 $controller = true)
	{
		// First, test if the URI satisfies the pattern
		$rsegments = explode('/', $pattern);
		if (substr($pattern, -2) === '[]') {
			$last_rs = end($rsegments);
			if (false === in_array($last_rs[0], self::$variablePrefixes)) {
				return false;
			}
			$rsegments = array_slice($rsegments, 0, -1);
		}
		$segments = array();
		$field_in_pattern = array();
		foreach ($rsegments as $rs) {			
			$rs_parts = explode('.', $rs);
			$rs_parts_count = count($rs_parts);
			$segment_parts = array();
			for ($j = 0; $j < $rs_parts_count; ++$j) {
				if (!isset($rs_parts[$j][0]) or (false === in_array($rs_parts[$j][0], self::$variablePrefixes))) {
					// literal value
					$segment_parts[] = urlencode(
						str_replace(self::$escapedVariablePrefixes, self::$variablePrefixes, $rs_parts[$j])
					);
					continue;
				}
				// otherwise, $variable
				$field_name = substr($rs_parts[$j], 1);
				if (!array_key_exists($field_name, $this->fields)) {
					return false;
				}
				if (is_array($this->fields[$field_name])) {
					return false; // arrays can only come at the end
				}
				$segment_parts[] = urlencode($this->fields[$field_name]);
				$field_in_pattern[$field_name] = true;
			}
			$segments[] = implode('.', $segment_parts);
		}
		
		// If pattern ends in [], process the last route segment
		if (!empty($last_rs)) {
			$field_name = substr($last_rs, 1, -2);
			if (!array_key_exists($field_name, $this->fields)) {
				return false;
			}
			if (is_string($this->fields[$field_name])) {
				$segments[] = urlencode($this->fields[$field_name]);
			} else {
				foreach ($this->fields[$field_name] as $f) {
					$segments[] = urlencode($f);
				}
			}
			$field_in_pattern[$field_name] = true;
		}

		// Then, test if all the fields match
		foreach ($fields as $name => $value) {
			if (!$name) {
				continue;
			}
			if (isset($field_in_pattern[$name])) {
				continue; // this is a regexp
			}
			if ((!isset($this->fields[$name])) or $this->fields[$name] != $value) {
				return false;
			}
		}

		// Test field matches the other way
		foreach ($this->fields as $name => $value) {
			if (!$name) {
				continue;
			}
			if (isset($field_in_pattern[$name])) {
				if (isset($fields[$name])) {
					if (!preg_match($fields[$name], $this->fields[$name])) {
						return false;
					}
				}
				continue;
			}
			if (!isset($fields[$name]) or $fields[$name] != $value) {
				return false;
			}
		}
		
		// If this route has a special condition, test it
		if (!empty($fields[''])) {
			$params = array(
				'fields' => $this->fields, 
				'pattern' => $pattern, 
				'controller' => $controller
			);
			if (false === Q::event($fields[''], $params)) {
				return false;
			}
		}
		$url = Q_Request::baseUrl($controller).'/'.implode('/', $segments);
		return $url;
	}
	
	/**
	 * If a proxy exists for this URL, returns the destination URL, otherwise returns the input URL
	 * @method proxyDestination
	 * @static
	 * @param {string} $url
	 * @return {string}
	 */
	static function proxyDestination($url)
	{
		$proxies = Q_Config::get('Q', 'proxies', array());
		foreach ($proxies as $dest_url => $src_url) {
			$src_url_strlen = strlen($src_url);
			if (substr($url, 0, $src_url_strlen) == $src_url) {
				if (!isset($url[$src_url_strlen]) 
				or $url[$src_url_strlen] == '/') {
					return $dest_url.substr($url, $src_url_strlen);
				}
			}
		}
		return $url;
	}
	
	/**
	 * If a proxy exists for this URL, returns the source URL, otherwise returns the input URL
	 * @method proxySource
	 * @static
	 * @param {string} $url
	 * @return {string}
	 */
	static function proxySource($url)
	{
		$url = self::fixUrl($url);
		$proxies = Q_Config::get('Q', 'proxies', array());
		foreach ($proxies as $dest_url => $src_url) {
			$dest_url_strlen = strlen($dest_url);
			if (substr($url, 0, $dest_url_strlen) == $dest_url) {
				if (!isset($url[$dest_url_strlen]) 
				or $url[$dest_url_strlen] == '/') {
					return $src_url.substr($url, $dest_url_strlen);
				}
			}
		}
		return $url;
	}
	
	/**
	 * @method documentRoot
	 * @static
	 * @return {string}
	 */
	static function documentRoot()
	{
		$docroot_dir = Q_Config::get('Q', 'docroot_dir', null);
		if (empty($docroot_dir))
			$docroot_dir = $_SERVER['DOCUMENT_ROOT'];
		$docroot_dir = str_replace("\\", '/', $docroot_dir);
		if (substr($docroot_dir, -1) == '/')
			$docroot_dir = substr($docroot_dir, 0, strlen($docroot_dir) - 1);
		return $docroot_dir;
	}
	
	/**
	 * Returns what the local filename of a local URL would typically be without any routing.
	 * If not found under docroot, also checks various aliases.
	 * @method filenamefromUrl
	 * @static
	 * @param {string} $url The url to translate, whether local or an absolute url beginning with the base URL
	 * @return {string} The complete filename of the file or directory.
	 *  It may not point to an actual file or directory, so use file_exists() or realpath()
	 */
	static function filenamefromUrl ($url)
	{
		if (Q_Valid::url($url)) {
			// This is an absolute URL. Get only the part after the base URL
			// Run it through proxies first
			$url = self::proxyDestination($url);
			$local_url = Q_Request::tail($url);
			if (!isset($local_url)) {
				return null;
			}
		} else {
			$local_url = $url;
		}
		$parts = explode('?', $local_url);
		$local_url = $parts[0];

		if ($local_url == '' || $local_url[0] != '/')
			$local_url = '/' . $local_url;

		// Try various aliases first
		$aliases = Q_Config::get('Q', 'aliases', array());
		foreach ($aliases as $alias => $path) {
			$alias_len = strlen($alias);
			if (substr($local_url, 0, $alias_len) == $alias) {
				return $path . substr($local_url, $alias_len);
			}
		}
		
		// Otherwise, we should use the document root.
		$docroot_dir = self::documentRoot();
		return $docroot_dir.$local_url;
	}
	
	/**
	 * Interpolate some standard placeholders inside a url, such as 
	 * {{AppName}} or {{PluginName}}
	 * @static
	 * @method interpolateUrl
	 * @param {string} $url
	 * @param {array} [$additional=array()] Any additional substitutions
	 * @return {strQ_Uri::interpolateUrlitutions applied
	 */
	static function interpolateUrl($url, $additional = array())
	{
		if (strpos($url, '{{') === false) {
			return $url;
		}
		$app = Q::app();
		$baseUrl = Q_Request::baseUrl();
		$substitutions = array(
			'baseUrl' => $baseUrl,
			$app => $baseUrl
		);
		$plugins = Q_Config::expect('Q', 'plugins');
		$plugins[] = 'Q';
		foreach ($plugins as $plugin) {
			$substitutions[$plugin] = Q_Uri::pluginBaseUrl($plugin);
		}
		$url = Q::interpolate($url, $substitutions);
		if ($additional) {
			$url = Q::interpolate($url, $additional);
		}
		return $url;
	}
	
	/**
	 * Interpolates a URL and fixes it to have only one question mark and hash mark.
	 * @method fixUrl
	 * @static
	 * @param {string} $url The url to fix
	 * @return {string} The URL with all subsequent ? and # replaced by &
	 */
	static function fixUrl($url)
	{
		$url = Q_Uri::interpolateUrl($url);
		$pieces = explode('?', $url);
		$url = $pieces[0];
		if (isset($pieces[1])) {
			$url .= '?' . implode('&', array_slice($pieces, 1));
		}
		$pieces = explode('#', $url);
		$url = $pieces[0];
		if (isset($pieces[1])) {
			$url .= '#' . implode('&', array_slice($pieces, 1));
		}
		return $url;
	}
	
	/**
	 * May append a "Q.cacheBust" parameter to URL's querystring, and also
	 * returns the content digest hash for that particular URL, 
	 * if it corresponds to a file processed by the urls.php script.
	 * This function is very useful to use with clients like PhoneGap which can
	 * intercept URLs and load whatever locally cached files are stored in their bundle.
	 * The urls for these files will be relative to the cache base url.
	 * (See Q_Uri::cacheBaseUrl function).
	 * In this case, the client is supposed to send the timestamp of when the
	 * cache it is using was generated.
	 * This function checks the current contents of the Q/config/Q/urls.php file,
	 * generated by scripts/Q/urls.php script.
	 * If the url's timestamp there is newer than the Q_Request::cacheTimestamp()
	 * (which the client can set by setting the 'Q_ct' field in the querystring)
	 * then that means the server has a newer version of the file, so the passed
	 * $url is used instead.
	 * Otherwise, the url relative to cacheBaseUrl is used, making the client
	 * load the locally cached version.
	 * @param {string} $url The url to get the cached URL and hash for
	 * @param {array} [$options=array()]
	 * @param {boolean} [$options.skipCacheBaseUrl=false] If true, skips the cacheBaseUrl transformations
	 * @return {array} array($urlWithCacheBust, $hash)
	 */
	static function cachedUrlAndHash($url, $options = array()) {
		$cacheTimestamp = Q_Request::cacheTimestamp();
		$environment = Q_Config::get('Q', 'environment', '');
		$config = Q_Config::get('Q', 'environments', $environment, 'urls', array());
		if (empty(Q_Uri::$urls)) {
			return array($url, null);
		}
		$fileTimestamp = null;
		$fileSHA = null;
		if (!empty($config['caching']) or !empty($config['integrity'])) {
			$parts = explode('?', $url);
			$head = $parts[0];
			$tail = (count($parts) > 1 ? $parts[1] : '');
			$urlRelativeToBase = substr($head, strlen(Q_Request::baseUrl(false)));
			$parts = explode('/', $urlRelativeToBase);
			array_shift($parts);
			$parts[] = null;
			$tree = new Q_Tree(Q_Uri::$urls);
			$info = call_user_func_array(array($tree, 'get'), $parts);
			if (!empty($config['caching'])) {
				$fileTimestamp = Q::ifset($info, 't', null);
			}
			if (!empty($config['integrity'])) {
				$fileSHA1 = Q::ifset($info, 'h', null);
			}
		}
		if ($cacheTimestamp
		and isset($fileTimestamp)
		and $fileTimestamp <= $cacheTimestamp
		and self::$cacheBaseUrl) {
			return array(self::$cacheBaseUrl . $urlRelativeToBase, $fileSHA1);
		}
		if ($fileTimestamp) {
			$field = Q_Config::get(Q::app(), 'response', 'cacheBustField', 'Q.cacheBust');
			$fields = parse_str($tail);
			$fields[$field] = $fileTimestamp;
			$qs = http_build_query($fields);
			return array(Q_Uri::fixUrl("$head?$qs"), $fileSHA1);
		}
		return array($url, $fileSHA1);
	}
	
	/**
	 * @method fromArray
	 * @static
	 * @protected
	 * @param {array} $source
	 * @return {Q_Uri}
	 */
	protected static function fromArray(
	 $source)
	{
		$u = new Q_Uri();
		if (isset($source['?'])) {
			$u->querystring = $source['?'];
		}
		if (isset($source['#'])) {
			$u->anchorstring = $source['#'];
		}
		$u->fields = $source;
		return $u;
	}
	
	/**
	 * @method fromString
	 * @static
	 * @protected
	 * @param {string} $source
	 * @return {Q_Uri}
	 * @throws {Q_Exception_WrongType}
	 */
	protected static function fromString(
	 $source)
	{		
		if (!is_string($source)) {
			// Better to throw an exception that return a non-object,
			// which may cause a fatal error
			throw new Q_Exception_WrongType(array('field' => 'source', 'type' => 'string'));
		}
		$uri = new Q_Uri();
		$source_parts = explode(' ', $source, 2);
		$parts = explode('#', $source_parts[0], 2);
		if (count($parts) > 1) {
			$uri->anchorstring = $parts[1];
		}
		$parts = explode('?', $parts[0], 2);
		if (count($parts) > 1){
			$uri->querystring = $parts[1];
		}
		$parts2 = explode('/', $parts[0], 2);
		if (count($parts2) < 2) {
			throw new Q_Exception('"' . $parts[0] . '" is not of the form $module/$action');
		}
		$uri->fields['module'] = $parts2[0];
		$uri->fields['action'] = $parts2[1];
		if (count($source_parts) > 1) {
			$more_fields = self::decode($source_parts[1]);
			foreach ($more_fields as $name => $value) {
				$uri->fields[$name] = $value;
			}
		}
		return $uri;
	}
	
	/**
	 * @method encode
	 * @static
	 * @protected
	 * @param {array} $fields An array of fields
	 * @return {string} A representation like a=b c=d where a, b, c, d are urlencoded
	 */
	protected static function encode(
	 $fields)
	{
		$clauses = array();
		ksort($fields);
		foreach ($fields as $k => $v) {
			if (is_array($v)) {
				$v_parts = array();
				foreach ($v as $v2) {
					$v_parts[] = urlencode($v2);
				}
				$clauses[] = urlencode($k).'='.implode('/', $v_parts);
			} else {
				$clauses[] = urlencode($k).'='.urlencode($v);
			}
		}
		return implode(' ', $clauses);
	}
	
	/**
	 * @method decode
	 * @static
	 * @protected
	 * @param {string} $tail This can either be JSON, or something that looks like a=b c=d where a, b, c, d are urlencoded
	 * @return {array}
	 */
	protected static function decode(
	 $tail)
	{
		if ($tail[0] === '{' and $result = Q::json_decode($tail, true)) {
			return $result;
		}
		$clauses = explode(' ', $tail);
		$result = array();
		foreach ($clauses as $clause) {
			list($left, $right) = explode('=', $clause);
			$right_parts = explode('/', $right);
			if (count($right_parts) === 1) {
				$result[urldecode($left)] = urldecode($right);
			} else {
				$left_parts = array();
				foreach ($right_parts as $rp) {
					$left_parts[] = urldecode($rp);
				}
				$result[urldecode($left)] = $left_parts;
			}
		}
		return $result;
	}
	
	/**
	 * @property $fields
	 * @protected
	 * @type array
	 */
	protected $fields = array();
	/**
	 * @property $route
	 * @protected
	 * @type string
	 */
	protected $route = null;
	/**
	 * @property $querystring
	 * @protected
	 * @type string
	 */
	protected $querystring = null;
	/**
	 * @property $anchorstring
	 * @protected
	 * @type string
	 */
	protected $anchorstring = null;
	/**
	 * @property $suffix
	 * @protected
	 * @type array
	 */
	protected static $suffix = array();
	/**
	 * @property $variablePrefixes
	 * @public
	 * @type array
	 */
	public static $variablePrefixes = array('$', ':');
	/**
	 * @property $variablePrefixes
	 * @public
	 * @type array
	 */
	public static $escapedVariablePrefixes = array('\$', '\:');
	/**
	 * @property $cacheBaseUrl
	 * @public
	 * @type string
	 */
	protected static $cacheBaseUrl = null;
	/**
	 * @property $urls
	 * @public
	 * @type string
	 */
	static $urls = array();
	/**
	 * @property $url
	 * @public
	 * @type array
	 */
	protected $Q_url = null;
}