PDFを作成するには本来AdobeAcrobatというソフトが必要です。
イラストを駆使したような美しいデザインのPDFを作成するには購入したほうがいいでしょうが、簡単な内容のPDFならPHPで作成することもできます。
PHPには元々PDFlibを利用したPDF関数があり、またPear::File_PDFパッケージもありますが、何故かFPDFというライブラリがよく使われています。
現時点で最新のFPDF Japanese version 1.6を使用してみます。
使用法は簡単で、ダウンロードしたFPDFフォルダを何処かに突っ込んでfpdf.phpをreuireするだけです。
include_pathの設定も必要ありません。
pdf.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
|
//FPDF
require_once('fpdf.php');
$pdf= new FPDF();
//所有者、タイトル等
$pdf->SetAuthor('Author');
$pdf->SetTitle('Title');
//表示モード
$pdf->SetDisplayMode('real','default');
//全体の設定
$pdf->SetFont('times','',20);
$pdf->SetTextColor(255,100,100);
$pdf->SetDrawColor(100,255,100);
$pdf->SetFillColor(100,100,255);
$pdf->SetFontSize(10);
//ページ作成
$pdf->AddPage('P');
//テキスト表示
$pdf->SetXY(10,50);
$pdf->Write(5,'text');
//画像を表示
$pdf->Image('a2.jpg',10,20,33,0,'jpg','http://example.com/');
//リンクを作成
$pdf->Rect(30, 30, 30, 30, 'DF');
$pdf->Link(30, 30, 30, 30, 'http://lexample.com/');
//表
$pdf->SetTextColor(50,60,255);
$pdf->SetFontSize(20);
$pdf->SetXY(50,20);
$pdf->Cell(100,10,'sample1','LRT',1,'C',0);
$pdf->SetX(50);
$pdf->Cell(100,10,'sample2',1,1,'',0);
//表示の実行
$pdf->Output();
|
後はこのように、要素を一つずつ順番に書き出していくだけです。
他にも多様なメソッド、引数がありますが、そこらへんは日本語ヘルプも存在するのでそれを見るとよいでしょう。
現時点では1.52用しかありませんが、内容は概ね同じなので大丈夫です。
FPDFには大きな問題点が二つほど存在します。
ひとつは、FPDFは素の状態では日本語に対応していないことです。
FPDF::WriteやCellに日本語を突っ込むと文字化けしてしまいます。
日本語対応のMBFPDFを使用してみましょう。
FPDFのサイトトップから「What languages can I use?」を選択、mbfpdfをダウンロードします。
FPDFと同じフォルダに置いたらmbfpdf.phpをrequireするだけです。
日本語フォントを使用できる以外はfpdf.phpとほぼ同じです。
サンプルとしてexja.phpが入っているので見てみるとよいでしょう。
mbpdf.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
|
//MBFPDF
require_once('mbfpdf.php');
$pdf= new MBFPDF();
//フォントの追加
$pdf->AddMBFont(MINCHO ,'SJIS');
$pdf->SetFont(MINCHO,'',20);
//全体の設定
$pdf->SetTextColor(255,100,100);
$pdf->SetFontSize(20);
//ページ作成
$pdf->AddPage('P');
//テキスト表示
$text=mb_convert_encoding('あいうえお','SJIS','UTF-8');
$pdf->SetXY(50,10);
$pdf->Write(10,$text);
//表示の実行
$pdf->Output();
|
ひとつだけ準備が必要で、MBFPDF::AddMBFontで使用するフォントを通知する必要があります。
使用できるフォントはmbfpdf.phpの頭に書いてありますが、現在はMS明朝とMSゴシックの5種類だけのようです。
また、$pdfに渡すフォントはSJISである必要があります。
EUC-JPは一部問題があり、UTF-8には対応していません。
無事にPDFファイルが作成されました。めでたし。
さてもう一つの問題点ですが、なんといっても面倒臭いこと。
座標を毎度毎度指定しないといけないので、テキスト部分だけを差し替えるような内容なら簡単にできますが、レイアウト自体を変更するようなアプリを書くには大掛かりな仕掛けが必要になります。
YUIなりFCKeditorなりでレイアウト作成させてHTTPRequestから座標計算して各メソッド割り振ってうひゃあ面倒い。
前回の続き。
こんな有用なラッパ関数誰かが作っていないはずがない、ということで当然既にPearになっています。
Pear::Net_FTP 1.3.7 (stable)
http://pear.php.net/manual/ja/package.networking.net-ftp.php
使用方法は前回のftpClassとほぼ同じです。
ftp.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
//接続用設定
$ftp_server='example.com';
$ftp_user='user';
$ftp_pass='password';
//FTP接続
require_once('Net/FTP.php');
$ftp=new Net_FTP($ftp_server);
$ftp->connect();
$ftp->login($ftp_user,$ftp_pass);
//ダウンロード
$ftp->get('hoge.txt','local.txt');
//アップロード
$ftp->put('local.txt','hogehoge.txt',true);
//削除
$ftp->rm('hoge.txt');
|
他にもカレントディレクトリを移動するcd、ディレクトリ一覧を取得するls等Linux的なコマンドが幾つか用意されています。
ディレクトリ丸ごとアップロード等もできて便利。
ただ、何故かテキストやストリームとして受信する手段がありません。
$text=$ftp->get('hoge.txt');
のような使用法ができず、必ずファイルとして保存しなければなりません。
実はこれPear::Net_FTPだけではなく、大元のFTP関数にも存在しないんですよね。
何故かファイルかファイルポインタにしか保存できません。
まあ元々FTPはファイル転送用のプロトコルですから当然と言えば当然ではありますが。
PHP5以降なら'php://temp'というダミーのポインタを利用することでテキストとして受信することはできますが、そもそもPHP5だったらURLラッパでアクセスした方が手っ取り早いという。
ブログとWiki全盛のこの時代ですが、旧来のホームページを作成する場合にはFTPは欠かせません。
まあこれもブラウザ上から行うことが可能ですが、当然PHPにもFTP接続を行う関数が用意されています。
ftp.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
//FTP
$ftp_server='example.com';
$ftp_user='user';
$ftp_pass='password';
//ダウンロード
$ftp_file='/download.txt';
$ftp_url='ftp://'.$ftp_user.':'.$ftp_pass.'@'.$ftp_server.$ftp_file;
$f = file_get_contents($ftp_url);
//アップロード
$ftp_file='/upload.txt';
$str='hogehoge';
//デフォルトでは上書きが禁止されているので許可する
$opts = array('ftp' => array('overwrite' => true));
$context = stream_context_create($opts);
$ftp_url='ftp://'.$ftp_user.':'.$ftp_pass.'@'.$ftp_server.$ftp_file;
$f=file_put_contents($ftp_url,$str,LOCK_EX,$context);
|
FTP関数は?
実は関数を使わなくても、簡単な方法でFTPアクセスを行うことができます。
上記の'ftp://'から始まるURLはURLラッパーと呼ばれ、FTPにアクセスする標準的な実装です。
別にPHP固有のものでも何でもなく、実際作成された$ftp_urlをブラウザに入力すると普通にファイル内容を取得することができます。
そのURLに対しfile_get_contentsすればダウンロードできますし、file_put_contentsすればアップロードすることができます。
ということになっているのですが、手元の適当なサーバに試してみたところ、file_put_contentsにLOCK_EXを掛けた場合何故かアップロードに失敗するという事態に。
バージョンは対応しているはずなのにぃ。
普通に
$fp=fopen($ftp_url,'w',false,$context);
flock($fp,LOCK_EX);
fwrite($fp,$str);
fclose($fp);
としてみたらflockがfalseになった。
残念ながらflockに非対応のサーバだったようです。
さてこのFTPラッパー、表現的にはわかりやすいしPHP5ではmkdirからunlinkまで何でもできるのですが、プログラム組む際には少々扱いにくいです。
例によってクラスに突っ込んでみましょう。
ftp.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
|
class ftpClass{
private $ftp=array();
private $fc='';
private $context='';
//コンストラクタ
function __construct($ftp_server,$ftp_user,$ftp_pass){
$this->ftp['ftp_server']=$ftp_server;
$this->ftp['ftp_user']=$ftp_user;
$this->ftp['ftp_pass']=$ftp_pass;
$opts = array('ftp' => array('overwrite' => true));
$this->context = stream_context_create($opts);
}
//ステータス
function status($ftp_file){
$this->ftp['ftp_file']=$ftp_file;
$this->_ftp_url();
$this->fc=stat($this->ftp['ftp_url']);
if(!$this->fc){$this->_error('取得失敗');}
return $this->fc;
}
//削除
function delete($ftp_file){
$this->ftp['ftp_file']=$ftp_file;
$this->_ftp_url();
$this->fc=unlink($this->ftp['ftp_url']);
if(!$this->fc){$this->_error('削除失敗');}
return true;
}
//アップロード
function put($ftp_file,$str){
$this->ftp['ftp_file']=$ftp_file;
$this->_ftp_url();
$fp=fopen($this->ftp['ftp_url'],'w',false,$this->context);
if(!$fp){$this->_error('ファイルオープン失敗');}
$ret=flock($fp,LOCK_EX);
//if(!$ret){$this->_error('ファイルロック失敗');}
$ret=fwrite($fp,$str);
if(!$ret){$this->_error('ファイル書き込み失敗');}
fclose($fp);
return true;
}
//ダウンロード
function get($ftp_file){
$this->ftp['ftp_file']=$ftp_file;
$this->_ftp_url();
$this->fc=file_get_contents($this->ftp['ftp_url']);
if(!$this->fc){$this->_error('取得失敗');}
$this->fc=mb_convert_encoding(
$this->fc
,'UTF-8'
,mb_detect_encoding(
$this->fc,'UTF-8,eucjp-win,sjis-win'
)
);
return $this->fc;
}
//URLを作成
function _ftp_url(){
if(
!$this->ftp['ftp_user']
|| !$this->ftp['ftp_pass']
|| !$this->ftp['ftp_server']
|| !$this->ftp['ftp_file']
){
$this->_error('引数不足');
}
if($this->ftp['ftp_file'][0]!=='/'){
$slash='/';
}else{
$slash='';
}
$this->ftp['ftp_url']=
'ftp://'
.$this->ftp['ftp_user']
.':'
.$this->ftp['ftp_pass']
.'@'
.$this->ftp['ftp_server']
.$slash
.$this->ftp['ftp_file'];
}
//えらー
function _error($str=''){
print("<html><pre>");var_dump($str,$this);die();
}
#↓クラスのおわり}
|
思ったより長くなってしまった。
$this->ftp['ftp_file']以外は先に作っておいた方が負荷が軽くなりそうだとか、file_get_contentsした後はクラス内に保持しておくべきとか、エラー処理が適当すぎるだろとか色々改善点もありますが、まあいいや。
使用する場合はこのように。
ftp.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
|
//接続用設定
$ftp_server='example.com';
$ftp_user='user';
$ftp_pass='password';
//FTP
require_once('./ftp.class.php');
$ftp=new ftpClass($ftp_server,$ftp_user,$ftp_pass);
//ダウンロード
$filename='sample1.txt';
$ret=$ftp->get($filename);
//アップロード
$filename='sample2.txt';
$str='テスト';
$ret=$ftp->put($filename,$str);
//ステータス
$ret=$ftp->status($filename);
//削除
$filename='sample2.txt';
$ret=$ftp->delete($filename);
|
今回はサーバがflock使えなかったのでエラー部分をコメントアウトしてあります。
これでFTP関数とか'ftp://'とかの面倒な処理を書かなくて済むようになりました。
いつものようにnewしてファイル名を突っ込むだけで簡単にFTPアクセスを行うことができます。
関数内で呼び出すと、その関数の返り値が使用されているかどうかを調べてくれるという、どういう使い道があるのかさっぱりわからない関数。
使ってみましょう。
runkit_return_value_used.php
1
2
3
4
5
6
7
8
9
10
11
12
13
|
//関数
function a(){
var_dump(runkit_return_value_used());
}
a();
$a=a();
echo(a());
print(a());
print_r(a());
var_dump(a());
var_export(a());
|
結果
| bool(false) bool(true) bool(true) bool(true) bool(true) bool(true) NULL bool(true) NULL |
最初のNULLはvar_dump()の表示、その次のtrueと最後のNULLはvar_export()によるものです。
echo()やprint()ではきちんとtrueが表示されるのに、var_dump()では何故かNULL。
var_export()に至っては二つも表示されてイミフ。
まあこんな関数誰も使わないと思うからいいけどさ。
前回の続き。
いよいよPDOのプリペアドステートメントを使用してみます。
pdo.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
//PDO
define('DB_DSN','mysql:host=localhost;dbname=testdb');
define('DB_USERNAME','testuser');
define('DB_PASSWORD','testpass');
$db = new PDO(DB_DSN,DB_USERNAME,DB_PASSWORD);
//引数
$prepare_id=isset($_REQUEST['id'])?$_REQUEST['id']:1;
//select文
$sql='SELECT * FROM books where id = ? ';
//プリペアドステートメント
$prepare =$db->prepare($sql);
$prepare->bindValue(1,$prepare_id,PDO::PARAM_INT);
//実行
$prepare->execute();
$ret=$prepare->fetchAll();
|
まずSQLを作成する際、引数を入力するところを?にします。
次に$db->prepareとすることでプリペアドステートメントを準備し、bindValueで実際の引数を当て嵌めます。
さて本プログラムのポイントは、$prepare_idを一切エスケープしていないことです。
プリペアドステートメントを使用した場合、DB側でエスケープを行ってくれます
このエスケープは非常に強力で、まず滅多なことでは突破されません(それでも時折話題になる)
bindValueの第一引数の1は、一番目の?に対して変数を割り当てるという意味です。
SQL文に?を複数書いた場合は、bindValueを複数回実行してそれぞれの?に変数を割り当てる必要があります。
また、bindValueにおいて指定しているPDO::PARAM_INT等の定数は、パラメータの型を表します。
が、必須ではありませんし、指定して別の型を突っ込んでもエラーになるわけでもないので、いまいち存在理由がわかりません。
さてプリペアドステートメントは、検索条件が動的な場合の検索には向いていません。
先にSQLを準備して後からその中に変数を突っ込むという構造上、検索条件が変動する場合のSQL構築が面倒なのです。
まあとりあえず作ってみましょう。
pdo2.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
|
//PDO
define('DB_DSN','mysql:host=localhost;dbname=testdb');
define('DB_USERNAME','testuser');
define('DB_PASSWORD','testpass');
$db = new PDO(DB_DSN,DB_USERNAME,DB_PASSWORD);
//プリペアドステートメント
$sql='SELECT * FROM books';
if(
isset($_REQUEST['id'])
|| isset($_REQUEST['name'])
|| isset($_REQUEST['author'])
){
$sql.=' where ';
if(isset($_REQUEST['id'])) {$sql.=' id = :id and ';}
if(isset($_REQUEST['name'])) {$sql.=' name like :name and ';}
if(isset($_REQUEST['author'])){$sql.=' author like :author and ';}
$sql=substr($sql,0,-4);
}
//バインド
$prepare =$db->prepare($sql);
if(isset($_REQUEST['id'])) {
$prepare->bindValue(
':id', $_REQUEST['id'], PDO::PARAM_INT
);
}
if(isset($_REQUEST['name'])) {
$prepare->bindValue(
':name', '%'.$_REQUEST['name'].'%', PDO::PARAM_STR
);
}
if(isset($_REQUEST['author'])){
$prepare->bindValue(
':author', '%'.$_REQUEST['author'].'%', PDO::PARAM_STR
);
}
//実行
$prepare->execute();
$ret=$prepare->fetchAll();
|
SQLを構築する部分とパラメータをバインドする部分、2箇所に対して条件分岐が必要となります。
少々見通しの悪いプログラムになってしまいました。
さて、今回は?ではなくid = :idというふうに指定しています。
その場合、bindValue(':id', 変数)と引数を指定することにより、':id'に対して変数を割り当てることができます。
=ではなくlikeで検索したい場合は、変数を%で挟めば大丈夫です。
以上がPDOの基本的な使い方となります。
あとは適当にマニュアル読んでください。
ところで昔の記事では、「無理に使う必要はないかもしれません」なんてことを書いているわけですが、忘れてください。
「可能な限り使用しなければなりません」に修正します。
XAMPPはMySQLですが、他に有名なフリーのDBとしてPostgreSQLが存在します。
商用としては高性能なOracleが、Windows用にはMSSQL等があります。
PHPはデフォルトで、それぞれのDBMSに対して専用の接続関数を用意しています。
MySQLならmysql_connect()、PostgreSQLならpg_connect()、Oracleならoci_connect()といったかんじです。
しかしこれではDBが変更になった際など、全てのプログラムからDB関係の関数を全て探し出して変更しなければなりません。
そんなことをすると恐ろしい手間がかかるので、それを避けるためにDataAccessObjectが編み出されました。
まあ簡単に言うとDBにアクセスするクラスを作成し、コンストラクタに'mysql'なり'postgres'なりの文字を与えれば、あとの接続やSQLの実行といった部分は全部クラス内でやってくれますよということです。
これによって、例えばDBが変更になった場合でも、最初の引数だけ変更すればOKとなるわけです。
更にDBのインスタンス化を行う部分を一つのファイルに纏めてしまえば、DBが変更になっても変更箇所はたった一箇所で済むようになります。
まあ実際はそううまくいくわけもなく、ベンダ固有のSQL実装など修正部分が出てくるのは避けられませんが、それでも素のDB接続関数を用いるよりは遙かに簡単になります。
PHPでDataAccessObjectパターンを行う手段は幾つか存在します。
有名どころではPear::MDB2やPear::DB_DataObjectがありますが、PHP5でようやくモジュールとしてPDOが登場しました。
http://jp.php.net/manual/ja/book.pdo.php
Pearと違ってCで書かれているので高速性が期待できます。
早速使ってみましょう。
Linuxではインストールが必要ですが、Windowsではphp.iniのコメントを外すだけです。
http://jp.php.net/manual/ja/pdo.installation.php
まずはDBの準備です。
phpMyAdminから編集するなりSQLを打つなりでテスト用DBとテーブル、ユーザを作成します。
|
CREATE DATABASE `testdb` DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci; |
できました。
booksテーブルは別にこんなに立派である必要はありませんが、昔作ったのを流用しました。
とりあえず適当に一件登録しておいてください。
次はPHPの用意です。
pdo.php
1
2
3
4
5
6
7
8
9
10
11
12
13
|
//PDO
define('DB_DSN','mysql:host=localhost;dbname=testdb');
define('DB_USERNAME','testuser');
define('DB_PASSWORD','testpass');
$db = new PDO(DB_DSN,DB_USERNAME,DB_PASSWORD);
$select=$db->query('SELECT * FROM books');
while($loop=$select->fetch(PDO::FETCH_ASSOC)){
$ret[]=$loop;
}
|
簡単に$dbにPDOオブジェクトが作成されました。
後はこれに対しSQLを書いていくだけですが、PDOを経由しているので実行する時にDBの種類を考える必要がありません。
もしDBが変更になった場合、最初のDB_DSNを、
define('DB_DSN','pgsql:host=localhost port=8888 dbname=testdb');
等と変更するだけです。
移植性、独立性の高いコードを書くことが出来ました。
さっそく実行。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
array(11) {
["id"]=>
string(1) "1"
["name"]=>
string(5) "?????"
["author"]=>
string(2) "??"
["publisher"]=>
string(6) "??????"
["price"]=>
string(4) "1050"
["isbn"]=>
string(14) "978-4883810451"
["date"]=>
string(10) "2006-10-01"
["comment"]=>
string(7) "????(?)"
["created_at"]=>
string(19) "2008-08-15 11:45:49"
["updated_at"]=>
NULL
["deleted_at"]=>
NULL
|
お?
見事に文字化けしています。
PHPもUTF8、MySQLもUTF8、ブラウザもUTF8なのに何故?
実はこれMySQLからPHPへの出力時に変な文字コードで出力しているのが原因です。
MySQLからの出力は、テーブルの文字コードそのままではなく、設定ファイルに従って変換された上で出力されます。
このデフォルト値がlatin1なる日本語の使えないコードであるため、日本語が全て文字化けしてしまうのです。
というわけで
$db->query('SET NAMES utf8');
の一文を入れるだけで直るのですが、セキュリティ上の理由から勧められていません。
http://nonn-et-twk.net/twk/why-set-names-in-php-is-bad
かわりにmysql_set_charset()を使えと書いてありますが、PDOでどうしろと。
MySQL側で動作を修正した場合、MySQL全体に影響が及ぶので、他のシステムが存在した場合そちらが動作不具合を起こす可能性もあります。
http://dev.mysql.com/doc/refman/4.1/ja/charset-connection.html
まあ今回はローカルで特に問題ないのでmy.cnfを修正しました。
マニュアルには
default-character-set-name=character_set_name
とか書いてありますが、正しくは
default-character-set = utf8
です。
修正方法は、my.cnf(Windows上では短縮ダイアルに見える)の適当なところに、
[mysqld]
default-character-set = utf8
skip-character-set-client-handshake
という文字を突っ込むだけです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
array(11) {
["id"]=>
string(1) "1"
["name"]=>
string(16) "恋空〈上〉"
["author"]=>
string(7) "美嘉"
["publisher"]=>
string(19) "スターツ出版"
["price"]=>
string(1) "0"
["isbn"]=>
string(15) "978-4883810451"
["date"]=>
string(10) "2006-10-01"
["comment"]=>
string(18) "スイーツ(笑)"
["created_at"]=>
string(19) "2008-01-01 01:01:01"
["updated_at"]=>
NULL
["deleted_at"]=>
NULL
|
めでたし。
さて、私はあなたを犯罪者にすることができます。
先日児童ポルノサイトへリンクを張った人が逮捕されました。
http://slashdot.jp/yro/article.pl?sid=09/07/08/0414236
http://www.j-cast.com/2007/05/09007471.html
http://www.yomiuri.co.jp/national/news/20090708-OYT1T00078.htm
http://news.livedoor.com/article/detail/4240457/
http://blog.livedoor.jp/dankogai/archives/51232218.html
他の記事にはまったくコメントが付いていないのにこの記事にはコメントがありますが、
http://yuubiseiharukana.blog.shinobi.jp/Entry/132/
の記事を読もうとした時点で自動的に貴方の足跡でここにコメントが書き込まれるようになっています。
今回はなんでもないコメントですが、これを妖しいサイトのアドレスを書き込むようにすることは非常に簡単です。
そうすると児童ポルノサイトへリンクを張った罪で貴方が逮捕されます。
現状のように私のブログに張ってしまえば私も逮捕されるので自爆ですが、他所の掲示板やブログに矛先を向けるのも難しくありません。
したらばBBSのように外部からの投稿を弾くようになっている場合は手間がかかりますが、回避方法もいろいろ存在します。
今回使用したのもまったくたいした技術ではなく、多少JavaScriptが使えるなら誰でも理解できる内容です。
ネカフェからどっかの緩いXSS脆弱性のある掲示板にでも投稿スクリプトを貼り付けてしまえば、作者を追うことがほぼ不可能な犯罪者作成装置が完成することでしょう。
つうか忍者ブログって外部からのPOST素通しかよ。
あぶねーなおい。
ちなみに名前や本文の後に付いてる変な数字は多重投稿チェックをスルーする為のものです。
dBug March 22, 2007
http://dbug.ospinto.com/
簡易デバッグにはvar_dump()を使用することが多いと思いますが、配列の中身が巨大な場合等あまり見やすいとは言えません。
といって本格的なデバッグを行うほどでもない、そんなときに便利なのがdBugです。
なんといっても使い方が超簡単。
requireしてnewするだけです。
index.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
require_once('../dBug.php');
class testclass{var $hoge;function hoge(){return $this->hoge;}}
$test1=array('hoge','xx'=>'yy',array(1,2,'2'=>'3'));
$test2=new testclass();
$test3=imagecreatefromjpeg('index.jpg');
$test4='<xml>xmlxml<a>aa</a><b c="cc">bb</b></xml>';
$dbug=new dBug($test1);
$dbug=new dBug($test2);
$dbug=new dBug($test3);
$dbug=new dBug($test4,'xml');
|
デフォルトで配列、オブジェクト、MySQLやGD等のリソース型に対応しています。
また、何気に知られていませんが、第二引数に'xml'を指定するとXMLのパースも行えます。
実行結果は以下のようになります。
| $test1 (array) | |||||||||
| 0 | hoge | ||||||||
| xx | yy | ||||||||
| 1 |
|
||||||||
| $test2 (object) | |
| hoge | [empty string] |
| hoge | [function] |
| $test3 (resource) | ||||||||
|
||||||||
| $test4,'xml' (xml document) | |||||||||||||||||||||||||||||||||||||||||
| xmlRoot |
|
||||||||||||||||||||||||||||||||||||||||
邪魔な要素は閉じたりすることもできて便利です。
てか、そんなことよりこんなJavaScriptを普通に受け入れる忍者ブログが恐ろしい。
とりあえずデフォルトでは入っていないのでpeclコマンドでインストールする必要があります。
ただXAMPPの場合最初から入っているので、php.iniのextension=php_runkit.dllの行のコメントを外すだけです。
本来はRunkit_Sandboxクラスを利用したデバッグ等に威力を発揮するのでしょうが、使い方がよくわかりません。
http://jp2.php.net/manual/ja/runkit.sandbox.php
さて、このモジュールには他にはない強力なメソッドが含まれています。
なんといっても定数の定義を覆すrunkit_constant_redefineが反則過ぎる。
runkit_constant_redefine.php
1
2
3
4
5
6
7
8
9
10
11
12
13
|
//定数定義
define('HOGE','aaa');
print(HOGE);
//定数再定義
runkit_constant_redefine('HOGE','bbb');
print(HOGE);
//定数削除
runkit_constant_remove('HOGE');
print(HOGE);
|
5行目で'aaa'、9行目で'bbb'が表示され、13行目で定数未定義のNoticeが発生します。
定数って何?
ちなみに、通常のdefineは再定義しようとするとエラーになりますが、runkit_constant_redefineは未定義の定数に対して使用するとエラーになります。
クラスの拡張はextendsやimplementsを使うことになっていますが、実行時に元のクラスを直接いじくってしまうrunkit_method関数群が用意されています。
extendsと何が違うかというと、クラス自体を変更するのでprivateなインスタンス変数にもアクセスできてしまいます。
runkit_method.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
//クラス
class testClass{
private $a=1;
}
//通常のextends
class testClass2 extends testClass{
private $a=2;
function add2($a){$this->a=$a;}
}
$test2=new testClass2();
$test2->add2(5);
print("<html><pre>");var_dump($test2);
//メソッド追加
runkit_method_add('testClass','add','$a','$this->a=$a;');
//実行
$test=new testClass();
$test->add(5);
var_dump($test);die();
|
実行結果は以下のようになります。
| object(testClass2)#1 (2) { ["a:private"]=> int(1) ["a"]=> int(5) } object(testClass)#2 (1) { ["a:private"]=> int(5) } |
Javaなんかだとprivateな$aは引き継がれなかったような気がしますが、PHPではprivateな$aと、extendsしたクラスで再定義した$aは別物として扱われます。
作ろうと思えば以下のようなクラスが作れてしまうのでキモい。
| ["a:private"]=> int(3) ["a:private"]=> int(2) ["a:private"]=> int(1) |
後者の場合testClass自体を書き換えてしまうので、testClass::addはtestClass::$aにしっかりとアクセスしてくれます。
他にも基底クラスを無理矢理extendsされたクラスにしてしまうrunkit_class_adoptといった楽しい関数が揃っています。
runkit_function_redefineやrunkit_lint_fileを試してみたところ、手元のXAMPPではApacheを巻き込んで落ちてくれやがりました。何故。
しかしこのrunkit関数、定数やクラスという定義をぶち壊してしまう強力過ぎにも程がある関数ばかりなので、使用は控えておいたほうがいいと思います、というか使うべきではないような。
セッションの仕組みは単純なキーの受け渡しとなっています。
session_startを呼んだ時点で、リクエストにセッションIDが含まれていたらそのIDを使用、セッションIDが無ければ新しく適当なセッションIDを作成します。
セッションIDは基本的にcookieに突っ込まれ、ブラウザへ返されます。
$_SESSIONに突っ込んだ値は、サーバ上のどこかに'sess_セッションID'のような名前のファイルとして保存されます。
再度アクセスがあり、そのときにリクエストにセッションIDがあればファイルに保存されたセッションファイルを読み出す、という仕組みです。
ここで問題となるのが、「リクエストにセッションIDが含まれていたらそのIDを使用」の部分です。
存在しないセッションIDを無理矢理くっつけた上でリクエストを出した場合、PHPはそのセッションをあっさりと受け入れてセッションを継続してしまいます。
これがセッション固定攻撃の原因です。
セッションは基本的にcookieに突っ込まれるのですが、携帯電話等cookieが保てないブラウザに対応するために、URLにセッションIDを含むことも出来ます。
session.use_trans_sidやuse_cookies等のディレクティブで設定できます。
デフォルトでは無効ですが、携帯電話用コンテンツを表示するために有効にしていた場合、
http://example.com/index.php?PHPSESSID=123456789abcdef
というリンクを踏んでしまったら、誰がアクセスしてもセッションIDが123456789abcdefだと判断されます。
123456789abcdefというセッションファイルが見つからなかった場合、PHPは新しくそのセッションIDでセッションを作成してしまいます。
これを利用すると、他者にセッションIDを付加したURLでアクセスさせ、自分でも同じURLでアクセスすると他者のセッションの中身を覗き見ることが可能になります。
session_fix.php
1
2
3
4
5
6
7
8
9
10
11
|
//セッション固定攻撃のシミュ
ini_set('session.use_trans_sid',1);
ini_set('session.use_cookies',0);
ini_set('session.use_only_cookies',0);
session_start();
if(isset($_REQUEST['param'])){$_SESSION['param']=$_REQUEST['param'];}
var_dump(session_id(),$_SESSION);
|
session_fix.php?param=hoge&PHPSESSID=123456789abcdef
でアクセスすると、セッションIDが'123456789abcdef'になっているのが確認できます。
ここで、別のPCから
session_fix.php?PHPSESSID=123456789abcdef
でアクセスすると、他人には見えてはならないはずの$_SESSION['param']の値が見えてしまいます。
これが例えばログイン情報をセッションで管理している会員サイトのようなシステムで起これば、自分はログインしていないのに他者のログインしたセッションを利用して会員サイトにアクセスできてしまいます。
直接$_SESSIONの中は覗けないので具体的なIDやパスワードが直接漏れるようなことが無いのが唯一の救いですが、パスワード再発行フロー等を利用してアカウントを乗っ取ったり、ディレクトリトラバーサル等別のセキュリティホールと組み合わせて個人情報を抜き取ったりといった攻撃も可能であるため、セッション固定攻撃自体を排除するようにしなければなりません。
このような場合のためにセッションIDを再発行することの出来るsession_regenerate_idが用意されています。
session_regenerate_idで新しいセッションを発行できますが、古い内容があった場合は削除せずにそのまま残してしまいます。
プログラム中でsession_regenerate_idを呼ぶとその場でセッションIDが変更されます。
アクセスしたユーザはその後は変更後のセッションIDでアクセスすることになるのですが、その前にアクセスしていたセッションIDが'123456789abcdef'の情報はそのままになっています。
その場合攻撃者は、古い内容ではありますが'123456789abcdef'のセッションの内容を取得できてしまいます。
PHP5.1以降ではsession_regenerate_id(true)で古いセッションを削除できるようになりました。
が、それ以前のバージョンでは、それまでのセッションを手動で削除しなければなりません。
session_destroy.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
//PHPSSID有効化
ini_set('session.use_trans_sid',1);
ini_set('session.use_cookies',0);
ini_set('session.use_only_cookies',0);
//古いセッション削除
session_start();
$old_session=$_SESSION;
session_destroy();
//新しいセッション
session_start();
session_regenerate_id();
$_SESSION=$old_session;
|
session_destroy()で現在のセッションを全て削除することが出来ます。
セッション内容が消えてしまっては困るので、一旦変数に確保します。
ただしセッションID自体はそのままになっているので、session_regenerate_id()で変更します。
最後に確保しておいたセッション内容を新しいセッションに書き込めば、古いセッションを削除しつつ新しいセッションに移行することが出来ます。
他にセッションファイルを物理的に削除する方法などもありますが、session_destroy()を使用した方が手っ取り早いと思います。
まあsession_set_save_handler()でセッション乗っ取るのが一番なのでしょうけど。