Typecho 源码笔记 数据库部分
早就想分析一下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
的变量,在Typecho
中private
的变量名均由_
开头。
$_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::READ
和self: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 Reporting
和throw Exception
的PDO
属性,但是很不幸,这个init()
又是abstract
,那只能等一下再分析了。
接下来是query()
函数,这个函数实际上就是用了prepare
和execute
,最后返回resource
。另外,如果传进来的$query
是Typecho_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
的问题,然后再处理limit
和offset
,最后进行拼接,没看出来有什么安全问题,才疏学浅。。。
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.php
和Mysql.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
那部分,当然也有可能是别的。