<?php /** * TPageService class file. * * @author Qiang Xue <qiang.xue@gmail.com> * @link http://www.pradosoft.com/ * @copyright Copyright © 2005-2015 PradoSoft * @license http://www.pradosoft.com/license/ * @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> * @author Carl G. Mathisen <carlgmathisen@gmail.com> * @package System.Web.Services * @since 3.0 */ class TPageService extends TService { /** * Configuration file name */ const CONFIG_FILE_XML='config.xml'; /** * Configuration file name */ const CONFIG_FILE_PHP='config.php'; /** * Default base path */ const DEFAULT_BASEPATH='Pages'; /** * Fallback base path - used to be the default up to Prado < 3.2 */ const FALLBACK_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 root path of pages */ private $_basePath=null; /** * @var string base path class in namespace format */ private $_basePageClass='TPage'; /** * @var string clientscript manager class in namespace format * @since 3.1.7 */ private $_clientScriptManagerClass='System.Web.UI.TClientScriptManager'; /** * @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=array(); /** * @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'); $pageConfig=$this->loadPageConfig($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(); foreach($pageConfig->getApplicationConfigurations() as $appConfig) $application->applyConfiguration($appConfig); $this->applyConfiguration($pageConfig); } /** * Applies a page configuration. * @param TPageConfiguration the configuration */ protected function applyConfiguration($config) { // initial page properties (to be set when page runs) $this->_properties=array_merge($this->_properties, $config->getProperties()); $this->getApplication()->getAuthorizationRules()->mergeWith($config->getRules()); $pagePath=$this->getRequestedPagePath(); // external configurations foreach($config->getExternalConfigurations() as $filePath=>$params) { list($configPagePath,$condition)=$params; if($condition!==true) $condition=$this->evaluateExpression($condition); if($condition) { if(($path=Prado::getPathOfNamespace($filePath,Prado::getApplication()->getConfigurationFileExt()))===null || !is_file($path)) throw new TConfigurationException('pageservice_includefile_invalid',$filePath); $c=new TPageConfiguration($pagePath); $c->loadFromFile($path,$configPagePath); $this->applyConfiguration($c); } } } /** * 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 TXmlElement additional configuration specified in the application configuration * @return TPageConfiguration */ protected function loadPageConfig($config) { $application=$this->getApplication(); $pagePath=$this->getRequestedPagePath(); if(($cache=$application->getCache())===null) { $pageConfig=new TPageConfiguration($pagePath); if($config!==null) { if($application->getConfigurationType()==TApplication::CONFIG_TYPE_PHP) $pageConfig->loadPageConfigurationFromPhp($config,$application->getBasePath(),''); else $pageConfig->loadPageConfigurationFromXml($config,$application->getBasePath(),''); } $pageConfig->loadFromFiles($this->getBasePath()); } else { $configCached=true; $currentTimestamp=array(); $arr=$cache->get(self::CONFIG_CACHE_PREFIX.$this->getID().$pagePath); if(is_array($arr)) { list($pageConfig,$timestamps)=$arr; if($application->getMode()!==TApplicationMode::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); $configPath=$this->getBasePath(); $fileName = $this->getApplication()->getConfigurationType()==TApplication::CONFIG_TYPE_PHP ? self::CONFIG_FILE_PHP : self::CONFIG_FILE_XML; foreach($paths as $path) { $configFile=$configPath.DIRECTORY_SEPARATOR.$fileName; $currentTimestamp[$configFile]=@filemtime($configFile); $configPath.=DIRECTORY_SEPARATOR.$path; } $appConfigFile=$application->getConfigurationFile(); $currentTimestamp[0]=$appConfigFile===null?0:@filemtime($appConfigFile); } if(!$configCached) { $pageConfig=new TPageConfiguration($pagePath); if($config!==null) { if($application->getConfigurationType()==TApplication::CONFIG_TYPE_PHP) $pageConfig->loadPageConfigurationFromPhp($config,$application->getBasePath(),''); else $pageConfig->loadPageConfigurationFromXml($config,$application->getBasePath(),''); } $pageConfig->loadFromFiles($this->getBasePath()); $cache->set(self::CONFIG_CACHE_PREFIX.$this->getID().$pagePath,array($pageConfig,$currentTimestamp)); } } return $pageConfig; } /** * @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=strtr($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 the URL for the default page */ public function getDefaultPageUrl() { return $this->constructUrl($this->getDefaultPage()); } /** * @return string the root directory for storing pages. Defaults to the 'pages' directory under the application base path. */ public function getBasePath() { if($this->_basePath===null) { $basePath=$this->getApplication()->getBasePath().DIRECTORY_SEPARATOR.self::DEFAULT_BASEPATH; if(($this->_basePath=realpath($basePath))===false || !is_dir($this->_basePath)) { $basePath=$this->getApplication()->getBasePath().DIRECTORY_SEPARATOR.self::FALLBACK_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); } /** * Sets the base page class name (in namespace format). * If a page only has a template file without page class file, * this base page class will be instantiated. * @param string class name */ public function setBasePageClass($value) { $this->_basePageClass=$value; } /** * @return string base page class name in namespace format. Defaults to 'TPage'. */ public function getBasePageClass() { return $this->_basePageClass; } /** * Sets the clientscript manager class (in namespace format). * @param string class name * @since 3.1.7 */ public function setClientScriptManagerClass($value) { $this->_clientScriptManagerClass=$value; } /** * @return string clientscript manager class in namespace format. Defaults to 'System.Web.UI.TClientScriptManager'. * @since 3.1.7 */ public function getClientScriptManagerClass() { return $this->_clientScriptManagerClass; } /** * 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'); $this->_page=$this->createPage($this->getRequestedPagePath()); $this->runPage($this->_page,$this->_properties); } /** * Creates a page instance based on requested page path. * @param string requested page path * @return TPage the requested page instance * @throws THttpException if requested page path is invalid * @throws TConfigurationException if the page class cannot be found */ protected function createPage($pagePath) { $path=$this->getBasePath().DIRECTORY_SEPARATOR.strtr($pagePath,'.',DIRECTORY_SEPARATOR); $hasTemplateFile=is_file($path.self::PAGE_FILE_EXT); $hasClassFile=is_file($path.Prado::CLASS_FILE_EXT); if(!$hasTemplateFile && !$hasClassFile) throw new THttpException(404,'pageservice_page_unknown',$pagePath); if($hasClassFile) { $className=basename($path); if(!class_exists($className,false)) include_once($path.Prado::CLASS_FILE_EXT); } else { $className=$this->getBasePageClass(); Prado::using($className); if(($pos=strrpos($className,'.'))!==false) $className=substr($className,$pos+1); } if(!class_exists($className,false) || ($className!=='TPage' && !is_subclass_of($className,'TPage'))) throw new THttpException(404,'pageservice_page_unknown',$pagePath); $page=Prado::createComponent($className); $page->setPagePath($pagePath); if($hasTemplateFile) $page->setTemplate($this->getTemplateManager()->getTemplateByFileName($path.self::PAGE_FILE_EXT)); return $page; } /** * Executes a page. * @param TPage the page instance to be run * @param array list of initial page properties */ protected function runPage($page,$properties) { foreach($properties as $name=>$value) $page->setSubProperty($name,$value); $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 true. * @param boolean whether to encode the GET parameters (their names and values), defaults to true. * @return string URL for the page and GET parameters */ public function constructUrl($pagePath,$getParams=null,$encodeAmpersand=true,$encodeGetItems=true) { return $this->getRequest()->constructUrl($this->getID(),$pagePath,$getParams,$encodeAmpersand,$encodeGetItems); } } /** * 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> * @package System.Web.Services * @since 3.0 */ class TPageConfiguration extends TComponent { /** * @var array list of application configurations */ private $_appConfigs=array(); /** * @var array list of page initial property values */ private $_properties=array(); /** * @var TAuthorizationRuleCollection list of authorization rules */ private $_rules=array(); /** * @var array list of included configurations */ private $_includes=array(); /** * @var string the currently request page in the format of Path.To.PageName */ private $_pagePath=''; /** * Constructor. * @param string the currently request page in the format of Path.To.PageName */ public function __construct($pagePath) { $this->_pagePath=$pagePath; } /** * @return array list of external configuration files. Each element is like $filePath=>$condition */ public function getExternalConfigurations() { return $this->_includes; } /** * 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 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; } /** * @return array list of application configurations specified along page path */ public function getApplicationConfigurations() { return $this->_appConfigs; } /** * Loads configuration for a page specified in a path format. * @param string root path for pages */ public function loadFromFiles($basePath) { $paths=explode('.',$this->_pagePath); $page=array_pop($paths); $path=$basePath; $configPagePath=''; $fileName = Prado::getApplication()->getConfigurationType()==TApplication::CONFIG_TYPE_PHP ? TPageService::CONFIG_FILE_PHP : TPageService::CONFIG_FILE_XML; foreach($paths as $p) { $this->loadFromFile($path.DIRECTORY_SEPARATOR.$fileName,$configPagePath); $path.=DIRECTORY_SEPARATOR.$p; if($configPagePath==='') $configPagePath=$p; else $configPagePath.='.'.$p; } $this->loadFromFile($path.DIRECTORY_SEPARATOR.$fileName,$configPagePath); $this->_rules=new TAuthorizationRuleCollection($this->_rules); } /** * Loads a specific config file. * @param string config file name * @param string the page path that the config file is associated with. The page path doesn't include the page name. */ public function loadFromFile($fname,$configPagePath) { Prado::trace("Loading page configuration file $fname",'System.Web.Services.TPageService'); if(empty($fname) || !is_file($fname)) return; if(Prado::getApplication()->getConfigurationType()==TApplication::CONFIG_TYPE_PHP) { $fcontent = include $fname; $this->loadFromPhp($fcontent,dirname($fname),$configPagePath); } else { $dom=new TXmlDocument; if($dom->loadFromFile($fname)) $this->loadFromXml($dom,dirname($fname),$configPagePath); else throw new TConfigurationException('pageserviceconf_file_invalid',$fname); } } public function loadFromPhp($config,$configPath,$configPagePath) { $this->loadApplicationConfigurationFromPhp($config,$configPath); $this->loadPageConfigurationFromPhp($config,$configPath,$configPagePath); } /** * Loads a page configuration. * The configuration includes information for both application * and page service. * @param TXmlElement config xml element * @param string the directory containing this configuration * @param string the page path that the config XML is associated with. The page path doesn't include the page name. */ public function loadFromXml($dom,$configPath,$configPagePath) { $this->loadApplicationConfigurationFromXml($dom,$configPath); $this->loadPageConfigurationFromXml($dom,$configPath,$configPagePath); } public function loadApplicationConfigurationFromPhp($config,$configPath) { $appConfig=new TApplicationConfiguration; $appConfig->loadFromPhp($config,$configPath); $this->_appConfigs[]=$appConfig; } /** * Loads the configuration specific for application part * @param TXmlElement config xml element * @param string base path corresponding to this xml element */ public function loadApplicationConfigurationFromXml($dom,$configPath) { $appConfig=new TApplicationConfiguration; $appConfig->loadFromXml($dom,$configPath); $this->_appConfigs[]=$appConfig; } public function loadPageConfigurationFromPhp($config, $configPath, $configPagePath) { // authorization if(isset($config['authorization']) && is_array($config['authorization'])) { $rules = array(); foreach($config['authorization'] as $authorization) { $patterns=isset($authorization['pages'])?$authorization['pages']:''; $ruleApplies=false; if(empty($patterns) || trim($patterns)==='*') // null or empty string $ruleApplies=true; else { foreach(explode(',',$patterns) as $pattern) { if(($pattern=trim($pattern))!=='') { // we know $configPagePath and $this->_pagePath if($configPagePath!=='') // prepend the pattern with ConfigPagePath $pattern=$configPagePath.'.'.$pattern; if(strcasecmp($pattern,$this->_pagePath)===0) { $ruleApplies=true; break; } if($pattern[strlen($pattern)-1]==='*') // try wildcard matching { if(strncasecmp($this->_pagePath,$pattern,strlen($pattern)-1)===0) { $ruleApplies=true; break; } } } } } if($ruleApplies) { $action = isset($authorization['action'])?$authorization['action']:''; $users = isset($authorization['users'])?$authorization['users']:''; $roles = isset($authorization['roles'])?$authorization['roles']:''; $verb = isset($authorization['verb'])?$authorization['verb']:''; $ips = isset($authorization['ips'])?$authorization['ips']:''; $rules[]=new TAuthorizationRule($action,$users,$roles,$verb,$ips); } } $this->_rules=array_merge($rules,$this->_rules); } // pages if(isset($config['pages']) && is_array($config['pages'])) { if(isset($config['pages']['properties'])) { $this->_properties = array_merge($this->_properties, $config['pages']['properties']); unset($config['pages']['properties']); } foreach($config['pages'] as $id => $page) { $properties = array(); if(isset($page['properties'])) { $properties=$page['properties']; unset($page['properties']); } $matching=false; $id=($configPagePath==='')?$id:$configPagePath.'.'.$id; if(strcasecmp($id,$this->_pagePath)===0) $matching=true; else if($id[strlen($id)-1]==='*') // try wildcard matching $matching=strncasecmp($this->_pagePath,$id,strlen($id)-1)===0; if($matching) $this->_properties=array_merge($this->_properties,$properties); } } // external configurations if(isset($config['includes']) && is_array($config['includes'])) { foreach($config['includes'] as $include) { $when = isset($include['when'])?true:false; if(!isset($include['file'])) throw new TConfigurationException('pageserviceconf_includefile_required'); $filePath = $include['file']; if(isset($this->_includes[$filePath])) $this->_includes[$filePath]=array($configPagePath,'('.$this->_includes[$filePath][1].') || ('.$when.')'); else $this->_includes[$filePath]=array($configPagePath,$when); } } } /** * Loads the configuration specific for page service. * @param TXmlElement config xml element * @param string base path corresponding to this xml element * @param string the page path that the config XML is associated with. The page path doesn't include the page name. */ public function loadPageConfigurationFromXml($dom,$configPath,$configPagePath) { // authorization if(($authorizationNode=$dom->getElementByTagName('authorization'))!==null) { $rules=array(); foreach($authorizationNode->getElements() as $node) { $patterns=$node->getAttribute('pages'); $ruleApplies=false; if(empty($patterns) || trim($patterns)==='*') // null or empty string $ruleApplies=true; else { foreach(explode(',',$patterns) as $pattern) { if(($pattern=trim($pattern))!=='') { // we know $configPagePath and $this->_pagePath if($configPagePath!=='') // prepend the pattern with ConfigPagePath $pattern=$configPagePath.'.'.$pattern; if(strcasecmp($pattern,$this->_pagePath)===0) { $ruleApplies=true; break; } if($pattern[strlen($pattern)-1]==='*') // try wildcard matching { if(strncasecmp($this->_pagePath,$pattern,strlen($pattern)-1)===0) { $ruleApplies=true; break; } } } } } if($ruleApplies) $rules[]=new TAuthorizationRule($node->getTagName(),$node->getAttribute('users'),$node->getAttribute('roles'),$node->getAttribute('verb'),$node->getAttribute('ips')); } $this->_rules=array_merge($rules,$this->_rules); } // pages if(($pagesNode=$dom->getElementByTagName('pages'))!==null) { $this->_properties=array_merge($this->_properties,$pagesNode->getAttributes()->toArray()); // at the page folder foreach($pagesNode->getElementsByTagName('page') as $node) { $properties=$node->getAttributes(); $id=$properties->remove('id'); if(empty($id)) throw new TConfigurationException('pageserviceconf_page_invalid',$configPath); $matching=false; $id=($configPagePath==='')?$id:$configPagePath.'.'.$id; if(strcasecmp($id,$this->_pagePath)===0) $matching=true; else if($id[strlen($id)-1]==='*') // try wildcard matching $matching=strncasecmp($this->_pagePath,$id,strlen($id)-1)===0; if($matching) $this->_properties=array_merge($this->_properties,$properties->toArray()); } } // external configurations foreach($dom->getElementsByTagName('include') as $node) { if(($when=$node->getAttribute('when'))===null) $when=true; if(($filePath=$node->getAttribute('file'))===null) throw new TConfigurationException('pageserviceconf_includefile_required'); if(isset($this->_includes[$filePath])) $this->_includes[$filePath]=array($configPagePath,'('.$this->_includes[$filePath][1].') || ('.$when.')'); else $this->_includes[$filePath]=array($configPagePath,$when); } } }