* @link http://www.pradosoft.com/ * @copyright Copyright © 2005 PradoSoft * @license http://www.pradosoft.com/license/ * @version $Id$ * @package System.Web.UI */ /** * Includes TOutputCache class file */ Prado::using('System.Web.UI.WebControls.TOutputCache'); /** * TTemplateManager class * * TTemplateManager manages the loading and parsing of control templates. * * There are two ways of loading a template, either by the associated template * control class name, or the template file name. * The former is via calling {@link getTemplateByClassName}, which tries to * locate the corresponding template file under the directory containing * the class file. The name of the template file is the class name with * the extension '.tpl'. To load a template from a template file path, * call {@link getTemplateByFileName}. * * By default, TTemplateManager is registered with {@link TPageService} as the * template manager module that can be accessed via {@link TPageService::getTemplateManager()}. * * @author Qiang Xue * @version $Id$ * @package System.Web.UI * @since 3.0 */ class TTemplateManager extends TModule { /** * Template file extension */ const TEMPLATE_FILE_EXT='.tpl'; /** * Prefix of the cache variable name for storing parsed templates */ const TEMPLATE_CACHE_PREFIX='prado:template:'; /** * Initializes the module. * This method is required by IModule and is invoked by application. * It starts output buffer if it is enabled. * @param TXmlElement module configuration */ public function init($config) { $this->getService()->setTemplateManager($this); } /** * Loads the template corresponding to the specified class name. * @return ITemplate template for the class name, null if template doesn't exist. */ public function getTemplateByClassName($className) { $class=new ReflectionClass($className); $tplFile=dirname($class->getFileName()).'/'.$className.self::TEMPLATE_FILE_EXT; return $this->getTemplateByFileName($tplFile); } /** * Loads the template from the specified file. * @return ITemplate template parsed from the specified file, null if the file doesn't exist. */ public function getTemplateByFileName($fileName) { if(($fileName=$this->getLocalizedTemplate($fileName))!==null) { Prado::trace("Loading template $fileName",'System.Web.UI.TTemplateManager'); if(($cache=$this->getApplication()->getCache())===null) return new TTemplate(file_get_contents($fileName),dirname($fileName),$fileName); else { $array=$cache->get(self::TEMPLATE_CACHE_PREFIX.$fileName); if(is_array($array)) { list($template,$timestamps)=$array; if($this->getApplication()->getMode()===TApplicationMode::Performance) return $template; $cacheValid=true; foreach($timestamps as $tplFile=>$timestamp) { if(!is_file($tplFile) || filemtime($tplFile)>$timestamp) { $cacheValid=false; break; } } if($cacheValid) return $template; } $template=new TTemplate(file_get_contents($fileName),dirname($fileName),$fileName); $includedFiles=$template->getIncludedFiles(); $timestamps=array(); $timestamps[$fileName]=filemtime($fileName); foreach($includedFiles as $includedFile) $timestamps[$includedFile]=filemtime($includedFile); $cache->set(self::TEMPLATE_CACHE_PREFIX.$fileName,array($template,$timestamps)); return $template; } } else return null; } /** * Finds a localized template file. * @param string template file. * @return string|null a localized template file if found, null otherwise. */ protected function getLocalizedTemplate($filename) { if(($app=$this->getApplication()->getGlobalization(false))===null) return $filename; foreach($app->getLocalizedResource($filename) as $file) { if(($file=realpath($file))!==false && is_file($file)) return $file; } return null; } } /** * TTemplate implements PRADO template parsing logic. * A TTemplate object represents a parsed PRADO control template. * It can instantiate the template as child controls of a specified control. * The template format is like HTML, with the following special tags introduced, * - component tags: a component tag represents the configuration of a component. * The tag name is in the format of com:ComponentType, where ComponentType is the component * class name. Component tags must be well-formed. Attributes of the component tag * are treated as either property initial values, event handler attachment, or regular * tag attributes. * - property tags: property tags are used to set large block of attribute values. * The property tag name is in the format of where AttributeName * can be a property name, an event name or a regular tag attribute name. * - group subproperty tags: subproperties of a common property can be configured using * * - directive: directive specifies the property values for the template owner. * It is in the format of <%@ property name-value pairs %>; * - expressions: They are in the formate of <%= PHP expression %> and <%% PHP statements %> * - comments: There are two kinds of comments, regular HTML comments and special template comments. * The former is in the format of , which will be treated as text strings. * The latter is in the format of , which will be stripped out. * * Tags other than the above are not required to be well-formed. * * A TTemplate object represents a parsed PRADO template. To instantiate the template * for a particular control, call {@link instantiateIn($control)}, which * will create and intialize all components specified in the template and * set their parent as $control. * * @author Qiang Xue * @version $Id$ * @package System.Web.UI * @since 3.0 */ class TTemplate extends TApplicationComponent implements ITemplate { /** * '' - template comments * '' - HTML comments * '<\/?com:([\w\.]+)((?:\s*[\w\.]+\s*=\s*\'.*?\'|\s*[\w\.]+\s*=\s*".*?"|\s*[\w\.]+\s*=\s*<%.*?%>)*)\s*\/?>' - component tags * '<\/?prop:([\w\.]+)\s*>' - property tags * '<%@\s*((?:\s*[\w\.]+\s*=\s*\'.*?\'|\s*[\w\.]+\s*=\s*".*?")*)\s*%>' - directives * '<%[%#~\\$=\\[](.*?)%>' - expressions * ')*)\s*\/>' - group subproperty tags */ const REGEX_RULES='/') // template comments { if($expectPropEnd) throw new TConfigurationException('template_comments_forbidden'); if($matchStart>$textStart) $tpl[$c++]=array($container,substr($input,$textStart,$matchStart-$textStart)); $textStart=$matchEnd+1; } // else, HTML comments and we do nothing } else throw new TConfigurationException('template_matching_unexpected',$match); } if(!empty($stack)) { $name=array_pop($stack); $tag=$name[0]==='@' ? '' : ""; throw new TConfigurationException('template_closingtag_expected',$tag); } if($textStartgetErrorCode()==='template_format_invalid' || $e->getErrorCode()==='template_format_invalid2')) throw $e; if($matchEnd===0) $line=$this->_startingLine+1; else $line=$this->_startingLine+count(explode("\n",substr($input,0,$matchEnd+1))); $this->handleException($e,$line,$input); } if($this->_directive===null) $this->_directive=array(); // optimization by merging consecutive strings, expressions, statements and bindings $objects=array(); $parent=null; $merged=array(); foreach($tpl as $id=>$object) { if(isset($object[2]) || $object[0]!==$parent) { if($parent!==null) { if(count($merged[1])===1 && is_string($merged[1][0])) $objects[$id-1]=array($merged[0],$merged[1][0]); else $objects[$id-1]=array($merged[0],new TCompositeLiteral($merged[1])); } if(isset($object[2])) { $parent=null; $objects[$id]=$object; } else { $parent=$object[0]; $merged=array($parent,array($object[1])); } } else $merged[1][]=$object[1]; } if($parent!==null) { if(count($merged[1])===1 && is_string($merged[1][0])) $objects[$id]=array($merged[0],$merged[1][0]); else $objects[$id]=array($merged[0],new TCompositeLiteral($merged[1])); } $tpl=$objects; return $objects; } /** * Parses the attributes of a tag from a string. * @param string the string to be parsed. * @return array attribute values indexed by names. */ protected function parseAttributes($str,$offset) { if($str==='') return array(); $pattern='/([\w\.]+)\s*=\s*(\'.*?\'|".*?"|<%.*?%>)/msS'; $attributes=array(); $n=preg_match_all($pattern,$str,$matches,PREG_SET_ORDER|PREG_OFFSET_CAPTURE); for($i=0;$i<$n;++$i) { $match=&$matches[$i]; $name=strtolower($match[1][0]); if(isset($attributes[$name])) throw new TConfigurationException('template_property_duplicated',$name); $value=$match[2][0]; if(substr($name,-8,8)==='template') { if($value[0]==='\'' || $value[0]==='"') $attributes[$name]=$this->parseTemplateProperty(substr($value,1,strlen($value)-2),$match[2][1]+1); else $attributes[$name]=$this->parseTemplateProperty($value,$match[2][1]); } else { if($value[0]==='\'' || $value[0]==='"') $attributes[$name]=$this->parseAttribute(substr($value,1,strlen($value)-2)); else $attributes[$name]=$this->parseAttribute($value); } } return $attributes; } protected function parseTemplateProperty($content,$offset) { $line=$this->_startingLine+count(explode("\n",substr($this->_content,0,$offset)))-1; return array(self::CONFIG_TEMPLATE,new TTemplate($content,$this->_contextPath,$this->_tplFile,$line,false)); } /** * Parses a single attribute. * @param string the string to be parsed. * @return array attribute initialization */ protected function parseAttribute($value) { if(($n=preg_match_all('/<%[#=].*?%>/msS',$value,$matches,PREG_OFFSET_CAPTURE))>0) { $isDataBind=false; $textStart=0; $expr=''; for($i=0;$i<$n;++$i) { $match=$matches[0][$i]; $token=$match[0]; $offset=$match[1]; $length=strlen($token); if($token[2]==='#') $isDataBind=true; if($offset>$textStart) $expr.=".'".strtr(substr($value,$textStart,$offset-$textStart),array("'"=>"\\'","\\"=>"\\\\"))."'"; $expr.='.('.substr($token,3,$length-5).')'; $textStart=$offset+$length; } $length=strlen($value); if($length>$textStart) $expr.=".'".strtr(substr($value,$textStart,$length-$textStart),array("'"=>"\\'","\\"=>"\\\\"))."'"; if($isDataBind) return array(self::CONFIG_DATABIND,ltrim($expr,'.')); else return array(self::CONFIG_EXPRESSION,ltrim($expr,'.')); } else if(preg_match('/\\s*(<%~.*?%>|<%\\$.*?%>|<%\\[.*?\\]%>)\\s*/msS',$value,$matches) && $matches[0]===$value) { $value=$matches[1]; if($value[2]==='~') // a URL return array(self::CONFIG_ASSET,trim(substr($value,3,strlen($value)-5))); else if($value[2]==='[') return array(self::CONFIG_LOCALIZATION,trim(substr($value,3,strlen($value)-6))); else if($value[2]==='$') return array(self::CONFIG_PARAMETER,trim(substr($value,3,strlen($value)-5))); } else return $value; } protected function validateAttributes($type,$attributes) { Prado::using($type); if(($pos=strrpos($type,'.'))!==false) $className=substr($type,$pos+1); else $className=$type; $class=new TReflectionClass($className); if(is_subclass_of($className,'TControl') || $className==='TControl') { foreach($attributes as $name=>$att) { if(($pos=strpos($name,'.'))!==false) { // a subproperty, so the first segment must be readable $subname=substr($name,0,$pos); if(!$class->hasMethod('get'.$subname)) throw new TConfigurationException('template_property_unknown',$type,$subname); } else if(strncasecmp($name,'on',2)===0) { // an event if(!$class->hasMethod($name)) throw new TConfigurationException('template_event_unknown',$type,$name); else if(!is_string($att)) throw new TConfigurationException('template_eventhandler_invalid',$type,$name); } else { // a simple property if(!$class->hasMethod('set'.$name)) { if($class->hasMethod('get'.$name)) throw new TConfigurationException('template_property_readonly',$type,$name); else throw new TConfigurationException('template_property_unknown',$type,$name); } else if(is_array($att) && $att[0]!==self::CONFIG_EXPRESSION) { if(strcasecmp($name,'id')===0) throw new TConfigurationException('template_controlid_invalid',$type); else if(strcasecmp($name,'skinid')===0) throw new TConfigurationException('template_controlskinid_invalid',$type); } } } } else if(is_subclass_of($className,'TComponent') || $className==='TComponent') { foreach($attributes as $name=>$att) { if(is_array($att) && ($att[0]===self::CONFIG_DATABIND)) throw new TConfigurationException('template_databind_forbidden',$type,$name); if(($pos=strpos($name,'.'))!==false) { // a subproperty, so the first segment must be readable $subname=substr($name,0,$pos); if(!$class->hasMethod('get'.$subname)) throw new TConfigurationException('template_property_unknown',$type,$subname); } else if(strncasecmp($name,'on',2)===0) throw new TConfigurationException('template_event_forbidden',$type,$name); else { // id is still alowed for TComponent, even if id property doesn't exist if(strcasecmp($name,'id')!==0 && !$class->hasMethod('set'.$name)) { if($class->hasMethod('get'.$name)) throw new TConfigurationException('template_property_readonly',$type,$name); else throw new TConfigurationException('template_property_unknown',$type,$name); } } } } else throw new TConfigurationException('template_component_required',$type); } /** * @return array list of included external template files */ public function getIncludedFiles() { return $this->_includedFiles; } /** * Handles template parsing exception. * This method rethrows the exception caught during template parsing. * It adjusts the error location by giving out correct error line number and source file. * @param Exception template exception * @param int line number * @param string template string if no source file is used */ protected function handleException($e,$line,$input=null) { $srcFile=$this->_tplFile; if(($n=count($this->_includedFiles))>0) // need to adjust error row number and file name { for($i=$n-1;$i>=0;--$i) { if($this->_includeAtLine[$i]<=$line) { if($line<$this->_includeAtLine[$i]+$this->_includeLines[$i]) { $line=$line-$this->_includeAtLine[$i]+1; $srcFile=$this->_includedFiles[$i]; break; } else $line=$line-$this->_includeLines[$i]+1; } } } if(empty($srcFile)) throw new TConfigurationException('template_format_invalid2',$line,$e->getMessage(),$input); else throw new TConfigurationException('template_format_invalid',$srcFile,$line,$e->getMessage()); } /** * Preprocesses the template string by including external templates * @param string template string * @return string expanded template string */ protected function preprocess($input) { if($n=preg_match_all('/<%include(.*?)%>/',$input,$matches,PREG_SET_ORDER|PREG_OFFSET_CAPTURE)) { for($i=0;$i<$n;++$i) { $filePath=Prado::getPathOfNamespace(trim($matches[$i][1][0]),TTemplateManager::TEMPLATE_FILE_EXT); if($filePath!==null && is_file($filePath)) $this->_includedFiles[]=$filePath; else { $errorLine=count(explode("\n",substr($input,0,$matches[$i][0][1]+1))); $this->handleException(new TConfigurationException('template_include_invalid',trim($matches[$i][1][0])),$errorLine,$input); } } $base=0; for($i=0;$i<$n;++$i) { $ext=file_get_contents($this->_includedFiles[$i]); $length=strlen($matches[$i][0][0]); $offset=$base+$matches[$i][0][1]; $this->_includeAtLine[$i]=count(explode("\n",substr($input,0,$offset))); $this->_includeLines[$i]=count(explode("\n",$ext)); $input=substr_replace($input,$ext,$offset,$length); $base+=strlen($ext)-$length; } } return $input; } } ?>