#!/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 }