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，就要分析很多引入库了。另外，这个代码的注释写的太好了。当然了，后续我也会分析这种代码的。 我分析的流程 分析代码有很多种 一个文件分析完，在分析另一个 遇到一个方法或者流程的改变就进入到相应方法或者流程 我在这边选用第二种，这样更符合我的风格 正文开始 进入 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 ' '; echo ' ' . htmlspecialchars($exception->getMessage()) . ' '; echo htmlspecialchars($exception->__toString()); echo ' '; } 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 {$code} {$message} 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; ?>>1<?php _e('欢迎使用'); ?> <li<?php if (isset($_GET['config'])) : ?> class="current"<?php endif; ?>>2<?php _e('初始化配置'); ?> <li<?php if (isset($_GET['start'])) : ?> class="current"<?php endif; ?>>3<?php _e('开始安装'); ?> <li<?php if (isset($_GET['finish'])) : ?> class="current"<?php endif; ?>>4<?php _e('安装成功'); ?> 这块就是根据 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'; ?> 不同的适配器会加载不同的数据库配置页面，我们这边用的是 mysql ，我们进入 install/mysql.php 页面，里面有很多环境的判断 sae、gae、bae 什么的判定。这些我们都跳过，直接看下面。 <?php else: ?> <?php _e('数据库地址'); ?> <input type="text" class="text" name="dbHost" id="dbHost" value="<?php _v('dbHost', 'localhost'); ?>"/> <?php _e('您可能会使用 "%s"', 'localhost'); ?> <?php _e('数据库端口'); ?> <input type="text" class="text" name="dbPort" id="dbPort" value="<?php _v('dbPort', '3306'); ?>"/> <?php _e('如果您不知道此选项的意义, 请保留默认设置'); ?> <?php _e('数据库用户名'); ?> <input type="text" class="text" name="dbUser" id="dbUser" value="<?php _v('dbUser', 'root'); ?>" /> <?php _e('您可能会使用 "%s"', 'root'); ?> <?php _e('数据库密码'); ?> <input type="password" class="text" name="dbPassword" id="dbPassword" value="<?php _v('dbPassword'); ?>" /> <?php _e('数据库名'); ?> <input type="text" class="text" name="dbDatabase" id="dbDatabase" value="<?php _v('dbDatabase', 'typecho'); ?>" /> <?php _e('请您指定数据库名称'); ?> <?php endif; ?> <input type="hidden" name="dbCharset" value="<?php _e('utf8'); ?>" /> <?php _e('数据库编码'); ?> <option value="utf8"<?php if (_r('dbCharset') == 'utf8'): ?> selected<?php endif; ?>>utf8 <option value="utf8mb4"<?php if (_r('dbCharset') == 'utf8mb4'): ?> selected<?php endif; ?>>utf8mb4 <?php _e('选择 utf8mb4 编码至少需要 MySQL 5.5.3 版本'); ?> <?php _e('数据库引擎'); ?> <option value="MyISAM"<?php if (_r('dbEngine') == 'MyISAM'): ?> selected<?php endif; ?>>MyISAM <option value="InnoDB"<?php if (_r('dbEngine') == 'InnoDB'): ?> selected<?php endif; ?>>InnoDB 这块就是我们显示配置的位置哟。 我们填写完配置信息以后，post 提交当前页面，进入各种判定的部分。 if (_r('created') && !file_exists('./config.inc.php')) { echo ' ' . _t('没有检测到您手动创建的配置文件, 请检查后再次创建') . ' '; $success = false; } else { if (NULL == _r('userUrl')) { $success = false; echo ' ' . _t('请填写您的网站地址') . ' '; } else if (NULL == _r('userName')) { $success = false; echo ' ' . _t('请填写您的用户名') . ' '; } else if (NULL == _r('userMail')) { $success = false; echo ' ' . _t('请填写您的邮箱地址') . ' '; } else if (32 < strlen(_r('userName'))) { $success = false; echo ' ' . _t('用户名长度超过限制, 请不要超过 32 个字符') . ' '; } else if (200 < strlen(_r('userMail'))) { $success = false; echo ' ' . _t('邮箱长度超过限制, 请不要超过 200 个字符') . ' '; } } 这部分会进行一些判定相关的东西。不符合规范的会进行报错 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 ' ' . _t('对不起, 无法连接数据库, 请先检查数据库配置再继续进行安装') . ' '; } catch (Typecho_Db_Exception $e) { $success = false; echo ' ' . _t('安装程序捕捉到以下错误: " %s ". 程序被终止, 请检查您的配置信息.',$e->getMessage()) . ' '; } } 这块是获取数据库连接配置，然后对数据库进行连接，数据库相关的代码是 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')) { ?> <?php _e('安装程序无法自动创建 config.inc.php 文件'); ?> <?php _e('您可以在网站根目录下手动创建 config.inc.php 文件, 并复制如下代码至其中'); ?> <?php echo htmlspecialchars($contents); ?> 创建完毕, 继续安装 » <?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)) : ?> <?php _e('安装失败!'); ?> <?php _e('您没有上传 config.inc.php 文件, 请您重新安装!'); ?> <?php _e('重新安装 »'); ?> <?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 ' ' . _t('已经删除完原有数据') . ' ' . _t('继续安装 »') . ' '; } 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 ' ' . _t('安装程序检查到原有数据表已经存在.') . ' ' . ' ' . _t('删除原有数据') . ' ' . _t('或者') . ' ' . _t('使用原有数据') . ' '; } 这块就是在异常的时候如果数据库存在，的判断过程。删库或者使用原有数据库，然后等我们决策后，在决定安装流程。 结语 至此，安装全部搞定了，我们分析完了一个安装模块，接下来说些什么呢，先说前台，部分，然后在说明上面没说明的 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('启用'); ?> 主要看 $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 接口。 下期预告 这篇比较短，但是不乱，所以可以说是很简洁了，下篇我们继续在插件查遍看看还有什么要说的。