LoginSignup
19
19

More than 5 years have passed since last update.

Mac の PHP はパーセントエンコーディングな日本語リダイレクトを解釈できない?

Last updated at Posted at 2016-06-21

『Mac 環境の PHP だけ挙動が違う』という、首をかしげる問題に遭遇したのでメモ。

追記 (2016/06/23 00:28)

厳密には

  • OS 依存ではなく、ロケールに依存する
  • パーセントエンコーディングが引き金ではなく、日本語の文字コードに含まれる制御文字が引き金

という問題のようです。詳しくは下部の追記およびコメントをどうぞ。
@rryu さん ご教示ありがとうございます :bow:

Location ヘッダのパーセントエンコーディングが解釈できない

file_get_contents を使って URL の内容を取得する、なんてことはよくあります。楽ですし。

そこで指定した URL が 301 リダイレクトを返す場合、Location ヘッダにパーセントエンコーディングを含むと Invalid redirect URL! の Warning が出る 、という問題に遭遇しました。

例:Wikipedia の HTTP ページ

例えば、Wikipediaのトップページは https://ja.wikipedia.org/wiki/メインページ です。このプロトコルを http にすると、https への 301 リダイレクトになります。

で、日本語部分をパーセントエンコーディングして、"http://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8" を file_get_contents() で取ってみます。

$ curl --head "http://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8"
HTTP/1.1 301 TLS Redirect
Server: Varnish
Location: https://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8
(...省略)

$ php -r "file_get_contents('http://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8');"
PHP Warning:  file_get_contents(http://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8):
failed to open stream: Invalid redirect URL! https://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8 in Command line code on line 1

PHP Stack trace:
PHP   1. {main}() Command line code:0
PHP   2. file_get_contents() Command line code:1

Invalid redirect URL! と怒られてしまいました。 (※ 上記は整形してます)

El Capitan に添付されている PHP 5.5.34 、および phpenv + php-build で導入した PHP 5.6.22、さらには現時点の最新の PHP 7.0.7 でもこの問題が発生します。

もちろんブラウザでは正しく閲覧できますし、curl -L ならちゃんとリダイレクトも追えます。

エンコーディングされていない Location ヘッダは?

パーセントエンコーディングされていない(日本語のままの)Wikipedia の URL を渡してあげると、Location ヘッダには そのまま日本語が入ってきます。

この場合、 file_get_contents() で Warning は 出ません。

$ curl --head "http://ja.wikipedia.org/wiki/メインページ"
HTTP/1.1 301 TLS Redirect
Server: Varnish
Location: https://ja.wikipedia.org/wiki/メインページ
(...省略)

$ php -r "file_get_contents('http://ja.wikipedia.org/wiki/メインページ');"
(正常終了)

Mac 固有の問題?

パーセントエンコーディングを含んだリダイレクトを検証するため、

  • ubuntu
  • Debian
  • CentOS

といった各種 Linux ディストリビューション向けにビルドされた PHP でも試してみました。

ところが、いずれの PHP もパーセントエンコーディングを正しく解釈し、Warning が出ずに正常終了 してしまいます。

$ curl --head "http://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8"
HTTP/1.1 301 TLS Redirect
Server: Varnish
Location: https://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8
(...省略)

$ php -r "file_get_contents('http://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8');"
(...正常終了!?)

…えっ、Mac 固有の問題なのこれ……!?

エラー発生箇所

PHP のソースで言うと、発生しているエラーはおそらくこのへん
間違ってました。詳しくはコメントをどうぞ。

回避策

もしこの問題に出会ってしまったら、 cURL 関数群 を使って回避するのが堅実でしょう。
file_get_contents は非常に楽ちんですが、curl は安心を買えます。

追記:file_get_contents のまま問題を回避することも可能ですが、ロケールへの依存を残すよりは やはり curl を素直に使った方が堅実です。

なぜ Mac だけ?

とはいえ、なぜ Mac の PHP だけで このような Warning が出るのか、正しい原因がまだ掴めてません。
PHP のバージョンや php.ini の設定も疑ったのですが、同一にしても Mac 環境だけはやはりエラーになります。

そもそも、確実に再現するのが Mac というだけで、『本当に Mac 固有の問題なのか』も微妙ですし、ちょっとモヤモヤしています。

この Warning の原因に関して、ご存知の方はご教示いただけると嬉しいです。 :bow:


追記:ご教示いただきました

コントロールコードかどうかの判定にiscntrl()関数を使っていますが、この関数の挙動はロケールに依存します。
ということでMacとそれ以外で環境変数''LC_*''系の値が異なるのではないかと思いますがどうでしょうか。

PHP のソースも読んだつもりでしたが、ここの部分だけ思いっきり見落としていました…。

追加検証

Warning が出た時の Mac のロケールは ja_JP.UTF-8 です。

$ locale
LANG="ja_JP.UTF-8"
LC_COLLATE="ja_JP.UTF-8"
LC_CTYPE="ja_JP.UTF-8"
LC_MESSAGES="ja_JP.UTF-8"
LC_MONETARY="ja_JP.UTF-8"
LC_NUMERIC="ja_JP.UTF-8"
LC_TIME="ja_JP.UTF-8"
LC_ALL=

$ php -r "file_get_contents('http://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8');"
PHP Warning:  file_get_contents(http://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8):
failed to open stream: Invalid redirect URL! https://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8 in Command line code on line 1

試しに LC_CTYPE=C に設定して同じコードを実行すると、Warning が出ず正常終了 しました。PHP コード内部でも setlocale() 関数を使用することで回避可能。

$ LC_CTYPE=C php -r "file_get_contents('http://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8');"
(正常終了)

$ php -r "setlocale(LC_CTYPE, 'C'); file_get_contents('http://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8');"
(正常終了)

Mac だけ Warning になる真相

どの文字が iscntrl() で制御文字と判定されているのか調べてみました。以下の検証用コード使用。

検証用PHPコード
<?php
for ($i = 0; $i <= 255; $i++) {
  if (ctype_cntrl($i)) printf('0x%02x ', $i);
}
  • Mac 10.11
    • LC_CTYPE=C0x000x1f 0x7f
    • LC_CTYPE=ja_JP.UTF-80x000x1f 0x7f 0x800x9f
  • 自分が試した Linux ディストリビューション (ubuntu, Debian, CentOS)
    • LC_CTYPE=C0x000x1f 0x7f
    • LC_CTYPE=ja_JP.UTF-80x000x1f 0x7f

Mac 環境『だけ』、日本語 UTF-8 の時は 0x800x9f が制御文字扱いになってます。
この範囲は UTF-8 の3バイト文字のうち、1バイト目が 0xed で始まる文字の 2バイト目 にあたります (RFC 3629 / UTF8-3)。

この範囲が、URL にある %82%83 とぶつかって、制御文字が含まれていると認識されてエラーになる訳ですね。1バイト目は %ED じゃないんだけどなぁ。。。

 

正しい回避策は?

仮に setlocale(LC_CTYPE, 'C') で回避したとしても ロケール依存問題は消えるわけじゃないので、やっぱり curl を素直に使うべきなんでしょうね…

逆に言えば、PHPコード内で setlocale() を使用してロケールを変更したり、PHP を動作させる環境のロケール設定が変わった場合(仮にLinuxだろうと)、想定せずこの Warning の影響を受ける可能性がある とも言えます。

 

追記2:私が本当に踏んだモノ (2016/06/24 23:00)

実は、この記事を書くきっかけになった「地雷」は、Mac 環境のコトじゃありません。CentOS です。

  • CentOS 6.8 環境で PHP が動作していた。
  • 使っていた PHP フレームワークの中で setlocale() を使用していた。
  • LC_CTYPE"ja_JP" を設定していた。

その結果、やはり例の文字コードが制御文字に。 :sob:

  • :o: LC_CTYPE=ja_JP.UTF-80x000x1f 0x7f
  • :x: LC_CTYPE=ja_JP0x000x1f 0x7f 0x800x8d 0x900x9f

UTF-8 設定を勝手に使ってくれると思い込むものでは無いですね。

Mac 環境と異なり、0x8e 0x8f の2字は制御文字扱いに なりません。 これは UNIX 由来の EUC-JP に合わせられたものと考えられます(それぞれ、半角カナ・補助漢字で使用されるコードで、領域も ja_JP.eucjp と一致します)。

結論

要は、こんな混沌としたハマり方をしないためにも、素直に curl_exec 使っておきましょう。
http://php.net/manual/ja/curl.examples-basic.php

19
19
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
19