WordPressでpingbackを検証できないサイトがある

最近うちのブログにpingbackを飛ばしてきたブログがあってそのブログのpingbackがエラーコード0、エラーメッセージnullで失敗とログに記録されていました。
なんだろうー?とXMLRPCサーバーの処理を追ってみたところ、HEADエレメント内のHTML文法エラーとJavaScriptの問題でした。

HTML文法エラーはそのまんまで <meta property="og:description" content=""説明""/> など普通の文法エラーによる解析失敗。
そしてJavaScriptはpackerで難読化した物がHEAD内にあると動作不良を起こしていました。

pingbackの主な処理はwp-includes/class-wp-xmlrpc-server.phpのfunction pingback_pingで行われます。

<?php
 
public function pingback_ping( $args ) {
    ...省略...
 
    $request = wp_safe_remote_get( $pagelinkedfrom, $http_api_args );
    $remote_source = $remote_source_original = wp_remote_retrieve_body( $request );
 
    if ( ! $remote_source ) {
        return $this->pingback_error( 16, __( 'The source URL does not exist.' ) );
    }
 
    /**
     * Filters the pingback remote source.
     *
     * @since 2.5.0
     *
     * @param string $remote_source Response source for the page linked from.
     * @param string $pagelinkedto  URL of the page linked to.
     */
    $remote_source = apply_filters( 'pre_remote_source', $remote_source, $pagelinkedto );
 
    // Work around bug in strip_tags():
    $remote_source = str_replace( '<!DOC', '<DOC', $remote_source );
    $remote_source = preg_replace( '/[\r\n\t ]+/', ' ', $remote_source ); // normalize spaces
    $remote_source = preg_replace( "/<\/*(h1|h2|h3|h4|h5|h6|p|th|td|li|dt|dd|pre|caption|input|textarea|button|body)[^>]*>/", "\n\n", $remote_source );
 
    preg_match( '|<title>([^<]*?)</title>|is', $remote_source, $matchtitle );
    $title = isset( $matchtitle[1] ) ? $matchtitle[1] : '';
    if ( empty( $title ) ) {
        return $this->pingback_error( 32, __( 'We cannot find a title on that page.' ) );
    }
 
    $remote_source = strip_tags( $remote_source, '<a>' ); // just keep the tag we need
 
    $preg_target = preg_quote($pagelinkedto, '|');
 
    foreach ( $p as $para ) {
        if ( strpos($para, $pagelinkedto) !== false ) { // it exists, but is it a link?
            preg_match("|<a[^>]+?".$preg_target."[^>]*>([^>]+?)</a>|", $para, $context);
 
            // If the URL isn't in a link context, keep looking
            if ( empty($context) )
                continue;
 
            // We're going to use this fake tag to mark the context in a bit
            // the marker is needed in case the link text appears more than once in the paragraph
            $excerpt = preg_replace('|\</?wpcontext\>|', '', $para);
 
            // prevent really long link text
            if ( strlen($context[1]) > 100 )
                $context[1] = substr($context[1], 0, 100) . '&#8230;';
 
            $marker = '<wpcontext>'.$context[1].'</wpcontext>';    // set up our marker
            $excerpt= str_replace($context[0], $marker, $excerpt); // swap out the link for our marker
            $excerpt = strip_tags($excerpt, '<wpcontext>');        // strip all tags but our context marker
            $excerpt = trim($excerpt);
            $preg_marker = preg_quote($marker, '|');
            $excerpt = preg_replace("|.*?\s(.{0,100}$preg_marker.{0,100})\s.*|s", '$1', $excerpt);
            $excerpt = strip_tags($excerpt); // YES, again, to remove the marker wrapper
            break;
        }
    }
 
    ...省略...
}
 
?>

ここのwp_safe_remote_getでpingback送信元へGETリクエストを送信しHTTPヘッダーとHTMLをダウンロードします。
そして次のwp_remote_retrieve_bodyでHTMLのみのデータを変数へセット。

その後はスペース、改行、エレメントを整理しpreg_match( '|<title>([^<]*?)</title>|is'....で記事タイトルをゲット。
strip_tagsでアンカーを消してexplode( "\n\n", $remote_source );で改行毎に配列に格納します。

後はforeach ($p as $para)で配列の中からpingback送信先URLが含まれる配列を探して、配列内にあるpingback送信先URLの前後の文章を抜粋して終了の流れ。

抜粋処理周りにはフィルターが一切ないので処理の変更ができません。
packerの難読化を使っているどうしようもないサイトからのpingbackへどうしても対応したい・・って事なら処理が開始される前に通るフィルターpre_remote_sourceがあるので、これでscriptタグを全て削除すると良いかもしれません。
 
私的には・・・
packerの難読化のような何にもならない物を使っているサイトは放置しても良いと考えます。
と言うのも、packerには悪いと思いますが、後ろめたいコードを書いている多くのサイトに使われている物ですし、元のコードへ戻すのが非常に簡単で使う意味がほぼ無な物なので。

送信側の方はpingbackの送信に失敗するな?と思ったら自分のブログのコードが正常か確認を。

Bingクローラーは学習しない

うちのブログは今年9月に完全SSL化しましたが、SNI未対応クライアントを配慮してSSLバーチャルホストのデフォルトをblog.wolfs.jpに設定していました。
その影響でSSLのwww.wolfs.jp、www.blog.wolfs.jpのアクセスも許可していました。

上記サブドメインを許可していた期間は僅か1ヶ月ほどでしたが一部の検索結果に上記サブドメインも載るようになったようで、www.wolfs.jp、www.blog.wolfs.jpへのアクセスがかなり増えましたw
これはダメだな・・と言うことでSNI未対応環境を捨ててblog.wolfs.jpドメインからのみアクセスを許可するよう設定し、検索結果から消すために301 Moved Permanentlyでリダイレクトするように設定して更に1ヶ月が経過。

Googleは9割blog.wolfs.jpへアクセスするようになったけれど、Bingだけは2ヶ月前と全く変わりません。
ログを見るとリダイレクト先へアクセスしていない状況。
但し、robots.txtのリダイレクトだけはちゃんとリダイレクト先にアクセスするよう。

Bingの挙動はかわってるなーと思っていたけど301を理解しないバカクローラーだとは思いませんでしたw
と言うことで、Bingは301を理解しないのでドメインを変更するときは注意が必要です。

ちなみに、リダイレクトをしているのはユーザーエージェントがGoogleとBingだけです。
リダイレクトのコードはこんな感じ。

<?php
 
if ($_SERVER['HTTP_HOST'] !== 'blog.wolfs.jp') {
    $agent = strtolower($_SERVER['HTTP_USER_AGENT']);
    if (strstr($agent, 'googlebot') !== false || strstr($agent, 'bingbot') !== false || strstr($agent, 'msnbot') !== false) {
        $redirect_host = 'blog.wolfs.jp';
        if (strstr($_SERVER['HTTP_HOST'], 'wolfs.jp') !== false) {
            $redirect_host = 'blog.wolfs.jp';
        } else if (strstr($_SERVER['HTTP_HOST'], 'xn--n6x.jp') !== false) {
            $redirect_host = 'xn--n6x.jp';
        } else {
            http_response_code(400);
            exit;
        }
 
        http_response_code(301);
        header('Location: https://'.$redirect_host.$_SERVER['REQUEST_URI'], true, 301);
        exit;
    }
 
    http_response_code(403);
    exit;
}
 
?>

これをwp-config.phpの一番上とかに書いておけば勝手にやってくれます。

WordPress 4.6で追加されたdns-prefetchを無効化する

WordPress 4.6からヘッダーに<link rel="dns-prefetch" href="//s.w.org/">が追加されるようになりました。
見つけた瞬間サイトがクラックされたのか?!とヒヤっとしたけれど、s.w.orgはWordPressのショートドメインて事がわかって一安心しましたw

うちはWordPressの絵文字とかを使っていないのでs.w.orgのDNSプリフェッチは必要なし。
なのでこれを無効化してみました。

作成したPHPコードはこれ。

<?php
 
add_filter('wp_resource_hints', function ($urls, $relation_type) {
    if (is_admin() === false) {
        if ($relation_type === 'dns-prefetch') {
            return array();
        }
    }
 
    return $urls;
}, 10, 2);
 
?>

コードは簡単でwp_resource_hintsにフィルターをかけて、管理ページ以外かつタイプがdns-prefetchの場合は定義されている内容を空っぽにして返すって感じ。
管理ページの場合は、なにかあるとダメなので引数をそのまま返すようにしてあります。

全く使わないならremove_action('wp_head', 'wp_resource_hints');の方がはやいんじゃない?って思ったんだけど、なぜかこれが効かなかったのでadd_filterで対処する荒療治に。

ちなみに、この記事を書いた時の$relation_typeの種類はdns-prefetch、preconnect、prefetch、prerenderとありました。
詳しい動作を追いたい場合はwp-includes/general-template.phpの2800行付近にfunction wp_resource_hints()があるのでそこから見てください。

WordPressが生成したサムネイル画像からexif情報を削除する

WordPressで画像をアップロードするとサムネイル画像が生成されますよねー。
そのサムネイル生成方法はサーバー設定でImagickが有効化されているとImagickがGDが有効な場合はGDが使用されています。
うちの環境はImagickを実行するとApacheを巻き込んでクラッシュするので仕方なくというかPHPではメジャーで軽いGDほぼ一択。

しかしGDでJPEGを生成するとexifコメントに「CREATOR: gd-jpeg v1.0 (using IJG JPEG v90), quality = ...」って情報が追加されてしまいます。
たかが数バイトのデータですが、Yslow等のWebサイトの計測ページでは警告されマイナスポイントになりますし、要らないデータなのでない方がいいかなー。

なので、アップロードした画像と作成されるサムネイルのexif情報を削除するプラグインを作ってみました。
サムネイルのexif削除はGDを使用しているサーバー向けで、Imagickを使ってる場合はサムネイルにはexif情報は記録されません。

exifを削除するのに使用する物はImageMagickのスタンドアローンバージョン又はIMagick。
スタンドアローンを使う場合は、ImageMagickがインストールされている環境でないとダメ。

まずは完成PHPソース。

<?php
/*
Plugin Name: Remove EXIF
Plugin URI: http://blog.wolfs.jp/20160707-3638/
Description: アップロードした画像とサムネイル画像のexif情報を削除します。
Version: 1.0.0
Author: Kerberos
Author URI: http://blog.wolfs.jp/
*/
 
class removeExif {
    private $imagemagick_cli = true;
    private $imagemagick_path = '/usr/imagemagick/convert';
 
    public function __construct() {
        add_filter('wp_handle_upload', array($this, 'wp_handle_upload'));
        add_filter('wp_generate_attachment_metadata', array($this, 'wp_generate_attachment_metadata'), 10, 2);
    }
 
    public function wp_handle_upload($arg) {
        if ($this->check_mime($arg['type']) === true) {
            $this->remove_exif($arg['file']);
        }
 
        return $arg;
    }
 
    public function wp_generate_attachment_metadata($metadata, $attachment_id) {
        $dirArr = explode('/', $metadata['file']);
        $baseDir = wp_upload_dir(null, false);
        $uploadDir = $baseDir['basedir'].'/'.$dirArr[0].'/'.$dirArr[1].'/';
 
        foreach ($metadata['sizes'] as $entry) {
            if ($this->check_mime($entry['mime-type']) === true) {
                $this->remove_exif($uploadDir.$entry['file']);
            }
        }
 
        return $metadata;
    }
 
    private function check_mime($mime = null) {
        return ($mime === 'image/jpeg' || $mime === 'image/jpg');
    }
 
    private function remove_exif($filePath) {
        $filePath = addslashes($filePath);
 
        if (file_exists($filePath) === true) {
            if ($this->imagemagick_cli === true) {
                if (is_executable($this->imagemagick_path) === true) {
                    try {
                        exec('"'.$this->imagemagick_path.'" "'.$filePath.'" -strip "'.$filePath.'"');
                    } catch (Exception $e) {}
                }
            } else {
                if (class_exists('Imagick') === true) {
                    $im = new Imagick($filePath);
 
                    try {
                        $im->stripImage();
                        $im->writeImage($filePath);
                        $im->clear();
                        $im->destroy();
                    } catch (Exception $e) {}
                }
            }
        }
    }
}
 
new removeExif();
?>

かなり簡単なコード。(`・ω・)b

各ファンクションの簡単な説明は・・・
続きを読む

スタイルシートをLESSにかえてみた。

サーバー側のCSS/JavaScriptをまとめて最適化+難読化するシステムを作り直して、The Dynamic Stylesheet language (LESS)に対応させました。
LESSのコンパイルにはlessphpライブラリを使用。

LESSってナニー?って感じですが、LESSは主に開発者側がCSSを使いやすく、メンテナンスが容易にするためのCSSプリプロセッサなんです。
CSSでネックだった変数、関数、演算が使えるので、開発者からすればCSSの視認性の上昇やコードの短縮ができます。

なのでWebを閲覧するユーザー側からしたら特にメリットはなく、クライアント側コンパイラを使う場合はページ表示速度低下があるのでデメリットしかありません。
しかし、コンパイルをサーバー側でやっておくとユーザー側のストレスはCSSと全く同じ。

これはメンテナンスをし易くするために使ってみねば!という事で・・・
とりあえず、よく使うアンカーエレメントの訪問済みリンクやマウスオーバーの色を指定するやつはこうなります。

CSS

a:link {
    color: #0d85cc;
    text-decoration: none;
}
 
a:visited {
    color: #0d85cc;
    text-decoration: none;;
}
 
a:active, a:hover {
    color: #12a7ff;
}
LESS

a {
    &:link {
        color: #0d85cc;
        text-decoration: none;
    }
 
    &:visited {
        color: #0d85cc;
        text-decoration: none;
    }
 
    &:active, &:hover {
        color: #12a7ff;
    }
}

 
少しややこしいCSSのサンプル。
このブログのフォームにも使っているCSSで、inputやtextareaのマウスオーバーやフォーカスを当てた時に縁が変化するやつはこうなる。
CSS

input, textarea, select {
    color: #666;
    background: #fff;
    font-size: 12px;
    line-height: 18px;
}
 
input:not([readonly]):not([disabled]):hover, 
textarea:not([readonly]):not([disabled]):hover, 
select:not([readonly]):not([disabled]):hover {
    background: #f6f6f6;
}
 
input:not([readonly]):not([disabled]):focus, 
textarea:not([readonly]):not([disabled]):focus, 
select:not([readonly]):not([disabled]):focus {
    transition-duration: 0.3s;
 
    background: #fff;
    border-color: #75b9f0;
    outline: 0px none;
    box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.075) inset, 0px 0px 8px #75b9f0;
}

LESS

input, textarea, select {
    color: #666;
    background: #fff;
    font-size: 12px;
    line-height: 18px;
 
    &:not([readonly]):not([disabled]):hover {
        background: #f6f6f6;
    }
 
    &:not([readonly]):not([disabled]):focus {
        transition-duration: 0.3s;
 
        background: #fff;
        border-color: #75b9f0;
        outline: 0px none;
        box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.075) inset, 0px 0px 8px #75b9f0;
    }
}

ロケーション疑似クラスを使ってたり複数指定しているCSSは格段に見やすく、そしてシンプルになりますねー!
 
うちのブログのCSSもLESSにかえてみたら、CSSで62.5Kb、LESSで37.7Kbのファイルサイズに。
LESSをコンパイルすれば同じぐらいのサイズになったので、書き方の違いがあるだけでCSSとLESSをコンパイルした物はかわらなかった。
CSSとLESS
画像はCSSをLESSに作り直しをしているところだけれど、視認性が高くファイルサイズが小さくなってるのがよくわかりますねw

本当にこれは使いやすくて良い感じ(`・ω・)b