PHP の $_SERVER['REQUEST_URI'] と parse_url() の予想外な動作について。

REQUEST_URI と HTTP_HOST


PHP のサーバ変数 $_SERVER['REQUEST_URI'] には、ふつうパスとクエリが設定される。

'REQUEST_URI'
ページにアクセスするために指定された URI。例えば、 '/index.html'

PHP: $_SERVER - Manual

ただし、常にパスから始まると保証されているわけではない。以下のように、 GET に続けて絶対 URL を書いたリクエストを送ると、

GET http://localhost/test.php?query=value#fragment HTTP/1.1
Host: localhost

$_SERVER['REQUEST_URI'] の値は、

http://localhost/test.php?query=value#fragment

になる。

ポイント: REQUEST_URI は / から始まるとは限らない。リクエスト次第で http://... からの値を設定できる。

PHP では HTTP コンテキストオプションで request_fulluri を設定すると、こういう(不正な) *1リクエストを送れる。

request_fulluri boolean

TRUE を指定すると、リクエストを生成する際に完全な URI (GET http://www.example.com/path/to/file.html HTTP/1.0) が用いられます。これは標準のリクエストフォーマットではありませんが、 このようなフォーマットを要求するプロキシサーバも存在します。

デフォルトは FALSE です。

PHP: HTTP コンテキストオプション - Manual

よく見るコードで、

$url = 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];

こんな風に書いてはいけない。これは、 REQUEST_URI だけでなく HTTP_HOST にも問題がある。

'HTTP_HOST'
現在のリクエストに Host: ヘッダが もしあればその内容。

PHP: $_SERVER - Manual

ということは、

GET http://localhost/test.php?query=value#fragment HTTP/1.0

という Host ヘッダが無い HTTP/1.0 のリクエストを送ると、上の $url の値は、

http://http://localhost/test.php?query=value#fragment

になる。HTTP_HOST は空になり、 REQUEST_URI には http:// から始まる値が設定される。

ポイント: HTTP_HOST は値が存在しないかもしれないし、何でも好きな値を設定されるかもしれない。

Apache だとネームベースのバーチャルホストを設定していなければ、あるいは設定していても、適当な Host ヘッダで PHP をリクエストできる。

parse_url()

$_SERVER['REQUEST_URI'] を parse_url() でパースすれば、ホスト・パス・クエリ・フラグメントに分離できてハッピーだ。

と思ったのだが。

この関数は、指定された URL が有効かどうかを調べるためのもの ではなく、単に URL を上で示した 要素に分解するだけのものです。不完全な URL であっても受け入れられますし、 そのような場合でも parse_url() は可能な限り 正しく解析しようとします。

PHP: parse_url - Manual

注意:
この関数は相対 URL では動作しません。

PHP: parse_url - Manual

これ正確には、「相対URLを渡したらどうなっても知らんよ」という意味だった。

相対URLを渡しても大抵パースできるのだが、たまに予想外な失敗の仕方をする。

<?php

var_dump(parse_url("/abc?a=x&time=09:00&x=y"));

var_dump(parse_url("/abc?a=x&time=09:00"));

この結果は、

array(2) {
  ["path"]=>
  string(4) "/abc"
  ["query"]=>
  string(18) "a=x&time=09:00&x=y"
}
bool(false)

になる。上は成功するけど下は失敗するのですよ。驚きだよ。

ポイント: parse_url()に相対URLを渡しても動くように見えるが、渡してはいけない。

しかも、バグレポートが出てるのに却下してる。

ひどくね?

まとめ

PHP はひどい。

*1:追記 HTTP/1.1でGETの後ろに絶対URL (http://...) を書けました。不正じゃないです。 http://tools.ietf.org/html/rfc2616#section-5.1.2