Typecho 源码分析(1)

2019-11-06 22:15:03

crazyhl 2019-11-06 22:15:03
分类专栏: # typecho 源码分析 文章标签: php typecho 源码分析

先说点前置的东西

从今天开始我要开始写源码分析的文章了,以前用 csdn 博客写过一些 android 和 java 的东西,后来脑袋抽筋被我删除了。所以这次等于是全新的开始,准备输出一些东西了。做了 5 年的开发,发现自己缺乏很多东西,所以这次从源码分析开始,输出东西以及补充自己的知识。

为什么选用 typecho ?

这个博客系统很经典,说是 cms 也不为过,毕竟他的对手 wordpress 也可以说是 cms 了,现在估计更多的企业都是用它来做展示站了。而不仅仅是博客。

也许会有朋友说,这么老的代码分析有何用?但是我想说虽然它没有用 composer,没有各种新奇的东西,而且还有很多 php 和 html 混排的代码。但是他的代码我认为我可以分析的比较清楚,没有更多外部的引用,让我们可以全身心的投入到它本身的代码中去。不用考虑各种引用的代码,让我们在深入进去,比如如果分析 laravel,就要分析很多引入库了。另外,这个代码的注释写的太好了。当然了,后续我也会分析这种代码的。

我分析的流程

分析代码有很多种

  1. 一个文件分析完,在分析另一个
  2. 遇到一个方法或者流程的改变就进入到相应方法或者流程

我在这边选用第二种,这样更符合我的风格

正文开始

进入 index.php

/** 载入配置支持 */
if (!defined('__TYPECHO_ROOT_DIR__') && !@include_once 'config.inc.php') {
    file_exists('./install.php') ? header('Location: install.php') : print('Missing Config File');
    exit;
}
12345

如果没有定义 __TYPECHO_ROOT_DIR__ 并且 引入 config.inc.php 失败就进入判断。
这时候检测 ./install.php 如果存在就转到 ./install.php 文件,否则就报错 Missing Config File。

第一次进入这个文件的时候,我们肯定是没有安装的,所以我们就进入 install.php

来到 install.php

<?php if (!file_exists(dirname(__FILE__) . '/config.inc.php')): ?>
1

检测 install.php 同目录下是否存在 config.inc.php 文件 如果存在就到了第 35 行开始执行下面这段代码

require_once dirname(__FILE__) . '/config.inc.php';

    //判断是否已经安装
    $db = Typecho_Db::get();
    try {
        $installed = $db->fetchRow($db->select()->from('table.options')->where('name = ?', 'installed'));
        if (empty($installed) || $installed['value'] == 1) {
            Typecho_Response::setStatus(404);
            exit;
        }
    } catch (Exception $e) {
        // do nothing
    }
12345678910111213

引入 config.inc.php 配置文件,紧接着查询 table.options 表,是否包含 'name = installed 的行,如果没有没有检测到就报错 404,如果存在就继续执行。当然现在的我们是没有安装的,我们执行上面的代码,如果已经安装的流程,我们会在后面分析。现在让我们回到上面的部分看这段代码

<?php
/**
 * Typecho Blog Platform
 *
 * @copyright  Copyright (c) 2008 Typecho team (http://www.typecho.org)
 * @license    GNU General Public License 2.0
 * @version    $Id$
 */

/** 定义根目录 */
define('__TYPECHO_ROOT_DIR__', dirname(__FILE__));

/** 定义插件目录(相对路径) */
define('__TYPECHO_PLUGIN_DIR__', '/usr/plugins');

/** 定义模板目录(相对路径) */
define('__TYPECHO_THEME_DIR__', '/usr/themes');

/** 后台路径(相对路径) */
define('__TYPECHO_ADMIN_DIR__', '/admin/');

/** 设置包含路径 */
@set_include_path(get_include_path() . PATH_SEPARATOR .
__TYPECHO_ROOT_DIR__ . '/var' . PATH_SEPARATOR .
__TYPECHO_ROOT_DIR__ . __TYPECHO_PLUGIN_DIR__);

/** 载入API支持 */
require_once 'Typecho/Common.php';

/** 程序初始化 */
Typecho_Common::init();
12345678910111213141516171819202122232425262728293031

前面几个定义路径我们就不说了,注释写的很完美,我们说说 set_include_path 和 get_include_path 这两个方法可以看 https://www.jianshu.com/p/303feaaeded1 这篇文章,这块还得说下 PATH_SEPARATOR 这个东西,不仅是 PATH_SEPARATOR 还有 DIRECTORY_SEPARATOR 这两个分隔符,我们写代码的时候容易写死,但是实际上用这种定义好的常量,他会根据系统来决定用什么分隔符,这就很容易避免 windows 和 linux 系统不一致的区别,导致代码报错。set_include_path 和 get_include_path 配置好了系统引入路径和 typecho 自定义的引入路径,包括了 var 、插件目录,这两个字目录。以后再引入文件的时候,系统就会根据设置的这些目录,引入相关文件了。这块代码,再现在我们大量使用 namespace,以及 comopser 使用的概率很低了。关于 composer 的一些东西,我会在另一篇去说明。

先暂停一下

今天就先到这吧,我决定明天写完 composer 用的 spl_autoload_register 和 set_include_path 的相关对比后,在继续

PS

好久没写了,感觉状态不好,估计以后会慢慢提升的。

Typecho 源码分析(2)

crazyhl 2019-11-09 20:24:03

分类专栏: # typecho 源码分析 文章标签: typecho 源码分析

前情提要

上一篇我们分析到了 install.php 文件的 set_include_path。今天我们继续。

进入安装流程

来到了引入 Typecho/Common.php 这样,也许你会很奇怪,找不到这个目录呢?不要忘了我们上面设置了好几个引入路径,所以我们要一个个的查找,最后我们会在 var 目录下,找到 Typecho/Common.php。你看用这种方法引入,找文件都不好弄。所以命名空间什么的才会愈发的重要,set_include_path 这种引入的方案也会逐渐减少使用。

紧接着执行 Typecho_Common::init(); 这行代码的方法就在刚才我们引入的文件中,我们进入这个方法。

/**
     * 自动载入类
     *
     * @param $className
     */
    public static function __autoLoad($className)
    {
        @include_once str_replace(array('\\', '_'), '/', $className) . '.php';
    }

    /**
     * 程序初始化方法
     *
     * @access public
     * @return void
     */
    public static function init()
    {
        /** 设置自动载入函数 */
        spl_autoload_register(array('Typecho_Common', '__autoLoad'));

        /** 兼容php6 */
        if (function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc()) {
            $_GET = self::stripslashesDeep($_GET);
            $_POST = self::stripslashesDeep($_POST);
            $_COOKIE = self::stripslashesDeep($_COOKIE);

            reset($_GET);
            reset($_POST);
            reset($_COOKIE);
        }

        /** 设置异常截获函数 */
        set_exception_handler(array('Typecho_Common', 'exceptionHandle'));
    }

先看 if (function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc()) { 这段,这块在php5.4 以后永远都返回 false 了,所以这段在5.4 以后不会执行。如果是老版本呢,这块的处理就是把一些转义带有反斜线的字符给恢复过来,变成原始的内容。
然后剩下的部分就是注册自动加载,和异常处理函数了。自动加载可以看我上一篇文章,然后自己理解一下。

异常处理部分,就是根据是否是 debug 两种输出模式。

/**
     * 异常截获函数
     *
     * @access public
     * @param $exception 截获的异常
     * @return void
     */
    public static function exceptionHandle($exception)
    {
        if (defined('__TYPECHO_DEBUG__')) {
            echo '<pre><code>';
            echo '<h1>' . htmlspecialchars($exception->getMessage()) . '</h1>';
            echo htmlspecialchars($exception->__toString());
            echo '</code></pre>';
        } else {
            @ob_end_clean();
            if (404 == $exception->getCode() && !empty(self::
                $exceptionHandle)) {
                $handleClass = self::$exceptionHandle;
                new $handleClass($exception);
            } else {
                self::error($exception);
            }
        }

        exit;
    }

self::$exceptionHandle` 这个会在初始化的时候我们再说。
如果没有 exceptionHandle 的时候会调用 `error` 方法,进行错误输出,并且记录 `error_log
/**
     * 输出错误页面
     *
     * @access public
     * @param mixed $exception 错误信息
     * @return void
     */
    public static function error($exception)
    {
        $isException = is_object($exception);
        $message = '';

        if ($isException) {
            $code = $exception->getCode();
            $message = $exception->getMessage();
        } else {
            $code = $exception;
        }

        $charset = self::$charset;

        if ($isException && $exception instanceof Typecho_Db_Exception) {
            $code = 500;
            @error_log($message);

            //覆盖原始错误信息
            $message = 'Database Server Error';

            if ($exception instanceof Typecho_Db_Adapter_Exception) {
                $code = 503;
                $message = 'Error establishing a database connection';
            } else if ($exception instanceof Typecho_Db_Query_Exception) {
                $message = 'Database Query Error';
            }
        } else {
            switch ($code) {
                case 500:
                    $message = 'Server Error';
                    break;

                case 404:
                    $message = 'Page Not Found';
                    break;

                default:
                    $code = 'Error';
                    break;
            }
        }


        /** 设置http code */
        if (is_numeric($code) && $code > 200) {
            Typecho_Response::setStatus($code);
        }

        $message = nl2br($message);

        if (defined('__TYPECHO_EXCEPTION_FILE__')) {
            require_once __TYPECHO_EXCEPTION_FILE__;
        } else {
            echo
<<<EOF
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="{$charset}">
        <title>{$code}</title>
        <style>
            html {
                padding: 50px 10px;
                font-size: 16px;
                line-height: 1.4;
                color: #666;
                background: #F6F6F3;
                -webkit-text-size-adjust: 100%;
                -ms-text-size-adjust: 100%;
            }

            html,
            input { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; }
            body {
                max-width: 500px;
                _width: 500px;
                padding: 30px 20px;
                margin: 0 auto;
                background: #FFF;
            }
            ul {
                padding: 0 0 0 40px;
            }
            .container {
                max-width: 380px;
                _width: 380px;
                margin: 0 auto;
            }
        </style>
    </head>
    <body>
        <div class="container">
            {$message}
        </div>
    </body>
</html>
EOF;
        }

        exit;
    }

到这里 init 方法已经执行完了,我们回到 install.php 文件继续往下看。

// 挡掉可能的跨站请求
    if (!empty($_GET) || !empty($_POST)) {
        if (empty($_SERVER['HTTP_REFERER'])) {
            exit;
        }

        $parts = parse_url($_SERVER['HTTP_REFERER']);
    if (!empty($parts['port'])) {
            $parts['host'] = "{$parts['host']}:{$parts['port']}";
        }

        if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
            exit;
        }
    }

这块就是挡掉跨域攻击,比如 iframe 的嵌套页面,为了安全,我们会判断 referer 如果跟请求的 host 不一致就阻挡掉

    $options = new stdClass();
    $options->generator = 'Typecho ' . Typecho_Common::VERSION;
    list($soft, $currentVersion) = explode(' ', $options->generator);

    $options->software = $soft;
    $options->version = $currentVersion;

    list($prefixVersion, $suffixVersion) = explode('/', $currentVersion);

    /** 获取语言 */
    $lang = _r('lang', Typecho_Cookie::get('__typecho_lang'));
    $langs = Widget_Options_General::getLangs();

    if (empty($lang) || (!empty($langs) && !isset($langs[$lang]))) {
        $lang = 'zh_CN';
    }

    if ('zh_CN' != $lang) {
        $dir = defined('__TYPECHO_LANG_DIR__') ? __TYPECHO_LANG_DIR__ : __TYPECHO_ROOT_DIR__ . '/usr/langs';
        Typecho_I18n::setLang($dir . '/' . $lang . '.mo');
    }

    Typecho_Cookie::set('__typecho_lang', $lang);

设置版本,设置语言,顺便把语言写入到 cookie 中。

剩下的部分就都是安装流程了,我们慢慢拆分来看。
先说 安装文件 最后的部分

    <?php
    include 'admin/copyright.php';
    include 'admin/footer.php';
    ?>

因为这两个文件都因为开头

    <?php if(!defined('__TYPECHO_ADMIN__')) exit; ?>

由于没有定义那个常量而退出了,所以这两个部分都在我们用到的时候再说。

<li<?php if (!isset($_GET['finish']) && !isset($_GET['config'])) : ?> class="current"<?php endif; ?>><span>1</span><?php _e('欢迎使用'); ?></li>
        <li<?php if (isset($_GET['config'])) : ?> class="current"<?php endif; ?>><span>2</span><?php _e('初始化配置'); ?></li>
        <li<?php if (isset($_GET['start'])) : ?> class="current"<?php endif; ?>><span>3</span><?php _e('开始安装'); ?></li>
        <li<?php if (isset($_GET['finish'])) : ?> class="current"<?php endif; ?>><span>4</span><?php _e('安装成功'); ?></li>

这块就是根据 url 的参数状态决定显示的问题 _e 和 _t 都是 一个是翻译并 echo 另一个是翻译。
注意哦,start 这个状态,在正常状态时看不到的哦,只有失败才会看得到。

安装第一步

显示一些说明文件,如果语言配置有多个,那么就出现语言选择列表框,不过默认就只有一个简体中文。然后点击下一步以后会跳转到当前 url ,增加config 参数。

进入配置

点击下一步以后我们就进入到了配置的步骤,当我们输入完相关数据参数,以及管理员信息以后点击下一步,会 post 方法跳转到当前 config 网址。这里有个主要注意的地方是,当我们改变数据库的适配器以后,会跳转到切换相应的数据库适配器配置页面。而且会在页面加载的时候判定支持什么数据。这两段在下面的代码中

<?php
        $adapters = array('Mysql', 'Mysqli', 'Pdo_Mysql', 'SQLite', 'Pdo_SQLite', 'Pgsql', 'Pdo_Pgsql');
        foreach ($adapters as $firstAdapter) {
            if (_p($firstAdapter)) {
                break;
            }
        }
        $adapter = _r('dbAdapter', $firstAdapter);
        $parts = explode('_', $adapter);

        $type = $adapter == 'Mysqli' ? 'Mysql' : array_pop($parts);
?>
<?php require_once './install/' . $type . '.php'; ?>

<script>
var _select = document.config.dbAdapter;
_select.onchange = function() {
    setTimeout("window.location.href = 'install.php?config&dbAdapter=" + this.value + "'; ",0);
}
</script>

不同的适配器会加载不同的数据库配置页面,我们这边用的是 mysql ,我们进入 install/mysql.php 页面,里面有很多环境的判断 sae、gae、bae 什么的判定。这些我们都跳过,直接看下面。

<?php  else: ?>
    <li>
        <label class="typecho-label" for="dbHost"><?php _e('数据库地址'); ?></label>
        <input type="text" class="text" name="dbHost" id="dbHost" value="<?php _v('dbHost', 'localhost'); ?>"/>
        <p class="description"><?php _e('您可能会使用 "%s"', 'localhost'); ?></p>
    </li>
    <li>
        <label class="typecho-label" for="dbPort"><?php _e('数据库端口'); ?></label>
        <input type="text" class="text" name="dbPort" id="dbPort" value="<?php _v('dbPort', '3306'); ?>"/>
        <p class="description"><?php _e('如果您不知道此选项的意义, 请保留默认设置'); ?></p>
    </li>
    <li>
        <label class="typecho-label" for="dbUser"><?php _e('数据库用户名'); ?></label>
        <input type="text" class="text" name="dbUser" id="dbUser" value="<?php _v('dbUser', 'root'); ?>" />
        <p class="description"><?php _e('您可能会使用 "%s"', 'root'); ?></p>
    </li>
    <li>
        <label class="typecho-label" for="dbPassword"><?php _e('数据库密码'); ?></label>
        <input type="password" class="text" name="dbPassword" id="dbPassword" value="<?php _v('dbPassword'); ?>" />
    </li>
    <li>
        <label class="typecho-label" for="dbDatabase"><?php _e('数据库名'); ?></label>
        <input type="text" class="text" name="dbDatabase" id="dbDatabase" value="<?php _v('dbDatabase', 'typecho'); ?>" />
        <p class="description"><?php _e('请您指定数据库名称'); ?></p>
    </li>

<?php  endif; ?>
<input type="hidden" name="dbCharset" value="<?php _e('utf8'); ?>" />

    <li>
        <label class="typecho-label" for="dbCharset"><?php _e('数据库编码'); ?></label>
        <select name="dbCharset" id="dbCharset">
            <option value="utf8"<?php if (_r('dbCharset') == 'utf8'): ?> selected<?php endif; ?>>utf8</option>
            <option value="utf8mb4"<?php if (_r('dbCharset') == 'utf8mb4'): ?> selected<?php endif; ?>>utf8mb4</option>
        </select>
        <p class="description"><?php _e('选择 utf8mb4 编码至少需要 MySQL 5.5.3 版本'); ?></p>
    </li>

    <li>
        <label class="typecho-label" for="dbEngine"><?php _e('数据库引擎'); ?></label>
        <select name="dbEngine" id="dbEngine">
            <option value="MyISAM"<?php if (_r('dbEngine') == 'MyISAM'): ?> selected<?php endif; ?>>MyISAM</option>
            <option value="InnoDB"<?php if (_r('dbEngine') == 'InnoDB'): ?> selected<?php endif; ?>>InnoDB</option>
        </select>
    </li>

这块就是我们显示配置的位置哟。
我们填写完配置信息以后,post 提交当前页面,进入各种判定的部分。

if (_r('created') && !file_exists('./config.inc.php')) {
    echo '<p class="message error">' . _t('没有检测到您手动创建的配置文件, 请检查后再次创建') . '</p>';
    $success = false;
} else {
    if (NULL == _r('userUrl')) {
        $success = false;
        echo '<p class="message error">' . _t('请填写您的网站地址') . '</p>';
    } else if (NULL == _r('userName')) {
        $success = false;
        echo '<p class="message error">' . _t('请填写您的用户名') . '</p>';
    } else if (NULL == _r('userMail')) {
        $success = false;
        echo '<p class="message error">' . _t('请填写您的邮箱地址') . '</p>';
    } else if (32 < strlen(_r('userName'))) {
        $success = false;
        echo '<p class="message error">' . _t('用户名长度超过限制, 请不要超过 32 个字符') . '</p>';
    } else if (200 < strlen(_r('userMail'))) {
        $success = false;
        echo '<p class="message error">' . _t('邮箱长度超过限制, 请不要超过 200 个字符') . '</p>';
    }
}

这部分会进行一些判定相关的东西。不符合规范的会进行报错 if (_r('created') && !file_exists('./config.inc.php')) { 注意这块,我们后面再说。

$_dbConfig = _rFrom('dbHost', 'dbUser', 'dbPassword', 'dbCharset', 'dbPort', 'dbDatabase', 'dbFile', 'dbDsn', 'dbEngine');

    $_dbConfig = array_filter($_dbConfig);
    $dbConfig = array();
    foreach ($_dbConfig as $key => $val) {
        $dbConfig[strtolower(substr($key, 2))] = $val;
    }

    // 在特殊服务器上的特殊安装过程处理
    if (_r('config')) {
        $replace = array_keys($dbConfig);
        foreach ($replace as &$key) {
            $key = '{' . $key . '}';
        }

        if (!empty($_dbConfig['dbDsn'])) {
            $dbConfig['dsn'] = str_replace($replace, array_values($dbConfig), $dbConfig['dsn']);
        }
        $config = str_replace($replace, array_values($dbConfig), _r('config'));
    }

    if (!isset($config) && $success && !_r('created')) {
        $installDb = new Typecho_Db($adapter, _r('dbPrefix'));
        $installDb->addServer($dbConfig, Typecho_Db::READ | Typecho_Db::WRITE);


        /** 检测数据库配置 */
        try {
            $installDb->query('SELECT 1=1');
        } catch (Typecho_Db_Adapter_Exception $e) {
            $success = false;
            echo '<p class="message error">'
            . _t('对不起, 无法连接数据库, 请先检查数据库配置再继续进行安装') . '</p>';
        } catch (Typecho_Db_Exception $e) {
            $success = false;
            echo '<p class="message error">'
            . _t('安装程序捕捉到以下错误: " %s ". 程序被终止, 请检查您的配置信息.',$e->getMessage()) . '</p>';
        }
    }

这块是获取数据库连接配置,然后对数据库进行连接,数据库相关的代码是 typecho 自己的封装的,大家可以自己看一下,很厉害。如果连接失败,会进行报错。
如果成功了,就重置数据库相关信息,这块应该是应对重复安装的。然后 cookie 写入数据库配置信息

// 重置原有数据库状态
    if (isset($installDb)) {
        try {
            $installDb->query($installDb->update('table.options')
                ->rows(array('value' => 0))->where('name = ?', 'installed'));
        } catch (Exception $e) {
            // do nothing
        }
    }

    Typecho_Cookie::set('__typecho_config', base64_encode(serialize(array_merge(array(
        'prefix'    =>  _r('dbPrefix'),
        'userName'  =>  _r('userName'),
        'userPassword'  =>  _r('userPassword'),
        'userMail'  =>  _r('userMail'),
        'adapter'   =>  $adapter,
        'siteUrl'   =>  _r('userUrl')
    ), $dbConfig))));

注意下面这段

if (_r('created')) {
    header('Location: ./install.php?start');
    exit;
}

这段什么意思?我们后面再说

/** 初始化配置文件 */
    $lines = array_slice(file(__FILE__), 1, 31);
    $lines[] = "
    /** 定义数据库参数 */
    \$db = new Typecho_Db('{$adapter}', '" . _r('dbPrefix') . "');
    \$db->addServer(" . (empty($config) ? var_export($dbConfig, true) : $config) . ", Typecho_Db::READ | Typecho_Db::WRITE);
    Typecho_Db::set(\$db);
    ";
    $contents = implode('', $lines);
    if (!Typecho_Common::isAppEngine()) {
        @file_put_contents('./config.inc.php', $contents);
    }

这段就写入配置文件了。

if (!file_exists('./config.inc.php')) {
    ?>
    <div class="message notice"><p><?php _e('安装程序无法自动创建 <strong>config.inc.php</strong> 文件'); ?><br />
    <?php _e('您可以在网站根目录下手动创建 <strong>config.inc.php</strong> 文件, 并复制如下代码至其中'); ?></p>
    <p><textarea rows="5" onmouseover="this.select();" class="w-100 mono" readonly><?php echo htmlspecialchars($contents); ?></textarea></p>
    <p><button name="created" value="1" type="submit" class="btn primary">创建完毕, 继续安装 &raquo;</button></p></div>
    <?php
    } else {
        header('Location: ./install.php?start');
        exit;
    }

如果写入文件失败了,就会跳转到当前页面了并且携带 created 参数,就应对上一步的判定了。如果写入成功了,就跳转到 start。

// 安装不成功删除配置文件
    if(!$success && file_exists(__TYPECHO_ROOT_DIR__ . '/config.inc.php')) {
        @unlink(__TYPECHO_ROOT_DIR__ . '/config.inc.php');
    }

安装失败了,就删除文件。

来到 start

这一步就到了比较关键的一步了。

require_once dirname(__FILE__) . '/config.inc.php';

//判断是否已经安装
$db = Typecho_Db::get();
try {
    $installed = $db->fetchRow($db->select()->from('table.options')->where('name = ?', 'installed'));
    if (empty($installed) || $installed['value'] == 1) {
        Typecho_Response::setStatus(404);
        exit;
    }
} catch (Exception $e) {
    // do nothing
}
12345678910111213

这块就应对上前面说得了,如果包含了配置文件,就会链接数据库,并且查询是否已安装了,如果已安装了就报错 404。

    <?php if (!isset($db)) : ?>
    <h1 class="typecho-install-title"><?php _e('安装失败!'); ?></h1>
    <div class="typecho-install-body">
        <form method="post" action="?config" name="config">
        <p class="message error"><?php _e('您没有上传 config.inc.php 文件, 请您重新安装!'); ?> <button class="btn primary" type="submit"><?php _e('重新安装 &raquo;'); ?></button></p>
        </form>
    </div>
    <?php else : ?>

如果没有连接 db 就报错。
如果一切顺利就执行创建数据库,初始化配置文件。如果失败了就报错安装失败,如果成功了,就跳转到,安装成功。
上面说的看不到 安装过程 页面就是因为太快了,所以就一闪而过了,上面表述的不清楚,这里在说明一下。

 if(('Mysql' == $type && (1050 == $code || '42S01' == $code)) ||
('SQLite' == $type && ('HY000' == $code || 1 == $code)) ||
('Pgsql' == $type && '42P07' == $code)) {
    if(_r('delete')) {
        //删除原有数据
        $dbPrefix = $config['prefix'];
        $tableArray = array($dbPrefix . 'comments', $dbPrefix . 'contents', $dbPrefix . 'fields', $dbPrefix . 'metas', $dbPrefix . 'options', $dbPrefix . 'relationships', $dbPrefix . 'users',);
        foreach($tableArray as $table) {
            if($type == 'Mysql') {
                $installDb->query("DROP TABLE IF EXISTS `{$table}`");
            } elseif($type == 'Pgsql') {
                $installDb->query("DROP TABLE {$table}");
            } elseif($type == 'SQLite') {
                $installDb->query("DROP TABLE {$table}");
            }
        }
        echo '<p class="message success">' . _t('已经删除完原有数据') . '<br /><br /><button class="btn primary" type="submit" class="primary">'
            . _t('继续安装 &raquo;') . '</button></p>';
    } elseif (_r('goahead')) {
        //使用原有数据
        //但是要更新用户网站
        $installDb->query($installDb->update('table.options')->rows(array('value' => $config['siteUrl']))->where('name = ?', 'siteUrl'));
        unset($_SESSION['typecho']);
        header('Location: ./install.php?finish&use_old');
        exit;
    } else {
            echo '<p class="message error">' . _t('安装程序检查到原有数据表已经存在.')
            . '<br /><br />' . '<button type="submit" name="delete" value="1" class="btn btn-warn">' . _t('删除原有数据') . '</button> '
            . _t('或者') . ' <button type="submit" name="goahead" value="1" class="btn primary">' . _t('使用原有数据') . '</button></p>';
    }

这块就是在异常的时候如果数据库存在,的判断过程。删库或者使用原有数据库,然后等我们决策后,在决定安装流程。

结语

至此,安装全部搞定了,我们分析完了一个安装模块,接下来说些什么呢,先说前台,部分,然后在说明上面没说明的 db 部分,最后说后台。我们下次再见

Typecho 源码分析(3)

crazyhl 2019-11-13 21:51:20

分类专栏: # typecho 源码分析 文章标签: php typecho 源码分析

前情提要

通过之前我们的分析,我们已经把安装流程搞定可,本篇开始,我们分析前台相关的流程。

正文开始

我们又一次回到了 index.php 在第一篇说完前几行判断是否安装之后,我们就暂时离开了这个文件,当我们安装完以后,进入首页就又回到了这里,我们跳过判断安装那一块,直接看后面。

/** 初始化组件 */
Typecho_Widget::widget('Widget_Init');

/** 注册一个初始化插件 */
Typecho_Plugin::factory('index.php')->begin();

/** 开始路由分发 */
Typecho_Router::dispatch();

/** 注册一个结束插件 */
Typecho_Plugin::factory('index.php')->end();

别看只有4行,却是大量的精髓都在这边,我们一行一行看。
第一行,初始化组件,我们进入代码

/**
     * 工厂方法,将类静态化放置到列表中
     *
     * @access public
     * @param string $alias 组件别名
     * @param mixed $params 传递的参数
     * @param mixed $request 前端参数
     * @param boolean $enableResponse 是否允许http回执
     * @return Typecho_Widget
     * @throws Typecho_Exception
     */
    public static function widget($alias, $params = NULL, $request = NULL, $enableResponse = true)
    {
        $parts = explode('@', $alias);
        $className = $parts[0];
        $alias = empty($parts[1]) ? $className : $parts[1];

        if (isset(self::$_widgetAlias[$className])) {
            $className = self::$_widgetAlias[$className];
        }

        if (!isset(self::$_widgetPool[$alias])) {
            /** 如果类不存在 */
            if (!class_exists($className)) {
                throw new Typecho_Widget_Exception($className);
            }

            /** 初始化request */
            if (!empty($request)) {
                $requestObject = new Typecho_Request();
                $requestObject->setParams($request);
            } else {
                $requestObject = Typecho_Request::getInstance();
            }

            /** 初始化response */
            $responseObject = $enableResponse ? Typecho_Response::getInstance()
            : Typecho_Widget_Helper_Empty::getInstance();

            /** 初始化组件 */
            $widget = new $className($requestObject, $responseObject, $params);

            $widget->execute();
            self::$_widgetPool[$alias] = $widget;
        }

        return self::$_widgetPool[$alias];
    }

先看第一个参数 $alias 第一步把传入的变量利用 @ 拆分成两个变量,一个是要初始化的类名,另一个是个别名,如果没有别名的设置,雷德明就是别名。 然后在判断别名池里面有没有这个别名,如果包含了这个别名,类名就变成别名池设置的类名。这个别名池,在这个类下的 alias 方法中可以设置,当前这个方法貌似没有使用。

继续下去,如果组件池里面没有这个实例,就要初始化这个类,先初始化了,request,初始化的时候,会把 get 和 post 参数,设置到 request 的 $_widgetAlias 里面。然后调用了 request 的 setParams 把 widget 方法传入的 params 参数传入,这个方法,会把 params 设置到 request 的 _params 里面。当然,如果传入的 request 参数不是空,则会获取已经存在的 request,不过 request 的 getInstance 方法中不存在 request 实例的话,也会重新初始化一下。

紧接着会根据 $enableResponse 是否为 true 决定,是创建 resposne 或者 Typecho_Widget_Helper_Empty。Typecho_Widget_Helper_Empty 这个类,我们后面再说。

最后,初始化我们传入的 class ,并且执行 execute 方法。最后把 class 实例,放入到组件池当中,并且把组件返回。

好了,现在我们说说初始化的 Widget_Init 类。这个类继承了 Typecho_Widget 类,这个类是个组件根类,这里面封装了不少数据结构,以及 request 、response 之类的东西,这个类,我们在后面用到的时候分别来说再说。初始化 Typecho_Widget 的时候,会传入 request 、resposne 、$params。request 、resposne 会赋值给实例的 request 、resposne。紧接着初始化了 Typecho_Config, 这个类就当做数组看就行了。再把 $params 调用给 Typecho_Config 的 setDefault 方法,设置初始值。

下期预告

紧接着我们看看 Widget_Init 类的 execute 方法。这里面方法也很复杂,我们下期再说。

Typecho 源码分析(4)

crazyhl 2019-11-14 21:56:03 分类专栏: # typecho 源码分析 文章标签: php 源码分析 typecho

上篇文章忘记说的

我们还是在入口文件徘徊,而且还是在初始化 widget 的第一行。我们来到了初始化 Widget_Init 的地方。在 Init->execute 方法中,我们到了初始化 option 的地方。

初始化的构造地方,还初始化了 db 数据库。

数据库连接的地方在,index 文件的引入 config 有构造连接,大家不要忘记哦。

正文开始

我们来看 Option->execute() 方法。

/**
     * 执行函数
     *
     * @access public
     * @return void
     */
    public function execute()
    {
        $this->db->fetchAll($this->db->select()->from('table.options')
        ->where('user = 0'), array($this, 'push'));

        /** 支持皮肤变量重载 */
        if (!empty($this->row['theme:' . $this->row['theme']])) {
            $themeOptions = NULL;

            /** 解析变量 */
            if ($themeOptions = unserialize($this->row['theme:' . $this->row['theme']])) {
                /** 覆盖变量 */
                $this->row = array_merge($this->row, $themeOptions);
            }
        }

        $this->stack[] = &$this->row;

        /** 动态获取根目录 */
        $this->rootUrl = defined('__TYPECHO_ROOT_URL__') ? __TYPECHO_ROOT_URL__ : $this->request->getRequestRoot();
        if (defined('__TYPECHO_ADMIN__')) {
            /** 识别在admin目录中的情况 */
            $adminDir = '/' . trim(defined('__TYPECHO_ADMIN_DIR__') ? __TYPECHO_ADMIN_DIR__ : '/admin/', '/');
            $this->rootUrl = substr($this->rootUrl, 0, - strlen($adminDir));
        }

        /** 初始化站点信息 */
        if (defined('__TYPECHO_SITE_URL__')) {
            $this->siteUrl = __TYPECHO_SITE_URL__;
        } else if (defined('__TYPECHO_DYNAMIC_SITE_URL__') && __TYPECHO_DYNAMIC_SITE_URL__) {
            $this->siteUrl = $this->rootUrl;
        }

        $this->originalSiteUrl = $this->siteUrl;
        $this->siteUrl = Typecho_Common::url(NULL, $this->siteUrl);
        $this->plugins = unserialize($this->plugins);

        /** 动态判断皮肤目录 */
        $this->theme = is_dir($this->themeFile($this->theme)) ? $this->theme : 'default';

        /** 增加对SSL连接的支持 */
        if ($this->request->isSecure() && 0 === strpos($this->siteUrl, 'http://')) {
            $this->siteUrl = substr_replace($this->siteUrl, 'https', 0, 4);
        }

        /** 自动初始化路由表 */
        $this->routingTable = unserialize($this->routingTable);
        if (!isset($this->routingTable[0])) {
            /** 解析路由并缓存 */
            $parser = new Typecho_Router_Parser($this->routingTable);
            $parsedRoutingTable = $parser->parse();
            $this->routingTable = array_merge(array($parsedRoutingTable), $this->routingTable);
            $this->db->query($this->db->update('table.options')->rows(array('value' => serialize($this->routingTable)))
            ->where('name = ?', 'routingTable'));
        }
    }

我们来看第一行的 fetchAll 方法

/**
     * 一次取出所有行
     *
     * @param mixed $query 查询对象
     * @param array $filter 行过滤器函数,将查询的每一行作为第一个参数传入指定的过滤器中
     * @return array
     */
    public function fetchAll($query, array $filter = NULL)
    {
        //执行查询
        $resource = $this->query($query, self::READ);
        $result = array();

        /** 取出过滤器 */
        if (!empty($filter)) {
            list($object, $method) = $filter;
        }

        //取出每一行
        while ($rows = $this->_adapter->fetch($resource)) {
            //判断是否有过滤器
            $result[] = $filter ? call_user_func(array(&$object, $method), $rows) : $rows;
        }

        return $result;
    }

这里面有两个步骤,第一个是去除所有行,如果没有 filter 参数,就把结果放到 $result 里面,如果有 $filter 参数, 就会调用 $filter 的方法,把并且把 $fiter 方法的返回值放入到 $result 里面。这里面传入了 option->push 方法,等于把结果都传入到了 $option->row 里面,所以可以看到 execute 方法没有接受返回值,因为目的已经达到了。

紧接着初始化了各种参数,比较值得看的就是初始化路由表。

路由表这块就是根据 url 执行相应的 action 。这块我们在需要的地方在具体进入再说。

紧接着就回到了 Init 里面,这里面就初始化了 cookie router 等组件,这些组件我们在到了请求那边再说。

最后我们说一下 User 那行,这块判断了,是否登录,决定是否开启 session。
初始化 User 的时候 获取了 db 和 option,这两个字在前边都初始化了,所以直接就可以拿到数据。
在 execute 这边判断是否登陆以后,如果登陆了,会把 登录的用户信息,放到 $option 里面,然后刷新上次活动时间。

在看看 hasLogin 方法,如果 hasLogin 有值,就利用这个值,如果没有,就通过 cookie 校验,通过后,更新 hasLogin 的值,并且把值返回。

至此,Init 就都跑完了。

下期预告

我们用了两篇跑完了,一个初始化,不要担心,随着我们的初始化的东西越来越多,以后,就会快起来了。

下期,我们来看

/** 注册一个初始化插件 */
Typecho_Plugin::factory('index.php')->begin();

Typecho 源码分析(5)

crazyhl 2019-11-18 22:02:17

分类专栏: # typecho 源码分析 文章标签: php 源码分析 typecho

上一篇我们已经分析完了组件的初始化相关的东西,今天我们继续。插件部分。

正文开始

/** 注册一个初始化插件 */
Typecho_Plugin::factory('index.php')->begin();
12

进入方法内部

    /**
     * 获取实例化插件对象
     *
     * @access public
     * @param string $handle 插件
     * @return Typecho_Plugin
     */
    public static function factory($handle)
    {
        return isset(self::$_instances[$handle]) ? self::$_instances[$handle] :
        (self::$_instances[$handle] = new Typecho_Plugin($handle));
    }
12345678910111213

就是看插件池有没有这个插件,没有就初始化,如果有就返回已存在的。
到这里我们还是没有这个插件的,执行初始化。

/**
     * 插件初始化
     *
     * @access public
     * @param string $handle 插件
     */
    public function __construct($handle)
    {
        /** 初始化变量 */
        $this->_handle = $handle;
    }
1234567891011

这里初始化的时候就是给插件赋值一下。

/** 注册一个结束插件 */
Typecho_Plugin::factory('index.php')->end();
12

这两个 begin 和 end 方法,我们在插件部分再说。

插件到这边就先告一段落。

下期预告

这篇比较短,因为用到的方法,很少或者没有用到。我们下期,说一下 路由部分,这块估计会说比较多的东西。我们下篇再见

Typecho 源码分析(6)

crazyhl 2019-11-21 22:22:11

分类专栏: # typecho 源码分析 文章标签: php 源码分析 typecho

前情提要

上一篇说了一下插件相关的东西,可是发现插件需要很多东西去说,于是就没说全,因为还是要抓紧把全部流程跑通,所以就省略下来了,这篇我们说一下路由相关的。等这个说完,等于就把全部流程跑通了。后面我们就可以展开来说各种模块了。

正文开始

/** 开始路由分发 */
Typecho_Router::dispatch();

index 文件的最后一行了。开始吧,让我们进入方法内部,

 /**
     * 路由分发函数
     *
     * @return void
     * @throws Exception
     */
    public static function dispatch()
    {
        /** 获取PATHINFO */
        $pathInfo = self::getPathInfo();

        foreach (self::$_routingTable as $key => $route) {
            if (preg_match($route['regx'], $pathInfo, $matches)) {
                self::$current = $key;

                try {
                    /** 载入参数 */
                    $params = NULL;

                    if (!empty($route['params'])) {
                        unset($matches[0]);
                        $params = array_combine($route['params'], $matches);
                    }

                    $widget = Typecho_Widget::widget($route['widget'], NULL, $params);

                    if (isset($route['action'])) {
                        $widget->{$route['action']}();
                    }

                    Typecho_Response::callback();
                    return;

                } catch (Exception $e) {
                    if (404 == $e->getCode()) {
                        Typecho_Widget::destory($route['widget']);
                        continue;
                    }

                    throw $e;
                }
            }
        }

        /** 载入路由异常支持 */
        throw new Typecho_Router_Exception("Path '{$pathInfo}' not found", 404);
    }

第一行,获取 pathInfo 我们再次进入方法。

/**
     * 获取全路径
     *
     * @access public
     * @return string
     */
    public static function getPathInfo()
    {
        if (NULL === self::$_pathInfo) {
            self::setPathInfo();
        }

        return self::$_pathInfo;
    }

pathInfo 为 null 的时候执行 set 方法,默认值是 / 。我们看下返回值是什么。
string(1) "/" 返回了一个 / 是默认值。
这个值是从哪里过来的呢,是在 init 里面赋值的,我们看一下方法。

 /**
     * 获取当前pathinfo
     *
     * @access public
     * @param string $inputEncoding 输入编码
     * @param string $outputEncoding 输出编码
     * @return string
     */
    public function getPathInfo($inputEncoding = NULL, $outputEncoding = NULL)
    {
        /** 缓存信息 */
        if (NULL !== $this->_pathInfo) {
            return $this->_pathInfo;
        }

        //参考Zend Framework对pahtinfo的处理, 更好的兼容性
        $pathInfo = NULL;

        //处理requestUri
        $requestUri = $this->getRequestUri();
        $finalBaseUrl = $this->getBaseUrl();

        // Remove the query string from REQUEST_URI
        if ($pos = strpos($requestUri, '?')) {
            $requestUri = substr($requestUri, 0, $pos);
        }

        if ((NULL !== $finalBaseUrl)
            && (false === ($pathInfo = substr($requestUri, strlen($finalBaseUrl)))))
        {
            // If substr() returns false then PATH_INFO is set to an empty string
            $pathInfo = '/';
        } elseif (NULL === $finalBaseUrl) {
            $pathInfo = $requestUri;
        }

        if (!empty($pathInfo)) {
            //针对iis的utf8编码做强制转换
            //参考http://docs.moodle.org/ja/%E5%A4%9A%E8%A8%80%E8%AA%9E%E5%AF%BE%E5%BF%9C%EF%BC%9A%E3%82%B5%E3%83%BC%E3%83%90%E3%81%AE%E8%A8%AD%E5%AE%9A
            if (!empty($inputEncoding) && !empty($outputEncoding) &&
            (stripos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') !== false
            || stripos($_SERVER['SERVER_SOFTWARE'], 'ExpressionDevServer') !== false)) {
                if (function_exists('mb_convert_encoding')) {
                    $pathInfo = mb_convert_encoding($pathInfo, $outputEncoding, $inputEncoding);
                } else if (function_exists('iconv')) {
                    $pathInfo = iconv($inputEncoding, $outputEncoding, $pathInfo);
                }
            }
        } else {
            $pathInfo = '/';
        }

        // fix issue 456
        return ($this->_pathInfo = '/' . ltrim(urldecode($pathInfo), '/'));
    }

这个方法首先获取了 requestUri

/**
     * 获取请求地址
     * 
     * @access public
     * @return string
     */
    public function getRequestUri()
    {
        if (!empty($this->_requestUri)) {
            return $this->_requestUri;
        }

        //处理requestUri
        $requestUri = '/';

        if (isset($_SERVER['HTTP_X_REWRITE_URL'])) { // check this first so IIS will catch
            $requestUri = $_SERVER['HTTP_X_REWRITE_URL'];
        } elseif (
            // IIS7 with URL Rewrite: make sure we get the unencoded url (double slash problem)
            isset($_SERVER['IIS_WasUrlRewritten'])
            && $_SERVER['IIS_WasUrlRewritten'] == '1'
            && isset($_SERVER['UNENCODED_URL'])
            && $_SERVER['UNENCODED_URL'] != ''
            ) {
            $requestUri = $_SERVER['UNENCODED_URL'];
        } elseif (isset($_SERVER['REQUEST_URI'])) {
            $requestUri = $_SERVER['REQUEST_URI'];
            $parts       = @parse_url($requestUri);

            if (isset($_SERVER['HTTP_HOST']) && strstr($requestUri, $_SERVER['HTTP_HOST'])) {
                if (false !== $parts) {
                    $requestUri  = (empty($parts['path']) ? '' : $parts['path'])
                                 . ((empty($parts['query'])) ? '' : '?' . $parts['query']);
                }
            } elseif (!empty($_SERVER['QUERY_STRING']) && empty($parts['query'])) {
                // fix query missing
                $requestUri .= '?' . $_SERVER['QUERY_STRING'];
            }
        } elseif (isset($_SERVER['ORIG_PATH_INFO'])) { // IIS 5.0, PHP as CGI
            $requestUri = $_SERVER['ORIG_PATH_INFO'];
            if (!empty($_SERVER['QUERY_STRING'])) {
                $requestUri .= '?' . $_SERVER['QUERY_STRING'];
            }
        }

        return $this->_requestUri = $requestUri;
    }

我们主要看

} elseif (isset($_SERVER['REQUEST_URI'])) {
            $requestUri = $_SERVER['REQUEST_URI'];
            $parts       = @parse_url($requestUri);

            if (isset($_SERVER['HTTP_HOST']) && strstr($requestUri, $_SERVER['HTTP_HOST'])) {
                if (false !== $parts) {
                    $requestUri  = (empty($parts['path']) ? '' : $parts['path'])
                                 . ((empty($parts['query'])) ? '' : '?' . $parts['query']);
                }
            } elseif (!empty($_SERVER['QUERY_STRING']) && empty($parts['query'])) {
                // fix query missing
                $requestUri .= '?' . $_SERVER['QUERY_STRING'];
            }
        }

这个判断里面的,因为我们主要是 nginx 不是用的iis 。
可以看到最后拿到的 requestUri 就是请求 / 后面的所有加上参数。
紧接着就是获取 $finalBaseUrl 这个就是请求前缀,用于选择目录。
最后在通过 $finalBaseUrl 获取最后的 $pathInfo。
然后再去初始化时候的 route 里面匹配,就找到了 controller 和 method。

下期预告

我们找到了需要调用的方法,我们在下篇在继续

Typecho 源码分析(7)

crazyhl 2019-12-15 22:53:40

分类专栏: # typecho 源码分析 文章标签: php

题外话

两周之前搬了家,以前搬家从来没觉得东西这么多,收拾起来这么麻烦,基本上整理整理就可以过日子了。这次换了个整租,才发现屋子大了也不好,东西找不到,现在基本上算是步入正轨了,不过nas还没有就位,等我再整理整理在看看吧nas放到哪里。机械硬盘实在是太吵了,这次要放到一个安静的地方。

前情提要

第五篇简单分析了插件,其实什么都没说。上一篇分析了路由部分,也说的比较混乱,本周五和周六我用了一些时间,仔细的读了源码,把自己混乱的部分也都弄清了,所以这篇我就慢慢的再说一次路由,用两个请求,首页和文章页 来分析,将来在模块分析的时候也会把路由需要的部分,在分析。

正文开始

我们再次来到路由的 dispatch 方法

/**
     * 路由分发函数
     *
     * @return void
     * @throws Exception
     */
    public static function dispatch()
    {
        /** 获取PATHINFO */
        $pathInfo = self::getPathInfo();

        foreach (self::$_routingTable as $key => $route) {
            if (preg_match($route['regx'], $pathInfo, $matches)) {
                self::$current = $key;

                try {
                    /** 载入参数 */
                    $params = NULL;

                    if (!empty($route['params'])) {
                        unset($matches[0]);
                        $params = array_combine($route['params'], $matches);
                    }

                    $widget = Typecho_Widget::widget($route['widget'], NULL, $params);

                    if (isset($route['action'])) {
                        $widget->{$route['action']}();
                    }

                    Typecho_Response::callback();
                    return;

                } catch (Exception $e) {
                    if (404 == $e->getCode()) {
                        Typecho_Widget::destory($route['widget']);
                        continue;
                    }

                    throw $e;
                }
            }
        }

        /** 载入路由异常支持 */
        throw new Typecho_Router_Exception("Path '{$pathInfo}' not found", 404);
    }

首先获取 pathInfo ,这个 pathInfo 是从那里获取的呢,是从 Init 里面初始化的,我们看下初始化部分

$pathInfo = $this->request->getPathInfo();

进入这个方法

/**
     * 获取当前pathinfo
     *
     * @access public
     * @param string $inputEncoding 输入编码
     * @param string $outputEncoding 输出编码
     * @return string
     */
    public function getPathInfo($inputEncoding = NULL, $outputEncoding = NULL)
    {
        /** 缓存信息 */
        if (NULL !== $this->_pathInfo) {
            return $this->_pathInfo;
        }

        //参考Zend Framework对pahtinfo的处理, 更好的兼容性
        $pathInfo = NULL;

        //处理requestUri
        $requestUri = $this->getRequestUri();
        var_dump($requestUri);
        $finalBaseUrl = $this->getBaseUrl();
        var_dump($requestUri);

        // Remove the query string from REQUEST_URI
        if ($pos = strpos($requestUri, '?')) {
            $requestUri = substr($requestUri, 0, $pos);
        }

        if ((NULL !== $finalBaseUrl)
            && (false === ($pathInfo = substr($requestUri, strlen($finalBaseUrl)))))
        {
            // If substr() returns false then PATH_INFO is set to an empty string
            $pathInfo = '/';
        } elseif (NULL === $finalBaseUrl) {
            $pathInfo = $requestUri;
        }

        if (!empty($pathInfo)) {
            //针对iis的utf8编码做强制转换
            //参考http://docs.moodle.org/ja/%E5%A4%9A%E8%A8%80%E8%AA%9E%E5%AF%BE%E5%BF%9C%EF%BC%9A%E3%82%B5%E3%83%BC%E3%83%90%E3%81%AE%E8%A8%AD%E5%AE%9A
            if (!empty($inputEncoding) && !empty($outputEncoding) &&
            (stripos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') !== false
            || stripos($_SERVER['SERVER_SOFTWARE'], 'ExpressionDevServer') !== false)) {
                if (function_exists('mb_convert_encoding')) {
                    $pathInfo = mb_convert_encoding($pathInfo, $outputEncoding, $inputEncoding);
                } else if (function_exists('iconv')) {
                    $pathInfo = iconv($inputEncoding, $outputEncoding, $pathInfo);
                }
            }
        } else {
            $pathInfo = '/';
        }

        // fix issue 456
        return ($this->_pathInfo = '/' . ltrim(urldecode($pathInfo), '/'));
    }

这个方法了里面第一步,如果有 pathInfo 就返回,如果没有就进入后续的流程,我们这里面肯定是没有的,所以继续后续执行

先获取了 $requestUri ,

/**
     * 获取请求地址
     * 
     * @access public
     * @return string
     */
    public function getRequestUri()
    {
        if (!empty($this->_requestUri)) {
            return $this->_requestUri;
        }

        //处理requestUri
        $requestUri = '/';

        if (isset($_SERVER['HTTP_X_REWRITE_URL'])) { // check this first so IIS will catch
            $requestUri = $_SERVER['HTTP_X_REWRITE_URL'];
        } elseif (
            // IIS7 with URL Rewrite: make sure we get the unencoded url (double slash problem)
            isset($_SERVER['IIS_WasUrlRewritten'])
            && $_SERVER['IIS_WasUrlRewritten'] == '1'
            && isset($_SERVER['UNENCODED_URL'])
            && $_SERVER['UNENCODED_URL'] != ''
            ) {
            $requestUri = $_SERVER['UNENCODED_URL'];
        } elseif (isset($_SERVER['REQUEST_URI'])) {
            $requestUri = $_SERVER['REQUEST_URI'];
            $parts       = @parse_url($requestUri);

            if (isset($_SERVER['HTTP_HOST']) && strstr($requestUri, $_SERVER['HTTP_HOST'])) {
                if (false !== $parts) {
                    $requestUri  = (empty($parts['path']) ? '' : $parts['path'])
                                 . ((empty($parts['query'])) ? '' : '?' . $parts['query']);
                }
            } elseif (!empty($_SERVER['QUERY_STRING']) && empty($parts['query'])) {
                // fix query missing
                $requestUri .= '?' . $_SERVER['QUERY_STRING'];
            }
        } elseif (isset($_SERVER['ORIG_PATH_INFO'])) { // IIS 5.0, PHP as CGI
            $requestUri = $_SERVER['ORIG_PATH_INFO'];
            if (!empty($_SERVER['QUERY_STRING'])) {
                $requestUri .= '?' . $_SERVER['QUERY_STRING'];
            }
        }

        return $this->_requestUri = $requestUri;
    }

进入方法内部,第一步还是判断是否存在,不存在就从 $_SERVER 中获取相关参数,因为我们是在nginx中,所以在下面这个判断中获取参数

} elseif (isset($_SERVER['REQUEST_URI'])) {
            $requestUri = $_SERVER['REQUEST_URI'];
            $parts       = @parse_url($requestUri);

            if (isset($_SERVER['HTTP_HOST']) && strstr($requestUri, $_SERVER['HTTP_HOST'])) {
                if (false !== $parts) {
                    $requestUri  = (empty($parts['path']) ? '' : $parts['path'])
                                 . ((empty($parts['query'])) ? '' : '?' . $parts['query']);
                }
            } elseif (!empty($_SERVER['QUERY_STRING']) && empty($parts['query'])) {
                // fix query missing
                $requestUri .= '?' . $_SERVER['QUERY_STRING'];
            }
        }

获得 REQUEST_URI,紧接着解析 用 parse_url 解析 获取到的 uri 得到 parts,紧接着判断 如果

    if (isset($_SERVER['HTTP_HOST']) && strstr($requestUri, $_SERVER['HTTP_HOST'])) {
                if (false !== $parts) {
                    $requestUri  = (empty($parts['path']) ? '' : $parts['path'])
                                 . ((empty($parts['query'])) ? '' : '?' . $parts['query']);
                }
            } elseif (!empty($_SERVER['QUERY_STRING']) && empty($parts['query'])) {
                // fix query missing
                $requestUri .= '?' . $_SERVER['QUERY_STRING'];
            }

server 的 host 存在 并且 uri 在 host 里面,就判断解析的 parts 是否为 false, 然后拼接 uri,这里if (isset($_SERVER['HTTP_HOST']) && strstr($requestUri, $_SERVER['HTTP_HOST']))是 false 所以走下面的判断逻辑,

    elseif (!empty(_SERVER['QUERY_STRING']) && empty(parts['query']))

当前这个url

http://typecho.test/index.php/archives/1/

也是false,所以请求的 uri 就是

/index.php/archives/1/

紧接着获取 getBaseUrl

/**
     * getBaseUrl  
     * 
     * @access public
     * @return string
     */
    public function getBaseUrl()
    {
        if (NULL !== $this->_baseUrl) {
            return $this->_baseUrl;
        }

        //处理baseUrl
        $filename = (isset($_SERVER['SCRIPT_FILENAME'])) ? basename($_SERVER['SCRIPT_FILENAME']) : '';

        if (isset($_SERVER['SCRIPT_NAME']) && basename($_SERVER['SCRIPT_NAME']) === $filename) {
            $baseUrl = $_SERVER['SCRIPT_NAME'];
        } elseif (isset($_SERVER['PHP_SELF']) && basename($_SERVER['PHP_SELF']) === $filename) {
            $baseUrl = $_SERVER['PHP_SELF'];
        } elseif (isset($_SERVER['ORIG_SCRIPT_NAME']) && basename($_SERVER['ORIG_SCRIPT_NAME']) === $filename) {
            $baseUrl = $_SERVER['ORIG_SCRIPT_NAME']; // 1and1 shared hosting compatibility
        } else {
            // Backtrack up the script_filename to find the portion matching
            // php_self
            $path    = isset($_SERVER['PHP_SELF']) ? $_SERVER['PHP_SELF'] : '';
            $file    = isset($_SERVER['SCRIPT_FILENAME']) ? $_SERVER['SCRIPT_FILENAME'] : '';
            $segs    = explode('/', trim($file, '/'));
            $segs    = array_reverse($segs);
            $index   = 0;
            $last    = count($segs);
            $baseUrl = '';
            do {
                $seg     = $segs[$index];
                $baseUrl = '/' . $seg . $baseUrl;
                ++$index;
            } while (($last > $index) && (false !== ($pos = strpos($path, $baseUrl))) && (0 != $pos));
        }

        // Does the baseUrl have anything in common with the request_uri?
        $finalBaseUrl = NULL;
        $requestUri = $this->getRequestUri();

        if (0 === strpos($requestUri, $baseUrl)) {
            // full $baseUrl matches
            $finalBaseUrl = $baseUrl;
        } else if (0 === strpos($requestUri, dirname($baseUrl))) {
            // directory portion of $baseUrl matches
            $finalBaseUrl = rtrim(dirname($baseUrl), '/');
        } else if (!strpos($requestUri, basename($baseUrl))) {
            // no match whatsoever; set it blank
            $finalBaseUrl = '';
        } else if ((strlen($requestUri) >= strlen($baseUrl))
            && ((false !== ($pos = strpos($requestUri, $baseUrl))) && ($pos !== 0)))
        {
            // If using mod_rewrite or ISAPI_Rewrite strip the script filename
            // out of baseUrl. $pos !== 0 makes sure it is not matching a value
            // from PATH_INFO or QUERY_STRING
            $baseUrl = substr($requestUri, 0, $pos + strlen($baseUrl));
        }

        return ($this->_baseUrl = (NULL === $finalBaseUrl) ? rtrim($baseUrl, '/') : $finalBaseUrl);
    }
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162

首先获取从 server 的 SCRIPT_FILENAME 获取 $filename ,如果SCRIPT_FILENAME 存在,则用 basename 方法获取 $filename ,当前的 filename 是 index.php 。basenme 方法的作用就是返回路径中的文件名,当前 如果 SCRIPT_FILENAME 值是 /var/www/typecho/index.php ,所以文件名就是

index.php

紧接着判断 server 中的 SCRIPT_NAME 或 PHP_SELF 的内容经过 basename 处理后的文件名是否跟 filename 相同。我们的请求在 SCRIPT_NAME 这里的判断就符合了条件,所以 baseurl 就是 server 中的 SCRIPT_NAME 的值。

/index.php

接下来 判断 baseurl 或 baseurl 的 dirname 在 requesturi 中是否开头,我们这里的场景是

else if (0 === strpos($requestUri, dirname($baseUrl))) 

这里的判断中达成的,所以

$finalBaseUrl = rtrim(dirname($baseUrl), '/');

就是吧 dirname 后的 $baseUrl 去掉右侧的/后的值。最后 baseurl 就是

(NULL === $finalBaseUrl) ? rtrim($baseUrl, '/') : $finalBaseUrl

判断 finalBaseUrl 是否为 null ,如果为 null 就是把 baseurl 去掉右侧的 / ,否则就是 finalBaseUrl 。我们这里 finalBaseUrl 不是 null,所以 baseurl就是 finalBaseUrl。为 /index.php。

接下来判断 requesturi 中是否包含 ? ,如果包含,就截取 ? 前面的不部分,我们这边不包含,所以 requesturi 依然是

/index.php/archives/1/

然后判断 $finalBaseUrl 是否为 null, 如果是 null ,$pathInfo = $requestUri; ,如果不是并且

false===(pathInfo =substr(requestUri, strlen(finalBaseUrl))) 

substr 后的 pathinfo 是 false ,就是没有提取到子串的时候 pathinfo 是 /。 我们的场景下,成功提取到了,所以 pathinfo 就是

/archives/1/

接下来,pathinfo 不为空的时候对 iis 请求的编码,这里不存在,就忽略了,如果pathinfo是空,就赋值 /。最后完整的 pathinfo就是

'/' . ltrim(urldecode($pathInfo), '/')

去掉左侧的 / 在拼接一个 / 这个目的就是防止做的没有 /。 最后 pathinfo就是

/archives/1/

最后把,Typecho_Router::setPathInfo($pathInfo); 设置到路由里面。

接下来就回到了路由的 dispatch 方法。首先获取一下 pathinfo 。
然后用配置里面的 routeTable 进行匹配,这个 routeTable 就是在数据库里面的配置,可以看 option 表里面的数据。

遍历 routeTable ,用路由里面的 regex 来匹配 pathInfo ,如果没有匹配到,就抛出 路由 没有匹配到的 404。

如果匹配到了,把 路由的 key 设置到 current 。

如果设置了路由的 params ,就把匹配到的参数跟 params 组合成数组。例如

if (!empty($route['params'])) {    
    unset($matches[0]);    $params = array_combine($route['params'], $matches)
;}



http://typecho.test/index.php/archives/1/

array(6) { ["url"]=> string(24) "/archives/[cid:digital]/" ["widget"]=> string(14) "Widget_Archive" ["action"]=> string(6) "render" ["regx"]=> string(26) "|^/archives/([0-9]+)[/]?$|" ["format"]=> string(13) "/archives/%s/" ["params"]=> array(1) { [0]=> string(3) "cid" } } array(2) { [0]=> string(12) "/archives/1/" [1]=> string(1) "1" }

上面这种路径的话,就会把匹配到的 1 跟 params 组合,合成参数数组

array(1) { ["cid"]=> string(1) "1" }

给后续的方法使用。

紧接着初始化,路由对应的组件,上面这个文章详情的例子就是 Widget_Archive ,
然后,判断是否设置了 路由的 action , 如果设置了就执行这个方法

if (isset($route['action'])) {    
    $widget->{$route['action']}();
}

最后调用,

Typecho_Response::callback();

最后就返回了,如果执行相关方法出错了,就执行异常部分。

if (404 == $e->getCode()) {
    Typecho_Widget::destory($route['widget']);    
    continue;
}
throw $e;

到这,整个路由就跑完了,大家可以多多的测试各种页面看看各种结果。

下期预告

下次我们就来具体的分析插件,这个好玩的东西,刚开始学 php 的时候,就觉得很高级,后来看过 thinkphp3.2 的源码的时候也在其他地方看到了类似的东西,这个做法真的很好玩。敬请期待。

Typecho 源码分析(8)-- 后台插件列表

crazyhl 2019-12-21 22:35:25

分类专栏: # typecho 源码分析 文章标签: php

前情提要

前面 7 篇文章基本上已经分析完成 Typecho 的运行流程了,从本篇开始就开始分析各种模块了,原本是想分析路由的,但是我更对插件感兴趣,所以就从插件开始了。

正文开始

我们先不分析插件的加载流程,因为我们还没有启用任何插件,所以我们从后台的插件列表开始。我们打开 admin\plugins.php 文件。前面的几行加载我们稍后再说,直奔重点插件列表而去。

<?php Typecho_Widget::widget('Widget_Plugins_List@activated', 'activated=1')->to($activatedPlugins); ?>

这行代码会获取启用的插件列表,我们来到 Widget\Plugins\List.php 文件,为什么是这个文件,请看前三篇文章和 var\Typecho\Common.php 的 __autoLoad 方法就了解了。

调用这个文件的 execute 方法,

首先获取插件目录,设置默认参数,获取已经启用的插件,把已经启用的插件放到

$this->activatedPlugins = $plugins['activated'];

里面,紧接着如果插件目录存在,就开始遍历插件目录。
获取插件,根据目录和文件的形态,返回插件信息

return array($pluginName, $pluginFileName);

接下来获取插件信息,
判断插件版本,判断插件是否在数据库中启用,如果启用了,就放到插件list 的栈中。

上面的是获取激活的插件部分,第二部分是获取禁用的插件,我们用禁用的部分测试一下上面的对不对,
插件目录

array(1) { [0]=> string(40) "/var/www/typecho//usr/plugins/HelloWorld" }

设置默认参数,

$this->parameter->setDefault(array('activated' => NULL));

这边传入的的是null,返回的却是0。

object(Typecho_Config)#27 (1) {
  ["_currentConfig":"Typecho_Config":private]=>
  array(1) {
    ["activated"]=>
    string(1) "0"
  }
}

是因为在初始化 List 组件的时候,我们传入了 params activated=0,这边相关的可以去看 组件 初始化的部分,
以及 $this->parameter->setDefault($params) 这个方法的具体实现。

接下来导出数据库里面激活的插件信息,现在还是空的,

 $this->activatedPlugins = $plugins['activated'];

把 activated 放到 $this->activatedPlugins 中。

如果插件目录不为空,就开始遍历插件目录里面的插件,剩下的就可以参照上面的说明了。

/** 默认即插即用 */
$info['activated'] = true;

if ($info['activate'] || $info['deactivate'] || $info['config'] || $info['personalConfig']) {
    $info['activated'] = isset($this->activatedPlugins[$pluginName]);

    if (isset($this->activatedPlugins[$pluginName])) {
        unset($this->activatedPlugins[$pluginName]);
    }
}

if ($info['activated']  == $this->parameter->activated) {
    $this->push($info);
}

说说这块吧,默认激活的,然后进入if判断,判断已激活的插件是否在数据存储中,如果存在,就在 activatedPlugins 卸载掉,这一步就是为了防止数据重复。

$info['activated'] == $this->parameter->activated 这行判断,就是看跟传入的参数是否相同,如果相同就放入到组件的 stack 中。

页面html部分就自己看看好了,不分析了。

下期预告

这篇开始就不会大篇幅的贴源码了,只贴一些重要的东西,大家要自己去看源码了,自己追踪一下总会更好,下篇会是一个短篇,分析部分的 $security 这个组件

Typecho 源码分析(9)-- 部分Security&User 组件分析

crazyhl 2019-12-24 22:45:41

分类专栏: # typecho 源码分析 文章标签: php

前情提要

前面我们分析了插件列表,但是 html 部分我们没有分析,今天我们就来分析一下 url 生成部分。

正文开始

后台的 common.php 会加载 Security 组件。
先看 Security 的 execute 方法,

/**
     * 初始化函数
     *
     */
    public function execute()
    {
        $this->_options = $this->widget('Widget_Options');
        $user = $this->widget('Widget_User');

        $this->_token = $this->_options->secret;
        if ($user->hasLogin()) {
            $this->_token .= '&' . $user->authCode . '&' . $user->uid;
        }
    }

首先会加载 Options ,然后会加载 User 组件。
我们再去看 User 组件,这个组件的构造方法会加载 Option,再去看 User 的 execute 方法,

public function execute()
    {
        if ($this->hasLogin()) {
            $rows = $this->db->fetchAll($this->db->select()
            ->from('table.options')->where('user = ?', $this->_user['uid']));

            $this->push($this->_user);

            foreach ($rows as $row) {
                $this->options->__set($row['name'], $row['value']);
            }

            //更新最后活动时间
            $this->db->query($this->db
            ->update('table.users')
            ->rows(array('activated' => $this->options->time))
            ->where('uid = ?', $this->_user['uid']));
        }
    }

这里会判断是否登陆,我们先去看 hasLogin 方法,

public function hasLogin()
    {
        if (NULL !== $this->_hasLogin) {
            return $this->_hasLogin;
        } else {
            $cookieUid = Typecho_Cookie::get('__typecho_uid');
            if (NULL !== $cookieUid) {
                /** 验证登陆 */
                $user = $this->db->fetchRow($this->db->select()->from('table.users')
                ->where('uid = ?', intval($cookieUid))
                ->limit(1));

                $cookieAuthCode = Typecho_Cookie::get('__typecho_authCode');
                if ($user && Typecho_Common::hashValidate($user['authCode'], $cookieAuthCode)) {
                    $this->_user = $user;
                    return ($this->_hasLogin = true);
                }

                $this->logout();
            }

            return ($this->_hasLogin = false);
        }
    }

如果 _hasLogin 不是 NULL 就返回当前状态,如果不是,就去 cookie 里面获取 uid,如果 uid 不是 NULL,就去数据区获取这个用户,然后在去 cookie 获取 __typecho_authCode 值,如果用户存在,并且 __typecho_authCode 通过了验证,就把查到的 user 放到 User 的 _user 值中,并且把 _hasLogin 设置为真返回,如果没有通过验证,就退出用户,并且把 _hasLogin 设置为假返回。

现在我们去看看,Typecho_Common::hashValidate 方法,

public static function hashValidate($from, $to)
    {
        if ('$T$' == substr($to, 0, 3)) {
            $salt = substr($to, 3, 9);
            return self::hash($from, $salt) === $to;
        } else {
            return md5($from) === $to;
        }
    }

这里就是个验证算法,大家看下就好,我们就不多说了,包括里面的 hash 方法。logout 方法,我们在登录部分再说。

回到 User 的 execute 方法,如果登录成功了,就去 Options 里面获取喝这个登录用户的单独配置,然后把登录的用户 _user 放到组件的 stack 中,接着遍历用户的配置,放到 options 变量里面,最后刷新这个用户的活跃时间,我们看下 push 这个方法

public function push(array $value)
    {
        //将行数据按顺序置位
        $this->row = $value;
        $this->length ++;

        $this->stack[] = $value;
        return $value;
    }

把放入的 value 放到组件的 row 中,把组件的 length 加一,再把值放入到 stack 中,返回 value,row 和 stack 中,我们后面再说。

再次回到 Security 的 execute 方法,
从选项中获取 secret 放入到组件的 token 中,如果用户登录了,在拼接登录用户的 authCode 和 uid 到组件的 token 中。

我们回到 plugins.php 文件,看未启用插件列表代码部分的这行代码

<a href="<?php $security->index('/action/plugins-edit?activate=' . $deactivatedPlugins->name); ?>"><?php _e('启用'); ?></a>

主要看 $security->index 这个方法,我们进入方法内部去看。

public function index($path)
    {
        echo $this->getIndex($path);
    }

这个方法调用了内部的 getIndex 方法并输出出来,看 getIndex 方法。

public function getIndex($path)
    {
        return Typecho_Common::url($this->getTokenUrl($path), $this->_options->index);
    }

先看 $this->_options->index 的值是多少,这个值在 Option 的 execute 方法并没有进行赋值,我们可以看到,Option 里面有个 ___index 方法,但是我们获取 index 的值的时候为什么会调用这个方法呢,我们可以看 Option 的基类里面的魔术方法

protected function ___index()
    {
        return ($this->rewrite || (defined('__TYPECHO_REWRITE__') && __TYPECHO_REWRITE__)) 
            ? $this->rootUrl : Typecho_Common::url('index.php', $this->rootUrl);
    }

public function __get($name)
    {
        if (array_key_exists($name, $this->row)) {
            return $this->row[$name];
        } else {
            $method = '___' . $name;

            if (method_exists($this, $method)) {
                return $this->$method();
            } else {
                $return = $this->pluginHandle()->trigger($plugged)->{$method}($this);
                if ($plugged) {
                    return $return;
                }
            }
        }

        return NULL;
    }

可以看到这里面,会查询 $method = '___' . $name 这样的方法名是否存在,如果存在就会调用这个方法。
我们再看下 ___index 方法运行了什么,在开启了 rewrite 以后,就会返回 rootUrl,如果没有开启就会生成 url,我们看下 url 的生成方法。

Typecho_Common::url('index.php', $this->rootUrl)

这个方法传入了两个参数,第一个是 index.php 和 rootUrl,那么 rootUrl 是怎么来的呢。这个值在 Option 的 execute 方法生成,我们看下

$this->rootUrl = defined('__TYPECHO_ROOT_URL__') ? __TYPECHO_ROOT_URL__ : $this->request->getRequestRoot();
if (defined('__TYPECHO_ADMIN__')) {
            /** 识别在admin目录中的情况 */
            $adminDir = '/' . trim(defined('__TYPECHO_ADMIN_DIR__') ? __TYPECHO_ADMIN_DIR__ : '/admin/', '/');
            $this->rootUrl = substr($this->rootUrl, 0, - strlen($adminDir));
        }

如果设置了 __TYPECHO_ROOT_URL__ 就返回 __TYPECHO_ROOT_URL__ 没有设置就调用 Request 的 getRequestRoot 方法。然后在判断,是不是 /admin 结尾,如果是以这个结尾就说明是

 public function getRequestRoot()
    {
        if (NULL === $this->_requestRoot) {
            $root = rtrim(self::getUrlPrefix() . $this->getBaseUrl(), '/') . '/';

            $pos = strrpos($root, '.php/');
            if ($pos) {
                $root = dirname(substr($root, 0, $pos));
            }

            $this->_requestRoot = rtrim($root, '/');
        }

        return $this->_requestRoot;
    }

先获取 getUrlPrefix ,

 public static function getUrlPrefix()
    {
        if (empty(self::$_urlPrefix)) {
            if (defined('__TYPECHO_URL_PREFIX__')) {
                self::$_urlPrefix == __TYPECHO_URL_PREFIX__;
            } else if (!defined('__TYPECHO_CLI__')) {
                self::$_urlPrefix = (self::isSecure() ? 'https' : 'http') . '://' 
                    . (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : $_SERVER['SERVER_NAME']);
            }
        }

        return self::$_urlPrefix;
    }

这个方法就是拼接了 server 的参数,获取了完整的请求url 域名部分。

紧接着调用了 getBaseUrl,这个就是获取了请求的文件,这个方法的解析请看前面的文章。

生成了 root 之后,会判断 '.php/' 的位置,如果查到了这个字符串,就会获取一下 root 的 dirname,这个就可以理解为,把最后的文件过滤掉,保留前面的部分。

好了,我们看下 url 方法

public static function url($path, $prefix)
    {
        $path = (0 === strpos($path, './')) ? substr($path, 2) : $path;
        return rtrim($prefix, '/') . '/' . str_replace('//', '/', ltrim($path, '/'));
    }

会先判断path是否已 ./ 开头,如果是,就截取一下,然后在把 prefix 放到前面,拼接 path 。

再看 getTokenUrl 方法

public function getTokenUrl($path)
    {
        $parts = parse_url($path);
        $params = array();

        if (!empty($parts['query'])) {
            parse_str($parts['query'], $params);
        }

        $params['_'] = $this->getToken($this->request->getRequestUrl());
        $parts['query'] = http_build_query($params);

        return Typecho_Common::buildUrl($parts);
    }

解析 path,如果解析的 url 包含 query ,就再次生成params,在调用 getToken 生成加密的串,这个生成就是 md5 一下。最后在生成url。

可以看到生成的 url 就是 http://typecho.test/index.php/action/plugins-edit?activate=HelloWorld&_=4d799a66e315807b50ca3773ede882f3。

下期预告

这篇文章说的比较乱,大家需要自己好好的多读一读,这样才能更好的理解后面的东西。下篇我们还在 plugins.php 里面徘徊。我们看更多的东西,下期再见。

Typecho 源码分析(10)-- DoAction 分析

crazyhl 2019-12-29 22:39:18

分类专栏: # typecho 源码分析

要

前面我们分析了插件列表,看到 html 部分,我们看到了请求的url 包含 action 我们这篇就分析这里。

正文开始

在做到插件启用相关部分的时候。发现了一个链接,

http://typecho.test/index.php/action/plugins-edit?activate=HelloWorld&_=a05deb76f571cfb798d3904cc6cecf77

这块我就比较好奇了,为什么跟首页部分的不太一样吗,难道是又包装了一层?我们从路由分发来看一下。

array(6) { ["url"]=> string(22) "/action/[action:alpha]" ["widget"]=> string(9) "Widget_Do" ["action"]=> string(6) "action" ["regx"]=> string(32) "|^/action/([_0-9a-zA-Z-]+)[/]?$|" ["format"]=> string(10) "/action/%s" ["params"]=> array(1) { [0]=> string(6) "action" } }

可以看到,这个路由,匹配了 action 开头的 url,执行的组件是 Do,参数是一个action。具体路由的分析请看前几篇文章。

我们看下 Do 的执行部分

public function execute()
{
    /** 验证路由地址 **/
    $action = $this->request->action;

    //兼容老版本
    if (empty($action)) {
        $widget = trim($this->request->widget, '/');
        $objectName = 'Widget_' . str_replace('/', '_', $widget);

        if (preg_match("/^[_a-z0-9]$/i", $objectName) && Typecho_Common::isAvailableClass($objectName)) {
            $widgetName = $objectName;
        }
    } else {
        /** 判断是否为plugin */
        $actionTable = array_merge($this->_map, unserialize($this->widget('Widget_Options')->actionTable));

        if (isset($actionTable[$action])) {
            $widgetName = $actionTable[$action];
        }
    }

    if (isset($widgetName) && class_exists($widgetName)) {
        $reflectionWidget =  new ReflectionClass($widgetName);
        if ($reflectionWidget->implementsInterface('Widget_Interface_Do')) {
            $this->widget($widgetName)->action();
            return;
        }
    }

    throw new Typecho_Widget_Exception(_t('请求的地址不存在'), 404);
}

先获取 请求的 action ,这个值是从构造组件的时候传递过来的,

$widget = Typecho_Widget::widget($route['widget'], NULL, $params);

$params 这个值就会传入到 request 的构造中,看下面

$requestObject = new Typecho_Request();$requestObject->setParams($request);

具体大家可以追一下代码就了解了。

通过判断 action 是否为空来决定 widgetname,判断的部分大家可以自己看一下。

紧接着,会去用反射相关的方法去调用具体的逻辑。

反射我们会单独找一篇文章去说明。对了,还有一个判断,是会这个组件是否实现了 Widget_Interface_Do 接口。

下期预告

这篇比较短,但是不乱,所以可以说是很简洁了,下篇我们继续在插件查遍看看还有什么要说的。

标签: Typecho, 源码分析

评论已关闭