<?php
/* WordPress Controller Implementation.
 *
 * The goals surrounding the controller implementation are the following:
 *    * Easier additions to routes using an API.
 *    * Demystify the black art around wp-rewrite.
 *    * Consolidate the controller from the three main classes in WordPress and have those classes
 *      use the controller implementation.
 *    * Implement a system similar and compatible to the other MVC frameworks out there with a few
 *      exceptions.
 *    * Allow for the system in WordPress to be replaced by another Controller class library, but
 *      not for example Code Igniter which is a MVC framework. The Controller part of any MVC
 *      framework or class library would have to be able to be independent of the rest of the system
 *      in order to replace this implementation.
 *
 * The exception for the MVC, when compared to other MVC frameworks is that the "module" feature
 * will not be implemented. Given the dynamic plugin structure of WordPress it would be unwise to
 * place such a restriction. The system will thus work more like MVC class libraries, in the sense
 * that you will specify the route and WordPress will iterate through the routes in order to check
 * which controller applies.
 *
 * No code has been used from the Zend Framework, nor is this a PHP4 compatible version of ZF
 * controller. While certain concepts or patterns may have been taken from ZF, the functionality of
 * ZF and this controller implementation are vastly different. It should be assumed that the base
 * implementation is of the most basic for a working controller.
 *
 * As the controller implementation needs to be simple, quick, and efficient, certain parts of the
 * controller pattern will be minimized or stripped out altogether. The parts of the controller that
 * are going to be implemented are the router, routes, and dispatcher.
 *
 * Actions will be implemented as part of the Plugin API and thus may not conform completely to Zend
 * Framework, Yii or other MVC class libraries. Actions as part of the Controller pattern may be
 * implemented later, when a need arises for functionality that is different from the Plugin API.
 *
 * @package WordPress
 * @subpackage Controller
 * @since {unknown}
 */

/**
 * Contains all of the available Routes for processing.
 *
 * The way the router works is that will run through all of the routes from the top to the bottom.
 * Once a route is reached that is matched, then the processing is stopped and that route is
 * returned. It is completely possible for two routes to be set to be matched against, and cause
 * confusion when the "right" one is not returned. The dispatcher may only process one controller
 * and if there are two or more, then a "bug" in the routes may be expected.
 *
 * The way to get around this is to place the most specific routes above the least specific routes.
 * So say, you match against '/my-base/page/(.*)', '/my-base/page' and '/my-base/'. You will want to
 * place them in that order. If you placed '/my-base' first, then the other two will never be
 * matched and the dispatcher will never run the controller for those two.
 *
 * The routes will have different implementations to allow for multiple choices for how the routes
 * will be checked. The basic check is against the full URL (after removing the base site URL), so
 * for example: does '/my-base/my-page/' == URL and if so then load the controller. This does not
 * allow for regex. It does lead to the second option, which is a regular expression route:
 * '/my-base/.*' == URL or '/my-base/(.*)' == URL, which will allow for checking against the
 * subclass.
 *
 * The additional of the Multi-Site feature in WordPress, the hostname will also be a route that
 * can be checked. For compatibility with Zend Framework, a chaining mechanism will also be
 * supported.
 *
 * @package WordPress
 * @subpackage Controller
 * @since {unknown}
 */
class WP_Router {

	/**
	 * Add a route.
	 *
	 * Supports adding multiple routes to the same route name, but be aware that if that route name
	 * is removed, then the entire set of routes will be removed as well. This is to allow for
	 * name clashes and still support working routes with minimal bugs resulting, unless the route
	 * is removed.
	 *
	 * @since {unknown}
	 * @access public
	 *
	 * @param string $name Reference for route.
	 * @param WP_Routes $route Not by reference. Requires route that extends WP_Routes.
	 */
	function add_route( $name, $route ) {
		if ( is_a( $route, 'WP_Routes' ) ) {
			if ( ! isset( $this->_routes[ $name ] ) ) {
				$this->_routes[ $name ] = $route;
			} else {
				if ( ! is_array( $this->_routes[ $name ] ) )
					$this->_routes[ $name ][] = $this->_routes[ $name ];

				$this->_routes[ $name ][] = $route;
			}
		}
	}

	/**
	 * Add a list of routes.
	 *
	 * @param array $routes Name should be key with value the route.
	 * @return null Returns before processing if empty list or not an array.
	 */
	function add_routes( $routes ) {
		if ( ! is_array($routes) || empty($routes) )
			return;

		foreach( $routes as $name => $route ) {
			$this->add_route( $name, $route );
		}
	}

	/**
	 * Used to remove route to prevent execution.
	 *
	 * @since {unknown}
	 * @access public
	 *
	 * @param string $name Reference for route.
	 */
	function remove_route($name) {
		if ( isset( $this->_routes[ $name ] ) )
			unset( $this->_routes[ $name ] );
	}
}

/**
 * Class for checking route classes.
 *
 * @abstract
 * @package WordPress
 * @subpackage Controller
 * @since {unknown}
 */
class WP_Routes {

	/**
	 * The map is class specific, so it might be a string or an array or another type.
	 *
	 * @since {unknown}
	 * @access private
	 * @var mixed
	 */
	var $_map = '';

	/**
	 * The controller will always be a callback type to be handled by the dispatcher.
	 *
	 * @since {unknown}
	 * @access private
	 * @var callback
	 */
	var $_controller = null;

	/**
	 * PHP4 backwards compatible constructor calls PHP5 constructor.
	 *
	 * @since {unknown}
	 * @access public
	 *
	 * @param string $map Class specific.
	 * @param callback $callback Callback to run in the dispatcher.
	 */
	function WP_Routes( $map, $callback ) {
		$this->__construct( $map, $callback );
	}

	/**
	 * Routes consist of the mapping between the URL and a callback.
	 *
	 * The callback should also support PHP5.3 closures as well.
	 *
	 * @since {unknown}
	 * @access public
	 *
	 * @param string $map Class specific.
	 * @param callback $callback Callback to run in the dispatcher.
	 */
	function __construct( $map, $callback ) {
		$this->_map = $map;

		if ( is_callable($callback) ) {
			$this->_controller = $callback;
		}
	}

	/**
	 * Checks whether the dispatcher may use this route.
	 *
	 * In the event that the constructor was not given a valid callback for the dispatcher, then the
	 * match may not be run. There is unfortunately no way to tell the creator of the route that the
	 * route will not be processed, unless this method is called before passing on to the router. It
	 * is assumed that the callback is going to be valid and that the passing of the route will be
	 * without assigning to a variable first.
	 *
	 * This method can be used during debugging to ensure that the callback is correct when passing
	 * to the router.
	 *
	 * @since {unknown}
	 * @access public
	 *
	 * @return bool Whether the dispatcher may use this route.
	 */
	function is_route() {
		if ( is_callable( $this->_controller ) )
			return true;

		return false;
	}

	/**
	 * Retrieve controller for dispatcher.
	 *
	 * The match method will return a boolean, therefore the controller has to be retrieved using a
	 * getter method.
	 *
	 * @since {unknown}
	 * @access public
	 *
	 * @return callback
	 */
	function get_dispatcher() {
		return $this->_controller;
	}

	/**
	 * Matches against specific functionality or processing in each class that implements method.
	 *
	 * This method must be implemented in each class, because this method will be used in the router
	 * to check whether the route matches and then run the dispatch.
	 *
	 * @abstract
	 * @access public
	 * @since {unknown}
	 *
	 * @return WP_Error
	 */
	function match() {
		return new WP_Error('wp-routes', sprintf( __('Class %s did not implement match() method.'), __CLASS__ ) );
	}

}

/**
 * Matches against URL path exactly.
 *
 * The URL path will not include the full URI, it will be minus the hostname and minus the base site
 * URL. So if the base site URL is 'http://example.com/wordpress' and the URL is
 * 'example.com/wordpress/my-base/controller/action', then this class will match against
 * '/my-base/controller/action'. Therefore, the map will have to be set to
 * '/my-base//controller/action' for match() to return true.
 *
 * Does not allow regular expressions! Use {@link WP_Route_Regex} for regular expressions.
 *
 * @package WordPress
 * @subpackage Controller
 * @since {unknown}
 */
class WP_Route_Static extends WP_Routes {

	/**
	 * Whether the match is for a specific string.
	 *
	 * @access public
	 * @since {unknown}
	 *
	 * @return bool Whether route is successful.
	 */
	function match() {
		// The URL map must have the slash at the beginning or it won't ever match. Add it and
		// remove the ending slash because the processed URL will not have it either.
		$url_static = '/'. trim($this->_map, '/');

		return ( $url_static == _get_uri_request() );
	}

}

class WP_Route_Regex extends WP_Routes {

	/**
	 * Matches against specific functionality or processing in each class that implements method.
	 *
	 * This method must be implemented in each class, because this method will be used in the router
	 * to check whether the route matches and then run the dispatch.
	 *
	 * @access public
	 * @since {unknown}
	 *
	 * @return bool Whether route is successful.
	 */
	function match() {
		/** @todo $matches must be passed to the controller through the dispatcher. */
		if ( preg_match( '|'. $this->_map .'|', _get_uri_request(), $matches ) ) {
			return true;
		}
	}

}

/**
 * Splits the URL into 'segments' to be matched with the remainder passed on.
 *
 * There will be a URL segment that will split the URL and allow for checking against different
 * paths. For example,* for a URL path that could be '/my-base/controller/action/option1',
  '/my-base/controller/' or '/my-base/controller/action/option1/option2' should all go to the
 * controller set for '/my-base/controller/' with the action and options as arguments. The segment
 * route would allow for that functionality.
 *
 * The specifications for this class is found in a ticket which will need to be referenced for how
 * this class should function.
 *
 */
class WP_Route_Segment extends WP_Routes {

	/**
	 * Matches against specific functionality or processing in each class that implements method.
	 *
	 * This method must be implemented in each class, because this method will be used in the router
	 * to check whether the route matches and then run the dispatch.
	 *
	 * @access public
	 * @since {unknown}
	 *
	 * @return bool Whether route is successful.
	 */
	function match() {
		return new WP_Error('wp-routes', sprintf( __('Class %s did not implement match() method.'), __CLASS__ ) );
	}

}

/**
 * Checks against an option in the table.
 *
 * This class can be extended for customize processing of options that require it. This base class
 * may only be used on options that are scalar (any value from the table will be a string). It will
 * fail against options are arrays or serialized.
 *
 * @package WordPress
 * @subpackage Controller
 * @extends WP_Routes
 * @since {unknown}
 */
class WP_Route_Option extends WP_Routes {

	/**
	 * Checks against option in the table.
	 *
	 * The way to use this route is by passing the map parameter an array that includes the option
	 * name as the key and the value as the value to check against the option. Only works with
	 * scalar options.
	 *
	 * <code>
	 * $route = new WP_Route_Option( array( 'home_url' => 'http://example.com/' ), 'my_callback' );
	 * </code>
	 *
	 * @access public
	 * @since {unknown}
	 *
	 * @param array $map Should have the option name as key 'option_name' and what the value should be as the value.
	 * @param callback $callback Controller callback.
	 */
	function __construct( $map, $callback ) {
		parent::__construct( $map, $callback );
	}

	/**
	 * Matches against specific functionality or processing in each class that implements method.
	 *
	 * This method must be implemented in each class, because this method will be used in the router
	 * to check whether the route matches and then run the dispatch.
	 *
	 * @access public
	 * @since {unknown}
	 *
	 * @return bool Whether route is successful.
	 */
	function match() {
		global $blog_id;

		// Will only fetch the first key, so any other pairs will be ignored. Also, the key to
		// check must be the first value.
		if ( empty( $blog_id ) || ! is_multisite() ) {
			$option = get_option( key($this->_map) );
		} else {
			$option = get_blog_option( $blog_id, key($this->_map) );
		}

		if ( ! $option )
			return false;

		if ( current( $this->_map ) == $option )
			return true;

		return false;
	}

}

/**
 * Processing against the rewrite_rules option in the database.
 *
 * Given that WordPress works differently than all other MVC libraries and frameworks, custom code
 * will be required to allow for what WordPress likes to place in the 'rewrite_rules' option in the
 * DB. This special code will be available for those wishing to assign additional dynamic rewrites.
 *
 * For an example, if the permalink structure is /page-name/, then WordPress will place each page
 * name in the 'rewrite_rules' option. Therefore, WordPress will then be required to check for the
 * slug for each page. That does not include the 7 or 8 extra rules for page numbers, etc that will
 * also have to be checked against. This leads to highly inefficient check against pages. However,
 * it is quite useful for those who wish to keep the structure of their former web site.
 *
 * The class is special for WordPress, in that it may only be used for WordPress. In the event that
 * another class library is used, this will have to be implemented to allow for backwards
 * compatibility, with how WordPress works.
 *
 * For the most part, this class will take over parts of the {@link WP::parse_request()) method. At
 * least the parts where routes are processed. Parts of the process will also be found in other
 * classes.
 *
 * @package WordPress
 * @subpackage Controller
 * @extends WP_Route_Option
 * @since {unknown}
 */
class WP_Route_Option_Rewrite_Rules extends WP_Route_Option {

	/**
	 * Matches against specific functionality or processing in each class that implements method.
	 *
	 * This method must be implemented in each class, because this method will be used in the router
	 * to check whether the route matches and then run the dispatch.
	 *
	 * @access public
	 * @since {unknown}
	 *
	 * @return bool Whether route is successful.
	 */
	function match() {
		$rewrite = get_option('rewrite_rules');

		if ( empty( $rewrite ) )
			return false;

		// Must keep track of the processed query vars for the rest of WordPress.
		global $_wp_perma_query_vars, $_wp_matched_rule, $_wp_matched_query;

		$request_match = $req_uri = _get_uri_request();

		// Don't try to match against AtomPub calls
		if ( $req_uri == 'wp-app.php' )
			return false;

		foreach ( (array) $rewrite as $match => $query) {
			if ( preg_match("#^$match#", $request_match, $matches) ||
				preg_match("#^$match#", urldecode($request_match), $matches) ) {
				// Got a match.
				$_wp_matched_rule = $match;

				// Trim the query of everything up to the '?'.
				$query = preg_replace("!^.+\?!", '', $query);

				// Substitute the substring matches into the query.
				$query = addslashes(WP_MatchesMapRegex::apply($query, $matches));

				$_wp_matched_query = $query;

				// Parse the query.
				parse_str($query, $perma_query_vars);

				// If we're processing a 404 request, clear the error var
				// since we found something.
				if ( isset($_GET['error']) )
					unset($_GET['error']);

				break;
			}
		}

		$home_path = get_home_url_path();

		// If req_uri is empty or if it is a request for ourself, unset error.
		if ( empty($req_uri) || $req_uri == $self || strpos($_SERVER['PHP_SELF'], 'wp-admin/') !== false ) {
			if ( isset($_GET['error']) )
				unset($_GET['error']);

			if ( isset($perma_query_vars) && strpos($_SERVER['PHP_SELF'], 'wp-admin/') !== false )
				unset($perma_query_vars);
		}
	}

}

class WP_Route_Hostname extends WP_Routes {

	/**
	 * Matches against multi-site blog hostname or blog ID.
	 *
	 * This should not be restricted based on supported host names in multi-site. This allows for
	 * a plugin to be implemented that supports other host names to create a simple site using
	 * WordPress.
	 *
	 * Checks against HTTP_HOST, SERVER_NAME and REQUEST_URI to ensure accuracy.
	 *
	 * @access public
	 * @since {unknown}
	 *
	 * @return bool Whether route is successful.
	 */
	function match() {
		$uri = parse_url($_SERVER['REQUEST_URI']);
		
		return ( $this->_map == $_SERVER['HTTP_HOST'] || $this->_map == $_SERVER['SERVER_NAME'] || $this->_map == $uri['host'] );
	}

}

/**
 * Chaining for rules and processing.
 *
 * I'm unsure how the chaining works in Zend Framework, so there are going to be incompatibilities
 * between the two, I'm sure. I don't believe that it matters much, someone using Zend Framework,
 * will know the difference by reading the below notes.
 *
 * The reason it does not matter how Zend Framework works in this respect is two folds. one is
 * copyright, which isn't much of an issue since ZF supports GPLv2 (unless they have increased that)
 * or at least New BSD, which is compatible with GPLv2, but also the fact that if code was copied
 * from the Zend Framework to maintain compatibility, then the copyright from Zend Framework would
 * have to be referenced as well. Rest assured that no code from Zend Framework has been used.
 *
 * The second is while specifications from the web site on how the Zend Framework Chain Route is
 * available, it isn't avaailble at the time of this implementation. The final, bonus reason, is
 * that if the Zend Framework doesn't work like this, then in my opinion it should.
 *
 * How the chaining works in this class is to process each of the routes and if all of the routes
 * are true, then the chain will return true. If any of the routes returns false, then the complete
 * route will return false.
 *
 * The reason this is useful, is for multi-site, where on one hostname, a special "module" is
 * implemented for a specific hostname. See example below:
 *
 * <code>
 * $chain = new WP_Route_Chain;
 * $chain->route( new WP_Route_Hostname('special.example.com') );
 * $chain->route( new WP_Route_Regex('/gallery/picture/(.*)') );
 * </code>
 *
 * In the above example, if the hostname is not 'special.example.com', then there is no reason to
 * check any further. This in some cases can be used to speed up routes where it is dependent on
 * another condition being true.
 *
 * Since the chain can be any route that extends the {@link WP_Routes} class, a custom route can be
 * written that checks different options in the database in order to dynamically customize the chain
 * based on options for the plugin or option or anything.
 *
 * @package WordPress
 * @subpackage Controller
 * @extends WP_Routes
 * @since {unknown}
 */
class WP_Route_Chain extends WP_Routes {

	/**
	 * PHP4 backwards compatible constructor calls PHP5 constructor.
	 *
	 * @since {unknown}
	 * @access public
	 * @see WP_Route_Chain::__construct()
	 *
	 * @param array $map Array of routes for processing.
	 * @param callback $callback Callback to run in the dispatcher.
	 */
	function WP_Route_Chain( $map, $callback ) {
		$this->__construct( $map, $callback );
	}

	/**
	 * Supports chaining routes for more advanced checks for whether controller should run.
	 *
	 * The callback also supports PHP5.3 closures as well.
	 *
	 * @since {unknown}
	 * @access public
	 *
	 * @param array $map Array of routes for processing.
	 * @param callback $callback Callback to run in the dispatcher.
	 */
	function __construct( $map, $callback ) {
		if ( is_array( $map ) ) {
			parent::__construct( $map, $callback );
		} else {
			$this->_map = array();
		}
	}

	/**
	 *
	 *
	 * @param WP_Routes $route Not by reference. Requires route that extends WP_Routes.
	 * @return WP_Route_Chain
	 */
	function add_route($route) {
		if ( is_a( $route, 'WP_Routes' ) ) {
			$this->_map[] = $route;
		}
		return $this;
	}

	/**
	 * Matches against specific functionality or processing in each class that implements method.
	 *
	 * This method must be implemented in each class, because this method will be used in the router
	 * to check whether the route matches and then run the dispatch.
	 *
	 * @access public
	 * @since {unknown}
	 *
	 * @return bool Whether route is successful.
	 */
	function match() {
		if ( ! is_array( $this->_map ) )
			return false;

		foreach ( $this->_map as $route ) {
			if ( ! $route->match() )
				return false;
		}

		// Assume that if any of the routes did not fail, that everything passed.
		return true;
	}

}

class WP_Uri_Paths {

	function WP_Controller_Uri() {
		$this->__construct();
	}

	function __construct() {
		$this->process();
	}

	/**
	 *
	 * @global WP_Rewrite $wp_rewrite
	 * @staticvar string $_uri_request Stores processed URI from either $_req_uri or $_pathinfo.
	 * @staticvar string $_req_uri Stores the processed URI from REQUEST_URI server variable.
	 * @staticvar string $_pathinfo Stores the processed URI from PATH_INFO server variable.
	 * @staticvar string $_self Stores the processed URI from PHP_SELF server variable.
	 * @param string $type (Optional) What path to return: either request, pathinfo, php_self, or processed_uri.
	 * @return string
	 */
	function process($type = null) {
		global $wp_rewrite;
		static $_uri_request, $_req_uri, $_pathinfo, $_self;

		if ( ! isset( $_uri_request ) ) {
			$pathinfo = '';
			if ( isset($_SERVER['PATH_INFO']) )
				$pathinfo = $_SERVER['PATH_INFO'];

			$pathinfo_array = explode('?', $pathinfo);
			$pathinfo = str_replace("%", "%25", $pathinfo_array[0]);
			$req_uri = $_SERVER['REQUEST_URI'];
			$req_uri_array = explode('?', $req_uri);
			$req_uri = $req_uri_array[0];

			// The home path is needed to strip out the base site URL and keep just the part of WordPress
			// for checking against.
			$home_path = parse_url( get_home_url() );

			if ( isset($home_path['path']) ) {
				$home_path = trim( $home_path['path'], '/');
			} else
				$home_path = '';

			// Trim path info from the end and the leading home path from the
			// front.  For path info requests, this leaves us with the requesting
			// filename, if any.  For 404 requests, this leaves us with the
			// requested permalink.
			$req_uri = str_replace($pathinfo, '', rawurldecode($req_uri));
			$req_uri = trim($req_uri, '/');
			$req_uri = preg_replace("|^$home_path|", '', $req_uri);
			$req_uri = rtrim($req_uri, '/');
			$_req_uri = $req_uri;
			$pathinfo = trim($pathinfo, '/');
			$pathinfo = preg_replace("|^$home_path|", '', $pathinfo);
			$pathinfo = rtrim($pathinfo, '/');
			$_pathinfo = $pathinfo;

			$self = $_SERVER['PHP_SELF'];
			$self = trim($self, '/');
			$self = preg_replace("|^$home_path|", '', $self);
			$self = trim($self, '/');
			$_self = $self;

			// The requested permalink is in $pathinfo for path info requests and
			//  $req_uri for other requests.
			if ( ! empty($pathinfo) && !preg_match('|^.*' . $wp_rewrite->index . '$|', $pathinfo) ) {
				$_uri_request = $pathinfo;
			} else {
				// If the request uri is the index, blank it out so that we don't try to match it against a rule.
				if ( $req_uri == $wp_rewrite->index )
					$req_uri = '';
				$_uri_request = $req_uri;
			}
		}

		switch ( $type ) {
			case 'req_uri':
			case 'request':
				return $_req_uri;
			case 'pathinfo':
				return $_pathinfo;
			case 'php_self':
				return $_self;
			case 'processed_uri':
			default:
				return $_uri_request;
		}
	}

	function request_uri() {
		return self::process('request');
	}

	function pathinfo() {
		return self::process('pathinfo');
	}

	function processed_uri() {
		return self::process('processed_uri');
	}

	function php_self() {
		return self::process('php_self');
	}
}

/**
 * Processes and stores the request URL for the routes matching.
 *
 * The code was taken from {@link WP::parse_request()} and thus should require minimal testing.
 *
 * @todo Remove dependency of WP_Rewrite class and global.
 * @since {unknown}
 * @global WP_Rewrite $wp_rewrite WP_Rewrite object container.
 * @staticvar string $_uri_request Stores the value for the processed request URL.
 *
 * @return string The processed URL request from either pathinfo or request_uri.
 */
function _get_uri_request() {
	$uri = new WP_Uri_Paths;
	return $uri->processed_uri();
}

/**
 * Retrieve the home url for a given site.
 *
 * Returns the 'home' option with the appropriate protocol,  'https' if is_ssl() and 'http'
 * otherwise. If $scheme is 'http' or 'https', is_ssl() is overridden.
 *
 * @package WordPress
 * @since 3.0.0
 *
 * @param  int $blog_id   (optional) Blog ID. Defaults to current blog.
 * @param  string $path   (optional) Path relative to the home url.
 * @param  string $scheme (optional) Scheme to give the home url context. Currently 'http','https'
 * @return string Home url link with optional path appended.
 */
function get_home_url( $blog_id = null, $path = '', $scheme = null ) {
	$orig_scheme = $scheme;

	if ( !in_array( $scheme, array( 'http', 'https' ) ) )
		$scheme = is_ssl() && !is_admin() ? 'https' : 'http';

	if ( empty( $blog_id ) || !is_multisite() )
		$home = get_option( 'home' );
	else
		$home = get_blog_option( $blog_id, 'home' );

	$url = str_replace( 'http://', "$scheme://", $home );

	if ( !empty( $path ) && is_string( $path ) && strpos( $path, '..' ) === false )
		$url .= '/' . ltrim( $path, '/' );

	return apply_filters( 'home_url', $url, $path, $orig_scheme, $blog_id );
}


/*
 * Tests for the Controller implementation.
 */

function __test_controller() {
	var_dump( _get_uri_request() );
}
add_action('init', '__test_controller');

// PHP close tag has been intentionally left out.