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


## 概要

[CodeIgniter](http://codeigniter.jp/user_guide_ja/general/urls.html)などのフレームワークでは、URLを分かりやすいものにするため、PATH_INFOを利用しています。
これを実現するため、`.htaccess`ファイルに以下の記述をします。

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

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

![](https://qiita-image-store.s3.amazonaws.com/0/35948/f94d4dba-eb77-57f0-4775-68e6f7a24d0d.png)

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

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

* [さくらサーバでNo input file specified. - せつないぶろぐ](http://blog.setunai.net/20121014/%E3%81%95%E3%81%8F%E3%82%89%E3%82%B5%E3%83%BC%E3%83%90%E3%81%A7no-input-file-specified/)
* [CodeIgniterで、”No input file specified error” エラーがでる | 開発業務日誌](http://office-ikason.com/devblog/archives/264)
* [PHP - CodeigniterでのNo input file specifiedの対処法 - Qiita](http://qiita.com/ytsnomiya/items/6c82df017f114c0b714e)
* [php - No input file specified - Stack Overflow](http://stackoverflow.com/questions/14555996/no-input-file-specified)
* [php - .htaccess problem: No input file specified - Stack Overflow](http://stackoverflow.com/questions/1557258/htaccess-problem-no-input-file-specified)
* [Getting a "No input file specified" error when trying to remove index.php via .htaccess in CodeIgniter | Jon Moore](http://www.jonmoore.co.uk/php/codeigniter/getting-a-no-input-file-specified-error-when-trying-to-remove-indexphp-via-htaccess)
* [ce cache - "no input file specified" - .htaccess - ExpressionEngine® Answers](http://expressionengine.stackexchange.com/questions/11672/no-input-file-specified-htaccess)

しかしこの記述は、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_input](http://php.net/manual/ja/function.filter-input.php)や[filter_input_array](http://php.net/manual/ja/function.filter-input-array.php)などの関数には対応していません。

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

    +   前者の問題については、`$_SERVER['REQUEST_TIME']`や`$_SERVER['PHP_AUTH_USER']`と同じく[取得できない変数](http://qiita.com/mugng/items/717924a015714b0aa266)と考えて下さい。

    +   後者の問題については、
PATH_INFOはクエリストリングのキーとして渡されており、偶然取得してしまうケースは極めて稀と考えられます。
また、その値も空文字となっています。

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

```php
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`から削除する処理が不要になり、簡潔になるのではないか?
先程、そう思いつきました。

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

### 注意

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

### コード

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

```php
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;
	}
});
```