typecho重新升級至1.2.0(實際上是重新刪除後安裝)後,typecho始終無法登錄後臺。
PHP這種Web編程語言實在沒接觸過,花了兩天來玩一下。
博客網站使用的技術:nginx+php-fpm+typecho(sqlite),nginx與php實際運行一個ARM電視盒子上。
正常運行的網站,各種調試、日誌都是關閉的,因此,首先打開php-fpm的日誌捕獲開關:
;catch_workers_output=no
修改爲yes,並將首字符;去掉。
此開關打開後,php的error_log就可以輸出信息了。
同時將php.ini的顯示錯誤開關打開:
;display_errors=off
修改爲display_errors=on。
修改好這些開關後,重啓php-fpm的服務。
之後就可以在typecho的php代碼中增加error_log以觀察代碼的運行軌跡。
登錄鏈接代碼在typecho的admin目錄login.php,但實際提交的代碼爲:
https://XXX.XXX.net/blog/index.php/action/login?_=7cf73d0584c577c96833bd2e3a58e0f0
_=後面的字符爲一串md5字符,可以不管。
而這個鏈接首先會被nginx的fastcgi_split_path_info功能拆分,對於blog的fastcgi_split_path_info代碼如下:
fastcgi_split_path_info ^(.+?\.php)(\/?.+)$;
fastcgi_split_path_info後面的正則表達式,將/blog/index.php/action/login?_=7cf73d0584c577c96833bd2e3a58e0f0鏈接拆分成兩個group
分別是:
/blog/index.php
與
/action/login?_=7cf73d0584c577c96833bd2e3a58e0f0
然後用$fastcgi_script_name與$fastcgi_path_info存放。
在其後的
set $path_info $fastcgi_path_info;
fastcgi_param PATH_INFO $path_info;
將後部內容傳遞至PATH_INFO(這個變量在typecho中,即可通過從request頭部中獲取PATH_INFO變量來取得)
實際執行腳本的是/blog/index.php
但從index.php來看,代碼很簡單:
<?php
/**
* Typecho Blog Platform
*
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
* @version $Id: index.php 1153 2009-07-02 10:53:22Z magike.net $
*/
/** 載入配置支持 */
if (!defined('__TYPECHO_ROOT_DIR__') && !@include_once 'config.inc.php') {
file_exists('./install.php') ? header('Location: install.php') : print('Missing Config File');
exit;
}
/** 初始化組件 */
\Widget\Init::alloc();
/** 註冊一個初始化插件 */
\Typecho\Plugin::factory('index.php')->begin();
/** 開始路由分發 */
\Typecho\Router::dispatch();
/** 註冊一個結束插件 */
\Typecho\Plugin::factory('index.php')->end();
從網絡中,搜索一堆資料,瞭解到,關鍵代碼在\Typecho\Router::dispatch()
主要內容即根據數據庫中的路由表,分別調用創建對應的Widget,然後調用相應Widget的action函數。
在Router.php的dispatch函數中加上相應的日誌輸出對應的widget。
public static function dispatch()
{
/** 獲取PATHINFO */
$pathInfo = Request::getInstance()->getPathInfo();
foreach (self::$routingTable as $key => $route) {
if (preg_match($route['regx'], $pathInfo, $matches)) {
self::$current = $key;
error_log('route widget ' . $route['widget']);
try {
/** 載入參數 */
$params = null;
if (!empty($route['params'])) {
unset($matches[0]);
$params = array_combine($route['params'], $matches);
}
$widget = Widget::widget($route['widget'], null, $params);
if (isset($route['action'])) {
$widget->{$route['action']}();
}
return;
} catch (\Exception $e) {
if (404 == $e->getCode()) {
Widget::destroy($route['widget']);
continue;
}
throw $e;
}
}
}
/** 載入路由異常支持 */
throw new RouterException("Path '{$pathInfo}' not found", 404);
}
從日誌看出/action/login?_=7cf73d0584c577c96833bd2e3a58e0f0對應的widget爲\Widget\Action
對於var\Widget\Action.php的代碼見內:
<?php
namespace Widget;
use Typecho\Widget;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 執行模塊
*
* @package Widget
*/
class Action extends Widget
{
/**
* 路由映射
*
* @access private
* @var array
*/
private $map = [
'ajax' => '\Widget\Ajax',
'login' => '\Widget\Login',
'logout' => '\Widget\Logout',
'register' => '\Widget\Register',
'upgrade' => '\Widget\Upgrade',
'upload' => '\Widget\Upload',
'service' => '\Widget\Service',
'xmlrpc' => '\Widget\XmlRpc',
'comments-edit' => '\Widget\Comments\Edit',
'contents-page-edit' => '\Widget\Contents\Page\Edit',
'contents-post-edit' => '\Widget\Contents\Post\Edit',
'contents-attachment-edit' => '\Widget\Contents\Attachment\Edit',
'metas-category-edit' => '\Widget\Metas\Category\Edit',
'metas-tag-edit' => '\Widget\Metas\Tag\Edit',
'options-discussion' => '\Widget\Options\Discussion',
'options-general' => '\Widget\Options\General',
'options-permalink' => '\Widget\Options\Permalink',
'options-reading' => '\Widget\Options\Reading',
'plugins-edit' => '\Widget\Plugins\Edit',
'themes-edit' => '\Widget\Themes\Edit',
'users-edit' => '\Widget\Users\Edit',
'users-profile' => '\Widget\Users\Profile',
'backup' => '\Widget\Backup'
];
/**
* 入口函數,初始化路由器
*
* @throws Widget\Exception
*/
public function execute()
{
/** 驗證路由地址 **/
$action = $this->request->action;
/** 判斷是否爲plugin */
$actionTable = array_merge($this->map, unserialize(Options::alloc()->actionTable));
if (isset($actionTable[$action])) {
$widgetName = $actionTable[$action];
}
if (isset($widgetName) && class_exists($widgetName)) {
$widget = self::widget($widgetName);
if ($widget instanceof ActionInterface) {
$widget->action();
return;
}
}
throw new Widget\Exception(_t('請求的地址不存在'), 404);
}
}
基本思想來看,查map表,然後轉到對應的類。對於當前動作,login顯然對應到\Widget\Login.php。
\Widget\Login.php的代碼如下:
<?php
namespace Widget;
use Typecho\Cookie;
use Typecho\Validate;
use Widget\Base\Users;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 登錄組件
*
* @category typecho
* @package Widget
* @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
* @license GNU General Public License 2.0
*/
class Login extends Users implements ActionInterface
{
/**
* 初始化函數
*
* @access public
* @return void
*/
public function action()
{
// protect
$this->security->protect();
/** 如果已經登錄 */
if ($this->user->hasLogin()) {
/** 直接返回 */
$this->response->redirect($this->options->index);
}
/** 初始化驗證類 */
$validator = new Validate();
$validator->addRule('name', 'required', _t('請輸入用戶名'));
$validator->addRule('password', 'required', _t('請輸入密碼'));
$expire = 30 * 24 * 3600;
/** 記住密碼狀態 */
if ($this->request->remember) {
Cookie::set('__typecho_remember_remember', 1, $expire);
} elseif (Cookie::get('__typecho_remember_remember')) {
Cookie::delete('__typecho_remember_remember');
}
/** 截獲驗證異常 */
if ($error = $validator->run($this->request->from('name', 'password'))) {
Cookie::set('__typecho_remember_name', $this->request->name);
/** 設置提示信息 */
Notice::alloc()->set($error);
$this->response->goBack();
}
/** 開始驗證用戶 **/
$valid = $this->user->login(
$this->request->name,
$this->request->password,
false,
1 == $this->request->remember ? $expire : 0
);
/** 比對密碼 */
if (!$valid) {
/** 防止窮舉,休眠3秒 */
sleep(3);
self::pluginHandle()->loginFail(
$this->user,
$this->request->name,
$this->request->password,
1 == $this->request->remember
);
Cookie::set('__typecho_remember_name', $this->request->name);
Notice::alloc()->set(_t('用戶名或密碼無效'), 'error');
$this->response->goBack('?referer=' . urlencode($this->request->referer));
}
self::pluginHandle()->loginSucceed(
$this->user,
$this->request->name,
$this->request->password,
1 == $this->request->remember
);
/** 跳轉驗證後地址 */
if (!empty($this->request->referer)) {
/** fix #952 & validate redirect url */
if (
0 === strpos($this->request->referer, $this->options->adminUrl)
|| 0 === strpos($this->request->referer, $this->options->siteUrl)
) {
$this->response->redirect($this->request->referer);
}
} elseif (!$this->user->pass('contributor', true)) {
/** 不允許普通用戶直接跳轉後臺 */
$this->response->redirect($this->options->profileUrl);
}
$this->response->redirect($this->options->adminUrl);
}
}
在此action函數中,加上相應日誌,問題定位到此處代碼:
$this->security->protect();
代碼在執行到此行,即已經返回,也就根本沒有再執行後面的校驗用戶名、密碼等動作。
protect函數的代碼如下,相當簡單
/**
* 保護提交數據
*/
public function protect()
{
if ($this->enabled && $this->request->get('_') != $this->getToken($this->request->getReferer())) {
$this->response->goBack();
}
}
在此函數中加上日誌定位到,此處的request->get('_')獲取即爲請求中的7cf73d0584c577c96833bd2e3a58e0f0,
而getToken函數如下:
/**
* 獲取token
*
* @param string|null $suffix 後綴
* @return string
*/
public function getToken(?string $suffix): string
{
return md5($this->token . '&' . $suffix);
}
根據token與request的getReferer計算的一個md5內容。
token及相應計算規則應該是沒有問題的,因此懷疑點定位到getReferer函數的返回內容。
在網上搜索一番後,發現已經有人記錄了此種問題及相應解決辦法,鏈接:https://blog.warhut.cn/dmbj/423.html
引用如下:
------------------------------------------------------------------------------------------------------------------
如果加入了no-referrer
,將會導致typecho
無法登錄後臺,原因如下:
<input type="hidden" name="referrer" value="<?php echo htmlspecialchars($request->get('referrer')); ?>" />
由於typecho
是通過referrer
登錄的後臺地址,傳輸參數,所以當加入no-referrer
之後相當於刪除了提交的地址。
通過下面即可解決,加在<head></head>
中。
<meta name="referrer" content="same-origin" />
------------------------------------------------------------------------------------------------------------------
解決辦法,先在admin\Header.php中加入上面的referer行,如下:
<?php
if (!defined('__TYPECHO_ADMIN__')) {
exit;
}
$header = '<link rel="stylesheet" href="' . $options->adminStaticUrl('css', 'normalize.css', true) . '">
<link rel="stylesheet" href="' . $options->adminStaticUrl('css', 'grid.css', true) . '">
<link rel="stylesheet" href="' . $options->adminStaticUrl('css', 'style.css', true) . '">';
/** 注▒~F~L▒~@個▒~H~]▒~K▒~L~V▒~O~R件 */
$header = \Typecho\Plugin::factory('admin/header.php')->header($header);
?><!DOCTYPE HTML>
<html>
<head>
<meta charset="<?php $options->charset(); ?>">
<meta name="renderer" content="webkit">
<meta name="referrer" content="same-origin" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title><?php _e('%s - %s - Powered by Typecho', $menu->title, $options->title); ?></title>
<meta name="robots" content="noindex, nofollow">
<?php echo $header; ?>
</head>
<body<?php if (isset($bodyClass)) {echo ' class="' . $bodyClass . '"';} ?>>
加入後,登錄功能即正常,但blog首頁中的退出登錄還有問題,點擊退出類似於登錄異常的情形,直接返回。
採取在首頁相應的header處,添加相應代碼。
themes中的header.php中代碼如下(默認theme):
<?php if (!defined('__TYPECHO_ROOT_DIR__')) exit; ?>
<!DOCTYPE HTML>
<html>
<head>
<meta charset="<?php $this->options->charset(); ?>">
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title><?php $this->archiveTitle([
'category' => _t('分類 %s 下的文章'),
'search' => _t('包含關鍵字 %s 的文章'),
'tag' => _t('標籤 %s 下的文章'),
'author' => _t('%s 發佈的文章')
], '', ' - '); ?><?php $this->options->title(); ?></title>
<!-- 使用url函數轉換相關路徑 -->
<link rel="stylesheet" href="<?php $this->options->themeUrl('normalize.css'); ?>">
<link rel="stylesheet" href="<?php $this->options->themeUrl('grid.css'); ?>">
<link rel="stylesheet" href="<?php $this->options->themeUrl('style.css'); ?>">
<!-- 通過自有函數輸出HTML頭部信息 -->
<?php $this->header(); ?>
</head>
<body>
這兒首頁的header()函數,實際對應到var/Widget/Archive.php中的header函數,
/**
* 輸出頭部元數據
*
* @param string|null $rule 規則
*/
public function header(?string $rule = null)
{
$rules = [];
$allows = [
'description' => htmlspecialchars($this->description),
'keywords' => htmlspecialchars($this->keywords),
'generator' => $this->options->generator,
'template' => $this->options->theme,
'pingback' => $this->options->xmlRpcUrl,
'xmlrpc' => $this->options->xmlRpcUrl . '?rsd',
'wlw' => $this->options->xmlRpcUrl . '?wlw',
'rss2' => $this->feedUrl,
'rss1' => $this->feedRssUrl,
'commentReply' => 1,
'antiSpam' => 1,
'atom' => $this->feedAtomUrl
];
/** 頭部是否輸出聚合 */
$allowFeed = !$this->is('single') || $this->allow('feed') || $this->makeSinglePageAsFrontPage;
if (!empty($rule)) {
parse_str($rule, $rules);
$allows = array_merge($allows, $rules);
}
$allows = self::pluginHandle()->headerOptions($allows, $this);
$title = (empty($this->archiveTitle) ? '' : $this->archiveTitle . ' » ') . $this->options->title;
$header = '<meta name="referrer" content="same-origin" />';
if (!empty($allows['description'])) {
$header .= '<meta name="description" content="' . $allows['description'] . '" />' . "\n";
}
if (!empty($allows['keywords'])) {
$header .= '<meta name="keywords" content="' . $allows['keywords'] . '" />' . "\n";
}
if (!empty($allows['generator'])) {
$header .= '<meta name="generator" content="' . $allows['generator'] . '" />' . "\n";
}
if (!empty($allows['template'])) {
$header .= '<meta name="template" content="' . $allows['template'] . '" />' . "\n";
}
if (!empty($allows['pingback']) && 2 == $this->options->allowXmlRpc) {
$header .= '<link rel="pingback" href="' . $allows['pingback'] . '" />' . "\n";
}
if (!empty($allows['xmlrpc']) && 0 < $this->options->allowXmlRpc) {
$header .= '<link rel="EditURI" type="application/rsd+xml" title="RSD" href="'
. $allows['xmlrpc'] . '" />' . "\n";
}
.....................
即可解決首頁退出登錄情況。