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,没有各种新奇的东西,而且还有很多 phphtml 混排的代码。但是他的代码我认为我可以分析的比较清楚,没有更多外部的引用,让我们可以全身心的投入到它本身的代码中去。不用考虑各种引用的代码,让我们在深入进去,比如如果分析 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_pathget_include_path 这两个方法可以看 https://www.jianshu.com/p/303feaaeded1 这篇文章,这块还得说下 PATH_SEPARATOR 这个东西,不仅是 PATH_SEPARATOR 还有 DIRECTORY_SEPARATOR 这两个分隔符,我们写代码的时候容易写死,但是实际上用这种定义好的常量,他会根据系统来决定用什么分隔符,这就很容易避免 windows 和 linux 系统不一致的区别,导致代码报错。set_include_pathget_include_path 配置好了系统引入路径和 typecho 自定义的引入路径,包括了 var 、插件目录,这两个字目录。以后再引入文件的时候,系统就会根据设置的这些目录,引入相关文件了。这块代码,再现在我们大量使用 namespace,以及 comopser 使用的概率很低了。关于 composer 的一些东西,我会在另一篇去说明。

先暂停一下

今天就先到这吧,我决定明天写完 composer 用的 spl_autoload_registerset_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,初始化的时候,会把 getpost 参数,设置到 request$_widgetAlias 里面。然后调用了 requestsetParamswidget 方法传入的 params 参数传入,这个方法,会把 params 设置到 request_params 里面。当然,如果传入的 request 参数不是空,则会获取已经存在的 request,不过 requestgetInstance 方法中不存在 request 实例的话,也会重新初始化一下。

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

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

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

下期预告

紧接着我们看看 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 的时候 获取了 dboption,这两个字在前边都初始化了,所以直接就可以拿到数据。
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

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

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

下期预告

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

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;
    }

pathInfonull 的时候执行 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 里面匹配,就找到了 controllermethod

下期预告

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

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'];
            }

serverhost 存在 并且 urihost 里面,就判断解析的 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

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

index.php

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

/index.php

接下来 判断 baseurlbaseurldirnamerequesturi 中是否开头,我们这里的场景是

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 后的 pathinfofalse ,就是没有提取到子串的时候 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" }

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

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 组件。
先看 Securityexecute 方法,

/**
     * 初始化函数
     *
     */
    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,再去看 Userexecute 方法,

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 方法,我们在登录部分再说。

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

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

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

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

再次回到 Securityexecute 方法,
从选项中获取 secret 放入到组件的 token 中,如果用户登录了,在拼接登录用户的 authCodeuid 到组件的 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 的值是多少,这个值在 Optionexecute 方法并没有进行赋值,我们可以看到,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.phprootUrl,那么 rootUrl 是怎么来的呢。这个值在 Optionexecute 方法生成,我们看下

$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__ 没有设置就调用 RequestgetRequestRoot 方法。然后在判断,是不是 /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/' 的位置,如果查到了这个字符串,就会获取一下 rootdirname,这个就可以理解为,把最后的文件过滤掉,保留前面的部分。

好了,我们看下 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, 源码分析

评论已关闭