早就想分析一下Typecho的源码了,最近好像事情比较少,把读源码的过程记录下来,整理到这里,希望大家看了有所感悟和收获。

源码版本:1.0/14.10.10

相关文件

与数据库相关的文件,都存放在/var/Typecho/Db/下面,以及/var/Typecho/Db.php文件。

Db.php

首先来看Db.php文件。这里有个Typecho_Db类,根据Typecho的命名规范,将_替换成\就可以找到对应的文件,例如这个类就存放在Typecho\Db.php文件中。

首先在这个类中定义了一堆的const常量,看一遍就好,没什么可说的地方。下面有几个private的变量,在Typechoprivate的变量名均由_开头。

$_adapter
$_config
$_pool
$_connectedPool
$_prefix
$_adapterName
$_instance

先从简单的看,有几个getXXX的函数,单纯的是获取内容,没有什么特殊的。

然后来看一下__construct这个构造函数,有两个参数,分别是适配器的名称,以及表前缀,并且表前缀默认为typecho_

public function __construct($adapterName, $prefix = 'typecho_')
{
    /** 获取适配器名称 */
    $this->_adapterName = $adapterName;

    /** 数据库适配器 */
    /*
        这里Typecho_Db_Adapter和后面的变量连接起来又是另一个类,根据明明风格可以找到源文件在哪里,后面分析这里。
    */
    $adapterName = 'Typecho_Db_Adapter_' . $adapterName;

    /*
        这里调用call_user_func,实际上是调用了$adapterName这个类的isAvailable()函数。如果返回false,就抛出异常(实际上这个异常已经被typecho接管了)。
    */
    if (!call_user_func(array($adapterName, 'isAvailable'))) {
        throw new Typecho_Db_Exception("Adapter {$adapterName} is not available");
    }
    /*
        下面是一些初始化、实例化的操作。
    */
    $this->_prefix = $prefix;

    /** 初始化内部变量 */
    $this->_pool = array();
    $this->_connectedPool = array();
    $this->_config = array();

    //实例化适配器对象
    $this->_adapter = new $adapterName();
}

到这里构造函数就分析完了,接下来分析剩下的几个函数:sql(), addServer(), set(), get()

sql()函数的作用只有一个,就是返回Typecho_Db_Query类的实例化对象。

return new Typecho_Db_Query($this->_adapter, $this->_prefix);

addServer()这个函数的作用也比较简单,实际上就是为多数据库添加支持,这个多数据库好像很主流了,CI框架也早就支持多数据库了。这里又涉及到了一个Typecho_Config::factory这么一个工厂方法,后面我们看到Config类的时候再说,这里我们看到给_config[]赋值了,$key为配置总数量减1。然后把这个数据库连接,根据第二个参数$op来判断如何放入连接池中。

set()get()的作用也比较简单,分别是设置默认数据对象和获取数据库实例化对象,用静态变量存储实例化的数据库对象,可以保证数据连接仅进行一次。

继续向后看,最后的几个select(), update()等比较类似,我们分析几个,首先来看select()函数:

public function select()
{
    /*
        func_get_args()的作用是返回一个包含这个函数参数的数组
    */
    $args = func_get_args();

    /*
        这里可能有些同学看不懂了,首先看$this->sql(),这个实际返回了一个Typecho_Db_Query的实例,然后这个call_user_func就调用了这个实例的select()函数,注意不是调用本类的select(),而是Typecho_Db_Query的select()函数,然后如果没有参数的话,就是传入*,相当于select * 吧。
    */
    return call_user_func_array(array($this->sql(), 'select'), $args ? $args : array('*'));
}

分析完了select(),继续向下分析insert()函数,其他的函数类似。

public function insert($table)
{
    /*
        $this->sql()返回一个Typecho_Db_Query的实例,然后调用这个实例的insert($table)函数。
    */
    return $this->sql()->insert($table);
}

后面会看到一个比较大的函数query(),主要作用是执行查询语句。

首先要做的是判断传入的这个$query是否为Typecho_Db_Query的实例,如果是的话,设置一下$action$op。如果这个$query既非对象又非string的话,将其视为查询得到的resource,直接返回。

接下来,选择连接池,之前在构造函数的时候已经已经按照self::READself:WRITE设置了连接池,这里就根据$_pool$_connectedPool选择一个合适的连接池,如果已经有已经连接的,那就拿来用,没有的话,如果$_pool也为空的话,就抛出异常,不空的话就获取一个随机的连接池里的连接。

然后就提交查询了,是由$this->_adapter进行query操作的,而_adapter又是根据$adapterName实例化的。

$adapterName = 'Typecho_Db_Adapter_' . $adapterName;

最后根据不同的查询动作,返回相应的资源。到这里query就分析完了,后面的fetchXXX系列就可以使用$this->query来进行不同的查询了。后面的fetchXXX系列比较简单,一看就能明白,但是需要注意的有个filter,这个后面再说。

/var/Typecho/Db

这个文件夹下面有很多的文件,我们先来总体看一下。

Db
├── Adapter
│   ├── Pdo             
│   │   ├── Mysql.php   class Typecho_Db_Adapter_Pdo_Mysql extends Typecho_Db_Adapter_Pdo
│   │   ├── Pgsql.php   class Typecho_Db_Adapter_Pdo_Pgsql extends Typecho_Db_Adapter_Pdo
│   │   └── SQLite.php  class Typecho_Db_Adapter_Pdo_SQLite extends Typecho_Db_Adapter_Pdo
│   ├── Pdo.php     abstract class Typecho_Db_Adapter_Pdo implements Typecho_Db_Adapter
│   ├── Pgsql.php       class Typecho_Db_Adapter_Pgsql implements Typecho_Db_Adapter
│   ├── SQLite.php      class Typecho_Db_Adapter_SQLite implements Typecho_Db_Adapter
│   ├── Exception.php   class Typecho_Db_Adapter_Exception extends Typecho_Db_Exception
│   └── Mysql.php       class Typecho_Db_Adapter_Mysql implements Typecho_Db_Adapter
├── Adapter.php     interface Typecho_Db_Adapter,定义通用的数据库适配接口
├── Exception.php       class Typecho_Db_Exception extends Typecho_Exception
├── Query.php           class Typecho_Db_Query
└── Query
    └── Exception.php   class Typecho_Db_Query_Exception extends Typecho_Db_Exception

到我手里的这个源码版本(const VERSION = '1.0/14.10.10';)为止,这几个Excpetion.php文件均为空。
看到一大堆的文件,其中还有刚刚熟悉的Typecho_Db_Query类,不急,我们先从那个接口文件开始看起。

/var/Typecho/Db/Adapter.php

这个文件的开头注释是:Typecho数据库适配器,定义通用的数据库适配接口。看了注释大概就知道这个接口是干啥的了,仔细分析一下后可以看到,这里面一共定义了几个函数,注释十分详细。

/var/Typecho/Db/Adapter/Pdo.php

下面来看看这个抽象类,这是个Pdo的适配器,注释不知道是否手误,写了mysql,不过不影响我们阅读。
注意这里面的两个protected变量也是由下划线开头的,后面不要弄混。

isAvailable()函数使用了extension_loaded这个函数判断一个扩展是否有效,这里判断了是否有PDO

connect()根据一个Typecho_Config的关于配置信息的对象来连接数据,这里简单的调用了本类的init()函数来初始化,接着设置了Error Reportingthrow ExceptionPDO属性,但是很不幸,这个init()又是abstract,那只能等一下再分析了。

接下来是query()函数,这个函数实际上就是用了prepareexecute,最后返回resource。另外,如果传进来的$queryTypecho_Db_Query对象的话,又会给一些变量赋值,$_lastTable大概是为了加快查询或是其他的一些加快速度的用途吧。

后面有两个fetchXXX函数,不说了。

再后面有两个quoteXXX系列,然而quoteColumn是空的。。。quoteValue这里用到了$this->_object的函数,而这个又是通过init这个函数来赋值的,所以也只能分析完下面再来看这个了。
(回来分析:init()返回的是个PDO对象,所以这里的$_object实际上就是个PDO对象,connect()里就是设置了PDO的属性,quoteValue()中调用了PDO的quote()函数,这个函数的具体作用详见:http://php.net/manual/zh/pdo.quote.php)

/var/Typecho/Db/Adapter/Mysql.php

下面看一下和Pdo.php同级的几个文件,分别是Mysql.php Pgsql.php SQLite.php我觉得基本上都一样吧,只有细节不同,所以我就挑一个我比较熟悉的Mysql来说吧。

这个文件是Mysql的适配器类,总体上和刚刚的Pdo.php基本一致。不同的是,这里的connect()函数直接是连接了MySQL数据库,query、fetch之类的函数也完全是MySQL风格的。

quoteColumn()函数则是在传入的字符串两边加上了反引号,而quoteValue()函数则是对传入的字符串进行了一个很神奇的替换,并在字符串前后加上了转义的单引号。

return '\'' . str_replace(array('\'', '\\'), array('\'\'', '\\\\'), $string) . '\'';

还有一个变化很大的函数是parseSelect(),这个函数则是将传入的array合并成一个SQL语句。这个函数先处理join的问题,然后再处理limitoffset,最后进行拼接,没看出来有什么安全问题,才疏学浅。。。

public function parseSelect(array $sql)
{
    if (!empty($sql['join'])) {
        foreach ($sql['join'] as $val) {
            list($table, $condition, $op) = $val;
            $sql['table'] = "{$sql['table']} {$op} JOIN {$table} ON {$condition}";
        }
    }

    $sql['limit'] = (0 == strlen($sql['limit'])) ? NULL : ' LIMIT ' . $sql['limit'];
    $sql['offset'] = (0 == strlen($sql['offset'])) ? NULL : ' OFFSET ' . $sql['offset'];

    return 'SELECT ' . $sql['fields'] . ' FROM ' . $sql['table'] .
    $sql['where'] . $sql['group'] . $sql['having'] . $sql['order'] . $sql['limit'] . $sql['offset'];
}

/var/Typecho/Db/Adapter/Pdo/Mysql.php

这个文件是数据库Pdo_Mysql适配器。跟上面的Pdo.phpMysql.php相比,内容又基本差不多。

不一样的是init()函数终于有了函数体,我们来分析一下。

public function init(Typecho_Config $config)
{
    $pdo = new PDO(!empty($config->dsn) ? $config->dsn :
        "mysql:dbname={$config->database};host={$config->host};port={$config->port}", $config->user, $config->password);
    $pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true);
    $pdo->exec("SET NAMES '{$config->charset}'");
    return $pdo;
}

可以看到函数十分的简单,根据$config建立一个PDO对象,然后给PDO的属性设置一下,设置一下编码,就返回了PDO对象,也就是说我们上面看到的$this->init()得到的都是个PDO对象。大家自己回去再看一下之前的代码,这里就不再赘述了。

/var/Typecho/Db/query.php

终于要讲到这个Db_Query类了,这个类是Typecho数据库查询语句构建类。注释里面也给出了详细的使用方法:

$query = new Typecho_Db_Query();    //或者使用DB积累的sql方法返回实例化对象
$query->select('posts', 'post_id, post_title')
->where('post_id = %d', 1)
->limit(1);
echo $query;
打印的结果将是
SELECT post_id, post_title FROM posts WHERE 1=1 AND post_id = 1 LIMIT 1

进入这个类,最先看到的是定义了数据库的关键字以及默认的字段,还有一些其他的私有变量,可以暂时不管,后面自然会遇到。

在这个类的最后,定义了__toString()方法,应该是用于生成SQL查询语句用的,方便我们输出。

首先从构造函数开始吧,传入的参数是一个Typecho_Db_Adapter的对象,还有表前缀。然后给本类的一些变量赋了值。别忘了这个Typecho_Db_Adapter只是个抽象类,在本系列的最后分析执行机制以及流程的时候再从整体来看,这里我们先关心每个小部分都是干啥的,最后再整合起来看。

filterPrefix()的作用是将$string中的前缀table.,替换为$this->prefix中存储的表前缀。
filterColumn()这个函数有点复杂,我也看的云里雾里的,大概就是过滤数组中的键值,传入的参数为待处理的字段,如果想知道这个函数的具体作用,建议把这个函数拎出来单独跑一下,看看结果就清除了。
getColumnFromParameters()这个函数就比较简单了,它是把给定的参数处理成查询字段。

剩下需要注意的就是那些以SQL关键词作为函数名的函数了,例如where()等。
这些应该算是这个类中比较关键的函数了吧,通读这几个函数的实现,可以发现都操作了一个叫做_sqlPreBuild的数组,offset()函数直接将偏移量放入了_sqlPreBuild['offset']这里,其他的函数处理方法类似,例如_sqlPreBuild['limit']等,不同的是有些是放入了二维数组中,即对应的值又是个数组,比如join()等。最后都返回了$this。而这个$_sqlPreBuild数组则是parseSelect()中实际处理的,将其组合成完整的SQL语句,如果想通过代码审计挖掘SQL注入漏洞的话,监控这一部分是最好的选择,可以看到都执行了些什么样子的SQL查询,进行了何种过滤等等。

数据库部分到这里就分析完了,接着可能会去分析Config那部分,当然也有可能是别的。