<?php if (!defined('VB_ENTRY')) die('Access denied.');
/*======================================================================*\
|| #################################################################### ||
|| # vBulletin 4.0.0 Alpha 5
|| # ---------------------------------------------------------------- # ||
|| # Copyright ©2000-2009 Jelsoft Enterprises Ltd. All Rights Reserved. ||
|| # This file may not be redistributed in whole or significant part. # ||
|| # ---------------- VBULLETIN IS NOT FREE SOFTWARE ---------------- # ||
|| # http://www.vbulletin.com | http://www.vbulletin.com/license.html # ||
|| #################################################################### ||
\*======================================================================*/

/**
 * Route Superclass.
 * The route object is responsible for parsing a route path into values, allowing
 * modification of those values, supplying the URL to the modified route and
 * delegating the appropriate controller for the route in it's current state.
 *
 * Which route class is used by default can be defined via the router.  Applications
 * need to create their own subclass of the route to enable application specific
 * behaviour.
 *
 * For this reason, route objects should only be created with the router factory
 * method router::newRoute().
 *
 * Note: Defined members are preceeded with an underscore to avoid collision with
 * magic properties in $_segments accessed via __get() and __set().
 *
 * @author vBulletin Development Team
 * @version $Revision: 29650 $
 * @since $Date: 2009-02-25 15:39:20 +0000 (Wed, 25 Feb 2009) $
 * @copyright Jelsoft Enterprises Ltd.
 */
abstract class vB_Route
{
	/*Properties====================================================================*/

	/**
	 * The value of the user request segment of this route class.
	 * This is fetched from vB_Router using the defined lookup table `route`.
	 * @see vB_Router::getRouteSegmentFromClass()
	 *
	 * @var string
	 */
	protected $_class_segment;

	/**
	 * Whether the given route is valid.
	 *
	 * @var bool
	 */
	protected $_is_valid = true;

	/**
	 * Array of segment names => values.
	 * Populated when the route is consumed or new segments are set.
	 *
	 * @var array string
	 */
	protected $_segments = array();

	/**
	 * A scheme for segments.
	 * Populating this assoc array allows you to automate parsing of the segments and
	 * can simplify extending the class for applications.
	 *
	 * The schema must be exhaustive and include all possible segments.
	 *
	 * Segments here can be interpreted in three ways:
	 *  Optional with Values
	 * 		If one of defined values is not matched, the segment is not consumed or
	 * 		rendered.
	 *  Expected no Values - D - G - T -
	 * 		The next available segment will always be consumed as this.
	 *  Expected with Values
	 * 		Only the defined values are allowed.  Consuming a route that fails the
	 * 		match will throw a 404 unless it is the last segment and has a default
	 * 		value to fall back on. Similarly, setting a segment value with client
	 * 		code that is not in the values list will throw a vB_Exception_Router.
	 *
	 * The scheme is used for both consuming and rendering a route.
	 *
	 * A default value can be specified.  When rendering a route, if the segment is
	 * optional and the value matches the default value, or is unspecified then the
	 * segment will not be rendered.
	 *
	 * Similarly, if the segment is expected and matches the default, and is the
	 * last segment, then it will not be rendered.
	 *
	 * When consuming a route, if a segment is not specified then the given defaults
	 * will be assumed.
	 *
	 * Example scheme:
	 * 	'output'	=>	'optional'	=>	true
	 * 					'values'	=>	'html'
	 * 									'rss'
	 * 									'atom'
	 * 									'ajax'
	 * 									'text'
	 * 					'default'	=>	'html'
	 * 	'language'	=>	'optional'	=>	false
	 * 					'default'	=>	'en-US'
	 * 	'controller'=>	'optional'	=>	false
	 * 					'values'	=>	'index'
	 * 									'news'
	 * 									'about'
	 * 					'default'	=>	'index'
	 *
	 * @var array mixed
	 */
	protected $_segment_scheme;

	/**
	 * Whether the scheme has been validated or not
	 *
	 * @var bool
	 */
	protected $_validated_scheme = false;

	/**
	 * A default controller class.
	 * Useful for routes that only use one controller class.
	 * @see vB_Route::getResponse()
	 *
	 * @var string
	 */
	protected $_default_controller_class;

	/**
	 * The class to use for error rerouting.
	 *
	 * @var string
	 */
	protected $_error_route_class = 'vB_Route_Error';

	/**
	 * The original or compiled route path.
	 * When a route is created an initial route should be specified, or the default
	 * path is used.
	 *
	 * If a segment value or parameters are changed and a URL is requested, the
	 * route path is recompiled.
	 *
	 * @var string
	 */
	protected $_route_path;

	/**
	 * Whether the route has been modified.
	 * When a segment value or parameter has been modified, this is set to true to
	 * allow the route to know that it should recompile the route path if a URL is
	 * requested.
	 *
	 * @var bool
	 */
	protected $_modified = false;

	/**
	 * Whether the original route path was parsed into values.
	 * When a segment value is set or requested the route first parses the original
	 * route path given upon creation and assigns the values to the defined segments.
	 *
	 * @var bool
	 */
	protected $_parsed = false;

	/**
	 * Controller Parameters.
	 * Controller parameters make up part of the route to pass values to the
	 * requested controller.  If the controller is changed, child classes should
	 * ensure that the parameters are emptied.
	 *
	 * Controllers can request their parameters with router::getParameters().
	 *
	 * NOTE: Controllers are responsible for validating all parameter and segment
	 * values.
	 *
	 * @var array mixed
	 */
	protected $_parameters = array();

	/**
	 * Whether to reset all parameters whenever any segment changes.
	 * If a route needs to reset parameters on selected segments only then it should
	 * override vB_Route::assignSegment().
	 *
	 * @var bool
	 */
	protected $_reset_params_on_change = false;


	/**
	 * The default route path.
	 * If a route path is not specified when the route is created, the default path
	 * is used.
	 *
	 * @var string
	 */
	protected $_default_path = false;

	/**
	 * The message of a reroute.
	 * If a reroute occurs, a message can be stored here for display.
	 *
	 * @var string
	 */
	protected static $_reroute_message;



	/*Initialisation================================================================*/

	/**
	 * The constructor.
	 * A base URL is required to prepend to compiled URL.  If a route path is not
	 * specified then the default route path is assumed.
	 * The route path can also be specified as integer 404 or integer 500 to use the
	 * error paths respectively.
	 *
	 * @param string | int $route_path			- The route path to compile
	 */
	public function __construct($route_path = false)
	{
		// Ensure the default path is set for this route
		if (false === $this->_default_path)
		{
			throw (new vB_Exception_Router('No default path has been defined for the route class'));
		}

		// Get the class segment for this route type
		if (!($this->_class_segment = trim(vB_Router::getRouteSegmentFromClass(get_class($this)))))
		{
			throw (new vB_Exception_Router('Could not get the request segment for the route class \'' . get_class($this) . '\''));
		}

		// Allow dynamic segment schemes
		$this->buildSegmentScheme();

		// Set the route path
		$this->_route_path = $this->resolveRoutePath($route_path);
	}


	/**
	 * Simple factory method for creating a route.
	 *
	 * @param string $class						- The class of the route to create
	 * @param string $route_path				- The route path to compile
	 * @return vB_Route							- The instantiated route object
	 */
	public static function create($class, $route_path = false)
	{
		return new $class($route_path);
	}



	/*URL===========================================================================*/

	/**
	 * String conversion.
	 * Returns a string representation of the current route as used in URLs.
	 *
	 * NOTE: The base_url is not prepended.  @see getURL() to create valid URLs.
	 *
	 * @return string							- The URL representing the route
	 */
	public function __toString()
	{
		try
		{
			// Ensure the route path is fresh
			$this->buildRoutePath();
	
			return $this->_route_path;
		}
		catch (exception $e)
		{
			return vB_Router::get500Path();
		}
	}


	/**
	 * Returns a representative URL of the current route state.
	 * Optional segments and parameters may be passed to set the route state.
	 *
	 * @param array mixed $segments				- Assoc array of segment => value
	 * @param array mixed $parameters			- Array of parameter values, in order
	 * @return string							- The URL representing the route
	 */
	public function getCurrentURL(array $segments = null, array $parameters = null)
	{
		// Ensure the route is valid
		if (!$this->isValid())
		{
			throw (new vB_Exception_Router('A route URL was requested but the route is invalid'));
		}

		// Set any values specified
		if ($segments)
		{
			$this->setSegments($segments);
		}

		// Set new parameters if specified
		if ($parameters)
		{
			$this->setParameters($parameters);
		}

		// Ensure the route path is fresh
		$this->buildRoutePath();

		// Return the appropriate route path
		if ($this->_route_path == $this->_class_segment)
		{
			return vB_Router::getBaseRoutingURL();
		}
		else
		{
			// Return the URL
			return vB_Router::getBaseRoutingURL() . $this->_route_path;
		}
	}


	/**
	 * Returns a representative URL of a route.
	 * Optional segments and parameters may be passed to set the route state.
	 *
	 * @TODO: Uncomment and remove child implementations when LSB is available
	 *
	 * @param array mixed $segments				- Assoc array of segment => value
	 * @param array mixed $parameters			- Array of parameter values, in order
	 * @return string							- The URL representing the route
	 */
	abstract public static function getURL(array $segments = null, array $parameters = null);
	/*
	{
		$class = __CLASS__;
		$route = new $class();
		return $route->getCurrentURL($segments, $parameters);
	}
	*/


	/**
	 * Resolves the route path to use.
	 *
	 * @param string $route_path
	 */
	protected function resolveRoutePath($route_path = false)
	{
		if (!$route_path)
		{
			return $this->getDefaultPath();
		}
		
		return trim($route_path, ' /');
	}


	/**
	 * Gets the default path.
	 *
	 * @return string
	 */
	public function getDefaultPath()
	{
		return $this->_class_segment . '/' . $this->_default_path;
	}


	/**
	 * Gets the 403 path.
	 *
	 * @return string
	 */
	public function get403Path()
	{
		return vB_Route::create($this->_error_route_class)->get403Path();
	}


	/**
	 * Gets the 404 path.
	 *
	 * @return string
	 */
	public function get404Path()
	{
		return vB_Route::create($this->_error_route_class)->get404Path();
	}


	/**
	 * Gets the 409 path.
	 *
	 * @return string
	 */
	public function get409Path()
	{
		return vB_Route::create($this->_error_route_class)->get409Path();
	}


	/**
	 * Gets the 500 path.
	 *
	 * @return string
	 */
	public function get500Path()
	{
		return vB_Route::create($this->_error_route_class)->get500Path();
	}


	/**
	 * Builds a string path representing the current segment and parameter values.
	 * How this path is built will vary wildly between implementations depending on
	 * the segment scheme.
	 */
	protected function buildRoutePath()
	{
		// Ensure the original route path was parsed
		$this->parseRoutePath();

		// Only compile the route path if it is stale
		if ($this->_modified OR !$this->_route_path)
		{
			// start with the class segment
			$this->_route_path = $this->_class_segment;
			$deferred_path = '';

			foreach ($this->_segment_scheme AS $name => $segment)
			{
				if (!$segment['optional'] AND ($this->_segments[$name] == $segment['default']))
				{
					$deferred_path .= ($deferred_path ? '/' : '') . urlencode($this->_segments[$name]);
				}
				else if ($this->_segments[$name] != $segment['default'])
				{
					$this->_route_path = ($this->_route_path ? $this->_route_path . '/' : '') . ($deferred_path ? $deferred_path . '/' : '') . urlencode($this->_segments[$name]);
					$deferred_path = '';
				}
			}

			if (sizeof($this->_parameters))
			{
				// ensure any deferred path is added before parameters
				if ($deferred_path)
				{
					$this->_route_path = ($this->_route_path ? $this->_route_path . '/' : '') . $deferred_path;
				}

				$parameters = array();
				foreach ($this->_parameters AS $parameter)
				{
					$parameters[] = urlencode($parameter);
				}

				$this->_route_path .= '/' . implode('/', $parameters);
			}
		}

		$this->_modified = false;
	}


	/**
	 * Parses a string path into segment and parameter values.
	 * How the path is parsed will vary between implementations.
	 *
	 * This is only done once when a new route is created and values are requested.
	 */
	protected function parseRoutePath()
	{
		// We only parse the original route path once
		if ($this->_parsed)
		{
			return;
		}

		$this->_parsed = true;

		// Ensure the segment scheme is valid
		$this->validateSegmentScheme();

		// If no route path was given, use the scheme's default values
		if (!$this->_route_path)
		{
			// the default segment values are used
			foreach ($this->_segment_scheme AS $name => $segment)
			{
				if (!$this->validateSegment($name, $this->_segment_scheme[$name]['default']))
				{
					throw (new vB_Exception_Router('The default value \'' . $this->_segment_scheme[$name]['default'] . '\ is not valid for the segment \'' . $name . '\''));
				}

				// validate the segment value and transform if applicable
				$this->setSegment($name, $this->_segment_scheme[$name]['default']);
			}

			return;
		}

		// Break down the given route path
		$segment_values = explode('/', $this->_route_path);

		// Remove and verify the class segment
		if ($this->_class_segment != ($class_segment = array_shift($segment_values)))
		{
			throw (new vB_Exception_Router('Parsing route path with incorrect route.  Path class segment: \'' . htmlspecialchars($class_segment) . '\', Route class segment: \'' . $this->_class_segment . '\''));
		}

		// Get defined segments
		foreach ($this->_segment_scheme AS $name => $segment)
		{
			if (sizeof($segment_values))
			{
				$valid = $this->validateSegment($name, $segment_values[0]);
			}

			if (!sizeof($segment_values) OR ($this->_segment_scheme[$name]['optional'] AND !$valid))
			{
				$this->setSegment($name, $this->_segment_scheme[$name]['default'], true);
			}
			else if ($this->_segment_scheme[$name]['optional'] OR ($valid))
			{
				$this->setSegment($name, $segment_values[0], true);
				array_shift($segment_values);
			}
			else
			{
				// the given route is not valid
				$this->_is_valid = false;

				$this->_segments[$name] = false;
				array_shift($segment_values);
			}
		}

		// Any remaining segments are parameters
		$this->_parameters = $segment_values;
	}


	/**
	 * Checks if the parsed route is valid.
	 *
	 * @return bool
	 */
	public function isValid()
	{
		// Ensure the route has been parsed
		$this->parseRoutePath();

		return $this->_is_valid;
	}


	/**
	 * Builds dynamic segment schemes.
	 */
	protected function buildSegmentScheme(){}


	/**
	 * Validates the segment scheme.
	 * This is done before any parsing to ensure a valid scheme was defined.
	 *
	 * @throws vB_Exception_Router
	 */
	protected function validateSegmentScheme()
	{
		if ($this->_validated_scheme)
		{
			return;
		}

		$this->_validated_scheme = true;

		if (empty($this->_segment_scheme) OR !is_array($this->_segment_scheme))
		{
			throw (new vB_Exception_Router('No valid segment schema defined in router'));
		}

		// Ensure default calues are et in the scheme
		foreach ($this->_segment_scheme AS $name => $segment)
		{
			if (!isset($this->_segment_scheme[$name]['default']))
			{
				throw (new vB_Exception_Router('The route segment \'' . $name . '\' has no default value'));
			}

			if (!isset($this->_segment_scheme[$name]['optional']))
			{
				$this->_segment_scheme[$name]['optional'] = false;
			}

			if ($this->_segment_scheme[$name]['optional'] AND (!is_array($this->_segment_scheme[$name]['values']) OR !sizeof($this->_segment_scheme[$name]['values'])))
			{
				throw (new vB_Exception_Router('The optional segment \'' . $name . '\' has no values defined'));
			}
		}
	}


	/**
	 * Validates a segment.
	 * The segment scheme is checked for constraints and boolean false is returned
	 * if the segment is not valid.
	 *
	 * Child classes can extend this if they use other validation methods.
	 *
	 * @throws vB_Exception_Router
	 *
	 * @param string $name						- The key name of the segment
	 * @param mixed $value						- The value to validate
	 * @return bool								- Success
	 */
	protected function validateSegment($name, $value)
	{
		// Ensure segment is defined
		if (!isset($this->_segment_scheme[$name]))
		{
			throw (new vB_Exception_Router('An invalid route segment was given for validation: \'' . $name . '\''));
		}

		// If the segment scheme has values, only those values are allowed
		if (isset($this->_segment_scheme[$name]['values']))
		{
			if (!in_array($value, $this->_segment_scheme[$name]['values']))
			{
				return false;
			}
		}

		// Is valid
		return true;
	}


	/**
	 * Sets a segment value.
	 * Also checks if the route segments have been modified, and transforms segment
	 * values where needed.  Values given here should be considered valid and should
	 * only be transformed to boolean false if it can be successfully transformed
	 * back into a valid route path segment string.
	 *
	 * @param string $segment					- The name of the segment to update
	 * @param mixed $value						- The value to set the segment to
	 * @param bool $nomodify					- Don't flag the route object as modified
	 */
	protected function setSegment($segment, $value, $nomodify = false)
	{
		if (!isset($this->_segment_scheme[$segment]))
		{
			throw (new vB_Exception_Warning('A route segment value was set but the segment is undefined: \'' . $segment . '\''));
		}

		// Ensure the original route path was parsed first
		$this->parseRoutePath();

		// Update the segment
		if ($this->assignSegment($segment, $value))
		{
			if (!$nomodify)
			{
				$this->_modified = true;
				$this->_parameters = array();
			}
		}
	}


	/**
	 * Assigns a value to a segment.
	 * Useful for overiding.
	 *
	 * @param string $segment					- The name of the segment to update
	 * @param mixed $value						- The value to set the segment to
	 * @return bool								- Whether the segment was modified
	 */
	protected function assignSegment($segment, $value)
	{
		if (!isset($this->_segments[$segment]) OR $this->_segments[$segment] != $value)
		{
			$this->_segments[$segment] = $value;

			if ($this->_reset_params_on_change)
			{
				$this->_parameters = array();
			}

			return true;
		}

		return false;
	}


	/**
	 * Allows multiple segments to be set.
	 * The array given must be in the form segment => value.
	 *
	 * @param array mixed $segment_values		- The segment name => values to set
	 * @param bool $nomodify					- Don't flag the route object as modified
	 */
	public function setSegments($segment_values, $nomodify = false)
	{
		if (!is_array($segment_values))
		{
			throw (new vB_Exception_Router('The values passed to route::setSegments are not an array'));
		}

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

		foreach ($segment_values AS $segment => $value)
		{
			$this->setSegment($segment, $value, $nomodify);
		}
	}


	/*Controller====================================================================*/

	/**
	 * Returns the response for the route.
	 * The default implementation delegates the controller class specified by
	 * vB_Route::$_default_controller_class.  Child implementations may vary.
	 *
	 * @return string							- The response
	 */
	public function getResponse()
	{
		if (!$this->_is_valid)
		{
			throw (new vB_Exception_404());
		}

		if ($this->_default_controller_class)
		{
			$controller = new $this->_default_controller_class($this->_parameters);
		}
		else
		{
			throw (new vB_Exception_Route('No default controller or getResponse() defined for route \'' . get_class($this) . '\''));
		}

		return $controller->getResponse();
	}


	/*Accessors=====================================================================*/

	/**
	 * Sets the parameters for the controller.
	 *
	 * NOTE: If a new controller is required then it should be set before any new
	 * parameters for that controller as the parameters should reset when changing
	 * the controller.
	 *
	 * NOTE: Parameters are indexed numerically.
	 * @see setParameter()
	 *
	 * @param array mixed $params				- The parameters for the current controller
	 */
	public function setParameters($params)
	{
		$params = (array)$params;

		if ($this->_parameters !== $params)
		{
			$this->_parameters = array_values($params);
			$this->_modified = true;
		}
	}


	/**
	 * Sets a single parameter value based on it's index.
	 *
	 * NOTE: Parameters are indexed numerically and cannot be named as the route and
	 * router are unaware of the controller that will be resolved.
	 *
	 * For this reason, all client code should be considered 'controller' aware, ie
	 * they should add parameter values based on the requirement of the concrete
	 * controller classes, and in the correct order.
	 *
	 * @param int $index						- The index of the parameter
	 * @param mixed $value						- The parameters value
	 */
	public function setParameter($index, $value)
	{
		if (!isset($this->_parameters[$index]) OR $this->_parameters[$index] != $value)
		{
			$this->_parameters[$index] = $value;
			$this->_modified = true;
		}
	}


	/**
	 * Gets all of the currently set parameters.
	 *
	 * NOTE: It is the responsibility of the delegated controller to request and
	 * validate the parameters.
	 *
	 * @return array							- The currently set parameters.
	 */
	public function getParameters()
	{
		// Ensure the route is valid
		if (!$this->isValid())
		{
			throw (new vB_Exception_Router('A route parameter value was requested but the route is invalid'));
		}

		return $this->_parameters;
	}


	/**
	 * Gets a single parameter value based on it's index.
	 *
	 * NOTE: Parameters are numerically indexed.
	 * @see setParameter()
	 *
	 * @param int $index						- The parameter to get
	 * @return mixed							- The value of the parameter
	 */
	public function getParameter($index)
	{
		// Ensure the route is valid
		if (!$this->isValid())
		{
			throw (new vB_Exception_Router('A route parameter value was requested but the route is invalid'));
		}

		if (!isset($this->_parameters[$index]))
		{
			return NULL;
		}

		return $this->_parameters[$index];
	}


	/**
	 * Gets the value of a defined segment.
	 * If the original route path has not yet been parsed then it will be parsed the
	 * first time a segment value is requested.
	 *
	 * The possible segment names and default values should be defined in
	 * $_segment_scheme.
	 * @see $_segments, $_segment_scheme
	 *
	 * @param string $segment					- The name of the segment to get
	 * @return mixed							- The current value of the segment
	 */
	public function __get($segment)
	{
		// Check the segment is defined
		if (!isset($this->_segment_scheme[$segment]))
		{
			throw (new vB_Exception_Router('A route segment value was requested but the segment is undefined: \'' . $segment . '\''));
		}

		// Ensure the route is valid
		if (!$this->isValid())
		{
			throw (new vB_Exception_Router('A route segment value was requested but the route is invalid'));
		}

		return $this->_segments[$segment];
	}


	/**
	 * Sets the value of a segment.
	 * If the value is not valid then an exception is thrown.
	 *
	 * @param string $segment					- The name of the segment
	 * @param mixed $value						- The value to set it to
	 */
	public function __set($segment, $value)
	{
		if (!$this->validateSegment($segment, $value))
		{
			throw (new vB_Exception_Router('The route segment \'' . $segment . '\' was set with an invalid value: \'' . $value . '\''));
		}

		$this->setSegment($segment, $value);
	}



	/*Rerouting=====================================================================*/

	/**
	 * Sets a reroute message.
	 *
	 * @param string $message
	 */
	public function setRerouteMessage($message)
	{
		self::$_reroute_message = $message;
	}


	/**
	 * Returns the reroute message if one is set.
	 *
	 * @return string
	 */
	public function getRerouteMessage()
	{
		return self::$_reroute_message;
	}
}

/*======================================================================*\
|| ####################################################################

|| # SVN: $Revision: 29650 $
|| ####################################################################
\*======================================================================*/