(转)DEDECMS模板原理、模板标签学习 - .Little Hann

本文,小瀚想和大家一起来学习一下DEDECMS中目前所使用的模板技术的原理:

什么是编译式模板、解释式模板,它们的区别是什么?

模板标签有哪些种类,它们的区别是什么,都应用在哪些场景?

学习模板的机制原理对我们修复目前CMS中常出现的模板类代码执行的漏洞能起到怎样的帮助?

带着这些问题,我们进入今天的代码研究,just hacking for fun!!

文章主要分为以下几个部分

1. 模板基本知识介绍
2. 怎么使用模板机制、模板标签的使用方法
3. DEDE模板原理学习1) 编译式模板2) 解释式模板3) 视图类模板
4. 针对模板解析底层代码的Hook Patch对CMS漏洞修复的解决方案

http://www.phpchina.com/archives/view-42534-1.html

http://tools.dedecms.com/uploads/docs/dede_tpl/index.htm

1. 模板基本知识介绍

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/tpllib2) 标签使用方法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 可选为 subrstype     返回结果类型,默认是 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/taglib2) 标签使用方法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.phpchannelunit.func.phpchannelunit.helper.php/include/taglib2) 标签使用方法2.1) 复用解释式模板标签的自定义函数标签,即钩子技术{dede:php}...{/dede:php} 

3. DEDE模板原理学习

要使用模板机制,我们就必须有一个代码层,负责提供数据,还得有一个UI层,负责调用模板标签进行UI显示,而模板标签的底层解析DEDECMS的核心库已经提供了,我们只要在我们的代码层进行引入就可以了,牢记这一点对我们理解模板标签的使用、以及模板解析的原理很有帮助

3.1 编译式模板

先来写个程序(以后root代表根目录) 
root/code.php

php//利用dedecms写php时,基本都要引入common.inc.phprequire_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:var.title/}

    {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" />

{dede:hello name='LittleHann' rstype='string'/}

这两个文件都编写完毕之后,访问code.php

访问静态html文件

了解了编译式模板的使用方法,接下来我们要一起深入DEDECMS的源代码,来看看DEDE在底层是怎么去实现这些方便的模板机制的,使用的版本为

DedeCMS-V5.7-GBK-SP1.tar

这里允许我再复制一遍code.php的代码,我们对照着它的代码来一行一行的解释

php//利用dedecms写php时,基本都要引入common.inc.phprequire_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;}}//结束遍历模板字符串if( $this->count > -1 && $this->isCompiler ){//调用/include/tplib/下的对应标签解析文件对指定标签进行解析$this->CompilerAll();}
}

回到code.php的代码分析上来,我们已经知道引擎会把php标签内的内容翻译为等价的<?php .. ?>代码

//把php值传到html 
$title = 'Hello World'; 
$tpl->SetVar('title',$title);

function SetVar($k, $v)
{/*1. 所谓的从代码层向UI层传值,本质上就是利用超全局变量进行变量共享2. 模板标签的本质就是等价的值替换*/$GLOBALS['_vars'][$k] = $v;
}

回到code.php

//显示编译后的模板文件

$tpl->Display();

function Display()
{ global $gtmpfile;//进行一次全局数组的变量注册extract($GLOBALS, EXTR_SKIP);//将编译后的模板文件写进告诉缓存文件中,以备下一次访问的时候加速访问速度$this->WriteCache(); /*1. 编译好的文件include引入进来2. 这一步是代码能够执行的关键,因为我们知道,编译式模板引擎在上一步翻译标签的时候只是单纯地将php标签内的内容翻译为等价的"",并不提供执行3. include进来后,代码就得到了执行*/include $this->cacheFile;
}

回到code.php

//把编译好的模板缓存做成code.html,就可以直接调用 
$tpl->SaveTo(dirname(__FILE__).'/code.html');

function SaveTo($savefile)
{extract($GLOBALS, EXTR_SKIP);//这就是为什么我们在访问了一次编译式模板.php代码后,可以继而访问已经生成了静态html文件$this->WriteCache();ob_start();//再次引入一次include $this->cacheFile;$okstr = ob_get_contents();ob_end_clean();$fp = @fopen($savefile,"w") or die(" Tag Engine Create File FALSE! ");fwrite($fp,$okstr);fclose($fp);
}

3.2 解释式模板

首先需要解释一下这个名词,为什么要称之为解释式模板引擎呢?我们都知道C语言属于编译式的语言,需要将源代码一次全部编译成exe文件才可以统一执行,而PHP属于解释式语言,zend引擎在解释的时候是逐条读取PHP源代码,然后逐条执行。

而回想我们之前学习编译式模板引擎的时候,编译式引擎会先将所有的php执行标签全部先翻译为等价的php可执行语法,然后在最后一个统一的include进行代码执行,这不就是编译式的思想吗?

而我们接下来要学习的解释式模板引擎,是逐个检测php执行标签,在解析的同时就直接进行eval执行,这恰好体现了解释式语言的思想,这就是编译式、解释式名词的由来

我们先来学习一下解释式标签的使用方法

编写/root/code.php,还是一样,记住模板的两个关键要素,代码层、UI层

phprequire_once (dirname(__file__).'/include/common.inc.php');//利用解析式模板所需的文件require_once (dirname(__file__).'/include/dedetag.class.php');$dtp=new DedeTagParse(); $dtp->LoadTemplate(dirname(__file__).'\code.tpl.htm ');foreach ($dtp->CTags as $id=>$tag){if($tag->GetName()=='my')//把id为$id的tag翻译成这是my标签$dtp->Assign($id,'this is my tag
');    else if($tag->GetName()=='test')$dtp->Assign($id,'this is test tag
');}$dtp->Display();
?>

编写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:my att1=1 att2='2'}[field:my/]{/dede:my}{dede:test att1=1 att2='2'}[field:test/]{/dede:test}{dede:tagname runphp='yes'}echo "LittleHann" . "
";{/dede:tagname} 

这两个文件都编写好之后,访问code.php

解释式模板引擎并不会产生静态html文件,即时解释,即时生效,并不保存

了解了解释式模板标签的使用方法后,我们接下来学习一下解释式模板引擎的代码原理

请允许我再次将code.php的代码复制出来,我们逐条的分析它的代码

phprequire_once (dirname(__file__).'/include/common.inc.php');//利用解析式模板所需的文件require_once (dirname(__file__).'/include/dedetag.class.php');//实例化一个DedeTagParse对象$dtp=new DedeTagParse(); //加载模板$dtp->LoadTemplate(dirname(__file__).'\code.tpl.htm ');foreach ($dtp->CTags as $id=>$tag){if($tag->GetName()=='my')//把id为$id的tag翻译成这是my标签$dtp->Assign($id,'this is my tag
');    else if($tag->GetName()=='test')$dtp->Assign($id,'this is test
');}$dtp->Display();
?>

//实例化一个DedeTagParse对象 
$dtp=new DedeTagParse();

function __construct()
{//设置是否保存高速缓存文件if(!isset($GLOBALS['cfg_tplcache'])){$GLOBALS['cfg_tplcache'] = 'N';}if($GLOBALS['cfg_tplcache']=='Y'){$this->IsCache = TRUE;}else{$this->IsCache = FALSE;}//设置默认命名空间为dede$this->NameSpace = 'dede';//设置模板标签开始定界符$this->TagStartWord = '{';//设置模板标签结束定界符$this->TagEndWord = '}';//模板标签最大长度$this->TagMaxLen = 64;$this->CharToLow = TRUE;//保存模板源文件$this->SourceString = '';//保存解析后的标签对象数组$this->CTags = Array();$this->Count = -1;$this->TempMkTime = 0;$this->CacheFile = '';
}

//加载模板 
$dtp->LoadTemplate(dirname(__file__).'\code.tpl.htm ');

function LoadTemplate($filename)
{ //设置默认模板文件路径$this->SetDefault();//检测模板文件是否存在if(!file_exists($filename)){$this->SourceString = " $filename Not Found! ";$this->ParseTemplet();}else{$fp = @fopen($filename, "r");while($line = fgets($fp,1024)){$this->SourceString .= $line;}fclose($fp);//如果高速缓存命中,则直接返回,加快访问速度if($this->LoadCache($filename)){return '';}else{//对模板源文件进行标签解析$this->ParseTemplet();}}
}

//对模板源文件进行标签解析 
$this->ParseTemplet();

function ParseTemplet()
{//模板标签开始定界符$TagStartWord = $this->TagStartWord;//模板标签结束定界符$TagEndWord = $this->TagEndWord;$sPos = 0; $ePos = 0;//命名空间的拼接$FullTagStartWord =  $TagStartWord.$this->NameSpace.":";$sTagEndWord =  $TagStartWord."/".$this->NameSpace.":";$eTagEndWord = "/".$TagEndWord;$tsLen = strlen($FullTagStartWord);$sourceLen=strlen($this->SourceString);//检测原始模板文件是否符合规范if( $sourceLen <= ($tsLen + 3) ){return;}//实例化一个标签属性解析对象$cAtt = new DedeAttributeParse();$cAtt->charToLow = $this->CharToLow;//遍历模板字符串,请取标记及其属性信息for($i=0; $i < $sourceLen; $i++){$tTagName = '';//如果不进行此判断,将无法识别相连的两个标记if($i-1 >= 0){$ss = $i-1;}else{$ss = 0;}$sPos = strpos($this->SourceString,$FullTagStartWord,$ss);$isTag = $sPos;if($i==0){$headerTag = substr($this->SourceString,0,strlen($FullTagStartWord));if($headerTag==$FullTagStartWord){$isTag=TRUE; $sPos=0;}}if($isTag===FALSE){break;} //开始遍历模板源文件for($j=($sPos+$tsLen); $j<($sPos+$tsLen+$this->TagMaxLen); $j++){if($j>($sourceLen-1)){break;}else if( preg_match("/[\/ \t\r\n]/", $this->SourceString[$j]) || $this->SourceString[$j] == $this->TagEndWord ){break;}else{$tTagName .= $this->SourceString[$j];}}//对标签的开始和结束、嵌套标签进行定位if($tTagName != ''){  $i = $sPos + $tsLen;$endPos = -1;$fullTagEndWordThis = $sTagEndWord.$tTagName.$TagEndWord; $e1 = strpos($this->SourceString,$eTagEndWord, $i);$e2 = strpos($this->SourceString,$FullTagStartWord, $i);$e3 = strpos($this->SourceString,$fullTagEndWordThis,$i);//$eTagEndWord = /} $FullTagStartWord = {tag: $fullTagEndWordThis = {/tag:xxx]$e1 = trim($e1); $e2 = trim($e2); $e3 = trim($e3);$e1 = ($e1=='' ? '-1' : $e1);$e2 = ($e2=='' ? '-1' : $e2);$e3 = ($e3=='' ? '-1' : $e3);//not found '{/tag:'if($e3==-1) {$endPos = $e1;$elen = $endPos + strlen($eTagEndWord);}//not found '/}'else if($e1==-1) {$endPos = $e3;$elen = $endPos + strlen($fullTagEndWordThis);}//found '/}' and found '{/dede:'else{//if '/}' more near '{dede:'、'{/dede:' , end tag is '/}', else is '{/dede:'if($e1 < $e2 &&  $e1 < $e3 ){$endPos = $e1;$elen = $endPos + strlen($eTagEndWord);}else{$endPos = $e3;$elen = $endPos + strlen($fullTagEndWordThis);}}//not found end tag , errorif($endPos==-1){echo "Tag Character postion $sPos, '$tTagName' Error!
\r\n";break;}$i = $elen;$ePos = $endPos;//分析所找到的标记位置等信息$attStr = '';$innerText = '';$startInner = 0;for($j=($sPos+$tsLen);$j < $ePos;$j++){if($startInner==0 && ($this->SourceString[$j]==$TagEndWord && $this->SourceString[$j-1]!="\\") ){$startInner=1;continue;}if($startInner==0){$attStr .= $this->SourceString[$j];}else{$innerText .= $this->SourceString[$j];}}//echo "$attStr\r\n";/*朋友们看到这里可以稍微停一下,我们将dedetag.class.php和dedetemplate.class.php进行一下横向对比1. 编译式模板引擎在loadTemplate的时候就会将所有的标签都翻译为等价的PHP代码,相当于一个编译的过程,等待之后的include进行引入执行2. 解释式模板引擎在laodTemplate的时候只是进行单纯的标签解析、提取出有效内容,并不做实际的翻译。而具体的解释和执行是在后面的Display中进行的,即边解释,边执行3. 在学习这两种模板机制的时候多多和传统编程中的概念进行对比,能够帮助我们更加深入地理解概念*/$cAtt->SetSource($attStr); if($cAtt->cAttributes->GetTagName()!=''){$this->Count++;$CDTag = new DedeTag();$CDTag->TagName = $cAtt->cAttributes->GetTagName();$CDTag->StartPos = $sPos;$CDTag->EndPos = $i;$CDTag->CAttribute = $cAtt->cAttributes;$CDTag->IsReplace = FALSE;$CDTag->TagID = $this->Count;$CDTag->InnerText = $innerText;$this->CTags[$this->Count] = $CDTag;} }else{$i = $sPos+$tsLen;break;}}//结束遍历模板字符串if($this->IsCache){ //保存标签解释完毕后的模板文件到高速缓存中(注意,因为这是解释式引擎,所以此时保存的cache中并不是PHP代码,而是附带标签的模板文件)$this->SaveCache(); }
}

回到code.php上来

$dtp->Display();

function Display()
{echo $this->GetResult();
}

echo $this->GetResult();

function GetResult()
{$ResultString = '';if($this->Count==-1){return $this->SourceString;}//进行标签的解释、并执行。这里就相当于解释器的作用了$this->AssignSysTag(); $nextTagEnd = 0;$strok = "";for($i=0;$i<=$this->Count;$i++){$ResultString .= substr($this->SourceString,$nextTagEnd,$this->CTags[$i]->StartPos-$nextTagEnd);$ResultString .= $this->CTags[$i]->GetValue();$nextTagEnd = $this->CTags[$i]->EndPos;}$slen = strlen($this->SourceString);if($slen>$nextTagEnd){$ResultString .= substr($this->SourceString,$nextTagEnd,$slen-$nextTagEnd);} //返回解释执行后的返回结果return $ResultString;
}

//进行标签的解释、并执行。这里就相当于解释器的作用了 
$this->AssignSysTag();

function AssignSysTag()
{global $_sys_globals;for($i=0;$i<=$this->Count;$i++){$CTag = $this->CTags[$i];$str = '';//获取一个外部变量if( $CTag->TagName == 'global' ){$str = $this->GetGlobals($CTag->GetAtt('name'));if( $this->CTags[$i]->GetAtt('function')!='' ){//$str = $this->EvalFunc( $this->CTags[$i]->TagValue, $this->CTags[$i]->GetAtt('function'),$this->CTags[$i] );$str = $this->EvalFunc( $str, $this->CTags[$i]->GetAtt('function'),$this->CTags[$i] );}$this->CTags[$i]->IsReplace = TRUE;$this->CTags[$i]->TagValue = $str;}//引入静态文件else if( $CTag->TagName == 'include' ){$filename = ($CTag->GetAtt('file')=='' ? $CTag->GetAtt('filename') : $CTag->GetAtt('file') );$str = $this->IncludeFile($filename,$CTag->GetAtt('ismake'));$this->CTags[$i]->IsReplace = TRUE;$this->CTags[$i]->TagValue = $str;}//循环一个普通数组else if( $CTag->TagName == 'foreach' ){$arr = $this->CTags[$i]->GetAtt('array');if(isset($GLOBALS[$arr])){foreach($GLOBALS[$arr] as $k=>$v){$istr = '';$istr .= preg_replace("/\[field:key([\r\n\t\f ]+)\/\]/is",$k,$this->CTags[$i]->InnerText);$str .= preg_replace("/\[field:value([\r\n\t\f ]+)\/\]/is",$v,$istr);}}$this->CTags[$i]->IsReplace = TRUE;$this->CTags[$i]->TagValue = $str;}//设置/获取变量值else if( $CTag->TagName == 'var' ){$vname = $this->CTags[$i]->GetAtt('name');if($vname==''){$str = '';}else if($this->CTags[$i]->GetAtt('value')!=''){$_vars[$vname] = $this->CTags[$i]->GetAtt('value');}else{$str = (isset($_vars[$vname]) ? $_vars[$vname] : '');}$this->CTags[$i]->IsReplace = TRUE;$this->CTags[$i]->TagValue = $str;}/*运行PHP接口当检测到有runphp这种标签属性的时候,则对这个标签进行PHP解析*/if( $CTag->GetAtt('runphp') == 'yes' ){$this->RunPHP($CTag, $i);}if(is_array($this->CTags[$i]->TagValue)){$this->CTags[$i]->TagValue = 'array';}}
}

$this->RunPHP($CTag, $i);

function RunPHP(&$refObj, $i){$DedeMeValue = $phpcode = '';if($refObj->GetAtt('source')=='value'){$phpcode = $this->CTags[$i]->TagValue; }else{$DedeMeValue = $this->CTags[$i]->TagValue;//获取标签内的内容$phpcode = $refObj->GetInnerText(); }//将@me替换成$DedeMeValue标签值$phpcode = preg_replace("/'@me'|\"@me\"|@me/i", '$DedeMeValue', $phpcode);/*这句是关键,对php执行标签内的内容直接调用eval进行执行\体会一下这是不是边解释、边执行的效果*/@eval($phpcode); //or die("<SPAN class="variable">$phpcode</span>");//保存执行的结果$this->CTags[$i]->TagValue = $DedeMeValue;$this->CTags[$i]->IsReplace = TRUE;}

3.3 视图类模板

接下来要学习的第三种模板称之为视图类模板,严格来说,它不能算是一种新的模板机制,因为它复用了很多解释式模板的代码逻辑

先来学习一下视图类模板的使用方法

在根目录下编写code.php

phprequire_once (dirname(__file__).'/include/common.inc.php');//利用解析式模板所需的文件require_once(DEDEINC.'/arc.partview.class.php');//实例化一个PartView对象$pv = new PartView();$tagbody = file_get_contents("code.tpl.htm");//加载模板$pv->SetTemplet($tagbody, 'string');echo $pv->GetResult();
?>

然后编写模板文件 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 "

LittleHann"; {/dede:php}

这两个文件都准备好之后,访问code.php

接下来,我们来分析一下这个视图类模板的解析原理

//实例化一个PartView对象 
$pv = new PartView();

function __construct($typeid=0,$needtypelink=TRUE)
{global $_sys_globals,$ftp;$this->TypeID = $typeid;$this->dsql = $GLOBALS['dsql'];/*实例化一个解释式模板引擎对象这句要重点注意,我们之后会看到视图类模板对象复用了解释式模板引擎的部分代码逻辑*/$this->dtp = new DedeTagParse();//设置模板标签的命名空间$this->dtp->SetNameSpace("dede","{","}");$this->dtp->SetRefObj($this);$this->ftp = &$ftp;$this->remoteDir = '';if($needtypelink){$this->TypeLink = new TypeLink($typeid);if(is_array($this->TypeLink->TypeInfos)){foreach($this->TypeLink->TypeInfos as $k=>$v){if(preg_match("/[^0-9]/", $k)){$this->Fields[$k] = $v;}}}$_sys_globals['curfile'] = 'partview';$_sys_globals['typename'] = $this->Fields['typename'];//设置环境变量SetSysEnv($this->TypeID,$this->Fields['typename'],0,'','partview');}SetSysEnv($this->TypeID,'',0,'','partview');$this->Fields['typeid'] = $this->TypeID;//设置一些全局参数的值foreach($GLOBALS['PubFields'] as $k=>$v){$this->Fields[$k] = $v;}
}

回到code.php上来

//加载模板 
$pv->SetTemplet($tagbody, 'string');

function SetTemplet($temp,$stype="file")
{if($stype=="string"){   //复用解释式模板引擎的LoadSource方法,去加载、匹配标签$this->dtp->LoadSource($temp);}else{$this->dtp->LoadTemplet($temp);}if($this->TypeID > 0){$this->Fields['position'] = $this->TypeLink->GetPositionLink(TRUE);$this->Fields['title'] = $this->TypeLink->GetPositionLink(false);}  //调用视图类模板引擎自己的标签解释方法ParseTemplet$this->ParseTemplet();
}

//复用解释式模板引擎的LoadSource方法,去加载、匹配标签 
$this->dtp->LoadSource($temp);

我们知道,对于解释式模板引擎来说,LoadSource只是在在加载模板,并对模板文件中的标签进行提取并保存,而具体的标签解析、执行要在Display中进行。

所以,视图类模板引擎复用了解释式模板引擎的这个LoadSource逻辑

接下来,视图类模板引擎调用了自己的 ParseTemplet 方法,进行具体的标签解析、执行

//调用视图类模板引擎自己的标签解释方法ParseTemplet 
$this->ParseTemplet();

function ParseTemplet()
{$GLOBALS['envs']['typeid'] = $this->TypeID;if($this->TypeID>0){$GLOBALS['envs']['topid'] = GetTopid($this->TypeID);}else {$GLOBALS['envs']['topid'] = 0;}if(isset($this->TypeLink->TypeInfos['reid'])){$GLOBALS['envs']['reid'] = $this->TypeLink->TypeInfos['reid'];}if(isset($this->TypeLink->TypeInfos['channeltype'])){$GLOBALS['envs']['channelid'] = $this->TypeLink->TypeInfos['channeltype'];}/*这个函数放在 channelunit.func.php 文件中 视图类模板引擎使用了钩子技术来对标签进行动态地解析这个函数是钩子的入口*/MakeOneTag($this->dtp,$this);
}

MakeOneTag($this->dtp,$this);

在arc.partview.class.php的开头,include了channelunit.class.php,而channelunit.class.php又引入了channelunit.func.php,在channelunit.func.php中加载了一个辅助类

helper('channelunit');

这个辅助类的加载函数helper的实现在common.func.php中

$_helpers = array();
function helper($helpers)
{//如果是数组,则进行递归操作if (is_array($helpers)){foreach($helpers as $dede){helper($dede);}return;}if (isset($_helpers[$helpers])){continue;}if (file_exists(DEDEINC.'/helpers/'.$helpers.'.helper.php')){ include_once(DEDEINC.'/helpers/'.$helpers.'.helper.php');$_helpers[$helpers] = TRUE;}// 无法载入小助手if ( ! isset($_helpers[$helpers])){exit('Unable to load the requested file: helpers/'.$helpers.'.helper.php');                }
}

这样,通过调用helper('channelunit')成功加载了channelunit.helper.php文件, MakeOneTag 的实现就在这个文件中

function MakeOneTag(&$dtp, &$refObj, $parfield='Y')
{global $cfg_disable_tags;//检测用户是否设置了禁用{dede:php}模板标签$cfg_disable_tags = isset($cfg_disable_tags)? $cfg_disable_tags : 'php';$disable_tags = explode(',', $cfg_disable_tags);$alltags = array();$dtp->setRefObj($refObj);//读取自由调用tag列表$dh = dir(DEDEINC.'/taglib');while($filename = $dh->read()){if(preg_match("/\.lib\./", $filename)){$alltags[] = str_replace('.lib.php','',$filename);}}$dh->Close();//遍历tag元素if(!is_array($dtp->CTags)){return '';}foreach($dtp->CTags as $tagid=>$ctag){$tagname = $ctag->GetName();if($tagname=='field' && $parfield=='Y'){$vname = $ctag->GetAtt('name');if( $vname=='array' && isset($refObj->Fields) ){$dtp->Assign($tagid,$refObj->Fields);}else if(isset($refObj->Fields[$vname])){$dtp->Assign($tagid,$refObj->Fields[$vname]);}else if($ctag->GetAtt('noteid') != ''){if( isset($refObj->Fields[$vname.'_'.$ctag->GetAtt('noteid')]) ){$dtp->Assign($tagid, $refObj->Fields[$vname.'_'.$ctag->GetAtt('noteid')]);}}continue;}//由于考虑兼容性,原来文章调用使用的标记别名统一保留,这些标记实际调用的解析文件为inc_arclist.phpif(preg_match("/^(artlist|likeart|hotart|imglist|imginfolist|coolart|specart|autolist)$/", $tagname)){$tagname='arclist';}if($tagname=='friendlink'){$tagname='flink';}if(in_array($tagname,$alltags)){if(in_array($tagname, $disable_tags)){echo 'DedeCMS Error:Tag disabled:"'.$tagname.'"           target="_blank">more...!';return FALSE;}if (DEBUG_LEVEL==TRUE) {$ttt1 = ExecTime();}/*从这里开始就是关于钩子技术的实现了1. 根据标签动态地决定要加载什么标签解析文件。我们知道,和解释式标签有关的解释代码都在/include/taglib/中2. 根据标签动态的拼接要调用的函数名,即PHP的动态函数执行,这是一种典型的钩子技术*/$filename = DEDEINC.'/taglib/'.$tagname.'.lib.php';include_once($filename);$funcname = 'lib_'.$tagname;//调用动态函数进行执行,并将返回结果传给UI层$dtp->Assign($tagid,$funcname($ctag,$refObj));if (DEBUG_LEVEL==TRUE) {$queryTime = ExecTime() - $ttt1;echo '标签:'.$tagname.'载入花费时间:'.$queryTime."
\r\n";}}}
}

例如,我们的模板文件的内容是{dede:php}echo 2;{/dede:php}

则在钩子函数MakeOneTag这里就会动态的去include引入php_lib.php的这个文件,并调用php_lib方法对这个标签进行解析,具体怎么解析的逻辑都在php_lib.php这个文件中

function lib_php(&$ctag, &$refObj)
{global $dsql;global $db;$phpcode = trim($ctag->GetInnerText());if ($phpcode == '')return '';ob_start();//再次进行一次本地啊变量注册extract($GLOBALS, EXTR_SKIP);//这句是关键,直接对标签内部的内容调用eval进行执行@eval($phpcode);//只不过和解释式模板引擎不同的是,这里并不是直接返回执行结果,而是将执行结果缓存起来,返回给调用方$revalue = ob_get_contents();ob_clean();return $revalue;
}

回到code.php上来

//模板标签的执行结果已经保存起来了,需要我们自己去显示出来 
echo $pv->GetResult();

总结一下和三种模板标签和代码执行有关的的PHP代码执行标签的用法

1. 编译式标签:
{dede:php   php代码 /}
或
{dede:php}  php代码 {/dede:php}2. 解释式标签
{dede:tagname runphp='yes'}php代码
{/dede:tagname}3. 视图类标签
{dede:php} php代码 {/dede:php}

黑客要利用模板类的漏洞进行代码执行,所会使用的模板标签就是这三种

4. 针对模板解析底层代码的Hook Patch对CMS漏洞修复的解决方案

所有模板类相关的漏洞都有一个共同的特点,就是代码执行,而在模板引擎中,进行代码执行的底层文件是比较集中的,我们可以针对某几个特定的文件进行Hook Patch,检测流经其中的数据是否包含敏感关键字,而从从底层来防御这种模板类漏洞,当然从原则上,CMS的其他漏洞也是可以采取相同的思路,这里面我们要做的就是对这些漏洞进行分类,从而找出漏洞的共同点

我希望在本文的研究中做出一些试探性的尝试,同时也希望引发大家的共同思考,对目前CMS漏洞的修复除了单纯地针对某个文件去做针对性的修复,然后每次ODAY爆发,再急忙到官网删去下补丁(或者站长自己就是程序员,自己做手工patch),这样带来的问题就是补丁的修复具有滞后性,如果能从根源上去思考漏洞的成因,在代码层的底部做一个总览性、归类性的防御,是不是能更好地解决目前CMS漏洞的发生呢?

从目前的情况来看,我的思考结果是,可以在两种模板引擎的解析函数中进行Hook,就可以达到目的了,因为视图类模板复用了解释式模板引擎的模板解析代码,所以也包含在这两个

dedetag.class.php -> ParseTemplet
dedetemplate.class.php -> ParseTemplate

我们可以在其中的关键代码位置Hook上这个函数

function find_tag_payload($tagbody)
{ $express = "/<\?(php){0,1}(.*)/i";if (preg_match($express, $tagbody)) {die("Request Error!");  }
}

我们来做一些尝试

1. dedetag.class.php -> ParseTemplet

在匹配、提取模板标签的位置做Hook

..
$cAtt->SetSource($attStr);
if($cAtt->cAttributes->GetTagName()!='')
{$this->Count++;$CDTag = new DedeTag();$CDTag->TagName = $cAtt->cAttributes->GetTagName();$CDTag->StartPos = $sPos;$CDTag->EndPos = $i;$CDTag->CAttribute = $cAtt->cAttributes;$CDTag->IsReplace = FALSE;$CDTag->TagID = $this->Count;$this->find_tag_payload($innerText);$CDTag->InnerText = $innerText;$this->CTags[$this->Count] = $CDTag;
}
....function find_tag_payload($tagbody)
{ $express = "/<\?(php){0,1}(.*)/i";if (preg_match($express, $tagbody)) {die("Request Error!");  }
}

2. dedetemplate.class.php -> ParseTemplate

在if、php标签的地方都做Hook

...
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);$this->find_tag_payload($innerText);$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;$this->find_tag_payload($attStr);$cAtt->cAttributes->items['code'] = '<'."?php\r\n".trim(preg_replace("/^php[0-9]{0,}[\r\n\t ]/","",$attStr))."\r\n?".'>';
}
...function find_tag_payload($tagbody)
{ $express = "/<\?(php){0,1}(.*)/i";if (preg_match($express, $tagbody)) {die("Request Error!");  }
}

这样做好Hook Patch之后,我们使用dede的一个很有名的模板类执行漏洞进行测试

http://ha.cker.in/1006.seo

http://www.i0day.com/1403.html

这是一个利用注入漏洞向数据库打入模板执行rootkit,然后再触发模板执行,从而进行写磁盘GETSHELL

访问

http://localhost/dede5.7/plus/mytag_js.php?aid=1

攻击被成功地防御住了。我觉得这有点类似于堡垒主机的思维方式,将复杂多变的上层漏洞风险集中到相对较少数量的底层代码逻辑上,在所有代码流都必须流经的关键点做攻击检测,从而从根本上防御一些已知、甚至未知的CMS漏洞攻击。

目前这种方法还处在完善中,也希望搞CMS、WEB漏洞攻防的朋友能分享一些更好的思路,代码的安全问题的路还很长。

今天的文章就到这里了,下一步调研一下DEDECMS的其他类型的漏洞,希望能将这些漏洞进行归类,找出一些通用性的修复方案

DEDECMS模板原理、模板标签学习相关推荐

  1. (转)DEDECMS模板原理、模板标签学习 - .Little Hann

    本文,小瀚想和大家一起来学习一下DEDECMS中目前所使用的模板技术的原理: 什么是编译式模板.解释式模板,它们的区别是什么? 模板标签有哪些种类,它们的区别是什么,都应用在哪些场景? 学习模板的机制 ...

  2. 织梦新建php支持标签,织梦教程:新建php页面且模板支持读取标签

    这篇文章主要为大家详细介绍了织梦教程:新建php页面且模板支持读取标签,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,有需要的朋友可以收藏方便以后借鉴. 在使用织梦dedecms建站时,想自己建立 ...

  3. 织梦php模板,DedeCMS的主要模板文件与功能

    DedeCMS系统的模板是非固定的,用户可以在新建栏目时可以自行选择栏目模板,官方仅提供最基本的默认模板,即是内置系统模型的各个模板,由于DedeCMS支持自定义频道模型,用户自定义新频道模型后,还需 ...

  4. 织梦php模板安装教程,[教程] DEDECMS织梦模板安装/更换教程大全

    [教程] DEDECMS织梦模板安装/更换教程大全 系列方法一: 1.下载一个模板之后要判断一下模板文件是否齐全.htm文件中是否包括首页.列表页和文章内容页等;htm文件用到的css文件是否包括;模 ...

  5. 织梦模板 diy.php修改,dedecms织梦模板源代码修改教程

    想要对织梦内核源码就行修改,首先要了解下织梦的基本构造和基本的HTML CSS知识,如果您对HTML代码不太了解,可以先去学习HTML知识然后再来学习织梦模板源代码的修改.在我们介绍织梦模板源代码修改 ...

  6. php smarty 原理,php模板原理PHP模板引擎smarty模板原理浅谈

    mvc是开发中的一个伟大的思想,使得开发代码有了更加清晰的层次,让代码分为了三层各施其职.无论是对代码的编写以及后期的阅读和维护,都提供了很大的便利. 我们在php开发中,视图层view是不允许有ph ...

  7. 织梦模板改html教程,dedecms织梦模板介绍及制作修改

    dedecms 站长新朋友们可能对织梦模板不太了解,更不知道如何修改.下面山西SEO优化傲远为大家介绍一下织梦模板及简单的修改操作. 关于dedecms模板,我们首选要了解一下它的几个基本定义.Ded ...

  8. php 模板 原理,php模板原理讲解

    php模板原理讲解 复制代码 代码如下: $data = array( 'title'=>'ilsea', 'list'=>array( 'hello', 'world' ) ); inc ...

  9. 变量模板smarty模板 入门学习

    近期个人几篇文章介绍了改变量模板的文章. 关联文章的地址 1.安装Smarty3.0 每日一道理 "多难兴才"曾一度被人定为规律.请看:屈原被放逐而作<离骚>:司马迁受 ...

最新文章

  1. linux查看服务依赖关系,服务管理(1)
  2. jsTree 插件Ajax数据
  3. 算法与数据结构(约瑟夫问题)
  4. Netty系列(三):说说NioEventLoop
  5. Cocos2d-x之Sprite
  6. php判断单向链表中有没有环,python判断链表是否有环的实例代码
  7. how is odata metadata request served 故意把configuration file里的GM6改成GM61之后
  8. [渝粤教育] 西南科技大学 土木工程施工 在线考试复习资料(1)
  9. moodle架构分析---表现层的设计(二)
  10. LintCode_514 Paint Fence
  11. Premiere Elements使用指南:键盘快捷键
  12. java递归 杨辉三角_java杨辉三角递归实现
  13. win10 多任务 多视图 多窗口 处理快捷键
  14. 实现DevOps的三步工作法
  15. ElasticSearch读流程
  16. OpenGL视角LooAt及Perspective理解
  17. 怎么学习英文--英国人教你如何学习英文
  18. PE、PM、PD、PR分别是什么岗位?
  19. java读取文件的字节数据
  20. 《比尔盖茨传》学习笔记

热门文章

  1. Css中用Google Fonts ,国内使用方法
  2. Android完全隐藏导航键/虚拟按键和状态栏
  3. 爬虫实践---抓取小说网站
  4. 征信报告有多重要?信用记录出现不良怎么办?
  5. RubyWin32Api Win32OLE
  6. 荆门市龙泉高中2021高考成绩查询,高中学业水平考试成绩查询系统
  7. 一拳超人激励我的台词
  8. 流量分析方面的公开数据集
  9. 调包侠系列之—调用face_recognition进行人脸识别
  10. grunt构建化之路——基础篇