原創技術
# handsome # 程式碼問題 # 程式碼

解決handsome時光機發布失敗的各類問題

1.1萬 字22 分鐘0 瀏覽0 按讚

摘要

本文針對Handsome主題時光機發布失敗問題,分析了錯誤碼200和PDO序列化異常的原因。作者透過降級PHP7.4及修改Typecho核心檔案Db.php成功解決,同時彙總了官方關於圖片上傳、emoji支援、評論失敗等常見問題的排查與修復方法,為使用該主題的使用者提供實用指南。

遷移到typecho的時候,自然而然地就選擇了handsome,不為別的因為我就只買了這一款主題,那麼對於我來說能用的功能都要用上,尤其是時光機的功能。

問題截圖

問題描述

再微信公眾號釋出內容提示以下報錯

错误码200<pre><code><h1>Serialization of 'PDO' is not allowed</h1></code></pre>
请求失败500 Server Error: Internal Server Error for url: https://tutouzhang.com/

在瀏覽器釋出內容提示以下報錯

Serialization of 'PDO' is not allowed
我沒有自行建立公眾號來發布,因此只使用了這兩種發布方式,但兩者都出現錯誤。其中微信公眾號發布出現了兩個錯誤。

接下來說明我是如何解決這些問題的。

官方問題解答:評論/時光機相關

時光機來自typecho的評論功能複用而來,所以大部分問題是相似的

時光機傳送微信顯示“該公眾號提供的服務出現故障”

  • 檢查作者的部落格是否正常開啟 www.ihewro.com 如果可以正常開啟,說明公眾號服務是正常的;如果打不開,可以聯絡作者或者稍作等待
  • 檢查你在授權平臺填寫的地址是不是和你的部落格地址完全一致,因為最終公眾號請求的地址是授權平臺上填寫的地址,而不是你在公眾號中繫結時候輸入的地址
    • 比如 www字首、http還是https 要和你的部落格地址完全一致
  • 在微信繫結新的網站之前,一定要先解除繫結,否則一旦一個微訊號繫結多個網站就會導致問題,此時需要聯絡作者手動解除繫結

sqlite 資料庫中時光機建立新標籤會導致頁面卡死

暫時需要透過修改typecho程式碼修復。

  • 開啟typecho目錄下的 var/Widget/Feedback.php
  • 搜尋 $this->db->fetchRow($this->select()->where('coid = ?' 所在行
  • 修改前:
          $this->db->fetchRow($this->select()->where('coid = ?', $commentId)
              ->limit(1), [$this, 'push']);
    
  • 修改後:
          $item = $this->db->fetchRow($this->select()->where('coid = ?', $commentId)
              ->limit(1));
          $this->push($item);
    

評論區不能鬥圖不能顯示圖片 | 說說不能顯示圖片/音樂播放器/影片播放器

  • 後臺 设置——评论——允许使用的HTML标签和属性 裡面新增html標籤
<img src="">
<audio class="" src="" preload>
<video src=""></video>

時光機/說說上傳圖片失敗

  • 檢查typecho目錄下 usr/uploads 目錄下是否有 time 資料夾,如果沒有手動建立一個,然後給該資料夾777許可權
  • Linux設定資料夾777許可權命令:sudo chmod -R 777 ./
  • 時光機上傳圖片增加了圖片壓縮的流程,如果你的伺服器環境不支援gd擴充套件,可能導致圖片上傳成功,但是時光機顯示“上傳失敗”,可以開啟主題目錄下 libs/Utils.php 搜尋 (new Imgcompress 註釋或者刪除該行程式碼即可

評論區無法使用markdown語法

  • 後臺 设置——评论——在评论中使用Markdown语法(选中) 後臺 设置——评论——允许使用的HTML标签和属性 裡面新增markdown語法對應的html標籤
<blockquote><pre><code><strong><em><h5><h6><a href title><table><thead><tr><th><tbody><td>

說說的地理位置資訊不顯示

地理資訊中包含了一個emoji,type cho預設不支援emoji,需要修改資料庫的欄位型別:typecho 支援 emoji 教程(具體可以百度 typecho 支援emoji 有教程的)

時光機如何刪除說說

  • 和評論管理一樣,在typecho後臺的管理——評論裡面刪除即可

無法向部落格時光機傳送內容

  • 授權平臺填寫的域名有錯誤,顯示的錯誤一般有 HTTPSConnectionPool(host= xxxx',port=443):Max retries exceeded with url:/Caused by
    • 授權平臺上填寫的域名是 https,但是你的部落格不支援ssl
    • 確保授權平臺上你的部落格域名和你的站點地址完全一致,比如是否有www,協議是http還是https
    • 最終是看授權平臺上填寫的域名,即使在微信中繫結資訊輸入的地址是 http://,但是在授權平臺上填寫的地址 https:// ,那最終還是請求的還是 https://的地址
  • 某些伺服器做了ua判斷,所以無法透過請求
  • 某些伺服器有cc防護,禁止請求

評論失敗的具體排查方式

請升級到主題的最新版本,對於低版本主題的評論失敗問題不再維護

評論失敗有兩種情況:

  • 一種有具體的錯誤提示,
  • 一種沒有具體的錯誤,提示「評論請求失敗」或者「評論可能被攔截且無反饋資訊」。
有具體錯誤提示

具體的錯誤提示分為下面幾種:

  • 具體的php錯誤輸出,比如提示呼叫某個函式不存在 Notice: Undefined function等等,一般是某個外掛導致的,可以排查一下與評論相關外掛(如評論郵件提醒等)
  • 「資料庫查詢錯誤 Database Query Error」,一般有兩種情況:
  • 具體中文攔截原因,如「對不起, 您的發言過於頻繁, 請稍侯再次釋出.」,或者是其它的中文錯誤提示,屬於正常的。
    • 在typecho的評論設定裡面開啟了「同一 IP 釋出評論的時間間隔限制」
    • 使用了評論反垃圾外掛觸發了攔截規則
  • 「頁面如果被快取無法顯示最新評論」,可能的情況為:
    • 使用的外掛向頁面插入了舊版本的jquery.js 與主題衝突,可以按下F12看下報錯,如「炫彩滑鼠」,需要在外掛設定裡面不載入jquery或者禁用外掛
    • CDN快取靜態快取了 HTML頁面,導致評論功能失敗(CDN快取建議只快取靜態資原始檔,如 css,js,image),因為你靜態快取了頁面,頁面就不會更新,怎麼能顯示你的評論呢!!
    • 使用了伺服器快取功能
    • 使用了資料庫快取外掛
    • 使用了頁面程式碼壓縮的外掛
    • 如果上述流程都排查後仍然有問題,可以切換到預設主題(切換前需要備份handsome設定資料,否則會被清空),如果預設主題不存在該問題,handsome主題存在該問題,可以聯絡作者看一下
沒有具體的錯誤提示,提示「評論請求失敗」或者「評論可能被攔截且無反饋資訊」
  • 不要在主題中加入 <meta name="referrer" content="no-referrer" />這行程式碼!
  • 開了防火牆之類的攔截了post請求,一般是評論中包含了html程式碼,防火牆就會被攔截

(這種情況,可以在外觀設定——主題增強功能裡面關掉 ajax評論的開關,然後再次評論檢視具體的錯誤資訊)

如果按照上面流程排查後仍然無法解決問題,可以切換到預設主題(切換前務必在外觀設定介面備份資料),如果預設主題評論正常,handsome主題不正常,可以聯絡我。

評論/時光機希望開啟的時候顯示最新的評論

如果希望開啟文章/時光機預設顯示最新的評論,需要在typecho 後臺設定——評論,啟用分頁, 並且 在列出時將第一頁作為預設顯示。(主題強制設定了「將較新的的評論顯示在前面」,也就是說第一頁是最新的評論,最後一頁是時間最久的評論)

時光機/評論頁面如何分頁

typecho後臺管理——設定——評論中啟用分頁即可:

请输入图片描述

我發現這對我沒有太大作用,因此還是自己動手排查比較好。

排查步驟

500的報錯就不用我解釋了,但是我瀏覽器可以訪問,公眾號卻不行,所以我懷疑是作者的問題,後來我嘗試從php8.0換到php7.4解決了這個問題。

公眾號錯誤碼200的這個報錯,說明是響應成功了的,但是返回的頁面是錯誤的, 那麼我懷疑typecho或者handsome進行了返回正常狀態,其實他可能還是500的報錯。

瀏覽器的這個報錯發生在嘗試序列化 PDO 物件時,PDO 是 PHP 的資料庫連線類,不能被序列化,

我猜想:會話管理($_SESSION)、快取機制、物件持久化,或某個類在儲存狀態時包含了 PDO。

因為這個猜想導致我禁用了所有的外掛進行排查,還是沒有排查出問題,所以我將矛頭指向了typecho,我嚴重懷疑他的1.2.1的版本有所改動。

完美解決

找到var/Typecho/Db.php

貼上以下程式碼進行覆蓋,強烈建議備份檔案

<?php

namespace Typecho;

use Typecho\Db\Adapter;
use Typecho\Db\Query;
use Typecho\Db\Exception as DbException;

/**
 * 包含获取数据支持方法的类.
 * 必须定义__TYPECHO_DB_HOST__, __TYPECHO_DB_PORT__, __TYPECHO_DB_NAME__,
 * __TYPECHO_DB_USER__, __TYPECHO_DB_PASS__, __TYPECHO_DB_CHAR__
 *
 * @package Db
 */
class Db
{
    /** 读取数据库 */
    public const READ = 1;

    /** 写入数据库 */
    public const WRITE = 2;

    /** 升序方式 */
    public const SORT_ASC = 'ASC';

    /** 降序方式 */
    public const SORT_DESC = 'DESC';

    /** 表内连接方式 */
    public const INNER_JOIN = 'INNER';

    /** 表外连接方式 */
    public const OUTER_JOIN = 'OUTER';

    /** 表左连接方式 */
    public const LEFT_JOIN = 'LEFT';

    /** 表右连接方式 */
    public const RIGHT_JOIN = 'RIGHT';

    /** 数据库查询操作 */
    public const SELECT = 'SELECT';

    /** 数据库更新操作 */
    public const UPDATE = 'UPDATE';

    /** 数据库插入操作 */
    public const INSERT = 'INSERT';

    /** 数据库删除操作 */
    public const DELETE = 'DELETE';

    /**
     * 数据库适配器
     * @var Adapter
     */
    private $adapter;

    /**
     * 默认配置
     *
     * @var array
     */
    private $config;

    /**
     * 已经连接
     *
     * @access private
     * @var array
     */
    private $connectedPool;

    /**
     * 前缀
     *
     * @access private
     * @var string
     */
    private $prefix;

    /**
     * 适配器名称
     *
     * @access private
     * @var string
     */
    private $adapterName;

    /**
     * 实例化的数据库对象
     * @var Db
     */
    private static $instance;

    /**
     * 数据库类构造函数
     *
     * @param mixed $adapterName 适配器名称
     * @param string $prefix 前缀
     *
     * @throws DbException
     */
    public function __construct($adapterName, string $prefix = 'typecho_')
    {
        /** 获取适配器名称 */
        $adapterName = $adapterName == 'Mysql' ? 'Mysqli' : $adapterName;
        $this->adapterName = $adapterName;

        /** 数据库适配器 */
        $adapterName = '\Typecho\Db\Adapter\\' . str_replace('_', '\\', $adapterName);

        if (!call_user_func([$adapterName, 'isAvailable'])) {
            throw new DbException("Adapter {$adapterName} is not available");
        }

        $this->prefix = $prefix;

        /** 初始化内部变量 */
        $this->connectedPool = [];

        $this->config = [
            self::READ => [],
            self::WRITE => []
        ];

        //实例化适配器对象
        $this->adapter = new $adapterName();
    }

    /**
     * @return Adapter
     */
    public function getAdapter(): Adapter
    {
        return $this->adapter;
    }

    /**
     * 获取适配器名称
     *
     * @access public
     * @return string
     */
    public function getAdapterName(): string
    {
        return $this->adapterName;
    }

    /**
     * 获取表前缀
     *
     * @access public
     * @return string
     */
    public function getPrefix(): string
    {
        return $this->prefix;
    }

    /**
     * @param Config $config
     * @param int $op
     */
    public function addConfig(Config $config, int $op)
    {
        if ($op & self::READ) {
            $this->config[self::READ][] = $config;
        }

        if ($op & self::WRITE) {
            $this->config[self::WRITE][] = $config;
        }
    }

    /**
     * getConfig
     *
     * @param int $op
     *
     * @return Config
     * @throws DbException
     */
    public function getConfig(int $op): Config
    {
        if (empty($this->config[$op])) {
            /** DbException */
            throw new DbException('Missing Database Connection');
        }

        $key = array_rand($this->config[$op]);
        return $this->config[$op][$key];
    }

    /**
     * 重置连接池
     *
     * @return void
     */
    public function flushPool()
    {
        $this->connectedPool = [];
    }

    /**
     * 选择数据库
     *
     * @param int $op
     *
     * @return mixed
     * @throws DbException
     */
    public function selectDb(int $op)
    {
        if (!isset($this->connectedPool[$op])) {
            $selectConnectionConfig = $this->getConfig($op);
            $selectConnectionHandle = $this->adapter->connect($selectConnectionConfig);
            $this->connectedPool[$op] = $selectConnectionHandle;
        }

        return $this->connectedPool[$op];
    }

    /**
     * 获取SQL词法构建器实例化对象
     *
     * @return Query
     */
    public function sql(): Query
    {
        return new Query($this->adapter, $this->prefix);
    }

    /**
     * 为多数据库提供支持
     *
     * @access public
     * @param array $config 数据库实例
     * @param integer $op 数据库操作
     * @return void
     */
    public function addServer(array $config, int $op)
    {
        $this->addConfig(Config::factory($config), $op);
        $this->flushPool();
    }

    /**
     * 获取版本
     *
     * @param int $op
     *
     * @return string
     * @throws DbException
     */
    public function getVersion(int $op = self::READ): string
    {
        return $this->adapter->getVersion($this->selectDb($op));
    }

    /**
     * 设置默认数据库对象
     *
     * @access public
     * @param Db $db 数据库对象
     * @return void
     */
    public static function set(Db $db)
    {
        self::$instance = $db;
    }

    /**
     * 获取数据库实例化对象
     * 用静态变量存储实例化的数据库对象,可以保证数据连接仅进行一次
     *
     * @return Db
     * @throws DbException
     */
    public static function get(): Db
    {
        if (empty(self::$instance)) {
            /** DbException */
            throw new DbException('Missing Database Object');
        }

        return self::$instance;
    }

    /**
     * 选择查询字段
     *
     * @param ...$ags
     *
     * @return Query
     * @throws DbException
     */
    public function select(...$ags): Query
    {
        $this->selectDb(self::READ);

        $args = func_get_args();
        return call_user_func_array([$this->sql(), 'select'], $args ?: ['*']);
    }

    /**
     * 更新记录操作(UPDATE)
     *
     * @param string $table 需要更新记录的表
     *
     * @return Query
     * @throws DbException
     */
    public function update(string $table): Query
    {
        $this->selectDb(self::WRITE);

        return $this->sql()->update($table);
    }

    /**
     * 删除记录操作(DELETE)
     *
     * @param string $table 需要删除记录的表
     *
     * @return Query
     * @throws DbException
     */
    public function delete(string $table): Query
    {
        $this->selectDb(self::WRITE);

        return $this->sql()->delete($table);
    }

    /**
     * 插入记录操作(INSERT)
     *
     * @param string $table 需要插入记录的表
     *
     * @return Query
     * @throws DbException
     */
    public function insert(string $table): Query
    {
        $this->selectDb(self::WRITE);

        return $this->sql()->insert($table);
    }

    /**
     * @param $table
     * @throws DbException
     */
    public function truncate($table)
    {
        $table = preg_replace("/^table\./", $this->prefix, $table);
        $this->adapter->truncate($table, $this->selectDb(self::WRITE));
    }

    /**
     * 执行查询语句
     *
     * @param mixed $query 查询语句或者查询对象
     * @param int $op 数据库读写状态
     * @param string $action 操作动作
     *
     * @return mixed
     * @throws DbException
     */
    public function query($query, int $op = self::READ, string $action = self::SELECT)
    {
        $table = null;

        /** 在适配器中执行查询 */
        if ($query instanceof Query) {
            $action = $query->getAttribute('action');
            $table = $query->getAttribute('table');
            $op = (self::UPDATE == $action || self::DELETE == $action
                || self::INSERT == $action) ? self::WRITE : self::READ;
        } elseif (!is_string($query)) {
            /** 如果query不是对象也不是字符串,那么将其判断为查询资源句柄,直接返回 */
            return $query;
        }

        /** 选择连接池 */
        $handle = $this->selectDb($op);

        /** 提交查询 */
        $resource = $this->adapter->query($query instanceof Query ?
            $query->prepare($query) : $query, $handle, $op, $action, $table);

        if ($action) {
            //根据查询动作返回相应资源
            switch ($action) {
                case self::UPDATE:
                case self::DELETE:
                    return $this->adapter->affectedRows($resource, $handle);
                case self::INSERT:
                    return $this->adapter->lastInsertId($resource, $handle);
                case self::SELECT:
                default:
                    return $resource;
            }
        } else {
            //如果直接执行查询语句则返回资源
            return $resource;
        }
    }

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

        return $filter ? array_map($filter, $result) : $result;
    }

    /**
     * 一次取出一行
     *
     * @param mixed $query 查询对象
     * @param callable|null $filter 行过滤器函数,将查询的每一行作为第一个参数传入指定的过滤器中
     * @return array|null
     * @throws DbException
     */
    public function fetchRow($query, ?callable $filter = null): ?array
    {
        $resource = $this->query($query);

        return ($rows = $this->adapter->fetch($resource)) ?
            ($filter ? call_user_func($filter, $rows) : $rows) :
            null;
    }

    /**
     * 一次取出一个对象
     *
     * @param mixed $query 查询对象
     * @param array|null $filter 行过滤器函数,将查询的每一行作为第一个参数传入指定的过滤器中
     * @return object|null
     * @throws DbException
     */
    public function fetchObject($query, ?array $filter = null): ?object
    {
        $resource = $this->query($query);

        return ($rows = $this->adapter->fetchObject($resource)) ?
            ($filter ? call_user_func($filter, $rows) : $rows) :
            null;
    }

    /**
     * 序列化时保存必要的配置信息,不包含连接池
     *
     * @return array
     */
    public function __serialize(): array
    {
        return [
            'adapterName' => $this->adapterName,
            'prefix' => $this->prefix,
            'config' => $this->config
        ];
    }

    /**
     * 反序列化时恢复配置并初始化适配器
     *
     * @param array $data 序列化数据
     * @return void
     */
    public function __unserialize(array $data): void
    {
        $this->adapterName = $data['adapterName'];
        $this->prefix = $data['prefix'];
        $this->config = $data['config'];
        $this->connectedPool = []; // 重置连接池,防止 PDO 被序列化

        // 重新实例化适配器
        $adapterName = '\Typecho\Db\Adapter\\' . str_replace('_', '\\', $this->adapterName);
        if (!class_exists($adapterName)) {
            throw new DbException("Adapter {$adapterName} is not available");
        }
        $this->adapter = new $adapterName();
    }
}

工作原理

  • 序列化時:__serialize() 返回一個不包含 $connectedPool 和 $adapter 的陣列,避免試圖序列化 PDO。
  • 反序列化時:__unserialize() 從資料中恢復配置,重置 $connectedPool,並重新建立 $adapter。下次資料庫操作(如 selectDb())會自動重新建立連線。

修改說明

  • 新增位置:__serialize() 和 __unserialize() 方法被新增到檔案末尾,作為 Db 類的最後兩個方法。

  • 功能:

    • __serialize():只儲存 $adapterName、$prefix 和 $config,忽略 $connectedPool 和 $adapter。
    • __unserialize():恢復儲存的屬性,重置 $connectedPool,並重新建立 $adapter 例項。
  • 相容性:適用於 PHP 7.4+。8.0測試也沒有問題。

留言載入中…
相關文章

相關文章