PHPの仕様上、au端末のCookieでは有効期限が設定できない問題を回避する


[header]:http://php.net/manual/ja/function.header.php
[header_remove]:http://php.net/manual/ja/function.header-remove.php
[header_register_callback]:http://php.net/manual/ja/function.header-register-callback.php
[ob_start]:http://php.net/manual/ja/function.ob-start.php
[register_shutdown_function]:http://php.net/manual/ja/function.register-shutdown-function.php

## 注意

PHP5.3系列は既に公式のサポートが打ち切られています。
この記事では後方互換性のためPHP5.4未満のバージョンに対応したコードも記載してありますが、**PHP5.4未満の利用そのものが推奨されるものではありません**。
レンタルサーバの都合など、どうしてもPHP5.4以降のバージョンが利用できない場合に限り採用してください。

## 概要

[今時の携帯ブラウザCookie事情。 - ありがとう。また会おう。](http://norii.hatenablog.com/entry/20080806/1218032059)

> Au端末のCookieのデフォルトの有効期限は24時間と結構長めです。
> そして、これまたやっかいなことに、有効期限の設定は
> 「HTTPレスポンスヘッダの「Set-Cookie」フィールドの「max age (有効残存秒数) 」で指定」
> とあるんですが、実はこの方法でしか有効期限が設定できません。
> PCサイトでよくある他の指定方法(Expireヘッダとか)では対応していません。

こちらの情報いわく、auの携帯端末では**max-age属性でなければCookieの有効期限を設定出来ない**とのこと。
(初期状態では、24時間保存されるようです。)
<del>そして、**PHPのCookieはmax-age属性を送出しない**らしい。</del>
<ins>PHP5.5以降、[PHPのCookieでmax-age属性が送出](http://php.net/manual/ja/function.setcookie.php#refsect1-function.setcookie-changelog)されるようになりました。</ins>

これを回避するためのコードが載っていたのですが、イマイチ活用しにくいものだったのでこちらで改変してみました。

## コード(新版)

[コメントの情報](#comment-aa4b359b0957440ffe90)を元にしたコードです。
@mpywさんのコードそのままですが…

<!--
@mpywさんのコードのほぼ完コピですが…
-->

PHP5.5以降、[PHPのCookieでmax-age属性が送出](http://php.net/manual/ja/function.setcookie.php#refsect1-function.setcookie-changelog)されるようになったため、auに限らず送出するようなコードにしておくべきでしょう。
なおこのコード、動作原理上[header_remove]関数の利用が前提となるので、PHP5.3未満では利用できません。

```php
<?php
/**
 * PHP5.5未満の場合、Set-CookieヘッダにMax-Age属性を追加する。
 */
if(version_compare(PHP_VERSION,'5.5.0','<') && !headers_sent()){
	ob_start(function($buffer){
		$headers=preg_replace_callback(
			'/\ASet-Cookie: [^;]*+; expires=([^;]*+)\K/',
			function($m){
				return '; Max-Age='.(strtotime($m[1]) - $_SERVER['REQUEST_TIME']);
			},
			headers_list()
		);
		header_remove();
		foreach($headers as $header){
			header($header,false);
		}
		return $buffer;
	});
}





//1時間後に消滅するCookieをセット
setcookie('test','example',time()+60*60);

echo 'Cookieを設定しました';
```

[ob_start]関数の利用を避けたい場合、[header_register_callback]関数で同じことが出来ます。
ただし[register_shutdown_function]関数と違い、[header_register_callback]関数はコールバック関数を複数登録できないので重複しないよう注意するべきでしょう。

```php
if(version_compare(PHP_VERSION,'5.5.0','<') && !headers_sent()){
	header_register_callback(function(){
		$headers=preg_replace_callback(
			'/\ASet-Cookie: [^;]*+; expires=([^;]*+)\K/',
			function($m){
				return '; Max-Age='.(strtotime($m[1]) - $_SERVER['REQUEST_TIME']);
			},
			headers_list()
		);
		header_remove();
		foreach($headers as $header){
			header($header,false);
		}
	});
}
```

## コード(旧版)

[header_register_callback]関数を使用しているため、対応環境はPHP5.4以上となります。
PHP5.4未満の環境~~(私のレンタルサーバ含む)~~では、最初の出力(print文やecho文など)の直前で[header_register_callback]関数のナカミを実行してください。

<ins>PHP5.4未満の環境で[header_register_callback]関数を利用するためのコードを投稿しました。
[PHP5.4未満でheader_register_callbackを利用する](http://qiita.com/sounisi5011/items/21eac8a01b8c2a158d6e)</ins>

```php
<?php
/**
 * au用にCookieを上書きするよう設定
 */
if($is_mobile_au){
	header_register_callback(function(){
		//Set-Cookieヘッダの値
		$cookie_strs=array();
		//Set-Cookieヘッダの文字数(":"の1文字も足している)
		$header_len=11;

		/* Set-Cookieヘッダの値を全て取得 */
		foreach(headers_list() as $header){
			if(strncmp($header,'Set-Cookie:',$header_len)===0){
				$cookie_strs[]=trim(substr($header,$header_len));
			}
		}

		/* Set-Cookieヘッダを削除 */
		header_remove('Set-Cookie');

		/* 取得したSet-Cookieヘッダの値を加工し、再送信 */
		foreach($cookie_strs as $cookie_str){
			/* expires属性をmax-age属性に変更 */
			$cookie_str=preg_replace_callback('/expires=([^;]+)/i',function($matches){
				$time=$matches[1];
				$time=strtotime($time);
				$time-=$_SERVER['REQUEST_TIME'];
				return 'Max-Age='.$time;
			},$cookie_str);

			/* Set-Cookieヘッダを再送信 */
			header('Set-Cookie: '.$cookie_str,false);
		}
	});
}





//1時間後に消滅するCookieをセット
setcookie('test','example',time()+60*60);

echo 'Cookieを設定しました';
```

### PHP5.3未満の場合

なお、PHP5.3未満の場合、[header_remove]関数が使えないため、[header]関数の第二引数をループで変更させて対処します。

```php:PHP5.3未満の場合
<?php

//1時間後に消滅するCookieをセット
setcookie('test','example',time()+60*60);





/**
 * au用にCookieを上書きするよう設定
 */
if($is_mobile_au){
	//Set-Cookieヘッダの値
	$cookie_strs=array();
	//Set-Cookieヘッダの文字数(":"の1文字も足している)
	$header_len=11;
	//Set-Cookieヘッダの置換フラグ
	$header_replace=true;

	/* Set-Cookieヘッダの値を全て取得 */
	foreach(headers_list() as $header){
		if(strncmp($header,'Set-Cookie:',$header_len)===0){
			$cookie_strs[]=trim(substr($header,$header_len));
		}
	}

	/* 取得したSet-Cookieヘッダの値を加工し、再送信 */
	foreach($cookie_strs as $cookie_str){
		/* expires属性をmax-age属性に変更 */
		$cookie_str=preg_replace_callback('/expires=([^;]+)/i',function($matches){
			$time=$matches[1];
			$time=strtotime($time);
			$time-=$_SERVER['REQUEST_TIME'];
			return 'Max-Age='.$time;
		},$cookie_str);

		/* Set-Cookieヘッダを再送信 */
		header('Set-Cookie: '.$cookie_str,$header_replace);

		/* 置換フラグが有効の場合、無効にする */
		if($header_replace){
			$header_replace=false;
		}
	}
}





echo 'Cookieを設定しました';
```

### 正規表現ではなく文字列関数の場合…

PHPにおいて正規表現は遅い処理です。
一般的に、文字列関数の処理に変更したほうがパフォーマンスは良くなります。
そこで、このコードで使われている正規表現を文字列処理関数で代用したところ、
1つや2つのCookieでは正規表現の方がパフォーマンスは上でした。

```php:文字列処理関数で代用(元のコードはPHP5.4以上用のもの)
<?php
/**
 * au用にCookieを上書きするよう設定
 */
if($is_mobile_au){
	header_register_callback(function(){
		//Set-Cookieヘッダの値
		$cookie_strs=array();
		//Set-Cookieヘッダの文字数(":"の1文字も足している)
		$header_len=11;

		/* Set-Cookieヘッダの値を全て取得 */
		foreach(headers_list() as $header){
			if(strncmp($header,'Set-Cookie:',$header_len)===0){
				$cookie_strs[]=trim(substr($header,$header_len));
			}
		}

		/* Set-Cookieヘッダを削除 */
		header_remove('Set-Cookie');

		/* 取得したSet-Cookieヘッダの値を加工し、再送信 */
		foreach($cookie_strs as $cookie_str){
			/* expires属性をmax-age属性に変更 */
			$ex_sp=strpos(strtolower($cookie_str),'expires=');
			//expires属性が存在する場合、書き換え処理を実行
			if($ex_sp!==false){
				$ex_vp=$ex_sp+8;
				$ex_ep=strpos($cookie_str,';',$ex_sp);
				if($ex_ep!==false){
					$time=substr($cookie_str,$ex_vp,$ex_ep-$ex_vp);
				}else{
					$time=substr($cookie_str,$ex_vp);
				}

				$time=strtotime($time);
				$time-=$_SERVER['REQUEST_TIME'];
				$cookie_str=substr($cookie_str,0,$ex_sp).'Max-Age='.$time.($ex_ep!==false ? substr($cookie_str,$ex_ep) : '');
			}

			/* Set-Cookieヘッダを再送信 */
			header('Set-Cookie: '.$cookie_str,false);
		}
	});
}





//1時間後に消滅するCookieをセット
setcookie('test','example',time()+60*60);

echo 'Cookieを設定しました';
```

## max-age属性の記述と値についての疑問

[header]関数のCookieでmax-age属性を直書きする際、max-age属性は大文字でいいのだろうか。
とりあえず原文に従い、大文字で書いたけれど…

`Max-Age` or `max-age` ?

それからmax-age属性って、負の秒数はOKなんだろうか。
このコードでは負の秒数を容認するように書いてあるけれど…