#!/usr/bin/ruby -Ku #CGIとPStoreとERB、基本的な構成 require 'cgi' require 'pstore' require 'erb' require 'digest/md5' #uploader.rb? #mode = submit | edit | get_file | delete | rss | [null] #id = (id指定) #以下mode=null時のみ有効 #offset = (オフセット量) #view = (一度にみれる記事の個数) #クエリ MODE = 'mode' OFFSET = 'offset' VIEW = 'view' ID = 'id' DATE = 'date' #表示初期値 VIEW_DEFAULT = 5 #アップロードサイズ上限, nilで上限なし FILE_SIZE_LIMIT = 10 * 1024 # byte #フォーム FORM_AUTHOR = 'author' FORM_EMAIL = 'email' FORM_FILE = 'file' FORM_MEMO = 'body' #Digest::MD5::digest('body' + Time::now.strftime("%Y%H")) FORM_DEFAULT = {} #ファイルの所在 CGI_FILE = (ENV['SCRIPT_NAME'].scan(/\/([^\/]*)/).flatten)[-1] DATA_DIR = './data' DB_FILE = "#{DATA_DIR}/uploader.db" LOCK_FILE = "#{DATA_DIR}/uploader.lock" ERB_FILE = 'uploader.ehtml' RSS_FILE = 'uploader.exml' #CGI設定 DOMAIN = ENV['HTTP_HOST'] PATH = ENV['SCRIPT_NAME'] COOKIE_NAME = 'SimpleUploader' COOKIE_EXPIRES = 365 * 24 * 60 * 60 #DB内定義 COUNT = 'COUNT' DATA = 'DATA' AUTHORIZE = 'AUTHORIZE' def id2file(id) return "#{DATA_DIR}/file.#{id}" end #投稿 def submit(cgi, id) params = cgi.params #データの整理 params.each{|key, value| value = params[key] = (key != FORM_FILE ? value.first.read : value.first) if value == (FORM_DEFAULT.include?(key) ? FORM_DEFAULT[key] : '') then params.delete(key) end } #データチェック if !params.include?(FORM_AUTHOR) then raise StandardError.new('Author name required!!') end if !params.include?(FORM_EMAIL) then raise StandardError.new('E-mail address required!!') end if !params[FORM_FILE] or (params[FORM_FILE].size == 0) then raise StandardError.new('No file!!') end if FILE_SIZE_LIMIT and params[FORM_FILE].size >= FILE_SIZE_LIMIT raise StandardError.new("Too big file, limit = #{FILE_SIZE_LIMIT} byte!!") end if !params.include?(FORM_MEMO) then =begin raise StandardError.new('Body is empty!!') =end params[FORM_MEMO] = '' end params[FORM_MEMO] = sanitize(params[FORM_MEMO], ['a', 'b', 'i', 'u', 'strong']) =begin case params[FORM_MEMO] when /[\x81-\x9f\xe0-\xef]/ when /(\x8e.|\x8f..|[^\x00-\x7F\x8e\x8f].)/ when /([\xc0-\xcf\xd0-\xdf].|[\xe0-\xef]..)/ else raise StandardError.new('Internal Server Error!!') end =end #認証キーの設定 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) File::delete(id2file(id)) end } cgi.out({'status' => 'REDIRECT', 'Location' => CGI_FILE}){"Redirect URL"} end #ファイルの取得 def get_file(cgi, id) content = nil file_info = nil 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.') else open(id2file(id), 'r'){|io| content = io.read file_info = db[DATA][id][FORM_FILE] } end } cgi.out({'Content-Type' => file_info[:file_type], 'Content-Length' => file_info[:file_size], 'Content-Disposition' => "attachment; filename=\"#{file_info[:file_name]}\""}){content} 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 #ファイル情報の抽出 uploaded_file = params[FORM_FILE] uploaded_file.original_filename =~ %r|([^\\/]+)$| params[FORM_FILE] = { :file_name => uploaded_file.original_filename, :file_type => uploaded_file.content_type, :file_size => uploaded_file.size, :file_name_short => $1} 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 !db.root?(DATA) then db[DATA] = {} end db[DATA][id] = params db[COUNT] += 1 open(id2file(id), 'w'){|io| io << uploaded_file.read } } 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 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 'get_file' get_file(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' => $!, 'error_detail' => $@}) end #}