【万字审计】Thinkphp3.2.x nday深度利用
前言 起初,师傅发了我一个题目,我分析了好久也没分析出来,便搁置了很久,在前不久又搬出来分析了,于是便有了这篇文章,这个题目的框架是thinkphp3.2.x的源码,然后在入口处自己添加了一些东西,所以分析起来比较麻烦,所以我把整个分析和利用过程都写出来了,也是花了一个月,然后这里是站库分离的,所以打起来比较麻烦,不能用sql来直接读 分析 入口IndexController,class,php
前言
起初,师傅发了我一个题目,我分析了好久也没分析出来,便搁置了很久,在前不久又搬出来分析了,于是便有了这篇文章,这个题目的框架是thinkphp3.2.x的源码,然后在入口处自己添加了一些东西,所以分析起来比较麻烦,所以我把整个分析和利用过程都写出来了,也是花了一个月,然后这里是站库分离的,所以打起来比较麻烦,不能用sql来直接读
分析
入口IndexController,class,php
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller
{
public function index()
{
$cid = I('cid', 0,'intval');
if($cid == 0) $this->show('<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} body{ background: #fff; font-family: "微软雅黑"; color: #b80202;font-size:24px} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.8em; font-size: 36px } a,a:hover{color:blue;}</style><div style="padding: 24px 48px;"> <h1>:)</h1><p>欢迎使用 <b>ThinkPHP</b>!</p><br/>版本 V{$Think.version}</div><script type="text/javascript" src="http://ad.topthink.com/Public/static/client.js"></script><thinkad id="ad_55e75dfae343f5a1"></thinkad><script type="text/javascript" src="http://tajs.qq.com/stats?sId=9347272" charset="UTF-8"></script>','utf-8');
else {
$this->page($cid);
}
}
public function page($cid = 0){
if($cid == 0) $this->error('error');
$content = M('Articles')->Field('content')->find($cid);
$this->show(base64_encode($content['content']),'utf-8');
}
}
(在路由处打了两个断点):
IndexController.class.php -> function Index() -> 调用 I 方法 I
I ('cid',0,'intval');
function I()
/**
* 获取输入参数 支持过滤和默认值
* 使用方法:
* <code>
* I('id',0); 获取id参数 自动判断get或者post
* I('post.name','','htmlspecialchars'); 获取$_POST['name']
* I('get.'); 获取$_GET
* </code>
* @param string $name 变量的名称 支持指定类型
* @param mixed $default 不存在的时候默认值
* @param mixed $filter 参数过滤方法
* @param mixed $datas 要获取的额外数据源
* @return mixed
*/
function I($name, $default = '', $filter = null, $datas = null)
{
static $_PUT = null;
if (strpos($name, '/')) // 指定修饰符
list($name, $type) = explode('/', $name, 2);
} elseif (C('VAR_AUTO_STRING')) {
// 默认强制转换为字符串
$type = 's';
}
if (strpos($name, '.')) {
// 指定参数来源
list($method, $name) = explode('.', $name, 2);
} else {
// 默认为自动判断
$method = 'param';
}
switch (strtolower($method)) {
case 'get':
$input = &$_GET;
break;
case 'post':
$input = &$_POST;
break;
case 'put':
if (is_null($_PUT)) {
parse_str(file_get_contents('php://input'), $_PUT);
}
$input = $_PUT;
break;
case 'param':
switch ($_SERVER['REQUEST_METHOD']) {
case 'POST':
$input = $_POST;
break;
case 'PUT':
if (is_null($_PUT)) {
parse_str(file_get_contents('php://input'), $_PUT);
}
$input = $_PUT;
break;
default:
$input = $_GET;
}
break;
case 'path':
$input = array();
if (!empty($_SERVER['PATH_INFO'])) {
$depr = C('URL_PATHINFO_DEPR');
$input = explode($depr, trim($_SERVER['PATH_INFO'], $depr));
}
break;
case 'request':
$input = &$_REQUEST;
break;
case 'session':
$input = &$_SESSION;
break;
case 'cookie':
$input = &$_COOKIE;
break;
case 'server':
$input = &$_SERVER;
break;
case 'globals':
$input = &$GLOBALS;
break;
case 'data':
$input = &$datas;
break;
default:
return null;
}
if ('' == $name) {
// 获取全部变量
$data = $input;
$filters = isset($filter) ? $filter : C('DEFAULT_FILTER');
if ($filters) {
if (is_string($filters)) {
$filters = explode(',', $filters);
}
foreach ($filters as $filter) {
$data = array_map_recursive($filter, $data); // 参数过滤
}
}
} elseif (isset($input[$name])) {
// 取值操作
$data = $input[$name];
$filters = isset($filter) ? $filter : C('DEFAULT_FILTER');
if ($filters) {
if (is_string($filters)) {
if (0 === strpos($filters, '/')) {
if (1 !== preg_match($filters, (string) $data)) {
// 支持正则验证
return isset($default) ? $default : null;
}
} else {
$filters = explode(',', $filters);
}
} elseif (is_int($filters)) {
$filters = array($filters);
}
if (is_array($filters)) {
foreach ($filters as $filter) {
if (function_exists($filter)) {
$data = is_array($data) ? array_map_recursive($filter, $data) : $filter($data); // 参数过滤
} else {
$data = filter_var($data, is_int($filter) ? $filter : filter_id($filter));
if (false === $data) {
return isset($default) ? $default : null;
}
}
}
}
}
if (!empty($type)) {
switch (strtolower($type)) {
case 'a': // 数组
$data = (array) $data;
break;
case 'd': // 数字
$data = (int) $data;
break;
case 'f': // 浮点
$data = (float) $data;
break;
case 'b': // 布尔
$data = (boolean) $data;
break;
case 's':// 字符串
default:
$data = (string) $data;
}
}
} else {
// 变量默认值
$data = isset($default) ? $default : null;
}
is_array($data) && array_walk_recursive($data, 'think_filter');
return $data;
}
可以看见传入的值$\name=\$cid, default=0,datas=null
我们把值带进去走一遍
首先,cid传入后先进行了一个判断
if (strpos($name, '/'))
strpos函数就是一个字符串函数
用法:
strpos(string
$haystack, string$needle, int$offset= 0): int|false
string haystack是被查找的字符串,string needle 是需要查找的字符串
所以在这里面就是在$name里面查找/返回索引值,若没找到那就是返回false
所以这里传参进去,这里返回的会是false因为$name = cid不带有 /
所以这里直接跳到elseif处
可以省略不看
看到这里了索性就去看了下explod()函数和list函数:
list($name, $type) = explode('/', $name, 2);
前面的strpos将$name中的\检测出来,若存在则将\$name中的\分离出来,并且最大分离为2
跟进下面,若传入的name没有分割符,则直接跳到Function C
Function C()
/**
* 获取和设置配置参数 支持批量定义
* @param string|array $name 配置变量
* @param mixed $value 配置值
* @param mixed $default 默认值
* @return mixed
*/
function C($name = null, $value = null, $default = null)
{
static $_config = array();
// 无参数时获取所有
if (empty($name)) {
return $_config;
}
// 优先执行设置获取或赋值
if (is_string($name)) {
if (!strpos($name, '.')) {
$name = strtoupper($name);
if (is_null($value)) {
return isset($_config[$name]) ? $_config[$name] : $default;
}
$_config[$name] = $value;
return null;
}
// 二维数组设置和获取支持
$name = explode('.', $name);
$name[0] = strtoupper($name[0]);
if (is_null($value)) {
return isset($_config[$name[0]][$name[1]]) ? $_config[$name[0]][$name[1]] : $default;
}
$_config[$name[0]][$name[1]] = $value;
return null;
}
// 批量设置
if (is_array($name)) {
$_config = array_merge($_config, array_change_key_case($name, CASE_UPPER));
return null;
}
return null; // 避免非法参数
}
首先看见function c中有三个参数需要传入分别为:$name、$value、default
VAR_AUTO_STRING将$name强制转化为string类型,
第一个if判断
传入(string)cid,- > 判断是否为字符串 - >如果没有.号 - > 利用strtoupper函数将\$name 转化为大写,
第二个if
判断value值是否为null,因为前面没有传递value,所以根据函数定义默认为null,所以返回ture,所以就到 - >return isset(\$_config[\$name]) \? \$_config[\$name] : \$default; 三元运算符,
这里var_dump了一下发现是没有CID这个键值的所以最后将CID的键值赋值为null了。
因为这里强制转换了字符串所以就不会触发二维数组的设置了,然后就到批量设置
若$name是数组,则利用array_change_key_case函数将name所有的键值转化为大写,然后讲这些内容合并到\$_config里面,
若不是数组则直接执行return null;
然后回到function I()里面继续走
这里把 #type = 's'了
然后到后面的if判断,因为传入的name是cid没有带.号所以直接跳到else后面
直接把 #method设置为'param' 默认为自动判断类型
然后后面的switch对请求方法进行了判断也就是判断请求方式是什么
然后这里有个特殊的点,当param='put'时
parse_str(file_get_contents('php://input'), $_PUT);
首先是file_get_contents函数读取来自php的输入流,然后通过parse_str()函数将查询字符串解析为变量,然后存在$_PUT数组中
但是这里因为method直接默认为paramter
所以直接跳入 param 的 case,这里先让它默认判断为get
这里debug了一下分为两个过程
1、如果不传值 -> is_array(\$data) &\& array_walk_recursive($data, 'think_filter');
2、如果传入值 cid=1
elseif (isset($input[$name])) {
// 取值操作
$data = $input[$name]; // $name:"cid" $input:{cid => "1"}["1"]
$filters = isset($filter) ? $filter : C('DEFAULT_FILTER'); //$filter: "intval"
if ($filters) {
if (is_string($filters)) {
if (0 === strpos($filters, '/')) { $filter: "intval"
if (1 !== preg_match($filters, (string) $data)) {
// 支持正则验证
return isset($default) ? $default : null;
}
} else {
$filters = explode(',', $filters);
}
} elseif (is_int($filters)) {
$filters = array($filters);
}
if (is_array($filters)) {
foreach ($filters as $filter) {
if (function_exists($filter)) {
$data = is_array($data) ? array_map_recursive($filter, $data) : $filter($data); // 参数过滤
} else {
$data = filter_var($data, is_int($filter) ? $filter : filter_id($filter));
if (false === $data) {
return isset($default) ? $default : null;
}
}
}
}
}
if (!empty($type)) {
switch (strtolower($type)) {
case 'a': // 数组
$data = (array) $data;
break;
case 'd': // 数字
$data = (int) $data;
break;
case 'f': // 浮点
$data = (float) $data;
break;
case 'b': // 布尔
$data = (boolean) $data;
break;
case 's':// 字符串
default:
$data = (string) $data;
}
}
}
做了 #filter= intval 的过滤,也就是只能传入整数型数字
跳到对于filter的判断,$filter中没有/所以不会跳入preg_matc进行正则,
继续跟进:
if (is_array($filters)) //$filter: {"intval"}["intval"]
判断完成后,遍历filter的值(这里var_dump了一下,只有
array(1) {[0] = > string(6)"intval"}
filter函数存在
并且debug发现data的值为1不为数组,所以进入三元运算
function array_map_recurisive()
这里的array_map_recurisive()函数为:
function array_map_recursive($filter, $data)
{
$result = array();
foreach ($data as $key => $val) {
$result[$key] = is_array($val)
? array_map_recursive($filter, $val)
: call_user_func($filter, $val);
}
return $result;
}
- 这个函数有两个参数: $filter和 $data。 $filer是一个回调函数,用于处理数组中的每个元素;\$data则是需要处理的多维数组。
- 函数先创建了一个$result变量来存储处理后的结果。
- 然后,通过'foreach'循环遍历输入的
$data数组,对于数组中的每个元素执行以下操作:- 如果当前遍历到的元素
$val是一个数组返回true,那么就会对这个子数组递归调用array\_map\_recurisive函数,并且将字数组的每个元素传递给$filter处理。 - 如果遍历到的元素不是数组,那么就将这个元素传递给filter进行处理。
- 如果当前遍历到的元素
- 最后传回result里面。
最后到
is_array(\$data) \&& array_walk_recursive($data, 'think_filter');
显而易见,这里返回的是false
最后返回$data = 1;
到这里,整个I函数的数据流就分析完了
前面的I函数就走完了,接下来就是
if($cid == 0) $this->show('<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} body{ background: #fff; font-family: "微软雅黑"; color: #b80202;font-size:24px} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.8em; font-size: 36px } a,a:hover{color:blue;}</style><div style="padding: 24px 48px;"> <h1>:)</h1><p>欢迎使用 <b>ThinkPHP</b>!</p><br/>版本 V{$Think.version}</div><script type="text/javascript" src="http://ad.topthink.com/Public/static/client.js"></script><thinkad id="ad_55e75dfae343f5a1"></thinkad><script type="text/javascript" src="http://tajs.qq.com/stats?sId=9347272" charset="UTF-8"></script>','utf-8');
else {
$this->page($cid);
}
先传一个cid = 0;开始跟进
大概流程: -> show()->view->display()->G();->listen()->fetch()->getThemePath->getTemplateTheme->C();
function show()
protected function show($content, $charset = '', $contentType = '', $prefix = '')
{
$this->view->display('', $charset, $contentType, $content, $prefix);
}
调用了view属性,找了一下继承的控制器Controller.class.php,发现到了这个属性
protected $view = null;
可以看到上面给show传递了\$content和\$charset
所以给display传值
再来看一下display函数如何定义的
function display()
public function display($templateFile = '', $charset = '', $contentType = '', $content = '', $prefix = '')
{
G('viewStartTime');
// 视图开始标签
Hook::listen('view_begin', $templateFile);
// 解析并获取模板内容
$content = $this->fetch($templateFile, $content, $prefix);
// 输出模板内容
$this->render($content, $charset, $contentType);
// 视图结束标签
Hook::listen('view_end');
}
跟进到G函数
function G()
/**
* 记录和统计时间(微秒)和内存使用情况
* 使用方法:
* <code>
* G('begin'); // 记录开始标记位
* // ... 区间运行代码
* G('end'); // 记录结束标签位
* echo G('begin','end',6); // 统计区间运行时间 精确到小数后6位
* echo G('begin','end','m'); // 统计区间内存使用情况
* 如果end标记位没有定义,则会自动以当前作为标记位
* 其中统计内存使用需要 MEMORY_LIMIT_ON 常量为true才有效
* </code>
* @param string $start 开始标签
* @param string $end 结束标签
* @param integer|string $dec 小数位或者m
* @return mixed
*/
function G($start, $end = '', $dec = 4)
{
static $_info = array();
static $_mem = array();
if (is_float($end)) {
// 记录时间
$_info[$start] = $end;
} elseif (!empty($end)) {
// 统计时间和内存使用
if (!isset($_info[$end])) {
$_info[$end] = microtime(true);
}
if (MEMORY_LIMIT_ON && 'm' == $dec) {
if (!isset($_mem[$end])) {
$_mem[$end] = memory_get_usage();
}
return number_format(($_mem[$end] - $_mem[$start]) / 1024);
} else {
return number_format(($_info[$end] - $_info[$start]), $dec);
}
} else {
// 记录时间和内存使用
$_info[$start] = microtime(true);
if (MEMORY_LIMIT_ON) {
$_mem[$start] = memory_get_usage();
}
}
return null;
}
参数:
G(\$start='viewStartTime',\$end='', $dec = 4)
在$_info、$_mem_后面var_dump了一下,发现都是一些时间和内存的记录
因为end默认是string所以直接跳到
$_info[$start] = microtime(true);
if (MEMORY_LIMIT_ON) {
$_mem[$start] = memory_get_usage();
}
根据前面注释可以知道,其实就是记录和统计时间(微秒)和内存使用情况
最后return了null所以就继续跟进
可以看到这里利用了一个Hook的机制(刚好不太懂所以就去顺便学习了一下)
Hook::listen('view_begin', $templateFile);
跟进去 - > listen函数
function listen()
public static function listen($tag, &$params = null)
{
if (isset(self::$tags[$tag])) {
if (APP_DEBUG) {
G($tag . 'Start');
trace('[ ' . $tag . ' ] --START--', '', 'INFO');
}
foreach (self::$tags[$tag] as $name) {
APP_DEBUG && G($name . '_start');
$result = self::exec($name, $tag, $params);
if (APP_DEBUG) {
G($name . '_end');
trace('Run ' . $name . ' [ RunTime:' . G($name . '_start', $name . '_end', 6) . 's ]', '', 'INFO');
}
if (false === $result) {
// 如果返回false 则中断插件执行
return;
}
}
if (APP_DEBUG) {
// 记录行为的执行日志
trace('[ ' . $tag . ' ] --END-- [ RunTime:' . G($tag . 'Start', $tag . 'End', 6) . 's ]', '', 'INFO');
}
}
return;
}
这里view_begin不存在这个键,所以直接跳到ruturn
所以跟进到:
$content = $this->fetch($templateFile, $content, $prefix);
function fetch()
/**
* 解析和获取模板内容 用于输出
* @access public
* @param string $templateFile 模板文件名
* @param string $content 模板输出内容
* @param string $prefix 模板缓存前缀
* @return string
*/
public function fetch($templateFile = '', $content = '', $prefix = '')
{
if (empty($content)) {
$templateFile = $this->parseTemplate($templateFile);
// 模板文件不存在直接返回
if (!is_file($templateFile)) {
E(L('_TEMPLATE_NOT_EXIST_') . ':' . $templateFile);
}
} else {
defined('THEME_PATH') or define('THEME_PATH', $this->getThemePath());
}
// 页面缓存
ob_start();
ob_implicit_flush(0);
if ('php' == strtolower(C('TMPL_ENGINE_TYPE'))) {
// 使用PHP原生模板
$_content = $content;
// 模板阵列变量分解成为独立变量
extract($this->tVar, EXTR_OVERWRITE);
// 直接载入PHP模板
empty($_content) ? include $templateFile : eval('?>' . $_content);
} else {
// 视图解析标签
$params = array('var' => $this->tVar, 'file' => $templateFile, 'content' => $content, 'prefix' => $prefix);
Hook::listen('view_parse', $params);
}
// 获取并清空缓存
$content = ob_get_clean();
// 内容过滤标签
Hook::listen('view_filter', $content);
// 输出模板文件
return $content;
}
1、content不为空->
defined('THEME_PATH') or define('THEME_PATH', $this->getThemePath());
因为跟进后发现进入了getThemePath函数,所以可以判断‘THEME_PATH’没有被定义
function getThemePath()
/**
* 获取当前的模板路径
* @access protected
* @param string $module 模块名
* @return string
*/
protected function getThemePath($module = MODULE_NAME)
{
// 获取当前主题名称
$theme = $this->getTemplateTheme();
// 获取当前主题的模版路径
$tmplPath = C('VIEW_PATH'); // 模块设置独立的视图目录
if (!$tmplPath) {
// 定义TMPL_PATH 则改变全局的视图目录到模块之外
$tmplPath = defined('TMPL_PATH') ? TMPL_PATH . $module . '/' : APP_PATH . $module . '/' . C('DEFAULT_V_LAYER') . '/';
}
return $tmplPath . $theme;
}
我们接下来跟进 function getThemePath - > getTemplateTheme();
function getTemplateTheme()
private function getTemplateTheme()
{
if ($this->theme) {
// 指定模板主题
$theme = $this->theme;
} else {
/* 获取模板主题名称 */
$theme = C('DEFAULT_THEME');
if (C('TMPL_DETECT_THEME')) {
// 自动侦测模板主题
$t = C('VAR_TEMPLATE');
if (isset($_GET[$t])) {
$theme = $_GET[$t];
} elseif (cookie('think_template')) {
$theme = cookie('think_template');
}
if (!in_array($theme, explode(',', C('THEME_LIST')))) {
$theme = C('DEFAULT_THEME');
}
cookie('think_template', $theme, 864000);
}
}
defined('THEME_NAME') || define('THEME_NAME', $theme); // 当前模板主题名称
return $theme ? $theme . '/' : '';
}
}
然后这个函数其实就是一直在调用C函数进行配置的读取,后面考虑到这里的参数都不可控,所以直接跳到后面的page路由了
这里就提一嘴:
如果在index路由进行传参会被intval过滤器进行过滤,所以我们直接对page路由进行传参就行了(虽然前面花了大部分时间分析,但是后面有调用到前面的函数就可以很好的去分析了)
page路由
public function page($cid = 0){
if($cid == 0) $this->error('error');
$content = M('Articles')->Field('content')->find($cid);
$this->show(base64_encode($content['content']),'utf-8');
}
避免抛出error所以我们直接传一个cid=1
function M()
/**
* 实例化一个没有模型文件的Model
* @param string $name Model名称 支持指定基础模型 例如 MongoModel:User
* @param string $tablePrefix 表前缀
* @param mixed $connection 数据库连接信息
* @return Think\Model
*/
function M($name = '', $tablePrefix = '', $connection = '')
{
static $_model = array();
if (strpos($name, ':')) {
list($class, $name) = explode(':', $name);
} else {
$class = 'Think\\Model';
}
$guid = (is_array($connection) ? implode('', $connection) : $connection) . $tablePrefix . $name . '_' . $class;
if (!isset($_model[$guid])) {
$_model[$guid] = new $class($name, $tablePrefix, $connection);
}
return $_model[$guid];
}
$name = 'Articles'
- var_dump($_model)之后发现: array(0){ },也就是为空,用于存储已创建的模型实例。
- 如果传入的$name参数中包含冒号,则使用explode进拆分为
\$class和\$name两部分 - 如果
$name中不包含冒号,则默认$class= 'Think\\Model'; 表示使用tp的基础模型类 - 根据传入的参数构建了一个用于唯一标识模型实例的
$guid字符串 - 检查
$_model数组中是否已经存在对应的模型实例,如果不存在则创建一个新的模型实例并存储在$_model数组之中 - 最后返回
$guid的模型实例
跟进之后发现直接进入了autoload函数
这个地方有点不懂,所以问了一下gpt
gpt是这样解释的:
在PHP中,当使用`new $class`语法创建一个类的实例时,如果该类在当前命名空间或引入的命名空间中没有定义,PHP会调用自动加载类尝试加载此类文件
也就是自动加载Think\\Model这个类
跟进到
$_model[$guid] = new $class($name, $tablePrefix, $connection);
可以看到传参:
$guid:"Articles_Think|Model" $class:"Think\Model" $_model: array[0] $connection = " "
$tablePrefix: " " $name:"Articles"
function autoload()
* 类库自动加载
* @param string $class 对象类名
* @return void
*/
public static function autoload($class)
{
// 检查是否存在映射
if (isset(self::$_map[$class])) {
include self::$_map[$class];
} elseif (false !== strpos($class, '\\')) {
$name = strstr($class, '\\', true);
if (in_array($name, array('Think', 'Org', 'Behavior', 'Com', 'Vendor')) || is_dir(LIB_PATH . $name)) {
// Library目录下面的命名空间自动定位
$path = LIB_PATH;
} else {
// 检测自定义命名空间 否则就以模块为命名空间
$namespace = C('AUTOLOAD_NAMESPACE');
$path = isset($namespace[$name]) ? dirname($namespace[$name]) . '/' : APP_PATH;
}
$filename = $path . str_replace('\\', '/', $class) . EXT;
if (is_file($filename)) {
// Win环境下面严格区分大小写
if (IS_WIN && false === strpos(str_replace('/', '\\', realpath($filename)), $class . EXT)) {
return;
}
include $filename;
}
} elseif (!C('APP_USE_NAMESPACE')) {
// 自动加载的类库层
foreach (explode(',', C('APP_AUTOLOAD_LAYER')) as $layer) {
if (substr($class, -strlen($layer)) == $layer) {
if (require_cache(MODULE_PATH . $layer . '/' . $class . EXT)) {
return;
}
}
}
// 根据自动加载路径设置进行尝试搜索
foreach (explode(',', C('APP_AUTOLOAD_PATH')) as $path) {
if (import($path . '.' . $class))
// 如果加载类成功则返回
{
return;
}
}
}
}
代码逻辑:
- 首先,检查是否存在类名到文件路径的映射关系。如果存在,直接包含该文件。
- 如果类名包含命名空间
\,则会根据命名空间的规则尝试定位类文件- 如果类名的空间属于
Think、Org、Behavior、Com、Vendor或者这些命名空间在LIB_PATH目录下存在,则将此类文件路径设置为LIB_PATH - 否则,检查是否有自定义的命名空间配置,并根据配置的路径加载文件。
- 如果以上步骤均未找到对应的类文件,将以当前模块为命名空间进行加载。
- 如果类名的空间属于
- 如果未使用名空间(
C('APP_USE_NAMESPACE')为false)- 根据配置的类库层(
C('APP_AUTOLOAD_LAYER')) 尝试加载类文件 - 尝试根据配置的自动加载路径(
C('APP_AUTOLOAD_PATH'))进行加载
- 根据配置的类库层(
- 最后,如果成功加载类文件,则返回,否则结束函数执行
我在autoload函数开头加了一个
echo "autoload函数被调用切值为" . $class;
并且在映射判断处加了一个
echo "映射存在";
最后得到的结果就是:
autoload被调用了12次,分别是:
Think\Storage
Think\Storage\Driver\File
Think\Log //映射存在并且被包含
Behavior\ReadHtmlCacheBehavior
Home\Controller\IndexController
Think\Model //映射存在并且被包含
Think\Db //映射存在并且被包含
Think\Db\Driver\Mysql
Think\Db\Driverfield
Behavior\WriteHtmlCacheBehavior
Behavior\ShowPageTraceBehavior
Think\Log\Driver\File //映射存在并且被包含
经过了autoload之后就进入了Model.class.php的model类
所以自动调用到__construct函数
function __construct()
public function __construct($name = '', $tablePrefix = '', $connection = '')
{
// 模型初始化
$this->_initialize();
// 获取模型名称
if (!empty($name)) {
if (strpos($name, '.')) {
// 支持 数据库名.模型名的 定义
list($this->dbName, $this->name) = explode('.', $name);
} else {
$this->name = $name;
}
} elseif (empty($this->name)) {
$this->name = $this->getModelName();
}
// 设置表前缀
if (is_null($tablePrefix)) {
// 前缀为Null表示没有前缀
$this->tablePrefix = '';
} elseif ('' != $tablePrefix) {
$this->tablePrefix = $tablePrefix;
} elseif (!isset($this->tablePrefix)) {
$this->tablePrefix = C('DB_PREFIX');
}
// 数据库初始化操作
// 获取数据库操作对象
// 当前模型有独立的数据库连接信息
$this->db(0, empty($this->connection) ? $connection : $this->connection, true);
}
先进入 __initialize进行初始化
function _initialize()
// 回调方法 初始化模型
protected function _initialize()
{}
传参:
$name : string(8) "Articles" ,\$tablePrefix: string(0) "", \$connection :string(0) ""
逻辑:
- 设置模型的名称:
- 根据传入的
$name参数设置模型的名称, - 如传入的模型中包含
.,则按照数据库名.模型名的格式解析出数据库名和模型名。 - 如果传入的模型名或者未在构造函数中设置模型名称,则使用
getModelNmae()方法获取模型名称.
- 根据传入的
- 设置表前缀:
- 如果传入的
$tablePrefix参数为null,则表示没有表前缀。 - 如果
$tablePrefix不为空字符串,则表前缀设置为传入的值。 - 如果未设置
$tablePrefix且模型实例中未定义表前缀,则使用配置文件中的DB_PREFIX设置为表前缀。
- 如果传入的
- 数据库初始化操作:
- 调用了
db()方法,获取数据库操作对象。 - 如果当前模型有独立的数据库连接信息,则使用独立连接的信息,否则使用构造函数传入的链接信息。
- 最后一个
true,表示强制重新连接数据库。
- 调用了
插一嘴(表前缀是什么?): (Table Prefix)是指在数据库中表明前添加的一个固定的字符或字符串,通常用于区分不同应用或模块所创建的表,以避免不同应用或者模块之间的表名冲突。
跟进之后,模型的名称就为Articles、表前缀为null,接下来就走到db()方法
function db()
/**
* 切换当前的数据库连接
* @access public
* @param integer $linkNum 连接序号
* @param mixed $config 数据库连接信息
* @param boolean $force 强制重新连接
* @return Model
*/
public function db($linkNum = '', $config = '', $force = false)
{
if ('' === $linkNum && $this->db) {
return $this->db;
}
if (!isset($this->_db[$linkNum]) || $force) {
// 创建一个新的实例
if (!empty($config) && is_string($config) && false === strpos($config, '/')) {
// 支持读取配置参数
$config = C($config);
}
$this->_db[$linkNum] = Db::getInstance($config);
} elseif (null === $config) {
$this->_db[$linkNum]->close(); // 关闭数据库连接
unset($this->_db[$linkNum]);
return;
}
// 切换数据库连接
$this->db = $this->_db[$linkNum];
$this->_after_db();
// 字段检测
if (!empty($this->name) && $this->autoCheckFields) {
$this->_checkTableInfo();
}
return $this;
}
逻辑:
- 如果未传入连接编号
#$linkNum并且当前模型对象已经存在数据库连接对象$this->db,则直接返回已存在的数据库连接对象 - 如果未创建对应连接编号的数据库对象或者设置了强制创建实例(
$force为true),则根据传入的配置信息$config创建一个新的数据库连接实例,并存储在_db数组中 - 如果传入了
null作为$config的参数,则关闭对应连接编号的数据库连接,并将该连接从_db数组中移除。 - 将当前数据库连接对象,设为刚创建或者已经存在的连接对象。
- 在数据库切换后,如果设置了模型名称并且使用了自动字段检测(
$this->autoCheckFields= true)则执行_checkTableInfo()方法进行字段检查 - 最后,返回当前模型对象本身,以便进行链式操作
dump了一下this(单纯想看一下有什么):
class Think\Model#7 (21) { protected $db => NULL private $_db => array(0) { } protected $pk => string(2) "id" protected $autoinc => bool(false) protected $tablePrefix => string(0) "" protected $name => string(8) "Articles" protected $dbName => string(0) "" protected $connection => string(0) "" protected $tableName => string(0) "" protected $trueTableName => string(0) "" protected $error => string(0) "" protected $fields => array(0) { } protected $data => array(0) { } protected $options => array(0) { } protected $_validate => array(0) { } protected $_auto => array(0) { } protected $_map => array(0) { } protected $_scope => array(0) { } protected $autoCheckFields => bool(true) protected $patchValidate => bool(false) protected $methods => array(14) { [0] => string(6) "strict" [1] => string(5) "order" [2] => string(5) "alias" [3] => string(6) "having" [4] => string(5) "group" [5] => string(4) "lock" [6] => string(8) "distinct" [7] => string(4) "auto" [8] => string(6) "filter" [9] => string(8) "validate" [10] => string(6) "result" [11] => string(5) "token" [12] => string(5) "index" [13] => string(5) "force" } }
db方法后面静态调用了getInstance,因为要加载一个类,所以自动会跳到前面分析过得autoload方法
* 类库自动加载
* @param string $class 对象类名
* @return void
*/
public static function autoload($class)
{
// 检查是否存在映射
if (isset(self::$_map[$class])) {
include self::$_map[$class];
} elseif (false !== strpos($class, '\\')) {
$name = strstr($class, '\\', true);
if (in_array($name, array('Think', 'Org', 'Behavior', 'Com', 'Vendor')) || is_dir(LIB_PATH . $name)) {
// Library目录下面的命名空间自动定位
$path = LIB_PATH;
} else {
// 检测自定义命名空间 否则就以模块为命名空间
$namespace = C('AUTOLOAD_NAMESPACE');
$path = isset($namespace[$name]) ? dirname($namespace[$name]) . '/' : APP_PATH;
}
$filename = $path . str_replace('\\', '/', $class) . EXT;
if (is_file($filename)) {
// Win环境下面严格区分大小写
if (IS_WIN && false === strpos(str_replace('/', '\\', realpath($filename)), $class . EXT)) {
return;
}
include $filename;
}
} elseif (!C('APP_USE_NAMESPACE')) {
// 自动加载的类库层
foreach (explode(',', C('APP_AUTOLOAD_LAYER')) as $layer) {
if (substr($class, -strlen($layer)) == $layer) {
if (require_cache(MODULE_PATH . $layer . '/' . $class . EXT)) {
return;
}
}
}
// 根据自动加载路径设置进行尝试搜索
foreach (explode(',', C('APP_AUTOLOAD_PATH')) as $path) {
if (import($path . '.' . $class))
// 如果加载类成功则返回
{
return;
}
}
}
}
自动加载完类后进入getInstance方法
function getInstance()
/**
* 取得数据库类实例
* @static
* @access public
* @param mixed $config 连接配置
* @return Object 返回数据库驱动类
*/
public static function getInstance($config = array())
{
$md5 = md5(serialize($config));
if (!isset(self::$instance[$md5])) {
// 解析连接参数 支持数组和字符串
$options = self::parseConfig($config);
// 兼容mysqli
if ('mysqli' == $options['type']) {
$options['type'] = 'mysql';
}
// 如果采用lite方式 仅支持原生SQL 包括query和execute方法
$class = !empty($options['lite']) ? 'Think\Db\Lite' : 'Think\\Db\\Driver\\' . ucwords(strtolower($options['type']));
if (class_exists($class)) {
self::$instance[$md5] = new $class($options);
} else {
// 类没有定义
E(L('_NO_DB_DRIVER_') . ': ' . $class);
}
}
self::$_instance = self::$instance[$md5];
return self::$_instance;
}
- 计算配置信息的MD5值作为连接实例的唯一标识,用于检查是否已经存在对应数据库连接实例
- 如果尚未创建特定配置信息对应数据库连接实例,将解析给定的连接配置信息,并跟进配置信息选择相应的数据库驱动类。
- 根据配置信息中指定的数据库类型,动态加载对应的数据库驱动类,这里有一个兼容的处理,如果数据库类型是mysqli,则会转化为mysql。
- 如果对应的数据库驱动类存在,则创建一个新的数据库连接实例,并存储在静态数组
self::$instance中。 - 如果特定的数据库驱动类不存在,则会抛出一个异常,提示数据库驱动类未定义。
- 最后返回创建的数据库连接实例
跟进后得到的结果$md5:"14d71035552fef7c92e4b5c611232830" $config=" "
跟进到 parseConfig方法
function parseConfig()
/**
* 数据库连接参数解析
* @static
* @access private
* @param mixed $config
* @return array
*/
private static function parseConfig($config)
{
if (!empty($config)) {
if (is_string($config)) {
return self::parseDsn($config);
}
$config = array_change_key_case($config);
$config = array(
'type' => $config['db_type'],
'username' => $config['db_user'],
'password' => $config['db_pwd'],
'hostname' => $config['db_host'],
'hostport' => $config['db_port'],
'database' => $config['db_name'],
'dsn' => isset($config['db_dsn']) ? $config['db_dsn'] : null,
'params' => isset($config['db_params']) ? $config['db_params'] : null,
'charset' => isset($config['db_charset']) ? $config['db_charset'] : 'utf8',
'deploy' => isset($config['db_deploy_type']) ? $config['db_deploy_type'] : 0,
'rw_separate' => isset($config['db_rw_separate']) ? $config['db_rw_separate'] : false,
'master_num' => isset($config['db_master_num']) ? $config['db_master_num'] : 1,
'slave_no' => isset($config['db_slave_no']) ? $config['db_slave_no'] : '',
'debug' => isset($config['db_debug']) ? $config['db_debug'] : APP_DEBUG,
'lite' => isset($config['db_lite']) ? $config['db_lite'] : false,
);
} else {
$config = array(
'type' => C('DB_TYPE'),
'username' => C('DB_USER'),
'password' => C('DB_PWD'),
'hostname' => C('DB_HOST'),
'hostport' => C('DB_PORT'),
'database' => C('DB_NAME'),
'dsn' => C('DB_DSN'),
'params' => C('DB_PARAMS'),
'charset' => C('DB_CHARSET'),
'deploy' => C('DB_DEPLOY_TYPE'),
'rw_separate' => C('DB_RW_SEPARATE'),
'master_num' => C('DB_MASTER_NUM'),
'slave_no' => C('DB_SLAVE_NO'),
'debug' => C('DB_DEBUG', null, APP_DEBUG),
'lite' => C('DB_LITE'),
);
}
return $config;
}
这是一个私有的静态方法。
逻辑:
- 接收一个数据库连接配置数组
$config或从配置文件中读取数据库连接配置信息 - 如果传入了非空的配置数组
$config,则跟进数组键名不区分大小写的特性,将数组键名统一转化为小写,并从给定的配置数组中提取数据库连接所需要的参数,参数包括:- 数据库类型
- 用户名
- 主机名
- 密码
- 主机端口
- 数据库名
- 数据库DSN
- 连接参数
- 字符集
- 数据库部署方式
- 读写分离
- 主服务器数量
- 从服务器编号
- 调试模式
- 是否使用轻量级数据库
- 如果没有传入配置数组
$config,则从系统的全局之中读取相同的数据库连接参数,即用上面所说的C()方法进行获取 - 返回整理后的数据库连接配置数组
然后因为上面的$config为空,所以直接利用C()方法去获取配置。
获取一下然后dump出来看看
array(15) {
'type' => string(5) "mysql"
'username' => string(4) "root"
'password' => string(7) "root123"
'hostname' => string(9) "127.0.0.1"
'hostport' => int(3306)
'database' => string(3) "web"
'dsn' => NULL
'params' => array(0) { }
'charset' => string(4) "utf8"
'deploy' => int(0)
'rw_separate' => bool(false)
'master_num'=> int(1)
'slave_no' => string(0) ""
'debug' => bool(true)
lite' => NULL }
回到db方法,
在几个判断处打了一个断点且用echo测试
发现:
- type为mysql不需要转化
$option['lite']为空即$class的值为Think\Db\Driver\Mysql$clas被定义且静态调用instance方法,实例化一个对象
然后就到了Drive.class.php中的__construct方法
这个时候有点好奇为什么不是经过autoload方法而是直接进入Driver类,
就去问了一下师傅,说是经历过autoload加载后的类就不需要再autoload了,所以就直接进入对应的类。(使用get_decleared_classes就可以看见被声明过的类了)
function __construct() in Drive.class.php
/**
* 架构函数 读取数据库配置信息
* @access public
* @param array $config 数据库配置数组
*/
public function __construct($config = '')
{ echo "Drive类的construct方法被调用!\r\n"; //自己添加用于测试
if (!empty($config)) {
$this->config = array_merge($this->config, $config);
if (is_array($this->config['params'])) {
$this->options = $this->config['params'] + $this->options;
}
}
}
逻辑
- 如果传入了
$config参数,会将传入的配置信息与当前实例的配置信息进行合并,$config参数可以包含数据库连接需要的各种参数 - 如果
$this->$config['params']是一个数组,会将其合并到$this->option数组中。
然后同样dump了一下,
$this->$config:
array(17) { 'type' => string(5) "mysql"
'hostname' => string(9) "127.0.0.1"
'database' => string(3) "web"
'username' => string(4) "root"
'password' => string(7) "root123"
'hostport' => int(3306)
'dsn' => NULL
'params' => array(0) { }
'charset' => string(4) "utf8"
'prefix' => string(0) ""
'debug' => bool(true)
'deploy' => int(0)
'rw_separate' => bool(false)
'master_num' => int(1)
'slave_no' => string(0) ""
'db_like_fields' => string(0) ""
'lite' => NULL
}
$this->options
array(4) {
[8] => int(2)
[3] => int(2)
[11] => int(0)
[17] => bool(false)
}
跟进后返回到db方法
// 切换数据库连接
$this->db = $this->_db[$linkNum];
$this->_after_db();
// 字段检测
if (!empty($this->name) && $this->autoCheckFields) {
$this->_checkTableInfo();
}
return $this;
dump出$this->db的值
D:\phpstudy_pro\WWW\N1start\tp\ThinkPHP\Library\Think\Model.class.php:1620: class Think\Db\Driver\Mysql#
8 (17) {
protected $PDOStatement => NULL
protected $model => string(7) "_think_"
protected $queryStr => string(0) ""
protected $modelSql => array(0) { }
protected $lastInsID => NULL
protected $numRows => int(0)
protected $transTimes => int(0)
protected $error => string(0) ""
protected $linkID => array(0) { }
protected $_linkID => NULL
protected $config => array(17) {
'type' => string(5) "mysql"
'hostname' => string(9) "127.0.0.1"
'database' => string(3) "web"
'username' => string(4) "root"
'password' => string(7) "root123"
'hostport' => int(3306)
'dsn' => NULL
'params' => array(0) { }
'charset' => string(4) "utf8"
'prefix' => string(0) ""
'debug' => bool(true)
'deploy' => int(0)
'rw_separate' => bool(false)
'master_num' => int(1)
'slave_no' => string(0) ""
'db_like_fields' => string(0) ""
'lite' => NULL }
protected $exp => array(14) {
'eq' => string(1) "="
'neq' => string(2) "<>"
'gt' => string(1) ">"
'egt' => string(2) ">="
'lt' => string(1) "<"
'elt' => string(2) "<="
'notlike' => string(8) "NOT LIKE"
'like' => string(4) "LIKE"
'in' => string(2) "IN"
'notin' => string(6) "NOT IN"
'not in' => string(6) "NOT IN"
'between' => string(7) "BETWEEN"
'not between' => string(11) "NOT BETWEEN"
'notbetween' => string(11) "NOT BETWEEN" }
protected $selectSql => string(109) "SELECT%DISTINCT% %FIELD% FROM %TABLE%%FORCE%%JOIN%%WHERE%%GROUP%%HAVING%%ORDER%%LIMIT% %UNION%%LOCK%%COMMENT%" protected $queryTimes => int(0)
protected $executeTimes => int(0)
protected $options => array(4) {
[8] => int(2)
[3] => int(2)
[11] => int(0)
[17] => bool(false)
}
protected $bind => array(0) { }
}
进入_checkTableInfo方法
funtion _checkTableInfo()
/**
* 自动检测数据表信息
* @access protected
* @return void
*/
protected function _checkTableInfo()
{
// 如果不是Model类 自动记录数据表信息
// 只在第一次执行记录
if (empty($this->fields)) {
// 如果数据表字段没有定义则自动获取
if (C('DB_FIELDS_CACHE')) {
$db = $this->dbName ?: C('DB_NAME');
$fields = F('_fields/' . strtolower($db . '.' . $this->tablePrefix . $this->name));
if ($fields) {
$this->fields = $fields;
if (!empty($fields['_pk'])) {
$this->pk = $fields['_pk'];
}
return;
}
}else{
echo "配置不存在!\r\n";//自己加的
}
// 每次都会读取数据表信息
$this->flush();
}
}
C('DB_FIELDS_CACHE')
这个用来获取数据库字段缓存配置的函数或方法调用。
然后这里直接跳出了判断说明对应的配置值为空
那就紧接着进入到flush方法
function flush()
/**
* 获取字段信息并缓存
* @access public
* @return void
*/
public function flush()
{
// 缓存不存在则查询数据表信息
$this->db->setModel($this->name);
$fields = $this->db->getFields($this->getTableName());
if (!$fields) {
// 无法获取字段信息
return false;
}
$this->fields = array_keys($fields);
unset($this->fields['_pk']);
foreach ($fields as $key => $val) {
// 记录字段类型
$type[$key] = $val['type'];
if ($val['primary']) {
// 增加复合主键支持
if (isset($this->fields['_pk']) && null != $this->fields['_pk']) {
if (is_string($this->fields['_pk'])) {
$this->pk = array($this->fields['_pk']);
$this->fields['_pk'] = $this->pk;
}
$this->pk[] = $key;
$this->fields['_pk'][] = $key;
} else {
$this->pk = $key;
$this->fields['_pk'] = $key;
}
if ($val['autoinc']) {
$this->autoinc = true;
}
}
}
// 记录字段类型信息
$this->fields['_type'] = $type;
// 2008-3-7 增加缓存开关控制
if (C('DB_FIELDS_CACHE')) {
// 永久缓存数据表信息
$db = $this->dbName ?: C('DB_NAME');
F('_fields/' . strtolower($db . '.' . $this->tablePrefix . $this->name), $this->fields);
}
}
逻辑:
- 首先,该方法通过数据库操作对象 $this->db 获取当前数据表的字段信息。
- 如果缓存中不存在字段信息,会通过数据库查询获取数据表的字段信息。 如果成功获取到数据表的字段信息,则会对字段进行处理,提取字段名并记录字段类型。同时,如果存在主键字段,则会将主键字段信息记录下来。
- 根据配置项 C('DB_FIELDS_CACHE') 的值来决定是否开启数据库字段信息的缓存功能。如果配置项为真(即开启状态),则将字段信息以永久缓存的方式存储起来,使用了 F('_fields/' . strtolower($db . '.' . $this->tablePrefix . $this->name), $this->fields) 进行缓存。缓存的键名包含了数据库名称、表前缀和表名等信息,以确保缓存的唯一性和准确性。
跟进一下进入setModel方法
function setModel()
public function setModel($model)
{
$this->model = $model;
}
传入的model是
string(8) "Articles"
然后进入getTableName方法
function getTableName()
/**
* 得到完整的数据表名
* @access public
* @return string
*/
public function getTableName()
{ echo "getTableName方法被调用!\r\n";
if (empty($this->trueTableName)) {
$tableName = !empty($this->tablePrefix) ? $this->tablePrefix : '';
if (!empty($this->tableName)) {
$tableName .= $this->tableName;
} else {
$tableName .= parse_name($this->name);
}
$this->trueTableName = strtolower($tableName);
}
return (!empty($this->dbName) ? $this->dbName . '.' : '') . $this->trueTableName;
}
逻辑
- 当调用该方法时,会先判断是否已经存在缓存的数据表名(
$this->trueTableName)。如果存在,则直接返回缓存的表名,无需重新生成。 - 如果不存在缓存的表名,会根据以下规则生成数据表名:
- 如果设置了表前缀($this->tablePrefix),将其添加到表名中。
- 如果设置了具体的表名($this->tableName),则使用该表名。
- 如果没有设置具体的表名,则将模型名称($this->name)进行转换,然后作为表名。
- 最后,将生成的表名转换为小写,并存储在 $this->trueTableName 中缓存起来。
- 返回格式化后的数据表名。如果设置了数据库名($this->dbName),则返回 数据库名.表名 的格式,否则只返回表名。
$this->trueTableName为空所以进入到if判断中,因为传入的tablePrefix也为空所以$tableName = ""
这里的判断有点不理解所以就自己写了一个例子:
<?php
$a = "";
if(!empty($a)){
echo "TRUE";
}else{
echo "FALSE";
}
?>
a存在的时候为TRUE,不存在(NULL)的时候为FALSE
所以
$tableName .= parse_name($this->name);
这里直接进入parse_name方法(.=表示字符串拼接赋值)
function parse_name()
/**
* 字符串命名风格转换
* type 0 将Java风格转换为C的风格 1 将C风格转换为Java的风格
* @param string $name 字符串
* @param integer $type 转换类型
* @return string
*/
function parse_name($name, $type = 0)
{
if ($type) {
return ucfirst(preg_replace_callback('/_([a-zA-Z])/', function ($match) {return strtoupper($match[1]);}, $name));
} else {
return strtolower(trim(preg_replace("/[A-Z]/", "_\\0", $name), "_"));
}
}
逻辑:
- 当
$type为真时,执行驼峰命名到下划线命名的转化:- 使用正则表达式
/_([a-zA-Z])/匹配下划线跟着一个字母的情况。 - 使用
preg_replace_callback()方法进行匹配替换,将下划线后的字符转化为大写形式。 - 最后,使用
unfirst方法将首字母转化为大写。
- 使用正则表达式
- 当
$type为假的时候,执行下划线命名到驼峰命名的转化。- 使用正则表达式
/[A-Z]/匹配大写字母的情况。 - 使用
preg_reoplace方法将匹配到的大写字母替换为_加上相同的大写字母 - 使用
trim()方法去除字符串首尾的_ - 最后,使用
strtolower方法将字符串转化为小写
- 使用正则表达式
然后注意到了这里有个匿名函数:
function ($match) {return strtoupper($match[1]);}
前面使用了preg_replacer_callback()方法,这个匿名函数被用做回调函数
$match是preg_replacer_callback()方法传递给回调函数的匹配结果数组。在这里,他表示正则表达式匹配到的结果。$match[1]包含了正则表达式中括号内匹配到的内容,即一个小写字母。strtoupper($match[1])将匹配到的小写字母转换为大写字母
最后转化完之后为:articles
最后这个方法返回值为:articles
随后进入getfield方法
function getfield()
/**
* 取得数据表的字段信息
* @access public
*/
public function getFields($tableName)
{
$this->initConnect(true);
list($tableName) = explode(' ', $tableName);
if (strpos($tableName, '.')) {
list($dbName, $tableName) = explode('.', $tableName);
$sql = 'SHOW COLUMNS FROM `' . $dbName . '`.`' . $tableName . '`';
} else {
$sql = 'SHOW COLUMNS FROM `' . $tableName . '`';
}
$result = $this->query($sql);
$info = array();
if ($result) {
foreach ($result as $key => $val) {
if (\PDO::CASE_LOWER != $this->_linkID->getAttribute(\PDO::ATTR_CASE)) {
$val = array_change_key_case($val, CASE_LOWER);
}
$info[$val['field']] = array(
'name' => $val['field'],
'type' => $val['type'],
'notnull' => (bool) ('' === $val['null']), // not null is empty, null is yes
'default' => $val['default'],
'primary' => (strtolower($val['key']) == 'pri'),
'autoinc' => (strtolower($val['extra']) == 'auto_increment'),
);
}
}
return $info;
}
逻辑:
- 初始化数据库连接。
- 解析传入的 $tableName 参数,以便准备构建 SQL 查询语句。如果 $tableName 包含了数据库名称和数据表名称,则将其分离出来,以便构建查询语句。如果没有数据库名称,则默认在当前数据库中查询。
- 根据构建好的 SQL 查询语句,执行 SHOW COLUMNS FROM 查询获取表的字段信息。
- 对查询结果进行处理,遍历结果集并将每个字段的相关信息存储在数组 $info 中:
- 字段名作为数组的键;
- 字段的相关信息(如类型、是否为主键、是否自增等)作为数组的值。
- 返回存储字段信息的数组 $info。
根据上面的传参可以知道这里进入getfield方法的参数为:“articles‘,然后进行初始化数据库连接(即进入initConnect方法)
function initConnect()
/**
* 初始化数据库连接
* @access protected
* @param boolean $master 主服务器
* @return void
*/
protected function initConnect($master = true)
{
if (!empty($this->config['deploy']))
// 采用分布式数据库
{
$this->_linkID = $this->multiConnect($master);
} else
// 默认单数据库
if (!$this->_linkID) {
$this->_linkID = $this->connect();
}
}
- 首先,检查配置中是否启用了数据库的分布式部署
- 如果分配了分布式数据库,则用
multiConnect方法来建立数据库的连接 - 如果为设置分布式数据库,或者分布式数据库连接为
false,则通过connect方法建立单个数据库连接
这里通过给的文件可以看出来,肯定是属于单数据库的,所以我们跟进到connect方法进行初始化
function connect()
/**
* 连接数据库方法
* @access public
*/
public function connect($config = '', $linkNum = 0, $autoConnection = false)
{
if (!isset($this->linkID[$linkNum])) {
if (empty($config)) {
$config = $this->config;
}
try {
if (empty($config['dsn'])) {
$config['dsn'] = $this->parseDsn($config);
}
if (version_compare(PHP_VERSION, '5.3.6', '<=')) {
// 禁用模拟预处理语句
$this->options[PDO::ATTR_EMULATE_PREPARES] = false;
}
$this->linkID[$linkNum] = new PDO($config['dsn'], $config['username'], $config['password'], $this->options);
} catch (\PDOException $e) {
if ($autoConnection) {
trace($e->getMessage(), '', 'ERR');
return $this->connect($autoConnection, $linkNum);
} elseif ($config['debug']) {
E($e->getMessage());
}
}
}
return $this->linkID[$linkNum];
}
- 因为前面的调用的时候并没有传参,所以这里三个参数的默认值不变
- 首先判断之前指定的连接编号的数据库有无建立连接,若无,则创建新的连接
- 先获取了配置信息
$configarray(17) { 'type' => string(5) "mysql" 'hostname' => string(9) "127.0.0.1" 'database' => string(3) "web" 'username' => string(4) "root" 'password' => string(7) "root123" 'hostport' => int(3306) 'dsn' => NULL 'params' => array(0) { }· 'charset' => string(4) "utf8" 'prefix' => string(0) "" 'debug' => bool(true) 'deploy' => int(0) 'rw_separate' => bool(false) 'master_num' => int(1) 'slave_no' => string(0) "" 'db_like_fields' => string(0) "" 'lite' => NULL }
- 进入到获取DSN中,这里我就直接把获取的配置dump出来了,就不进去进行分析了
string(54) "mysql:dbname=web;host=127.0.0.1;port=3306;charset=utf8" - 这里的判断version也是没有过去,所以就跳到下面的
new PDO了 - 在连接过程中,捕获可能出现的
PDOException异常,如果异常被捕获到,根据情况做以下处理- 如果启动了自动连接
$autoconnection为真,则记录异常信息并尝试重新连接数据库 - 如果没有启动自动连接,并且配置中的调试模式开启,则抛出异常信息
- 如果启动了自动连接
- 最后返回建立的数据库连接对象
$this->linkID[$linkNum],或者在异常的时候抛出false
建立连接后我们继续跟进getfield方法
这里的$tableName没有.号所以这里直接跳到else,然后定义了sql语句
$sql = 'SHOW COLUMNS FROM
' . $tableName . '';
跟进query方法
function query()
/**
* 执行查询 返回数据集
* @access public
* @param string $str sql指令
* @param boolean $fetchSql 不执行只是获取SQL
* @return mixed
*/
public function query($str, $fetchSql = false)
{
$this->initConnect(false);
if (!$this->_linkID) {
return false;
}
$this->queryStr = $str;
if (!empty($this->bind)) {
$that = $this;
$this->queryStr = strtr($this->queryStr, array_map(function ($val) use ($that) {return '\'' . $that->escapeString($val) . '\'';}, $this->bind));
}
if ($fetchSql) {
return $this->queryStr;
}
//释放前次的查询结果
if (!empty($this->PDOStatement)) {
$this->free();
}
$this->queryTimes++;
N('db_query', 1); // 兼容代码
// 调试开始
$this->debug(true);
$this->PDOStatement = $this->_linkID->prepare($str);
if (false === $this->PDOStatement) {
$this->error();
return false;
}
foreach ($this->bind as $key => $val) {
if (is_array($val)) {
$this->PDOStatement->bindValue($key, $val[0], $val[1]);
} else {
$this->PDOStatement->bindValue($key, $val);
}
}
$this->bind = array();
try {
$result = $this->PDOStatement->execute();
// 调试结束
$this->debug(false);
if (false === $result) {
$this->error();
return false;
} else {
return $this->getResult();
}
} catch (\PDOException $e) {
$this->error();
return false;
}
}
- 这里再次调用了一次initconnect方法进行初始化数据库,所以后面的逻辑不在说明
$this->queryStr = $str;这里将查询语句赋值给queryStr属性- 这里跟进代码后直接跳过了两个if判断也就是说明
$fetchSqlandPDOStatement都不存在或者为null(也就是说明上次查询的结果为空,Fetchsql默认为false所以这里直接过了) - queryTimes++也就是查询次数+1
- 进入N方法
function N()
/**
* 设置和获取统计数据
* 使用方法:
* <code>
* N('db',1); // 记录数据库操作次数
* N('read',1); // 记录读取次数
* echo N('db'); // 获取当前页面数据库的所有操作次数
* echo N('read'); // 获取当前页面读取次数
* </code>
* @param string $key 标识位置
* @param integer $step 步进值
* @param boolean $save 是否保存结果
* @return mixed
*/
function N($key, $step = 0, $save = false)
{ echo "N方法被调用!!!\r\n";
static $_num = array();
if (!isset($_num[$key])) {
$_num[$key] = (false !== $save) ? S('N_' . $key) : 0;
}
if (empty($step)) {
return $_num[$key];
} else {
$_num[$key] = $_num[$key] + (int) $step;
}
if (false !== $save) {
// 保存结果
S('N_' . $key, $_num[$key], $save);
}
return null;
}
- 先定义了一个数组
$_num(guess是用来保存计数器类的东西) - dump了一下
$_num[$key]的值,为NULL所以可以进入到下面的三元运算符 $save默认为false所以这里最后的值为0- 然后调用的时候传入的参数
$step=1所以进入else- 将
$_num[$key]的值改为step的值
- 将
- 最后没有保存结果返回了null
继续回到query方法 - 进入debug方法
### function debug()
/**
* 数据库调试 记录当前SQL
* @access protected
* @param boolean $start 调试开始标记 true 开始 false 结束
*/
protected function debug($start)
{
if ($this->config['debug']) {
// 开启数据库调试模式
if ($start) {
G('queryStartTime');
} else {
$this->modelSql[$this->model] = $this->queryStr;
//$this->model = '_think_';
// 记录操作结束时间
G('queryEndTime');
trace($this->queryStr . ' [ RunTime:' . G('queryStartTime', 'queryEndTime') . 's ]', '', 'SQL');
}
}
}
- 传参
$start = true - 这里G的调用前面分析过了就是用来记录和统计时间和内存使用情况的
- 最后就跳出debug
回到query() 没绷住这里来回跳跃.jpg
$this->PDOStatement = $this->_linkID->prepare($str);
- 利用
PDO::prepare方法准备查询语句,将绑定的参数$sql绑定到查询中。
同样的我dump出来:这里把查询语句搬出来一下:class PDOStatement#10 (1) {| public $queryString => string(57) "SELECT `content` FROM `articles` WHERE `id` = 1 LIMIT 1 " }
select SELECT content FROM articles WHERE id = 1 LIMIT 1
so,这个地方的$this->PDOStatement不为false,直接走下去
foreach ($this->bind as $key => $val) {
if (is_array($val)) {
$this->PDOStatement->bindValue($key, $val[0], $val[1]);
} else {
$this->PDOStatement->bindValue($key, $val);
}
}
这段debug的时候直接跳了就不分析了
- 重新给
$this->bind赋值为0 - 关闭了debug(可能是不在需要记录调试信息)
$result = 1->return $this->getResult()
function getResult()
/**
* 获得所有的查询数据
* @access private
* @return array
*/
private function getResult()
{
//返回数据集
$result = $this->PDOStatement->fetchAll(PDO::FETCH_ASSOC);
$this->numRows = count($result);
return $result;
}
$this->PDOStatement->fetchAll(PDO::FETCH_ASSOC);fetchALL是PDOStatement对象的方法,用于从数据库中获取查询的结构集PDO::FETCH_ASSOC是一个常量,表示返回的结果以关联数组的形式进行返回,其中键名是列名。
$this->numRows = count($result);- 查询结果中的行数存储在
$this->numRows这个类属性中。
这里最后就返回了结果,也就是我们现在回到getFields方法继续进行分析
然后这里我就把返回的结果dump一下
array(3) {
[0] =>
array(6) {
'field' =>
string(2) "id"
'type' =>
string(7) "int(11)"
'null' =>
string(2) "NO"
'key' =>
string(3) "PRI"
'default' =>
NULL
'extra' =>
string(14) "auto_increment"
}
[1] =>
array(6) {
'field' =>
string(7) "content"
'type' =>
string(4) "text"
'null' =>
string(3) "YES"
'key' =>
string(0) ""
'default' =>
NULL
'extra' =>
string(0) ""
}
[2] =>
array(6) {
'field' =>
string(5) "title"
'type' =>
string(4) "text"
'null' =>
string(3) "YES"
'key' =>
string(0) ""
'default' =>
NULL
'extra' =>
string(0) ""
}
}
- 先定义个空的数组
$info用来存储数据 - 利用遍历循环,遍历了数据库查询返回的结果集
$result。每个$var变量代表一个记录,存储了字段的信息。 if (\PDO::CASE_LOWER != $this->_linkID->getAttribute(\PDO::ATTR_CASE))\PDO::CASE_LOWER是PDO类的一个常量,表示键名转化为小写。PDO::ATTR_CASE用于获取键名大小写转化的模式
- 所以这个逻辑就是:首先检查当前数据库连接的属性,确定数据库是否对键名大小写敏感,如果对键名大小写不敏感则转化为小写(即最后保存在数组中的数据都是小写)
dump一下info
array(3) {
'id' =>
array(6) {
'name' =>
string(2) "id"
'type' =>
string(7) "int(11)"
'notnull' =>
bool(false)
'default' =>
NULL
'primary' =>
bool(true)
'autoinc' =>
bool(true)
}
'content' =>
array(6) {
'name' =>
string(7) "content"
'type' =>
string(4) "text"
'notnull' =>
bool(false)
'default' =>
NULL
'primary' =>
bool(false)
'autoinc' =>
bool(false)
}
'title' =>
array(6) {
'name' =>
string(5) "title"
'type' =>
string(4) "text"
'notnull' =>
bool(false)
'default' =>
NULL
'primary' =>
bool(false)
'autoinc' =>
bool(false)
}
}
那么到这里就返回了内容后回到flush方法继续跟进
- 传回info之后赋值给
fields - 调用
$this→field赋值为fileld的name值 - dump:
array(3) {
[0] =>
string(2) "id"
[1] =>
string(7) "content"
[2] =>
string(5) "title"
}
利用unset销毁
$this→fields[’_pk’](这里应该是局部变量)循环遍历数据库查询结果中的字段信息。
将字段类型记录到
$type数组中,以字段名为键名。dump值:
string(7) "int(11)" string(4) "text" string(4) "text"
判断当前是否是主键字段
- 如果已经存在主键字段(
$this->fields['_pk']),并且不为空,则表示当前表是复合主键。 - 如果主键是字符串类型,将其转换为数组形式,并重新赋值给
$_pk。 - 将当前字段名追加到主键数组中,并更新
_pk字段信息。
- 如果已经存在主键字段(
如果不存在主键字段,则将当前字段名作为主键字段。
如果当前字段具有自动增长属性(
$val['autoinc']为真),将$this->autoinc设置为真,表示主键是自动增长的。这里debug了一下发现只有一个主键,所以后面两个值不是主键。并且第一个不是复合主键
在循环之后记录字段类型信息,dump(其实也就是前面循环遍历处理来的值)
array(3) {
'id' => string(7) "int(11)"
'content' => string(4) "text"
'title' => string(4) "text"
}
- 通过C方法判断数据库字段缓存是否开启,若开启则永久保存数据表信息(debug直接跳过了所以没有开启)
然后这里就把这些参数一一传回去,然后到这里为止M方法走完了,接下来就进入了field方法field('content')
function field()
/**
* 指定查询字段 支持字段排除
* @access public
* @param mixed $field
* @param boolean $except 是否排除
* @return Model
*/
public function field($field, $except = false)
{ echo "field函数被调用!!\r\n";
if (true === $field){
// 获取全部字段
$fields = $this->getDbFields();
$field = $fields ?: '*';
} elseif ($except) {
// 字段排除
if (is_string($field)) {
$field = explode(',', $field);
}
$fields = $this->getDbFields();
$field = $fields ? array_diff($fields, $field) : $field;
}
$this->options['field'] = $field;
return $this;
}
- 因为传入的
field值不是bool值并且$except = false所以直接到最后面两个语句最后return的结果是:$this->options['field'] = $field; return $this;
之后就到find函数了(这里就有可控参数class Think\Model#7 (21) { protected $db => class Think\Db\Driver\Mysql#8 (17) { protected $PDOStatement => class PDOStatement#10 (1) { public $queryString => string(28) "SHOW COLUMNS FROM `articles`" } protected $model => string(8) "Articles" protected $queryStr => string(28) "SHOW COLUMNS FROM `articles`" protected $modelSql => array(1) { 'Articles' => string(28) "SHOW COLUMNS FROM `articles`" } protected $lastInsID => NULL protected $numRows => int(3) protected $transTimes => int(0) protected $error => string(0) "" protected $linkID => array(1) { [0] => class PDO#9 (0) { ... } } protected $_linkID => class PDO#9 (0) { } protected $config => array(17) { 'type' => string(5) "mysql" 'hostname' => string(9) "127.0.0.1" 'database' => string(3) "web" 'username' => string(4) "root" 'password' => string(7) "root123" 'hostport' => int(3306) 'dsn' => NULL 'params' => array(0) { ... } 'charset' => string(4) "utf8" 'prefix' => string(0) "" 'debug' => bool(true) 'deploy' => int(0) 'rw_separate' => bool(false) 'master_num' => int(1) 'slave_no' => string(0) "" 'db_like_fields' => string(0) "" 'lite' => NULL } protected $exp => array(14) { 'eq' => string(1) "=" 'neq' => string(2) "<>" 'gt' => string(1) ">" 'egt' => string(2) ">=" 'lt' => string(1) "<" 'elt' => string(2) "<=" 'notlike' => string(8) "NOT LIKE" 'like' => string(4) "LIKE" 'in' => string(2) "IN" 'notin' => string(6) "NOT IN" 'not in' => string(6) "NOT IN" 'between' => string(7) "BETWEEN" 'not between' => string(11) "NOT BETWEEN" 'notbetween' => string(11) "NOT BETWEEN" } protected $selectSql => string(109) "SELECT%DISTINCT% %FIELD% FROM %TABLE%%FORCE%%JOIN%%WHERE%%GROUP%%HAVING%%ORDER%%LIMIT% %UNION%%LOCK%%COMMENT%" protected $queryTimes => int(1) protected $executeTimes => int(0) protected $options => array(5) { [8] => int(2) [3] => int(2) [11] => int(0) [17] => bool(false) [1002] => string(14) "SET NAMES utf8" } protected $bind => array(0) { } } private $_db => array(1) { [0] => class Think\Db\Driver\Mysql#8 (17) { protected $PDOStatement => class PDOStatement#10 (1) { ... } protected $model => string(8) "Articles" protected $queryStr => string(28) "SHOW COLUMNS FROM `articles`" protected $modelSql => array(1) { ... } protected $lastInsID => NULL protected $numRows => int(3) protected $transTimes => int(0) protected $error => string(0) "" protected $linkID => array(1) { ... } protected $_linkID => class PDO#9 (0) { ... } protected $config => array(17) { ... } protected $exp => array(14) { ... } protected $selectSql => string(109) "SELECT%DISTINCT% %FIELD% FROM %TABLE%%FORCE%%JOIN%%WHERE%%GROUP%%HAVING%%ORDER%%LIMIT% %UNION%%LOCK%%COMMENT%" protected $queryTimes => int(1) protected $executeTimes => int(0) protected $options => array(5) { ... } protected $bind => array(0) { ... } } } protected $pk => string(2) "id" protected $autoinc => bool(true) protected $tablePrefix => string(0) "" protected $name => string(8) "Articles" protected $dbName => string(0) "" protected $connection => string(0) "" protected $tableName => string(0) "" protected $trueTableName => string(8) "articles" protected $error => string(0) "" protected $fields => array(5) { [0] => string(2) "id" [1] => string(7) "content" [2] => string(5) "title" '_pk' => string(2) "id" '_type' => array(3) { 'id' => string(7) "int(11)" 'content' => string(4) "text" 'title' => string(4) "text" } } protected $data => array(0) { } protected $options => array(1) { 'field' => string(7) "content" } protected $_validate => array(0) { } protected $_auto => array(0) { } protected $_map => array(0) { } protected $_scope => array(0) { } protected $autoCheckFields => bool(true) protected $patchValidate => bool(false) protected $methods => array(14) { [0] => string(6) "strict" [1] => string(5) "order" [2] => string(5) "alias" [3] => string(6) "having" [4] => string(5) "group" [5] => string(4) "lock" [6] => string(8) "distinct" [7] => string(4) "auto" [8] => string(6) "filter" [9] => string(8) "validate" [10] => string(6) "result" [11] => string(5) "token" [12] => string(5) "index" [13] => string(5) "force" } }
$cid了)
### function find()
/**
* 查询数据
* @access public
* @param mixed $options 表达式参数
* @return mixed
*/
public function find($options = array())
{
if (is_numeric($options) || is_string($options)) {
$where[$this->getPk()] = $options;
$options = array();
$options['where'] = $where;
}
// 根据复合主键查找记录
$pk = $this->getPk();
if (is_array($options) && (count($options) > 0) && is_array($pk)) {
// 根据复合主键查询
$count = 0;
foreach (array_keys($options) as $key) {
if (is_int($key)) {
$count++;
}
}
if (count($pk) == $count) {
$i = 0;
foreach ($pk as $field) {
$where[$field] = $options[$i];
unset($options[$i++]);
}
$options['where'] = $where;
} else {
return false;
}
}
// 总是查找一条记录
$options['limit'] = 1;
// 分析表达式
$options = $this->_parseOptions($options);
// 判断查询缓存
if (isset($options['cache'])) {
$cache = $options['cache'];
$key = is_string($cache['key']) ? $cache['key'] : md5(serialize($options));
$data = S($key, '', $cache);
if (false !== $data) {
$this->data = $data;
return $data;
}
}
$resultSet = $this->db->select($options);
if (false === $resultSet) {
return false;
}
if (empty($resultSet)) {
// 查询结果为空
return null;
}
if (is_string($resultSet)) {
return $resultSet;
}
// 读取数据后的处理
$data = $this->_read_data($resultSet[0]);
$this->_after_find($data, $options);
if (!empty($this->options['result'])) {
return $this->returnResult($data, $this->options['result']);
}
$this->data = $data;
if (isset($cache)) {
S($key, $data, $cache);
}
return $this->data;
}
- 首先将当前对象的
$options设置为空数组,而$options的值为:string(1) "2" - 首先通过
is_numeric()和is_string()函数对参数$option进行检查。如果传入的参数是数字或者字符类型,就会假设这是要根据主键进行查询
function getPk()
/**
* 获取主键名称
* @access public
* @return string
*/
public function getPk()
{ echo "getPk方法被调用!!\r\n";
var_dump($this->pk);
return $this->pk;
}
(有些东西自己debug的时候加的)
这里返回的当前对象的主键
string(2) "id"
其实也就是将$options赋值给了这个where["id"]
- 如果传入的参数是数字或字符串类型,他会创建一个
$where数组,并用主键作为键,传入的参数为值,然后这个$where数组放入$options['where']中
dump->$options['where']:
array(1) {
'id' =>
string(1) "2"
}
其实就是交换了一下并且转化为数组的形式(我的理解)
- 接着就是将当前对象利用
getPk方法获取当前对象的主键赋值给$pk变量
dump(虽然知道和前面是一样的但是我还是写出来一下,主打一个仔细):
string(2) "id"
接下来进入下一个判断
- 判断
$options是否为数组且不为空,并且检查$pk是否为数组(也就是复合主键) - 先将count赋值为0,利用
arrary_key函数返回$options数组中所有的键或键的子集(也就是里面的where和id吧) - 如果某个键是整数(
is_int($key)成立),则递增$count。 - 把增加完的
count与$pk进行比较判断是否数量相同 - 使用循环将复合主键字段和对应的值存储到
$where数组中,并从$options中移除相应的元素。 将
$where数组赋值给$options['where'],若前面的count不匹配则返回false接下来设置了
$options[‘limit’] = 1也就是只能返回一条记录
### function _parseOptions/** * 分析表达式 * @access protected * @param array $options 表达式参数 * @return array */ protected function _parseOptions($options = array()) { if (is_array($options)) { $options = array_merge($this->options, $options); } if (!isset($options['table'])) { // 自动获取表名 $options['table'] = $this->getTableName(); $fields = $this->fields; } else { // 指定数据表 则重新获取字段列表 但不支持类型检测 $fields = $this->getDbFields(); } // 数据表别名 if (!empty($options['alias'])) { $options['table'] .= ' ' . $options['alias']; } // 记录操作的模型名称 $options['model'] = $this->name; // 字段类型验证 if (isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join'])) { // 对数组查询条件进行字段类型检查 foreach ($options['where'] as $key => $val) { $key = trim($key); if (in_array($key, $fields, true)) { if (is_scalar($val)) { $this->_parseType($options['where'], $key); } } elseif (!is_numeric($key) && '_' != substr($key, 0, 1) && false === strpos($key, '.') && false === strpos($key, '(') && false === strpos($key, '|') && false === strpos($key, '&')) { if (!empty($this->options['strict'])) { E(L('_ERROR_QUERY_EXPRESS_') . ':[' . $key . '=>' . $val . ']'); } unset($options['where'][$key]); } } } // 查询过后清空sql表达式组装 避免影响下次查询 $this->options = array(); // 表达式过滤 $this->_options_filter($options); return $options; }
- 根据上面传递进来的值,dump出来看一下传入了哪些东西
```php
array(2) {
'where' =>
array(1) {
'id' =>
string(1) "2"
}
'limit' =>
int(1)
}
当前对象的`options`:
```php
array(1) {
'field' =>
string(7) "content"
}
接下来来分析逻辑
- 判断传入的
$options是否是数组(当然这里直接看就知道是数组),使用arrat_merge讲当前对象的options和传入的options进行合并
合并之后:
```php
array(3) {
'field' =>
string(7) "content"
'where' =>
array(1) {
'id' =>
string(1) "2"
}
'limit' =>
int(1)
}
- 当前没有传入表名,所以会自动通过`getTableName()`方法进行自动获取表名,这个方法上面已经分析过了,这里就不再次分析了
- 获取完表名之后将当前对象的`$field`赋值给了变量`$fields`
当前对象`$fields`
```php
array(5) {
[0] =>
string(2) "id"
[1] =>
string(7) "content"
[2] =>
string(5) "title"
'_pk' =>
string(2) "id"
'_type' =>
array(3) {
'id' =>
string(7) "int(11)"
'content' =>
string(4) "text"
'title' =>
string(4) "text"
}
}
- 否则调用
getDbFields方法来赋值
function getDbFields()
/**
* 获取数据表字段信息
* @access public
* @return array
*/
public function getDbFields()
{
if (isset($this->options['table'])) {
// 动态指定表名
if (is_array($this->options['table'])) {
$table = key($this->options['table']);
} else {
$table = $this->options['table'];
if (strpos($table, ')')) {
// 子查询
return false;
}
}
$fields = $this->db->getFields($table);
return $fields ? array_keys($fields) : false;
}
if ($this->fields) {
$fields = $this->fields;
unset($fields['_type'], $fields['_pk']);
return $fields;
}
return false;
}
- 首先判断当前对象的表名(键名)是否设置了
- 若有设置:
- 判断 table是否是数组,获取数组的键作为表名,如果是字符串那么就直接设置为表名,如果里面包含了'('则认为是子查询直接返回false
- 使用数据库连接对象的
getFields方法获取字段信息,这个方法在之前也分析过一次了 - 如果成功获取到字段信息,就返回字段名的数组,否则返回false
- 若没有设置:
- 检查当前对象的fields是否存在,如果定义了,就返回字段列表,移除
_typeand_pk
- 检查当前对象的fields是否存在,如果定义了,就返回字段列表,移除
- 判断['alias']这个键值是否存在(也就是数据库别名) 就会将表名与别名结合起来进行查询(当然通过前面的dump可以看出来是没有别名的)
- 记录当前操作模型的名称传入
model的值(Articles) - 判断
[where]是否存在且是否为数组;[$field]存在且[join]不存在,则对数组查询条件进行字段类型检查 - 首先遍历
where的值,先利用trim进行去除空格,判断$key是否存在于$fields之中(下面dump出来好分析),array(5) { [0] => string(2) "id" [1] => string(7) "content" [2] => string(5) "title" '_pk' => string(2) "id" '_type' => array(3) { 'id' => string(7) "int(11)" 'content' => string(4) "text" 'title' => string(4) "text" } }
这里的进入判断类型之前还有一个判断,这里忘记说了,这里的in_array检查$key是否存在于数组fields中,且true表示要执行严格的比较,比较完之后才要判断下面的东西
- 判断键名的值是否是标量(标量变量是包含int、 float、string或bool的变量。类型array、object、resources 和null不是标量。)很明显是的
- 进入_praseTyoe方法
function _parseType
/**
* 数据类型检测
* @access protected
* @param mixed $data 数据
* @param string $key 字段名
* @return void
*/
protected function _parseType(&$data, $key)
{
if (!isset($this->options['bind'][':' . $key]) && isset($this->fields['_type'][$key])) {
$fieldType = strtolower($this->fields['_type'][$key]);
if (false !== strpos($fieldType, 'enum')) {
// 支持ENUM类型优先检测
} elseif (false === strpos($fieldType, 'bigint') && false !== strpos($fieldType, 'int')) {
$data[$key] = intval($data[$key]);
} elseif (false !== strpos($fieldType, 'float') || false !== strpos($fieldType, 'double')) {
$data[$key] = floatval($data[$key]);
} elseif (false !== strpos($fieldType, 'bool')) {
$data[$key] = (bool) $data[$key];
}
}
}
where的键值传入了$data(注意这里直接传的是地址&),键名key传入$key,很明显这里第一个if语句是成立的,我们直接看下面做了什么操作就行了- 判断是什么类型(ENUM、INT、BOOL、DOUBLE、FLOAT)这里的枚举类型具有优先级,但是没有进行什么操作,int类型将值转化为整数,float和double转化为浮点数,bool类型还是转化为布尔型。
- 其实就是为了确保数据可以以正确形式写入数据库进行查询
回到_parseOptions方法
elseif的条件是: - $key不是一个纯数字,而是一个字段名或者是表达式
- $key不能以下划线开头
- 确保不能包含点号
- 确保不能包含括号
- 确保不能包含
|和&
如果key不满足上面二的条件,那么配置中的strict选项可能会报错,并且移除$key - 查询完之后又将当前对象的
options进行清空 - 后面的filter没有进行什么操作就不分析了,最后返回
$options
转化后的options可以和前面原来的options对比一下看看是不是跟着逻辑走的array(5) { 'field' => string(7) "content" 'where' => array(1) { 'id' => int(2) } 'limit' => int(1) 'table' => string(8) "articles" 'model' => string(8) "Articles" }
这里返回后回到find方法 - 判断查询缓存是否存在,跟进后发现直接跳出去了,说明是不存在的也就是cache这个缓存不存在
- 若存在:将缓存配置保存到
$cache变量中,如果缓存配置中设置了键名key则将其作为缓存的键名,否则,使用序列化后的查询选项作为键名;从cache中获取数据赋值给$data(调用了S()方法);如果成功就赋值给当前对象的data之中 - 接下来利用
select方法进行查询返回给$resultSet
function select()
/**
* 查找记录
* @access public
* @param array $options 表达式
* @return mixed
*/
public function select($options = array())
{
$this->model = $options['model'];
$this->parseBind(!empty($options['bind']) ? $options['bind'] : array());
$sql = $this->buildSelectSql($options);
$result = $this->query($sql, !empty($options['fetch_sql']) ? true : false);
return $result;
}
- 同样的还是将
$options设置成空数组(当前对象中),设置当前模型名称,进入parseBind方法,进行参数绑定
function parseBind
/**
* 参数绑定分析
* @access protected
* @param array $bind
* @return array
*/
protected function parseBind($bind)
{
$this->bind = array_merge($this->bind, $bind);
}
- 接下来调用
buildselectsql方法,生成查询sql语句
function buildSelectSql()
/**
* 生成查询SQL
* @access public
* @param array $options 表达式
* @return string
*/
public function buildSelectSql($options = array())
{
if (isset($options['page'])) {
// 根据页数计算limit
list($page, $listRows) = $options['page'];
$page = $page > 0 ? $page : 1;
$listRows = $listRows > 0 ? $listRows : (is_numeric($options['limit']) ? $options['limit'] : 20);
$offset = $listRows * ($page - 1);
$options['limit'] = $offset . ',' . $listRows;
}
$sql = $this->parseSql($this->selectSql, $options);
return $sql;
}
- 如果
$options中包含page键,表示需要进行分页查询,提取出页数和每页的行数,根据他们计算出limit字句,将其添加到$optionsp['limit']中,如果未设置每页行数,默认使用20
function parseSql()
/**
* 替换SQL语句中表达式
* @access public
* @param array $options 表达式
* @return string
*/
public function parseSql($sql, $options = array())
{
$sql = str_replace(
array('%TABLE%', '%DISTINCT%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'),
array(
$this->parseTable($options['table']),
$this->parseDistinct(isset($options['distinct']) ? $options['distinct'] : false),
$this->parseField(!empty($options['field']) ? $options['field'] : '*'),
$this->parseJoin(!empty($options['join']) ? $options['join'] : ''),
$this->parseWhere(!empty($options['where']) ? $options['where'] : ''),
$this->parseGroup(!empty($options['group']) ? $options['group'] : ''),
$this->parseHaving(!empty($options['having']) ? $options['having'] : ''),
$this->parseOrder(!empty($options['order']) ? $options['order'] : ''),
$this->parseLimit(!empty($options['limit']) ? $options['limit'] : ''),
$this->parseUnion(!empty($options['union']) ? $options['union'] : ''),
$this->parseLock(isset($options['lock']) ? $options['lock'] : false),
$this->parseComment(!empty($options['comment']) ? $options['comment'] : ''),
$this->parseForce(!empty($options['force']) ? $options['force'] : ''),
), $sql);
return $sql;
}
- 这个方法用于解析sql查询语句中的各种选项
替换 %TABLE%: 调用 $this->parseTable($options['table']) 方法,解析数据表名。 替换 %DISTINCT%: 调用 $this->parseDistinct(isset($options['distinct']) ? $options['distinct'] : false) 方法,解析是否需要 DISTINCT。 替换 %FIELD%: 调用 $this->parseField(!empty($options['field']) ? $options['field'] : '*') 方法,解析查询的字段。 替换 %JOIN%: 调用 $this->parseJoin(!empty($options['join']) ? $options['join'] : '') 方法,解析 JOIN 子句。 替换 %WHERE%: 调用 $this->parseWhere(!empty($options['where']) ? $options['where'] : '') 方法,解析 WHERE 子句。 替换 %GROUP%: 调用 $this->parseGroup(!empty($options['group']) ? $options['group'] : '') 方法,解析 GROUP BY 子句。 替换 %HAVING%: 调用 $this->parseHaving(!empty($options['having']) ? $options['having'] : '') 方法,解析 HAVING 子句。 替换 %ORDER%: 调用 $this->parseOrder(!empty($options['order']) ? $options['order'] : '') 方法,解析 ORDER BY 子句。 替换 %LIMIT%: 调用 $this->parseLimit(!empty($options['limit']) ? $options['limit'] : '') 方法,解析 LIMIT 子句。 替换 %UNION%: 调用 $this->parseUnion(!empty($options['union']) ? $options['union'] : '') 方法,解析 UNION 子句。 替换 %LOCK%: 调用 $this->parseLock(isset($options['lock']) ? $options['lock'] : false) 方法,解析是否加锁。 替换 %COMMENT%: 调用 $this->parseComment(!empty($options['comment']) ? $options['comment'] : '') 方法,解析注释。 替换 %FORCE%: 调用 $this->parseForce(!empty($options['force']) ? $options['force'] : '') 方法,解析 FORCE 选项。 将上述解析结果应用到 SQL 查询语句中。 使用 str_replace 函数将 SQL 查询语句中的占位符替换为具体的解析结果。
- 其实就是构造sql语句
生成完之后返回然后在select方法中返回查询结果
当然很明显可以知道,查询的结果是:
array(1) {
[0] =>
array(1) {
'content' =>
string(41) "This is the content of the second article"
}
}
find函数:
- 后面的if判断都跳过了,直接到读取数据后的处理(这里的cache留个眼子,感觉有漏洞点)
- 接下来进入_read_data方法
function _read_data
/**
* 数据读取后的处理
* @access protected
* @param array $data 当前数据
* @return array
*/
protected function _read_data($data)
{
// 检查字段映射
if (!empty($this->_map) && C('READ_DATA_MAP')) {
foreach ($this->_map as $key => $val) {
if (isset($data[$val])) {
$data[$key] = $data[$val];
unset($data[$val]);
}
}
}
return $data;
}
首先检查模型类中是否定义了字段映射并且读取(READ_DATA_MAP)这个配置项,判断是否启动数据映射功能,这里是没有开启的,所以就直接跳出
_after_find方法没做什么操作。判断
result键名是否存在,存在则进行returnResult方法,不存在则不作操作。接下来将$data赋值给当前对象的data属性
data:array(1) { 'content' => string(41) "This is the content of the second article" }
- 判断缓存cache是否存在,存在的话就利用S方法进行对缓存进行读取和写入(这里跟进也直接跳过了,因为前面也没有cache)
- 最后返回查询结果也就是当前对象刚刚被赋值为
$data
function returnResult()
protected function returnResult($data, $type = '')
{
if ($type) {
if (is_callable($type)) {
return call_user_func($type, $data);
}
switch (strtolower($type)) {
case 'json':
return json_encode($data);
case 'xml':
return xml_encode($data);
}
}
return $data;
}
//这里就直接粘贴了gpt解释的
if ($type) {: 检查是否提供了输出类型$type。if (is_callable($type)) {: 如果$type是一个可调用的函数或方法,则通过call_user_func调用该函数,并传递查询结果$data,然后将结果返回。这样允许用户定义自己的输出处理逻辑。switch (strtolower($type)) {: 如果$type不是可调用的函数,则转换为小写,并使用switch语句基于不同的输出类型执行相应的处理。case 'json':: 如果输出类型是 JSON,使用json_encode将查询结果$data转换为 JSON 格式,并返回。case 'xml':: 如果输出类型是 XML,调用xml_encode方法将查询结果$data转换为 XML 格式,并返回。
return $data;: 如果没有提供输出类型或者提供的输出类型不在处理范围内,直接返回原始查询结果$data- 判断缓存cache是否存在,存在的话就利用S方法进行对缓存进行读取和写入
- 最后返回查询结果也就是当前对象刚刚被赋值为
$data
这里补充一下,因为前面的$options是可以被污染的,也就是cache等键都是可以自己添加进来的,比如传参?cid[cache]=1,是可以在options中加入的,也就是前面还有一个S方法没有分析,这里就拿出来分析一下
function S()
/**
* 缓存管理
* @param mixed $name 缓存名称,如果为数组表示进行缓存设置
* @param mixed $value 缓存值
* @param mixed $options 缓存参数
* @return mixed
*/
function S($name, $value = '', $options = null)
{
static $cache = '';
if (is_array($options)) {
// 缓存操作的同时初始化
$type = isset($options['type']) ? $options['type'] : '';
$cache = Think\Cache::getInstance($type, $options);
} elseif (is_array($name)) {
// 缓存初始化
$type = isset($name['type']) ? $name['type'] : '';
$cache = Think\Cache::getInstance($type, $name);
return $cache;
} elseif (empty($cache)) {
// 自动初始化
$cache = Think\Cache::getInstance();
}
if ('' === $value) {
// 获取缓存
return $cache->get($name);
} elseif (is_null($value)) {
// 删除缓存
return $cache->rm($name);
} else {
// 缓存数据
if (is_array($options)) {
$expire = isset($options['expire']) ? $options['expire'] : null;
} else {
$expire = is_numeric($options) ? $options : null;
}
return $cache->set($name, $value, $expire);
}
}
- 首先函数接受三个参数,显而易见,分别是
$name,$value``$options. - 接下来设置了一个静态变量
$cache为了保存后面的think\Cache实例 - 分别判断
$options是否是数组,$name是否是数组,$cache是否为空(按顺序),
然后最后也是调用了一次show方法,这个在前面已经分析过了,与上面的区别就是利用了base64_encode进行了加密输出。
利用
首先我们还是把find方法放出来
find()
/**
* 查询数据
* @access public
* @param mixed $options 表达式参数
* @return mixed
*/
public function find($options = array())
{
if (is_numeric($options) || is_string($options)) {
$where[$this->getPk()] = $options;
$options = array();
$options['where'] = $where;
}
// 根据复合主键查找记录
$pk = $this->getPk();
if (is_array($options) && (count($options) > 0) && is_array($pk)) {
// 根据复合主键查询
$count = 0;
foreach (array_keys($options) as $key) {
if (is_int($key)) {
$count++;
}
}
if (count($pk) == $count) {
$i = 0;
foreach ($pk as $field) {
$where[$field] = $options[$i];
unset($options[$i++]);
}
$options['where'] = $where;
} else {
return false;
}
}
// 总是查找一条记录
$options['limit'] = 1;
// 分析表达式
$options = $this->_parseOptions($options);
// 判断查询缓存
if (isset($options['cache'])) {
$cache = $options['cache'];
$key = is_string($cache['key']) ? $cache['key'] : md5(serialize($options));
$data = S($key, '', $cache);
if (false !== $data) {
$this->data = $data;
return $data;
}
}
$resultSet = $this->db->select($options);
if (false === $resultSet) {
return false;
}
if (empty($resultSet)) {
// 查询结果为空
return null;
}
if (is_string($resultSet)) {
return $resultSet;
}
// 读取数据后的处理
$data = $this->_read_data($resultSet[0]);
$this->_after_find($data, $options);
if (!empty($this->options['result'])) {
return $this->returnResult($data, $this->options['result']);
}
$this->data = $data;
if (isset($cache)) {
S($key, $data, $cache);
}
return $this->data;
}
这里我们着重看
// 判断查询缓存
if(isset($options['cache'])){
$cache = $options['cache'];
$key = is_string($cache['key'])?$cache['key']:md5(serialize($options));
$data = S($key,'',$cache);
if(false !== $data){
$this->data = $data;
return $data;
}
}
和
// 读取数据后的处理
$data = $this->_read_data($resultSet[0]);
$this->_after_find($data,$options);
if(!empty($this->options['result'])) {
return $this->returnResult($data,$this->options['result']);
}
$this->data = $data;
if(isset($cache)){
S($key,$data,$cache);
}
两个部分,分别是从缓存取数据和存入数据的操作,因为tp3.2.3有个nday,也就是sql注入,它的where注入的原理就是污染了$options数组,也就是我们在下面写入数据中可以控制$key,$data,$cache的值,从而实现任意文件写入,但是这里有个问题,我们来到S方法
S()
function S($name, $value = '', $options = null)
{
static $cache = '';
if (is_array($options)) {
$type = isset($options['type']) ? $options['type'] : '';
$cache = Think\Cache::getInstance($type, $options);
} elseif (is_array($name)) {
$type = isset($name['type']) ? $name['type'] : '';
$cache = Think\Cache::getInstance($type, $name);
return $cache;
} elseif (empty($cache)) {
$cache = Think\Cache::getInstance();
}
if ('' === $value) {
return $cache->get($name);
} elseif (is_null($value)) {
return $cache->rm($name);
} else {
if (is_array($options)) {
$expire = isset($options['expire']) ? $options['expire'] : null;
} else {
$expire = is_numeric($options) ? $options : null;
}
return $cache->set($name, $value, $expire);
}
}
这里我们会走到
if ('' === $value) {
return $cache->get($name);
这个地方
GET()
/**
* 读取缓存
* @access public
* @param string $name 缓存变量名
* @return mixed
*/
public function get($name)
{
$filename = $this->filename($name);
if (!is_file($filename)) {
return false;
}
N('cache_read', 1);
$content = file_get_contents($filename);
if (false !== $content) {
$expire = (int) substr($content, 8, 12);
if (0 != $expire && time() > filemtime($filename) + $expire) {
//缓存过期删除缓存文件
unlink($filename);
return false;
}
if (C('DATA_CACHE_CHECK')) {
//开启数据校验
$check = substr($content, 20, 32);
$content = substr($content, 52, -3);
if (md5($content) != $check) {
//校验错误
return false;
}
} else {
$content = substr($content, 20, -3);
}
if (C('DATA_CACHE_COMPRESS') && function_exists('gzcompress')) {
//启用数据压缩
$content = gzuncompress($content);
}
$content = unserialize($content);
return $content;
} else {
return false;
}
}
原本的想法是可以进行unserialize从而来触发反序列化,再利用tp原来存在的一些方法来进行读取flag,但是这个地方的content是不可控的,所以这个地方就卡住了,但是我在这里发现了调用了filename的方法。
filename()
/**
* 取得变量的存储文件名
* @access private
* @param string $name 缓存变量名
* @return string
*/
private function filename($name)
{
$name = md5(C('DATA_CACHE_KEY') . $name);
if (C('DATA_CACHE_SUBDIR')) {
// 使用子目录
$dir = '';
for ($i = 0; $i < C('DATA_PATH_LEVEL'); $i++) {
$dir .= $name{$i} . '/';
}
if (!is_dir($this->options['temp'] . $dir)) {
mkdir($this->options['temp'] . $dir, 0755, true);
}
$filename = $dir . $this->options['prefix'] . $name . '.php';
} else {
$filename = $this->options['prefix'] . $name . '.php';
}
return $this->options['temp'] . $filename;
}
可以看见对文件进行了写入的操作,那么我们的值都可控,是不是就可以写入马子了?但是这道题专门将tp的目录隔离在了web路径访问不到的地方并且对文件名进行了加密,且web根目录无可写权限。所以这里又卡住了。
后面看见一个common~runtime.php,这个文件里面记录了所有thinkphp在运行时调用的类和函数。
namespace Behavior {
use Think\Storage;
use Think\Think;
class ParseTemplateBehavior
{
public function run(&$_data)
{
$engine = strtolower(C('TMPL_ENGINE_TYPE'));
$_content = empty($_data['content']) ? $_data['file'] : $_data['content'];
$_data['prefix'] = !empty($_data['prefix']) ? $_data['prefix'] : C('TMPL_CACHE_PREFIX');
if ('think' == $engine) {
if ((!empty($_data['content']) && $this->checkContentCache($_data['content'], $_data['prefix'])) || $this->checkCache($_data['file'], $_data['prefix'])) {
Storage::load(C('CACHE_PATH') . $_data['prefix'] . md5($_content) . C('TMPL_CACHFILE_SUFFIX'), $_data['var']);
} else {
$tpl = Think::instance('Think\\Template');
$tpl->fetch($_content, $_data['var'], $_data['prefix']);
}
} else {
if (strpos($engine, '\\')) {
$class = $engine;
} else {
$class = 'Think\\Template\\Driver\\' . ucwords($engine);
}
if (class_exists($class)) {
$tpl = new $class;
$tpl->fetch($_content, $_data['var']);
} else {
E(L('_NOT_SUPPORT_') . ': ' . $class);
}
}
}
protected function checkCache($tmplTemplateFile, $prefix = '')
{
if (!C('TMPL_CACHE_ON')) {
return false;
}
$tmplCacheFile = C('CACHE_PATH') . $prefix . md5($tmplTemplateFile) . C('TMPL_CACHFILE_SUFFIX');
if (!Storage::has($tmplCacheFile)) {
return false;
} elseif (filemtime($tmplTemplateFile) > Storage::get($tmplCacheFile, 'mtime')) {
return false;
} elseif (C('TMPL_CACHE_TIME') != 0 && time() > Storage::get($tmplCacheFile, 'mtime') + C('TMPL_CACHE_TIME')) {
return false;
}
if (C('LAYOUT_ON')) {
$layoutFile = THEME_PATH . C('LAYOUT_NAME') . C('TMPL_TEMPLATE_SUFFIX');
if (filemtime($layoutFile) > Storage::get($tmplCacheFile, 'mtime')) {
return false;
}
}
return true;
}
protected function checkContentCache($tmplContent, $prefix = '')
{
if (Storage::has(C('CACHE_PATH') . $prefix . md5($tmplContent) . C('TMPL_CACHFILE_SUFFIX'))) {
return true;
} else {
return false;
}
}
}
}
这个类的run方法,恰好也是使用了md5的content的值进行文件的命名,最后走到Storage::load其实也就是调用了文件包含的include方法。那么我们就有了思路,将cache的key的值构造成之前show函数中传入的base64_encode($content['content'])的值,同时将生成缓存文件路径即options['temp']的位置替换成C('CACHE_PATH') . $_data['prefix'],就可以完美的进行cache的覆盖,从而让server加载我们写入的webshell缓存。
所以步骤如下:

获取cid为2的content的base64的值。(不要在意其他的,都是debug时候自己加的东西)

最后直接rce

直接cat /flag即可