#!/usr/local/bin/ruby
#CGIとPStoreとERB、基本的な構成
require 'cgi'
require 'pstore'
require 'erb'
#board.rb?
#mode = submit | edit | delete | rss | [null]
#id = (id指定)
#以下mode=null時のみ有効
#offset = (オフセット量)
#view = (一度にみれる記事の個数)
#クエリ
MODE = 'mode'
OFFSET = 'offset'
VIEW = 'view'
ID = 'id'
DATE = 'date'
#表示初期値
VIEW_DEFAULT = 10
#フォーム
FORM_AUTHOR = 'author'
FORM_EMAIL = 'email'
FORM_TITLE = 'title'
FORM_BODY = 'body'
FORM_URL = 'url'
FORM_TRACKBACK = 'tackback'
FORM_DEFAULT = {FORM_URL => 'http://'}
#ファイルの所在
CGI_FILE = (ENV['SCRIPT_NAME'].scan(/\/([^\/]*)/).flatten)[-1]
DB_FILE = './data/board.db'
LOCK_FILE = './data/board.lock'
ERB_FILE = 'board.ehtml'
RSS_FILE = 'board.exml'
#CGI設定
DOMAIN = ENV['HTTP_HOST']
PATH = ENV['SCRIPT_NAME']
COOKIE_NAME = 'TrackBoard'
COOKIE_EXPIRES = 365 * 24 * 60 * 60
#DB内定義
COUNT = 'COUNT'
DATA = 'DATA'
AUTHORIZE = 'AUTHORIZE'
#投稿
def submit(cgi, id)
params = cgi.params
#データ最適化
params.each{|key, value|
if value.size == 1 then params[key] = value = value.first end
if value == (FORM_DEFAULT.include?(key) ? FORM_DEFAULT[key] : '') then params.delete(key) end
}
#データチェック
if !params.include?(FORM_AUTHOR) then raise StandardError.new('Need author name.') end
if !params.include?(FORM_EMAIL) then raise StandardError.new('Need e-mail address.') end
if !params.include?(FORM_BODY) then raise StandardError.new('Body is empty!!') end
params[FORM_BODY] = sanitize(params[FORM_BODY], ['a', 'b', 'i', 'u', 'strong'])
#認証キーの設定
params[AUTHORIZE] = parseCookie(cgi.cookies[COOKIE_NAME])[AUTHORIZE]
if !params[AUTHORIZE]
if !id
params[AUTHORIZE] = authorizeKey(params[FORM_AUTHOR], params[FORM_EMAIL])
else
raise StandardError.new('Not found authorize key.')
end
end
#保存処理
store(params, id)
#転送
cookie = bakeCookie({FORM_AUTHOR => params[FORM_AUTHOR],
FORM_EMAIL => params[FORM_EMAIL],
AUTHORIZE => params[AUTHORIZE]})
cgi.out({'status' => 'REDIRECT',
'Location' => CGI_FILE,
'cookie' => cookie}){"Redirect URL"}
end
#編集
def edit(cgi, id)
get(id).each{|key, value|
cgi.params[key] = value
}
display(cgi, [id], {'edit' => id})
end
#削除
def delete(cgi, id)
PStore.new(DB_FILE).transaction{|db|
if !db.root?(DATA) then db[DATA] = {} end
if !db[DATA].key?(id)
raise StandardError.new('Id is incorrect.')
elsif !cgi.cookies[COOKIE_NAME] then
raise StandardError.new('Not found authorize key.')
elsif db[DATA][id][AUTHORIZE] != parseCookie(cgi.cookies[COOKIE_NAME])[AUTHORIZE]
raise StandardError.new('Authorize key is incorrect.')
else
db[DATA].delete(id)
end
}
cgi.out({'status' => 'REDIRECT',
'Location' => CGI_FILE}){"Redirect URL"}
end
#表示
#@param list [(表示するアイテムのid)...]
#@param option {(表示オプション)}
def display(cgi, list = [], option = {})
FORM_DEFAULT.each{|name, value|
if !cgi.params.include?(name) then cgi.params[name] = value end
}
print cgi.header
File.open(ERB_FILE){|fh|
ERB.new(fh.read.untaint).run(binding)
}
end
#RSS出力
def rss(cgi)
list = get.sort.reverse.slice(0, 15)
print cgi.header({"type" => "application/xml"})
File.open(RSS_FILE){|fh|
ERB.new(fh.read.untaint).run(binding)
}
end
#保存、idがnilのとき新規投稿
def store(params, id = nil)
#付加情報
params[DATE] = Time.now
PStore.new(DB_FILE).transaction{|db|
if !id
id = (db.root?(COUNT) ? db[COUNT] : (db[COUNT] = 0)) #id割り当て
else
#認証キー照合
if db[DATA][id][AUTHORIZE] != params[AUTHORIZE] then raise StandardError.new('Authorize key is incorrect.') end
end
params[ID] = id
#トラックバック
if params[FORM_TRACKBACK] == 'on' then sendTrackBack(params) end
#データ保存
if !db.root?(DATA) then db[DATA] = {} end
db[DATA][id] = params
db[COUNT] += 1
}
end
#取得
#@param id (アイテムのid)、id = nilの場合は全アイテムidを配列で返す
def get(id = nil)
PStore.new(DB_FILE).transaction{|db|
if !db.root?(DATA) then db[DATA] = {} end
if !id
return db[DATA].keys
elsif db[DATA].key?(id)
return db[DATA][id]
end
}
return nil
end
#認証キー
def authorizeKey(author, email)
PStore.new(DB_FILE).transaction{|db|
if !db.root?(AUTHORIZE) then db[AUTHORIZE] = {} end
if db[AUTHORIZE].include?(email)
return db[AUTHORIZE][email]
else
return db[AUTHORIZE][email] = author.crypt(email)
end
}
end
#トラックバックを送信
def sendTrackBack(params)
senderURL = "http://#{DOMAIN}#{PATH}?#{ID}=#{params[ID]}"
title = params[FORM_TITLE]
excerpt = sanitize(params[FORM_BODY])
name = params[FORM_AUTHOR]
if (params[FORM_URL] =~ %r|http://([^/:]*):?(\d*)(.*)|) == nil
raise StandardError('URL is invalid.')
end
(address = $1).untaint
(port = ($2 == '' ? 80 : $2.to_i)).untaint
(path = $3).untaint
require 'net/http'
Net::HTTP.version_1_2
Net::HTTP.start(address, port){|http|
#トラックバック送信
response = http.post(path, "title=#{title}&url=#{senderURL}&excerpt=#{excerpt}&blog_name=#{name}")
if response.code != '200' or response.body[%r|\s*(\d)\s*|mi].to_i != 0
raise StandardError.new('Fail trackback.')
end
#URL変換
response = http.get(path + '?__mode=rss')
if response.code == '200'
if response.body =~ %r|\s*([^<]*?)\s*|mi
params[FORM_URL] = $1
end
end
}
end
#クッキーを焼く
def bakeCookie(values = {})
serialize = []
values.each{|key, value|
serialize << (key + '=' + value)
}
return CGI::Cookie.new({'name' => COOKIE_NAME,
'value' => serialize,
'domain' => DOMAIN,
'path' => PATH,
'expires' => Time.now + COOKIE_EXPIRES})
end
#クッキーを解析
def parseCookie(cookie = [])
parsed = {}
cookie.each{|value|
if value =~ /([^=]*)=(.*)/ then parsed[$1] = $2 end
}
return parsed
end
#HTML無毒化
#@param except [(例外事項、ここに指定したタグはデフォルトの指定と逆の動作)]
#@param accept (デフォルトでタグを受け付けるか)
def sanitize(html, except = [], accept = false)
return html.gsub(/<\/?(\w*)\s*[^>]*>/){|matched|
except.include?($1) ? (accept ? '' : $&) : (accept ? $& : '')
}
end
#URLのパース
def query2hash(query)
result = {}
if query
query.scan(/([^=]+)=([^&]+)&?/){|key, value|
result[key] = value
}
end
return result
end
#ロックファイル
def lock fname
begin
begin
f = File.open(fname, "r+")
rescue
f = File.open(fname, "w+")
end
f.flock(File::LOCK_EX)
yield
ensure
f.flock(File::LOCK_UN)
f.close
end
end
#メインルーチン
lock(LOCK_FILE){
cgi = CGI.new
#クエリの処理
query = query2hash(ENV['QUERY_STRING'])
[ID, OFFSET, VIEW].each{|key|
if query[key] then query[key] = query[key].to_i end
}
begin
case query[MODE]
when 'submit'
submit(cgi, query[ID])
when 'edit'
edit(cgi, query[ID])
when 'delete'
delete(cgi, query[ID])
when 'rss'
rss(cgi)
else
if query[ID]
display(cgi, [query[ID]], {'form' => 'off'})
else
option = {}
option[VIEW] = view = (query[VIEW] ? query[VIEW] : VIEW_DEFAULT)
offset = (query[OFFSET] ? query[OFFSET] : 0)
if offset > 0
option['before'] = (offset - view > 0 ? offset - view : 0)
end
if offset + view < get.size
option['after'] = offset + view
end
display(cgi, get.sort.reverse.slice(offset, view), option)
end
end
rescue
display(cgi, [], {'error' => $!})
end
}