さて、
http://yuubiseiharukana.blog.shinobi.jp/Entry/17/
のフォームをJavaScriptでバリデートしてみましょう。
JavaScriptでバリデートを行う理由は、サーバ側の負担軽減のためです。
素のフォームのままだと、毎回サーバにデータを送信しチェックを行うのでサーバに負荷がかかります。
ユーザ側のブラウザでチェックを行わせることで、サーバ側の負担を和らげることができます。
まずformタグを変更します。
<form method="POST" action="<?php print(htmlspecialchars($_SERVER['SCRIPT_NAME'])); ?>" onSubmit="return check()">
formのonSubmit部分に関数を追加しました。
これでJavaScript対応ブラウザでsubmitボタンを押した場合、check()関数が発動します。
未対応ブラウザの場合は、そのまま送信されます。
では早速関数checkを作りましょう。
|
function check |
入力されているかどうかだけを簡単にチェックしています。
onSubmitで呼ばれる関数がfalseを返さない限り、フォームはそのままactionを実行してしまいます。
エラーが見つかった場合のみfalseを返すことで、その場合は先に進まないようにしています。
特になんて難しいことはありませんね。
今後基本的にJavaScriptは別ファイルにして、
<SCRIPT TYPE="text/javascript">
で呼び出すことにします。
未対応ブラウザのためにコメントで囲ったりという手間も無くなり、ソースも見やすくなるので推奨されます。
index.php
|
<?php |
check.js
| function check var errorMessage=""; if(document.form[0].shimei.value == ""){ errorMessage +="氏名を入力してください\n"; } if(document.form[0].mail.value == ""){ errorMessage +="メールアドレスを入力してください\n"; } if(document.form[0].sex.value == ""){ errorMessage +="性別を選択してください\n"; } if(errorMessage == ""){ return true; }else{ alert(errorMessage); return false; } } |
さて、JavaScript側とPHP側、両方で入力のチェックを行っているわけですが、では二重に同じ事を行うのは無駄だからどちらかを省くべきでしょうか?
いいえ。この場合は、両方でチェックを行うべきです。
なぜならば、知ってのとおりJavaScriptによるチェックは簡単にすり抜けられるからです。
ちなみにJavaScriptの代入演算子+=は、数字でなければ文字の連結を行ってくれますが、
PHPで+=と書くと無理矢理数値に変換した上で計算してくれます。
PHPで文字列連結は.=と書かなければなりません。
PHPの自動型変換は時に相当不可解な振舞いを見せてくれるので注意が必要です。
innerHTMLを利用してリアルタイムHTMLエディタを作ってみましょう。
たいしたことはしていないわりに、見た目のおかげで、知らない人からはなんだかすごいことをしているように見られるという効果もあります。
とりあえず簡単にhtmlから
wisiwig.html
|
<html lang="ja"> |
2箇所にIDを指定しているだけでそれ以外は特に何もありません。
JavaScript的には毎秒openFile()を実行します。
というわけで肝心のwisiwigEdit()を書いてしまいましょう。
wisiwig.js
|
/*==========================================================*/ |
inputFieldが入力する部分、printFieldが表示する部分です。
inputFieldの値が変更されたら、printFieldにその値を表示します。
そのときにその値をinputFieldOldValueに保存しておきます。
次回inputFieldと比較して、変更されているかどうかをチェックします。
毎回過去のデータと比較しているのは単に負荷軽減のためです。
面倒ならinputFieldOldValueの部分は一切無視して実装しても特に問題はありません。
特にどこと通信しているわけでも入力値を使用しているないのでセキュリティ特に問題はなく、危険が及ぶにしても自分のブラウザだけなのですが、念のために<script>タグは無効にしておきます。
もっとも、それでも
<img src="javascript:alert('hello');">
<BR style=left:expression(eval('document.location="http://www.google.co.jp/";'))>
などと書かれたら反応してしまいますが。
これでひとまず完成です。
http://yuubiseiharukana.creativeroot.jp/js2/js2-2-1.html
何か文字を入力するだけでリアルタイムに表示が変更されるので、知らない人に作ってあげるとそこはかとなく尊敬されるかもしれません。
wisiwig.css
| /*----------------------------------------------------------*/ /* 結果表示 */ #printField{ width:80%; margin-right:10%; margin-left:10%; border:1px solid #aaa; padding:5px; } /*----------------------------------------------------------*/ /* 入力テキストエリア */ #inputField{ background-color:#efe; } |
クラスの特徴のひとつが、継承です。
継承とは、あるクラスを引き継いで、新たな別のクラスを作成することができるという機能です。
まあ早速ですが作ってみましょう。
とりあえず乗り物クラスでも作ってみましょう。
|
class Vehicle{ |
名前だけしか決まってないクラスです。
さて乗り物だけでは範囲が広すぎるので、次は飛行機クラスを作ってみましょう。
この場合、一から飛行機クラスを作るのではなく、さきほど作った乗り物クラスを引き継いで作成することができます。
|
class Airplane extends Vehicle{ |
extendsで親クラスを引き継ぐことができます。
Airplaneクラスにはどこにも$aNameプロパティ、setNameメソッドおよびgetNameメソッドがありませんが、
親クラスであるVehicleクラスで定義されているため、きちんと使用することができます。
もちろん飛行機クラスをさらに引き継いで、戦闘機クラスを作ることもできます。
乗り物クラスを継承して自動車クラスや船クラスを作ったり、このように元のクラスを利用してどんどん拡張していけるのがクラスの特徴です。
ただ、あんまりやりすぎると、絶対使わないような死にクラスだらけになったり、仕様変更で使えなくなったり、同名なのに動作が違うメソッドが存在したりというJavaみたいな悲惨なことになるので程々にしましょう。
PHPのクラスにはいくつか問題点が存在します。
オーバーロードができません。
引数の有無で振り分ける等の対応をする必要があります。
子クラスにコンストラクタが存在する場合、親クラスのコンストラクタは実行されません。
親クラスのコンストラクタを実行させたい場合、super()的な指定方法が存在しないので明示的に指定せねばならず微妙に不便。
前回も挙げましたが、クラスの外から自由にオブジェクトを操作できてしまうという問題点があります。
その方向を突き詰めたプロトタイプベースという書き方もありますが(JavaScriptが筆頭)、PHP5では逆にJavaやC++のような実装が追加されました。
クラスはデータベースに接続するときのDB種類による差異を吸収したり、ユーザのデータを簡単に取り扱いたい場合などに使用すると便利です。
ただし、継承を繰り返したりしていると、後から見て何をやっているのか絶対にわからなくなりますので、コードよりヘルプが多いくらいの勢いで注釈を書くようにしましょう。
Javaのせいで難しいとレッテルを貼られてしまったクラスですが、さほど難しいものではありません。
ただ単に、幾つかの関数を纏めて一塊にしてみました、それだけのことです。
特にPHPのクラスは実のところ連想配列そのものです。
継承など面倒な概念も幾つかありますが、まあそれはおいおい。
ちなみに以下はPHP4での話です。
PHP5でも基本は同じだと思いますが、よりオブジェクト指向的な書き方ができるようになっています。
さて、さっそくクラスを作ってみましょう。
作り方は簡単です。
関数を作る要領で、functionのかわりにclassで全体を囲むだけです。
また、クラスの外部から操作を行うためのメソッドというものを書く必要がありますが、こちらは関数そのものです。
まあとりあえず作ってみましょう。
|
class Animal{ //名前を決めるメソッド //名前を取得するメソッド |
クラスを初めて触ったときに一番わかりにくい概念が、インスタンス化だと思います。
クラスは関数のように書いただけでは使用することができず、インスタンス化という儀式を通さないといけません。
$AnimalA = new Animal;
こうすることでAnimalクラスが$AnimalAという名前で実体化されました。
以降は$AnimalAという実体化されたクラスに対して操作を行うことができます。
Animalクラスの中のsetName関数に触ってみましょう。
$AnimalA->setName("dog");
クラスとクラス内関数や変数との繋ぎは「->」で表します。
さて、上の関数でどうなったかというと、クラス内の変数$aNameに、setName関数で与えた引数"dog"が設定されました。
同様に、
$AnimalA->getName();
という関数を実行すると$AnimalAクラス内の変数$aName、すなわち先ほど設定した"dog"が返ってきます、
以後クラス内の変数には、直接触るのではなく、常に関数を通じてアクセスすることになります。
今度はもうすこし複雑なクラスを作ってみます。
|
class Circle{ var $radius; //半径をセット //面積を取得 |
pow($this->radius,2)*M_PIは半径から面積を計算しています。
M_PIは円周率を表す定数(=3.14159265358979323846)、pow(a,b)はaのb乗です。
関数の恐ろしく豊富なPHPですが、何故か^は使えません(何故かxorにアサインされている)
$circle1=new Circle;
とすることで$circle1をCircleクラスとして扱うことができるようになります。
$a->setRadius("3");
で半径3をセットし、
$a->getArea();
で先ほどセットした半径から計算した円の面積を取得することができます。
さてコンストラクタという見慣れぬ名前の関数があります。
クラス名と同名の関数は、クラスのインスタンス化と同時に実行される、という特徴があります。
この場合、$circle1=new Circle;と書いた時点で自動的にfunction Circleも実行されるのです。
Circle関数が何をやってるかというと、引数を半径にセットしています。
つまり、
$circle2=new Circle(3);
と書けば最初から半径に3がセットされるというわけです。
コンストラクタは主に変数の初期化等に使われます。
クラスを使って何が便利かというと、同じ定義を使いまわせることです。
例えばAnimalクラスは以下の連想配列とほぼ同義です。
$AnimalA=array("aName",$name);
しかし、同じ定義の連想配列を他で使用したくなった場合も再度同じことを書かなければなりません。
$AnimalB=array("aName",$name);
クラスならば最初に定義してしまえば、何度でも使いまわすことができます。
$AnimalA = new Animal;
$AnimalB = new Animal;
この場合は中身がひとつだけなのでたいして変わりませんが、キーが何十個もあるような複雑な連想配列を考えてみるとクラスの便利さがわかるでしょう。
同じ処理は関数に纏めましょう、というのと同じように、同じオブジェクトはクラスに纏めましょう、ということです。
さて、PHPのクラスはほぼ連想配列なので、何気に連想配列的な操作を行うことができます。
例えば$AnimalA->aName="dog";と書くと、メソッドを介さずに直接変数をセットできてしまいます。
同様にprint_r($AnimalA);と書いてしまうとクラス変数の中身が全部表示されてしまいます。
それどころか$AnimalA->aBark="wanwan";などと元のクラスに存在しないプロパティを作成できてしまったりとやりたい放題です。
オブジェクト指向としては非常に正しくない実装なので大問題です。
PHP5ではprivateなどのアクセス権を設定できるようになっており、上記の問題点は改善されつつあります。
さて次回はクラスの継承についてと言いたいのだが、そこまでやったところで個人レベルじゃどうせ使わないんだよなあ継承。
継承すればするほどオブジェクト指向の目的から外れていくと思うのは私だけでしょうか。
実用的なサブルーチンを作ってみましょう。
サブルーチンと関数はほぼ同義語なので今後混ざって出てきても気にしないで下さい。
ちなみにJavaScriptではクラスも関数とほぼ同義だったりします。
あらゆる言語で問題になるのが日本語対応です。
その第一が文字化けで、第二が全角半角の対応です。
全角の「0」と半角の「0」は当然ながら違う文字として認識されます。
そこで問題になるのがフォーム等の入力値です。
たとえば住所欄など、全角に統一して扱いたい入力欄があるとき、
よくある解決法のひとつが「半角文字が入っていたら入力させなおす」です。
しかしこれはユーザビリティ的には最悪です。
Atok等、数値記号は自動的に半角入力してくれるIMEを使用していた場合、そのようなフォームに突き当たったらわざわざ設定を変更しなければなりません。
そんな酔狂に付き合ってくれるユーザーは少ないでしょう。
全角半角変換程度のことはフォーム側で処理してあげるべきです。
さて、PHPではphp mb_convert_kanaという命令一発で非常に容易に変換できます。
PerlでもEncodeやjcodeというモジュールが用意されており簡単に使用できます。
しかし何故かJavaScriptには簡単に変換する方法が存在しません。
もっとも、さほど難しくはないので作ってしまいましょう。
とりあえず数値を半角から全角に変換するスクリプトです。
単純に、charAtを使用して、入力文字列に変換対象文字が見つかったら入れ替えているだけです。
convertInt.js
| function convertInt(input){ var hanList = "0123456789"; var zenList = "0123456789"; var str = ""; var oneStr; var i; var c; var n; for(i=0;i<input.length;i++){ oneStr=input.charAt(i); n = hanList.indexOf(oneStr,0); c = n>=0 ? zenList.charAt(n) : oneStr; //三項演算子 str+=c; } return str; } |
使用時は、普通に
var output=convertInt(input);
と書くだけです。
さて、どちらかというと全角入力された英数字を半角に変換することが多いと思います。
どちらの場合でも使えるよう入力値に変換方向も書き加えてしまいましょう。
convertStr.js
|
function convertStr(input,flag){ |
var output=convertStr(input,true);
として使用します。
第二引数にfalseを設定すると半角を全角に変換し、それ以外だと全角から半角に変換します。
次回はこのライブラリを使用してフォームのバリデートを行ってみましょう。
index.php
| <form method="POST" action="./validate.php"> 氏名:<input type="text" name="shimei" maxlength="64"><br /> メアド:<input type="text" name="mail" maxlength="64"><br /> 性別: <input type="radio" name="sex" value="1">男 <input type="radio" name="sex" value="2">女<br /> <input type="submit" name="soushin" value=" 確認 "> </form> |
まあよくあるフォームです。actionを指定したのでvalidate.phpを作らなくてはなりません。
何をするかというと入力値のチェックです。
validate.php
|
#初期設定 #入力をチェック #エラーの有無で分岐 |
簡単に流れを書くと以上のようになります。
性別は入る値が1か2に決まっているので、判定も1か2かで行います。
radio、checkbox、hiddenは最初から決められている値以外に変更することはできないように見えますが、実は簡単に行えます。
外部から入ってくるデータは基本的に汚染されているものだと考えてチェックを行う必要があります。
保存部分は簡単に作成します。
保存部分は「,」で区切っているだけなので、入力値に「,」を入れられると困ったことになりますが、今回はまあパスします。
form.txtは予め作成しておかねばなりませんが、これで保存部分ができました。
validate_ok.php
| $data=$shimei.','.$mail.','.$sex; $fp = fopen("form.txt","r+"); flock($fp,LOCK_EX); fputs($fp,$data); fclose($fp); |
さて、入力のエラーがあった場合もとの入力画面に戻されるのはよくある光景ですが、このときにせっかく登録した内容が消えていたりすると非常にがっかりです。
入力した内容を保持しておいて、入力画面に戻ってきた場合には最初から表示されるようにしましょう。
新たに以下のようなエラー画面を用意して、validate.phpに組み込んであげればよいことになります。
内容的には、valueにフォームから送信されてきた値をセットしているだけです。
htmlspecialcharsという不思議な関数ですが、入力値に「"><script>alert("XSS");</script>」と入れてみるとその理由がわかります。
validate_error.php
| <?php print($error_message_array); ?> <form method="POST" action="./validate.php"> 氏名:<input type="text" name="shimei" maxlength="64" value="<?php print(htmlspecialchars($shimei)); ?>"><br /> メアド:<input type="text" name="mail" maxlength="64" value="<?php print(htmlspecialchars($mail)); ?>"><br /> 性別: <input type="radio" name="sex" value="1"<?php if($sex==1){print(" checked");} ?>>男 <input type="radio" name="sex" value="2"<?php if($sex==2){print(" checked");} ?>>女<br /> <input type="submit" name="soushin" value=" 確認 "> </form> |
しかしindex.phpとほとんど同じ内容なのに、わざわざもう一度作るのも馬鹿みたいです。
多くの言語と違い、PHPはデフォルトの設定では、未定義の変数を呼び出したとき中身が空として扱ってくれます。
従ってvalidate_error.phpをそのままindex.phpにリネームすれば何事もなく動いてくれます。
ただしerror_reporting等で厳しくエラーチェックしているとエラーが発生します。
その場合はまた変数の存在確認などの処理を行ったりしなければなりません。
面倒なので全部纏めてしまいましょう。
index2.php
|
<?php <?php print($error_message_array); ?> <?php |
フォームのデータを自分自身に送っています。
$_POSTの値が存在すればそれを代入し、存在しなかった場合は空として扱ってくれます。
1ページで投稿フォームを作成することができました。
細かな部分でまだ問題がありますが、ひとまずは以上で完成です。
次回は投稿確認画面でも作りましょうか。
ところでこのブログって横幅どうにかならないんですかね?
実用プログラムの第一歩として、よくあるアクセスカウンターを作ってみましょう。
その前にファイルの扱いについて簡単に説明しておきます。
とりあえず初心者が躓く点として、PHPはファイルを直接扱うことができず、ファイルポインタという変数を通して扱わなければならないことがあげられます。
$fp = fopen("counter.txt","r+");
というふうに、counter.txtを$fpという変数に代入し、以後変数を通してファイルを操作することになります。
PHPによるファイルの操作はけっこう不自由で面倒なので、今後は徐々にデータベースを使用するようにしていきます。
以下の2ファイルを用意します。
counter.txt
| 0 |
counter.php
| <?php //ファイルを書き込み可能状態で開く $fp = fopen("counter.txt","r+"); //ファイルから1行読み込む $count = fgets($fp); //カウンターを1増やす ++$count; //ファイルポインタを戻す rewind($fp); //ファイルに書き込む fputs($fp,$count); //ファイルを閉じる fclose($fp); ?> <html> <head></head> <body> アクセスカウンター<br /> <div align="center"><?php print($count); ?> </div> </body> </html> |
counter.txtは改行を入れず、アップロード時にパーミッションを666に変更します。
注意すべきは一点、rewindです。
ファイルポインタは、今自分がどの場所にいるかという情報、要するにカーソルの位置を記憶しています。
fgets等でファイルの内容を取得すると、その場所までファイルポインタの位置が移動します。
この場合「1」というファイルの中身を読んだ後、「1」の後ろの部分になるということです。
その状態で書き込みを行ってしまうと、ファイルの中身は「12」ということになってしまうので、一旦rewindでファイルポインタを最初に戻しています。
さて、上のプログラムはとりあえず動くのですが、問題があります。
ほぼ同時に二人がアクセスした場合を考えてみます。
| A | B |
| fopen | |
| fgets(=1) | fopen |
| $count++(=2) | fgets(=1) |
| fputs(=2) | $count++(=2) |
| fclose | fputs(=2) |
| fclose |
二人がアクセスしたにもかかわらず、カウントが1しか増えていません。
同時にアクセスされた場合の処理を考えていないからです。
アクセスカウンター程度なら一人や二人ずれたところでどうでもいいですが、これがたとえば銀行の送金処理で起こったりしたらとんでもないことになります。
このような問題を防ぐためには、同時に一人しかファイルにアクセスできないようにすればいいわけです。
PHPには簡単にファイルロックを行える関数flockがあるので使用することにしましょう。
counter2.php
|
<?php //ファイルロック |
こうすることで、ほぼ同時にリクエストが来た場合でも後者が待たされるので、正しい結果を出すことができます。
| A | B |
| fopen | |
| fgets(=1) | ↓ |
| $count++(=2) | ↓ |
| fputs(=2) | ↓ |
| fclose | fopen |
| fgets(=2) | |
| $count++(=3) | |
| fputs(=3) | |
| fclose |
ちなみにロック解除のflock($fp, LOCK_UN)は、fcloseの際に自動的に行われるので書かなくてもいいです。
上のプログラムでまあとりあえず配布可能レベルにはなっていますが、できれば「ファイルを作成してパーミッションを云々」というような余計な手間をかけさせないほうがいいでしょう。
自動作成するようにしてみます。
defineは定数を定義するもので、ディレクトリ名やファイル名等、ファイル全体で使用し、定義後は変更しないデータを突っ込んでおきます。
counter3.php
|
<?php //カウンターファイルが存在するかどうかで分岐 |
ファイルが存在しない場合のfopenのみエラー処理を行っている理由は、パーミッションの関係でファイルの作成が行えない可能性があるからです。
fopen,flock,fgets,fputs,fcloseは返り値に成功失敗を渡すので、本格的にプログラム開発をするならば各関数についてそれぞれエラーチェックを行わなければならないのですが、まあさすがにそこまではいいでしょう。
カウンターひとつでいろいろと面倒ですね。
動かすだけならもっと適当でもいいのですが、不用意なバグを防ぐためにも丁寧な書き方をお勧めします。
まあ、人のことは言えませんが。
HTMLにはセッション機能がありません。
セッションとは、簡単に言うとリクエスト間の繋がりです。
ある時ページAが呼び出され、次にページBが呼び出されたとして、そのAとBが同一人物から呼び出されたのか、それとも別人から呼び出されたのかがわかりません。
また、ページAからページBに情報を渡すといったこともできません。
要するにログイン認証のあるページが作れず、ショッピングカートも作れないということです。
それではあんまりなので、多くのサイトではPerlやPHP等の言語を用いてセッションを実装しています。
以下は、よく使われているセッションの仕組みです。
まずサーバ側で適当な文字列を用意し、クライアントのクッキーにセットします(セッションIDと呼ぶ)
クライアントはサーバにアクセスする度に、そのセットされたクッキーを送信します(ブラウザの仕様)
サーバ側ではクライアントが送ってきたセッションIDをチェックして、サーバに記録されているセッションIDと同じであれば同じクライアントのアクセスからだ、と判断します。
ショッピングカートを例に、実装の流れを考えてみましょう。
クライアントがカタログページにアクセスしてくる
・セッションIDを作成する
・クライアントのクッキーにセットする
クライアントが商品を注文した
・必要な情報を、セッションIDとセットにしてサーバで保存する
クライアントが買い物レジに移動した
・クライアントとサーバのセッションIDをチェックし、同じものを取り出して表示
以上のような流れとなります。
面倒そうですね。
上記の流れでは書いていませんが、実際には他にもセッションの有効期限を決めたり、不正なセッションIDがやってきた場合の処理など、実装しなければならないことがいろいろあります。
またセッションはセキュリティ上考慮すべき点も多いので、毎回考えながら行うのは大変です。
特にセッションIDが他者に漏れたりすると買い物され放題なので、実装には十分注意しなければなりません。
Perlではこのあたりの処理も自力で実行しなければなりません。
CGI::Session等のモジュールもありますが、それでも実装するにはやや敷居が高いのが実情です。
PHPでは、
session_start();
と書くだけであとはPHPが全ての管理を行ってくれます。
session_start();
と書くことで使えるようになるスーパーグローバル変数があります。
$_SESSIONです。
普通のローカル変数同様、普通に代入するだけで使えます。
自動的にクライアント毎に内容が振り分けられ、Perlで必要だったクライアントのセッションIDとサーバのセッションIDをチェックして云々、という処理が一切必要ないという恐ろしく便利な変数です。
フォームのスクリプトa.htmlとa.php、およびそれとは何の関係もないb.phpを用意します。
http://yuubiseiharukana.creativeroot.jp/php1/a.html
| <form action="a.php" method="POST"> <input type="text" name="userid"> <input type="text" name="pass"> </form> |
http://yuubiseiharukana.creativeroot.jp/php1/a.php
| <?php session_start(); $_SESSION['userid']=$_POST['userid'] $_SESSION['pass']=$_POST['pass'] ?> |
http://yuubiseiharukana.creativeroot.jp/php1/b.php
| <?php session_start(); print_r($_SESSION); ?> |
a.htmlでフォームを登録した後、適当に他のサイトを見て回った後でb.phpにアクセスすると、a.phpでセットした変数がしっかり表示されます。
有効期限は設定で変更できますが、デフォルトはブラウザを閉じるまでです。
他にもセッション名を指定する関数session_name()やセッションIDを取得する変数session_id()等セッションに関するいろいろな関数がありますが、使用法についてはまたそのうち。
ちなみにsession_start();しないとsession_id()は取得できませんが、クッキー自体は毎回送信されているはずで、実際$_COOKIE['PHPSESSID']で取得できます。
とりあえずは実例で見てみましょう。
http://yuubiseiharukana.creativeroot.jp/php2/php2-3-1.php
| <html> <head> </head> <body> <form method="GET" action=" <?php print(htmlspecialchars($_SERVER['SCRIPT_NAME'])); ?> "> <input type="text" name="data"><br /> <input type="submit" value="送信"> </form> <hr> <?php if($_GET['data']){ $x=htmlspecialchars($_GET['data']); print($x); } ?> </body> </html> |
ただ単に送られてきたフォームを表示するという内容です。
フォームがGETなので、
http://yuubiseiharukana.creativeroot.jp/php2/php2-3-1.php?data=あいうえお
と入力すればいきなり結果画面を表示できます。
さて、上のURLへのリンク、これがクロスサイトリクエストフォージェリそのものです。
これは単に表示するだけのプログラムだから特に問題が無かっただけであって、フォームの内容によっては大変なことになります。
http://yuubiseiharukana.creativeroot.jp/php2/php2-3-2.php
| <html> <head> </head> <body> <form method="GET" action="<?php print(htmlspecialchars($_SERVER['SCRIPT_NAME'])); ?>"> <select name="to"> <option value="webmaster@xxx.jp">管理者 <option value="userA@xxx.jp">利用者A <option value="userB@xxx.jp">利用者B <option value="userC@xxx.jp">利用者C </select> メールアドレス:<input type="text" name="from"><br /> 本文:<input type="text" name="data"><br /> <input type="submit" value="送信"> </form> </body> </html> |
時々見かける、送信先を選択できるタイプのメールフォームです。
何も考えずにこれを実装していた場合、
http://yuubiseiharukana.creativeroot.jp/php2/php2-3-2.php?from=匿名&data=メールボム&to=aaa@yyy.jp,bbb@zzz.jp,ccc@vvv.jp
というリンクを踏ませるだけでメールを送信できてしまいます。
メソッドがGETのせいだから問題なのだ、POSTならURLを作れないから問題ない、かと思えばそういうこともありません。
http://yuubiseiharukana.creativeroot.jp/php2/php2-3-3.html
| <form action="php2.php" method="POST"> <input type="hidden" name="from" value="匿名"> <input type="hidden" name="data" value="メールボム"> <input type="hidden" name="to" value="aaa@yyy.jp,bbb@zzz.jp,ccc@vvv.jp"> </form> <script>document.forms[0].submit();</script> |
こんなフォームを作って適当な場所にアップロードしてしまえば、このリンクを踏んだ時点でメールが送信されてしまいます。
むしろ見た目普通のhtmlなのでより悪質になったというべきでしょう。
あまつさえこのフォームをwhile(1)で繰り返したりしてしまえば、本物のメールボムの完成です。
対処法はいくつかありますが、最初にすべきなのはリクエスト元URLの限定です。
最初のphp1.php以外からのリクエストは受け付けないようにしてしまえば、少なくともリンクを踏ませるだけの攻撃に簡単に引っかかることはありません。
また、selectやcheckboxなどのフォームでは、本来ありえないはずの入力もチェックする必要があります。
もっと言うと、フォームで直接メールアドレスを扱うべきではありません。
フォーム側からは適当なパラメータを渡し、プログラム側で正しいメールアドレスに変換を行うなどの処理を行いましょう。
さて今回のはまだまだ本物のCSRFとは言いがたいので、次回は認証サイト内にも潜り込める、CSRFの本格的利用例でも。
XMLHttpRequestと直接は関係ありませんが、同時に使うことがほぼ前提になっているinnerHTMLについて解説しておきます。
JavaScript時計はよく見ますが、そのほとんどがinputタグ内に書かれたものです。
http://yuubiseiharukana.creativeroot.jp/ajax1/ajax1-2-1.html
| document.forms[0].sampleWatch.value=new Date(); <input type="text" name="sampleWatch"> |
なにしろ実質的に上の2行で実現できるのですから非常にお手軽だと言わざるをえません。
リアルタイムに更新を行いたいときも、window.setTimeoutを仕込むだけです。
今後JavaScript部分は別ファイルに追い出すことになりますが、この程度なら同一ファイルでかまわないでしょう。
http://yuubiseiharukana.creativeroot.jp/ajax1/ajax1-2-2.html
| <script type="text/javascript"> <!-- function openFile(){ document.forms[0].sampleWatch.value=new Date(); window.setTimeout("openFile();",1000); } --> </script> <body onload="openFile()"> <form> <input type="text" name="sampleWatch" size="60"> </form> </body> |
さて、フォーム時計はわかりやすいのはいいのですがデザイン的に困り者です。
cssでなんとかなるとはいえ、「今日は4月1日です」程度の文章がブロック要素で分断されてしまうのは何かと釈然としません。
どうにかしてインライン要素で表示しましょう。
getElementByIdというメソッドがあります。
HTMLタグのidプロパティを取得できます。
HTML内の適当な場所に
<div id="sampleWatchInc">さんぷる</div>
と入力した場合、
document.getElementById(sampleWatchInc);
と入力することで、その部分のオブジェクトを取得することができます。
さて、getElementByIdメソッドにはinnerHTMLというプロパティがあるのですが、上記の例で言うと具体的には<div>タグで挟まれた部分のことを示します。
というわけで
var node=document.getElementById(sampleWatchInc).innerHTML;
とするとnodeの中には"さんぷる"が入ります。
それはいいのですが、驚いたことに
document.getElementById(sampleWatchInc).innerHTML="ほげほげ";
と入力すると、<div>タグで囲まれた部分の「さんぷる」が「ほげほげ」に書き換えられてしまいます。
なんともあっさり文字列を変更できてしまいました。
さて、最後にリアルタイム時計と、よくある押すと文字が変更になるボタンを置いておきます。
forms.nameがgetElementById.innerHTMLに変わっただけなので、簡単にわかると思います。
http://yuubiseiharukana.creativeroot.jp/ajax1/ajax1-2-3.html
| <script type="text/javascript" src="./ajax1-2-3.js"></script> <body onload="loadDate()";> <form> <input type="button" value="ボタンを押すと文字が変わる" onClick="changeText()"> </form> <div id="time">さんぷる</div><br /> <div id="disp">さんぷる</div> |
http://yuubiseiharukana.creativeroot.jp/ajax1/ajax1-2-3.js
|
var i=0; |
よく考えてみたらdivもブロック要素ですが。