<?php

/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Controller
 * @subpackage Dispatcher
 * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id: Standard.php 24861 2012-06-01 23:40:13Z adamlundrigan $
 */
/** Zend_Loader */
require_once 'Zend/Loader.php';

/** Zend_Controller_Dispatcher_Abstract */
require_once 'Zend/Controller/Dispatcher/Abstract.php';

/**
 * @category   Zend
 * @package    Zend_Controller
 * @subpackage Dispatcher
 * @copyright  Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Controller_Dispatcher_Standard extends Zend_Controller_Dispatcher_Abstract {

	/**
	 * Current dispatchable directory
	 * @var string
	 */
	protected $_curDirectory;

	/**
	 * Current module (formatted)
	 * @var string
	 */
	protected $_curModule;

	/**
	 * Controller directory(ies)
	 * @var array
	 */
	protected $_controllerDirectory = array();

	/**
	 * Constructor: Set current module to default value
	 *
	 * @param  array $params
	 * @return void
	 */
	public function __construct(array $params = array()) {
		parent::__construct($params);
		$this->_curModule = $this->getDefaultModule();
	}

	/**
	 * Add a single path to the controller directory stack
	 *
	 * @param string $path
	 * @param string $module
	 * @return Zend_Controller_Dispatcher_Standard
	 */
	public function addControllerDirectory($path, $module = null) {
		if (null === $module) {
			$module = $this->_defaultModule;
		}

		$module = (string) $module;
		$path = rtrim((string) $path, '/\\');

		$this->_controllerDirectory[$module] = $path;
		return $this;
	}

	/**
	 * Set controller directory
	 *
	 * @param array|string $directory
	 * @return Zend_Controller_Dispatcher_Standard
	 */
	public function setControllerDirectory($directory, $module = null) {
		$this->_controllerDirectory = array();

		if (is_string($directory)) {
			$this->addControllerDirectory($directory, $module);
		}
		elseif (is_array($directory)) {
			foreach ((array) $directory as $module => $path) {
				$this->addControllerDirectory($path, $module);
			}
		}
		else {
			require_once 'Zend/Controller/Exception.php';
			throw new Zend_Controller_Exception('Controller directory spec must be either a string or an array');
		}

		return $this;
	}

	/**
	 * Return the currently set directories for Zend_Controller_Action class
	 * lookup
	 *
	 * If a module is specified, returns just that directory.
	 *
	 * @param  string $module Module name
	 * @return array|string Returns array of all directories by default, single
	 * module directory if module argument provided
	 */
	public function getControllerDirectory($module = null) {
		if (null === $module) {
			return $this->_controllerDirectory;
		}

		$module = (string) $module;
		if (array_key_exists($module, $this->_controllerDirectory)) {
			return $this->_controllerDirectory[$module];
		}

		return null;
	}

	/**
	 * Remove a controller directory by module name
	 *
	 * @param  string $module
	 * @return bool
	 */
	public function removeControllerDirectory($module) {
		$module = (string) $module;
		if (array_key_exists($module, $this->_controllerDirectory)) {
			unset($this->_controllerDirectory[$module]);
			return true;
		}
		return false;
	}

	/**
	 * Format the module name.
	 *
	 * @param string $unformatted
	 * @return string
	 */
	public function formatModuleName($unformatted) {
		if (($this->_defaultModule == $unformatted) && !$this->getParam('prefixDefaultModule')) {
			return $unformatted;
		}

		return ucfirst($this->_formatName($unformatted));
	}

	/**
	 * Format action class name
	 *
	 * @param string $moduleName Name of the current module
	 * @param string $className Name of the action class
	 * @return string Formatted class name
	 */
	public function formatClassName($moduleName, $className) {
		return $this->formatModuleName($moduleName) . '_Controller_' . $className;
	}

	/**
	 * Convert a class name to a filename
	 *
	 * @param string $class
	 * @return string
	 */
	public function classToFilename($class) {
		return str_replace('_', DIRECTORY_SEPARATOR, $class) . '.php';
	}

	/**
	 * Returns TRUE if the Zend_Controller_Request_Abstract object can be
	 * dispatched to a controller.
	 *
	 * Use this method wisely. By default, the dispatcher will fall back to the
	 * default controller (either in the module specified or the global default)
	 * if a given controller does not exist. This method returning false does
	 * not necessarily indicate the dispatcher will not still dispatch the call.
	 *
	 * @param Zend_Controller_Request_Abstract $action
	 * @return boolean
	 */
	public function isDispatchable(Zend_Controller_Request_Abstract $request) {
		$className = $this->getControllerClass($request);
		if (!$className) {
			return false;
		}

		$finalClass = $className;
		if (($this->_defaultModule != $this->_curModule) || $this->getParam('prefixDefaultModule')) {
			$finalClass = $this->formatClassName($this->_curModule, $className);
		}
		if (class_exists($finalClass)) {
			return true;
		}

		$fileSpec = $this->classToFilename($className);
		$dispatchDir = $this->getDispatchDirectory();
		$test = $dispatchDir . DIRECTORY_SEPARATOR . $fileSpec;
		return Zend_Loader::isReadable($test);
	}

	/**
	 * Dispatch to a controller/action
	 *
	 * By default, if a controller is not dispatchable, dispatch() will throw
	 * an exception. If you wish to use the default controller instead, set the
	 * param 'useDefaultControllerAlways' via {@link setParam()}.
	 *
	 * @param Zend_Controller_Request_Abstract $request
	 * @param Zend_Controller_Response_Abstract $response
	 * @return void
	 * @throws Zend_Controller_Dispatcher_Exception
	 */
	public function dispatch(Zend_Controller_Request_Abstract $request, Zend_Controller_Response_Abstract $response) {
		$this->setResponse($response);

		/**
		 * Get controller class
		 */
		if (!$this->isDispatchable($request)) {
			$controller = $request->getControllerName();
			if (!$this->getParam('useDefaultControllerAlways') && !empty($controller)) {
				require_once 'Zend/Controller/Dispatcher/Exception.php';
				throw new Zend_Controller_Dispatcher_Exception('Invalid controller specified (' . $request->getControllerName() . ')');
			}

			$className = $this->getDefaultControllerClass($request);
		}
		else {
			$className = $this->getControllerClass($request);
			if (!$className) {
				$className = $this->getDefaultControllerClass($request);
			}
		}

		/**
		 * If we're in a module or prefixDefaultModule is on, we must add the module name
		 * prefix to the contents of $className, as getControllerClass does not do that automatically.
		 * We must keep a separate variable because modules are not strictly PSR-0: We need the no-module-prefix
		 * class name to do the class->file mapping, but the full class name to insantiate the controller
		 */
		$moduleClassName = $className;
		if (($this->_defaultModule != $this->_curModule) || $this->getParam('prefixDefaultModule')) {
			$moduleClassName = $this->formatClassName($this->_curModule, $className);
		}

		/**
		 * Load the controller class file
		 */
		$className = $this->loadClass($className);

		/**
		 * Instantiate controller with request, response, and invocation
		 * arguments; throw exception if it's not an action controller
		 */
		$controller = new $moduleClassName($request, $this->getResponse(), $this->getParams());
		if (!($controller instanceof Zend_Controller_Action_Interface) &&
			!($controller instanceof Zend_Controller_Action)) {
			require_once 'Zend/Controller/Dispatcher/Exception.php';
			throw new Zend_Controller_Dispatcher_Exception(
			'Controller "' . $moduleClassName . '" is not an instance of Zend_Controller_Action_Interface'
			);
		}

		/**
		 * Retrieve the action name
		 */
		$action = $this->getActionMethod($request);

		/**
		 * Dispatch the method call
		 */
		$request->setDispatched(true);

		// by default, buffer output
		$disableOb = $this->getParam('disableOutputBuffering');
		$obLevel = ob_get_level();
		if (empty($disableOb)) {
			ob_start();
		}

		try {
			$controller->dispatch($action);
		}
		catch (Exception $e) {
			// Clean output buffer on error
			$curObLevel = ob_get_level();
			if ($curObLevel > $obLevel) {
				do {
					ob_get_clean();
					$curObLevel = ob_get_level();
				}
				while ($curObLevel > $obLevel);
			}
			throw $e;
		}

		if (empty($disableOb)) {
			$content = ob_get_clean();
			$response->appendBody($content);
		}

		// Destroy the page controller instance and reflection objects
		$controller = null;
	}

	/**
	 * Load a controller class
	 *
	 * Attempts to load the controller class file from
	 * {@link getControllerDirectory()}.  If the controller belongs to a
	 * module, looks for the module prefix to the controller class.
	 *
	 * @param string $className
	 * @return string Class name loaded
	 * @throws Zend_Controller_Dispatcher_Exception if class not loaded
	 */
	public function loadClass($className) {
		$finalClass = $className;
		if (($this->_defaultModule != $this->_curModule) || $this->getParam('prefixDefaultModule')) {
			$finalClass = $this->formatClassName($this->_curModule, $className);
		}
		if (class_exists($finalClass, false)) {
			return $finalClass;
		}

		$dispatchDir = $this->getDispatchDirectory();
		$loadFile = $dispatchDir . DIRECTORY_SEPARATOR . $this->classToFilename($className);

		if (Zend_Loader::isReadable($loadFile)) {
			include_once $loadFile;
		}
		else {
			require_once 'Zend/Controller/Dispatcher/Exception.php';
			throw new Zend_Controller_Dispatcher_Exception('Cannot load controller class "' . $className . '" from file "' . $loadFile . "'");
		}

		if (!class_exists($finalClass, false)) {
			require_once 'Zend/Controller/Dispatcher/Exception.php';
			throw new Zend_Controller_Dispatcher_Exception('Invalid controller class ("' . $finalClass . '")');
		}

		return $finalClass;
	}

	/**
	 * Get controller class name
	 *
	 * Try request first; if not found, try pulling from request parameter;
	 * if still not found, fallback to default
	 *
	 * @param Zend_Controller_Request_Abstract $request
	 * @return string|false Returns class name on success
	 */
	public function getControllerClass(Zend_Controller_Request_Abstract $request) {
		$controllerName = $request->getControllerName();
		if (empty($controllerName)) {
			if (!$this->getParam('useDefaultControllerAlways')) {
				return false;
			}
			$controllerName = $this->getDefaultControllerName();
			$request->setControllerName($controllerName);
		}

		$className = $this->formatControllerName($controllerName);

		$controllerDirs = $this->getControllerDirectory();
		$module = $request->getModuleName();
		if ($this->isValidModule($module)) {
			$this->_curModule = $module;
			$this->_curDirectory = $controllerDirs[$module];
		}
		elseif ($this->isValidModule($this->_defaultModule)) {
			$request->setModuleName($this->_defaultModule);
			$this->_curModule = $this->_defaultModule;
			$this->_curDirectory = $controllerDirs[$this->_defaultModule];
		}
		else {
			require_once 'Zend/Controller/Exception.php';
			throw new Zend_Controller_Exception('No default module defined for this application');
		}

		return $className;
	}

	/**
	 * Determine if a given module is valid
	 *
	 * @param  string $module
	 * @return bool
	 */
	public function isValidModule($module) {
		if (!is_string($module)) {
			return false;
		}

		$module = strtolower($module);
		$controllerDir = $this->getControllerDirectory();
		foreach (array_keys($controllerDir) as $moduleName) {
			if ($module == strtolower($moduleName)) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Retrieve default controller class
	 *
	 * Determines whether the default controller to use lies within the
	 * requested module, or if the global default should be used.
	 *
	 * By default, will only use the module default unless that controller does
	 * not exist; if this is the case, it falls back to the default controller
	 * in the default module.
	 *
	 * @param Zend_Controller_Request_Abstract $request
	 * @return string
	 */
	public function getDefaultControllerClass(Zend_Controller_Request_Abstract $request) {
		$controller = $this->getDefaultControllerName();
		$default = $this->formatControllerName($controller);
		$request->setControllerName($controller)
			->setActionName(null);

		$module = $request->getModuleName();
		$controllerDirs = $this->getControllerDirectory();
		$this->_curModule = $this->_defaultModule;
		$this->_curDirectory = $controllerDirs[$this->_defaultModule];
		if ($this->isValidModule($module)) {
			$found = false;
			if (class_exists($default, false)) {
				$found = true;
			}
			else {
				$moduleDir = $controllerDirs[$module];
				$fileSpec = $moduleDir . DIRECTORY_SEPARATOR . $this->classToFilename($default);
				if (Zend_Loader::isReadable($fileSpec)) {
					$found = true;
					$this->_curDirectory = $moduleDir;
				}
			}
			if ($found) {
				$request->setModuleName($module);
				$this->_curModule = $this->formatModuleName($module);
			}
		}
		else {
			$request->setModuleName($this->_defaultModule);
		}

		return $default;
	}

	/**
	 * Return the value of the currently selected dispatch directory (as set by
	 * {@link getController()})
	 *
	 * @return string
	 */
	public function getDispatchDirectory() {
		return $this->_curDirectory;
	}

	/**
	 * Determine the action name
	 *
	 * First attempt to retrieve from request; then from request params
	 * using action key; default to default action
	 *
	 * Returns formatted action name
	 *
	 * @param Zend_Controller_Request_Abstract $request
	 * @return string
	 */
	public function getActionMethod(Zend_Controller_Request_Abstract $request) {
		$action = $request->getActionName();
		if (empty($action)) {
			$action = $this->getDefaultAction();
			$request->setActionName($action);
		}

		return $this->formatActionName($action);
	}

}
