Typecho 源码分析(转)
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 '<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">创建完毕, 继续安装 »</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('重新安装 »'); ?></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('继续安装 »') . '</button></p>';
} elseif (_r('goahead')) {
//使用原有数据
//但是要更新用户网站
$installDb->query($installDb->update('table.options')->rows(array('value' => $config['siteUrl']))->where('name = ?', 'siteUrl'));
unset($_SESSION['typecho']);
header('Location: ./install.php?finish&use_old');
exit;
} else {
echo '<p class="message error">' . _t('安装程序检查到原有数据表已经存在.')
. '<br /><br />' . '<button type="submit" name="delete" value="1" class="btn btn-warn">' . _t('删除原有数据') . '</button> '
. _t('或者') . ' <button type="submit" name="goahead" value="1" class="btn primary">' . _t('使用原有数据') . '</button></p>';
}
这块就是在异常的时候如果数据库存在,的判断过程。删库或者使用原有数据库,然后等我们决策后,在决定安装流程。
结语
至此,安装全部搞定了,我们分析完了一个安装模块,接下来说些什么呢,先说前台,部分,然后在说明上面没说明的 db
部分,最后说后台。我们下次再见
Typecho 源码分析(3)
crazyhl 2019-11-13 21:51:20
分类专栏: # typecho 源码分析 文章标签: php typecho 源码分析
前情提要
通过之前我们的分析,我们已经把安装流程搞定可,本篇开始,我们分析前台相关的流程。
正文开始
我们又一次回到了 index.php
在第一篇说完前几行判断是否安装之后,我们就暂时离开了这个文件,当我们安装完以后,进入首页就又回到了这里,我们跳过判断安装那一块,直接看后面。
/** 初始化组件 */
Typecho_Widget::widget('Widget_Init');
/** 注册一个初始化插件 */
Typecho_Plugin::factory('index.php')->begin();
/** 开始路由分发 */
Typecho_Router::dispatch();
/** 注册一个结束插件 */
Typecho_Plugin::factory('index.php')->end();
别看只有4行,却是大量的精髓都在这边,我们一行一行看。
第一行,初始化组件,我们进入代码
/**
* 工厂方法,将类静态化放置到列表中
*
* @access public
* @param string $alias 组件别名
* @param mixed $params 传递的参数
* @param mixed $request 前端参数
* @param boolean $enableResponse 是否允许http回执
* @return Typecho_Widget
* @throws Typecho_Exception
*/
public static function widget($alias, $params = NULL, $request = NULL, $enableResponse = true)
{
$parts = explode('@', $alias);
$className = $parts[0];
$alias = empty($parts[1]) ? $className : $parts[1];
if (isset(self::$_widgetAlias[$className])) {
$className = self::$_widgetAlias[$className];
}
if (!isset(self::$_widgetPool[$alias])) {
/** 如果类不存在 */
if (!class_exists($className)) {
throw new Typecho_Widget_Exception($className);
}
/** 初始化request */
if (!empty($request)) {
$requestObject = new Typecho_Request();
$requestObject->setParams($request);
} else {
$requestObject = Typecho_Request::getInstance();
}
/** 初始化response */
$responseObject = $enableResponse ? Typecho_Response::getInstance()
: Typecho_Widget_Helper_Empty::getInstance();
/** 初始化组件 */
$widget = new $className($requestObject, $responseObject, $params);
$widget->execute();
self::$_widgetPool[$alias] = $widget;
}
return self::$_widgetPool[$alias];
}
先看第一个参数 $alias
第一步把传入的变量利用 @
拆分成两个变量,一个是要初始化的类名,另一个是个别名,如果没有别名的设置,雷德明就是别名。 然后在判断别名池里面有没有这个别名,如果包含了这个别名,类名就变成别名池设置的类名。这个别名池,在这个类下的 alias
方法中可以设置,当前这个方法貌似没有使用。
继续下去,如果组件池里面没有这个实例,就要初始化这个类,先初始化了,request
,初始化的时候,会把 get
和 pos
t 参数,设置到 request
的 $_widgetAlias
里面。然后调用了 request
的 setParams
把 widget
方法传入的 params
参数传入,这个方法,会把 params
设置到 request
的 _params
里面。当然,如果传入的 request
参数不是空,则会获取已经存在的 request
,不过 request
的 getInstance
方法中不存在 request
实例的话,也会重新初始化一下。
紧接着会根据 $enableResponse
是否为 true
决定,是创建 resposne
或者 Typecho_Widget_Helper_Empty
。Typecho_Widget_Helper_Empty
这个类,我们后面再说。
最后,初始化我们传入的 class
,并且执行 execute
方法。最后把 class
实例,放入到组件池当中,并且把组件返回。
好了,现在我们说说初始化的 Widget_Init
类。这个类继承了 Typecho_Widget
类,这个类是个组件根类,这里面封装了不少数据结构,以及 request
、response
之类的东西,这个类,我们在后面用到的时候分别来说再说。初始化 Typecho_Widget
的时候,会传入 request
、resposne
、$params
。request
、resposne
会赋值给实例的 request
、resposne
。紧接着初始化了 Typecho_Config
, 这个类就当做数组看就行了。再把 $params
调用给 Typecho_Config
的 setDefault
方法,设置初始值。
下期预告
紧接着我们看看 Widget_Init
类的 execute
方法。这里面方法也很复杂,我们下期再说。
Typecho 源码分析(4)
crazyhl 2019-11-14 21:56:03 分类专栏: # typecho 源码分析 文章标签: php 源码分析 typecho
上篇文章忘记说的
我们还是在入口文件徘徊,而且还是在初始化 widget
的第一行。我们来到了初始化 Widget_Init
的地方。在 Init->execute
方法中,我们到了初始化 option
的地方。
初始化的构造地方,还初始化了 db
数据库。
数据库连接的地方在,index
文件的引入 config
有构造连接,大家不要忘记哦。
正文开始
我们来看 Option->execute()
方法。
/**
* 执行函数
*
* @access public
* @return void
*/
public function execute()
{
$this->db->fetchAll($this->db->select()->from('table.options')
->where('user = 0'), array($this, 'push'));
/** 支持皮肤变量重载 */
if (!empty($this->row['theme:' . $this->row['theme']])) {
$themeOptions = NULL;
/** 解析变量 */
if ($themeOptions = unserialize($this->row['theme:' . $this->row['theme']])) {
/** 覆盖变量 */
$this->row = array_merge($this->row, $themeOptions);
}
}
$this->stack[] = &$this->row;
/** 动态获取根目录 */
$this->rootUrl = defined('__TYPECHO_ROOT_URL__') ? __TYPECHO_ROOT_URL__ : $this->request->getRequestRoot();
if (defined('__TYPECHO_ADMIN__')) {
/** 识别在admin目录中的情况 */
$adminDir = '/' . trim(defined('__TYPECHO_ADMIN_DIR__') ? __TYPECHO_ADMIN_DIR__ : '/admin/', '/');
$this->rootUrl = substr($this->rootUrl, 0, - strlen($adminDir));
}
/** 初始化站点信息 */
if (defined('__TYPECHO_SITE_URL__')) {
$this->siteUrl = __TYPECHO_SITE_URL__;
} else if (defined('__TYPECHO_DYNAMIC_SITE_URL__') && __TYPECHO_DYNAMIC_SITE_URL__) {
$this->siteUrl = $this->rootUrl;
}
$this->originalSiteUrl = $this->siteUrl;
$this->siteUrl = Typecho_Common::url(NULL, $this->siteUrl);
$this->plugins = unserialize($this->plugins);
/** 动态判断皮肤目录 */
$this->theme = is_dir($this->themeFile($this->theme)) ? $this->theme : 'default';
/** 增加对SSL连接的支持 */
if ($this->request->isSecure() && 0 === strpos($this->siteUrl, 'http://')) {
$this->siteUrl = substr_replace($this->siteUrl, 'https', 0, 4);
}
/** 自动初始化路由表 */
$this->routingTable = unserialize($this->routingTable);
if (!isset($this->routingTable[0])) {
/** 解析路由并缓存 */
$parser = new Typecho_Router_Parser($this->routingTable);
$parsedRoutingTable = $parser->parse();
$this->routingTable = array_merge(array($parsedRoutingTable), $this->routingTable);
$this->db->query($this->db->update('table.options')->rows(array('value' => serialize($this->routingTable)))
->where('name = ?', 'routingTable'));
}
}
我们来看第一行的 fetchAll
方法
/**
* 一次取出所有行
*
* @param mixed $query 查询对象
* @param array $filter 行过滤器函数,将查询的每一行作为第一个参数传入指定的过滤器中
* @return array
*/
public function fetchAll($query, array $filter = NULL)
{
//执行查询
$resource = $this->query($query, self::READ);
$result = array();
/** 取出过滤器 */
if (!empty($filter)) {
list($object, $method) = $filter;
}
//取出每一行
while ($rows = $this->_adapter->fetch($resource)) {
//判断是否有过滤器
$result[] = $filter ? call_user_func(array(&$object, $method), $rows) : $rows;
}
return $result;
}
这里面有两个步骤,第一个是去除所有行,如果没有 filter
参数,就把结果放到 $result
里面,如果有 $filter
参数, 就会调用 $filter
的方法,把并且把 $fiter
方法的返回值放入到 $result
里面。这里面传入了 option->push
方法,等于把结果都传入到了 $option->row
里面,所以可以看到 execute
方法没有接受返回值,因为目的已经达到了。
紧接着初始化了各种参数,比较值得看的就是初始化路由表。
路由表这块就是根据 url
执行相应的 action
。这块我们在需要的地方在具体进入再说。
紧接着就回到了 Init
里面,这里面就初始化了 cookie
router
等组件,这些组件我们在到了请求那边再说。
最后我们说一下 User
那行,这块判断了,是否登录,决定是否开启 session
。
初始化 User
的时候 获取了 db
和 option
,这两个字在前边都初始化了,所以直接就可以拿到数据。
在 execute
这边判断是否登陆以后,如果登陆了,会把 登录的用户信息,放到 $option
里面,然后刷新上次活动时间。
在看看 hasLogin
方法,如果 hasLogin
有值,就利用这个值,如果没有,就通过 cookie
校验,通过后,更新 hasLogin
的值,并且把值返回。
至此,Init
就都跑完了。
下期预告
我们用了两篇跑完了,一个初始化,不要担心,随着我们的初始化的东西越来越多,以后,就会快起来了。
下期,我们来看
/** 注册一个初始化插件 */
Typecho_Plugin::factory('index.php')->begin();
Typecho 源码分析(5)
crazyhl 2019-11-18 22:02:17
分类专栏: # typecho 源码分析 文章标签: php 源码分析 typecho
上一篇我们已经分析完了组件的初始化相关的东西,今天我们继续。插件部分。
正文开始
/** 注册一个初始化插件 */
Typecho_Plugin::factory('index.php')->begin();
12
进入方法内部
/**
* 获取实例化插件对象
*
* @access public
* @param string $handle 插件
* @return Typecho_Plugin
*/
public static function factory($handle)
{
return isset(self::$_instances[$handle]) ? self::$_instances[$handle] :
(self::$_instances[$handle] = new Typecho_Plugin($handle));
}
12345678910111213
就是看插件池有没有这个插件,没有就初始化,如果有就返回已存在的。
到这里我们还是没有这个插件的,执行初始化。
/**
* 插件初始化
*
* @access public
* @param string $handle 插件
*/
public function __construct($handle)
{
/** 初始化变量 */
$this->_handle = $handle;
}
1234567891011
这里初始化的时候就是给插件赋值一下。
/** 注册一个结束插件 */
Typecho_Plugin::factory('index.php')->end();
12
这两个 begin
和 end
方法,我们在插件部分再说。
插件到这边就先告一段落。
下期预告
这篇比较短,因为用到的方法,很少或者没有用到。我们下期,说一下 路由部分,这块估计会说比较多的东西。我们下篇再见
Typecho 源码分析(6)
crazyhl 2019-11-21 22:22:11
分类专栏: # typecho 源码分析 文章标签: php 源码分析 typecho
前情提要
上一篇说了一下插件相关的东西,可是发现插件需要很多东西去说,于是就没说全,因为还是要抓紧把全部流程跑通,所以就省略下来了,这篇我们说一下路由相关的。等这个说完,等于就把全部流程跑通了。后面我们就可以展开来说各种模块了。
正文开始
/** 开始路由分发 */
Typecho_Router::dispatch();
index 文件的最后一行了。开始吧,让我们进入方法内部,
/**
* 路由分发函数
*
* @return void
* @throws Exception
*/
public static function dispatch()
{
/** 获取PATHINFO */
$pathInfo = self::getPathInfo();
foreach (self::$_routingTable as $key => $route) {
if (preg_match($route['regx'], $pathInfo, $matches)) {
self::$current = $key;
try {
/** 载入参数 */
$params = NULL;
if (!empty($route['params'])) {
unset($matches[0]);
$params = array_combine($route['params'], $matches);
}
$widget = Typecho_Widget::widget($route['widget'], NULL, $params);
if (isset($route['action'])) {
$widget->{$route['action']}();
}
Typecho_Response::callback();
return;
} catch (Exception $e) {
if (404 == $e->getCode()) {
Typecho_Widget::destory($route['widget']);
continue;
}
throw $e;
}
}
}
/** 载入路由异常支持 */
throw new Typecho_Router_Exception("Path '{$pathInfo}' not found", 404);
}
第一行,获取 pathInfo
我们再次进入方法。
/**
* 获取全路径
*
* @access public
* @return string
*/
public static function getPathInfo()
{
if (NULL === self::$_pathInfo) {
self::setPathInfo();
}
return self::$_pathInfo;
}
pathInfo
为 null
的时候执行 set
方法,默认值是 /
。我们看下返回值是什么。
string(1) "/"
返回了一个 /
是默认值。
这个值是从哪里过来的呢,是在 init
里面赋值的,我们看一下方法。
/**
* 获取当前pathinfo
*
* @access public
* @param string $inputEncoding 输入编码
* @param string $outputEncoding 输出编码
* @return string
*/
public function getPathInfo($inputEncoding = NULL, $outputEncoding = NULL)
{
/** 缓存信息 */
if (NULL !== $this->_pathInfo) {
return $this->_pathInfo;
}
//参考Zend Framework对pahtinfo的处理, 更好的兼容性
$pathInfo = NULL;
//处理requestUri
$requestUri = $this->getRequestUri();
$finalBaseUrl = $this->getBaseUrl();
// Remove the query string from REQUEST_URI
if ($pos = strpos($requestUri, '?')) {
$requestUri = substr($requestUri, 0, $pos);
}
if ((NULL !== $finalBaseUrl)
&& (false === ($pathInfo = substr($requestUri, strlen($finalBaseUrl)))))
{
// If substr() returns false then PATH_INFO is set to an empty string
$pathInfo = '/';
} elseif (NULL === $finalBaseUrl) {
$pathInfo = $requestUri;
}
if (!empty($pathInfo)) {
//针对iis的utf8编码做强制转换
//参考http://docs.moodle.org/ja/%E5%A4%9A%E8%A8%80%E8%AA%9E%E5%AF%BE%E5%BF%9C%EF%BC%9A%E3%82%B5%E3%83%BC%E3%83%90%E3%81%AE%E8%A8%AD%E5%AE%9A
if (!empty($inputEncoding) && !empty($outputEncoding) &&
(stripos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') !== false
|| stripos($_SERVER['SERVER_SOFTWARE'], 'ExpressionDevServer') !== false)) {
if (function_exists('mb_convert_encoding')) {
$pathInfo = mb_convert_encoding($pathInfo, $outputEncoding, $inputEncoding);
} else if (function_exists('iconv')) {
$pathInfo = iconv($inputEncoding, $outputEncoding, $pathInfo);
}
}
} else {
$pathInfo = '/';
}
// fix issue 456
return ($this->_pathInfo = '/' . ltrim(urldecode($pathInfo), '/'));
}
这个方法首先获取了 requestUri
/**
* 获取请求地址
*
* @access public
* @return string
*/
public function getRequestUri()
{
if (!empty($this->_requestUri)) {
return $this->_requestUri;
}
//处理requestUri
$requestUri = '/';
if (isset($_SERVER['HTTP_X_REWRITE_URL'])) { // check this first so IIS will catch
$requestUri = $_SERVER['HTTP_X_REWRITE_URL'];
} elseif (
// IIS7 with URL Rewrite: make sure we get the unencoded url (double slash problem)
isset($_SERVER['IIS_WasUrlRewritten'])
&& $_SERVER['IIS_WasUrlRewritten'] == '1'
&& isset($_SERVER['UNENCODED_URL'])
&& $_SERVER['UNENCODED_URL'] != ''
) {
$requestUri = $_SERVER['UNENCODED_URL'];
} elseif (isset($_SERVER['REQUEST_URI'])) {
$requestUri = $_SERVER['REQUEST_URI'];
$parts = @parse_url($requestUri);
if (isset($_SERVER['HTTP_HOST']) && strstr($requestUri, $_SERVER['HTTP_HOST'])) {
if (false !== $parts) {
$requestUri = (empty($parts['path']) ? '' : $parts['path'])
. ((empty($parts['query'])) ? '' : '?' . $parts['query']);
}
} elseif (!empty($_SERVER['QUERY_STRING']) && empty($parts['query'])) {
// fix query missing
$requestUri .= '?' . $_SERVER['QUERY_STRING'];
}
} elseif (isset($_SERVER['ORIG_PATH_INFO'])) { // IIS 5.0, PHP as CGI
$requestUri = $_SERVER['ORIG_PATH_INFO'];
if (!empty($_SERVER['QUERY_STRING'])) {
$requestUri .= '?' . $_SERVER['QUERY_STRING'];
}
}
return $this->_requestUri = $requestUri;
}
我们主要看
} elseif (isset($_SERVER['REQUEST_URI'])) {
$requestUri = $_SERVER['REQUEST_URI'];
$parts = @parse_url($requestUri);
if (isset($_SERVER['HTTP_HOST']) && strstr($requestUri, $_SERVER['HTTP_HOST'])) {
if (false !== $parts) {
$requestUri = (empty($parts['path']) ? '' : $parts['path'])
. ((empty($parts['query'])) ? '' : '?' . $parts['query']);
}
} elseif (!empty($_SERVER['QUERY_STRING']) && empty($parts['query'])) {
// fix query missing
$requestUri .= '?' . $_SERVER['QUERY_STRING'];
}
}
这个判断里面的,因为我们主要是 nginx
不是用的iis
。
可以看到最后拿到的 requestUri
就是请求 /
后面的所有加上参数。
紧接着就是获取 $finalBaseUrl
这个就是请求前缀,用于选择目录。
最后在通过 $finalBaseUrl
获取最后的 $pathInfo
。
然后再去初始化时候的 route
里面匹配,就找到了 controller
和 method
。
下期预告
我们找到了需要调用的方法,我们在下篇在继续
Typecho 源码分析(7)
crazyhl 2019-12-15 22:53:40
分类专栏: # typecho 源码分析 文章标签: php
题外话
两周之前搬了家,以前搬家从来没觉得东西这么多,收拾起来这么麻烦,基本上整理整理就可以过日子了。这次换了个整租,才发现屋子大了也不好,东西找不到,现在基本上算是步入正轨了,不过nas还没有就位,等我再整理整理在看看吧nas放到哪里。机械硬盘实在是太吵了,这次要放到一个安静的地方。
前情提要
第五篇简单分析了插件,其实什么都没说。上一篇分析了路由部分,也说的比较混乱,本周五和周六我用了一些时间,仔细的读了源码,把自己混乱的部分也都弄清了,所以这篇我就慢慢的再说一次路由,用两个请求,首页和文章页 来分析,将来在模块分析的时候也会把路由需要的部分,在分析。
正文开始
我们再次来到路由的 dispatch
方法
/**
* 路由分发函数
*
* @return void
* @throws Exception
*/
public static function dispatch()
{
/** 获取PATHINFO */
$pathInfo = self::getPathInfo();
foreach (self::$_routingTable as $key => $route) {
if (preg_match($route['regx'], $pathInfo, $matches)) {
self::$current = $key;
try {
/** 载入参数 */
$params = NULL;
if (!empty($route['params'])) {
unset($matches[0]);
$params = array_combine($route['params'], $matches);
}
$widget = Typecho_Widget::widget($route['widget'], NULL, $params);
if (isset($route['action'])) {
$widget->{$route['action']}();
}
Typecho_Response::callback();
return;
} catch (Exception $e) {
if (404 == $e->getCode()) {
Typecho_Widget::destory($route['widget']);
continue;
}
throw $e;
}
}
}
/** 载入路由异常支持 */
throw new Typecho_Router_Exception("Path '{$pathInfo}' not found", 404);
}
首先获取 pathInfo
,这个 pathInfo
是从那里获取的呢,是从 Init
里面初始化的,我们看下初始化部分
$pathInfo = $this->request->getPathInfo();
进入这个方法
/**
* 获取当前pathinfo
*
* @access public
* @param string $inputEncoding 输入编码
* @param string $outputEncoding 输出编码
* @return string
*/
public function getPathInfo($inputEncoding = NULL, $outputEncoding = NULL)
{
/** 缓存信息 */
if (NULL !== $this->_pathInfo) {
return $this->_pathInfo;
}
//参考Zend Framework对pahtinfo的处理, 更好的兼容性
$pathInfo = NULL;
//处理requestUri
$requestUri = $this->getRequestUri();
var_dump($requestUri);
$finalBaseUrl = $this->getBaseUrl();
var_dump($requestUri);
// Remove the query string from REQUEST_URI
if ($pos = strpos($requestUri, '?')) {
$requestUri = substr($requestUri, 0, $pos);
}
if ((NULL !== $finalBaseUrl)
&& (false === ($pathInfo = substr($requestUri, strlen($finalBaseUrl)))))
{
// If substr() returns false then PATH_INFO is set to an empty string
$pathInfo = '/';
} elseif (NULL === $finalBaseUrl) {
$pathInfo = $requestUri;
}
if (!empty($pathInfo)) {
//针对iis的utf8编码做强制转换
//参考http://docs.moodle.org/ja/%E5%A4%9A%E8%A8%80%E8%AA%9E%E5%AF%BE%E5%BF%9C%EF%BC%9A%E3%82%B5%E3%83%BC%E3%83%90%E3%81%AE%E8%A8%AD%E5%AE%9A
if (!empty($inputEncoding) && !empty($outputEncoding) &&
(stripos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') !== false
|| stripos($_SERVER['SERVER_SOFTWARE'], 'ExpressionDevServer') !== false)) {
if (function_exists('mb_convert_encoding')) {
$pathInfo = mb_convert_encoding($pathInfo, $outputEncoding, $inputEncoding);
} else if (function_exists('iconv')) {
$pathInfo = iconv($inputEncoding, $outputEncoding, $pathInfo);
}
}
} else {
$pathInfo = '/';
}
// fix issue 456
return ($this->_pathInfo = '/' . ltrim(urldecode($pathInfo), '/'));
}
这个方法了里面第一步,如果有 pathInfo
就返回,如果没有就进入后续的流程,我们这里面肯定是没有的,所以继续后续执行
先获取了 $requestUri
,
/**
* 获取请求地址
*
* @access public
* @return string
*/
public function getRequestUri()
{
if (!empty($this->_requestUri)) {
return $this->_requestUri;
}
//处理requestUri
$requestUri = '/';
if (isset($_SERVER['HTTP_X_REWRITE_URL'])) { // check this first so IIS will catch
$requestUri = $_SERVER['HTTP_X_REWRITE_URL'];
} elseif (
// IIS7 with URL Rewrite: make sure we get the unencoded url (double slash problem)
isset($_SERVER['IIS_WasUrlRewritten'])
&& $_SERVER['IIS_WasUrlRewritten'] == '1'
&& isset($_SERVER['UNENCODED_URL'])
&& $_SERVER['UNENCODED_URL'] != ''
) {
$requestUri = $_SERVER['UNENCODED_URL'];
} elseif (isset($_SERVER['REQUEST_URI'])) {
$requestUri = $_SERVER['REQUEST_URI'];
$parts = @parse_url($requestUri);
if (isset($_SERVER['HTTP_HOST']) && strstr($requestUri, $_SERVER['HTTP_HOST'])) {
if (false !== $parts) {
$requestUri = (empty($parts['path']) ? '' : $parts['path'])
. ((empty($parts['query'])) ? '' : '?' . $parts['query']);
}
} elseif (!empty($_SERVER['QUERY_STRING']) && empty($parts['query'])) {
// fix query missing
$requestUri .= '?' . $_SERVER['QUERY_STRING'];
}
} elseif (isset($_SERVER['ORIG_PATH_INFO'])) { // IIS 5.0, PHP as CGI
$requestUri = $_SERVER['ORIG_PATH_INFO'];
if (!empty($_SERVER['QUERY_STRING'])) {
$requestUri .= '?' . $_SERVER['QUERY_STRING'];
}
}
return $this->_requestUri = $requestUri;
}
进入方法内部,第一步还是判断是否存在,不存在就从 $_SERVER
中获取相关参数,因为我们是在nginx中,所以在下面这个判断中获取参数
} elseif (isset($_SERVER['REQUEST_URI'])) {
$requestUri = $_SERVER['REQUEST_URI'];
$parts = @parse_url($requestUri);
if (isset($_SERVER['HTTP_HOST']) && strstr($requestUri, $_SERVER['HTTP_HOST'])) {
if (false !== $parts) {
$requestUri = (empty($parts['path']) ? '' : $parts['path'])
. ((empty($parts['query'])) ? '' : '?' . $parts['query']);
}
} elseif (!empty($_SERVER['QUERY_STRING']) && empty($parts['query'])) {
// fix query missing
$requestUri .= '?' . $_SERVER['QUERY_STRING'];
}
}
获得 REQUEST_URI
,紧接着解析 用 parse_url
解析 获取到的 uri
得到 parts
,紧接着判断 如果
if (isset($_SERVER['HTTP_HOST']) && strstr($requestUri, $_SERVER['HTTP_HOST'])) {
if (false !== $parts) {
$requestUri = (empty($parts['path']) ? '' : $parts['path'])
. ((empty($parts['query'])) ? '' : '?' . $parts['query']);
}
} elseif (!empty($_SERVER['QUERY_STRING']) && empty($parts['query'])) {
// fix query missing
$requestUri .= '?' . $_SERVER['QUERY_STRING'];
}
server
的 host
存在 并且 uri
在 host
里面,就判断解析的 parts
是否为 false
, 然后拼接 uri
,这里if (isset($_SERVER['HTTP_HOST']) && strstr($requestUri, $_SERVER['HTTP_HOST']))
是 false
所以走下面的判断逻辑,
elseif (!empty(_SERVER['QUERY_STRING']) && empty(parts['query']))
当前这个url
http://typecho.test/index.php/archives/1/
也是false,所以请求的 uri 就是
/index.php/archives/1/
紧接着获取 getBaseUrl
/**
* getBaseUrl
*
* @access public
* @return string
*/
public function getBaseUrl()
{
if (NULL !== $this->_baseUrl) {
return $this->_baseUrl;
}
//处理baseUrl
$filename = (isset($_SERVER['SCRIPT_FILENAME'])) ? basename($_SERVER['SCRIPT_FILENAME']) : '';
if (isset($_SERVER['SCRIPT_NAME']) && basename($_SERVER['SCRIPT_NAME']) === $filename) {
$baseUrl = $_SERVER['SCRIPT_NAME'];
} elseif (isset($_SERVER['PHP_SELF']) && basename($_SERVER['PHP_SELF']) === $filename) {
$baseUrl = $_SERVER['PHP_SELF'];
} elseif (isset($_SERVER['ORIG_SCRIPT_NAME']) && basename($_SERVER['ORIG_SCRIPT_NAME']) === $filename) {
$baseUrl = $_SERVER['ORIG_SCRIPT_NAME']; // 1and1 shared hosting compatibility
} else {
// Backtrack up the script_filename to find the portion matching
// php_self
$path = isset($_SERVER['PHP_SELF']) ? $_SERVER['PHP_SELF'] : '';
$file = isset($_SERVER['SCRIPT_FILENAME']) ? $_SERVER['SCRIPT_FILENAME'] : '';
$segs = explode('/', trim($file, '/'));
$segs = array_reverse($segs);
$index = 0;
$last = count($segs);
$baseUrl = '';
do {
$seg = $segs[$index];
$baseUrl = '/' . $seg . $baseUrl;
++$index;
} while (($last > $index) && (false !== ($pos = strpos($path, $baseUrl))) && (0 != $pos));
}
// Does the baseUrl have anything in common with the request_uri?
$finalBaseUrl = NULL;
$requestUri = $this->getRequestUri();
if (0 === strpos($requestUri, $baseUrl)) {
// full $baseUrl matches
$finalBaseUrl = $baseUrl;
} else if (0 === strpos($requestUri, dirname($baseUrl))) {
// directory portion of $baseUrl matches
$finalBaseUrl = rtrim(dirname($baseUrl), '/');
} else if (!strpos($requestUri, basename($baseUrl))) {
// no match whatsoever; set it blank
$finalBaseUrl = '';
} else if ((strlen($requestUri) >= strlen($baseUrl))
&& ((false !== ($pos = strpos($requestUri, $baseUrl))) && ($pos !== 0)))
{
// If using mod_rewrite or ISAPI_Rewrite strip the script filename
// out of baseUrl. $pos !== 0 makes sure it is not matching a value
// from PATH_INFO or QUERY_STRING
$baseUrl = substr($requestUri, 0, $pos + strlen($baseUrl));
}
return ($this->_baseUrl = (NULL === $finalBaseUrl) ? rtrim($baseUrl, '/') : $finalBaseUrl);
}
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
首先获取从 server
的 SCRIPT_FILENAME
获取 $filename
,如果SCRIPT_FILENAME
存在,则用 basename
方法获取 $filename
,当前的 filename
是 index.php
。basenme
方法的作用就是返回路径中的文件名,当前 如果 SCRIPT_FILENAME
值是 /var/www/typecho/index.php
,所以文件名就是
index.php
紧接着判断 server
中的 SCRIPT_NAME
或 PHP_SELF
的内容经过 basename
处理后的文件名是否跟 filename
相同。我们的请求在 SCRIPT_NAME
这里的判断就符合了条件,所以 baseurl
就是 server
中的 SCRIPT_NAME
的值。
/index.php
接下来 判断 baseurl
或 baseurl
的 dirname
在 requesturi
中是否开头,我们这里的场景是
else if (0 === strpos($requestUri, dirname($baseUrl)))
这里的判断中达成的,所以
$finalBaseUrl = rtrim(dirname($baseUrl), '/');
就是吧 dirname
后的 $baseUrl
去掉右侧的/后的值。最后 baseurl
就是
(NULL === $finalBaseUrl) ? rtrim($baseUrl, '/') : $finalBaseUrl
判断 finalBaseUrl
是否为 null
,如果为 null
就是把 baseurl
去掉右侧的 /
,否则就是 finalBaseUrl
。我们这里 finalBaseUrl
不是 null
,所以 baseurl就是 finalBaseUrl。为 /index.php
。
接下来判断 requesturi
中是否包含 ?
,如果包含,就截取 ?
前面的不部分,我们这边不包含,所以 requesturi
依然是
/index.php/archives/1/
然后判断 $finalBaseUrl
是否为 null
, 如果是 null
,$pathInfo = $requestUri;
,如果不是并且
false===(pathInfo =substr(requestUri, strlen(finalBaseUrl)))
substr
后的 pathinfo
是 false
,就是没有提取到子串的时候 pathinfo
是 /
。 我们的场景下,成功提取到了,所以 pathinfo
就是
/archives/1/
接下来,pathinfo
不为空的时候对 iis
请求的编码,这里不存在,就忽略了,如果pathinfo是空,就赋值 /。最后完整的 pathinfo就是
'/' . ltrim(urldecode($pathInfo), '/')
去掉左侧的 /
在拼接一个 /
这个目的就是防止做的没有 /
。 最后 pathinfo就是
/archives/1/
最后把,Typecho_Router::setPathInfo($pathInfo);
设置到路由里面。
接下来就回到了路由的 dispatch
方法。首先获取一下 pathinfo
。
然后用配置里面的 routeTable
进行匹配,这个 routeTable
就是在数据库里面的配置,可以看 option
表里面的数据。
遍历 routeTable
,用路由里面的 regex
来匹配 pathInfo
,如果没有匹配到,就抛出 路由 没有匹配到的 404。
如果匹配到了,把 路由的 key
设置到 current
。
如果设置了路由的 params
,就把匹配到的参数跟 params
组合成数组。例如
if (!empty($route['params'])) {
unset($matches[0]); $params = array_combine($route['params'], $matches)
;}
http://typecho.test/index.php/archives/1/
array(6) { ["url"]=> string(24) "/archives/[cid:digital]/" ["widget"]=> string(14) "Widget_Archive" ["action"]=> string(6) "render" ["regx"]=> string(26) "|^/archives/([0-9]+)[/]?$|" ["format"]=> string(13) "/archives/%s/" ["params"]=> array(1) { [0]=> string(3) "cid" } } array(2) { [0]=> string(12) "/archives/1/" [1]=> string(1) "1" }
上面这种路径的话,就会把匹配到的 1
跟 params
组合,合成参数数组
array(1) { ["cid"]=> string(1) "1" }
给后续的方法使用。
紧接着初始化,路由对应的组件,上面这个文章详情的例子就是 Widget_Archive
,
然后,判断是否设置了 路由的 action
, 如果设置了就执行这个方法
if (isset($route['action'])) {
$widget->{$route['action']}();
}
最后调用,
Typecho_Response::callback();
最后就返回了,如果执行相关方法出错了,就执行异常部分。
if (404 == $e->getCode()) {
Typecho_Widget::destory($route['widget']);
continue;
}
throw $e;
到这,整个路由就跑完了,大家可以多多的测试各种页面看看各种结果。
下期预告
下次我们就来具体的分析插件,这个好玩的东西,刚开始学 php
的时候,就觉得很高级,后来看过 thinkphp3.2
的源码的时候也在其他地方看到了类似的东西,这个做法真的很好玩。敬请期待。
Typecho 源码分析(8)-- 后台插件列表
crazyhl 2019-12-21 22:35:25
分类专栏: # typecho 源码分析 文章标签: php
前情提要
前面 7 篇文章基本上已经分析完成 Typecho
的运行流程了,从本篇开始就开始分析各种模块了,原本是想分析路由的,但是我更对插件感兴趣,所以就从插件开始了。
正文开始
我们先不分析插件的加载流程,因为我们还没有启用任何插件,所以我们从后台的插件列表开始。我们打开 admin\plugins.php
文件。前面的几行加载我们稍后再说,直奔重点插件列表而去。
<?php Typecho_Widget::widget('Widget_Plugins_List@activated', 'activated=1')->to($activatedPlugins); ?>
这行代码会获取启用的插件列表,我们来到 Widget\Plugins\List.php
文件,为什么是这个文件,请看前三篇文章和 var\Typecho\Common.php
的 __autoLoad
方法就了解了。
调用这个文件的 execute
方法,
首先获取插件目录,设置默认参数,获取已经启用的插件,把已经启用的插件放到
$this->activatedPlugins = $plugins['activated'];
里面,紧接着如果插件目录存在,就开始遍历插件目录。
获取插件,根据目录和文件的形态,返回插件信息
return array($pluginName, $pluginFileName);
接下来获取插件信息,
判断插件版本,判断插件是否在数据库中启用,如果启用了,就放到插件list 的栈中。
上面的是获取激活的插件部分,第二部分是获取禁用的插件,我们用禁用的部分测试一下上面的对不对,
插件目录
array(1) { [0]=> string(40) "/var/www/typecho//usr/plugins/HelloWorld" }
设置默认参数,
$this->parameter->setDefault(array('activated' => NULL));
这边传入的的是null,返回的却是0。
object(Typecho_Config)#27 (1) {
["_currentConfig":"Typecho_Config":private]=>
array(1) {
["activated"]=>
string(1) "0"
}
}
是因为在初始化 List
组件的时候,我们传入了 params
activated=0
,这边相关的可以去看 组件 初始化的部分,
以及 $this->parameter->setDefault($params)
这个方法的具体实现。
接下来导出数据库里面激活的插件信息,现在还是空的,
$this->activatedPlugins = $plugins['activated'];
把 activated
放到 $this->activatedPlugins
中。
如果插件目录不为空,就开始遍历插件目录里面的插件,剩下的就可以参照上面的说明了。
/** 默认即插即用 */
$info['activated'] = true;
if ($info['activate'] || $info['deactivate'] || $info['config'] || $info['personalConfig']) {
$info['activated'] = isset($this->activatedPlugins[$pluginName]);
if (isset($this->activatedPlugins[$pluginName])) {
unset($this->activatedPlugins[$pluginName]);
}
}
if ($info['activated'] == $this->parameter->activated) {
$this->push($info);
}
说说这块吧,默认激活的,然后进入if判断,判断已激活的插件是否在数据存储中,如果存在,就在 activatedPlugins
卸载掉,这一步就是为了防止数据重复。
$info['activated'] == $this->parameter->activated
这行判断,就是看跟传入的参数是否相同,如果相同就放入到组件的 stack 中。
页面html部分就自己看看好了,不分析了。
下期预告
这篇开始就不会大篇幅的贴源码了,只贴一些重要的东西,大家要自己去看源码了,自己追踪一下总会更好,下篇会是一个短篇,分析部分的 $security
这个组件
Typecho 源码分析(9)-- 部分Security&User 组件分析
crazyhl 2019-12-24 22:45:41
分类专栏: # typecho 源码分析 文章标签: php
前情提要
前面我们分析了插件列表,但是 html
部分我们没有分析,今天我们就来分析一下 url
生成部分。
正文开始
后台的 common.php
会加载 Security
组件。
先看 Security
的 execute
方法,
/**
* 初始化函数
*
*/
public function execute()
{
$this->_options = $this->widget('Widget_Options');
$user = $this->widget('Widget_User');
$this->_token = $this->_options->secret;
if ($user->hasLogin()) {
$this->_token .= '&' . $user->authCode . '&' . $user->uid;
}
}
首先会加载 Options
,然后会加载 User
组件。
我们再去看 User
组件,这个组件的构造方法会加载 Option
,再去看 User
的 execute
方法,
public function execute()
{
if ($this->hasLogin()) {
$rows = $this->db->fetchAll($this->db->select()
->from('table.options')->where('user = ?', $this->_user['uid']));
$this->push($this->_user);
foreach ($rows as $row) {
$this->options->__set($row['name'], $row['value']);
}
//更新最后活动时间
$this->db->query($this->db
->update('table.users')
->rows(array('activated' => $this->options->time))
->where('uid = ?', $this->_user['uid']));
}
}
这里会判断是否登陆,我们先去看 hasLogin
方法,
public function hasLogin()
{
if (NULL !== $this->_hasLogin) {
return $this->_hasLogin;
} else {
$cookieUid = Typecho_Cookie::get('__typecho_uid');
if (NULL !== $cookieUid) {
/** 验证登陆 */
$user = $this->db->fetchRow($this->db->select()->from('table.users')
->where('uid = ?', intval($cookieUid))
->limit(1));
$cookieAuthCode = Typecho_Cookie::get('__typecho_authCode');
if ($user && Typecho_Common::hashValidate($user['authCode'], $cookieAuthCode)) {
$this->_user = $user;
return ($this->_hasLogin = true);
}
$this->logout();
}
return ($this->_hasLogin = false);
}
}
如果 _hasLogin
不是 NULL
就返回当前状态,如果不是,就去 cookie
里面获取 uid
,如果 uid
不是 NULL
,就去数据区获取这个用户,然后在去 cookie
获取 __typecho_authCode
值,如果用户存在,并且 __typecho_authCode
通过了验证,就把查到的 user
放到 User
的 _user
值中,并且把 _hasLogin
设置为真返回,如果没有通过验证,就退出用户,并且把 _hasLogin
设置为假返回。
现在我们去看看,Typecho_Common::hashValidate
方法,
public static function hashValidate($from, $to)
{
if ('$T$' == substr($to, 0, 3)) {
$salt = substr($to, 3, 9);
return self::hash($from, $salt) === $to;
} else {
return md5($from) === $to;
}
}
这里就是个验证算法,大家看下就好,我们就不多说了,包括里面的 hash
方法。logout
方法,我们在登录部分再说。
回到 User
的 execute
方法,如果登录成功了,就去 Options
里面获取喝这个登录用户的单独配置,然后把登录的用户 _user
放到组件的 stack
中,接着遍历用户的配置,放到 options
变量里面,最后刷新这个用户的活跃时间,我们看下 push
这个方法
public function push(array $value)
{
//将行数据按顺序置位
$this->row = $value;
$this->length ++;
$this->stack[] = $value;
return $value;
}
把放入的 value
放到组件的 row
中,把组件的 length
加一,再把值放入到 stack
中,返回 value
,row
和 stack
中,我们后面再说。
再次回到 Security
的 execute
方法,
从选项中获取 secret
放入到组件的 token
中,如果用户登录了,在拼接登录用户的 authCode
和 uid
到组件的 token
中。
我们回到 plugins.php
文件,看未启用插件列表代码部分的这行代码
<a href="<?php $security->index('/action/plugins-edit?activate=' . $deactivatedPlugins->name); ?>"><?php _e('启用'); ?></a>
主要看 $security->index
这个方法,我们进入方法内部去看。
public function index($path)
{
echo $this->getIndex($path);
}
这个方法调用了内部的 getIndex
方法并输出出来,看 getIndex
方法。
public function getIndex($path)
{
return Typecho_Common::url($this->getTokenUrl($path), $this->_options->index);
}
先看 $this->_options->index
的值是多少,这个值在 Option
的 execute
方法并没有进行赋值,我们可以看到,Option
里面有个 ___index
方法,但是我们获取 index
的值的时候为什么会调用这个方法呢,我们可以看 Option
的基类里面的魔术方法
protected function ___index()
{
return ($this->rewrite || (defined('__TYPECHO_REWRITE__') && __TYPECHO_REWRITE__))
? $this->rootUrl : Typecho_Common::url('index.php', $this->rootUrl);
}
public function __get($name)
{
if (array_key_exists($name, $this->row)) {
return $this->row[$name];
} else {
$method = '___' . $name;
if (method_exists($this, $method)) {
return $this->$method();
} else {
$return = $this->pluginHandle()->trigger($plugged)->{$method}($this);
if ($plugged) {
return $return;
}
}
}
return NULL;
}
可以看到这里面,会查询 $method = '___' . $name
这样的方法名是否存在,如果存在就会调用这个方法。
我们再看下 ___index
方法运行了什么,在开启了 rewrite
以后,就会返回 rootUrl
,如果没有开启就会生成 url,我们看下 url 的生成方法。
Typecho_Common::url('index.php', $this->rootUrl)
这个方法传入了两个参数,第一个是 index.php
和 rootUrl
,那么 rootUrl
是怎么来的呢。这个值在 Option
的 execute
方法生成,我们看下
$this->rootUrl = defined('__TYPECHO_ROOT_URL__') ? __TYPECHO_ROOT_URL__ : $this->request->getRequestRoot();
if (defined('__TYPECHO_ADMIN__')) {
/** 识别在admin目录中的情况 */
$adminDir = '/' . trim(defined('__TYPECHO_ADMIN_DIR__') ? __TYPECHO_ADMIN_DIR__ : '/admin/', '/');
$this->rootUrl = substr($this->rootUrl, 0, - strlen($adminDir));
}
如果设置了 __TYPECHO_ROOT_URL__
就返回 __TYPECHO_ROOT_URL__
没有设置就调用 Request
的 getRequestRoot
方法。然后在判断,是不是 /admin
结尾,如果是以这个结尾就说明是
public function getRequestRoot()
{
if (NULL === $this->_requestRoot) {
$root = rtrim(self::getUrlPrefix() . $this->getBaseUrl(), '/') . '/';
$pos = strrpos($root, '.php/');
if ($pos) {
$root = dirname(substr($root, 0, $pos));
}
$this->_requestRoot = rtrim($root, '/');
}
return $this->_requestRoot;
}
先获取 getUrlPrefix
,
public static function getUrlPrefix()
{
if (empty(self::$_urlPrefix)) {
if (defined('__TYPECHO_URL_PREFIX__')) {
self::$_urlPrefix == __TYPECHO_URL_PREFIX__;
} else if (!defined('__TYPECHO_CLI__')) {
self::$_urlPrefix = (self::isSecure() ? 'https' : 'http') . '://'
. (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : $_SERVER['SERVER_NAME']);
}
}
return self::$_urlPrefix;
}
这个方法就是拼接了 server
的参数,获取了完整的请求url 域名部分。
紧接着调用了 getBaseUrl
,这个就是获取了请求的文件,这个方法的解析请看前面的文章。
生成了 root
之后,会判断 '.php/'
的位置,如果查到了这个字符串,就会获取一下 root
的 dirname
,这个就可以理解为,把最后的文件过滤掉,保留前面的部分。
好了,我们看下 url
方法
public static function url($path, $prefix)
{
$path = (0 === strpos($path, './')) ? substr($path, 2) : $path;
return rtrim($prefix, '/') . '/' . str_replace('//', '/', ltrim($path, '/'));
}
会先判断path是否已 ./
开头,如果是,就截取一下,然后在把 prefix
放到前面,拼接 path
。
再看 getTokenUrl
方法
public function getTokenUrl($path)
{
$parts = parse_url($path);
$params = array();
if (!empty($parts['query'])) {
parse_str($parts['query'], $params);
}
$params['_'] = $this->getToken($this->request->getRequestUrl());
$parts['query'] = http_build_query($params);
return Typecho_Common::buildUrl($parts);
}
解析 path
,如果解析的 url
包含 query
,就再次生成params
,在调用 getToken
生成加密的串,这个生成就是 md5
一下。最后在生成url。
可以看到生成的 url
就是 http://typecho.test/index.php/action/plugins-edit?activate=HelloWorld&_=4d799a66e315807b50ca3773ede882f3
。
下期预告
这篇文章说的比较乱,大家需要自己好好的多读一读,这样才能更好的理解后面的东西。下篇我们还在 plugins.php
里面徘徊。我们看更多的东西,下期再见。
Typecho 源码分析(10)-- DoAction 分析
crazyhl 2019-12-29 22:39:18
分类专栏: # typecho 源码分析
要
前面我们分析了插件列表,看到 html 部分,我们看到了请求的url 包含 action 我们这篇就分析这里。
正文开始
在做到插件启用相关部分的时候。发现了一个链接,
http://typecho.test/index.php/action/plugins-edit?activate=HelloWorld&_=a05deb76f571cfb798d3904cc6cecf77
这块我就比较好奇了,为什么跟首页部分的不太一样吗,难道是又包装了一层?我们从路由分发来看一下。
array(6) { ["url"]=> string(22) "/action/[action:alpha]" ["widget"]=> string(9) "Widget_Do" ["action"]=> string(6) "action" ["regx"]=> string(32) "|^/action/([_0-9a-zA-Z-]+)[/]?$|" ["format"]=> string(10) "/action/%s" ["params"]=> array(1) { [0]=> string(6) "action" } }
可以看到,这个路由,匹配了 action
开头的 url
,执行的组件是 Do
,参数是一个action
。具体路由的分析请看前几篇文章。
我们看下 Do
的执行部分
public function execute()
{
/** 验证路由地址 **/
$action = $this->request->action;
//兼容老版本
if (empty($action)) {
$widget = trim($this->request->widget, '/');
$objectName = 'Widget_' . str_replace('/', '_', $widget);
if (preg_match("/^[_a-z0-9]$/i", $objectName) && Typecho_Common::isAvailableClass($objectName)) {
$widgetName = $objectName;
}
} else {
/** 判断是否为plugin */
$actionTable = array_merge($this->_map, unserialize($this->widget('Widget_Options')->actionTable));
if (isset($actionTable[$action])) {
$widgetName = $actionTable[$action];
}
}
if (isset($widgetName) && class_exists($widgetName)) {
$reflectionWidget = new ReflectionClass($widgetName);
if ($reflectionWidget->implementsInterface('Widget_Interface_Do')) {
$this->widget($widgetName)->action();
return;
}
}
throw new Typecho_Widget_Exception(_t('请求的地址不存在'), 404);
}
先获取 请求的 action
,这个值是从构造组件的时候传递过来的,
$widget = Typecho_Widget::widget($route['widget'], NULL, $params);
$params
这个值就会传入到 request
的构造中,看下面
$requestObject = new Typecho_Request();$requestObject->setParams($request);
具体大家可以追一下代码就了解了。
通过判断 action
是否为空来决定 widgetname
,判断的部分大家可以自己看一下。
紧接着,会去用反射相关的方法去调用具体的逻辑。
反射我们会单独找一篇文章去说明。对了,还有一个判断,是会这个组件是否实现了 Widget_Interface_Do
接口。
下期预告
这篇比较短,但是不乱,所以可以说是很简洁了,下篇我们继续在插件查遍看看还有什么要说的。
评论已关闭