XAMPPには最初からOpenSSLが付属しているので、簡単にオレオレ証明書を作成できます。
SSLで暗号化通信をするためには暗号化キーの受け渡しが必要ですが、その暗号化キー自体が本当に正しいものなのかどうかは、それ自身だけではわかりません。
サーバが正しい鍵を送信したつもりでも、途中で不正な鍵にすり替えられてクライアントに届き、それを使って暗号化したつもりが覗かれ放題、という可能性があるからです。
それを防ぐため、サーバではベリサインなどの"信用できる"認証局から証明書を発行してもらい、それを自分の証明書に組み合わせて新しい暗号化キーを作ります。
ブラウザ側では、その鍵にベリサインなどの"信用できる"証明書が含まれていればその証明書は正しいと判断します。
まあ↑の説明は端折りすぎて正しくないので知ったかしないように。私のことですかそうですか。
そんなわけで個人情報の厳重な取り扱いが必要な企業などでは正しい証明書を発行するべきですが、個人でテストするレベルではそんな必要もないでしょう。
第一種または第四種オレオレ証明書となるでしょう。
さくっとやってみます。
C:\xampp\apache\bin>openssl version
WARNING: can't open config file: /usr/local/ssl/openssl.cnf
OpenSSL 0.9.8i 15 Sep 2008
いやあ……なんだそれ?
$res=openssl_pkey_new();
print($res);
bool(false)
あるぇー?
というわけで証明書を作成してくれるバッチファイルが提供されています。
デフォルトだとここらへんにあると思われます。
C:\xampp\apache\makecert.bat
コマンドプロンプトから実行して質問に適当に答えていくと、自動的に暗号化キーが作成されます。
C:\xampp\apache\conf\ssl.crt\server.crtが公開鍵、C:\xampp\apache\conf\ssl.key\server.keyが秘密鍵となります。
無かったら適当にそこら辺を探してください。
鍵のペアができましたので、とりあえず暗号化してみます。
encrypt_decrypt.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
|
//暗号化する文字列
$str='あいうえお';
//公開鍵
$key_public=file_get_contents('C:\xampp\apache\conf\ssl.crt\server.crt');
//秘密鍵
$key_private=file_get_contents('C:\xampp\apache\conf\ssl.key\server.key');
//暗号化
$ret=openssl_public_encrypt($str,$str_encrypt,$key_public);
if($ret===false){print_openssl_error();}
//復号化
$ret=openssl_private_decrypt($str_encrypt,$str_decrypt,$key_private);
if($ret===false){print_openssl_error();}
//確認
print("<html><pre>");var_dump($str,$str_encrypt,$str_decrypt);die();
//失敗したときにエラーログを拾う関数
function print_openssl_error(){
while($msg = openssl_error_string()){
print($msg.'<br />');
}
} |
$strと$str_decryptが同じになり、無事に復号化できました。
ちなみに$str_encryptは実行するたびに違う値になります。びっくりですね。
さて、暗号化復号化ができるのはわかったが、で、これでどうやってサーバ←→クライアント間を暗号化するんだ?
なんといってもAPIです。
他サイトにリクエストを投げるわけですからどうしても時間がかかってしまいます。
またリクエスト回数が増えると負荷もかかるので、多くのサイトでは上限回数が指定されているはずです。
そんなときこそAPCです。やってみましょう。
題材は適当にググって引っかかったサムネイル画像作成API
http://img.simpleapi.net/
まずは普通に実行
thumbnail.php
1
2
3
4
5
6
7
8
9
10
|
//画像を拾ってくる
$url='http://img.simpleapi.net/small/http://www.google.co.jp/';
$ret=file_get_contents($url);
//画像表示
header('Content-Length: '.strlen($ret));
header('Content-Type: image/jpeg');
print($ret);
|
リクエストがあると毎回img.simpleapi.netまで画像を取りに行きます。
次にAPCを使ってみます。
thumbnail_apc.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
//APCからキャッシュロード
$api_cache=apc_fetch('thumbnail_cache');
//キャッシュロードに失敗したら
if($api_cache===false){
//画像を拾ってくる
$url='http://img.simpleapi.net/small/http://www.google.co.jp/';
$api_cache=file_get_contents($url);
//APCにキャッシュを保存
apc_store('thumbnail_cache',$api_cache,3600);
}
//画像表示
header('Content-Length: '.strlen($api_cache));
header('Content-Type: image/gif');
print($api_cache);
|
APCにデータが無ければ拾ってきてそれを突っ込みますが、あればAPIにアクセスすること無しにキャッシュから画像を表示します。
というわけであっさり成功。
さて、ここらへん見てみるとどうもAPCにはさらに別の使い方があるようで、というかこっちが主題でapc_fetch()とかがおまけっぽくない?
http://neta.ywcafe.net/000952.html
http://www.maido3.com/server/option/php.html
ちなみにapc_fetchに第二引数を渡したらエラーになりました。
mixed apc_fetch ( string $key [, bool &$success ] )
Warning: apc_fetch() expects exactly 1 parameter, 2 given
はて??
お天気予報APIなどをアクセスがあるたびに呼びに行く必要はありません。
毎秒1アクセスがある人気サイトだった場合、毎日86400回もリクエスト要求を出してしまうことになります。
これを例えば1分キャッシュを保存しておくだけで一日のリクエスト回数を1440回に激減させることができます。
一時間に一回なら一日に24回です。
更新頻度が重要でないものは積極的にキャッシュを利用しましょう。
また、APIでなく内製のものでも、毎回DBやファイルにアクセスして動的にページを作成するのに時間や負荷がかかる場合、動的にページを作成した結果をキャッシュしておいてその後は単にそれを表示するだけ、というのはよい考えです。
たとえばSmartyなんかはテンプレートをファイルとしてキャッシュしています。
逆にリアルタイム更新が必要な掲示板やブログの投稿やコメント、ショッピングカートの中身などにはキャッシュを使ってはいけません。
とりあえず使用が簡単なAPCを使ってみます。
最初は使用できない状態ですが、XAMPPならphp.iniの
;extension=php_apc.dll
となってる行のコメントを外せばいきなり使用できます。
パッケージがない場合はpeclから持ってきましょう。
peclのパッケージにはAPCの状態をビジュアルに見ることができるサンプルが含まれているのですが、当方の環境ではApache毎落ちてしまいます。何故。 //GDが悪いっぽい
APCの使い方は非常に簡単。
a.php
1
2
3
4
|
//APCキャッシュに保存
apc_store('a','データ');
|
b.php
1
2
3
4
5
6
|
//APCキャッシュから取得
$ret=apc_fetch('a');
var_dump($ret);
|
a.phpを実行した後にb.phpを実行すると、a.phpで保存した内容が表示されます。
見ての通りセッションも何も使っていませんが、apc_store()で突っ込んだデータは、そのままapc_fetch()で取り出すことができます。
まあ平たく言うとアプリケーションスコープのようなものです。
さて、最初の設定のままだといつまでもAPCキャッシュの中身が残り続けてデータが際限なく増えていくので、
php.iniの[APC]の項目に
apc.ttl=3600
apc.user_ttl=3600
と書いておきます。
これで、APCに突っ込んで3600秒たったデータは自動的に削除されます。
ソース側でapc_store()に生存秒数を指定することができ、またapc_clear_cache()を使用すれば手動で削除できますが、うっかり忘れた場合のためにも面倒なことはPHP本体に任せてしまいましょう。
次回はAPCキャッシュの具体的な使い方でも。
基本的にPHPはprint()とかecho()とか書いた時点で文字が出力されますが、小出しに出力せずにバッファに保存しておいて、後で一気に出力、なんてことができます。
ob_start.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
|
//出力バッファリングスタート
ob_start();
//printしても表示されずバッファに入れられる
print('example');
//バッファリングを終了した時点でバッファが表示される
ob_end_flush();
//引数に関数を入れると、ob_end_flush()時に呼ばれる
ob_start('htmlspecialchars');
//printしても表示されずバッファに入れられる
print('<br />');
//バッファの内容がhtmlspecialcharsされて表示される
ob_end_flush();
//出力バッファリングスタート
ob_start();
//printしても表示されずバッファに入れられる
print('hogehoge');
//現在のバッファの内容を取得
$tmp=ob_get_contents();
//バッファを削除してバッファリングを終了
ob_end_clean();
//$tmpには、ob_start()~ob_get_contents()までに出力した内容が含まれる
var_dump($tmp);
|
こんなふうに出力をためておくことができます。
ただこれ、どういう場合に使えばいいのかいまいちよくわからない。
http://www.ideaxidea.com/archives/2008/04/phpob_start.html
こちらでは文字コードをまとめて変更して出力、というのが紹介されていて一見便利そうに見えますが、このような書き方はわかりづらいのでそもそも根本的に行わない方がいいと思います。
変換処理はコントローラあたりに追い出したいところです。
まあ、MVCに分ける程でもない小規模なものならばこれで十分かもしれませんが。
前回ページャーを作りましたが、あれには致命的な欠点がひとつあります。
Pear::Pagerのインスタンスに、ページングする配列をそのまま突っ込んでいる部分です。
前回は1000件だったので余裕で検索できましたが、たとえばこれが数百万件あるデータベースだったりした場合、全部読み込んだりしようものならメモリが死にます。
そのような場合のため、件数の数値だけでページャーを作成できます。
pager_int.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
|
//件数
$total_item=500000;
//Pear::Pager
require_once('Pager.php');
//ページャーのインスタンス
$params = array(
//SlidingまたはJumpingを指定
'mode' => 'Sliding'
//1ページあたりの件数
,'perPage' => 100
//前後に表示する件数
,'delta' => 5
//件数
,'totalItems' => $total_item
//Aタグのclass名を指定する、CSS等で利用
,'linkClass' => 'linkClass'
);
$pager = Pager::factory($params);
//ページャーの状態について調べられる
//現在のページ
print($pager->getCurrentPageID().'<br />');
//全ページ数
print($pager->numPages().'<br />');
//現在のページのデータは当然取得できない
//var_dump($pager->getPageData());
//リンクを作成
$links = $pager->getLinks();
//表示
//一個前に
print($links['back'].'<br />');
//一個後に
print($links['next'].'<br />');
//ページャを表示
print($links['pages'].'<br />');
//一番前に
print($links['first'].'<br />');
//一番後に
print($links['last'].'<br />');
//上記全部
print($links['all'].'<br />');
//データそのものは、別途取得する
$limit_from=$_REQUEST['PageID']*100-100;
$ret=$PDO->query('select * from table limit '.$limit_from.' , 100'));
|
itemDataに配列そのものを渡すかわりに、totalItemsに件数を渡します。
この場合Pager::getPageData()でデータを取得することができないのでデータそのものは別途自力で取得する必要がありますが、全件を取得しないので動作は軽くなります。
実際に使用する場合は、まずSELECT COUNT(*)で件数を取得、SELECT * で実際の内容を取得、totalItemsに取得した件数を突っ込んでページャーを作る、という順番になるでしょう。
http://jp.php.net/manual/ja/language.variables.variable.php
マニュアルでは
>ドル記号を二つ使用することにより、 変数の名前として使用することができます
とか書かれていますが、この説明はちょっとわかりづらいです。
前${1}という気持ち悪い変数名を作成できるという話をしましたが、言ってしまえばあれも一種の可変変数です(正確には違うが)
最も簡単な例はこうなります。
variable.php
1
2
3
4
5
|
$abc='xyz';
$a='abc';
print($$a); //xyzが表示
|
ただ、$$aという書き方は分かり難い上に$aが配列だった場合に困ったことになったりするので、通常は${$a}のように{}で括ります。
variable2.php
1
2
3
4
5
|
$abc='xyz';
$a='abc';
print(${$a}); //xyzが表示
|
配列の要素を呼ぶときの、$a[$b]といった書き方と殆ど同じようなものです。
$$a→${$a}→${'abc'}→$abc
$a[$b]→$a['abc']
PHPの場合、配列が非常に充実しているので、可変変数を使う機会は滅多にないと思います。
http://blog.promob.jp/fri/2009/04/php-5.html
等で挙げられている例の場合、
$str[1] = "さいたー さいたー";
とすれば済む話ですから。
http://d.hatena.ne.jp/web_program/20090318/1237352866
こちらではSmarty的に配列を変数に代入していますが、………extract()というものがあってだな(略
ちなみにSmarty自体はどうやっているかというと、コンパイル時に{$name}を<?php echo $this->_tpl_vars['name']; ?>に置換しているだけでした。
Pager 2.4.7 (stable)
http://pear.php.net/package/Pager
googleの検索結果の「前へ」「次へ」ボタン、まさにあれが作成できます。
自力で実装してもさほど難しいものではありませんが、終端処理とか例外とか面倒な部分もありますし、既にあるものをわざわざ再発明することもないので積極的に使用しましょう。
pager.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
|
//適当な配列を作成してるだけ
$arr=array();
for($i=1;$i<1000;$i++){
$tmp=array(
'id'=>$i
,'md5'=>md5($i)
);
$arr[]=$tmp;
}
//Pear::Pager
require_once('Pager.php');
//ページャーのインスタンス
$params = array(
//SlidingまたはJumpingを指定
'mode' => 'Jumping'
//1ページあたりの件数
,'perPage' => 3
//前後に表示する件数
,'delta' => 10
//ページングするデータの配列
,'itemData' => $arr
);
$pager = Pager::factory($params);
//ページャーの状態について調べられる
//現在のページ
print($pager->getCurrentPageID().'<br />');
//全ページ数
print($pager->numPages().'<br />');
//現在のページのデータ
var_dump($pager->getPageData());
//リンクを作成
$links = $pager->getLinks();
//表示
//一個前に
print($links['back'].'<br />');
//一個後に
print($links['next'].'<br />');
//ページャを表示
print($links['pages'].'<br />');
//一番前に
print($links['first'].'<br />');
//一番後に
print($links['last'].'<br />');
//上記全部
print($links['all'].'<br />');
|
例ではよく使うオプションを指定していますが、factoryには他にも多数のオプションが用意されています。
http://pear.php.net/manual/ja/package.html.pager.factory.php
Pagerの便利なところとして、GETリクエストがあった場合、そのリクエストがPager::getLinks()で取得するリンクに自動的に追加されます。
たとえば上記のpager.phpにそのままアクセスした場合、ページャーのリンクは
pager.php?pageID=2
というふうに(勝手に)なりますが、
pager.php?hoge=fuga
というURLでアクセスした場合、ページャーのリンクは自動的に
pager.php?hoge=fuga&pageID=2
となります。
これにより、複数条件で検索等の面倒な処理も何も書かなくても行うことができてしまいます。
圧倒的に便利な機能です。
ただ、#でフラグメントに飛ばしたい等、手動でURLを変更したい場合はappend=falseにしてfileNameを自力で書かないといけなくなってしまうので面倒です。
そうするよりはgetLinksでリンクを取得した後に置換したほうが楽かと思います。
したらばの書き込み削除は一件一件チェックしないといけないので、広告が大量に貼り付けられたときなんか非常に面倒です。
というわけでNGワードが含まれる発言を一気に削除するPHPスクリプトを作成してみました。
ダウンロード(zip)
shitaraba_delフォルダを適当な場所(ドキュメントルートの外にするべき)にアップし、index.phpにそのパスを記入、shitaraba_del/templatesフォルダに書き込み権限を付けてshitaraba_del/config.phpにURLやパスワード等を指定すれば動きます。
あとはreadme読んでください。
PHPが動くサーバがないと動作できないので敷居が高いです。
そもそもこれくらいならJavaScriptで作った方が環境を選ばなくて楽だと思うので誰か作ってください。
たった3画面なのに表示はSmartyを使っています。
もう私の身体はSmarty無しでは生きていけません。
作っててわかったこと。
デザインが致命的に苦手。
ロジック組んでる時間よりHTMLいじってる時間の方が長いかもうね。
そんだけ時間かけてこれかよ、とか言わない。
なんかしたらば側の動作が微妙。
削除スクリプトにPOST送ったら対応してねえ、GET送れとか言うのに実際は削除されてるし逆にGET送っても削除されないし勝手に透明削除になってるしなんかよくわからん。
解説なんかは気が向いたら。
ソース見れば大体書いてあるけどな。
さて前回使い道がないとか書いたserializeですが、ひとつ便利な使い道があって、それがAPIとしての利用です。
WebサービスAPIの返り値はXMLとなっている場合がほとんどです。
ところが、PHPのXML解析はそれほど優秀ではありません。
PHP5でSimpleXMLが出てだいぶましになりましたが、それ以前のXML処理関数は何故かやたら面倒です。
http://jp.php.net/manual/ja/refs.xml.php
もっと簡単にどうにかならないかということで、Yahooの中の人が「XMLじゃなくてserializeで流せばよくね?」とか言っています。
日本ではまだ対応していないようですが、yahoo.comでは対応しているサービスもあるようです。
http://api.search.yahoo.com/ImageSearchService/V1/imageSearch?appid=YahooDemo&query=Enkumi&results=1
これはイメージ検索のAPIですが、最後にoutput=phpとするとserialize形式で出力されます。
http://api.search.yahoo.com/ImageSearchService/V1/imageSearch?appid=YahooDemo&query=Enkumi&results=1&output=php
XMLとserial形式それぞれでパースしてみます。
yahoo_search_api.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
//取得
$url_xml='http://api.search.yahoo.com/ImageSearchService/V1/'
.'imageSearch?appid=YahooDemo&query=Enkumi&results=1';
$url_ser=$url_xml.'&output=php';
//XMLパーサで解析
$tmp_xml=file_get_contents($url_xml);
$xml_parser = xml_parser_create() ;
xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, 0);
xml_parse_into_struct($xml_parser, $tmp_xml,$data_xmlparse);
xml_parser_free($xml_parser);
//PHP5ならSimpleXMLで非常に簡単
$data_simplexml=simplexml_load_file($url_xml);
//Serializeだと
$tmp_ser=file_get_contents($url_ser);
$data_ser=unserialize($tmp_ser);
var_dump($data_xmlparse,$data_simplexml,$data_ser);
|
XMLパーサではxml_parse_into_structを使っていますが、これには致命的な欠点があり、パースした結果が一次元配列になってしまいます。
深さ情報自体は残っていますが、XMLツリーを再現するにはパースした結果をさらにパースして云々なんてことをしなくてはなりません。。
もしくは、xml_set_default_handlerでパース関数を自作したりする必要があります。
非常に面倒です。
あと、デフォルトだとタグが勝手に全部大文字になります。なんで?
SimpleXMLではようやくそんな煩わしさから解放されました。
XMLをそのままの形でオブジェクトに格納できます。
serial形式だと、unserializeするだけで送信元が想定した形に簡単に戻すことができます。
オブジェクトだけではなく配列でもstringでも何でもOKです。
stringのシリアル化とか意味はありませんが。
Javaでよくシリアライズという言葉が出てきてたんですが、まったく理解できませんでした。
Serializableインターフェイスって何だよ。
PHPでのシリアライズはもう少し簡単で、インスタンスを丸ごとそのまま保存という意味です。
a.phpでnewしたオブジェクトをシリアライズして保存し、b.phpでそれを読み込むと元のインスタンスが復活できます。
具体的にどのようなことができるかというと以下のような感じです。
Serial.class.php
1
2
3
4
5
6
7
8
9
10
11
12
|
//適当なクラス
class Serial{
public $param='';
public function setParam($param){
$this->param=$param;
}
public function getParam(){
return $this->param;
}
}
|
a.php
1
2
3
4
5
6
7
8
9
10
11
12
13
|
require_once('Serial.class.php');
$serial=new Serial();
//適当に値を入力
$serial->setParam('あいうえお');
//シリアライズ
$tmp_serial=serialize($serial);
//保存
file_put_contents('serial.txt',$tmp_serial);
|
b.php
1
2
3
4
5
6
7
8
9
10
11
12
|
require_once('Serial.class.php');
//読込
$tmp_serial=file_get_contents('serial.txt');
//アンシリアライズ
$serial=unserialize($tmp_serial);
//a.phpで代入した'あいうえお'が出てくる
var_dump($serial->getParam());
|
a.phpとb.phpにはセッション的関係すら存在しませんが、a.phpにアクセスした後でb.phpにアクセスすると、a.phpでの変更内容がb.phpに反映されています。
仕組み的には、serializeした時点でそのオブジェクトが単なる文字列に変換されます。
その文字列をunserializeするとオブジェクトになります。
保存されたテキストを覗いてみると、
O:6:"Serial":1:{s:5:"param";s:15:"あいうえお";}
というテキストになっています。
Serialというクラスのオブジェクトで、インスタンス変数がひとつあって中身が'あいうえお'という文字列だ、という内容です。
serializeで保存されるのはインスタンス変数だけで、メソッドやクラス変数等は保存されません。
また、serializeしたものをそのままunserializeするとインスタンス変数だけが入った__PHP_Incomplete_Classというオブジェクトになります。
しかしb.phpのように、先に該当のクラスを読み込んでおくと、unserializeした時に自動的にそのクラスと組み合わされ、完全に元通りになります。
b.phpでunserializeした$serialは、a.phpでserializeしたものと全く同一の内容になるということです。
オブジェクトを流用できるので便利と言えば便利なのですが、そこまでしてひとつのオブジェクトを使い回すということ自体がそんなにないような気が。
大体のことならセッションやDBに突っ込んでおけば事足りますし。