filter_var()関数の真価を見極める

もともとPECLにあって、PHPのバージョン5.2.0から標準に組み込まれたfilter拡張モジュール。URLのクエリ文字列やPOSTされた入力文字列の妥当性検査と「サニタイズ」という2つの役割を果たすもので、また突っ込みどころがいろいろあるんだけど、まあ利用法によってはそれなりに便利なんだと思う。で、個人的には使わないな、とスルーしていたんだけど、その拡張モジュールの提供するfilter_var()という関数がメールアドレスの妥当性検査にも利用できるという話を今回の騒動で知ったので、ちょっと中身を見てみることにした。

ちなみにfilter_var()自体は、指定されたフィルタの種類が妥当性検査フィルタだった場合には、妥当な場合に第1引数に与えられた値そのものを、妥当でない場合にfalseを返すことになっている。

<?php
var_dump(filter_var("...aho...@example.com", FILTER_VALIDATE_EMAIL));
var_dump(filter_var("test+test@example.com", FILTER_VALIDATE_EMAIL));
?>

結果:

bool(false)
string(21) "test+test@example.com"

こんな感じ。

で、肝心の実装。

void php_filter_validate_email(PHP_INPUT_FILTER_PARAM_DECL) /* {{{ */
{
    /* From http://cvs.php.net/co.php/pear/HTML_QuickForm/QuickForm/Rule/Email.php?r=1.4 */
    const char regexp[] = "/^((\\\"[^\\\"\\f\\n\\r\\t\\b]+\\\")|([A-Za-z0-9_\\!\\#\\$\\%\\&\\'\\*\\+\\-\\~\\/\\^\\`\\|\\{\\}]+(\\.[A-Za-z0-9_\\!\\#\\$\\%\\&\\'\\*\\+\\-\\~\\/\\^\\`\\|\\{\\}]*)*))@((\\[(((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9])))\\])|(((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9])))|((([A-Za-z0-9])(([A-Za-z0-9\\-])*([A-Za-z0-9]))?\\.?)+[A-Za-z\\-]*))$/D";

うはwwwww正規表現がベタ書きwww

それはいいとして、

/^((\"[^\"\f\n\r\t\b]+\")|([A-Za-z0-9_\!\#\$\%\&\'\*\+\-\~\/\^\`\|\{\}]+(\.[A-Za-z0-9_\!\#\$\%\&\'\*\+\-\~\/\^\`\|\{\}]*)*))@((\[(((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9])))\])|(((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9]))\.((25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9])))|((([A-Za-z0-9])(([A-Za-z0-9\-])*([A-Za-z0-9]))?\.?)+[A-Za-z\-]*))$/D

ちなみに、正規表現のDオプションは「PCRE_DOLLAR_ENDONLY」というやつで、「$」が末尾の「\n」にマッチしなくなるというものらしい。これも知らなかった。

これをわかりやすく書き下したのが以下。

addr-spec: ^(?P=local-part)@(?P=domain)$
local-part: (?P=dot-atom)|(?P=quoted-string)
quoted-string: \"[^\"\f\n\r\t\b]+\"
dot-atom: (?P=atext)+(\.(?P=atext)*)*
atext: [A-Za-z0-9_\!\#\$\%\&\'\*\+\-\~\/\^\`\|\{\}]
domain: (?P=ipv4-addr-or-something)|(?P=braced-ipv4-addr)
braced-ipv4-addr: \[(?P=ipv4-addr)\]
ipv4-addr-or-something: (?P=ipv4-addr)|(?P=something)
ipv4-addr: (?P=octet-digits)\.(?P=octet-digits)\.(?P=octet-digits)\.(?P=octet-digits)
octet-digits: (25[0-5])|(2[0-4][0-9])|([0-1]?[0-9]?[0-9])
something: ((?P=alnum)((?P=alnum-or-hyphen)*(?P=alnum))?.?)+(?P=alnum-or-hyphen)*
alnum: [A-Za-z0-9]
alnum-or-hyphen: (?P=alnum)|-

RFC2822によるとdomainのところはdot-atomとかが許可されているはずなのにsomething (とここでは表現した)はずいぶんいい加減なルールになっている。とはいってもまったく無根拠というわけでもない。一応RFC1034を見ると、

3.5. Preferred name syntax

The DNS specifications attempt to be as general as possible in the rules

for constructing domain names.  The idea is that the name of any
existing object can be expressed as a domain name with minimal changes.
However, when assigning a domain name for an object, the prudent user
will select a name which satisfies both the rules of the domain system
and any existing rules for the object, whether these rules are published
or implied by existing programs.

For example, when naming a mail domain, the user should satisfy both the
rules of this memo and those in RFC-822.  When creating a new host name,
the old rules for HOSTS.TXT should be followed.  This avoids problems
when old software is converted to use domain names.

The following syntax will result in fewer problems with many
applications that use domain names (e.g., mail, TELNET).

<domain> ::= <subdomain> | " "

<subdomain> ::= <label> | <subdomain> "." <label>

<label> ::= <letter> [ [ <ldh-str> ] <let-dig> ]

<ldh-str> ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>

<let-dig-hyp> ::= <let-dig> | "-"

<let-dig> ::= <letter> | <digit>

とあって、これに基づこうとしているようなんだけど、その割に

<?php
var_dump(filter_var("a@a.b.c-", FILTER_VALIDATE_EMAIL));
var_dump(filter_var("a@123x", FILTER_VALIDATE_EMAIL));
?>

なんかを通してしまうので、まあなんというか、よくわかんないですね。