WordPress プラグインやテーマの翻訳でうまく手を抜く(いい意味で)

WordCamp Kansai 2014 に行ってきました。2日め“コントリビューターデイ”、プラグイン・テーマ翻訳の世話役だったのですが、可搬 PC は持っていないし、子連れだし、遠方まで帰るために途中で抜けなければならないし、さらに、「Mac/Win で Poedit というアプリケーションを使う」というのが最も一般的と思われるのに自分では使っていないため ((ふだんは Debian 上の Emacs の po-mode で作業しています。))に操作方法を即答できない……、という幾重もの役立たずぶりですみませんでした。こどもの相手やら何やら、こちらのほうがお世話になりっぱなしで、ありがとうございました。

そういう中で質問を受けたことを、今さらながらここに書いておこうと思います。

翻訳作業のステップ

WordPress のプラグインやテーマの翻訳をやってみようという記事はあちこちにたくさんあります。ここでは細かく書きませんので、それらを参照してみてください。「POT ファイルとは何か」「PO ファイルとは何か」「それらはどこにあるか」などは既に知っているという前提で進めます。

おさらいです。WordPress のプラグインやテーマの翻訳作業のステップは

  1. プラグインやテーマのプログラムの中の翻訳されるべきメッセージに __() や _e() などのマークを付ける
  2. マークされた文字列を抽出して POT ファイルと呼ばれる、翻訳者にとって原本となるものを作成する
  3. POT ファイルを元に、メッセージをそれぞれの言語に翻訳した PO ファイル を準備する
  4. PO ファイルを編集する (これがほんとうの翻訳作業)

です。作業のスタート地点が後ろに近いほど、とっかかりやすいです ((このあと、それを開発者に送るとか、開発者の側では送られてきたものを最新版に合わせてからパッケージに同梱するとかの作業もありますが、ここでは省略します。それらとステップ2については以前「プラグインやテーマの国際化を少し楽に」に書きました。))。

最も始めやすい状態: ステップ4から

たくさんある記事のひとつ、Nao さんの「2014年版: WordPress プラグイン・テーマの翻訳を始めてみよう」を見てみます。

その記事にありますように、最も手軽に始められるのが、既に全部または一部翻訳がなされているものに対して、誤訳を修正するとか、別の訳語にするとか、訳の抜けている部分を補う、というものです。これなら最初のハードルがぐっと低く、ともかく「やってみよう」という気になれます。

Poedit なら、書き換えたい PO ファイルを開き、当該箇所を修正・加筆するだけです。

その次に楽な状態: ステップ3から

プラグインやテーマそのものは翻訳可能 (POT ファイルが用意されている) だが日本語訳 (name-ja.po) がない状態というのが、その次にとっつきやすい状態です。

公式ディレクトリに登録されているプラグインテーマなら「translation-ready」というタグが付けられているでしょう。ここでいうステップ1や2の処理が既にされているという意味です。

Poedit なら、「ファイル」-「POT ファイルを元に新しいカタログを作成…」で、「言語」に ja を指定して適切な名前 (name-ja.po の形) で保存します。

コマンドラインを使えるなら Poedit を使わずに、

msginit -i name.pot -o name-ja.po -l ja 

です。意味はなんとなくわかりますね。

この時点で、翻訳率0% の日本語翻訳ファイル name-ja.po ができます。

ステップ3をステップ4に

「0%」……嫌な響きですね。これだけでやる気が削がれてしまいます。なので、過去の資産から使えるものは少しでも流用します。

たとえばテーマの翻訳をしようとしているとします。その最初に、既存の、たとえばデフォルトテーマの twentyfourteen の翻訳を取り込んでしまおうというのです。

もしステップ3をコマンドラインでできたようなら、これまたコマンドラインで

msgmerge -o name-ja.po -C wordpress/wp-content/languages/themes/twentyfourteen-ja.po name-ja.po name.pot

のように、msgmerge を使います。-C で参考とする PO ファイルを指定します。-C はひとつのコマンドラインの中に何度でも指定できるので、WordPress 本体の ja.po や admin-ja.po も追加するといいかもしれません。

Poedit ではこれに相当する操作が簡単にできないようです。そこでステップ3の代わりに、次のようにします。name-ja.po がまだ存在しない状態で、参考とする PO ファイル (たとえば twentyfourteen-ja.po) を name-ja.po という名前でコピーします。これを Poedit で開き、「カタログ」-「POTファイルでカタログを更新…」します。

すると、英文が合致する分の翻訳がそのまま取り込まれます ((合致しなかった部分を掃除するには Poedit では「カタログ」-「Purge Deleted Translations」です。))。プラグインでは参考にする似たものを見つけてくることが難しいかもしれませんが、テーマの場合は、デフォルトのテーマを参考にしてうまくいけば80%ほども翻訳済みにしてしまうことができます。

これで、スタート地点をステップ3からではなく、ステップ4からにすることができます。

翻訳メモリ

Poedit には翻訳メモリという機能があります。データベースに登録しておけば、それを自動的に参照して、英文が合致する分を埋めてくれます。まずは WordPress 本体の ja.mo や admin-ja.mo それにデフォルトのテーマの twentyfourteen-ja.mo を登録しておきましょう。

これによってもスタート地点をステップ3からではなく、ステップ4からにすることができます。

前節の方法と翻訳メモリの方法は同時に使うこともできますので、積極的に翻訳メモリを使うのがいいでしょう。

といっても私自身が Poedit 自体(ということは翻訳メモリも)使っていませんので詳しく書けません。かなり古い記事ですが Tai さんの「poEditの翻訳メモリ機能を使う」や Miyoshi さんの「poEdit で翻訳ファイルを作る 」の「翻訳メモリを活用する」の節などを参考にしてください。

手抜きの効果

「手抜き」というと負のイメージの言葉ですが、ここではまったくそういうことはありません。むしろ用語の統一という点でも、積極的にここに書いた方法をとるべきだと思います。

そこで、最初掲げたステップを修正して、

  1. プラグインやテーマのプログラムの中の翻訳されるべきメッセージに __() や _e() などのマークを付けておく
  2. マークされた文字列を抽出して POT ファイルと呼ばれる、翻訳者にとって原本となるものを作成
  3. POT ファイルを元に、メッセージをそれぞれの言語に翻訳した PO ファイル を作る
    1. 過去の資産を参照して自動的に、できるだけ翻訳を埋める
  4. PO ファイルを編集する (これがほんとうの翻訳作業)

の赤い字の項目を必ず実行するように意識することを強くお奨めします。

ざっと検索してみた Poedit の使い方、WordPress の翻訳や WordPress 翻訳祭りのような WordBench などでの解説でも「POT ファイルから PO ファイルを作りましょう。空っぽの ja.po ができましたね。さあ翻訳を始めましょう」と、このあいだのステップが飛ばされているように思います。

いまからやろうとしている翻訳は、単語レベルでは既に誰かがやっているかもしれません。WordCamp Kansai 2014 のキーワードは「Share 分かちあい」でしたね。share できるものは share して、抜ける手はなるべく抜いて、その余力は別のところで活かしましょう。

これくらいの下調べは前もってやっておくべきでした。すみません。今後の各地のイベントや、あるいは個人で、翻訳やってみようかなというときには、前に書いた「WordPress 翻訳祭り—今さら注意してもらえない「日本語」について 」と併せて参考にしてもらえればと思います。

Mew のサマリに表示される本文の部分が変わった

Web のフォームによって送られる次のような書式のメールをしょっちゅう受け取ります。冒頭から

[氏名] ...
[よみかな] ...
[郵便番号] ...
[住所] ...
     ... 

のように、 [ ] ではじまる行があります(「氏名」や「住所」というのはあくまで例です)。

メールリーダーには Mew を使っているのですが、そのサマリ表示に上記の部分がばっさりカットされて、どうでもいいところからの本文の一部が現れるようになりました。

さて検索してみると、“[” ではじまる行を引用部分とみなし無視する、という変更が行われたようです。自分の手元でこのような変化がつい最近起こったのは Debian のパッケージの更新の時期によるものでしょう。

この部分(mew-scan.el の mew-regex-ignore-scan-body-list 定義部分)を元に戻しながら、~/.mew.el に setq

(setq mew-regex-ignore-scan-body-list
  '("^[ \t]*$"
    "^[ \t]*[-a-zA-Z0-9]+: "
;;    "^[ \t]*[[>:|#;/_}]" ;; https://github.com/kazu-yamamoto/Mew/commit/98d4fd7be3216792824e2c737006921b3b49b4b0 で加えられた変更
    "^[ \t]*[>:|#;/_}]" ;; を戻す。すなわち、"[" で始まる行を引用とはみなさない
    "^[ \t]*\\w+\\(['._-]+\\w+\\)*>"
    "^[ \t]*[[</(.-]+ *\\(snip\\|\\.\\.\\)"
    "^   "
    "^--"
    "^- --"
    "^=2D"
    "^.\\{1,100\\}\\(:\\|;\\|/\\)[ \t]*$"
    "^.\\{1,100\\}\\(wrote\\|writes?\\|said\\|says?\\)[^.!\n]?[ \t]*$"
    "^[ \t]*\\(On\\|At\\) .*[^.! \t\n][ \t]*$"
    "^[ \t]*In \\(message\\|article\\|mail\\|news\\|<\\|\"\\|\\[\\|(\\)"))

と書くことで、以前の状態にすることができました。

pandoc の使い方メモ — 相互参照について

やりたいことは「元の文章を markdown で複数のファイルに分割して書き、それをウェブで公開するために個々に HTML に変換したい。もう一方では印刷できるような一括した PDF を LaTeX 経由で作りたい。ついでに EPUB も作りたい」です。割とありがちなケースだと思うので、もっとすっきりした方法があるように思うのですが、うまく見つけられませんでした。

相互参照とは、HTML では id 属性と a タグで実現されるもの、LaTeX では \label{}\ref{} で表現されるものです。

索引は、LaTeX では後で処理するために \index{} で印を付けておくものです。HTML と EPUB では使いません。

例を挙げます。

a.md

はじめに {#hajimeni}
========

...

用意するもの
============

...

道具 {#dougu}
----

* 包丁
* まな板
* ...

これらは[「下ごしらえ」](b.html#shita)で使います。

* 大きな鍋\index{おおきななべ@大きな鍋}
* ...

これらは[「調理」](b.html#chouri)で使います。

材料 {#zairyo}
----

...

b.md

手順
====

下ごしらえ {#shita}
----------

ここで使う材料は[「材料」](a.html#zairyo)にまとめてあります。
...

調理 {#chouri}
----

...
大きな鍋\index{おおきななべ@大きな鍋}([「道具」](a.html#dougu)を参照のこと)を使います。
...

盛り付け {#moritsuke}
--------

おわりに
========

...


という文書が元になります。

HTML

a.md → a.html, b.md → b.html のように個々のページを独立して生成することにします。それを見越して、md ではリンクの書式のところにこれらのファイル名を入れておきます。

md を pandoc

pandoc -o a.html a.md
pandoc -o b.html b.md

と処理すると

a.html
<h1 id="hajimeni">はじめに</h1>
<p>...</p>
<h1 id="用意するもの">用意するもの</h1>
<p>...</p>
<h2 id="dougu">道具</h2>
<ul>
<li>包丁</li>
<li>まな板</li>
<li>...</li>
</ul>
<p>これらは<a href="b.html#shita">「下ごしらえ」</a>で使います。</p>
<ul>
<li>大きな鍋</li>
<li>...</li>
</ul>
<p>これらは<a href="b.html#chouri">「調理」</a>で使います。</p>
<h2 id="zairyo">材料</h2>
<p>...</p>
b.html
<h1 id="手順">手順</h1>
<h2 id="shita">下ごしらえ</h2>
<p>ここで使う材料は<a href="a.html#zairyo">「材料」</a>にまとめてあります。 ...</p>
<h2 id="chouri">調理</h2>
<p>... 大きな鍋(<a href="a.html#dougu">「道具」</a>を参照のこと)を使います。 ...</p>
<h2 id="moritsuke">盛り付け</h2>
<h1 id="おわりに">おわりに</h1>
<p>...</p>

ができます。LaTeX の索引のために挿入しておいた \index{} は無視されるので、特に気にすることはありません。

LaTeX 経由 PDF

ソースは複数ファイルに分割されていても最終的な成果物はひとつであってほしいため、main.tex を用意しておき、

main.tex
\documentclass[a5paper]{jarticle}
\usepackage[dvipdfmx]{hyperref}
\usepackage{makeidx}
\makeindex
\begin{document}
\tableofcontents
%
\input{a}
\input{b}
%
\printindex
\end{document}

これから dvi を作り、さらにそれから pdf を作ることにします(pandoc から PDF を出力させることもできるようですが、日本語のとおる環境を設定したり次に述べる処理を挟んだりするのがやりにくいので、こういうやり方にします)。

さて、ここで読み込まれる a.tex, b.tex を md から pandoc で生成するのですが、単純にやるだけでは、HTML 向けに書いていたリンクのところが

これらは\href{b.html\#shita}{「下ごしらえ」}で使います。

のようになってしまいます。これではよろしくないので、

sed 's/\(\[[^\]*\]\)[\(][^#]*\(#[^\)]*\)[\)]/\1(\2)/g' a.md \
| pandoc -t latex \
| sed 's/\(\\hyperref\[\([^\]*\)\]\)[\{][^\}]*[\}]/\1\{\\ref\{\2\}\}/g' \
> a.tex

のようにします。バックスラッシュと括弧だらけでわかりにくいですが、前段の sed

[「下ごしらえ」](b.html#shita)

[「下ごしらえ」](#shita)

のように、丸カッコ内のファイル名相当の部分を削除します。これで生成物の当該部分は

\hyperref[shita]{「下ごしらえ」}

となるので、後段の sed でこれを

\hyperref[shita]{\ref{shita}}

のように書き換えます。

これにより、生成物は

a.tex
\section{はじめに}\label{hajimeni}

\ldots{}

\section{用意するもの}\label{ux7528ux610fux3059ux308bux3082ux306e}

\ldots{}

\subsection{道具}\label{dougu}

\begin{itemize}
\itemsep1pt\parskip0pt\parsep0pt
\item
  包丁
\item
  まな板
\item
  \ldots{}
\end{itemize}

これらは\hyperref[shita]{\ref{shita}}で使います。

\begin{itemize}
\itemsep1pt\parskip0pt\parsep0pt
\item
  大きな鍋\index{おおきななべ@大きな鍋}
\item
  \ldots{}
\end{itemize}

これらは\hyperref[chouri]{\ref{chouri}}で使います。

\subsection{材料}\label{zairyo}

\ldots{}
b.tex
\section{手順}\label{ux624bux9806}

\subsection{下ごしらえ}\label{shita}

ここで使う材料は\hyperref[zairyo]{\ref{zairyo}}にまとめてあります。 \ldots{}

\subsection{調理}\label{chouri}

\ldots{}
大きな鍋\index{おおきななべ@大きな鍋}(\hyperref[dougu]{\ref{dougu}}を参照のこと)を使います。
\ldots{}

\subsection{盛り付け}\label{moritsuke}

\section{おわりに}\label{ux304aux308fux308aux306b}

\ldots{}

となります。

これで相互参照が正しくなった中間ソース a.tex, b.tex ができましたので、main.tex を

platex main
platex main
mendex main
platex main
dvipdfmx main

と処理して、最終的に main.pdf を得ることができます。

EPUB

pandoc は EPUB 形式を出力することもできます。

ソースは複数ファイルに分割されていても成果物はひとつにまとまっていたほうがいいので cat でつなぎます。また、HTML 向けに付けていたリンクの書き方を変更して(LaTeX の場合の前処理と同じです)、pandoc にかけます。

cat a.md b.md \
| sed 's/\(\[[^\]*\]\)[\(][^#]*\(#[^\)]*\)[\)]/\1(\2)/g' \
| pandoc --self-contained -o main.epub

これで最終成果物の main.epub ができます。LaTeX の索引のために挿入しておいた \index{} は無視されるので、特に気にすることはありません。

HTML や LaTeX、EPUB を出力する際には、もっと適切な pandoc のオプションを付けたり、テンプレートを用意したほうがいいのですが、ここでは相互参照に絞って説明するため、これらを割愛しました。

KVM に Kernel Samepage Merging (KSM)

いわゆる自宅サーバーの機器を更新し、これを機にソフトも KVM の上に載せてみることにしました。

用途と向きによって仮想サーバーを切り分けます。ホストも複数のゲストも、ぜんぶ Debian で同じバージョンです。ネットの検索によって得た知識でも何とかなります。

当初気づかずに、後から「こういうのがあるのか」と気づいたのが、KSM (Kernel Samepage Merging) でした(解説はたとえば Linux カーネル共有メモリーの徹底調査)。

KVM on Debian Squeeze – My Notes を参考に設定しました。

ホストとゲストの両方で /etc/rc.local を編集し、次を追加します。

echo 1 > /sys/kernel/mm/ksm/run
echo 300 > /sys/kernel/mm/ksm/sleep_millisecs

再起動後、/sys/kernel/mm/KSM/pages_sharing がゼロより大きな値になっていれば、有効になっています。

KSM は使わないほうがいい?

さて、はじめ気づかなかった KSM についての情報に行き当たったのは、とりあえずしばらく動かしていると、ゲストOSが徐々にメモリを食いつぶしていくという現象をどうにかしたいと思って調べてみたからです。 そこで上記のように KSM を有効にしてみたのですが、引き続き検索していると、「KVMで複数VMを起動してVM間の相互作用を減らしたいときに考えること 」という記事を見つけました。
  • メモリはオーバーコミットしない
    • メモリをオーバーコミットするとろくなことがありません。なので要るだけ積みあげます。安いし。
      • 特にゲストOSがlinuxの場合、メモリはあるだけ使おうとする。本来だとキャッシュとしての有効活用になるキャッシュを積極的に使う戦略が、メモリオーバーコミット環境下ではswapを多発させる原因になってしまう

うーむ。これが原因かもしれません。個々のゲストはさほど深刻な負荷があるわけではなし、一方が急に働くことになっても他方はそれほどでもないことがほとんどなので、個々のゲストのメモリ割り当てを、ほぼ実メモリと同じくらい、としていました。すなわち、ゲストに割り当てたメモリを合計すると実メモリの数倍になっていました。しかし、この記述によると

    • メモリは足りてるはずなのでKSMは停止する

確かに KSM はホストに負荷がかかるので、動かさずに済むならそのほうがいいかもしれません。

もうしばらく様子を見ながら(まだ理解できていないことも多いし)、考えたいと思います。

第何何曜日 再び

昨年末に「2013年に読まれた記事トップ10」を書く際にサイト統計を見たのですが、圏外すぐに「第何何曜日」がありました。古い記事ですが、それなりによく読まれています。

ところでいま調べなおしてみたら、PHP の strtotime() には便利な機能があるではありませんか。前の記事を書くときには見落としていたのか……。前の記事の脚注を見ると PHP 4 と書いているから、まだ使えなかったのかな。

というわけで、いま風に書き直してみます。

Y 年 m 月の第 i の w 曜日は、その月の d 日か

PHP ならこれはもう簡単で、そのまま書くだけです。

Y, m, i, w という記号では書きにくいので、具体的に「2013年12月の第3木曜日」を例にすると、

$d = date("d", strtotime("third Thursday of Dec 2013"));

です。「今月の第3木曜日」なら “third Thursday of this month” ですし、「次の第3木曜日」なら “third Thursday” だけです ((なお、この書式でも「第5週」の場合は注意が必要です。))。なんて簡単なんだ。

ここに日本語が使えると便利なのですが、さすがにそこまではできないようです。というのは、「おととい」を指定するのが、英語だと “day before yesterday” で、まどろっこしいからです。日本語だと今日の前後2日までは短い言葉があるのに ((その先も「さきおととい」とか「しあさって」あたりまでは、たまに耳にします。))、英語だと前後1日までで、その先は持って回ったような言い方になってしまいます。話はやや違いますが、PostgreSQL の SQL でも “today”, “yesterday”, “tomorrow” のキーワードは使えるのですが(ほかの DB はどうなんでしょう?)、「おととい」をしょっちゅう入力したい私はしかたなく日付を使っていて、どうにかしたいなあと思っているのでした。

さて、前の記事の際の動機はゴミの収集日でした。「第 2, 4 月曜日」だとすると、次のゴミの日は

$gomi = date("Y-m-d", min( strtotime("second Monday"), strtotime("fourth Monday") ) );

で済みます。

ちょうど当該日にこれを実行すると、その日になります。しかしゴミの収集時刻 14:00 を過ぎていたら次の収集日を出力してほしいですね。

$base = ( date("G") <= 14 ) ? strtotime("today") : strtotime("tomorrow");
$gomi = date("Y-m-d", min( strtotime( "second Monday", $base ), strtotime( "fourth Monday", $base ) ) );
}

ちょっときれいじゃない気がするけど、こんなのでいいのかなあ。

WordPress のプラグインを公式リポジトリと GitHub に公開する

昨年末に公開したプラグイン wp_mail to XMPP は、Git で管理しつつ作ってみました。ひととおりできたところで GitHub に公開。それから公式リポジトリを申請して、こちらは SVN なので、git-svn で送ろうとしてもなんだかうまくいかず、とりあえず手動で(ローカルの別の場所にコピーして、svn で。つまり Git とは完全に切り離して)作業しました。

それからゆっくり見なおしてみました。

WordPress公式プラグインリポジトリに登録しているプラグインをGitHubで管理する」や「公式リポジトリのWordPressプラグインをGitHubでバージョン管理できる『wp-plugin-in-github』使ってみたよ」を見て、これは便利そうだと思ったのですが、tai さんと同じく readme と本体のバージョンが合わないエラーとか、

  • 手元で、勉強がてら git flow を導入していたので、そのままでは構成が合わない
  • 翻訳ファイルの作業は自前の Makefile と重複する

など、そのままでは使えませんでした。そこでその中身を個々にやるような形ですが、次のようにやってみました。参考にしたページは

です。

手順

出発点は

  • 既に公式リポジトリに何らかがある状態
  • 公式リポジトリの branches は使っていない
  • 手元では git-svn がインストールされている状態

という中途半端なところなので、どれほど意味があるのかわかりませんが、自分のメモとして。そのため、例とするプラグインは wp_mail to XMPP です。

公式リポジトリと連携

まず、trac で見るなどして、はじめと終わりのリビジョン番号を調べておきます。

それと、内容が

mako09 = Mako N <mako@pasero.net>
plugin-master = Mako N <mako@pasero.net>

というファイル(名前は何でもいいですが、ここではauthor.txt とします)を作っておきます。

次に、ローカルの何もない場所で、

git svn clone --no-minimize-url -s -A author.txt -r829275:829448 http://plugins.svn.wordpress.org/wp-mail2xmpp/

を行います。-r(はじめ):(終わり)を付けます。この -r ではじめと終わりを指定すれば膨大な時間がかかることもありません。-r(はじめ) だけでは、はじめのリビジョンは申請によって向こうで(plugin-masterに)場所だけ作ってもらったものなので、空っぽのディレクトリだけになります。

手元で作業していた git リポジトリで、このコマンドを実行してもうまくいきませんでした。これ自体はうまくいっているように見えるのですが、後段の git svn rebase が “Unable to determine upstream SVN information from working tree history” と言うのです。何もない場所でやるとうまくいきます。

git の設定

git config user.name などの設定を行います。git flow を使うため、

git flow init

も行います。

ブランチの作成

git checkout -b svn

で、svn という名前のブランチを作ります。

svn → 公式、master → gitHub で、develop は手元、という構成です。svn と master の違いは、今回の例では公式には置かないファイル(Makefile, LINGUAS)だけです。

git svn fetch
git svn rebase

で、同期させておきます。

ローカルから取り込み

既に作業していたローカルのリポジトリから

git checkout master
git remote add temp file:///...
git pull temp master
git checkout develop
git pull temp develop
git remote remove temp

で、取り込みます。

GitHub との連携

git checkout master
git remote add origin git@github.com:mako09/wp-mail2xmpp.git

として、

git pull origin master

で、同期させておきます。GitHub には何もなくてこれからという場合は、むしろ git push することになります。

以上で、GitHub とも公式リポジトリとも連携した git リポジトリが手元に出来ました。これまで作業してきた手元の git リポジトリは不要になります。

その先に作業してきた git リポジトリに、git-svn で svn リポジトリを連携させることができればよかったのですが、上述したようにうまくいきませんでした。このへんの仕組みはよく理解できていません。そのため、このようなとてもまわりくどい手順になってしまいました。

以後の作業

以後は、手元の git リポジトリで作業します。master が完成したら(git checkout master の状態)、GitHub に送ります。以下、まだその場面ではないので十分試していませんが、

git push           # GitHub に送られる

続いて、ブランチ svn に移って、svn に送ります。

git checkout svn
git merge master   # --squash をつけるか、merge ではなく個々のファイルを checkout のほうがいいかも
git svn fetch      # 念のため
git svn rebase
git svn dcommit    #公式リポジトリに送られる

git のタグは(ブランチ master で)

git tag 0.9

のようにします。これを GitHub に送るには

git push origin 0.9

です。

svn にタグをつけるのは(ブランチ svn で)

git svn tag 0.9

のようにします。メッセージは自動的に “Create tag 0.9” のようになります。このコマンドで公式側にタブができます。

wp-login.php への攻撃に対処する

サーバーにログインして作業しようとしたら非常に反応が悪く、どうしたことかと見てみると(普段と違って、コマンドを入力してから応答があるまで数十秒も待たされるのでヒヤヒヤしましたが)、大量の apache プロセスが走っていて、ログには wp-login.php への大量のアクセスが記録されていました。最近よく耳にする wp-login.php へのブルートフォースアタックです。

1つの IP アドレスから5,6回のアクセスがあり、すぐに(あるいは同時に)別の IP アドレスからのアクセスがあります。結局、2時間ほどのあいだに数千回のアクセスがあり、その後沈静化しました。

特に対策らしい対策をしていなかったので、パスワードの強度のみで耐えたことになりますが、実際目の当たりにすると気持ちのいいものではありません。それにどちらかというとサーバーに対する高負荷のほうが心配です。

そこでいまさらではありますが、少し手を打つことにしました。

強いパスワードにする

今回行った対策ではありませんが、そこそこ強いパスワードにしていたので、攻撃に気づくまでの30分間ほどはこのことだけで侵入を防ぎました。

.htaccess で制限する

攻撃に気づいてすぐ、 .htaccess に次のように書いて wp-login.php へのアクセスを制限しました。これは効果てきめんで、すぐにサーバーの負荷は通常の範囲まで下がりました。

<Files wp-login.php>
Order deny,allow
Deny from all
Allow from xxx.xxx.xxx.xxx
</Files>

前述のように攻撃側の IP アドレスは絞り切れないので、Deny に列挙することができません。

さて当面はこれでしのいだとしても、正当に管理画面にアクセスするのが複数の人間で、それぞれの接続元の IP アドレスが固定していなかったりすると、Allow に列挙するのもやっかいです。

WordPress のロックダウン

IP アドレスでふるい分けられないとすれば、wp-login.php のところで何らかの制限をかける方法が考えられます。たとえば Force Email Login は、中心となる機能は「ログインをユーザー名ではなくメールアドレスで」という点なのですが、そのほかに

  • ログインに失敗すると、ログイン処理に対して10秒間 wp_die() を発火させて膨大な量のブルートフォースアタックによるサーバー負荷を軽減します。

ということも行います。

こちらの問題のサイトでは「ログインをユーザー名ではなくメールアドレスで」という機能は必要ないので(というか、ようやく「ユーザー名」が何かを理解するくらいの人たちにもう一度ログイン方法を周知徹底するのが面倒すぎるので)、その部分を削ぎ落とすことにしました。

ただ、これだと誰か(攻撃者)がログインに失敗するとその後10秒間は誰も(正当なユーザーも)ログインできません。たとえば5秒に1回、1週間にわたってログインを失敗し続けるという、もはや「ブルートフォース」とは呼べないようなぬるい攻撃(?)で、そのあいだ正当なユーザーのログインを妨害することができるという欠点があります。

やはり「IP アドレスごとに」という機能をつけざるを得ないのかもしれません。そこで、Limit Login Attempts in WordPress … のようなものにすることにしました。これだと、同一 IP アドレスから1秒間にn回のログインの失敗があったらその後m秒間、その IP アドレスからのアクセスを禁止します ((実際は、1回の失敗ですぐ制限をかけるようにするため、このページのコードを少し修正しました。))。ユーザー名/パスワードを試みる回数は、IP アドレスをチェックしないものよりは増えますが、何も対策しないよりはずいぶん減らせるでしょう。

さて、たとえば1秒間に2か所からそれぞれ2回、3回の攻撃があった場合の対応は、この対策を行わない場合、

  • wp-login.php にアクセス → データベースでユーザー名とパスワードを調べる → 失敗
  • wp-login.php にアクセス → データベースでユーザー名とパスワードを調べる → 失敗
  • wp-login.php にアクセス → データベースでユーザー名とパスワードを調べる → 失敗
  • wp-login.php にアクセス → データベースでユーザー名とパスワードを調べる → 失敗
  • wp-login.php にアクセス → データベースでユーザー名とパスワードを調べる → 失敗

となっていたわけですが、これが

  • wp-login.php にアクセス → データベースで IP アドレスごとの失敗回数を調べる → データベースでユーザー名とパスワードを調べる → 失敗
  • wp-login.php にアクセス → データベースで IP アドレスごとの失敗回数を調べる → 失敗(403)
  • wp-login.php にアクセス → データベースで IP アドレスごとの失敗回数を調べる → データベースでユーザー名とパスワードを調べる → 失敗
  • wp-login.php にアクセス → データベースで IP アドレスごとの失敗回数を調べる → 失敗(403)
  • wp-login.php にアクセス → データベースで IP アドレスごとの失敗回数を調べる → 失敗(403)

となります。いずれにせよデータベースにはアクセスするわけで、1回の試行の負荷はどれほど違い、全体での負荷はどれほど違ってくるのでしょうか。そこまでは調べていません。

apache のモジュールで制限する

データベースへのアクセス前に制限できれば負荷を小さくできるのは明らかなので、前述の .htaccess によるものに代わる、apache のモジュールはないか探してみました。

mod_bw は回数制限ではない、mod_dosdetector は特定ページ(wp-login.php)のみに限定できない、mod_dosblock は IP アドレスごとではない……と、どれも一長一短のようです。ModSecurity だと要求にかなっていそうですが、これだけのためには機能が豊富すぎてルールが難しく、まだ理解できていません。

mod_evasive というのは、IP アドレスごとのアクセス回数を制限できますが特定ページの指定はできないようで、 mod_dosdetector と同じです。しばらくのあいだ、これを試してみることにしました。


管理するいくつかのサイトで、実情に合わせて上述の対策のいずれかをとることにしました。