DEDECMS模板原理、模板标签学习 - .Little Hann - 推酷

mikel阅读(1411)

来源: 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/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
      仅支持 ifelseelse 直接用{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: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.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 "\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("");

        //保存执行的结果
        $this->CTags[$i]->TagValue = $DedeMeValue;
        $this->CTags[$i]->IsReplace = TRUE;
    }

3.3 视图类模板

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

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

在根目录下编写code.php

php
    require_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.php
        if(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的其他类型的漏洞,希望能将这些漏洞进行归类,找出一些通用性的修复方案

iOS系统框架概述 - 简书

mikel阅读(1638)

来源: iOS系统框架概述 – 简书

iOS操作系统是苹果为公司移动设备提供的操作系统,为方便应用开发者采用了先进的设计模式。采用了经典的MVC设计模式和框架模式。本篇文章我们主要关注iOS系统提供的框架,对MVC设计模式不做过多的描述。

我们通常称呼iOS的框架为Cocoa Touch框架,Cocoa Touch是一个框架的集合,里面包含了众多的子框架。每一个子框架都是一个目录,包含了共享资源库,用于访问该资源库中储存的代码的头文件,以及图像、声音文件等其他资源,共享资源库定义应用程序可以调用的函数和方法。框架中的类相互依赖构成一个整体,提供完善的某一方面的服务或解决方案,多个框架一起实现整个应用程序的结构。由于应用程序的结构是通用的,开发者通过框架提供的函数和方法,做细致个性化的处理,从而满足不同应用的不同需求。开发一个应用程序就是将需求细致化的代码插入到框架提供的设计中来组合成一个整体完成最终的应用设计。

框架的结构是泛型结构,为应用程序提供一个模板。就像开发商开发好的毛坯房,你需要用应用程序代码来布置不同的家具地板门窗,这才让不同的房屋有不同的格调,不同的幸福。当然,有时候,做装修的时候你需要简单的改装你的房屋,但承重墙是不能改造的。就像我们之前说的,多个框架一起实现了整个应用程序的结构,我们必须接受它所定义好的应用程序结构,设计的时候让我们的应用适合该结构。

Android开发中,采用模板模式来实现应用程序的一些特性行为,Android提供了Activity,Service,Content providers,Broadcast receivers四大组件默认功能,应用通过继承这些组件根据需要覆盖组件的一些方法来完成应用程序开发。在iOS中则采用代理和协议模式来实现应用的特性行为。例如Cocoa Touch框架集合中的UIKit框架的UIApplication对象,它负责整个应用程序生命周期的事件分发。是应用最核心的一个对象,Android的设计中就需要对其子类化,覆盖父类的方法,iOS中则交给UIApplication的代理AppDeleagte来处理应用程序的各种状态改变相关事件(AppDelegate需要实现UIApplicationDelegate协议) 。在iOS的框架中,大量的使用代理和协议。

iOS提供的许多可使用的框架,构成了iOS操作系统的层次结构,从下到上依次是:Core OS、Core Ssevices、MediaLayer、Cocoa Touch共四层。下图为iOS8.3系统的框架架构图。

iOS8.3系统框架架构图

Core OS Layer,系统核心层包含大多数低级别接近硬件的功能,它所包含的框架常常被其它框架所使用。Accelerate框架包含数字信号,线性代数,图像处理的接口。针对所有的iOS设备硬件之间的差异做优化,保证写一次代码在所有iOS设备上高效运行。CoreBluetooth框架利用蓝牙和外设交互,包括扫描连接蓝牙设备,保存连接状态,断开连接,获取外设的数据或者给外设传输数据等等。Security框架提供管理证书,公钥和私钥信任策略,keychain,hash认证数字签名等等与安全相关的解决方案。

Core Services Layer,系统服务层提供给应用所需要的基础的系统服务。如Accounts账户框架,广告框架,数据存储框架,网络连接框架,地理位置框架,运动框架等等。这些服务中的最核心的是CoreFoundation和Foundation框架,定义了所有应用使用的数据类型。CoreFoundation是基于C的一组接口,Foundation是对CoreFoundation的OC封装。

Media Layer,媒体层提供应用中视听方面的技术,如图形图像相关的CoreGraphics,CoreImage,GLKit,OpenGL ES,CoreText,ImageIO等等。声音技术相关的CoreAudio,OpenAL,AVFoundation,视频相关的CoreMedia,Media Player框架,音视频传输的AirPlay框架等等。

Cocoa Touch Layer,触摸层提供应用基础的关键技术支持和应用的外观。如NotificationCenter的本地通知和远程推送服务,iAd广告框架,GameKit游戏工具框架,消息UI框架,图片UI框架,地图框架,连接手表框架,自动适配等等

在上面所有的框架中,最重要也最经常使用的就是UIKit和Foundation框架。Foundation框架提供许多基本的对象类和数据类型,使其成为应用程序开发的基础,为所有应用程序提供最基本的系统服务,和界面无关。 UIKit框架提供的类是基础的UI类库,用于创建基于触摸的用户界面,所有 iOS 应用程序都是基于 UIKit,它提供应用程序的基础架构,用于构建用户界面,绘图、处理和用户交互事件,响应手势等等。UIKit通过控制器对象管理屏幕上显示的内容,界面的跳转,来组织应用程序。没有UIKit框架就没有iOS应用程序。

之后的文章里,我们会介绍Foundation框架最常用的一些基本类,如NSString,NSArrary,NSDictionary,NSAttributedString,NSBundle等等。也会具体介绍UIKit框架的一些最基本最常用的控件。


作者:pican
链接:https://www.jianshu.com/p/0f2ab4b09e8b
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

地图两点间距离算法 - tangrongyue - 博客园

mikel阅读(1153)

来源: 地图两点间距离算法 – tangrongyue – 博客园

原文地址:http://blog.chinaunix.net/space.php?uid=22363424&do=blog&cuid=2108521

2009-02-13 11:14:40 发表于电子技术 本文链接: 通过经纬度计算距离的公式

在去年cosbeta曾经发布了一个网页计算工具,这个作用就是根据地球上两点之间的经纬度计算两点之间的直线距离。经纬度到距离的计算在通信工程中应用比较广泛,所以cosbeta通过搜索找到了一个js的计算脚本(其实是google map的计算脚本,应该算是比较准确了),做成了这个经纬度算距离的工具

今天有人给cosbeta发邮件,询问计算的公式是什么样的。其实,若是把地球当作一个正常的球体(其实它是椭球)来说,球面两点之间的距离计算并不复杂,运用球坐标很容易就能计算出两点之间的弧长。当然这都是高中的知识,我和你一样,也没有那个耐心来将其推导,所以我就利用google map的经纬度到距离计算的js脚本,将球面弧长的公式给还原出来(估计这个公式是经过部分修正的),还原出来的公式如下:

对上面的公式解释如下:

公式中经纬度均用弧度表示,角度到弧度的转化应该是很简单的了吧,若不会,依然请参考这个这个经纬度算距离的工具

Lat1 Lung1 表示A点经纬度,Lat2 Lung2 表示B点经纬度;

a=Lat1 – Lat2 为两点纬度之差  b=Lung1 -Lung2 为两点经度之差;

6378.137为地球半径,单位为公里;

计算出来的结果单位为公里;

http://www.storyday.com/wp-content/uploads/2008/09/latlung_dis.html

从google maps的脚本里扒了段代码,没准啥时会用上。大家一块看看是怎么算的。
private const double EARTH_RADIUS = 6378.137;
private static double rad(double d)
{
return d * Math.PI / 180.0;
}

public static double GetDistance(double lat1, double lng1, double lat2, double lng2)
{
double radLat1 = rad(lat1);
double radLat2 = rad(lat2);
double a = radLat1 – radLat2;
double b = rad(lng1) – rad(lng2);
double s = 2 * Math.Asin(Math.Sqrt(Math.Pow(Math.Sin(a/2),2) +
Math.Cos(radLat1)*Math.Cos(radLat2)*Math.Pow(Math.Sin(b/2),2)));
s = s * EARTH_RADIUS;
s = Math.Round(s * 10000) / 10000;
return s;
}

整理好后的完整程序如下:

#include <stdio.h>
#include <math.h>
#define EARTH_RADIUS  6378.137
#define PI 3.1415926
double rad(double d)
{
return d * PI / 180.0;
}
double dis(double lat1, double lng1, double lat2, double lng2)
{
double radLat1 = rad(lat1);
double radLat2 = rad(lat2);
double a = radLat1 – radLat2;
double b = rad(lng1) – rad(lng2);
double s = 2 * asin(sqrt(pow(sin(a/2),2) + cos(radLat1)*cos(radLat2)*pow(sin(b/2),2)));
s = s * EARTH_RADIUS;
return s;
}

int main()
{
double lat1,lng1,lat2,lng2,distance;
printf(“please input lat1:\n”);
scanf(“%lf:”,&lat1);
printf(“please input lng1:\n”);
scanf(“%lf:”,&lng1);
printf(“please input lat2:\n”);
scanf(“%lf:”,&lat2);
printf(“please input lng2:\n”);
scanf(“%lf:”,&lng2);
distance=dis(lat1,lng1,lat2,lng2);
printf(“the distance is %f Km\n:”,distance);

return 0;

}

dede修改描述description限制字数长度 - 努力的小笨 - CSDN博客

mikel阅读(1061)

来源: dede修改描述description限制字数长度 – 努力的小笨 – CSDN博客

修改了好几个地方:

1、在dede文件夹下面article_description_main.php页面,找到“if($dsize>250) $dsize = 250;”语句把250修改为500。

2、dede 文件下的 article_edit.php(这里5.7以后不用改)和 article_edit.php 修改 $description = cn_substrR($description,250); 为 $description = cn_substrR($description,500);

3、登录后台,在系统-系统基本参数-其它选项中,自动摘要长度,改成500。

4、登录后台,执行语句:alter table `dede_archives` change `description` `description` varchar( 500 ) 或者 直接登录主机数据库后台修改表dede_archives

5、调用:{dede:field.description function=’cn_substr(@me,500)’ /}。

[转载]php发送get、post请求的6种方法简明总结

mikel阅读(1558)

方法1: 用file_get_contents 以get方式获取内容:

1
2
3
4
5
<?php
$html = file_get_contents($url);
echo $html;
?>

方法2: 用fopen打开url, 以get方式获取内容:

1
2
3
4
5
6
7
8
9
<?php
$fp = fopen($url, ‘r');
stream_get_meta_data($fp);
while(!feof($fp)) {
$result .= fgets($fp, 1024);
}
echo “url body: $result”;
fclose($fp);
?>

方法3:用file_get_contents函数,以post方式获取url

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
$data = array (‘foo' => ‘bar');
$data = http_build_query($data);
$opts = array (
‘http' => array (
‘method' => ‘POST',
‘header'=> “Content-type: application/x-www-form-urlencodedrn” .
“Content-Length: ” . strlen($data) . “rn”,
‘content' => $data
)
);
$context = stream_context_create($opts);
$html = file_get_contents(‘http://localhost/e/admin/test.html', false, $context);
echo $html;
?>

方法4:用fsockopen函数打开url,以get方式获取完整的数据,包括header和body,fsockopen需要 PHP.ini 中 allow_url_fopen 选项开启

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
function get_url ($url,$cookie=false)
{
$url = parse_url($url);
$query = $url[path].”?”.$url[query];
echo “Query:”.$query;
$fp = fsockopen( $url[host], $url[port]?$url[port]:80 , $errno, $errstr, 30);
if (!$fp) {
return false;
} else {
$request = “GET $query HTTP/1.1rn”;
$request .= “Host: $url[host]rn”;
$request .= “Connection: Closern”;
if($cookie) $request.=”Cookie:  $cookien”;
$request.=”rn”;
fwrite($fp,$request);
while(!@feof($fp)) {
$result .= @fgets($fp, 1024);
}
fclose($fp);
return $result;
}
}
//获取url的html部分,去掉header
function GetUrlHTML($url,$cookie=false)
{
$rowdata = get_url($url,$cookie);
if($rowdata)
{
$body= stristr($rowdata,”rnrn”);
$body=substr($body,4,strlen($body));
return $body;
}
return false;
}
?>

方法5:用fsockopen函数打开url,以POST方式获取完整的数据,包括header和body

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php
function HTTP_Post($URL,$data,$cookie, $referrer=”")
{
// parsing the given URL
$URL_Info=parse_url($URL);
// Building referrer
if($referrer==”") // if not given use this script as referrer
$referrer=”111″;
// making string from $data
foreach($data as $key=>$value)
$values[]=”$key=”.urlencode($value);
$data_string=implode(“&”,$values);
// Find out which port is needed – if not given use standard (=80)
if(!isset($URL_Info["port"]))
$URL_Info["port"]=80;
// building POST-request:
$request.=”POST “.$URL_Info["path"].” HTTP/1.1n”;
$request.=”Host: “.$URL_Info["host"].”n”;
$request.=”Referer: $referern”;
$request.=”Content-type: application/x-www-form-urlencodedn”;
$request.=”Content-length: “.strlen($data_string).”n”;
$request.=”Connection: closen”;
$request.=”Cookie:  $cookien”;
$request.=”n”;
$request.=$data_string.”n”;
$fp = fsockopen($URL_Info["host"],$URL_Info["port"]);
fputs($fp, $request);
while(!feof($fp)) {
$result .= fgets($fp, 1024);
}
fclose($fp);
return $result;
}
?>

方法6:使用curl库,使用curl库之前,可能需要查看一下php.ini是否已经打开了curl扩展

1
2
3
4
5
6
7
8
9
10
11
<?php
$ch = curl_init();
$timeout = 5;
curl_setopt ($ch, CURLOPT_URL, ‘http://www.jb51.net/');
curl_setopt ($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt ($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
$file_contents = curl_exec($ch);
curl_close($ch);
echo $file_contents;
?>

AlexNet原理及Tensorflow实现 - CSDN博客

mikel阅读(1317)

来源: AlexNet原理及Tensorflow实现 – CSDN博客

AlexNet的出现点燃了深度学习的热潮,下面对其进行介绍,并使用tensorflow实现.

1. AlexNet网络结构

这里写图片描述

图片来源:AlexNet的论文

整个网络有8个需要训练的层,前5个为卷积层,最后3层为全连接层.

第一个卷积层

  1. 输入的图片大小为:224*224*3
  2. 第一个卷积层为:11*11*96即尺寸为11*11,有96个卷积核,步长为4,卷积层后跟ReLU,因此输出的尺寸为 224/4=56,去掉边缘为55,因此其输出的每个feature map 为 55*55*96,同时后面跟LRN层,尺寸不变.
  3. 最大池化层,核大小为3*3,步长为2,因此feature map的大小为:27*27*96.

第二层卷积层

  1. 输入的tensor为27*27*96
  2. 卷积和的大小为: 5*5*256,步长为1,尺寸不会改变,同样紧跟ReLU,和LRN层.
  3. 最大池化层,和大小为3*3,步长为2,因此feature map为:13*13*256

第三层至第五层卷积层

  1. 输入的tensor为13*13*256
  2. 第三层卷积为 3*3*384,步长为1,加上ReLU
  3. 第四层卷积为 3*3*384,步长为1,加上ReLU
  4. 第五层卷积为 3*3*256,步长为1,加上ReLU
  5. 第五层后跟最大池化层,核大小3*3,步长为2,因此feature map:6*6*256

第六层至第八层全连接层

接下来的三层为全连接层,分别为:
1. FC : 4096 + ReLU
2. FC:4096 + ReLU
3. FC: 1000
最后一层为softmax为1000类的概率值.

2. AlexNet中的trick

AlexNet将CNN用到了更深更宽的网络中,其效果分类的精度更高相比于以前的LeNet,其中有一些trick是必须要知道的.

ReLU的应用

AlexNet使用ReLU代替了Sigmoid,其能更快的训练,同时解决sigmoid在训练较深的网络中出现的梯度消失,或者说梯度弥散的问题.

Dropout随机失活

随机忽略一些神经元,以避免过拟合,

重叠的最大池化层

在以前的CNN中普遍使用平均池化层,AlexNet全部使用最大池化层,避免了平均池化层的模糊化的效果,并且步长比池化的核的尺寸小,这样池化层的输出之间有重叠,提升了特征的丰富性.

提出了LRN层

局部响应归一化,对局部神经元创建了竞争的机制,使得其中响应小打的值变得更大,并抑制反馈较小的.

使用了GPU加速计算

使用了gpu加速神经网络的训练

数据增强

使用数据增强的方法缓解过拟合现象.

3. Tensorflow实现AlexNet

下面是tensorflow的开源实现:https://github.com/tensorflow/models

AlexNet训练非常耗时,因此只定义网络结构,并进行前向后向的测试.这里自己使用的是CPU运行的…

首先定义一个接口,输入为图像,输出为第五个卷积层最后的池化层的数据,和每一个层的参数信息.都很简单,如果不懂可以参考tensorflow实战这本书或者共同交流.

def print_activations(t):
  print(t.op.name, ' ', t.get_shape().as_list())
  • 1
  • 2

上面的函数为输出当前层的参数的信息.下面是我对开源实现做了一些参数上的修改,代码如下:

def inference(images):
  """Build the AlexNet model.
  Args:
    images: Images Tensor
  Returns:
    pool5: the last Tensor in the convolutional component of AlexNet.
    parameters: a list of Tensors corresponding to the weights and biases of the
        AlexNet model.
  """
  parameters = []
  # conv1
  with tf.name_scope('conv1') as scope:
    kernel = tf.Variable(tf.truncated_normal([11, 11, 3, 96], dtype=tf.float32,
                                             stddev=1e-1), name='weights')
    conv = tf.nn.conv2d(images, kernel, [1, 4, 4, 1], padding='SAME')
    biases = tf.Variable(tf.constant(0.0, shape=[96], dtype=tf.float32),
                         trainable=True, name='biases')
    bias = tf.nn.bias_add(conv, biases)
    conv1 = tf.nn.relu(bias, name=scope)
    print_activations(conv1)
    parameters += [kernel, biases]

  # lrn1
  # TODO(shlens, jiayq): Add a GPU version of local response normalization.

  # pool1
  pool1 = tf.nn.max_pool(conv1,
                         ksize=[1, 3, 3, 1],
                         strides=[1, 2, 2, 1],
                         padding='VALID',
                         name='pool1')
  print_activations(pool1)

  # conv2
  with tf.name_scope('conv2') as scope:
    kernel = tf.Variable(tf.truncated_normal([5, 5, 96, 256], dtype=tf.float32,
                                             stddev=1e-1), name='weights')
    conv = tf.nn.conv2d(pool1, kernel, [1, 1, 1, 1], padding='SAME')
    biases = tf.Variable(tf.constant(0.0, shape=[256], dtype=tf.float32),
                         trainable=True, name='biases')
    bias = tf.nn.bias_add(conv, biases)
    conv2 = tf.nn.relu(bias, name=scope)
    parameters += [kernel, biases]
  print_activations(conv2)

  # pool2
  pool2 = tf.nn.max_pool(conv2,
                         ksize=[1, 3, 3, 1],
                         strides=[1, 2, 2, 1],
                         padding='VALID',
                         name='pool2')
  print_activations(pool2)

  # conv3
  with tf.name_scope('conv3') as scope:
    kernel = tf.Variable(tf.truncated_normal([3, 3, 256, 384],
                                             dtype=tf.float32,
                                             stddev=1e-1), name='weights')
    conv = tf.nn.conv2d(pool2, kernel, [1, 1, 1, 1], padding='SAME')
    biases = tf.Variable(tf.constant(0.0, shape=[384], dtype=tf.float32),
                         trainable=True, name='biases')
    bias = tf.nn.bias_add(conv, biases)
    conv3 = tf.nn.relu(bias, name=scope)
    parameters += [kernel, biases]
    print_activations(conv3)

  # conv4
  with tf.name_scope('conv4') as scope:
    kernel = tf.Variable(tf.truncated_normal([3, 3, 384, 384],
                                             dtype=tf.float32,
                                             stddev=1e-1), name='weights')
    conv = tf.nn.conv2d(conv3, kernel, [1, 1, 1, 1], padding='SAME')
    biases = tf.Variable(tf.constant(0.0, shape=[384], dtype=tf.float32),
                         trainable=True, name='biases')
    bias = tf.nn.bias_add(conv, biases)
    conv4 = tf.nn.relu(bias, name=scope)
    parameters += [kernel, biases]
    print_activations(conv4)

  # conv5
  with tf.name_scope('conv5') as scope:
    kernel = tf.Variable(tf.truncated_normal([3, 3, 384, 256],
                                             dtype=tf.float32,
                                             stddev=1e-1), name='weights')
    conv = tf.nn.conv2d(conv4, kernel, [1, 1, 1, 1], padding='SAME')
    biases = tf.Variable(tf.constant(0.0, shape=[256], dtype=tf.float32),
                         trainable=True, name='biases')
    bias = tf.nn.bias_add(conv, biases)
    conv5 = tf.nn.relu(bias, name=scope)
    parameters += [kernel, biases]
    print_activations(conv5)

  # pool5
  pool5 = tf.nn.max_pool(conv5,
                         ksize=[1, 3, 3, 1],
                         strides=[1, 2, 2, 1],
                         padding='VALID',
                         name='pool5')
  print_activations(pool5)

  return pool5, parameters

测试的函数:
image是随机生成的数据,不是真实的数据

def run_benchmark():
  """Run the benchmark on AlexNet."""
  with tf.Graph().as_default():
    # Generate some dummy images.
    image_size = 224
    # Note that our padding definition is slightly different the cuda-convnet.
    # In order to force the model to start with the same activations sizes,
    # we add 3 to the image_size and employ VALID padding above.
    images = tf.Variable(tf.random_normal([FLAGS.batch_size,
                                           image_size,
                                           image_size, 3],
                                          dtype=tf.float32,
                                          stddev=1e-1))

    # Build a Graph that computes the logits predictions from the
    # inference model.
    pool5, parameters = inference(images)

    # Build an initialization operation.
    init = tf.global_variables_initializer()

    # Start running operations on the Graph.
    config = tf.ConfigProto()
    config.gpu_options.allocator_type = 'BFC'
    sess = tf.Session(config=config)
    sess.run(init)

    # Run the forward benchmark.
    time_tensorflow_run(sess, pool5, "Forward")

    # Add a simple objective so we can calculate the backward pass.
    objective = tf.nn.l2_loss(pool5)
    # Compute the gradient with respect to all the parameters.
    grad = tf.gradients(objective, parameters)
    # Run the backward benchmark.
    time_tensorflow_run(sess, grad, "Forward-backward")

输出的结果为:
下面为输出的尺寸,具体的分析过程上面已经说的很详细了.

conv1   [128, 56, 56, 96]
pool1   [128, 27, 27, 96]
conv2   [128, 27, 27, 256]
pool2   [128, 13, 13, 256]
conv3   [128, 13, 13, 384]
conv4   [128, 13, 13, 384]
conv5   [128, 13, 13, 256]
pool5   [128, 6, 6, 256]

下面是训练的前后向耗时,可以看到后向传播比前向要慢3倍.

2017-05-02 15:40:53.118788: step 0, duration = 3.969
2017-05-02 15:41:30.003927: step 10, duration = 3.550
2017-05-02 15:42:07.242987: step 20, duration = 3.797
2017-05-02 15:42:44.610630: step 30, duration = 3.487
2017-05-02 15:43:20.021931: step 40, duration = 3.535
2017-05-02 15:43:55.832460: step 50, duration = 3.687
2017-05-02 15:44:31.803954: step 60, duration = 3.567
2017-05-02 15:45:08.156715: step 70, duration = 3.803
2017-05-02 15:45:44.739322: step 80, duration = 3.584
2017-05-02 15:46:20.349876: step 90, duration = 3.569
2017-05-02 15:46:53.242329: Forward across 100 steps, 3.641 +/- 0.130 sec / batch
2017-05-02 15:49:01.054495: step 0, duration = 11.493
2017-05-02 15:50:55.424543: step 10, duration = 10.905
2017-05-02 15:52:47.021526: step 20, duration = 11.797
2017-05-02 15:54:42.965286: step 30, duration = 11.559
2017-05-02 15:56:36.329784: step 40, duration = 11.185
2017-05-02 15:58:32.146361: step 50, duration = 11.945
2017-05-02 16:00:21.971351: step 60, duration = 10.887
2017-05-02 16:02:10.775796: step 70, duration = 10.914
2017-05-02 16:04:07.438658: step 80, duration = 11.409
2017-05-02 16:05:56.403530: step 90, duration = 10.915
2017-05-02 16:07:34.297486: Forward-backward across 100 steps, 11.247 +/- 0.448 sec / batch

完整的代码和测试在我的github:https://github.com/yqtaowhu/MachineLearning

参考资料

  1. ImageNet Classification with Deep Convolutional Neural Networks
  2. https://github.com/tensorflow/models
  3. tensorflow实战
  4. http://www.cnblogs.com/yymn/p/4553839.html

十图详解tensorflow数据读取机制(附代码)

mikel阅读(1184)

来源: 十图详解tensorflow数据读取机制(附代码)

在学习tensorflow的过程中,有很多小伙伴反映读取数据这一块很难理解。确实这一块官方的教程比较简略,网上也找不到什么合适的学习材料。今天这篇文章就以图片的形式,用最简单的语言,为大家详细解释一下tensorflow的数据读取机制,文章的最后还会给出实战代码以供参考。

一、tensorflow读取机制图解

首先需要思考的一个问题是,什么是数据读取?以图像数据为例,读取数据的过程可以用下图来表示:

假设我们的硬盘中有一个图片数据集0001.jpg,0002.jpg,0003.jpg……我们只需要把它们读取到内存中,然后提供给GPU或是CPU进行计算就可以了。这听起来很容易,但事实远没有那么简单。事实上,我们必须要把数据先读入后才能进行计算,假设读入用时0.1s,计算用时0.9s,那么就意味着每过1s,GPU都会有0.1s无事可做,这就大大降低了运算的效率。

如何解决这个问题?方法就是将读入数据和计算分别放在两个线程中,将数据读入内存的一个队列,如下图所示:

读取线程源源不断地将文件系统中的图片读入到一个内存的队列中,而负责计算的是另一个线程,计算需要数据时,直接从内存队列中取就可以了。这样就可以解决GPU因为IO而空闲的问题!

而在tensorflow中,为了方便管理,在内存队列前又添加了一层所谓的“文件名队列”

为什么要添加这一层文件名队列?我们首先得了解机器学习中的一个概念:epoch。对于一个数据集来讲,运行一个epoch就是将这个数据集中的图片全部计算一遍。如一个数据集中有三张图片A.jpg、B.jpg、C.jpg,那么跑一个epoch就是指对A、B、C三张图片都计算了一遍。两个epoch就是指先对A、B、C各计算一遍,然后再全部计算一遍,也就是说每张图片都计算了两遍。

tensorflow使用文件名队列+内存队列双队列的形式读入文件,可以很好地管理epoch。下面我们用图片的形式来说明这个机制的运行方式。如下图,还是以数据集A.jpg, B.jpg, C.jpg为例,假定我们要跑一个epoch,那么我们就在文件名队列中把A、B、C各放入一次,并在之后标注队列结束。

程序运行后,内存队列首先读入A(此时A从文件名队列中出队):

再依次读入B和C:

此时,如果再尝试读入,系统由于检测到了“结束”,就会自动抛出一个异常(OutOfRange)。外部捕捉到这个异常后就可以结束程序了。这就是tensorflow中读取数据的基本机制。如果我们要跑2个epoch而不是1个epoch,那只要在文件名队列中将A、B、C依次放入两次再标记结束就可以了。

二、tensorflow读取数据机制的对应函数

如何在tensorflow中创建上述的两个队列呢?

对于文件名队列,我们使用tf.train.string_input_producer函数。这个函数需要传入一个文件名list,系统会自动将它转为一个文件名队列。

此外tf.train.string_input_producer还有两个重要的参数,一个是num_epochs,它就是我们上文中提到的epoch数。另外一个就是shuffle,shuffle是指在一个epoch内文件的顺序是否被打乱。若设置shuffle=False,如下图,每个epoch内,数据还是按照A、B、C的顺序进入文件名队列,这个顺序不会改变:

如果设置shuffle=True,那么在一个epoch内,数据的前后顺序就会被打乱,如下图所示:

在tensorflow中,内存队列不需要我们自己建立,我们只需要使用reader对象从文件名队列中读取数据就可以了,具体实现可以参考下面的实战代码。

除了tf.train.string_input_producer外,我们还要额外介绍一个函数:tf.train.start_queue_runners。初学者会经常在代码中看到这个函数,但往往很难理解它的用处,在这里,有了上面的铺垫后,我们就可以解释这个函数的作用了。

在我们使用tf.train.string_input_producer创建文件名队列后,整个系统其实还是处于“停滞状态”的,也就是说,我们文件名并没有真正被加入到队列中(如下图所示)。此时如果我们开始计算,因为内存队列中什么也没有,计算单元就会一直等待,导致整个系统被阻塞。

而使用tf.train.start_queue_runners之后,才会启动填充队列的线程,这时系统就不再“停滞”。此后计算单元就可以拿到数据并进行计算,整个程序也就跑起来了,这就是函数tf.train.start_queue_runners的用处。

三、实战代码

我们用一个具体的例子感受tensorflow中的数据读取。如图,假设我们在当前文件夹中已经有A.jpg、B.jpg、C.jpg三张图片,我们希望读取这三张图片5个epoch并且把读取的结果重新存到read文件夹中。

对应的代码如下:

# 导入tensorflow
import tensorflow as tf 

# 新建一个Session
with tf.Session() as sess:
    # 我们要读三幅图片A.jpg, B.jpg, C.jpg
    filename = ['A.jpg', 'B.jpg', 'C.jpg']
    # string_input_producer会产生一个文件名队列
    filename_queue = tf.train.string_input_producer(filename, shuffle=False, num_epochs=5)
    # reader从文件名队列中读数据。对应的方法是reader.read
    reader = tf.WholeFileReader()
    key, value = reader.read(filename_queue)
    # tf.train.string_input_producer定义了一个epoch变量,要对它进行初始化
    tf.local_variables_initializer().run()
    # 使用start_queue_runners之后,才会开始填充队列
    threads = tf.train.start_queue_runners(sess=sess)
    i = 0
    while True:
        i += 1
        # 获取图片数据并保存
        image_data = sess.run(value)
        with open('read/test_%d.jpg' % i, 'wb') as f:
            f.write(image_data)

我们这里使用filename_queue = tf.train.string_input_producer(filename, shuffle=False, num_epochs=5)建立了一个会跑5个epoch的文件名队列。并使用reader读取,reader每次读取一张图片并保存。

运行代码后,我们得到就可以看到read文件夹中的图片,正好是按顺序的5个epoch:

如果我们设置filename_queue = tf.train.string_input_producer(filename, shuffle=False, num_epochs=5)中的shuffle=True,那么在每个epoch内图像就会被打乱,如图所示:

我们这里只是用三张图片举例,实际应用中一个数据集肯定不止3张图片,不过涉及到的原理都是共通的。

四、总结

这篇文章主要用图解的方式详细介绍了tensorflow读取数据的机制,最后还给出了对应的实战代码,希望能够给大家学习tensorflow带来一些实质性的帮助。如果各位小伙伴还有什么疑问,欢迎评论或私信告诉我,谢谢~

Django开发小型站之前期准备(一) - LOVESTYUDY - 博客园

mikel阅读(1060)

来源: Django开发小型站之前期准备(一) – LOVESTYUDY – 博客园

语言:python3.5

工具:JetBrains PyCharm

virtualenvwrapper优点:

1、使不同的应用开发环境独立

2、环境升级不影响其他应用,也不会影响全局的python环境

3、它可以防止系统中出现包管理混乱和版本的冲突

1、安装virtualenvwrapper

pip3 install virtualenvwrapper-win

2、新建虚拟环境

mkvirtualenv testvir2

3、查看当前虚拟环境安装了哪些库

pip3 list –format=columns

4、安装django

pip3 install django

5、安装mySQL

pip3 intstall pymySQL

Django 在Python3.5 下报 没有模块MySQLdb

解决方法:

在整个项目站点下的__init__.py 文件里(即和setting.py在同一个文件下)写入以下代码:

import pymysql
pymysql.install_as_MySQLdb()

需要提前安装pymysql模块,相当于Python2中的MySQLdb模块。

即可解决问题。

6、退出当前虚拟环境

deactivate

Angular 2 WhatsApp clone with Meteor & Ionic

mikel阅读(1388)

来源: Angular 2 WhatsApp clone with Meteor & Ionic

Angular-Meteor & Ionic 2

Your next project

Facing your next app project, web, mobile or both, you want to choose the best solutions to start fast while also solutions that will stay relevant when your project grows and scales.

Angular, Meteor and Ionic are all platforms that aim to supply everything you need to write when creating an app.

Angular – Angular is a frontend platform that tried to include everything you need in order to build the frontend part of your Angular. Angular also has their own CLI that is based on Webpack.

Ionic – Ionic is based on Angular. it has become one of the most popular solutions to develop hybrid mobile apps fast across different platform.

The Ionic platform includes solutions for prototyping, build, testing, deploying apps, a market of starter apps, plugins and themes, CLI integration and push notifications service. (Further writing by an Ionic person)

Meteor – But your app needs a full stack solution.

Meteor has become the only open source JavaScript platform that supply the complete set of solutions you need to create a real time mobile connected apps.

The Meteor platform is reliable, fast and easy to develop and deploy and it will also handle all the complexities of your app when it grows and scales with time.

How to choose?

Angular Meteor

Your best option is to use all of them together! With angular-meteor, Meteor became the best backend for Angular and Ionic apps.

So now you can use the strengthnesses of each of those platform combined the create the ultimate stack for your mobile apps.

The Angular Meteor project’s goal is to make the process of creating apps as easy and fast as possible. I do that by keeping track of the latest releases and libraries and comparing them with each other on top of a real apps that our community developers and we support.

But I also believe that education and resources is a crucial part of having a great platform so that’s why I’ve created the tutorial and the tutorial infrastructure that are based on the real WhatsApp and Slack clone app that I use myself and in our community team.

In this tutorial we will create a full WhatsApp and Slack clone, using Angular and the Ionic Framework, using Meteor’s realtime collections for the chat and Meteor’s simple Authentication packages for SMS based authentication.

But which CLI tool should you choose?
Each platform has it’s own CLI and build process solutions. You can choose the CLI and build process that you prefer.

But which one is best for you? Meteor CLI, Ionic CLI or Angular CLI?

So I’ve decided to create to two versions of the tutorial, one, using the Ionic CLI and one, using the Meteor CLI and build process.

The goal of the tutorial is to learn Angular but we are using Ionic because it’s just an addition on top of Angular and it doesn’t require to learn a lot of different concepts then just Angular. Also, the steps that require in order to use the Ionic CLI with Meteor are almost identical to the steps that require in order to use the Angular CLI.

The tutorial is completely based on git and that means that we can compare tools like Ionic, Webpack, Meteor and Angular with an actual git diff between the same app written with each of these flavors. So just go in the tutorial and click between the different versions to compare the difference.

Are we missing a flavor you want? open an issue and help us out

Please send feedback and requests about this tutorial with opening issues on the Angular Meteor Github repository.

Ionic-wechat项目边开发边学(四):可伸缩输入框,下拉刷新, 置顶删除 - Frogmarch - 博客园

mikel阅读(1329)

来源: Ionic-wechat项目边开发边学(四):可伸缩输入框,下拉刷新, 置顶删除 – Frogmarch – 博客园

摘要

上一篇文章主要介绍了ion-list的使用, ion-popup的使用, 通过sass自定义样式, localStorage的使用, 自定义指令和服务. 这篇文章实现的功能有消息的置顶与删除, 了聊天详情页面, 可伸缩输入框, 下拉刷新聊天记录, 要介绍的知识点有:

  1. filter orderBy的使用
  2. 引入angular-elastic模块
  3. 下拉刷新
  4. keyboard插件的使用
  5. 如何在真机中调试

先看效果图(键盘弹起会覆盖聊天记录, 已修复):

清晰效果见视频

filter orderBy的应用

聊天列表需要按时间顺序排列, 同时点击置顶后, 置顶的记录需要排在最上面, 这个效果就使用angularJS内置的过滤器
orderBy来排序, 使用方式

ng-repeat="item in items | orderBy : expression : reverse

这里的expression可以是functionangular expressionArray, 当为数组的时候, 首先按第一个排, 当相等的时候再按第二个排, 以此类推
reverse布尔值, 表示正序还是反序

所以我们只需要设置两个变量, 一个最后一条消息时间, 一个设置置顶的时间就可以实现置顶:

ng-repeat="message in messages | orderBy:['isTop', 'lastMessage.timeFrome1970']:true

可伸缩输入框

细心的人可能会发现, 微信的输入框超过一行后, 高度会变大的, 所以我们也来做一个可伸缩的输入框
Angular Elastic这个是一个autosize textareas的
angularJS插件, 我们需要融入到Ionic中, 首先把该插件源码放到js/目录下, 并在app.js中注入

angular.module('wechat', ['ionic', 'wechat.controllers', 'wechat.routes',
     'wechat.services', 'wechat.directives', 'monospaced.elastic'
      ])//注入monospaced.elastic

然后在textarea中添加msd-elastic指令

<textarea msd-elastic ng-model="foo">
  ...
</textarea>

做到这里, textare还是不能伸缩的, 因为ion-footer-bar的高度是固定的, 所以我们需要先动态调整ion-footer-bar
的高度, 在elastic.js中向上传播taResize事件, 这个名字可以自己定义

if (taHeight !== mirrorHeight) {
     scope.$emit('elastic:resize', $ta, taHeight, mirrorHeight);
     ta.style.height = mirrorHeight + 'px';
}   
 
scope.$emit('taResize', $ta); //添加此行
// small delay to prevent an infinite loop
$timeout(function() {
     active = false;
}, 1, false);

再在directives.js中创建一个指令:

     .directive('resizeFootBar', ['$ionicScrollDelegate', function($ionicScrollDelegate){
         // Runs during compile
         return {
             replace: false,
             link: function(scope, iElm, iAttrs, controller) {
                 //绑定taResize事件
                 scope.$on("taResize", function(e, ta) {
                     if (!ta) return;
                     var scroll = document.body.querySelector("#message-detail-content");
                     var scrollBar = $ionicScrollDelegate.$getByHandle('messageDetailsScroll');
                     var taHeight = ta[0].offsetHeight;
                     var newFooterHeight = taHeight + 10;
                     newFooterHeight = (newFooterHeight > 44) ? newFooterHeight : 44;
 
                     //调整ion-footer-bar高度
                     iElm[0].style.height = newFooterHeight + 'px';
                     //下面两行代码, 是解决键盘弹出覆盖聊天内容的bug
                     //第一行增加内容区高度
                     //第二行滑动到底部
                     scroll.style.bottom = newFooterHeight + 'px';
                     scrollBar.scrollBottom();
                 });
             }
         };
     }]);

最后再在ion-footer-bar中添加这个指令就行拉, 别忘了把左右图标固定到底部哦~

keyboard插件的使用

ionic-plugin-keyboard插件可以让你更轻松的处理键盘相关的事件
细心的人会发现, 上面的输入框在弹出键盘后会覆盖聊天记录的内容, 所以我们需要引入这个插件, 当弹出键盘的时候, 需要把scroll
滑到底部, 在项目目录下输入下面命令安装插件:

cordova plugin add ionic-plugin-keyboard

然后就可以注册键盘相关消息

window.addEventListener("native.keyboardshow", function(e){
               viewScroll.scrollBottom();
           });

这样的话, 就不会遮住聊天记录啦, 注意刚刚那个指令中的两行代码哦~

如何在真机中调试

调试键盘消息时, 电脑上不会弹出键盘, 就需要在手机上调试, 但是手机上的打印消息看不到, 如何调试呢?
相信大家跟我一样迫切需要这个功能, 给大家介绍一下利用chrome调试, 很强大哦~
首先手机连上电脑ionic run Android安装好应用
再在chrome地址栏中输入chrome://inspect/#devices
然后点击inspect就行拉, 就跟调试网页一样哦, 很方便~

最后

到这里message这块就差不多了, 还有些细节我没写出来, 大家有疑问可以在下面评论哦, 或者直接查看代码, 下一章将开始联系人列表模块.
最近公司开始新项目, 比较忙, 更新有点慢, 但我会坚持下去!