リライトで「index.php?/$1」とした場合でもPATH_INFOを取得するフォールバックコード

  • 7
    いいね
  • 4
    コメント
この記事は最終更新日から1年以上が経過しています。

概要

CodeIgniterなどのフレームワークでは、URLを分かりやすいものにするため、PATH_INFOを利用しています。
これを実現するため、.htaccessファイルに以下の記述をします。

.htaccess
RewriteEngine on
RewriteCond $1 !^(index\.php|images|robots\.txt)
RewriteRule ^(.*)$ /index.php/$1 [L]

しかし、レンタルサーバによっては「No input file specified.」というエラーが発生し、PHPが動作しなくなる場合があります。

この解決方法として、記述を以下のように変更する方法があります。

.htaccess
RewriteEngine on
RewriteCond $1 !^(index\.php|images|robots\.txt)
- RewriteRule ^(.*)$ /index.php/$1 [L]
+ RewriteRule ^(.+)$ /index.php?/$1 [QSA,L]

しかしこの記述は、PATH_INFOをクエリストリングとしてPHPに渡してしまいます。
そのため、$_SERVER['PATH_INFO']でPATH_INFOが取得できなくなります。

以下のコードは、そのような場合でも$_SERVER['PATH_INFO']を使えるようにするためのフォールバックコードです。
$_SERVER['PATH_INFO']を定義することで、フレームワークや自作コードを修正する手間を省きます。

※参考サイトでは[QSA,L]ではなく[L]ですが、[L]の場合はクエリストリングが上書きされるため、クエリストリングを正しく処理できなくなります。
このため、クエリストリングを上書きしないようQSAを追加しています。
この改変は本投稿で紹介するフォールバックコードには影響しません。フォールバックコードは[L]でも[QSA,L]でも正しく動作します。

コード

  • $_SERVER['PATH_INFO']が未定義の場合に再定義を行います。
  • $_SERVER['QUERY_STRING']$_GETに渡された本来のPATH_INFOを削除します。

注意

  • このフォールバックコードより前で$_SERVER['QUERY_STRING']$_GETを変更しないで下さい。
    もし片方のみ変更されていた場合、$_GET$_SERVER['QUERY_STRING']を基準とした値に上書きされてしまい、変更が無効になってしまう可能性があります。

  • filter_inputfilter_input_arrayなどの関数には対応していません。

    具体的には、filter_input関数などではPATH_INFOを取得できず
    またクエリストリングに渡された本来のPATH_INFOを取得できてしまいます。

    • 前者の問題については、$_SERVER['REQUEST_TIME']$_SERVER['PHP_AUTH_USER']と同じく取得できない変数と考えて下さい。
    • 後者の問題については、
      PATH_INFOはクエリストリングのキーとして渡されており、偶然取得してしまうケースは極めて稀と考えられます。
      また、その値も空文字となっています。

      //example/news/article/my_articleなどのパス文字列をキーとするクエリストリングをfilter_input関数で取得している、などの稀なケースを除き、
      クエリストリングに渡された本来のPATH_INFOを取得できてしまうこの問題は考慮する必要はないものと考えます。

call_user_func(function(){
    if(!isset($_SERVER['PATH_INFO'])){
        if(isset($_SERVER['ORIG_PATH_INFO'])){
            $path_info=$_SERVER['ORIG_PATH_INFO'];
        }else{
            /**
             * `RewriteRule ^(.*)$ index.php?/$1 [QSA,L]`
             * などの記述によりPATH_INFOを取得出来ない場合のフォールバック
             */

            $sn_sp=strrpos($_SERVER['SCRIPT_NAME'],'/index.php');
            if($sn_sp===false){
                $sn_sp=strrpos($_SERVER['SCRIPT_NAME'],'index.php');
            }
            if(isset($_SERVER['REDIRECT_URL'])){
                $pi_urlencode=
                $pi_urldecode=ltrim(substr($_SERVER['REDIRECT_URL'],$sn_sp),'/');
            }else{
                $pi_urlencode=ltrim(substr($_SERVER['REQUEST_URI'],$sn_sp),'/');
                $pi          =strstr($pi_urlencode,'?',true);
                if($pi!==false){
                    $pi_urlencode=$pi;
                }

                /**
                 * REQUEST_URIから取得したPATH_INFOの値をURLデコード
                 */
                $pi_urldecode=implode(
                    '/',
                    array_map(
                        function($pp){
                            return urldecode($pp);
                        },
                        explode('/',$pi_urlencode)
                    )
                );
            }

            $query_string=isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : http_build_query($_GET);

            /**
             * 先頭のスラッシュを統一
             */
            $slash_start_query=strncmp($query_string,'/',1)===0;
            if($slash_start_query){
                $pi_urlencode='/'.$pi_urlencode;
                $pi_urldecode='/'.$pi_urldecode;
            }

            /**
             * 相対パスを変換
             * @link http://webdesign-dackel.com/2014/06/12/php-relativepath-to-absolutepath/
             */
            list($pi_urlencode_absolute,$pi_urldecode_absolute)=array_map(function($pi){
                $new_pi=array();
                foreach(explode('/',$pi) as $pp){
                    if($pp==='.'){
                        array_shift($new_pi);
                        array_unshift($new_pi,'');
                    }elseif($pp==='..'){
                        array_pop($new_pi);
                        if(count($new_pi)===0){
                            $new_pi=array('');
                        }
                    }else{
                        $new_pi[]=$pp;
                    }
                }
                return implode('/',$new_pi);
            },array($pi_urlencode,$pi_urldecode));

            /**
             * $_SERVER['QUERY_STRING']からPATH_INFOを削除
             */
            $pi_query=null;
            foreach(array(
                //URLエンコードされた相対パス
                $pi_urlencode,
                //URLエンコードされた絶対パス
                $pi_urlencode_absolute,
                //URLデコードされた相対パス
                $pi_urldecode,
                //URLデコードされた絶対パス
                $pi_urldecode_absolute,
            ) as $pi_query){
                foreach(array(
                    $pi_query.'&',
                    $pi_query
                ) as $pi_query_amp){
                    $pi_q_len=strlen($pi_query_amp);
                    if(strncmp($query_string,$pi_query_amp,$pi_q_len)===0){
                        $query_string=substr($query_string,$pi_q_len);
                        break 2;
                    }
                }
            }
            $_SERVER['QUERY_STRING']=$query_string;

            /**
             * $_GETからPATH_INFOを削除
             */
            parse_str($query_string,$new_get);
            if($pi_query===$pi_urlencode || $pi_query===$pi_urlencode_absolute){
                $pi_query=urldecode($pi_query);
            }
            if(!isset($new_get[$pi_query]) && isset($_GET[$pi_query]) && $_GET[$pi_query]===''){
                unset($_GET[$pi_query]);
            }

            /**
             * PATH_INFOを取得
             * PATH_INFOはURLデコードされ、絶対パスに変換された、先頭にスラッシュが付いている値
             */
            $path_info=$pi_urldecode_absolute;
            if(!$slash_start_query){
                $path_info='/'.$path_info;
            }
        }
        $_SERVER['PATH_INFO']=$path_info;
    }
});

より簡潔な手法

上記で提示した方法では、$_SERVER['REQUEST_URI']からPATH_INFOを取得し、$_SERVER['QUERY_STRING']$_GETからPATH_INFOに相当するクエリストリングを削除しています。
ここで、PATH_INFOをクエリストリングに追加しないようにすれば、$_SERVER['QUERY_STRING']$_GETから削除する処理が不要になり、簡潔になるのではないか?
先程、そう思いつきました。

以下では、その考えによるコードを書き留めておきます。

注意

  • 以下のコードは、不完全かもしれません。利用前によく検証してください

コード

.htaccess
RewriteEngine on
RewriteCond $1 !^(index\.php|images|robots\.txt)
RewriteRule ^(.+)$ /index.php [L]
call_user_func(function(){
    if(!isset($_SERVER['PATH_INFO'])){
        if(isset($_SERVER['ORIG_PATH_INFO'])){
            $path_info=$_SERVER['ORIG_PATH_INFO'];
        }else{
            $sn_sp=strrpos($_SERVER['SCRIPT_NAME'],'/index.php');
            if($sn_sp===false){
                $sn_sp=strrpos($_SERVER['SCRIPT_NAME'],'index.php');
            }
            if(isset($_SERVER['REDIRECT_URL'])){
                $pi_urldecode=ltrim(substr($_SERVER['REDIRECT_URL'],$sn_sp),'/');
            }else{
                $pi_urlencode=ltrim(substr($_SERVER['REQUEST_URI'],$sn_sp),'/');
                $pi          =strstr($pi_urlencode,'?',true);
                if($pi!==false){
                    $pi_urlencode=$pi;
                }

                /**
                 * REQUEST_URIから取得したPATH_INFOの値をURLデコード
                 */
                $pi_urldecode=implode(
                    '/',
                    array_map(
                        function($pp){
                            return urldecode($pp);
                        },
                        explode('/',$pi_urlencode)
                    )
                );
            }

            /**
             * 相対パスを変換
             * @link http://webdesign-dackel.com/2014/06/12/php-relativepath-to-absolutepath/
             */
            $new_pi=array();
            foreach(explode('/',$pi_urldecode) as $pp){
                if($pp==='.'){
                    array_shift($new_pi);
                    array_unshift($new_pi,'');
                }elseif($pp==='..'){
                    array_pop($new_pi);
                    if(count($new_pi)===0){
                        $new_pi=array('');
                    }
                }else{
                    $new_pi[]=$pp;
                }
            }
            $pi_urldecode_absolute=implode('/',$new_pi);

            /**
             * 連続した"/"を1つに置換
             */
            $pi_urldecode_absolute=preg_replace('|/{2,}|','/',$pi_urldecode_absolute);

            /**
             * PATH_INFOを取得
             * PATH_INFOはURLデコードされ、絶対パスに変換された、先頭にスラッシュが付いている値
             */
            $path_info=$pi_urldecode_absolute;
            if(strncmp($path_info,'/',1)!==0){
                $path_info='/'.$path_info;
            }
        }
        $_SERVER['PATH_INFO']=$path_info;
    }
});