补锅:handsome引用文章随机头图重复

BUG简述

近期更新了handsome 5.2.0,后台写文章时发现编辑器工具栏上贴心地增加了增强功能的快捷按钮,于是稍微试了试。今天的主角就是我们的“调用其他文章”功能。

方便起见,在本地搭建typecho+handsome测试环境。

复现如下:

Bug复现

emm,原来你管这叫随机……

修复方法

众所周知出锅需要补。

根据常识,对增强功能的解析在libs/Content.php中完成,于是定位到该文件相关位置:

postContent方法开始跟踪,相关代码如下:

    /**
     * 输入内容之前做一些有趣的替换+输出文章内容
     *
     * @param $obj
     * @param $status
     * @return string
     */
    public static function postContent($obj,$status){
        $content = $obj->content;
        $options = mget();
        //...
        //中略
        //...
            $content = Content::parseContentPublic($content);
        }
        return trim($content);
    }

注意到解析外部文章调用逻辑存在于parseContentPublic方法中,定位到该方法继续跟踪,相关代码如下:

    /**
     * 一些公用的解析,文章、评论、时光机公用的,与用户状态无关
     * @param $content
     * @return null|string|string[]
     */
    public static function parseContentPublic($content){
        $options = mget();
        //...
        //中略
        //...
        //调用其他文章页面的摘要
        if ( strpos( $content, '[post')!== false) {//提高效率,避免每篇文章都要解析
            $pattern = self::get_shortcode_regex(array('post'));
            $content = preg_replace_callback("/$pattern/",array('Content','quoteOtherPostCallback'), $content);
        }
        //...
        //后略
        //...

显然,具体内容替换已经交由Content::quoteOtherPostCallback()处理,定位至该方法,相关代码如下:

    /**
     * 一篇文章中引用另一篇文章正则替换回调函数
     * @param $matches
     * @return bool|string
     */
    public static function quoteOtherPostCallback($matches){
        $options = mget();
        // 不解析类似 [[post]] 双重括号的代码
        if ( $matches[1] == '[' && $matches[6] == ']' ) {
            return substr($matches[0], 1, -1);
        }

        //对$matches[3]的url如果被解析器解析成链接,这些需要反解析回来
        $matches[3] = preg_replace("/<a href=.*?>(.*?)<\/a>/",'$1',$matches[3]);
        $attr = htmlspecialchars_decode($matches[3]);//还原转义前的参数列表
        $attrs = self::shortcode_parse_atts($attr);//获取短代码的参数

        //这里需要对id做一个判断,避免空值出现错误
        $cid = @$attrs["cid"];
        $url = @$attrs['url'];
        $cover = @$attrs['cover'];//封面
        $targetTitle = "";//标题
        $targetUrl = "";//链接
        $targetSummary ="";//简介文字
        $targetImgSrc = "";//封面图片地址
        if (!empty($cid)){
            $db = Typecho_Db::get();
            $prefix = $db->getPrefix();
            $posts = $db->fetchAll($db
                ->select()->from($prefix.'contents')
                ->orWhere('cid = ?',$cid)
                ->where('type = ? AND status = ? AND password IS NULL', 'post', 'publish'));
            //这里需要对id正确性进行一个判断,避免查找文章失败
            if (count($posts) == 0){
                $targetTitle = "文章不存在,或文章是加密、私密文章";
            }else{
                $result = Typecho_Widget::widget('Widget_Abstract_Contents')->push($posts[0]);
                if ($cover == ""){
                    $thumbArray =  $db->fetchAll($db
                        ->select()->from($prefix.'fields')
                        ->orWhere('cid = ?',$cid)
                        ->where('name = ? ', 'thumb'));
                    $targetImgSrc = Content:: whenSwitchHeaderImgSrc(0,2,null,$result['text'],@$thumbArray[0]['str_value']);
                }else{
                    $targetImgSrc = $cover;
                }
                $targetSummary = Content::excerpt(Markdown::convert($result['text']),60);
                $targetTitle = $result['title'];
                $targetUrl = $result['permalink'];
            }
        }else if (empty($cid) && $url !=""){
            $targetUrl = $url;
            $targetSummary = @$attrs['intro'];
            $targetTitle = @$attrs['title'];
            $targetImgSrc = $cover;
        }else{
            $targetTitle = "文章不存在,请检查文章CID";
        }

        $imageHtml = "";
        $noImageCss = "";
        if (trim($targetImgSrc) != ""){
            $imageHtml = '<div class="inner-image bg" style="background-image: url('.$targetImgSrc.');background-size: cover;"></div>
';
        }else{
            $noImageCss = 'style="margin-left: 10px;"';
        }

        return <<<EOF
<div class="preview">
   <div class="post-inser post">
    <a href="{$targetUrl}" target="_blank" class="post_inser_a ">
    {$imageHtml}
    <div class="inner-content" $noImageCss>
     <p class="inser-title">{$targetTitle}</p>
     <div class="inster-summary">
      {$targetSummary}
     </div>
    </div>
    </a>
    <!-- .inner-content #####-->
   </div>
   <!-- .post-inser ####-->
  </div>
EOF;

    }

该方法中头图又由Content::whenSwitchHeaderImgSrc()获取,跟踪:

    /**
     * 处理具体的头图显示逻辑:当有头图时候,显示随机图片还是第一个附件还是一张图片还是thumb字段
     * @param int $index
     * @param $howToThumb 显示缩略图的方式,0,1,2,3
     * @param $attach 文章的第一个附件
     * @param $content 文章内容
     * @param $thumbField thumb字段
     * @return string
     */
    public static function whenSwitchHeaderImgSrc($index =0,$howToThumb,$attach,$content,$thumbField){
        $options = mget();
        $randomNum = unserialize(INDEX_IMAGE_ARRAY);

        // 随机缩略图路径
        $random = THEME_URL . 'usr/img/sj/' . @$randomNum[$index] . '.png';//如果有文章置顶,这里可能会导致index not undefined
        $pattern = '/\<img.*?src\=\"(.*?)\"[^>]*>/i';
        $patternMD = '/\!\[.*?\]\((http(s)?:\/\/.*?(jpg|png))/i';
        $patternMDfoot = '/\[.*?\]:\s*(http(s)?:\/\/.*?(jpg|png))/i';

        if ($howToThumb == '0'){
            return $random;
        }elseif ($howToThumb == '1' || $howToThumb == '2'){
            if (!empty($thumbField)){
                return $thumbField;
            }elseif ($attach!=null && isset($attach->isImage) && $attach->isImage == 1){
                return $attach->url;
            }else{
                if (preg_match_all($pattern, $content, $thumbUrl)){
                    $thumb = $thumbUrl[1][0];
                }elseif (preg_match_all($patternMD, $content, $thumbUrl)){
                    $thumb = $thumbUrl[1][0];
                }elseif (preg_match_all($patternMDfoot, $content, $thumbUrl)){
                    $thumb = $thumbUrl[1][0];
                }else{//文章中没有图片
                    if ($howToThumb == '1'){
                        return '';
                    }else{
                        return $random;
                    }
                }
                return $thumb;
            }
        }elseif ($howToThumb == '3'){
            if (!empty($thumbField)){
                return $thumbField;
            }else{
                return $random;
            }
        }
    }

由于无论几次调用,传入的$index始终为0(仅在文章列表显示逻辑中该参数发挥作用,具体暂未作分析),而随机头图的逻辑是将整个随机头图集随机乱序从中取索引值为$index的一项进行输出,且乱序操作仅在开始前进行一次,故此时当前文章大头图(如果随机)、文章内引用的其他文章展现头图(如果随机)均为一致项目。

个人认为展现效果较差,尤其是当这篇文章是随机头图,而引用处又距离大头图较近,将产生单调感。

出于稳定性和维护方便考虑不操作$index参数,而是考虑在whenSwitchHeaderImgSrc方法中新建static变量,每次随机调用时将该变量+1,从而达到显示不同的随机头图效果。

先放出代码,然后进行必要解释。

    /**
     * 处理具体的头图显示逻辑:当有头图时候,显示随机图片还是第一个附件还是一张图片还是thumb字段
     * @param int $index
     * @param $howToThumb 显示缩略图的方式,0,1,2,3
     * @param $attach 文章的第一个附件
     * @param $content 文章内容
     * @param $thumbField thumb字段
     * @param $quote 是否在处理调用外部文章 0没有 1正在处理 2复位
     * @return string
     */
    public static function whenSwitchHeaderImgSrc($index =0,$howToThumb,$attach,$content,$thumbField,$quote=0){
        static $used=1;//(ucw)解决引用其他文章随机头图重复问题
        //(ucw)附注:存在潜在bug,随机头图集的size需要足够大
        if($quote==2){$used=1;return '';}
        $options = mget();
        $randomNum = unserialize(INDEX_IMAGE_ARRAY);
        // 随机缩略图路径
        $random = THEME_URL . 'usr/img/sj/' . @$randomNum[$quote?$used:$index] . '.png';//如果有文章置顶,这里可能会导致index not undefined
        $pattern = '/\<img.*?src\=\"(.*?)\"[^>]*>/i';
        $patternMD = '/\!\[.*?\]\((http(s)?:\/\/.*?(jpg|png))/i';
        $patternMDfoot = '/\[.*?\]:\s*(http(s)?:\/\/.*?(jpg|png))/i';

        if ($howToThumb == '0'){
            if($quote)$used++;
            return $random;
        }elseif ($howToThumb == '1' || $howToThumb == '2'){
            if (!empty($thumbField)){
                return $thumbField;
            }elseif ($attach!=null && isset($attach->isImage) && $attach->isImage == 1){
                return $attach->url;
            }else{
                if (preg_match_all($pattern, $content, $thumbUrl)){
                    $thumb = $thumbUrl[1][0];
                }elseif (preg_match_all($patternMD, $content, $thumbUrl)){
                    $thumb = $thumbUrl[1][0];
                }elseif (preg_match_all($patternMDfoot, $content, $thumbUrl)){
                    $thumb = $thumbUrl[1][0];
                }else{//文章中没有图片
                    if ($howToThumb == '1'){
                        return '';
                    }else{
                        if($quote)$used++;
                        return $random;
                    }
                }
                return $thumb;
            }
        }elseif ($howToThumb == '3'){
            if (!empty($thumbField)){
                return $thumbField;
            }else{
                if($quote)$used++;
                return $random;
            }
        }
    }

我新建了一个integer型参数$quote,作为是否正在处理引用的标志。static变量$used标记目前可用(未重复)的首个随机头图索引,当且仅当需要返回随机结果且正在处理引用情况时,使$used++$used初始化为1防止与当前文章的随机头图重复。

这样做有一个潜在bug:当随机头图的数量小于或等于引用文章的数目时,数组的对应位置不存在,此时会出现显示问题。(由于php的变量声明真的很随意,这只是一个warning级别的错误,最终输出的url是一个不存在的地址,前台访问时会404导致图片无法显示)

我忽视这个bug的原因是我为自己配置了44张随机头图(具体来源信息参见左侧边栏“友情链接->总览”),而我不太可能一次性引用44篇文章。

修复这个潜在bug也非常简单,只需要将$used对随机头图数目取模。

上述的修复方法有些绕弯子,甚至增加了参数。但我考虑到实装阅读模式(在我的blog,它是“启动魔眼”)后,postContent方法不止调用一次,替换后的内容应该保证对同一次生成的内容是一致的,不应该出现阅读模式下头图与常规模式下有差异的情况,且这样会需要更大的随机头图集,容易带来不可预料的结果,所以我额外编写了重置$used变量的逻辑,在每次调用parseContentPublic方法时:

    /**
     * 一些公用的解析,文章、评论、时光机公用的,与用户状态无关
     * @param $content
     * @return null|string|string[]
     */
    public static function parseContentPublic($content){
        $options = mget();
        //...
        //中略
        //...
        //调用其他文章页面的摘要
        if ( strpos( $content, '[post')!== false) {//提高效率,避免每篇文章都要解析
            Content::whenSwitchHeaderImgSrc(0,0,0,0,0,2);//* 这里
            $pattern = self::get_shortcode_regex(array('post'));
            $content = preg_replace_callback("/$pattern/",array('Content','quoteOtherPostCallback'), $content);
        }
        //...
        //后略
        //...

首先进行复位,在几乎不影响效率的前提下保证内容的稳定性。

同时,调用时加上最后一个参数:

    /**
     * 一篇文章中引用另一篇文章正则替换回调函数
     * @param $matches
     * @return bool|string
     */
    public static function quoteOtherPostCallback($matches){
        $options = mget();
        // 不解析类似 [[post]] 双重括号的代码
        if ( $matches[1] == '[' && $matches[6] == ']' ) {
            return substr($matches[0], 1, -1);
        }

        //对$matches[3]的url如果被解析器解析成链接,这些需要反解析回来
        $matches[3] = preg_replace("/<a href=.*?>(.*?)<\/a>/",'$1',$matches[3]);
        $attr = htmlspecialchars_decode($matches[3]);//还原转义前的参数列表
        $attrs = self::shortcode_parse_atts($attr);//获取短代码的参数

        //这里需要对id做一个判断,避免空值出现错误
        $cid = @$attrs["cid"];
        $url = @$attrs['url'];
        $cover = @$attrs['cover'];//封面
        $targetTitle = "";//标题
        $targetUrl = "";//链接
        $targetSummary ="";//简介文字
        $targetImgSrc = "";//封面图片地址
        if (!empty($cid)){
            $db = Typecho_Db::get();
            $prefix = $db->getPrefix();
            $posts = $db->fetchAll($db
                ->select()->from($prefix.'contents')
                ->orWhere('cid = ?',$cid)
                ->where('type = ? AND status = ? AND password IS NULL', 'post', 'publish'));
            //这里需要对id正确性进行一个判断,避免查找文章失败
            if (count($posts) == 0){
                $targetTitle = "文章不存在,或文章是加密、私密文章";
            }else{
                $result = Typecho_Widget::widget('Widget_Abstract_Contents')->push($posts[0]);
                if ($cover == ""){
                    $thumbArray =  $db->fetchAll($db
                        ->select()->from($prefix.'fields')
                        ->orWhere('cid = ?',$cid)
                        ->where('name = ? ', 'thumb'));
                    $targetImgSrc = Content:: whenSwitchHeaderImgSrc(0,2,null,$result['text'],@$thumbArray[0]['str_value'],1);  // * 这里! 小小的“,1”作用极大
                }else{
                    $targetImgSrc = $cover;
                }
                $targetSummary = Content::excerpt(Markdown::convert($result['text']),60);
                $targetTitle = $result['title'];
                $targetUrl = $result['permalink'];
            }
        }else if (empty($cid) && $url !=""){
            $targetUrl = $url;
            $targetSummary = @$attrs['intro'];
            $targetTitle = @$attrs['title'];
            $targetImgSrc = $cover;
        }else{
            $targetTitle = "文章不存在,请检查文章CID";
        }

        $imageHtml = "";
        $noImageCss = "";
        if (trim($targetImgSrc) != ""){
            $imageHtml = '<div class="inner-image bg" style="background-image: url('.$targetImgSrc.');background-size: cover;"></div>
';
        }else{
            $noImageCss = 'style="margin-left: 10px;"';
        }

        return <<<EOF
<div class="preview">
   <div class="post-inser post">
    <a href="{$targetUrl}" target="_blank" class="post_inser_a ">
    {$imageHtml}
    <div class="inner-content" $noImageCss>
     <p class="inser-title">{$targetTitle}</p>
     <div class="inster-summary">
      {$targetSummary}
     </div>
    </div>
    </a>
    <!-- .inner-content #####-->
   </div>
   <!-- .post-inser ####-->
  </div>
EOF;

    }

至此,修复工作全部完成。

最终效果

修复完成

最后修改:2019 年 08 月 01 日 10 : 13 AM
欢迎投食喵 ~

发表评论

1 条评论

  1. 无限

    众所周知bug越修越多,上述写法可以保证评论中引用的随机图片一定重复。