PHPを学習し始めてまだ間がない者です。

生まれてはじめて自分でスクリプト(簡単なリンク集)を作成してみました。
http://hatena88.web.fc2.com/hatena/newpage1.html

これについて、下記の点を留意しつつ、
もっとスマートにするにはどうしたら良いか、
いろいろ細かく、やさしくつっこみを入れてください。

・削除ボタンを押すと、その行全体が削除するという風にしたいのですが
 うまく削除できません。これを解決してほしいです。
・テーブル名も変数にしたいが、エラーが出てうまくいかなかったです。
・セキュリティ対策はこれでよいのか?
・フォームから送信される値を全部配列として処理したい。
 つまり項目が増えてもいちいちそれを書き直さなくても良いようにしたい。

その他どんな細かいことでも教えていただけると嬉しいです。
私ならこうするといった代替案でも結構です。

なお回答には理論だけでなく、
実際に動くスクリプトをお示しくださいますようお願いします。

回答の条件
  • 1人2回まで
  • 登録:
  • 終了:2008/04/10 02:35:17
※ 有料アンケート・ポイント付き質問機能は2023年2月28日に終了しました。

ベストアンサー

id:tobeoscontinue No.4

回答回数220ベストアンサー獲得回数59

ポイント120pt

>うまく削除できません。

削除ボタンを押した場合、hiddenでの名前が"del"になっているためです。ので"del"を"id"にすればokでしょう。ただ<form>が閉じていないので正しいidが戻っていません。</form>で閉じることで正しいidは戻りますが$_POST["title"]が設定されていないと

if (!isset($_POST["title"])){

の部分が実行されて終了していました。


>$query = "insert into site (title,category,launguage,link_ok,url,server_name,uptime)";

>この部分の項目が増減してもいちいち書き直さないでも良いようにしたいと言うことです。

php側だけではなくMySQL側でも直さなくてはならないのでその都度(頻繁に変更があることの方が問題)、書き換えでもいいと思います。

あるいは新たに追加がある場合はその部分だけのupdate文で処理するようにするなど。


エラー処理はやっかいです。ユーザーには詳細な内容を知らせない方がいいですし、自分には詳細な内容が必要です。ある程度の記述が必要ですのでset_error_handler()が便利です。後はエラーが発生したらtrigger_error()を呼ぶだけでset_error_handler()で指定した関数へ飛びます。


phpはhtml文の中に混在して書き込めるのが特徴の一つです。単純な場合はそれでいいのですが、複雑になるとこれが逆に足かせになってきます。今回は更にSQL文もあるので三つのものが混在しています。

提案としてはこれらはなるべく別々にするようにし、それらをつなぐものとして配列を使います。


$_POST,$_GET,$_COOKIE,$_SERVERなどのPHPの外部から来る変数は信用できません。

ので使用には常に注意が必要です。常に言えることは直接使ってはダメということです。


SQLインジェクションについて記憶に新しいものとしてはトレンドマイクロがあります。

http://jp.trendmicro.com/jp/about/notice/0312/index.html

考え方として攻撃を無害化するというのがmysql_real_escape_stringに代表されるものでしょう。

私の場合は攻撃を感知するということに重点を置いています。どういう攻撃があるかがわからないと感知できないのが難点ですが。

考え方が違えば、やり方も変わってきますし、どれが正解ということはないと思います。ようは攻撃を防げればいいのですから。


prepared文はMySQL 4.1から追加されたようです

http://lists.mysql.com/mysql-ja/77

これを使えば、あらかじめquery文を評価してしまい、後でexecute文で引数で置き換えて実行するというものです。

ので外部から与えられるものは単なるデータになるのでSQLインジェクションにはならないと理解しています(多分)。

プリペアドステートメントに渡すパラメータは、引用符で括る必要は ありません。それはドライバが自動的に行います。 アプリケーションで明示的にプリペアドステートメントを使用するように すれば、SQL インジェクションは決して発生しません (しかし、もし信頼できない入力をもとにクエリの他の部分を構築している のならば、その部分に対するリスクを負うことになります)。

プリペアドステートメントおよびストアドプロシージャ

mysql_xxxでは使えないようなのですがPEAR::DBなら使えます(多少オーバーヘッドは発生するでしょうが)。

php5ではPEAR::DBがうまく動かなかったのでPDOで実装しました。mysqliが改良版拡張サポートということでprepared文が使えるのですが使いかたが面倒そうなので断念しました。

<?php
define('CR', "\n");
define('BR', "<br>");
define('DB_NAME', 'link');
define('USER',    'xxxxxx');
define('PASSWD',  'xxxxxxx');

//date_default_timezone_set('Asia/Tokyo');

function error_handler($errno, $errstr, $file, $line, $context) {
  static $errortype = array (
    E_ERROR              => 'Error',
    E_WARNING            => 'Warning',
    E_PARSE              => 'Parsing Error',
    E_NOTICE             => 'Notice',
    E_CORE_ERROR         => 'Core Error',
    E_CORE_WARNING       => 'Core Warning',
    E_COMPILE_ERROR      => 'Compile Error',
    E_COMPILE_WARNING    => 'Compile Warning',
    E_USER_ERROR         => 'User Error',
    E_USER_WARNING       => 'User Warning',
    E_USER_NOTICE        => 'User Notice',
    E_STRICT             => 'Runtime Notice',
    E_RECOVERABLE_ERROR  => 'Catchable Fatal Error');
  echo $errortype["$errno"].':'.$file.':'.$line.'<br>'.$errstr.'<br>'.'<br>';
  return TRUE;
}

function mysql_prepared($sql, $parms=NULL) {
  $mysql = new PDO('mysql:host=localhost;dbname='.DB_NAME, USER, PASSWD);
    $stmt = $mysql->prepare($sql);
    $stmt->execute($parms);
  return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

function mysql_insert($parms) {
  $sql = "insert into site (title, category, launguage, link_ok, url, server_name, uptime)".
         " values (?, ?, ?, ?, ?, ?, CURRENT_DATE())";
  return mysql_prepared($sql, $parms);
}

function mysql_delete($parms) {
  $sql = "DELETE FROM site WHERE id = ?";
  return mysql_prepared($sql, $parms);
}

function table_tr_html($attr, $tr_ary) {
  $contents = '';
  foreach ($tr_ary as $col)
    $contents .= '<td>'.$col.'</td>';
  return '<tr'.$attr.'>'.$contents.'</tr>';
}

function table_html($table_ary) {
  $contents = table_tr_html(' bgcolor="#88bbff" height="32"', array_shift($table_ary));
  foreach ($table_ary as $id=>$row)
    $contents .= table_tr_html($id%2 ? ' bgcolor="#fafaff"':'', $row);
  return '<table>'.$contents.'</table>';
}

function db_table_html($db_data) {
  $map = array();
  foreach ($db_data as $row)
    $map[] = array(
      $row['id'],
      BR.'<form action="'.$_SERVER['SCRIPT_NAME'].'" method="post">'.
        '<input type="submit" name="op" value="削除">'.
        '<input type="hidden" name="id" value="'.$row['id'].'">'.'</form>',
      $row['title'],
      $row['category'],
      $row['launguage'],
      $row['link_ok'],
      str_replace(array('http://','www.'),array('',''),$row['url']),
      $row['server_name'],
      date('Ymd',strtotime($row['uptime'])));
  return $map;
}

function validate($vars, $request) {
  $parms = array();
  foreach ($request as $name)
    if (isset($vars["$name"]))
      $parms["$name"] = htmlspecialchars($vars["$name"]);
  return $parms;
}

set_error_handler("error_handler");
  $request = array('id','','title','category','launguage','link_ok','url','server_name');
  $parms = validate($_POST, $request);
  if ($parms && isset($_POST['op'])) {
    if ($_POST['op'] === '新規')
      mysql_insert(array_values($parms));
    else if ($_POST['op'] === '削除')
      mysql_delete(array($parms['id']));
  }

  $db_all_data = mysql_prepared("SELECT * FROM site ORDER BY id DESC");
  $db_table = array_merge(array(array_merge($request, array('uptime'))),
    db_table_html($db_all_data),
    array(array('',BR.'<form action="'.$_SERVER['SCRIPT_NAME'].'" method="post">'.
        '<input type="submit" name="op" value="新規">',
        '<input type="text" name="title" value="タイトル">',
        '<input type="text" name="category" value="カテゴリー">',
        '<input type="text" name="launguage" value="言語">',
        '<input type="text" name="link_ok" value="リンク">',
        '<input type="text" name="url" value="url">',
        '<input type="text" name="server_name" value="サーバー">'.'</form>','')));
restore_error_handler();
?>
<html>
<head>
</head>
<body>
<?= table_html($db_table) ?>
</body>
</html>

動作確認はphp5、mysql4.1以上でdefineのUSER,PASSWDを設定してあれば動くとは思います。


error_handler()

set_error_handler("error_handler")としているのでエラーがあればここに飛んできます。そのまま表示していますのでユーザーには別のメッセージを表示して、自分にはメールを飛ばすようにするなどにした方がいいでしょう。


mysql_prepared()

prepared文を使ってquery()します。


mysql_insert($parms)

mysql_prepared()使って挿入します。挿入する内容についてはチェックしていません。prepared文を使っているのでSQLインジェクションの問題やhtmlspecialchars()が施されているので大まかな問題は解決されていると思いますが、あまりにも長すぎるとか不適切な内容などについては調べる必要があるでしょう。


mysql_delete($parms)

mysql_prepared()使って削除します。idについてはチェックしていませんが不適切なidなら削除されないだけなので実害は無いという判断です。


table_tr_html($attr, $tr_ary)

htmlの<tr>..</tr>の部分を生成します。


table_html($table_ary)

htmlの<table>..</table>の部分を生成します。


db_table_html($db_data)

dbの内容(二次元)をhtmlに変換する前段階として各要素を多少変更します。また削除のボタンを追加しています。


validate($vars, $request)

セキュリティの要としています。この部分をパスするということは処理を続行しても問題無い、あるいは大きな問題は無いと判断したということにしています。今回はhtmlspecialchars()だけしていますが、$requestで受け取るべき名前を列挙しているので、足りなかったり、逆に$requestにないものが$_POSTにあったら処理を止めるなどが可能です。


以下がメインの処理です。

set_error_handler("error_handler");

"error_handler"を設定します。エラーや問題を検知した場合はtrigger_error("メッセージ", E_USER_ERROR)で通知するようにしています。


$request = array('id','','title','category','launguage','link_ok','url','server_name');

$_POSTで受け取る名前を列挙します(テーブルのヘッダーとしても利用しているので途中に空の要素を入れていますが)。


$parms = validate($_POST, $request);

$_POSTの中で$requestで列挙されたものだけhtmlspecialchars()して$parmsに取り出します。


if ($parms && isset($_POST['op'])) {

if ($_POST['op'] === '新規')

mysql_insert(array_values($parms));

else if ($_POST['op'] === '削除')

mysql_delete(array($parms['id']));

}

ボタン用に"op"という名前を追加しています。本来"op"も$requestの中に入れて$parms['op']とすべきですが多少処理が長くなるので$_POSTを直接使っています。単純に新規(追加)、削除をしていますが追加の場合は、重複しないかチェックするなどの処理が必要かもしれません。新規の場合、prepared文の?と$parmsの数が合ってないとエラーになってしまいます。<input>でテキストが空の場合、送られないと思うので、それがvalidate()で処理されず、数が合わなくなってしまいます(既知のバグ)。


$db_all_data = mysql_prepared("SELECT * FROM site ORDER BY id DESC");

データを全て取り込む。


$db_table = array_merge(array(array_merge($request, array('uptime'))),

db_table_html($db_all_data),

array(array('',BR.'<form action="'.$_SERVER['SCRIPT_NAME'].'" method="post">'.

'<input type="submit" name="op" value="新規">',

'<input type="text" name="title" value="タイトル">',

'<input type="text" name="category" value="カテゴリー">',

'<input type="text" name="launguage" value="言語">',

'<input type="text" name="link_ok" value="リンク">',

'<input type="text" name="url" value="url">',

'<input type="text" name="server_name" value="サーバー">'.'</form>','')));

テーブル用にヘッダー用($requestに'uptime'を追加)、dbからのデータ、新規用の<input>をマージして置きます。

最終的にでhtmlに展開されます。


restore_error_handler();

エラーハンドラーを元に戻します。

id:taroemon

ご回答ありがとうございます。

完全に動く物と丁寧な説明ありがとうございます。

私には難しいことがかなりありましたので、それを調べるのに時間が必要でした。

お返事遅れて済みません。


ただ、私には高度すぎて理解できないことも多く、下記の点で困っています。

あまりに基本的で大変恐縮なのですが、

お時間のあるときにでもお答えいただければと思います。


質問①

新規ボタンを押すとちゃんとデータが追加するのですが、

別ページに作成したフォームからは追加されません。

エラーなどは出ませんが、追加されずただ表示されるだけです。

このスクリプトをどのように変更したらよいのでしょうか?

ひょっとしたら項目を追加したりしていじってる内に

別ページから登録できなくなったのかもしれませんので、

このスクリプトで問題なければその旨お知らせください。


質問②

表の1行1行が高いのでこれを低くしたいです。

「height="32"」の数値を小さくしたのですが、

最上段が狭まるだけで他は変わりません。どうしたら良いのでしょうか?


よろしくお願いします。

2008/04/10 02:32:20

その他の回答3件)

id:wizemperor No.1

回答回数379ベストアンサー獲得回数52

ポイント25pt

スクリプトはキリがないのでいくつかヒントを。(まともに書いたらこの何倍もの量になります。)

フォームから送信される値を配列にする処理は、既に書かれているように見えますが、個別に処理したいなら、

extract($_POST);

で変数に展開するのではなく、

//クエリ生成
// 実際には、クエリを生成する前に$_POSTの値が想定しているものか必ずチェックしましょう。
// つまり、質問のように楽をしようとすることは、現時点では難しいでしょうし、おすすめしません。
$query = 'value(';
foreach($_POST as $key => $value) {
  // ここに処理
  $query .= '"' . $value . '", ';
}
$query .= 'CURRENT_DATE()';
$query .= ')';
// この後、実際にクエリを送る前に、最低限、mysql_escape_string()でエスケープします。

のようにループで回してやるとよいでしょう。

あとはあまりに危険すぎるので、いくつか。

データベースにクエリを送る場合は、実際に送る全てのクエリに対して、最低限、


http://jp2.php.net/manual/ja/function.mysql-escape-string.php


を使ってエスケープしましょう。データが簡単に削除されたりしてしまいますよ。


それから、echoなどで出力する場合は、それも全てhtmlspecialchars($変数, ENT_QUOTES, 'utf-8')などでエスケープしましょう。

$_POSTの値をそのままチェックなし・エスケープなしで表示したり、データベースに送るのは危険です。


今のうちから、変数を出力したり、データベースにクエリとして使う場合は、きちんとその値が想定した範囲のものかチェックしたり、エスケープしておくようにすると後々楽です。

その他、例えば$idはおそらく数字ですよね。数字以外が入ってきた場合はデータベースにアクセスしないようにするなどです。

id:taroemon

ご回答ありがとうございます。


>質問のように楽をしようとすることは、現時点では難しいでしょうし、おすすめしません。

そういっていただけるときっぱりあきらめがつきます。


セキュリティは思ってたよりうまくいってませんでしたね。

それがわかってよかったです。教えていただいたことを参考に作り直してみます。


>スクリプトはキリがないのでいくつかヒントを。(まともに書いたらこの何倍もの量になります。)

やっぱりいろいろあるんですね。

もちろん特別気になるところだけでも結構です。大変参考になりました。

2008/04/06 00:32:01
id:pahoo No.2

回答回数5960ベストアンサー獲得回数633

ポイント25pt

なお回答には理論だけでなく、

実際に動くスクリプトをお示しくださいますようお願いします。

プログラムの仕様(目的)やDB構造が明らかではありませんから、そのお願いは無茶というものです。


・セキュリティ対策はこれでよいのか?

すぐに気がついた部分を指摘します。

$sv = "localhost";
$id = "xxxxxx";
$pass = "xxxxxxx";
$db = "link";

これらはセキュアな情報ですので、切り離して、リモートからアクセス不可能なパスに配置すべきです。include関数で読み込んでください。

$value = htmlspecialchars($value);

これだけではSQLインジェクション攻撃を受けます。SQL文に投入する変数については、mysql_real_escape_string 関数でエスケープすべきです。ただし、各々の変数に日本語が入るのかどうか分からないですし、その場合の文字コードが何になるのかも分からないので、一概に mysql_real_escape_string 関数を使えばいいというものではありません。

このようにプログラムの仕様が分からない状況では、具体的なコードをお示しできません。

$sql = "DELETE FROM site WHERE id = {$_POST['id']}";

SQLインジェクション」攻撃を受けます。リンク先のアドバイスに従って対策を立ててください。

echo "<form method=\"POST\" action=\"".$_SERVER[PHP_MYSELF]."\">";

$_SERVER[PHP_MYSELF] は公式マニュアルに見あたりませんが、$_SERVER['PHP_SELF'] と同等でしょうか? だとしたら、$_SERVER['SCRIPT_NAME'] に置き換えてください。理由は、「PHP PHP_SELFとSCRIPT_NAMEを比較してみた」を参照ください。


・フォームから送信される値を全部配列として処理したい。

コードを見ると、すべて$_POST配列に入っているので、お望みの処理ができるのではないかと思うのですが‥‥。

id:taroemon

いつもご回答ありがとうございます。


>プログラムの仕様(目的)やDB構造が明らかではありませんから、

>そのお願いは無茶というものです。

>プログラムの仕様が分からない状況では、具体的なコードをお示しできません。

もっともなことです。私の見識不足で失礼しました。

全部作り直していただかなくても、

一部だけでもわかる範囲でお示しいただければと思いました。

どのような情報を提示すべきか教えていただければお答えします。

それさえもわからぬ初心者ですのでなにとぞ平にご容赦くださいませ。


>リモートからアクセス不可能なパスに配置すべきです。include関数で読み込んでください。

実は気になってた部分だったので、非常に参考になりました。

他のセキュリティも教えていただいたページを参考にしてもっと勉強してみます。


>すべて$_POST配列に入っているので、お望みの処理ができるのではないかと思うのですが‥‥。

これは明らかに質問の仕方が間違えてますね。

データベースにデータを追加以降の項目で、たとえば下記のようになってますが、

$query = "insert into site (title,category,launguage,link_ok,url,server_name,uptime)";

この部分の項目が増減してもいちいち書き直さないでも良いようにしたいと言うことです。


実は削除の項目に取り組んで3日目になるのですが、

どうしてもうまくいかず途方に暮れていました。

不足した情報が何かもお知らせいただければお知らせしますので

今後ご回答いただく皆さんにはこの部分に対するご指導もお待ちしています。

2008/04/06 01:12:44
id:tezcello No.3

回答回数460ベストアンサー獲得回数69

ポイント120pt

横長に書くと解答欄からはみ出してしまうので、縦長になるように書いています。

一応動作確認はしてみましたが、環境により多少修正が必要かもしれません。

(例えば、内部文字エンコードとか、mbstring.encoding_translation とか、マジッククォートとか)

当方のテスト環境では MySQL が使えないので、SQLite でテストしています。

MySQL を使われるのでしょうから、適宜読み替えて下さい。

  関連して、テストフォルダはスクリプトから書き込みできなかったので、直下に

  db という書き込み可能なフォルダを作ってテストしました。


DB構造が分からないので、DBのオープン時に適当な値で2レコード追加するようにしています。既にDBが存在するなら、不要です。


削除できないのは、$_POST['id'] から削除するIDを得ているが、削除用IDは $_POST['del'] だからでは?


> フォームから送信される値を全部配列として処理したい。

下記の例で、フィールド名を name="new[title]" にして対応しています。最近はてなで教えてもらった方法です。

これによって、

> つまり項目が増えてもいちいちそれを書き直さなくても良いように

は、部分的には達成できます(下記の例の insertData() や array_walk() )が、

DB定義時のカラム順と表示の順序が一致しないと、このような配列でまわす方法は使えません。

(連想配列は順序を期待しない使い方が本来ですし---PHPでは順序は保持されるらしいけど)

更に、入力値のチェックに個別に最適なものを用いるのが面倒な事になりませんか?

  今回は、ゼロ値の削除と、DB用のエスケープ、削除IDが数値である事 をしましたが

  URLのチェックや言語などもっと絞り込む方法があると思います。


データを表示する際に、str_replace で置き換えていますが、DBには http://www.hoge.com

のように入れなくてはいけないのでしょうか?(下記の例では同じようにしていますが)

使い方が決まっているなら、登録時のチェックで処理した方が分かりやすいと思います。

(当然専用データとなってしまいますから、他に流用は出来なくなりますが)

また、このアドレスの場合表示されるのは、.hoge.com とドットから始まるのは想定通りですか?

https://abc.fuga.com/iwwwa/ のようなアドレスが入力される事は考えなくてもよいですか?


フォームを各行毎に入れるのではなく、テーブル全体を一つのフォーム内に収めました。

質問のスクリプトでは、どのボタンが押されたのかを判断できないとの理由で複数に分けたのだと予想しますが、こんな方法もあると思います。


日付(と思われる)は、UNIXタイムスタンプにしてみました。きっと経過時間などで使われるでしょうから、人間が読み易い値にしておく必要もないかと思って。

CURRENT_DATE() という関数もどのように定義されているか分かりませんので。

<?php
// DBの情報
$dbname = 'db/link';

$db_default_table = '
  id integer primary key,
  title text,
  category text,
  launguage text,
  link_ok text,
  url text,
  server_name text,
  uptime datetime';

// DBのオープン
if ($db = sqlite_open($dbname, 0666, $sqliteerror)) {
  $result = @sqlite_query($db, 'SELECT * FROM site ORDER BY id DESC LIMIT 1', SQLITE_ASSOC);
  if (!$result){
    // テーブルが存在しないと思われるので新規に作る(データも適当に追加する)
    sqlite_query($db, 'CREATE TABLE site ('.$db_default_table.')');
    sqlite_query($db, 'INSERT INTO  site VALUES (NULL, "タイトル", "カテゴリ1", "日本語", "リンクOK", "http://web.page.com", "サーバ名",'.time().')');
    sqlite_query($db, 'INSERT INTO  site VALUES (NULL, "たいとる", "カテゴリ2", "日本語", "リンクOK", "http://www.hogehoge.fuga.com", "server",'.time().')');
  }
}else{
  exit($sqliteerror);
}

// データを削除する
if ($key = array_search('削除', $_POST)) deleteData($db, $key);

// データを追加する
if (isset($_POST['add'])){
  array_walk($_POST['new'], 'quotevalues');
  insertData($db);
}

showAllData($db);

sqlite_close($db);


// データを追加
function insertData($d){
  $query = 'INSERT INTO  site (';
  $values = 'VALUES  (';

  foreach ($_POST['new'] as $k=>$v){
    $query .= $k.', ';
    $values .= '"'.$v.'", ';
  }
  $query .= 'uptime) ';
  $values .= time().')';

  $res = sqlite_query($d, $query.$values);
  // 必要なら追加出来なかった場合の処理

}

// 不適文字をエスケープ
function quotevalues(&$value, $key){
  $value = str_replace( "\0", '', $value);
  $value = sqlite_escape_string($value);
}

// データを削除
function deleteData($d, $k){
  preg_match('/sub(\d+)/', $k, $id) or exit('削除キーがありません');

  $res = sqlite_query($d, 'DELETE FROM site WHERE id = '.$id[1]);
  // 必要なら削除出来なかった場合の処理

  return;
}

// 全データの表示
function showAllData($d){
  $res = sqlite_unbuffered_query($d, 'SELECT * FROM site ORDER BY id DESC', SQLITE_ASSOC);
  // 項目名
  print
    '<tr>'
      .'<td>id</td>'
      .'<td></td>'      // 空
      .'<td>題名</td>'
      .'<td>分類</td>'
      .'<td>言語</td>'
      .'<td>サイト名</td>'
      .'<td>URL</td>'
      .'<td>リンク</td>'
      .'<td>日付</td>'
    .'</tr>'."\n";

  //データを表示する
  $lastID = 1;
  while ($row = sqlite_fetch_array($res, SQLITE_ASSOC)){
    print
       '<tr>'
        .'<td>'.$row['id'].'</td>'
        .'<td><input type="submit" value="削除" name="sub'.$row['id'].'"></td>'    //削除用ボタン
        .'<td>'.htmlspecialchars($row['title'], ENT_QUOTES, 'UTF-8').'</td>'
        .'<td>'.htmlspecialchars($row['category'], ENT_QUOTES, 'UTF-8').'</td>'
        .'<td>'.htmlspecialchars($row['launguage'], ENT_QUOTES, 'UTF-8').'</td>'
        .'<td>'.htmlspecialchars($row['server_name'], ENT_QUOTES, 'UTF-8').'</td>'
        .'<td>'.htmlspecialchars(preg_replace('!(http://|www)!', '', $row['url']), ENT_QUOTES, 'UTF-8').'</td>'
        .'<td>'.htmlspecialchars($row['link_ok'], ENT_QUOTES, 'UTF-8').'</td>'
        .'<td>'.htmlspecialchars(date('Y/m/d', $row['uptime']), ENT_QUOTES, 'UTF-8').'</td>'
      .'</tr>'."\n";
    $lastID++;
  }

  // 追加用の入力フィールド
  print 
    '<tr><td>'.$lastID.'</td>'
      .'<td><input type="submit" value="追加" name="add"></td>'
      .'<td><input type="text" name="new[title]"></td>'
      .'<td><input type="text" name="new[category]"></td>'
      .'<td><input type="text" name="new[launguage]"></td>'
      .'<td><input type="text" name="new[server_name]"></td>'
      .'<td><input type="text" name="new[url]"></td>'
      .'<td><input type="text" name="new[link_ok]"></td>'
    .'</tr>'."\n";
}
?>
id:taroemon

ご回答ありがとうございます。

すみません。全部作っていただけるとは思いませんでした。

わざわざありがとうございます。

大変参考になりました。

2008/04/08 18:39:53
id:tobeoscontinue No.4

回答回数220ベストアンサー獲得回数59ここでベストアンサー

ポイント120pt

>うまく削除できません。

削除ボタンを押した場合、hiddenでの名前が"del"になっているためです。ので"del"を"id"にすればokでしょう。ただ<form>が閉じていないので正しいidが戻っていません。</form>で閉じることで正しいidは戻りますが$_POST["title"]が設定されていないと

if (!isset($_POST["title"])){

の部分が実行されて終了していました。


>$query = "insert into site (title,category,launguage,link_ok,url,server_name,uptime)";

>この部分の項目が増減してもいちいち書き直さないでも良いようにしたいと言うことです。

php側だけではなくMySQL側でも直さなくてはならないのでその都度(頻繁に変更があることの方が問題)、書き換えでもいいと思います。

あるいは新たに追加がある場合はその部分だけのupdate文で処理するようにするなど。


エラー処理はやっかいです。ユーザーには詳細な内容を知らせない方がいいですし、自分には詳細な内容が必要です。ある程度の記述が必要ですのでset_error_handler()が便利です。後はエラーが発生したらtrigger_error()を呼ぶだけでset_error_handler()で指定した関数へ飛びます。


phpはhtml文の中に混在して書き込めるのが特徴の一つです。単純な場合はそれでいいのですが、複雑になるとこれが逆に足かせになってきます。今回は更にSQL文もあるので三つのものが混在しています。

提案としてはこれらはなるべく別々にするようにし、それらをつなぐものとして配列を使います。


$_POST,$_GET,$_COOKIE,$_SERVERなどのPHPの外部から来る変数は信用できません。

ので使用には常に注意が必要です。常に言えることは直接使ってはダメということです。


SQLインジェクションについて記憶に新しいものとしてはトレンドマイクロがあります。

http://jp.trendmicro.com/jp/about/notice/0312/index.html

考え方として攻撃を無害化するというのがmysql_real_escape_stringに代表されるものでしょう。

私の場合は攻撃を感知するということに重点を置いています。どういう攻撃があるかがわからないと感知できないのが難点ですが。

考え方が違えば、やり方も変わってきますし、どれが正解ということはないと思います。ようは攻撃を防げればいいのですから。


prepared文はMySQL 4.1から追加されたようです

http://lists.mysql.com/mysql-ja/77

これを使えば、あらかじめquery文を評価してしまい、後でexecute文で引数で置き換えて実行するというものです。

ので外部から与えられるものは単なるデータになるのでSQLインジェクションにはならないと理解しています(多分)。

プリペアドステートメントに渡すパラメータは、引用符で括る必要は ありません。それはドライバが自動的に行います。 アプリケーションで明示的にプリペアドステートメントを使用するように すれば、SQL インジェクションは決して発生しません (しかし、もし信頼できない入力をもとにクエリの他の部分を構築している のならば、その部分に対するリスクを負うことになります)。

プリペアドステートメントおよびストアドプロシージャ

mysql_xxxでは使えないようなのですがPEAR::DBなら使えます(多少オーバーヘッドは発生するでしょうが)。

php5ではPEAR::DBがうまく動かなかったのでPDOで実装しました。mysqliが改良版拡張サポートということでprepared文が使えるのですが使いかたが面倒そうなので断念しました。

<?php
define('CR', "\n");
define('BR', "<br>");
define('DB_NAME', 'link');
define('USER',    'xxxxxx');
define('PASSWD',  'xxxxxxx');

//date_default_timezone_set('Asia/Tokyo');

function error_handler($errno, $errstr, $file, $line, $context) {
  static $errortype = array (
    E_ERROR              => 'Error',
    E_WARNING            => 'Warning',
    E_PARSE              => 'Parsing Error',
    E_NOTICE             => 'Notice',
    E_CORE_ERROR         => 'Core Error',
    E_CORE_WARNING       => 'Core Warning',
    E_COMPILE_ERROR      => 'Compile Error',
    E_COMPILE_WARNING    => 'Compile Warning',
    E_USER_ERROR         => 'User Error',
    E_USER_WARNING       => 'User Warning',
    E_USER_NOTICE        => 'User Notice',
    E_STRICT             => 'Runtime Notice',
    E_RECOVERABLE_ERROR  => 'Catchable Fatal Error');
  echo $errortype["$errno"].':'.$file.':'.$line.'<br>'.$errstr.'<br>'.'<br>';
  return TRUE;
}

function mysql_prepared($sql, $parms=NULL) {
  $mysql = new PDO('mysql:host=localhost;dbname='.DB_NAME, USER, PASSWD);
    $stmt = $mysql->prepare($sql);
    $stmt->execute($parms);
  return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

function mysql_insert($parms) {
  $sql = "insert into site (title, category, launguage, link_ok, url, server_name, uptime)".
         " values (?, ?, ?, ?, ?, ?, CURRENT_DATE())";
  return mysql_prepared($sql, $parms);
}

function mysql_delete($parms) {
  $sql = "DELETE FROM site WHERE id = ?";
  return mysql_prepared($sql, $parms);
}

function table_tr_html($attr, $tr_ary) {
  $contents = '';
  foreach ($tr_ary as $col)
    $contents .= '<td>'.$col.'</td>';
  return '<tr'.$attr.'>'.$contents.'</tr>';
}

function table_html($table_ary) {
  $contents = table_tr_html(' bgcolor="#88bbff" height="32"', array_shift($table_ary));
  foreach ($table_ary as $id=>$row)
    $contents .= table_tr_html($id%2 ? ' bgcolor="#fafaff"':'', $row);
  return '<table>'.$contents.'</table>';
}

function db_table_html($db_data) {
  $map = array();
  foreach ($db_data as $row)
    $map[] = array(
      $row['id'],
      BR.'<form action="'.$_SERVER['SCRIPT_NAME'].'" method="post">'.
        '<input type="submit" name="op" value="削除">'.
        '<input type="hidden" name="id" value="'.$row['id'].'">'.'</form>',
      $row['title'],
      $row['category'],
      $row['launguage'],
      $row['link_ok'],
      str_replace(array('http://','www.'),array('',''),$row['url']),
      $row['server_name'],
      date('Ymd',strtotime($row['uptime'])));
  return $map;
}

function validate($vars, $request) {
  $parms = array();
  foreach ($request as $name)
    if (isset($vars["$name"]))
      $parms["$name"] = htmlspecialchars($vars["$name"]);
  return $parms;
}

set_error_handler("error_handler");
  $request = array('id','','title','category','launguage','link_ok','url','server_name');
  $parms = validate($_POST, $request);
  if ($parms && isset($_POST['op'])) {
    if ($_POST['op'] === '新規')
      mysql_insert(array_values($parms));
    else if ($_POST['op'] === '削除')
      mysql_delete(array($parms['id']));
  }

  $db_all_data = mysql_prepared("SELECT * FROM site ORDER BY id DESC");
  $db_table = array_merge(array(array_merge($request, array('uptime'))),
    db_table_html($db_all_data),
    array(array('',BR.'<form action="'.$_SERVER['SCRIPT_NAME'].'" method="post">'.
        '<input type="submit" name="op" value="新規">',
        '<input type="text" name="title" value="タイトル">',
        '<input type="text" name="category" value="カテゴリー">',
        '<input type="text" name="launguage" value="言語">',
        '<input type="text" name="link_ok" value="リンク">',
        '<input type="text" name="url" value="url">',
        '<input type="text" name="server_name" value="サーバー">'.'</form>','')));
restore_error_handler();
?>
<html>
<head>
</head>
<body>
<?= table_html($db_table) ?>
</body>
</html>

動作確認はphp5、mysql4.1以上でdefineのUSER,PASSWDを設定してあれば動くとは思います。


error_handler()

set_error_handler("error_handler")としているのでエラーがあればここに飛んできます。そのまま表示していますのでユーザーには別のメッセージを表示して、自分にはメールを飛ばすようにするなどにした方がいいでしょう。


mysql_prepared()

prepared文を使ってquery()します。


mysql_insert($parms)

mysql_prepared()使って挿入します。挿入する内容についてはチェックしていません。prepared文を使っているのでSQLインジェクションの問題やhtmlspecialchars()が施されているので大まかな問題は解決されていると思いますが、あまりにも長すぎるとか不適切な内容などについては調べる必要があるでしょう。


mysql_delete($parms)

mysql_prepared()使って削除します。idについてはチェックしていませんが不適切なidなら削除されないだけなので実害は無いという判断です。


table_tr_html($attr, $tr_ary)

htmlの<tr>..</tr>の部分を生成します。


table_html($table_ary)

htmlの<table>..</table>の部分を生成します。


db_table_html($db_data)

dbの内容(二次元)をhtmlに変換する前段階として各要素を多少変更します。また削除のボタンを追加しています。


validate($vars, $request)

セキュリティの要としています。この部分をパスするということは処理を続行しても問題無い、あるいは大きな問題は無いと判断したということにしています。今回はhtmlspecialchars()だけしていますが、$requestで受け取るべき名前を列挙しているので、足りなかったり、逆に$requestにないものが$_POSTにあったら処理を止めるなどが可能です。


以下がメインの処理です。

set_error_handler("error_handler");

"error_handler"を設定します。エラーや問題を検知した場合はtrigger_error("メッセージ", E_USER_ERROR)で通知するようにしています。


$request = array('id','','title','category','launguage','link_ok','url','server_name');

$_POSTで受け取る名前を列挙します(テーブルのヘッダーとしても利用しているので途中に空の要素を入れていますが)。


$parms = validate($_POST, $request);

$_POSTの中で$requestで列挙されたものだけhtmlspecialchars()して$parmsに取り出します。


if ($parms && isset($_POST['op'])) {

if ($_POST['op'] === '新規')

mysql_insert(array_values($parms));

else if ($_POST['op'] === '削除')

mysql_delete(array($parms['id']));

}

ボタン用に"op"という名前を追加しています。本来"op"も$requestの中に入れて$parms['op']とすべきですが多少処理が長くなるので$_POSTを直接使っています。単純に新規(追加)、削除をしていますが追加の場合は、重複しないかチェックするなどの処理が必要かもしれません。新規の場合、prepared文の?と$parmsの数が合ってないとエラーになってしまいます。<input>でテキストが空の場合、送られないと思うので、それがvalidate()で処理されず、数が合わなくなってしまいます(既知のバグ)。


$db_all_data = mysql_prepared("SELECT * FROM site ORDER BY id DESC");

データを全て取り込む。


$db_table = array_merge(array(array_merge($request, array('uptime'))),

db_table_html($db_all_data),

array(array('',BR.'<form action="'.$_SERVER['SCRIPT_NAME'].'" method="post">'.

'<input type="submit" name="op" value="新規">',

'<input type="text" name="title" value="タイトル">',

'<input type="text" name="category" value="カテゴリー">',

'<input type="text" name="launguage" value="言語">',

'<input type="text" name="link_ok" value="リンク">',

'<input type="text" name="url" value="url">',

'<input type="text" name="server_name" value="サーバー">'.'</form>','')));

テーブル用にヘッダー用($requestに'uptime'を追加)、dbからのデータ、新規用の<input>をマージして置きます。

最終的にでhtmlに展開されます。


restore_error_handler();

エラーハンドラーを元に戻します。

id:taroemon

ご回答ありがとうございます。

完全に動く物と丁寧な説明ありがとうございます。

私には難しいことがかなりありましたので、それを調べるのに時間が必要でした。

お返事遅れて済みません。


ただ、私には高度すぎて理解できないことも多く、下記の点で困っています。

あまりに基本的で大変恐縮なのですが、

お時間のあるときにでもお答えいただければと思います。


質問①

新規ボタンを押すとちゃんとデータが追加するのですが、

別ページに作成したフォームからは追加されません。

エラーなどは出ませんが、追加されずただ表示されるだけです。

このスクリプトをどのように変更したらよいのでしょうか?

ひょっとしたら項目を追加したりしていじってる内に

別ページから登録できなくなったのかもしれませんので、

このスクリプトで問題なければその旨お知らせください。


質問②

表の1行1行が高いのでこれを低くしたいです。

「height="32"」の数値を小さくしたのですが、

最上段が狭まるだけで他は変わりません。どうしたら良いのでしょうか?


よろしくお願いします。

2008/04/10 02:32:20
  • id:wizemperor
    回答の関数名、一部間違えたかもしれません。
    もし間違えてたら読み替えてくださいね。
    リンク先をちゃんと見てもらえばすぐに気づくと思いますけど、mysql_real_escape_string()ですので。
  • id:taroemon
    wizemperorさん
    丁寧なコメントありがとうございます。
    全て了解しました。
  • id:tobeoscontinue
    たくさんのポイントありがとうございます。

    >ただ、私には高度すぎて理解できないことも多く、下記の点で困っています。
    行数を減らすため変数に代入したほうが解りやすい部分でも直接引数に渡すなどしているので理解しにくくなっていると思います。
    また
    mysql_insert(array_values($parms));
    のようになぜarray_values()しているのかphpのマニュアルのPDOの部分を読まないと解からないこともあります。

    validate()はQuickFormからのパクリです。

    まぁそのように説明していない部分が多々あるので理解しにくいのはいたしかたないのかなぁと。

    セキュリティを除けば二次元の配列を経由するというのが私の案の肝。
    データベースの内容を二次元の配列に入れ、
    二次元の配列をhtmlに変換する。それだけです。
    途中でフォームのhtmlも二次元の配列の中に入れていますが、中に何が入っているか気にしません。画像の<img>を入れれば画像のテーブルになります。
    質問の趣旨(削除されない)とそれていますが。

    質問①
    オリジナルに無かったnameがopという名前のボタン(Submit)を使っておりその内容が"新規"だったら登録するようにしています。
    別ページで作成したフォームでボタン(Submit)を
    <input type="submit" name="op" value="新規">
    としていただくか
    <input type="hidden" name="op" value="新規">
    を追加してもらえれば動くのではないかと。

    あるいはオリジナルにあるようにtitleがあれば追加するようにするのであれば
    if ($_POST['op'] === '新規' || isset($parms['title']))
    mysql_insert(array_values($parms));
    また
    if (isset($parms['title']))
      mysql_insert(array_values($parms));
    など。

    質問②
    ボタン(削除)の表示位置と、<td>でのテキストの位置が合わなかったので先頭にBRをいれています。
    BR.'<form action="'.$_SERVER['SCRIPT_NAME'].'" method="post">'.

    array(array('',BR.'<form action="'.$_SERVER['SCRIPT_NAME'].'" method="post">'.
    にあるBR.を削除して下さい。
    位置調整にCSSなどを使う場合、<tr>にはtable_tr_html($attr, $tr_ary)の$attrの部分に' class="xxx"'に指定することは可能ですが<td>の部分には書けません。書けるようにするにはもう少し修正が必要になります。ある程度決まったhtmlの生成ならtable_html()のような書き方もいいのですが自由度が必要な場合は逆に足を引っ張られてしまうのが欠点です。
  • id:taroemon
    tobeoscontinueさん。
    丁寧なコメントありがとうございます。

    > まぁそのように説明していない部分が多々あるので理解しにくいのはいたしかたないのかなぁと。
    いえ。私が本当に初心者なので・・・。はずかしいです。
    でもいただいたスクリプトを参考にして、勉強したら2次元配列もわかるようになりました。

    > セキュリティを除けば(中略)
    > 質問の趣旨(削除されない)とそれていますが。
    いろいろ教えていただいてありがとうございます。
    今後に使えそうで、大変参考になりました。

    今、ちょっと時間がなくて、質問①、質問②を試してません。でも理解はできました。
    わからないことなど、あらためて質問し直すか、ポイント送信して直接うかがうかもしれません。
    その節はよろしくお願いします。

この質問への反応(ブックマークコメント)

「あの人に答えてほしい」「この質問はあの人が答えられそう」というときに、回答リクエストを送ってみてましょう。

これ以上回答リクエストを送信することはできません。制限について

回答リクエストを送信したユーザーはいません