cgilib2を利用して、簡単な写真日記システムを作成してみる。
簡素な日記システムとして、以下のような仕様のものを考える。
登録されたものを表示する機能において、 本文と写真を同時に出力したいところではあるが、SQL 文でテキストと画像表示(<img src="...">)を同時に出すことはできないため、 第1段階として「日時」、「本文」、「ファイル名」のみHTMLの表形式で出力し、 ファイル名部分をクリックすることで画像が出るように作成してみよう。
まず、必要なテーブルを以下のように設計する。
カラム | 意味 | 制約 |
---|---|---|
datetime | 日付 | UNIQUE |
body | 本文 | |
filename | 画像ファイル名 | |
filecontent | データ |
これに対応するSQL文は以下のようになる。
CREATE TABLE blog(datetime UNIQUE, body, filename, filecontent);
これを入力させるためのHTML文の概略は以下のようになる。
本文と添付画像の入力フォーム
<form method="POST" enctype="multipart/form-data" action="./storeblog.cgi">
<p>一言:</p>
<textarea name="hitokoto" cols="40" rows="3">
</textarea>
<p><label>添付ファイル:<input name="attach" type="file"></label></p>
<p><input type="submit" value="投稿">
<input type="reset" value="リセット"></p>
</form>
action=... に指定した「storeblog.cgi」がこれから作成する予定のものである。 「日時」は投稿時の時刻から自動生成するため入力フォームは入れていない。
上記のHTML断片は以下の画面例のような入力フォームを出す。
ここに、一言=「あいうえお」、添付ファイル=「bar.jpg」 の入力を与えて「投稿」ボタンを押した場合を考える。cgilib2 でこれを受け取ると cgipars テーブルに以下のように値が格納される。 他のセッションでの入力値も混在する様子を tag カラムの値を単純化して示してある(※)。
※ cgipars のタグ分けについては「プロセスごとのフォームデータ分離」に説明がある。
tag | name | val | filename |
---|---|---|---|
tag_A | hitokoto | いろはにほへと | NULL |
tag_A | attach | foo.jpgの中味 | foo.jpg |
tag_B | hitokoto | あいうえお | NULL |
tag_B | attach | bar.jpgの中味 | bar.jpg |
tag_C | hitokoto | 本日は晴天なり | NULL |
tag_C | attach | NULL | NULL |
tag_D | hitokoto | NULL | NULL |
tag_D | attach | hoge.jpgの中味 | hoge.jpg |
今回の入力例に相当するのはtagカラムが tag_B の2行である。 blogテーブルに挿入するなら以下のように問い合わせ文を構築する。
REPLACE INTO blog VALUES(
日時, 'あいうえお', 'bar.jpg', bar.jpgの中味
);
実際にはVALUES句で即値を指定するのではなく、cgipars テーブルからの問い合わせを用いて挿入するカラム並びを構築する。 このときに使う構文が、副問い合わせによる行挿入である。
INSERT INTO テーブル1 副問い合わせ;
副問い合わせによって得られるカラム並びが テーブル1 に入る。これを応用するために、 cgipars から tag='tag_B' に当てはまるものを「横持ち」に変換する。 縦持ちから横持ちへの変換は「記入結果一覧出力」の問い合わせ文で行なったものを参考に、以下のように組み立てる。
まず、CASE文で縦のカラムを横に展開する。
SELECT CASE name WHEN 'hitokoto' THEN val END,
CASE name WHEN 'attach' THEN filename END,
CASE name WHEN 'attach' THEN val END
FROM cgipars
WHERE tag='tag_B';
取り出されるものを表形式で表すと以下のようになる。
あいうえお | ||
bar.jpg | bar.jpgの中味 |
ここから集約関数maxでNULLカラムを消し去る。
SELECT max(CASE name WHEN 'hitokoto' THEN val END),
max(CASE name WHEN 'attach' THEN filename END),
max(CASE name WHEN 'attach' THEN val END)
FROM cgipars
WHERE tag='tag_B'
GROUP BY tag;
これで首尾よく以下の結果が得られる。
あいうえお | bar.jpg | bar.jpgの中味 |
この3つのカラムの前に日付カラムのための値を付ければ blog テーブルに挿入できる形となる。よって、cgipars テーブルに入力されたばかりのフォーム値を blog テーブルにコピーする問い合わせ文は以下のようになる。
REPLACE INTO blog
SELECT 日時の文字列,
max(CASE name WHEN 'hitokoto' THEN val END),
max(CASE name WHEN 'attach' THEN filename END),
max(CASE name WHEN 'attach' THEN val END)
FROM cgipars
WHERE tag=抽出するtagの値
GROUP BY tag;
日時の文字列はシェルスクリプトで「date "+%F %T"」で得ればよい(※)。
もしくは SQLite 関数でそれと同じ値を返す datetime('now', 'localtime') でもよい。
これらをまとめて、フォームに入力された「一言」と添付ファイルを blog テーブルに入れるだけのスクリプトstoreblog.cgiを示す。
#!/bin/sh
cd `dirname $0`
. ./cgilib2-sh
htmlhead "一言+写真の登録"
cat<<EOF
<form method="POST" enctype="multipart/form-data" action="./storeblog.cgi">
<p>一言:</p>
<textarea name="hitokoto" cols="40" rows="3"></textarea>
<p><label>添付ファイル:<input name="attach" type="file"></label></p>
<p><input type="submit" value="投稿">
<input type="reset" value="リセット"></p>
</form>
EOF
hitokoto=`getpar hitokoto`
if [ -n "$hitokoto" ]; then
date=`date "+%F %T"` # キーとなる日時は自動設定する
query<<-EOF
REPLACE INTO blog
SELECT '$date',
max(CASE name WHEN 'hitokoto' THEN val END),
max(CASE name WHEN 'attach' THEN filename END),
max(CASE name WHEN 'attach' THEN val END)
FROM cgipars
WHERE tag='$_tag'
GROUP BY tag;
EOF
echo "<p>$date の日記として登録しました</p>"
fi
echo "</body></html>"
上記のSQL文により、投稿された値はblogテーブルに以下のように格納される。
カラム | datetime | body | filename | filecontent |
---|---|---|---|---|
値 | 2015-11-01 21:22:23 | あいうえお | bar.jpg | bar.jpgの中味 |
CGIスクリプトでこれを出力したい。SELECT 文で出すとしても、画像(filecontent カラム)はそのままバイナリ列が出力されるだけで画像には見えない。 SELECT文による出力では画像ファイルのリンクだけ出し、 リンクをクリックしたら画像が出るようにしてみる。つまり、
日時 | 一言 | 添付ファイル |
---|---|---|
2015-11-01 21:22:23 | あいうえお | bar.jpg |
の3カラムの出力とし、bar.jpg の部分をクリックすると画像が現れるようにしたい。 bar.jpg にリンクを張るので
<a href="bar.jpgを出すためのURL">bar.jpg</a>
と出力したいのだが、これはSQLite3のSQL文一発では行かない。なぜなら、 a要素を埋め込むように問い合わせ文を工夫しても 「<」や「>」などは実体参照に変換されてそれぞれ 「<」、「>」に変換されるためリンクとして作用しない。
この2点について知る必要がある。
SQLite3のHTML出力モードでは、出力値自体にHTMLタグを埋められない。 セキュリティ的には好ましい設計で、たとえばデータに JavaScript コードなどを埋められて、 それがそのままブラウザに送信されたらどんな被害が起きるか分からない。
よって、タグを埋めたい場合は sqlite3 からの HTML 出力に記号を付けておき sed などで所望のタグに変換する方法を取る。たとえば、表 blog テーブルから 「blogテーブルに格納された1件の例」にある最初の 3つのカラムを HTML モードで取り出すには
SELECT datetime, body, filename FROM blog WHERE datetime='2015-11-01 21:22:23';
とする。これによる出力は以下のようになる(改行そのまま)。
<TR><TD>2015-11-01 21:22:23</TD> <TD>あいうえお</TD> <TD>bar.jpg</TD> </TR>
どんな場合でも「<TD>」は sqlite3 コマンドからの文字列であるから、 この直後にカラムの値を特徴づけるタグを埋め込んでおき、sed などでそれを置換する規則を考える。つまり、
sqlite3 -header -html DBファイル SQL文 | sed 置換パターン
のようなコマンド起動の流れでa要素によるリンクを作成するということである。 一例を示す。
SELECT '1:' || datetime "日時",
'2:' || body "一言",
'3:' || filename '添付ファイル'
FROM blog WHERE datetime='2015-11-01 21:22:23';
とすると、HTMLモードで生成されるものは以下のようになる。
<TR><TD>1:2015-11-01 21:22:23</TD> <TD>2:あいうえお</TD> <TD>3:bar.jpg</TD> </TR>
この出力をパイプの先で標準入力として受けて、
のようにする。なお、置換の必要のないカラムにも 1: や 2: を作るのは、一言欄にユーザが「3:」で始まる文章を入れた場合に不要な置換が 起きるのを防ぐためである。sedは1行ずつ読んで処理を進めるので、 読み込んでいる <TD> が何カラム目かを判断できない。 もっと高水準のスクリプト言語を使えばカラムの個数を数えた処理は可能だが、 何カラム目かを最初からカラム出力に入れてしまえば シンプルなアルゴリズムで処理できる。
上記のようなタグ付加を前提としたパターン置換を sed で行なうのであれば、以下のようなコマンドラインとなる。
... | sed -e 's,<TD>[12]:,<TD>,' \
-e 's,<TD>3:\(.*\)</TD>,<TD><a href="URL">\1</a></TD>,'
-e オプションの1つ目、「<TD>」の後ろに「1または2」がきて その後ろに「:」が来る場合は、「<TD>」だけに置換する。 -e オプションの2つ目、「<TD>:3」の後ろに任意の文字列が来て 「</TD>」が続いている場合は任意の文字列の部分が アンカー文字列となるようにa要素でリンクを示す文字列に置換する。 このURLの部分は次の節で考える。
Webブラウザからアクセスがあったときに、 バイナリデータの種別とバイトサイズを適切に返してから バイナリデータを標準出力に書き出すとブラウザは画像や動画であればそれを表示し、 アプリケーションと結び付いたデータであればそれに応じたアクションをしてくれる。 たとえば、ブラウザに対して
Content-type: image/jpeg
Content-Disposition: filename="photo.jpg"
Content-Length: 123456
バイナリデータ...
のようなストリームを送り返すと、123456バイトの JPEG ファイルとみなして処理してくれる。これをふまえて、特定の URL にアクセスがあったときに、そのURLに応じたバイナリデータを データベースから取り出して返すようなスクリプトを作成する。
まず、単純な実験スクリプトを作ろう。CGI プログラム実行の許されたディレクトリで以下のようにテストデータを作る。
: 一時ファイル用ディレクトリを作成(もしなければ)
mkdir -m 1777 tmp
: テスト用データベースのテーブル作成
sqlite3 tmp/binary.sq3 "CREATE TABLE bin(filename, filecontent);"
: 実験用画像ファイルの作成(ImageMagick)使用
import foo.jpg # 画面の適当な領域をキャプチャする。他の画像でもよい。
: importコマンドがなければGIMPなど画像作成ツールで何かJPEG画像を作成する
: 一時的にhexize関数を定義
hexize() {
perl -ne 'print unpack("H*", $_)'
}
: sqlite3で foo.jpg を挿入(hexize関数を使用)
sqlite3 tmp/binary.sq3<<EOF
INSERT INTO bin VALUES('foo.jpg', X'$(hexize < foo.jpg)');
EOF
このデータベースファイルから、foo.jpg を画像として Web クライアントに返すCGIの最も単純な部類の例 catfoo.cgi を以下に示す。
#!/bin/sh
PATH=/usr/local/sqlite3/bin:$PATH
binfile=./tmp/binary-data
cd `dirname $0` # スクリプトと同じディレクトリに移動
unhexize() {
perl -n -e 's/([0-9a-f]{2})/print chr hex $1/gie'
}
sqlite3 tmp/binary.sq3 <<EOF | unhexize > $binfile # 画像を一時ファイルに書き出す
SELECT quote(filecontent) FROM bin WHERE filename='foo.jpg';
EOF
cat<<EOF
Content-type: `file --mime-type $binfile | cut -d' ' -f2`
Content-Disposition: filename="foo.jpg"
Content-length: `wc -c < $binfile`
EOF
cat $binfile
rm -f $binfile
実験として、コマンドラインで以下のように起動すると、 画像をWebクライアントに送り返すプロトコルが把握できるだろう。
./catfoo.cgi | head -4 | cat -v | colrm 50
Content-type: image/jpeg
Content-Disposition: filename="foo.jpg"
Content-length: 68128
M-^?M-XM-^?M-`^@^PJFIF^@^A^A^A^A,^A,^@^@M-^?M-aM-
このような操作を storeblog.cgi に組み込み、
の3つの機能を持たせたスクリプト miniblog1.cgi を示す。
#!/bin/sh
cd `dirname $0` # スクリプトのあるディレクトリへ
. ./cgilib2-sh
myname=`basename $0`
query "CREATE TABLE IF NOT EXISTS blog(
datetime UNIQUE, body, filename, filecontent);"
#【1】
case "$1" in # miniblog1.cgi catfile/《rowid》で起動するとその行の画像を出力
catfile/*)
rowid=`echo "${1#catfile/}" | tr -cd '[0-9]'` # 数字以外は除去
from="FROM blog WHERE rowid=$rowid" # FROM句以下を変数に
echo -n "Content-type: " # 中味を調べて Content-type ヘッダ出力
query "SELECT hex(filecontent) $from" | unhexize \
| file --mime-type - | cut -d' ' -f2
fn=`query "SELECT filename $from;"| tr -d '\n'` # 改行あれば除去
echo "Content-Disposition: filename=\"$fn\"" # ファイル名と長さ出力
query "SELECT 'Content-Length: ' || length(filecontent) $from;"
echo ""
query "SELECT hex(filecontent) $from" | unhexize # 画像を書き出して終了
exit ;;
esac
htmlhead "一言日記" # ヘッダから h1 要素まで出力(cgilib2内)
putform() { # HTTPヘッダから入力フォームHTMLまで出力
cat<<-EOF
<form action="$myname" method="POST" enctype="multipart/form-data">
<label>ファイルを添付してください:
<input type="file" name="attach"></label><br>一言:<br>
<textarea name="hitokoto" rows="3" cols="40"></textarea>
<input type="submit" value="POST">
<input type="reset" value="Reset">
</form>
EOF
}
addnew() {
if [ -n "$CONTENT_LENGTH" ]; then # データが送信された場合の登録処理
now=`date '+%F %T'` # datetime は現在時刻から自動生成
query<<-EOF
REPLACE INTO blog
/* (日時, 一言, ファイル名, ファイル内容) を合成して挿入 */
SELECT '$now',
max(CASE name WHEN 'hitokoto' THEN val END),
max(CASE name WHEN 'attach' THEN filename END),
max(CASE name WHEN 'attach' THEN val END)
FROM cgipars
WHERE tag='$_tag'
GROUP BY tag;
EOF
fi
}
putblog() { # 既存のエントリをtableで出力
href="<TD><a href=\"$myname?catfile/\\1\">\\2</a><" #【2】
echo '<table border="1">'
query<<-EOF \
| sed -e "s,<TD>[12]:,<TD>," \
-e "s,<TD>3:\([0-9]*\):\(.*\)<,$href," #【2】
.mode html
.header ON
SELECT '1:' || datetime '日時',
'2:' || body '一言',
CASE WHEN filename IS NOT NULL THEN '3:'
|| rowid || ":" || filename
ELSE '' -- filenameが非NULLのときのみ出力
END '添付ファイル'
FROM blog;
EOF
echo '</table>'
}
putform # ヘッダと入力フォームを出力
addnew # 新規レコードの登録
putblog # データ更新後の登録エントリを出力
echo '</body></html>'
リスト中【1】と【2】で示した部分が、 画像出力とそのリンクを出力する役割をしている。一覧出力で filename カラムを「3:ファイル名」と出力しておいたものを sed で
<a href="miniblog1.cgi?catfile/rowid">ファイル名</a>
と置き換えて出力している。rowidについて以下で説明する。
SQLite3では、とくに指定のない限りすべての行を一意に識別するための rowid が自動的に振られる。簡単な例で確認しよう。
sqlite3 rowid.sq3
CREATE TABLE foo(var UNIQUE, val);
INSERT INTO foo VALUES('a',9);
INSERT INTO foo VALUES('b',8);
INSERT INTO foo VALUES('c',7);
SELECT rowid,var,val FROM foo;
1|a|9
2|b|8
3|c|7
他のDBMSへの乗り換えを考える必要のないデータベースの テーブル設計では、無理に人工的なキーを作らずに SQLite3 の rowid を積極的に活用すると設計の手間がかなり軽減される。今回の例のように、 既に登録されているデータのいずれかの行の特定カラムにアクセスさせる リンクを作りたい場合など、CGIを呼び出す引数に rowid を利用すると引数の構文解析負担が大幅に軽減できる。ただし、rowid は、その行のREPLACEを行なうと変わるので注意が必要である。上記の foo テーブルで
した挙動を以下に示す。
REPLACE INTO foo VALUES('a',99);
UPDATE foo SET val=88 WHERE rowid=2;
SELECT rowid,var,val FROM foo;
2|b|88
3|c|7
4|a|99
UPDATEでは行の置き換えが起こらないため rowid=2 のままであるが、 REPLACE では一度行を消してからINSERTをしているため新たな rowid を持つ行に変化して見える。
UNIQUE制約をつけたテーブルに、REPLACE INTOでデータ挿入を行なう方法は、 新規レコードの入力と既存レコードの更新を同じSQL文で行なえるため、 シェルスクリプトの行数を節約できる一方、rowid の変化に注意する必要がある。 既存レコードの更新をUPDATE文で行なう方針はスクリプトでの場合分けが煩雑になるが rowid の変化をあまり気にしなくてよい。どちらがよいかは吟味する必要がある。
miniblog1を起動して2件の画像を投稿した状態の画面は以下のようになる。
第3章までに利用した cgi.sq3 内の cgipars テーブルが残っている状態だと、 カラム数が異なるためエラーとなる(cgilib では3カラム、cgilib2 では4カラム)。 データベースファイルにアクセスし「.sch cgipars」によって4カラムか確認し、 もし3カラムのテーブルが残っていたら「DROP TABLE cgipars;」 によって削除してからCGIを起動する。
日記一覧画面の例
一言日記
日時 一言 添付ファイル 2015-08-09 15:38:52 苗名滝でリフレッシュ! naena.jpg 2015-09-27 13:49:36 栗山池の一足早い秋 kuriyama.jpg
起動して試してみると分かるが、ここに「一言」を足すことはできても 修正や削除をすることができない。修正や削除のためには、 データの行を特定するキーを指定して、 そのキーを「修正フォームの生成セッション」、 「修正したデータ登録セッション」に引き継ぐ必要がある(下図参照)。
一覧出力から修正までのページ遷移
(1) 日記一覧と 修正リンクの出力 →
(2) 修正フォームの 生成 →
(3) 修正した データの登録
図「日記一覧画面の例」における表形式出力の「日時」の部分をキーとみなし、 それを軸に「修正フォーム」を生成する流れを示す。
HTMLの入力フォーム、input や textarea 要素には初期値を指定できる。 これまで(miniblog1)のデータの入力フォーム出力をさほど変えずに修正フォームも出力できる。 そのためには、既存データの特定の行のカラムを取り出す機構を追加すればよい。 今回利用している blog テーブルでは datetime カラムに UNIQUE 制約を指定しているのでこれをキーとして利用する。
たとえば、シェル変数 editkey に編集したいレコードの datetime カラム値が入っていると仮定すると、その行の body カラム(本文)の値は
query "SELECT body FROM blog WHERE datetime='$datetime';"
によって得られる。これを HTML の input 要素の value 属性に入れておけばよい。ただし、HTML 的に意味を持つ記号はエスケープする必要がある。これは cgilib2 ライブラリの escape 関数で行なえる。必要なカラムを取り出し、HTML エスケープした値を返す getcol を以下のように定義する。
getcol() { # HTMLモードでescapeしてもらう
escape "`query \"SELECT $1 FROM blog WHERE datetime='$2';\"`"
}
問い合わせによって得られた結果を シェル関数 escape により HTML エスケープした値を返す。
HTML エスケープによりシングルクォートも ' に変換されるため、変換後の値をSQL文の文字列として sqlite3 に渡すときに安心してシングルクォートで括れる。
hitokoto=`getcol body "$editkey"`
このようにして得られた $hitokoto をフォームの初期値として入れればよい。
特定レコードを削除するフォームは、修正フォームと同じ画面に そのレコードを削除するかどうかのチェックボックスを配置すればよい。 そこにチェックが入っていた場合は修正ではなく削除するようにすればよい。
miniblog1.sh で定義した関数に 修正リンクと修正・削除の機能を追加したスクリプト miniblog2.cgiを示す。
#!/bin/sh
cd `dirname $0`
. ./cgilib2-sh
myname=`basename $0`
query "CREATE TABLE IF NOT EXISTS blog(
datetime UNIQUE, body, filename, filecontent);"
case "$1" in
catfile/*) # miniblog2.cgi catfile/《rowid》で起動するとその行の画像を出力
rowid=`echo "${1#catfile/}" | tr -cd '[0-9]'` # 数字以外は除去
from="FROM blog WHERE rowid=$rowid" # FROM句以下を変数に
echo -n "Content-type: " # 中味を調べて Content-type ヘッダ出力
query "SELECT hex(filecontent) $from" | unhexize \
| file --mime-type - | cut -d' ' -f2
fn=`query "SELECT filename $from;"| tr -d '\n'` # 改行あれば除去
echo "Content-Disposition: filename=\"$fn\"" # ファイル名と長さ出力
query "SELECT 'Content-Length: ' || length(filecontent) $from;"
echo ""
query "SELECT hex(filecontent) $from" | unhexize # 画像を書き出して終了
exit ;;
edit/*) # miniblog2.cgi edit/《日時》で起動すると修正フォーム出力
editkey=`echo ${1#edit/}|pdecode` ;; # pdecode不要なhttpdもあるesac
# $1に指定したカラムを$2の日付キーから得る関数
getcol() { # HTMLエスケープしてから返す
escape "`query \"SELECT $1 FROM blog WHERE datetime='$2';\"`"
}
putform() { # HTTPヘッダから入力フォームHTMLまで出力
title="一言日記"
if [ -n "$editkey" ]; then
datetime=`getcol datetime "$editkey"` # 実在するキーか?
if [ -n "$datetime" ]; then
hitokoto=`getcol body "$editkey"` # 既存のbodyカラム値を得る
hidden="<p><input type=\"hidden\" name=\"datetime\"
value=\"$datetime\">
<input id=\"rm\" type=\"checkbox\" name=\"remove\" value=\"yes\">
<label for=\"rm\">このエントリの削除</label>
<span class=\"confirm\">ほんとうに消してよいですか:
<label><input type=\"checkbox\" name=\"confirm\" value=\"yes\">はい
</label></span></p>"
title="$datetime の一言の修正"
fi
fi
htmlhead "$title" # ヘッダから h1 要素まで出力(cgilib2内) cat<<-EOF
<form action="$myname" method="POST" enctype="multipart/form-data">
${datetime:+$hidden} <!-- 有効な日付指定のみ $edit 出力 -->
<label>ファイルを添付してください:
<input type="file" name="attach"></label><br>一言:<br>
<textarea name="hitokoto" rows="3" cols="40">$hitokoto</textarea>
<input type="submit" value="POST">
<input type="reset" value="Reset">
</form>
EOF
}
addnew() {
if [ -n "$CONTENT_LENGTH" ]; then # データが送信された場合の登録処理
dt=`getpar datetime` # datetimeの値がある場合は既存行の更新
# 実在する datetime か値自身を引直して確認する
[ -n "$dt" ] && \
dt=`query "SELECT datetime from blog WHERE datetime='$dt';"`
if [ x`getpar remove``getpar confirm` = x"yesyes" ]; then
# 「削除」と確認の「はい」両方にチェックの場合
query "DELETE FROM blog WHERE datetime='$dt';"
else
[ -n "$dt" ] && now=$dt || now=`date '+%F %T'`
query<<-EOF
REPLACE INTO blog
/* (日時, 一言, ファイル名, ファイル内容) を合成して挿入 */
SELECT '$now',
max(CASE name WHEN 'hitokoto' THEN val END),
max(CASE name WHEN 'attach' THEN filename END),
max(CASE name WHEN 'attach' THEN val END)
FROM cgipars
WHERE tag='$_tag'
GROUP BY tag;
EOF
fi
fi
}
putblog() { # 既存のエントリをtableで出力
href1="<TD><a href=\"$myname?edit/\\1\">\\2</a><"
href2="<TD><a href=\"$myname?catfile/\\1\">\\2</a><"
echo '<table border="1">'
query<<-EOF \
| sed -e "s,<TD>1:\(\([-0-9 :]*\):[0-9]*\)<,$href1," \
-e "s,<TD>2:,<TD>," \
-e "s,<TD>3:\([0-9]*\):\(.*\)<,$href2,"
.mode html
.header ON
SELECT '1:' || datetime '日時',
'2:' || body '一言',
CASE
WHEN filename IS NOT NULL
THEN '3:' || rowid || ":" || filename
ELSE '' -- filenameが非NULLのときのみ出力
END '添付ファイル'
FROM blog;
EOF
echo '</table>'
}
putform # ヘッダと入力フォームを出力
addnew # 新規レコードの登録
[ -n "$editkey" ] || putblog # データ更新後の登録エントリを出力
echo '</body></html>'
miniblog2 が出力する既存の日記一覧では以下のように datetime カラム、つまり日時のところにハイパーリンクが付加される。
修正リンクの例
日時 一言 添付ファイル 2015-08-09 15:38:52 苗名滝でリフレッシュ! naena.jpg 2015-09-27 13:49:36 栗山池の一足早い秋 kuriyama.jpg
日時をクリックすると修正画面に移行する。
2017-09-27 13:49:36 の一言の修正
この画面で「□このエントリの削除」にチェックを入れることで 削除する流れとなるが、 何かを削除するときのチェックボックスは1つだけでなく確認用のものも 入れたくなる(確認不要とする考え方もありうる)。そのような場合、 確認用のチェックボックスがそのまま隣に並べて出すようでは不粋である。 「□このエントリの削除」にチェックを入れたときのみ「□はい」を出したい。 この程度の機構であれば、JavaScript など使わずとも CSS3 のセレクタ指定により可能で、 たとえば削除ボックスにチェックを入れたときに じわじわと確認ボックスが出てくるようなデザインができる。
cgilib2 のヘッダ出力ではCSSファイルとして mycgi.css を読むように指定してあるのでこれに以下のような定義を書き込む。
/*
* CSS for my CGI shell script library
*/
span.confirm { /* 完全透明、不可視、背景色は明るい赤 */
opacity: 0; visibility: hidden; background: #fcc;
}
input:checked ~ span.confirm { /* チェック付ボタンと同階層の span.confirm */
visibility: visible; /* 可視化したうえで */
opacity: 1.0; transition: 3s; /* 透明解除、3秒掛ける */
}
p.login {text-align: right;} /* ログイン画面ではテキスト右寄せで */
body.authok {background: #ffd;} /* 認証完了後の背景色を変える */
opacity特性 background特性 transition特性 ~ (CSSセレクタ) :checked (CSS) 以後の利用のために4つの定義があるがここでは最初の2つを用いる。 「span.confirm」は、<span class="confirm">...</span> のように class 属性に confirm を含む span 要素に対し、 不透明度(opacity)を0(つまり完全透明で見えない)に指定したうえで、 背景色(background)を赤っぽい色にする定義を行なっている。
「input:checked ~ span.confirm」は、 チェックされた状態の input 要素と同じ親を持つ span 要素のうちclass属性にconfirmを含むものの不透明度を1.0に指定し、 さらにこのスタイルへの移行時間を3s(3秒)に設定している。 単に確認ボタンの不透明度を0と1に変えるだけだと、 透明の状態でも場所が当たればクリックされるおそれがあるため、 visibility 特性を hidden と visible で切り替えている。visibility 特性の切り替えには transition は効かないので「クリックしたらじわじわ出現」 には opacity 特性との組み合わせが必要である。
この定義に呼応するように、削除確認の2つのチェックボックスには以下のように class 指定を付加しておく。
<input id="rm" type="checkbox" name="remove" value="yes">
<label for="rm">このエントリの削除</label>
<span class="confirm">ほんとうに消してよいですか:
<label><input type="checkbox" name="confirm" value="yes">はい</label></span>
1つめのinput要素は、label要素の子供とせず、 span要素とHTML的に同じ階層になるように工夫している。 これでこの span 要素は1つめの input 要素によるボックスがチェックされているときは見えず、 チェックされたときに3秒かけてじわじわと浮かびあがる。
↓チェックして3秒かけてすこしずつ姿を現す
このように、確認用のチェックボックスをあらかじめ出しておき、 CSSセレクタの ~ を活用することで動的に見える確認操作画面を出すことができる。 2つのチェックボックス両方がチェックされているときのみ レコード削除を行なう判定をしているのが、
if [ x`getpar remove``getpar confirm` = x"yesyes" ]; then
の部分である。remove、confirm の2つの入力名が "yes" になっている時のみ削除操作を行なうようになっている。
ボタンのチェック状態による表示要素の切り替えはこのあと多用するので練習しておこう。
ラジオボタンで選んだ場所だけ意味が見える単語帳をCSS3を利用して作成せよ。
たとえば次のようにul要素で単語帳的なものを記述しておく。
HTMLソース
<ul>
<li>グー 「グリコ」と言って3段進む</li>
<li>チョキ 「チヨコレイト」と言って6段進む</li>
<li>パー 「パイナツプル」と言って6段進む</li>
</ul>
画面
この「グー」、「チョキ」、「パー」の見出しの前にラジオボタンを配置し、 それをクリックした場合のみそれぞれの後ろの説明が見えるように変更せよ。 作成ファイルは glico.html とする。
画面例を示す。まず初期状態。
チョキを押したあと。
ラジオボタンは
<input name="gcp" type="radio">
などを見出し語直前に配置するだけでよい。name 属性は同じ値にするよう注意する。
隠したい部分を span で囲む。
<span>「グリコ」と言って3段進む</span>
この span に特別なスタイルを適用したいのでクラスを指定する。
<span class="hide">「グリコ」と言って3段進む</span>
CSS定義部分に span.hide のスタイルを記述し、 標準では見えないようなものを設定する。
チェックされたラジオボタン(input[type="radio"]:checked) と 同じ階層にある span.hide は見えるようなルールを設定する。
ヒントにしたがったシンプルな記述例を示す。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>グリコ単語帳</title>
<style type="text/css">
<!--
span.hide {visibility: hidden;}
input[type="radio"]:checked ~ span.hide {visibility: visible;}
-->
</style>
</head>
<body>
<h1>グリコ単語帳</h1>
<ul>
<li><input name="gcp" type="radio">グー
<span class="hide">「グリコ」と言って3段進む</span></li>
<li><input name="gcp" type="radio">チョキ
<span class="hide">「チヨコレイト」と言って6段進む</span></li>
<li><input name="gcp" type="radio">パー
<span class="hide">「パイナツプル」と言って6段進む</span></li>
</ul>
</body></html>
span に対する class="hide" 指定を毎回記述せずに済むよう、 CSSの親子関係のルールを利用してみる。CSSセレクタ・子孫選択子が利用できる。
E F { スタイル }
とすると、E 要素の子(内部)である F 要素にのみ適用されるスタイルが設定できる。これを用い、親の ul 要素にクラスを設定する。
<ul class="tango">
<li>... <span>...</span></li>
</ul>
「classにtangoを持つul」の子のspan、という書き方でCSS定義を修正する。
ul.tango span {visibility: hidden;}
ul.tango input[type="radio"]:checked span {visibility: visible;}
これに対応する単語帳部分は次のようになる。
<ul class="tango">
<li><input name="gcp" type="radio">グー
<span>「グリコ」と言って3段進む</span></li>
<li><input name="gcp" type="radio">チョキ
<span>「チヨコレイト」と言って6段進む</span></li>
<li><input name="gcp" type="radio">パー
<span>「パイナツプル」と言って6段進む</span></li>
</ul>
span に属性を付けていないが、親の ul へのクラス指定がスタイル適用の選択規準となる。
このように子孫選択子を利用すると多数配置すべき要素の記述を簡潔にできる。
その他利用できるCSSの主なセレクタを示しておく。 この表にある [ ] は大括弧の記号そのものである。 CSSセレクタ一覧
パターン | 意味 |
---|---|
* | すべての要素 |
E | すべての E 要素 |
E F | E 要素の子孫に位置するすべての F 要素 |
E > F | E 要素の直接の子であるすべての F 要素 |
E + F | E 要素の直後に位置するすべての F 要素 |
E ~ F | E 要素と同じ親を持つ要素のうち E より後ろにあるすべての F 要素 |
E:nth-child(N) | 親要素の N 番目の子である E 要素 |
E:checked | ボタン型の E 要素のうちチェックされているもの |
E:link E:visited |
アンカー要素の未読リンク(:link)または既読リンク(:visited) |
E:active E:focus E:hover |
E 要素のうち、アクティブになっているもの、 フォーカスされているもの、マウスが上に来ている(:hover)もの |
E[foo] | E 要素のうち、foo という属性設定を持つもの |
E[foo="bar"] | E 要素のうち、foo 属性に "bar" を設定しているもの |
E[foo~="bar"] | E 要素のうち、foo 属性に "bar" という語が含まれるもの |
E.cls | E 要素のうち、class 属性に
"cls" という語が含まれるもの E[class~="bar"] と等値 |
E#id | E 要素のうち、id="id" のもの |