开源达人带你学习DEDECMS模板原理、以及模板标签的学习。

本文,开源达人想和大家一起来学习一下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/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

<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd“>
<html xmlns=”http://www.w3.org/1999/xhtml“>
<head>
<meta http-equiv=”Content-Type” content=”text/html; charset=gb2312” />
<title>无标题文档</title>
</head>

<body>

{dede:var.title/} – {dede:php echo “Little”; /} –

{dede:php}
echo “Hann”;
{/dede:php}

</body>
</html>

同时,在当前目录下也生成了静态的html文件

code.html

<img src="http://www.tuicool.com/articles/RF3eimf" alt="" />

这也是所谓的"编译式模板"的意思,联想我们在写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
<div>
<pre class="prettyprint sql">"-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

       "Content-Type" content="text/html; char<span class="operator"><span class="keyword">set=utf-<span class="number">8<span class="string">" /></span></span></span></span></pre>
<h1>{dede:hello name='LittleHann' rstype='string'/}</h1>
</div>

这两个文件都编写完毕之后,访问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;
        }
    }//结束遍历模板字符串
    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层

php
    require_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的代码复制出来,我们逐条的分析它的代码

php
    require_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 , error
if($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";

/*

发表评论

邮箱地址不会被公开。