* @link http://www.pradosoft.com/
* @copyright Copyright © 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');
Prado::using('System.Web.UI.TAssetManager');
Prado::using('System.Web.UI.TPageStatePersister');
/**
* 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.
* A directory may be used to group together the pages serving for the similar goal.
* A directory may contain a configuration file config.xml 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 '/Users/Admin' is the directory
* containing the page 'Update'. Then the page can be requested via 'Users.Admin.Update'.
*
* 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 three modules:
* - {@link TTemplateManager} : manages page and control templates
* - {@link TThemeManager} : manages themes used in a Prado application
* - {@link TAssetManager} : manages assets 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 tag in
* each page directory configuration as follows,
*
*
*
*
*
* 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
* @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;
/**
* @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 TApplication application
*/
private $_application;
/**
* @var TAssetManager asset manager
*/
private $_assetManager=null;
/**
* @var TThemeManager theme manager
*/
private $_themeManager=null;
/**
* @var TTemplateManager template manager
*/
private $_templateManager=null;
/**
* @var IPageStatePersister page state persister
*/
private $_pageStatePersister=null;
/**
* Initializes the service.
* This method is required by IService interface and is invoked by application.
* @param TApplication application
* @param TXmlElement service configuration
*/
public function init($application,$config)
{
$application->setPageService($this);
$this->_application=$application;
if($this->_basePath===null)
{
$basePath=$application->getBasePath().'/'.self::DEFAULT_BASEPATH;
if(($this->_basePath=realpath($basePath))===false || !is_dir($this->_basePath))
throw new TConfigurationException('pageservice_basepath_invalid',$basePath);
}
$this->_pagePath=$application->getRequest()->getServiceParameter();
if(empty($this->_pagePath))
$this->_pagePath=$this->_defaultPage;
if(empty($this->_pagePath))
throw new THttpException(404,'pageservice_page_required');
if(($cache=$application->getCache())===null)
{
$pageConfig=new TPageConfiguration;
if($config!==null)
$pageConfig->loadXmlElement($config,$application->getBasePath(),null);
$pageConfig->loadConfigurationFiles($this->_pagePath,$this->_basePath);
}
else
{
$configCached=true;
$currentTimestamp=array();
$arr=$cache->get(self::CONFIG_CACHE_PREFIX.$this->_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('.',$this->_pagePath);
array_pop($paths);
$configPath=$this->_basePath;
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($this->_pagePath,$this->_basePath);
$cache->set(self::CONFIG_CACHE_PREFIX.$this->_pagePath,array($pageConfig,$currentTimestamp));
}
}
// set path aliases and using namespaces
foreach($pageConfig->getAliases() as $alias=>$path)
Prado::setPathOfAlias($alias,$path);
foreach($pageConfig->getUsings() as $using)
Prado::using($using);
$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
foreach($pageConfig->getModules() as $id=>$moduleConfig)
{
$module=Prado::createComponent($moduleConfig[0]);
$application->setModule($id,$module);
foreach($moduleConfig[1] as $name=>$value)
$module->setSubProperty($name,$value);
$module->init($this->_application,$moduleConfig[2]);
}
$application->getAuthorizationRules()->mergeWith($pageConfig->getRules());
$this->_initialized=true;
parent::init($application,$config);
}
/**
* @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($this->_application,null);
}
return $this->_templateManager;
}
/**
* @param TTemplateManager template manager
*/
public function setTemplateManager(TTemplateManager $value)
{
$this->_templateManager=$value;
}
/**
* @return TAssetManager asset manager
*/
public function getAssetManager()
{
if(!$this->_assetManager)
{
$this->_assetManager=new TAssetManager;
$this->_assetManager->init($this->_application,null);
}
return $this->_assetManager;
}
/**
* @param TAssetManager asset manager
*/
public function setAssetManager(TAssetManager $value)
{
$this->_assetManager=$value;
}
/**
* @return TThemeManager theme manager
*/
public function getThemeManager()
{
if(!$this->_themeManager)
{
$this->_themeManager=new TThemeManager;
$this->_themeManager->init($this->_application,null);
}
return $this->_themeManager;
}
/**
* @param TThemeManager theme manager
*/
public function setThemeManager(TThemeManager $value)
{
$this->_themeManager=$value;
}
/**
* @return IPageStatePersister page state persister
*/
public function getPageStatePersister()
{
if(!$this->_pageStatePersister)
{
$this->_pageStatePersister=new TPageStatePersister;
$this->_pageStatePersister->init($this->_application,null);
}
return $this->_pageStatePersister;
}
/**
* @param IPageStatePersister page state persister
*/
public function setPageStatePersister(IPageStatePersister $value)
{
$this->_pageStatePersister=$value;
}
/**
* @return string the requested page path
*/
public function getRequestedPagePath()
{
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
*/
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
*/
public function getBasePath()
{
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(($this->_basePath=realpath(Prado::getPathOfNamespace($value)))===false || !is_dir($this->_basePath))
throw new TConfigurationException('pageservice_basepath_invalid',$value);
}
/**
* 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()
{
$page=null;
$path=$this->_basePath.'/'.strtr($this->_pagePath,'.','/');
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->_properties['Template']=$this->getTemplateManager()->getTemplateByFileName($path.self::PAGE_FILE_EXT);
$this->_page=new $className($this->_properties);
}
else
throw new THttpException(404,'pageservice_page_unknown',$this->_pagePath);
$writer=$this->_application->getResponse()->createHtmlWriter();
$this->_page->run($writer);
$writer->flush();
}
/**
* 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->_application->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
* @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)
{
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');
if(($id=$properties->itemAt('id'))===null)
throw new TConfigurationException('pageserviceconf_module_invalid',$configPath);
if(isset($this->_modules[$id]))
{
if($type===null || $type===$this->_modules[$id][0])
{
$this->_modules[$id][1]=array_merge($this->_modules[$id][1],$properties->toArray());
$elements=$this->_modules[$id][2]->getElements();
foreach($node->getElements() as $element)
$elements->add($element);
}
else
{
$node->setParent(null);
$this->_modules[$id]=array($type,$properties->toArray(),$node);
}
}
else if($type===null)
throw new TConfigurationException('pageserviceconf_moduletype_required',$id,$configPath);
else
{
$node->setParent(null);
$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)
$this->_parameters[$id]=$node->getValue();
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());
}
}
}
}
}
?>