什么是编译式模板、解释式模板,它们的区别是什么?php
模板标签有哪些种类,它们的区别是什么,都应用在哪些场景?css
学习模板的机制原理对咱们修复目前CMS中常出现的模板类代码执行的漏洞能起到怎样的帮助?html
带着这些问题,咱们进入今天的代码研究,just hacking for fun!!前端
文章主要分为如下几个部分程序员
1. 模板基本知识介绍 2. 怎么使用模板机制、模板标签的使用方法 3. DEDE模板原理学习 1) 编译式模板 2) 解释式模板 3) 视图类模板 4. 针对模板解析底层代码的Hook Patch对CMS漏洞修复的解决方案
http://www.phpchina.com/archives/view-42534-1.htmlsql
http://tools.dedecms.com/uploads/docs/dede_tpl/index.htm数据库
1. 模板基本知识介绍express
cms模板是以cms为程序架构,就是在对应CMS系统的基础上制做的各种CMS内容管理系统的样式,页面模板等。业内对于CMS模板的定义亦是经过对于CMS系统的标签调用语言,实现CMS系统的前端展现风格,就像与一我的的外衣。编程
简单来讲,模板技术就是将业务逻辑代码和前台的UI逻辑进行了有效分离,使CMS的UI呈现和代码可以最大程序的解耦和,和MVC中的View层和Control层的思想很相似数组
系统的模板目录在系统根目录下的templets内,下面是模板目录的文件目录结构。
/templets
├─default·· 默认模板目录
│ ├─images·· 模板图片目录
│ │ ├─mood··
│ │ └─photo··
│ ├─js·· 模板JS脚本目录 │ └─style·· 模板CSS样式目录 ├─lurd·· LURD系统模板 ├─plus·· 插件模板目录 ├─system·· 系统底层模板目录 └─wap·· WAP模块模板目录
DedeCMS 从 V5 开始采用了解析式引擎与编译式引擎并存的模式,因为在生成 HTML 时,解析式引擎拥有巨大的优点,但对于动态浏览的互动性质的页面,编译式引擎更实用高效,织梦 CMS 采用双引擎并存的模式,事实上还有另外一种模板的使用方法,即视图类,不过它是对解释式模板的代码复用而成的,咱们接下来会注意学习它们
2. 怎么使用模板机制、模板标签的使用方法
在了解了模板的基本知识以后,咱们接下来学习一下在DEDECMS中的模板机制、以及模板标签的使用方法
整体来讲,目前DEDECMS有如下三种模板机制
1. 编译式模板 1) 核心文件: include/dedetemplate.class.php /include/tpllib 2) 标签使用方法 2.1) 配置变量 {dede:config name='' value=''/} 配置变量能够在载入模板后经过 $tpl->GetConfig($name) 得到,仅做为配置,不在模板中显示。 2.2) 短标记 {dede:global.name/} 外部变量 等同于 {dede:var.name/} var数组 等同于 'name']; ?> {dede:field.name/} field数组 等同于 'name']; ?> {dede:cfg.name/} 系统配置变量 等同于 考虑到大多数状况下都会在函数或类中调用模板,所以 $_vars、$fields 数组必须声明为 global 数组,不然模板引擎没法得到它的值从而致使产生错误。 2.3) 自由调用块标记 {tag:blockname bind='GetArcList' bindtype='class'} 循环代码 {/tag:blockname} 必要属性: bind 数据源来源函数 bindtype 函数类型,默认是 class 可选为 sub rstype 返回结果类型,默认是 array ,可选项为 string 自定义函数格式必须为 function(array $atts,object $refObj, array $fields); 在没有指定 bind 绑定的函数的状况下,默认指向 MakePublicTag($atts,$tpl->refObj,$fields) 统一管理。 2.4) 固定块标记 2.4.1) datalist 从绑定类成员函数GetArcList中获取数组并输出 {dede:datalist} 循环代码 {/dede:datalist} 遍历一个二给维数组,数据源是固定的,只适用用类调用。 等同于 {tag:blockname bind='GetArcList' bindtype='class' rstype='arrayu'} 循环代码 {/tag:blockname} 2.4.2) label 从绑定函数中获取字符串值并输出 等同于 {tag:blockname bind='func' bindtype='sub' rstype='string'/} 2.4.3) pagelist 从绑定类成员函数GetPageList中获取字符串值并输出 等同于 {tag:blockname bind='GetPageList' bindtype='class' rstype='string'/} 2.4.4) include {dede:include file=''/} {dede:include filename=''/} 2.4.5) php {dede:php php 代码 /} 或 {dede:php} php代码 {/dede:php} 2.4.6) If 仅支持 if ,else ,else 直接用{else}表示,但不支持{else if}这样的语法 ,通常建议模板中不要使用太复杂的条件语法,若是确实有须要,能够直接使用 php 语法。 {dede:if 条件} a-block {else} b-block {/dede:if} 条件中容许使用 var.name 、global.name 、field.name、cfg.name 表示相应的变量。 如: {dede:if field.id>10 }....{/dede:if} 2.4.7) 遍历一个 array 数组 {dede:array.name} {dede:key/} = {dede:value/} {/dede:array} 各类语法的具体编译后的代码,可查看dedetemplate.class.php的function CompilerOneTag(&$cTag) 2. 解释式模板 1) 核心文件: include/dedetag.class.php /include/taglib 2) 标签使用方法 2.1) 内置系统标记 2.1.1) global 标记,表示获取一个外部变量,除了数据库密码以外,能调用系统的任何配置参数,形式为: {dede:global name='变量名称'}{/dede:global} 或 {dede:global name='变量名称'/} 其中变量名称不能加$符号,如变量$cfg_cmspath,应该写成{dede:global name='cfg_cmspath'/}。 2.1.2) foreach 用来输出一个数组,形式为: {dede:foreach array='数组名称'}[field:key/] [field:value/]{/dede:foreach} 2.1.3) include 引入一个文件,形式为: {dede:include file='文件名称' ismake='是否为dede板块模板(yes/no)'/} 对文件的搜索路径为顺序为:绝对路径、include文件夹,CMS安装目录,CMS主模板目录 2.2) 自定义函数使用(以后在学习视图类的时候,会发现视图类的就是复用了解释式模板标签的这个自定义函数的标签用法) {dede:标记名称 属性='值' function='youfunction("参数一","参数二","@me")'/} 其中 @me 用于表示当前标记的值,其它参数由你的函数决定是否存在,例如: {dede:field name='pubdate' function='strftime("%Y-%m-%d %H:%M:%S","@me")'/} 2.3) 织梦标记容许有限的编程扩展 格式为: {dede:tagname runphp='yes'} $aaa = @me; @me = "123456"; {/dede:tagname} @me 表示这个标记自己的值,所以标记内编程是不能使用echo之类的语句的,只能把全部返回值传递给@me。 此外因为程序代码占用了底层模板InnerText的内容,所以需编程的标记只能使用默认的InnerText。 3. 视图类模板 1) 核心文件 .... arc.partview.class.php ... channelunit.class.php channelunit.func.php channelunit.helper.php /include/taglib 2) 标签使用方法 2.1) 复用解释式模板标签的自定义函数标签,即钩子技术 {dede:php}...{/dede:php}
3. DEDE模板原理学习
要使用模板机制,咱们就必须有一个代码层,负责提供数据,还得有一个UI层,负责调用模板标签进行UI显示,而模板标签的底层解析DEDECMS的核心库已经提供了,咱们只要在咱们的代码层进行引入就能够了,牢记这一点对咱们理解模板标签的使用、以及模板解析的原理颇有帮助
3.1 编译式模板
先来写个程序(之后root表明根目录)
root/code.php
php
//利用dedecms写php时,基本都要引入common.inc.php require_once (dirname(__FILE__) . '/include/common.inc.php'); //利用编译式模板所需的文件 require_once (DEDEINC.'/dedetemplate.class.php'); //生成编译模板引擎类对象 $tpl = new DedeTemplate(dirname(__file__)); //装载网页模板 $tpl->LoadTemplate('code.tpl.htm'); //把php值传到html $title = 'Hello World'; $tpl->SetVar('title',$title); $tpl->Display(); //把编译好的模板缓存作成code.html,就能够直接调用 $tpl->SaveTo(dirname(__FILE__).'/code.html'); ?>
root/code.tpl.htm
"-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> "Content-Type" content="text/html; charset=utf-8" />
{dede:php echo "Little"; /} {dede:php} echo "Hann"; {/dede:php}
这两个文件编写完成后,访问code.php
同时,在当前目录下也生成了静态的html文件
code.html
这也是所谓的"编译式模板"的意思,联想咱们在写C程序的时候,编译器会根据你的C代码编译出exe静态文件,dede的编译式引擎这里也采起了相似的思路。
咱们前面说过,编译式模板和标签解释的文件都放在/include/ tpllib 下,因此若是咱们须要编写、实现咱们本身的自定义标签,就须要按照DEDE的代码架构,在这个文件夹下添加新的标签处理代码逻辑
在include/tpllib中找一个文件来仿制。如plus_ask(咱们编写的自定义标签的解析逻辑须要知足DEDE的代码架构,这点在编写插件的时候也是一样的思路,由于咱们是在别人的基础上进行二次开发)
root/include/tpllib/plus_hello
php
if(!defined('DEDEINC')) exit('Request Error!'); /** * 动态模板hello标签 * * @version $Id: plus_ask.php 1 13:58 2010年7月5日Z tianya $ * @package DedeCMS.Tpllib * @copyright Copyright (c) 2007 - 2010, DesDev, Inc. * @license http://help.dedecms.com/usersguide/license.html * @link http://www.dedecms.com */ function plus_hello(&$atts,&$refObj,&$fields) { global $dsql,$_vars; //给出标签的属性默认参数值列表,以’,’分隔,即便不设置默认参数也要给出属性名 $attlist = "name="; FillAtts($atts,$attlist); FillFields($atts,$fields,$refObj); extract($atts, EXTR_OVERWRITE); //返回处理结果,以替换标签 return 'hello!'.$name; } ?>
仍是一样的思路,编写模板文件,去调用这个自定义标签
root/code.tpl.htm
"-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> "Content-Type" content="text/html; charset=utf-8" />
这两个文件都编写完毕以后,访问code.php
访问静态html文件
了解了编译式模板的使用方法,接下来咱们要一块儿深刻DEDECMS的源代码,来看看DEDE在底层是怎么去实现这些方便的模板机制的,使用的版本为
DedeCMS-V5.7-GBK-SP1.tar
这里容许我再复制一遍code.php的代码,咱们对照着它的代码来一行一行的解释
php
//利用dedecms写php时,基本都要引入common.inc.php require_once (dirname(__FILE__) . '/include/common.inc.php'); //利用编译式模板所需的文件 require_once (DEDEINC.'/dedetemplate.class.php'); //生成编译模板引擎类对象 $tpl = new DedeTemplate(dirname(__file__)); //装载网页模板 $tpl->LoadTemplate('code.tpl.htm'); //把php值传到html $title = 'Hello World'; $tpl->SetVar('title',$title); $tpl->Display(); //把编译好的模板缓存作成code.html,就能够直接调用 $tpl->SaveTo(dirname(__FILE__).'/code.html'); ?>
//生成编译模板引擎类对象
$tpl = new DedeTemplate(dirname(__file__));
function __construct($templatedir='',$refDir='') { //缓存目录 if($templatedir=='') { $this->templateDir = DEDEROOT.'/templates'; } else { //接收用户指定的模板目录 $this->templateDir = $templatedir; } //模板include目录 if($refDir=='') { if(isset($GLOBALS['cfg_df_style'])) { //根据用户在后台风格设置所选择风格设置模板 $this->refDir = $this->templateDir.'/'.$GLOBALS['cfg_df_style'].'/'; } else { $this->refDir = $this->templateDir; } } //设置模板编译缓存文件目录 $this->cacheDir = DEDEROOT.$GLOBALS['cfg_tplcache_dir']; }
//装载网页模板
$tpl->LoadTemplate('code.tpl.htm');
function LoadTemplate($tmpfile) { if(!file_exists($tmpfile)) { echo " Template Not Found! "; exit(); } //对用户传入的路径参数进行规范化 $tmpfile = preg_replace("/[\\/]{1,}/", "/", $tmpfile); $tmpfiles = explode('/',$tmpfile); $tmpfileOnlyName = preg_replace("/(.*)\//", "", $tmpfile); $this->templateFile = $tmpfile; $this->refDir = ''; for($i=0; $i < count($tmpfiles)-1; $i++) { $this->refDir .= $tmpfiles[$i].'/'; } //设置缓存目录 if(!is_dir($this->cacheDir)) { $this->cacheDir = $this->refDir; } if($this->cacheDir!='') { $this->cacheDir = $this->cacheDir.'/'; } if(isset($GLOBALS['_DEBUG_CACHE'])) { $this->cacheDir = $this->refDir; } //生成对应的高速缓存的文件名 $this->cacheFile = $this->cacheDir.preg_replace("/\.(wml|html|htm|php)$/", "_".$this->GetEncodeStr($tmpfile).'.inc', $tmpfileOnlyName); $this->configFile = $this->cacheDir.preg_replace("/\.(wml|html|htm|php)$/", "_".$this->GetEncodeStr($tmpfile).'_config.inc', $tmpfileOnlyName); /* 1. 不开启缓存 2. 当缓存文件不存在 3. 及模板未更新(即未被改动过)的文件的时候才载入模板并进行解析 */ if($this->isCache==FALSE || !file_exists($this->cacheFile) || filemtime($this->templateFile) > filemtime($this->cacheFile)) { $t1 = ExecTime(); //debug $fp = fopen($this->templateFile,'r'); $this->sourceString = fread($fp,filesize($this->templateFile)); fclose($fp); //对模板源文件进行解析,接下来重点分析 $this->ParseTemplate(); //模板解析时间 //echo ExecTime() - $t1; } else { //若是存在config文件,则载入此文件,该文件用于保存 $this->tpCfgs的内容,以供扩展用途 //模板中用{tag:config name='' value=''/}来设定该值 if(file_exists($this->configFile)) { //当前高速缓存文件有效命中(即在有效期以内),则引入之 include($this->configFile); } } }
//对模板源文件进行解析
$this->ParseTemplate();
function ParseTemplate() { if($this->makeLoop > 5) { return ; } //当前模板文件中的模板标签个数 $this->count = -1; //保存解析出的模板标签数组 $this->cTags = array(); $this->isParse = TRUE; $sPos = 0; $ePos = 0; //模板标签的开始定界符 $tagStartWord = $this->tagStartWord; //模板标签的结束定界符 $fullTagEndWord = $this->fullTagEndWord; $sTagEndWord = $this->sTagEndWord; $tagEndWord = $this->tagEndWord; $startWordLen = strlen($tagStartWord); //保存模板原始文件的字符串 $sourceLen = strlen($this->sourceString); //检测当前模板文件是不是有效模板文件 if( $sourceLen <= ($startWordLen + 3) ) { return; } //实例化标签属性解析对象 $cAtt = new TagAttributeParse(); $cAtt->CharToLow = TRUE; //遍历模板字符串,请取标记及其属性信息 $t = 0; $preTag = ''; $tswLen = strlen($tagStartWord); for($i=0; $i<$sourceLen; $i++) { $ttagName = ''; //若是不进行此判断,将没法识别相连的两个标记 if($i-1>=0) { $ss = $i-1; } else { $ss = 0; } $tagPos = strpos($this->sourceString,$tagStartWord,$ss); //判断后面是否还有模板标记 if($tagPos==0 && ($sourceLen-$i < $tswLen || substr($this->sourceString,$i,$tswLen) != $tagStartWord )) { $tagPos = -1; break; } //获取TAG基本信息 for($j = $tagPos+$startWordLen; $j < $tagPos+$startWordLen+$this->tagMaxLen; $j++) { if(preg_match("/[ >\/\r\n\t\}\.]/", $this->sourceString[$j])) { break; } else { $ttagName .= $this->sourceString[$j]; } } if($ttagName!='') { $i = $tagPos + $startWordLen; $endPos = -1; //判断 '/}' '{tag:下一标记开始' '{/tag:标记结束' 谁最靠近 $fullTagEndWordThis = $fullTagEndWord.$ttagName.$tagEndWord; $e1 = strpos($this->sourceString, $sTagEndWord, $i); $e2 = strpos($this->sourceString, $tagStartWord, $i); $e3 = strpos($this->sourceString, $fullTagEndWordThis, $i); $e1 = trim($e1); $e2 = trim($e2); $e3 = trim($e3); $e1 = ($e1=='' ? '-1' : $e1); $e2 = ($e2=='' ? '-1' : $e2); $e3 = ($e3=='' ? '-1' : $e3); if($e3==-1) { //不存在'{/tag:标记' $endPos = $e1; $elen = $endPos + strlen($sTagEndWord); } else if($e1==-1) { //不存在 '/}' $endPos = $e3; $elen = $endPos + strlen($fullTagEndWordThis); } //同时存在 '/}' 和 '{/tag:标记' else { //若是 '/}' 比 '{tag:'、'{/tag:标记' 都要靠近,则认为结束标志是 '/}',不然结束标志为 '{/tag:标记' if($e1 < $e2 && $e1 < $e3 ) { $endPos = $e1; $elen = $endPos + strlen($sTagEndWord); } else { $endPos = $e3; $elen = $endPos + strlen($fullTagEndWordThis); } } //若是找不到结束标记,则认为这个标记存在错误 if($endPos==-1) { echo "Tpl Character postion $tagPos, '$ttagName' Error!
\r\n"; break; } $i = $elen; //分析所找到的标记位置等信息 $attStr = ''; $innerText = ''; $startInner = 0; for($j = $tagPos+$startWordLen; $j < $endPos; $j++) { if($startInner==0) { if($this->sourceString[$j]==$tagEndWord) { $startInner=1; continue; } else { $attStr .= $this->sourceString[$j]; } } else { $innerText .= $this->sourceString[$j]; } } $ttagName = strtolower($ttagName); /* 1. if标记,把整个属性串视为属性 2. 注意到preg_replace的$format参数最后有一个"i",表明执行正则替换的同时,进行代码执行,也就是以PHP的方式对IF语句进行执行 */ if(preg_match("/^if[0-9]{0,}$/", $ttagName)) { $cAtt->cAttributes = new TagAttribute(); $cAtt->cAttributes->count = 2; $cAtt->cAttributes->items['tagname'] = $ttagName; $cAtt->cAttributes->items['condition'] = preg_replace("/^if[0-9]{0,}[\r\n\t ]/", "", $attStr); $innerText = preg_replace("/\{else\}/i", '<'."?php\r\n}\r\nelse{\r\n".'?'.'>', $innerText); } /* 1. php标记 2. 注意到preg_replace的$format参数最后有一个"i",表明执行正则替换的同时,并"不"进行代码执行,只是简单地将标签内的内容翻译为等价的PHP语法 */ else if($ttagName=='php') { $cAtt->cAttributes = new TagAttribute(); $cAtt->cAttributes->count = 2; $cAtt->cAttributes->items['tagname'] = $ttagName; $cAtt->cAttributes->items['code'] = '<'."?php\r\n".trim(preg_replace("/^php[0-9]{0,}[\r\n\t ]/", "",$attStr))."\r\n?".'>'; } else { //普通标记,解释属性 $cAtt->SetSource($attStr); } $this->count++; $cTag = new Tag(); $cTag->tagName = $ttagName; $cTag->startPos = $tagPos; $cTag->endPos = $i; $cTag->cAtt = $cAtt->cAttributes; $cTag->isCompiler = FALSE; $cTag->tagID = $this->count; $cTag->innerText = $innerText; $this->cTags[$this->count] = $cTag; } else { $i = $tagPos+$startWordLen; break; } }