<?php
/**
 * TPageService class file.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.pradosoft.com/
 * @copyright Copyright &copy; 2005 PradoSoft
 * @license http://www.pradosoft.com/license/
 * @version $Revision: $  $Date: $
 * @package System.Web.Services
 */

/**
 * Include classes to be used by page service
 */
Prado::using('System.Web.UI.TPage');
Prado::using('System.Web.UI.TTemplateManager');
Prado::using('System.Web.UI.TThemeManager');

/**
 * TPageService class.
 *
 * TPageService implements the service for serving user page requests.
 *
 * Pages that are available to client users are stored under a directory specified by
 * {@link setBasePath BasePath}. The directory may contain subdirectories.
 * Pages serving for a similar goal are usually placed under the same directory.
 * A directory may contain a configuration file <b>config.xml</b> whose content
 * is similar to that of application configuration file.
 *
 * A page is requested via page path, which is a dot-connected directory names
 * appended by the page name. Assume '<BasePath>/Users/Admin' is the directory
 * containing the page 'Update'. Then the page can be requested via 'Users.Admin.Update'.
 * By default, the {@link setBasePath BasePath} of the page service is the "pages"
 * directory under the application base path. You may change this default
 * by setting {@link setBasePath BasePath} with a different path you prefer.
 *
 * Page name refers to the file name (without extension) of the page template.
 * In order to differentiate from the common control template files, the extension
 * name of the page template files must be '.page'. If there is a PHP file with
 * the same page name under the same directory as the template file, that file
 * will be considered as the page class file and the file name is the page class name.
 * If such a file is not found, the page class is assumed as {@link TPage}.
 *
 * Modules can be configured and loaded in page directory configurations.
 * Configuration of a module in a subdirectory will overwrite its parent
 * directory's configuration, if both configurations refer to the same module.
 *
 * By default, TPageService will automatically load two modules:
 * - {@link TTemplateManager} : manages page and control templates
 * - {@link TThemeManager} : manages themes used in a Prado application
 *
 * In page directory configurations, static authorization rules can also be specified,
 * which governs who and which roles can access particular pages.
 * Refer to {@link TAuthorizationRule} for more details about authorization rules.
 * Page authorization rules can be configured within an <authorization> tag in
 * each page directory configuration as follows,
 * <authorization>
 *   <deny pages="Update" users="?" />
 *   <allow pages="Admin" roles="administrator" />
 *   <deny pages="Admin" users="*" />
 * </authorization>
 * where the 'pages' attribute may be filled with a sequence of comma-separated
 * page IDs. If 'pages' attribute does not appear in a rule, the rule will be
 * applied to all pages in this directory and all subdirectories (recursively).
 * Application of authorization rules are in a bottom-up fashion, starting from
 * the directory containing the requested page up to all parent directories.
 * The first matching rule will be used. The last rule always allows all users
 * accessing to any resources.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Revision: $  $Date: $
 * @package System.Services
 * @since 3.0
 */
class TPageService extends TService
{
	/**
	 * Configuration file name
	 */
	const CONFIG_FILE='config.xml';
	/**
	 * Default base path
	 */
	const DEFAULT_BASEPATH='pages';
	/**
	 * Prefix of ID used for storing parsed configuration in cache
	 */
	const CONFIG_CACHE_PREFIX='prado:pageservice:';
	/**
	 * Page template file extension
	 */
	const PAGE_FILE_EXT='.page';
	/**
	 * @var string id of this service (page)
	 */
	private $_id='page';
	/**
	 * @var string root path of pages
	 */
	private $_basePath=null;
	/**
	 * @var string default page
	 */
	private $_defaultPage='Home';
	/**
	 * @var string requested page (path)
	 */
	private $_pagePath=null;
	/**
	 * @var TPage the requested page
	 */
	private $_page=null;
	/**
	 * @var array list of initial page property values
	 */
	private $_properties;
	/**
	 * @var boolean whether service is initialized
	 */
	private $_initialized=false;
	/**
	 * @var TThemeManager theme manager
	 */
	private $_themeManager=null;
	/**
	 * @var TTemplateManager template manager
	 */
	private $_templateManager=null;

	/**
	 * Initializes the service.
	 * This method is required by IService interface and is invoked by application.
	 * @param TXmlElement service configuration
	 */
	public function init($config)
	{
		Prado::trace("Initializing TPageService",'System.Web.Services.TPageService');

		$this->getApplication()->setPageService($this);

		$pageConfig=$this->loadPageConfig($this->getRequestedPagePath(),$config);

		$this->initPageContext($pageConfig);

		$this->_initialized=true;
	}

	/**
	 * Initializes page context.
	 * Page context includes path alias settings, namespace usages,
	 * parameter initialization, module loadings, page initial properties
	 * and authorization rules.
	 * @param TPageConfiguration
	 */
	protected function initPageContext($pageConfig)
	{
		$application=$this->getApplication();

		// set path aliases and using namespaces
		foreach($pageConfig->getAliases() as $alias=>$path)
			Prado::setPathOfAlias($alias,$path);
		foreach($pageConfig->getUsings() as $using)
			Prado::using($using);

		// initial page properties (to be set when page runs)
		$this->_properties=$pageConfig->getProperties();

		// load parameters
		$parameters=$application->getParameters();
		foreach($pageConfig->getParameters() as $id=>$parameter)
		{
			if(is_string($parameter))
				$parameters->add($id,$parameter);
			else
			{
				$component=Prado::createComponent($parameter[0]);
				foreach($parameter[1] as $name=>$value)
					$component->setSubProperty($name,$value);
				$parameters->add($id,$component);
			}
		}

		// load modules specified in page directory config
		$modules=array();
		foreach($pageConfig->getModules() as $id=>$moduleConfig)
		{
			Prado::trace("Loading module $id ({$moduleConfig[0]})",'System.Web.Services.TPageService');
			$module=Prado::createComponent($moduleConfig[0]);
			if(is_string($id))
				$application->setModule($id,$module);
			foreach($moduleConfig[1] as $name=>$value)
				$module->setSubProperty($name,$value);
			$modules[]=array($module,$moduleConfig[2]);
		}
		foreach($modules as $module)
			$module[0]->init($module[1]);

		$application->getAuthorizationRules()->mergeWith($pageConfig->getRules());
	}

	/**
	 * Determines the requested page path.
	 * @return string page path requested
	 */
	protected function determineRequestedPagePath()
	{
		$pagePath=$this->getRequest()->getServiceParameter();
		if(empty($pagePath))
			$pagePath=$this->getDefaultPage();
		return $pagePath;
	}

	/**
	 * Collects configuration for a page.
	 * @param string page path in the format of Path.To.Page
	 * @param TXmlElement additional configuration
	 * @return TPageConfiguration
	 */
	protected function loadPageConfig($pagePath,$config=null)
	{
		$application=$this->getApplication();
		if(($cache=$application->getCache())===null)
		{
			$pageConfig=new TPageConfiguration;
			if($config!==null)
				$pageConfig->loadXmlElement($config,$application->getBasePath(),null);
			$pageConfig->loadConfigurationFiles($pagePath,$this->getBasePath());
		}
		else
		{
			$configCached=true;
			$currentTimestamp=array();
			$arr=$cache->get(self::CONFIG_CACHE_PREFIX.$pagePath);
			if(is_array($arr))
			{
				list($pageConfig,$timestamps)=$arr;
				if($application->getMode()!==TApplication::STATE_PERFORMANCE)
				{
					foreach($timestamps as $fileName=>$timestamp)
					{
						if($fileName===0) // application config file
						{
							$appConfigFile=$application->getConfigurationFile();
							$currentTimestamp[0]=$appConfigFile===null?0:@filemtime($appConfigFile);
							if($currentTimestamp[0]>$timestamp || ($timestamp>0 && !$currentTimestamp[0]))
								$configCached=false;
						}
						else
						{
							$currentTimestamp[$fileName]=@filemtime($fileName);
							if($currentTimestamp[$fileName]>$timestamp || ($timestamp>0 && !$currentTimestamp[$fileName]))
								$configCached=false;
						}
					}
				}
			}
			else
			{
				$configCached=false;
				$paths=explode('.',$pagePath);
				array_pop($paths);
				$configPath=$this->getBasePath();
				foreach($paths as $path)
				{
					$configFile=$configPath.'/'.self::CONFIG_FILE;
					$currentTimestamp[$configFile]=@filemtime($configFile);
					$configPath.='/'.$path;
				}
				$appConfigFile=$application->getConfigurationFile();
				$currentTimestamp[0]=$appConfigFile===null?0:@filemtime($appConfigFile);
			}
			if(!$configCached)
			{
				$pageConfig=new TPageConfiguration;
				if($config!==null)
					$pageConfig->loadXmlElement($config,$application->getBasePath(),null);
				$pageConfig->loadConfigurationFiles($pagePath,$this->getBasePath());
				$cache->set(self::CONFIG_CACHE_PREFIX.$pagePath,array($pageConfig,$currentTimestamp));
			}
		}
		return $pageConfig;
	}

	/**
	 * @return string id of this module
	 */
	public function getID()
	{
		return $this->_id;
	}

	/**
	 * @param string id of this module
	 */
	public function setID($value)
	{
		$this->_id=$value;
	}

	/**
	 * @return TTemplateManager template manager
	 */
	public function getTemplateManager()
	{
		if(!$this->_templateManager)
		{
			$this->_templateManager=new TTemplateManager;
			$this->_templateManager->init(null);
		}
		return $this->_templateManager;
	}

	/**
	 * @param TTemplateManager template manager
	 */
	public function setTemplateManager(TTemplateManager $value)
	{
		$this->_templateManager=$value;
	}

	/**
	 * @return TThemeManager theme manager
	 */
	public function getThemeManager()
	{
		if(!$this->_themeManager)
		{
			$this->_themeManager=new TThemeManager;
			$this->_themeManager->init(null);
		}
		return $this->_themeManager;
	}

	/**
	 * @param TThemeManager theme manager
	 */
	public function setThemeManager(TThemeManager $value)
	{
		$this->_themeManager=$value;
	}

	/**
	 * @return string the requested page path
	 */
	public function getRequestedPagePath()
	{
		if($this->_pagePath===null)
		{
			$this->_pagePath=$this->determineRequestedPagePath();
			if(empty($this->_pagePath))
				throw new THttpException(404,'pageservice_page_required');
		}
		return $this->_pagePath;
	}

	/**
	 * @return TPage the requested page
	 */
	public function getRequestedPage()
	{
		return $this->_page;
	}

	/**
	 * @return string default page path to be served if no explicit page is request. Defaults to 'Home'.
	 */
	public function getDefaultPage()
	{
		return $this->_defaultPage;
	}

	/**
	 * @param string default page path to be served if no explicit page is request
	 * @throws TInvalidOperationException if the page service is initialized
	 */
	public function setDefaultPage($value)
	{
		if($this->_initialized)
			throw new TInvalidOperationException('pageservice_defaultpage_unchangeable');
		else
			$this->_defaultPage=$value;
	}

	/**
	 * @return string root directory (in namespace form) storing pages. Defaults to 'pages' directory under application base path.
	 */
	public function getBasePath()
	{
		if($this->_basePath===null)
		{
			$basePath=$this->getApplication()->getBasePath().'/'.self::DEFAULT_BASEPATH;
			if(($this->_basePath=realpath($basePath))===false || !is_dir($this->_basePath))
				throw new TConfigurationException('pageservice_basepath_invalid',$basePath);
		}
		return $this->_basePath;
	}

	/**
	 * @param string root directory (in namespace form) storing pages
	 * @throws TInvalidOperationException if the service is initialized already or basepath is invalid
	 */
	public function setBasePath($value)
	{
		if($this->_initialized)
			throw new TInvalidOperationException('pageservice_basepath_unchangeable');
		else if(($path=Prado::getPathOfNamespace($value))===null || !is_dir($path))
			throw new TConfigurationException('pageservice_basepath_invalid',$value);
		$this->_basePath=realpath($path);
	}

	/**
	 * Runs the service.
	 * This will create the requested page, initializes it with the property values
	 * specified in the configuration, and executes the page.
	 */
	public function run()
	{
		Prado::trace("Running page service",'System.Web.Services.TPageService');
		$page=null;
		$path=$this->getBasePath().'/'.strtr($this->getRequestedPagePath(),'.','/');
		if(is_file($path.self::PAGE_FILE_EXT))
		{
			if(is_file($path.Prado::CLASS_FILE_EXT))
			{
				$className=basename($path);
				if(!class_exists($className,false))
					include_once($path.Prado::CLASS_FILE_EXT);
				if(!class_exists($className,false))
					throw new TConfigurationException('pageservice_pageclass_unknown',$className);
			}
			else
				$className='TPage';

			$this->_page=new $className();

			$this->_page->setPagePath($this->getRequestedPagePath());
			// initialize page properties with those set in configurations
			foreach($this->_properties as $name=>$value)
				$this->_page->setSubProperty($name,$value);

			// set page template
			$this->_page->setTemplate($this->getTemplateManager()->getTemplateByFileName($path.self::PAGE_FILE_EXT));
		}
		else
			throw new THttpException(404,'pageservice_page_unknown',$this->getRequestedPagePath());

		$this->_page->run($this->getResponse()->createHtmlWriter());
	}

	/**
	 * Constructs a URL with specified page path and GET parameters.
	 * @param string page path
	 * @param array list of GET parameters, null if no GET parameters required
	 * @param boolean whether to encode the ampersand in URL, defaults to false.
	 * @return string URL for the page and GET parameters
	 */
	public function constructUrl($pagePath,$getParams=null,$encodeAmpersand=false)
	{
		return $this->getRequest()->constructUrl($this->_id,$pagePath,$getParams,$encodeAmpersand);
	}
}


/**
 * TPageConfiguration class
 *
 * TPageConfiguration represents the configuration for a page.
 * The page is specified by a dot-connected path.
 * Configurations along this path are merged together to be provided for the page.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Revision: $  $Date: $
 * @package System.Services
 * @since 3.0
 */
class TPageConfiguration extends TComponent
{
	/**
	 * @var array list of page initial property values
	 */
	private $_properties=array();
	/**
	 * @var array list of namespaces to be used
	 */
	private $_usings=array();
	/**
	 * @var array list of path aliases
	 */
	private $_aliases=array();
	/**
	 * @var array list of module configurations
	 */
	private $_modules=array();
	/**
	 * @var array list of parameters
	 */
	private $_parameters=array();
	/**
	 * @var TAuthorizationRuleCollection list of authorization rules
	 */
	private $_rules=array();

	/**
	 * Returns list of page initial property values.
	 * Each array element represents a single property with the key
	 * being the property name and the value the initial property value.
	 * @return array list of page initial property values
	 */
	public function getProperties()
	{
		return $this->_properties;
	}

	/**
	 * Returns list of path alias definitions.
	 * The definitions are aggregated (top-down) from configuration files along the path
	 * to the specified page. Each array element represents a single alias definition,
	 * with the key being the alias name and the value the absolute path.
	 * @return array list of path alias definitions
	 */
	public function getAliases()
	{
		return $this->_aliases;
	}

	/**
	 * Returns list of namespaces to be used.
	 * The namespaces are aggregated (top-down) from configuration files along the path
	 * to the specified page. Each array element represents a single namespace usage,
	 * with the value being the namespace to be used.
	 * @return array list of namespaces to be used
	 */
	public function getUsings()
	{
		return $this->_usings;
	}

	/**
	 * Returns list of module configurations.
	 * The module configurations are aggregated (top-down) from configuration files
	 * along the path to the specified page. Each array element represents
	 * a single module configuration, with the key being the module ID and
	 * the value the module configuration. Each module configuration is
	 * stored in terms of an array with the following content
	 * ([0]=>module type, [1]=>module properties, [2]=>complete module configuration)
	 * The module properties are an array of property values indexed by property names.
	 * The complete module configuration is a TXmlElement object representing
	 * the raw module configuration which may contain contents enclosed within
	 * module tags.
	 * @return array list of module configurations to be used
	 */
	public function getModules()
	{
		return $this->_modules;
	}

	/**
	 * Returns list of parameter definitions.
	 * The parameter definitions are aggregated (top-down) from configuration files
	 * along the path to the specified page. Each array element represents
	 * a single parameter definition, with the key being the parameter ID and
	 * the value the parameter definition. A parameter definition can be either
	 * a string representing a string-typed parameter, or an array.
	 * The latter defines a component-typed parameter whose format is as follows,
	 * ([0]=>component type, [1]=>component properties)
	 * The component properties are an array of property values indexed by property names.
	 * @return array list of parameter definitions to be used
	 */
	public function getParameters()
	{
		return $this->_parameters;
	}

	/**
	 * Returns list of authorization rules.
	 * The authorization rules are aggregated (bottom-up) from configuration files
	 * along the path to the specified page.
	 * @return TAuthorizationRuleCollection collection of authorization rules
	 */
	public function getRules()
	{
		return $this->_rules;
	}

	/**
	 * Loads configuration for a page specified in a path format.
	 * @param string path to the page (dot-connected format)
	 * @param string root path for pages
	 */
	public function loadConfigurationFiles($pagePath,$basePath)
	{
		$paths=explode('.',$pagePath);
		$page=array_pop($paths);
		$path=$basePath;
		foreach($paths as $p)
		{
			$this->loadFromFile($path.'/'.TPageService::CONFIG_FILE,null);
			$path.='/'.$p;
		}
		$this->loadFromFile($path.'/'.TPageService::CONFIG_FILE,$page);
		$this->_rules=new TAuthorizationRuleCollection($this->_rules);
	}

	/**
	 * Loads a specific config file.
	 * @param string config file name
	 * @param string page name, null if page is not required
	 */
	private function loadFromFile($fname,$page)
	{
		Prado::trace("Loading $page with file $fname",'System.Web.Services.TPageService');
		if(empty($fname) || !is_file($fname))
			return;
		$dom=new TXmlDocument;
		if($dom->loadFromFile($fname))
			$this->loadXmlElement($dom,dirname($fname),$page);
		else
			throw new TConfigurationException('pageserviceconf_file_invalid',$fname);
	}

	/**
	 * Loads a specific configuration xml element.
	 * @param TXmlElement config xml element
	 * @param string base path corresponding to this xml element
	 * @param string page name, null if page is not required
	 */
	public function loadXmlElement($dom,$configPath,$page)
	{
		// paths
		if(($pathsNode=$dom->getElementByTagName('paths'))!==null)
		{
			foreach($pathsNode->getElementsByTagName('alias') as $aliasNode)
			{
				if(($id=$aliasNode->getAttribute('id'))!==null && ($p=$aliasNode->getAttribute('path'))!==null)
				{
					$p=str_replace('\\','/',$p);
					$path=realpath(preg_match('/^\\/|.:\\//',$p)?$p:$configPath.'/'.$p);
					if($path===false || !is_dir($path))
						throw new TConfigurationException('pageserviceconf_aliaspath_invalid',$id,$p,$configPath);
					if(isset($this->_aliases[$id]))
						throw new TConfigurationException('pageserviceconf_alias_redefined',$id,$configPath);
					$this->_aliases[$id]=$path;
				}
				else
					throw new TConfigurationException('pageserviceconf_alias_invalid',$configPath);
			}
			foreach($pathsNode->getElementsByTagName('using') as $usingNode)
			{
				if(($namespace=$usingNode->getAttribute('namespace'))!==null)
					$this->_usings[]=$namespace;
				else
					throw new TConfigurationException('pageserviceconf_using_invalid',$configPath);
			}
		}

		// modules
		if(($modulesNode=$dom->getElementByTagName('modules'))!==null)
		{
			foreach($modulesNode->getElementsByTagName('module') as $node)
			{
				$properties=$node->getAttributes();
				$type=$properties->remove('class');
				$id=$properties->itemAt('id');
				if($type===null)
					throw new TConfigurationException('pageserviceconf_moduletype_required',$id,$configPath);
				$node->setParent(null);
				if($id===null)
					$this->_modules[]=array($type,$properties->toArray(),$node);
				else
					$this->_modules[$id]=array($type,$properties->toArray(),$node);
			}
		}

		// parameters
		if(($parametersNode=$dom->getElementByTagName('parameters'))!==null)
		{
			foreach($parametersNode->getElementsByTagName('parameter') as $node)
			{
				$properties=$node->getAttributes();
				if(($id=$properties->remove('id'))===null)
					throw new TConfigurationException('pageserviceconf_parameter_invalid',$configPath);
				if(($type=$properties->remove('class'))===null)
				{
					if(($value=$properties->remove('value'))===null)
						$this->_parameters[$id]=$node;
					else
						$this->_parameters[$id]=$value;
				}
				else
					$this->_parameters[$id]=array($type,$properties->toArray());
			}
		}

		// authorization
		if(($authorizationNode=$dom->getElementByTagName('authorization'))!==null)
		{
			$rules=array();
			foreach($authorizationNode->getElements() as $node)
			{
				$pages=$node->getAttribute('pages');
				$ruleApplies=false;
				if(empty($pages))
					$ruleApplies=true;
				else if($page!==null)
				{
					$ps=explode(',',$pages);
					foreach($ps as $p)
					{
						if($page===trim($p))
						{
							$ruleApplies=true;
							break;
						}
					}
				}
				if($ruleApplies)
					$rules[]=new TAuthorizationRule($node->getTagName(),$node->getAttribute('users'),$node->getAttribute('roles'),$node->getAttribute('verb'));
			}
			$this->_rules=array_merge($rules,$this->_rules);
		}

		// pages
		if(($pagesNode=$dom->getElementByTagName('pages'))!==null)
		{
			$this->_properties=array_merge($this->_properties,$pagesNode->getAttributes()->toArray());
			if($page!==null)   // at the page folder
			{
				foreach($pagesNode->getElementsByTagName('page') as $node)
				{
					$properties=$node->getAttributes();
					if(($id=$properties->itemAt('id'))===null)
						throw new TConfigurationException('pageserviceconf_page_invalid',$configPath);
					if($id===$page)
						$this->_properties=array_merge($this->_properties,$properties->toArray());
				}
			}
		}
	}
}

?>