什麼是編譯式模板、解釋式模板,它們的區別是什麼?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; } }