前回のをさらに改良。
intArrayをInteger以外にも対応するように変更しました。
classArray.class.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
|
class classArray implements ArrayAccess,IteratorAggregate,Countable{
//適用可能な型
private $type_valid=array(
'string'
,'integer'
,'float'
,'boolean'
);
//型
private $type='';
//配列
private $arr=array();
//コンストラクタ
function __construct($type_valid='integer'){
//引数が適用可能な型でなければException
if(!in_array($type_valid,$this->type_valid)){
$debug=debug_backtrace();
throw new InvalidArgumentException(
'Value is not valid type in line <'.$debug[0]['line'].'>'
);
return false;
}
$this->type=$type_valid;
return true;
}
//マジックメソッド__set
public function __set($a,$b){
return $this->offsetSet($a,$b);
}
//マジックメソッド__get
public function __get($a){
return $this->offsetGet($a);
}
//引数の型チェック
private function _chechOffsetType($b){
switch($this->type){
case 'string':
return is_string($b);break;
case 'integer':
return is_int($b);break;
case 'float':
return is_float($b);break;
case 'boolean':
return is_bool($b);break;
}
return false;
}
//値セット ArrayAccess::offsetSet
public function offsetSet($a,$b){
//引数が正しい型かチェック
if(!$this->_chechOffsetType($b)){
//正しい型でなければException
$debug=debug_backtrace();
throw new InvalidArgumentException(
'Value is not '.$this->type.' in line <'.$debug[0]['line'].'>'
.' type <'.gettype($b).'>'
);
return false;
}
//挿入
if($a===NULL){
$this->arr[]=$b;
}else{
$this->arr[$a]=$b;
}
return true;
}
//値が存在するか ArrayAccess::offsetExists
public function offsetExists($a){
if($a===NULL){return false;}
return isset($this->arr[$a]);
}
//値を取得 ArrayAccess::offsetGet
public function offsetGet($a){
if($this->offsetExists($a)){
return $this->arr[$a];
}else{
return null;
}
}
//値削除 ArrayAccess::offsetUnset
public function offsetUnset($a){
if($this->offsetExists($a)){
unset($this->arr[$a]);
}
}
//個数カウント Countable::count
public function count(){
return count($this->arr);
}
//イテレータ IteratorAggregate::getIterator
public function getIterator(){
return new ArrayIterator($this->arr);
}
#↓クラスのおわり
}
|
new時に'boolean','integer','double','string'何れかの引数を指定できます。
そしてその後、それ以外の型の値を代入するとエラーになります。
判定は厳密に行われるので、string型にintegerを放り込むこともできません。
前回から変更したのはコンストラクタとoffsetSetです。
コンストラクタでは引数として型を指定し、offsetSetではコンストラクタの引数として指定した型で型チェックを行います。
型チェック部分は最初evalとかで簡単にできないかなーと思ったんですが、いまいちうまく行かないのでやめました。
evalは怖いですし。
classArray.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
//インスタンス
require_once('classArray.class.php');
$str=new classArray('string');
//代入
try{
$str[]='1';
$str[]=1; //ここでInvalidArgumentException
}catch(InvalidArgumentException $e){
print($e->getMessage());
}
print("<pre>");var_dump($str);
|
インスタンス生成時に'string'や'double'等と書くと、その型しか格納できない配列が作成できます。
値として真偽値のみが許される配列を作りたい、といった場合に使えるかも使えないかも。
前回のintArray.phpをちょっとだけ改造。
intArrayの各要素には$int['a']といった形でアクセスできますが、$int->aとしようとすると、勝手にintArray::aというメンバが作成されてしまいます。
では$int->aの挙動の変更は出来ないのか。できます。
intArray.class.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
|
class intArray implements ArrayAccess,IteratorAggregate,Countable{
//配列
private $arr=array();
//コンストラクタ
function __Construct(){}
//__set
public function __set($a,$b){
return $this->offsetSet($a,$b);
}
//__get
public function __get($a){
return $this->offsetGet($a);
}
//値セット ArrayAccess::offsetSet
public function offsetSet($a,$b){
//引数が数値かチェック
if(!is_int($b)){
//数値でなければエラー
$debug=debug_backtrace();
throw new InvalidArgumentException(
'Value is not integer in line <'.$debug[0]['line'].'>'
.' type <'.gettype($b).'>'
);
return false;
}
//挿入
if($a===NULL){
$this->arr[]=$b;
}else{
$this->arr[$a]=$b;
}
return true;
}
//値が存在するか ArrayAccess::offsetExists
public function offsetExists($a){
if($a===NULL){return false;}
return isset($this->arr[$a]);
}
//値を取得 ArrayAccess::offsetGet
public function offsetGet($a){
if($this->offsetExists($a)){
return $this->arr[$a];
}else{
return null;
}
}
//値削除 ArrayAccess::offsetUnset
public function offsetUnset($a){
if($this->offsetExists($a)){
unset($this->arr[$a]);
}
}
//個数カウント Countable::count
public function count(){
return count($this->arr);
}
//イテレータ IteratorAggregate::getIterator
public function getIterator(){
return new ArrayIterator($this->arr);
}
#↓クラスのおわり
}
|
__get()および__set()を実装すると、存在しないプロパティにアクセスした際にそのメソッドが呼び出されます。
http://jp.php.net/manual/ja/language.oop5.overloading.php#language.oop5.overloading.members
intArray.php
1
2
3
4
5
6
7
8
9
10
11
12
13
|
//インスタンス
require_once('intArray.class.php');
$int=new intArray();
//代入
$int[2]=10;
$int->a=20;
//取得
print($int->{2});
print($int['a']);
|
$int->a=10;
と書くと、intArrayクラスにaというプロパティは無いのでintArray::__set('a',10)というメソッドが呼ばれます。
これを利用してオブジェクト的、配列的どっちにでもアクセスできるようになりました。
まあ値の代入はArrayAccess::offsetSet()、ArrayAccess::offsetGet()で実装されているので、__set()、__get()自体はそれらを呼んでるだけという簡単実装です。
以上で、$int['a']=10と値を代入し、$int->aで値を取り出すことが出来ます。
まあ混在させると分かり難いだけなのでどちらかに統一すべきなんですが。
privateなメンバに対しても__set、__getが優先するので$int->arr=10;と書いても大丈夫です。
publicなメンバがあった場合のみ、そちらに対しての上書きが優先されます。
今回はpublicなメンバは存在しないので失敗する可能性はありません。
ちなみに数値で始まるメンバ変数にアクセスする場合、$int->{'1a'}と{}で括ります。
数値だけのメンバならさらに$int->{1}でもアクセスできます。
全くどうでもいいんですが、普通の変数でも${1}と数字だけの変数を作成できます。
激しく気持ち悪い。
前回作成したintArrayは一見配列のように代入取得できますが、foreachで値を取得しようとすると正しく動作しません。
intArrayをvar_dumpしてみると中身はこのようになっています。
object(intArray)#1 (1) {
["arr:private"]=>
array(2) {
[0]=>
int(123)
["abc"]=>
int(456)
}
}
PHPはObjectをforeachにかけると、その中のpublicなメンバ変数に順にアクセスします。
この場合privateしかないので、foreachしても何も返ってこないことになります。
foreachできない配列なんて役に立ちませんので、イテレータを実装してみます。
Iteratorを実装することでforeachができるようになり、ArrayAccessを実装することで配列としてアクセスすることができます。
で、この両者を足してみると配列そのものとなるわけです。
という方針で作ろうと思ってたのですが、連想配列に対してArrayAccess::offsetSet()とIterator::next()あたりを実装するあたりでおそろしく面倒なことになったのでとりあえずキャンセル。
作れないことはないのですが見難きことこの上ないソースになってしまった。
あまりに美しくないのでもっと綺麗な書き方を思いつくまで封印。
というわけでSPLを活用して簡単に実装。
intArray.class.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
class intArray implements ArrayAccess,IteratorAggregate,Countable{
//配列
private $arr=array();
//コンストラクタ
function __Construct(){}
//値セット ArrayAccess::offsetSet
public function offsetSet($a,$b){
//引数が数値かチェック
if(!is_int($b)){
//数値でなければエラー
$debug=debug_backtrace();
throw new InvalidArgumentException(
'Value is not integer in line <'.$debug[0]['line'].'>'
.' type <'.gettype($b).'>'
);
return false;
}
//挿入
if($a===NULL){
$this->arr[]=$b;
}else{
$this->arr[$a]=$b;
}
return true;
}
//値が存在するか ArrayAccess::offsetExists
public function offsetExists($a){
if($a===NULL){return false;}
return isset($this->arr[$a]);
}
//値を取得 ArrayAccess::offsetGet
public function offsetGet($a){
if($this->offsetExists($a)){
return $this->arr[$a];
}else{
return null;
}
}
//値削除 ArrayAccess::offsetUnset
public function offsetUnset($a){
if($this->offsetExists($a)){
unset($this->arr[$a]);
}
}
//個数カウント Countable::count
public function count(){
return count($this->arr);
}
//イテレータ IteratorAggregate::getIterator
public function getIterator(){
return new ArrayIterator($this->arr);
}
#↓クラスのおわり
}
|
こういう場合のためにArrayIteratorなんてものが出来ていますのでこれを使ってさくっと実装しました。
http://jp.php.net/manual/ja/class.arrayiterator.php
配列を突っ込むとそれに対してイテレータでアクセスすることができるようになります。
IteratorAggregateは、自分でイテレータを実装するかわりに別のクラスに実装を丸投げしてしまうという他人任せのインターフェイスです。
http://jp.php.net/manual/ja/class.iteratoraggregate.php
で、本来ならIteratorAggregate::getIteratorで返すインスタンス内に各イテレータの実装を書かないといけないわけですが、そこに対してArrayIteratorを突っ込めばそこらへんが自動的にできてしまいます。
IteratorAggregateとArrayIteratorを使用することでロジックをほとんどいじらずforeachを実装できてしまいました。
ArrayAccess::offsetSet()なんかは値を返す必要はないのですが、気分的に入れています。
これは大丈夫なのだろうか?
以上でforeachもできるほとんど配列的なクラスintArrayが作成できました。
ただ、これでint型しか格納できない配列が作れて万々歳かと思いきや、やはり実体はObjectなわけで、in_array()やarray_merge()といった配列関数に突っ込むとエラーになってしまいます。
in_array('666',$int);
みたいな検索を行うことが出来ません。残念。
CheckIP 1.2.1 (stable)
http://pear.php.net/package/Net_CheckIP
CheckIP2 1.0.0RC2 (beta)
http://pear.php.net/package/Net_CheckIP2
Pear公式に登録されているPearパッケージは現時点で500ちょいですが、その中で最も役に立たないライブラリはどれかと言われたら、私はCheckIPを挙げます。
まあ、とりあえず使ってみましょう。
checkip.php
1
2
3
4
5
6
7
8
|
require_once('Net/CheckIP.php');
$ret[]=Net_CheckIP::check_ip('192.168.0.1');
$ret[]=Net_CheckIP::check_ip('192.168.0.256');
var_dump($ret);
|
何をやっているかというと、IPアドレスが正しい形式かどうかチェックしています。
正しい形式というのがどの程度の正しさかというと、別に到達可能性をチェックしているわけでもなければIPv6に対応しているわけでもなくサブネットすら考慮しない、単なる形としてのIPアドレスです。
中身を見てみると.でexolodeして各値が0~255の間かチェックしてました。
で、数値のチェックにpreg_matchを使っていましたが、だったら最初から一発ですりゃいいのに。
そもそも標準関数だけで簡単に出来てしまうことをPear化する意味がわからない。
さて、あまりにしょっぱい内容だったせいかCheckIP2という後継パッケージが出ました。
今回はIPv6対応なんだろうな、IPv6の正規表現は確かに省略とかあって面倒だからなあ
checkip2.php
1
2
3
4
5
6
7
8
|
require_once('Net/CheckIP2.php');
$ret[]=Net_CheckIP2::check_ip('192.168.0.1');
$ret[]=Net_CheckIP2::check_ip('192.168.1.256');
var_dump($ret);
|
あれ?IPv6は?
>A package to determine if an IP (v4) is valid.
中を見てみたところ、コンストラクタと修飾子がくっついた以外全く同じでした。
なんだこれ。
ArrayAccessインターフェイスを実装すると、クラス内の要素に配列形式でアクセスできます。
http://jp.php.net/manual/ja/class.arrayaccess.php
これを使って何が便利かというと、通常出来ない値セット時の値チェックが行えることです。
PHPの利点の一つがアバウトな型付けではありますが、しかしどうしても厳密な型を使用したい場合もあります。
そんなときにJava並の強い型付けを行える配列を作成することができます。
数値のみを格納したい配列があった場合、
$int[0]=10;
$int[1]='10';
と$int[1]に代入した時点で型エラーを出すことができます。
これまで入力値は毎回チェックしてから変数に代入なんてことをやっていたわけですが、ArrayAccessを使えば、とりあえず値を突っ込んでエラーが返ってきたら駄目だったといった判断ができるようになります。
まあ、結局例外キャッチしないといけないので手間はあんまり変わらないような気もしますが。
とりあえずint型しか使用できない配列っぽいクラスを作成してみます。
intArray.class.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
class intArray implements ArrayAccess{
//配列置き
private $arr=array();
//コンストラクタ
function __Construct(){}
//値セット
function offsetSet($a,$b){
//引数が数値かチェック
if(is_int($b)){
//[]でセットした時用
if($a===null){
$this->arr[]=$b;
}else{
$this->arr[$a]=$b;
}
}else{
//数値でなければエラー
$debug=debug_backtrace();
throw new InvalidArgumentException(
'Value is not integer in line <'.$debug[0]['line'].'>'
.' type <'.gettype($b).'>'
);
}
}
//値が存在するか
function offsetExists($a){
return isset($this->arr[$a]);
}
//値を取得
function offsetGet($a){
if($this->offsetExists($a)){
return $this->arr[$a];
}else{
return null;
}
}
//値削除
function offsetUnset($a){
if($this->offsetExists($a)){
unset($this->arr[$a]);
}
}
#↓クラスのおわり
}
|
ArrayAccessインターフェイスに存在する4つのメソッドの実装を行いました。
ArrayAccess::offsetSetのときに、引数が数値かどうかチェックを行い数値でなければInvalidArgumentExceptionを出しています。
UnexpectedValueExceptionとどっちを使えばいいのかよくわからない。
このクラスは一旦newした後、普通に配列として使用できます。
intArray.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
//インスタンス
require_once('intArray.class.php');
$int=new intArray();
//代入
try{
$int[]=123;
$int['abc']=456;
$int[]='789'; //ここでInvalidArgumentExceptionが発生
}catch(InvalidArgumentException $e){
print($e->getMessage());
}
|
11行目で$int[]='789'と文字列を代入しているので、ここで例外が発生します。
それ以外は普通に配列表記でアクセスできます。
逆に$int->abcと指定しても値を取得することができません。
$int->arr['abc']としないといけないので、ここは改善の余地がありそうです。
またIteratorは実装していないので普通の配列のようにforeachでアクセスすることはできません。
以上で数値しか入れられない配列ができました。
数値以外にも列挙型なんかも簡単に作れるでしょう。
このような配列を用意しておくと、入力値チェックとかを毎回手動で行わなくて済むので簡単かもしれません。
キューもスタックもデータ型の一種で、先に入れた値が先に出てくるのがキュー、後で入れた値が先に出てくるのがスタックです。
キューが底のない筒で、スタックがコップのように底のあるものと考えるとわかりやすいでしょう。
PHPにとってはどちらも配列の一種で、array_push()、array_pop()あたりを使えば簡単に実装できます。
queue.class.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
//キュー
class Queue implements Iterator{
//配列置き
protected $arr=array();
//イテレータ
protected $loop=0;
//コンストラクタ
function __Construct(){}
//エンキュー
function enqueue($val){
array_push($this->arr,$val);
}
//デキュー
function dequeue(){
return array_shift($this->arr);
}
//イテレータ
function current(){
return $this->arr[$this->loop];
}
function next(){
$this->loop++;
}
function key(){
return $this->loop;
}
function rewind(){
$this->loop=0;
}
function valid(){
if(isset($this->arr[$this->loop])){
return true;
}else{
return false;
}
}
#↓クラスのおわり
}
//スタック
class Stack extends Queue{
//push
function push($val){
array_unshift($this->arr,$val);
}
//pop
function pop(){
return array_shift($this->arr);
}
#↓クラスのおわり
}
|
例によってエラー処理は作ってません。
キューを作ってしまえばスタックはほぼ同じなのでスタックもついでに作成しました。
入れる順番変えるだけですし。
ただこういう書き方をした場合、スタックからでもキューのメソッドを使えてしまえるのであまりよくありません。
共通のメソッドを集めた基底クラスを作ってそこからextendsしたほうがいいでしょう。
むしろどちらとしても使えるクラスが一個あれば十分という気もしないでもないですが。
使用時は普通にpush、pop、enqueue、decueするだけです。簡単。
queue.php
1
2
3
4
5
6
7
8
9
10
11
12
|
require_once('queue.class.php');
$stack=new Stack();
$stack->push(5);
$stack->push(4);
$stack->push(3);
$stack->pop();
foreach($stack as $key=>$val){
var_dump($val);
}
|
さて、PHP5.3でキュー、スタッククラスが実装されました。
http://jp.php.net/manual/ja/class.splstack.php
http://jp.php.net/manual/ja/class.splqueue.php
どう見てもSplQueue extends SplStackとしか思えない。
あと、PHPの配列って元々キューなのであまり意味がない気もしないでもない。
前回の続き。
Pear::HTTP_Session2の中を見てみると、session_set_save_handler()関数を使用しています。
http://jp.php.net/manual/ja/function.session-set-save-handler.php
このsession_set_save_handlerはsession_start()とか$_SESSIONとかの動作を自由に変更できるという素敵関数です。
早速使ってみましょう。
session_handler.class.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
|
class SessionHandler{
//セッションテーブルの定義
protected $session_table='user_session';
protected $session_dsn=array(
'server'=>'localhost'
,'database'=>'session'
,'table'=>'sessiondata'
,'user'=>'testuser'
,'pass'=>'testpass'
);
//セッション保持時間(秒)
protected $session_gc=3600;
//MySQLリソース
protected $session='';
//コンストラクタ
public function __construct(){
//セッション関連ini設定
ini_set('session.gc_probability', 50);
ini_set('session.save_handler', 'user');
//セッションハンドラ
session_set_save_handler(
array($this,'_admin_ses_open')
,array($this,'_admin_ses_close')
,array($this,'_admin_ses_read')
,array($this,'_admin_ses_write')
,array($this,'_admin_ses_destroy')
,array($this,'_admin_ses_gc')
);
//スクリプト終了時にセッション終了
register_shutdown_function('session_write_close');
//セッションスタート
session_start();
//セッションID変更
return $this->_regenerateID();
}
//セッションスタート
function _admin_ses_open($save_path, $session_name){
//DB接続
$this->session=mysql_connect(
$this->session_dsn['server']
,$this->session_dsn['user']
,$this->session_dsn['pass']
);
mysql_select_db($this->session_dsn['database'],$this->session);
return true;
}
//セッションクローズ
function _admin_ses_close(){
return TRUE;
}
//セッション読み込み
function _admin_ses_read($id){
//SQL
$sql ="SELECT * FROM ".$this->session_dsn['table'];
$sql.=" WHERE id = '".mysql_real_escape_string($id)."'";
$sql.=" AND expiry > UNIX_TIMESTAMP(now()) ";
$ret =mysql_query($sql,$this->session);
//結果返却
if($ret && (mysql_num_rows($ret) == 1)){
$tmp = mysql_fetch_assoc($ret);
return $tmp['data'];
}
return '';
}
//セッション書き込み
function _admin_ses_write($id, $session_data){
//セッション破棄時間
$expiry=time()+$this->session_gc;
//SQL
$sql ="REPLACE INTO ".$this->session_dsn['table'];
$sql.=" (id, data, expiry) ";
$sql.=" VALUES ( ";
$sql.="'".mysql_real_escape_string($id)."'";
$sql.=", '".mysql_real_escape_string($session_data)."'";
$sql.=", '".mysql_real_escape_string($expiry)."'";
$sql.=" ) ";
//書き込み
return mysql_query($sql,$this->session);
}
//セッション削除
function _admin_ses_destroy($id){
//SQL
$sql ="DELETE FROM ".$this->session_dsn['table'];
$sql.=" WHERE id = '".mysql_real_escape_string($id)."'";
//実行
return mysql_query($sql,$this->session);
}
//ガベージコレクション
function _admin_ses_gc($maxlifetime){
//本来はmaxlifetimeから有効期限を算出するが、
//今回は書き込み時に有効期限を記入しているので不要
//SQL
$sql =" DELETE FROM ".$this->session_dsn['table'];
$sql.=" WHERE expiry < UNIX_TIMESTAMP(NOW()) ";
return mysql_query($sql,$this->session);
}
//セッションIDを毎回変更
function _regenerateID(){
//古いセッションID
$session_id_old=session_id();
//初めてなら変更不要
if(!$session_id_old){
return true;
}
//新しいセッションID
session_regenerate_id(true);
$session_id_new=session_id();
return true;
//セッション変更 //5.1.0以前でsession_regenerate_idにtrueが使えない場合
//$sql =" UPDATE ".$this->session_dsn['table'];
//$sql.=" SET id='".mysql_real_escape_string($session_id_new)."'";
//$sql.=" WHERE id = '".mysql_real_escape_string($session_id_old)."'";
//return mysql_query($sql,$this->session);
}
#クラスのおわり
}
|
データベースは前回のを流用しています。
session_set_save_handlerの引数は、各状態の時に実行する関数名を与えるようになっています。
引数を配列で与えると、クラス名::ファンクション名を実行してくれます。
というわけで各関数を実装してsession_set_save_handlerを呼び出すと、セッションが始まったり終わったり値を入れたり出したりするときにそれらの各関数を実行してくれます。
SessionHandler::_regenerateIDは何をやっているかというと、SessionFixation対策です。
PHPのセッションは存在しないセッションIDがやってきた場合そのIDでセッションを始めてしまいます。
よって、攻撃者が誰かにindex.php?PHPSESSID=abcdefgというアドレスをメールで送りつけ、その人がアドレスをクリックしてログインしたりした場合、攻撃者はindex.php?PHPSESSID=abcdefgからログイン無しで内部にアクセスできてしまいます。
それを防止するため、session_regenerate_id()で毎回セッションIDを変更しています。
が、session_regenerate_idは古いセッションをそのままにしてしまいます。
古いセッションを削除するsession_regenerate_id(true)は5.1.0で搭載されたので、それ以前の場合は手動で削除しなければいけません。
ちなみにリンク先では
>このような動作をする理由がいまいち把握しきれていないのだが、これは明らかにアプリケーションの問題だろう。
とか書いてありますが、PHPの場合は仕様です。
さて、このクラスの使用方法はnewするだけです。
特に引数や返り値は要りません。
session_handler.php
1
2
3
4
5
6
7
8
|
//セッション変更
require_once('session_handler.class.php');
new SessionHandler();
//以降普通にセッションが使える
$_SESSION['a']='b';
|
以後普通にセッションがDBに書き込まれます。
汎用的に使い回し対場合はDSNなんかを引数にすればいいかもしれません。
ちなみに全くどうでもいいのですが、PHPのインスタンス変数の初期値には固定値が使えません。
protected $session_gc=60*60;とか書くと怒られます。
HTTP_Session2 0.7.2 (beta)
http://pear.php.net/package/HTTP_Session2
MDB2 2.5.0b2 (beta)
http://pear.php.net/package/MDB2
MDB2_Driver_mysqli 1.5.0b2 (beta)
http://pear.php.net/package/MDB2_Driver_mysqli
PHPのセッションはデフォルトではファイルで管理されています。
/tmpやC:\tmp等に、
sess_b84f39d3a34984b515ea715226f1b6fc
といったファイル名で保存されています。
中は単なるテキストファイルで、覗いてみると、$_SESSION['data']='hogehoge';等と書いた内容がそのまま記録されているのがわかります。
その後Cookieにb84f39d3a34984b515ea715226f1b6fcと入れて行くと、該当のファイルが読み込まれてセッションを継続することができます。
で、例えば2台のサーバでロードバランシングを行っている場合等、セッションをローカルに保存していると問題が起こります。
一回目のリクエストが片方のサーバに行き、次のリクエストがもう一方のサーバに行ってしまうと、当然そっちのサーバにはセッションファイルが無いので読み込まれません。
セッションが途切れてしまうわけです。
というわけでこのような問題の回避方法として、セッションの保存方法を書き換えることができます。
Pear::HTTP_Session2を使用してセッションをデータベースに保存してみましょう。
方針としては、2台のサーバでHTTPリクエストを処理しつつ、データベースは同じ一台を見るのでセッション自体は継続する、ということになります。
まあ今回はローカルなのでサーバも一台ですが。
HTTP_Session2はバックエンドとしてPear::DB、MDB2、Memcacheに対応していますが、Memcacheは意味がないのでMDB2を使用します。
入っていない場合は適当にpear installしましょう。
まず以下のSQLを実行し、データベースを作成しておきます。
CREATE DATABASE `session`; CREATE DATABASE CREATE TABLE sessiondata ( id varchar(32) NOT NULL, expiry int(10), data text, PRIMARY KEY (id) ); |
そうしたらHTTP_Session2を実行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
//require
require_once 'HTTP/Session2.php';
//URL、cookieでのセッションを禁止
HTTP_Session2::useTransSID(false);
HTTP_Session2::useCookies(false);
//MDB2を使用
HTTP_Session2::setContainer(
'MDB2'
,array('dsn' => 'mysql://testuser:testpass@localhost/session'
,'table' => 'sessiondata')
);
//その他設定
HTTP_Session2::start('s');
HTTP_Session2::setExpire(time() + 60); //有効期限
HTTP_Session2::setIdle(time() + 5); //アイドル時間
//期限切れなら破棄
if (HTTP_Session2::isExpired()) {
HTTP_Session2::destroy();
}
if (HTTP_Session2::isIdle()) {
HTTP_Session2::destroy();
}
//アイドル時間更新
HTTP_Session2::updateIdle();
//普通にセッションが使える
$_SESSION['sample']='あいうえお';
|
以後は普通に$_SESSION['a']='b';とか書くと、それがセッションファイルではなくDSNで指定したDBに記入されます。
セッションの挙動を書き換えることに成功しました。
簡単ですね。
万一文字化けしたらCREATE TABLEやset namesなんかで適当にどうにかしてください。
暗号化は大別すると2種類があります。
すなわち秘密鍵形式と公開鍵形式です。
復号可能か否かじゃないのか、と言われれば復号化できないのは暗号化ではなくハッシュなので違います。
今回は取り扱いが簡単な秘密鍵形式の暗号化を行ってみます。
秘密鍵形式の暗号化とは、いわゆる暗号化です。
'A'を'D'に変換するカエサル暗号を始め、暗号化のキーがわかればたちどころに復号化できるという暗号化です。
予め両者が暗号化キーを知っておく必要があるため、汎用的な暗号化としては適していません。
最初にキーを送る時点では暗号化できないので、ネットから送った時点でそれを傍受されてしまうと無駄になってしまうからです。
ただ、特定の会社間、会社と社員間など、ネットを介さずにキーを設定できる経路であれば十分に役立ちます。
まあとりあえず使ってみましょう。
PHPではMcryptやPEAR::Crypt_Blowfishパッケージがあります。
http://jp.php.net/manual/ja/book.mcrypt.php
http://pear.php.net/package/Crypt_Blowfish
今回はPHPモジュールなんで早そうなMcryptを使用してみます。
暗号化を行う関数であるところのmcrypt_encryptですが引数が多くてよくわかりません。
$cipherは暗号化形式です。
http://jp.php.net/manual/ja/mcrypt.ciphers.php
残念ながらAESは無いみたいなので3DESを使用します。
$keyはいわゆる暗号化キーです。
$dataは暗号化する文字列です。
$modeは暗号化メカニズムらしいです。
http://jp.php.net/manual/ja/mcrypt.constants.php
http://ja.wikipedia.org/wiki/%E6%9A%97%E5%8F%B7%E5%88%A9%E7%94%A8%E3%83%A2%E3%83%BC%E3%83%89
http://www.triplefalcon.com/Lexicon/Encryption-Block-Mode-1.htm
下のサイトがわかりやすいですが、EBCだと暗号化の単位が完全にブロックで分かれているため、解読されやすかったりブロック丸ごと入れ替えられたりという攻撃に会いやすいようです。
$ivはマニュアルだとなんのことだかわかりません。
例えば上記CBCを使用すると、現ブロックの暗号化を行うときに前のブロックのデータを使用して暗号化します。
その場合、最初のブロックには前のブロックがないので、そのかわりに$ivを与え、それを使用して最初の暗号化を行います。
mcript.class.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
class Mcrypt{
//暗号で使う変数
private $enc=array(
'cipher'=>MCRYPT_3DES
,'key'=>''
,'mode'=>MCRYPT_MODE_CBC
,'iv'=>''
);
//暗合する文字列
private $enc_data='';
//復号する文字列
private $dec_data='';
//コンストラクタ
public function __construct($key){
//暗号化キー
$this->enc['key']=$key;
}
//暗号化
public function encrypt($data){
//平文
$this->enc_data=$data;
//IV
if(empty($this->enc['iv'])){
$this->setIV();
}
//暗号化
$this->val=mcrypt_encrypt(
$this->enc['cipher']
,$this->enc['key']
,$this->enc_data
,$this->enc['mode']
,$this->enc['iv']
);
return $this->val;
}
//復号化
public function decrypt($data,$iv=false){
//暗号文
$this->dec_data=$data;
//IV
if($iv){
$this->enc['iv']=$iv;
}
//戻す
$this->val=mcrypt_decrypt(
$this->enc['cipher']
,$this->enc['key']
,$this->dec_data
,$this->enc['mode']
,$this->enc['iv']
);
return $this->val;
}
//IV取得
public function getIV(){
if(empty($this->enc['iv'])){
return $this->_setIV();
}else{
return $this->enc['iv'];
}
}
//IVセット
public function setIV(){
$ivsize=mcrypt_get_iv_size(
$this->enc['cipher'],$this->enc['mode']
);
$this->enc['iv']=mcrypt_create_iv($ivsize,MCRYPT_RAND);
}
#クラスのおわり}
|
mcrypt.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
//準備
require_once('mcrypt.class.php');
$crypt=new Mcrypt('hogehoge');
//暗号化するデータ
$data='hoge';
//暗号化
$enc_data=$crypt->encrypt($data);
$iv=$crypt->getIV();
//復号化
$dec_data=$crypt->decrypt($enc_data,$iv);
|
最終的に関数ふたつだけなのでわざわざクラス化するほどのものでもないのですが、これで纏まりました。
他にmcrypt_enc_get_supported_key_sizesなんかが欲しいと思えばラッピングしていくといいかもしれません。
公式がMcryptクラスを作成してくれればそれでおわりなんですがね。
3分LifeHacking:
Gmailが落ちたときに“アクセス”する方法を考える
http://www.itmedia.co.jp/bizid/articles/0902/25/news099.html
自力でIMAPすりゃいいじゃん?
ということで繋いでみた。
メールの送信はmail()やPear::Mail、そしてQdmailなんかでさくっと行うことができますが、受信についてはこれといったものがありません。
GmailはIMAPに対応しているので、PHPのIMAP関数でこりこり書いていくしかないようです。
IMAPを使用するので、最初にGmailの設定で「IMAPを許可」する必要があります。
http://ja.wikipedia.org/wiki/Internet_Message_Access_Protocol
http://jp.php.net/manual/ja/book.imap.php
gmail_imap.class.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
|
//初期設定
class gMail implements Iterator{
//メールサーバ情報
protected $params= array(
'host' => 'imap.gmail.com'
,'port' => 993
,'auth' => true
,'username' =>''
,'password' =>''
,'mailbox' =>''
);
//IMAPインスタンス
protected $imap=array();
protected $mailbox='';
//受信メール
private $loop=0;
protected $mail_data=array();
//コンストラクタ
public function __construct($user,$pass){
$this->params['username']=$user;
$this->params['password']=$pass;
//IMAP:mailboxの作成
$this->mailbox
='{'.$this->params['host'].':'.$this->params['port'].'/ssl}INBOX';
//インスタンス
$this->imap=imap_open(
$this->mailbox,$this->params['username'],$this->params['password']
);
}
//デストラクタ
public function __destruct(){
imap_close($this->imap);
}
//全件を取得
public function getAll(){
$count=$this->count();
for($i=1;$i<$count+1;$i++){
$this->mail_data[]=$this->get($i);
}
return $this->mail_data;
}
//件数を取得
public function count(){
$this->count=imap_num_msg($this->imap);
return $this->count;
}
//受信
public function get($num){
$this->tmp=false;
//ヘッダ
$this->tmp['head']=imap_headerinfo($this->imap,$num);
$this->tmp['head']->uid=imap_uid($this->imap,$num);
//本文
$this->tmp['body']=imap_body($this->imap,$num);
//デコード
$tmp=$this->_decode($this->tmp);
//必要なぶんだけ返却
$ret['to']=$tmp['head']->toaddress;
$ret['to_address']=$tmp['to_address'];
$ret['from']=$tmp['from'];
$ret['from_address']=$tmp['from_address'];
$ret['date']=$tmp['head']->date;
$ret['subject']=$tmp['subject_decode'];
$ret['body_decode']=$tmp['body_decode'];
return $ret;
}
//デコード
private function _decode($mail_data){
//件名
$mail_data['subject_decode']
=mb_decode_mimeheader($mail_data['head']->subject);
//宛先
if(isset($mail_data['head']->to[0]->personal)){
$mail_data['to']=mb_decode_mimeheader(
$mail_data['head']->to[0]->personal
);
}else{
$mail_data['to']=$mail_data['head']->toaddress;
}
$mail_data['to_address']
=$mail_data['head']->to[0]->mailbox
.'@'.$mail_data['head']->to[0]->host;
//差出人
if(isset($mail_data['head']->from[0]->personal)){
$mail_data['from']=mb_decode_mimeheader(
$mail_data['head']->from[0]->personal
);
}else{
$mail_data['from']=$mail_data['head']->fromaddress;
}
$mail_data['from_address']
=$mail_data['head']->from[0]->mailbox
.'@'.$mail_data['head']->from[0]->host;
//本文
if(isset($mail_data['body'])){
$mail_data['body_decode']=mb_convert_encoding(
$mail_data['body'],'UTF-8','JIS'
);
}
return $mail_data;
}
//イテレータ
function current(){
return $this->mail_data[$this->loop];
}
function next(){
$this->loop++;
}
function key(){
return $this->loop;
}
function rewind(){
$this->loop=0;
}
function valid(){
if(isset($this->mail_data[$this->loop])){
return true;
}else{
return false;
}
}
#↓クラスのおわり
}
|
gmail_imap.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
//初期設定
header("Content-type: text/html; charset=utf-8");
require_once('./gmail_imap.class.php');
//ユーザ情報
$mail_user='メアド';
$mail_pass='パスワード';
//アカウント
$gmail = new gMail($mail_user,$mail_pass);
//一件取得
$ret=$gmail->get('8');
//全件取得
$ret=$gmail->getAll();
foreach($ret as $key=>$val){
var_dump($key,$val);
}
|
何気にメール一覧の取得が難しいです。
imap_headersは、ヘッダ情報が文字列で返ってくるという謎仕様です。
string(83) " U 4)25-Feb-2009 =?ISO-2022-JP?B?GyRC =?ISO-2022-JP?B?MTUbJEI8f (10362 chars)"
こんなの送ってこられてもどうしようもないのですが…
他に全ヘッダを取得という関数が無いようなので、全メールについてimap_fetchheaderなりimap_headerinfoなりをせざるを得ないとかいうことになっています。
どうせなら一緒でいいやということでついでにimap_bodyも実行して全メール取得にしてしまいました。
これでメールの件数を取得、特定のメールを受信、全メール受信のスクリプトができました。
mb_detect_encodingがJISをまったくチェックしてくれないのでJISべた書きだったり、そのせいで標準以外のエンコードがされているメールが読めなかったり、そもそもマルチパート対策が全くなされていなかったりとメーラとしては弱いです。
またIMAPにはフォルダ移動や削除などサーバ側のメールを操作する機能もたくさんあるのですが、現在は受信トレイのメールを受信する以外の機能はありません。
そこらへんは必要になったら加えていけば便利になっていくことでしょう。
まあ根本的に外部メーラ使った方が遙かに手っ取り早いというのは秘密だ。