OSDN Git Service

change main-menu-link's method.
[feedblog/feedgenerator.git] / feedgenerator.rb
1 #!/usr/local/bin/ruby
2 # -*- coding: utf-8 -*-
3 #
4 #= Atom Feed 1.0を管理するWEBアプリケーション
5 #
6 #Autohr::    Kureha Hisame (http://lunardial.sakura.ne.jp/) & Yui Naruse (http://airemix.com/)
7 #Version::   2.0.0.0
8 #Copyright:: Copyright 2009 FeedBlog Project (http://sourceforge.jp/projects/feedblog/)
9 #License::   GPLv3
10
11 require "cgi"
12 require "cgi/session"
13 require "erb"
14 require "rexml/document"
15 require "pstore"
16 require 'time'
17 require "date"
18 require "fileutils"
19
20 # ログインID
21 LOGINID = "login"
22 # ログインパスワード
23 PASSWORD = "password"
24 # インターフェースのテーブルの幅
25 TABLEWIDTH = 800
26 # XMLファイル格納先までの相対パス
27 XMLPATH = "./../lunardial/xml/"
28 # loglist.xmlファイルの定義
29 LISTXMLPATH = "#{XMLPATH}loglist.xml"
30 # FeedBlog上の表示ページからログ格納ディレクトリまでのパス
31 FEEDXMLDIR = "./xml/"
32 # デバッガモード
33 DEBUG = false
34 # ファイルマネージャー機能を使用するならtrue
35 USEFILEMANAGER = true
36 # ファイルマネージャー機能スクリプト(filemanager.rb)のパス
37 FILEMANAGER = "./filemanager.rb"
38 # XMLに書き込む際、改行部分を<br>のまま保持するか、改行記号に直すか
39 REPLACEBRTAG = false
40
41 # バージョン情報を示す文字列です
42 APPVERSION = "- FeedGenerator for Ruby version 2.0.0.0 -<br>Copyright(c) 2009 Kureha.H (<a href=\"http://lunardial.sakura.ne.jp/\" target=\"_blank\">http://lunardial.sakura.ne.jp/</a>) & Yui Naruse (<a href=\"http://airemix.com/\" target=\"_blank\">http://airemix.com/</a>)"
43 # タイトル領域に表示される文字列です
44 APPTITLE = "FeedGenerator for Ruby version 2.0.0.0"
45
46 # = Objectクラス
47 #
48 # 基本クラスのオーバーライドを行います
49 class Object
50   # myopenメソッド
51   #
52   # ruby-1.9.x以降ではファイルを開いた際、エンコードの指定を行わないとエラーの原因になります。
53   # ただしruby-1.8.6以前はエンコードの指定に対応していないため、独自メソッドを定義してファイルの入出力を行います。
54   #
55   # _arg[0]_ :: 入出力を行うファイルのパス
56   # _arg[1]_ :: モードの指定。例 : w:utf-8(書き込みモード・UTF-8エンコードでファイルを開く)
57   def myopen(*arg)
58     mode = arg[1]
59     arg[1] = mode[/[^:]+/] if mode && RUBY_VERSION < "1.8.7" && mode.include?(':')
60     open(*arg) do |f|
61       f.flock(/\A\w+r/ =~ mode ? File::LOCK_SH : File::LOCK_EX)
62       return yield(f)
63     end
64   end
65 end
66
67 class NilClass
68   def blank?
69     nil?
70   end
71 end
72
73 class Array
74   def blank?
75     empty?
76   end
77 end
78
79 class String
80   def blank?
81     empty?
82   end
83 end
84
85 # = Feed/Entryのスーパークラス
86
87 # 入出力用のメソッドなど、共通する機能を提供します
88 class AbstractEntry
89   # 初期化メソッドです
90   #
91   # _hash_ :: 値を格納したhash配列。superfeed.new(CGI::Session.new(new CGI).params)のようにして使う。
92   def initialize(hash)
93     # 内部データ保持用のハッシュ配列です
94     @attr = {}
95     
96     # 引数を格納します
97     @paramlist.each do |key|
98       val = hash[key.to_sym] || hash[key.to_s]
99       if val
100         # val.strip!
101         val.gsub!(/\r\n|\r/, "\n")
102         @attr[key.to_sym] = CGI.escapeHTML(val)
103       else
104         # 空の場合の対策
105         @attr[key.to_sym] = ""
106       end
107     end
108     
109     # 可視・不可視を示すハッシュキーを格納する配列です
110     @display = {}
111     
112     # ハッシュキーの日本語説明を格納する配列です
113     @name = []
114   end
115   
116   # Accessor
117   attr_reader :attr, :paramlist, :name, :display
118   
119   # 内部の@attrハッシュにアクセスする手段を提供するメソッドです
120   #
121   # AbstractEntry.attr[:title]にアクセスしたい場合はAbstractEntry.titleで可能になります。
122   # AbstractEntry.send("title")という方法でもアクセス可能です。
123   def method_missing(methname, *args)
124     methname = methname.to_s
125     
126     if methname[-1] == ?=
127       # setter
128       raise ArgumentError, "wrong number of arguments (#{args.length} for 1)" unless args.length == 1
129       methname.chop!
130       methname = @paramlist.find{|par|par == methname}
131       return @attr[methname.to_sym] = args[0] if methname
132     else
133       # getter
134       raise ArgumentError, "wrong number of arguments (#{args.length} for 0)" unless args.empty?
135       return @attr[methname.to_sym] if @attr.key?(methname.to_sym)
136     end
137     # attr上にキーがない値を入れようとした場合はNoMethodError
138     raise NoMethodError
139   end
140   
141   def [](key)
142     @attr[key.to_sym]
143   end
144   
145   def []=(key, value)
146     @attr[key.to_sym] = value
147   end
148 end
149
150 # = Feedクラス
151 #
152 # Feedの基礎情報を保有するクラスです
153 class Feed < AbstractEntry
154   # 初期化メソッドです
155   #
156   # _hash_ :: 値を格納したhash配列。feed.new(CGI::Session.new(new CGI).params)のようにして使います。
157   def initialize(hash)
158     # 内部データ保持用のハッシュ配列です
159     @attr = {}
160     
161     # 内部データの項目名を格納する配列です
162     @paramlist = ["feedattr", "title", "subtitle", "self", "url", "updated", "feedid", "rights", "aname", "amail", "others"]
163     
164     # AbstractEntryのinitializeメソッドを呼び出し、値を@attr配列に格納します
165     super(hash)
166     
167     # 可視・不可視を示すハッシュキーを格納する配列です
168     @display = {"feedattr" => "none", "title" => "", "subtitle" => "", 
169       "self" => "", "url" => "", 
170       "updated" => "none", "feedid" => "", 
171       "rights" => "", "aname" => "", 
172       "amail" => "", "others" => "none"}
173     
174     # デバッグモードの場合、全ての入力要素を表示します
175     if DEBUG == true
176       @display.each do |key, val|
177         @display[key] = ""
178       end
179     end
180     
181     # ハッシュキーの日本語説明を格納する配列です
182     @name = {"feedattr" => "feedの保持している属性", "title" => "ウェブページのタイトル", "subtitle" => "ウェブページの簡単な説明", 
183       "self" => "このXMLファイルのURL", "url" => "ウェブページのURL", 
184       "updated" => "XMLファイルの最終更新日", "feedid" => "あなたのページ独自のID", 
185       "rights" => "ページの著作権表記", "aname" => "ページの製作者", 
186       "amail" => "ページ製作者のメールアドレス", "others" => "その他の要素"}
187     
188   end
189   
190   # Atom XMLファイルを読み込んで解析し、等価なFeedオブジェクトを返却するメソッドです
191   #
192   # _path_ :: Atom XMLファイルのパス
193   def Feed.readxml(path)
194     
195     # ファイルを読み込みます
196     doc = REXML::Document.new(myopen(path, "r:utf-8"){|f|f.read})
197     xml = {}
198     others = []
199     
200     # Feedの属性値を取得します
201     xmlattr = []
202     doc.elements["feed"].attributes.each_attribute do |attr|
203       xmlattr.push("#{attr.to_string} ")
204     end
205     xml[:feedattr] = xmlattr.join("\n").gsub(/"/, "'")
206     
207     # XML解析部分です。各element毎に判定を行います
208     doc.elements.each("feed") do |elm|
209       elm.elements.each { |child|
210         begin
211           case child.name
212             when "id"
213             xml[:feedid] = child.text
214             when "title", "subtitle", "updated", "rights"
215             xml[child.name.to_sym] = child.text
216             when "author"
217             child.elements.each do |gchild|
218               case gchild.name
219                 when "name"
220                 xml[:aname] = gchild.text
221                 when "email"
222                 xml[:amail] = gchild.text
223               end
224             end
225             when "link"
226             child.attributes.each do |k, v|
227               if k == "rel"
228                 if v == "self"
229                   xml[:self] = child.attributes["href"]
230                 elsif v == "alternate"
231                   xml[:url] = child.attributes["href"]
232                 end
233               end
234             end
235             when "entry"
236             # Entry要素は無視します
237           else
238             # 上記判定以外の全要素は配列に格納します
239             others.push(child.to_s)
240           end
241         rescue NoMethodError
242         end
243         
244       }
245     end
246     
247     # Others要素を結合して代入します
248     xml[:others] = others.join("\n")
249     feed = Feed.new(xml)
250     return feed
251   end
252   
253   # 内部に保持している情報を、Atom Feed1.0形式の文字列に出力するメソッドです
254   def to_s
255     buf = []
256     
257     buf.push("<feed #{@attr[:feedattr]}>")
258     buf.push("<title type=\"text\">#{@attr[:title]}</title>")
259     buf.push("<subtitle type=\"text\">#{@attr[:subtitle]}</subtitle>")
260     buf.push("<link rel=\"self\" type=\"application/atom+xml\" href=\"#{@attr[:self]}\" />")
261     buf.push("<link rel=\"alternate\" type=\"text/html\" href=\"#{@attr[:url]}\" />")
262     buf.push("<updated>#{Time.now.iso8601}</updated>")
263     buf.push("<id>#{@attr[:feedid]}</id>")
264     buf.push("<rights type=\"text\">#{@attr[:rights]}</rights>")
265     buf.push("<author>")
266     buf.push("\t<name>#{@attr[:aname]}</name>")
267     buf.push("\t<email>#{@attr[:amail]}</email>")
268     buf.push("</author>")
269     buf.push("#{CGI.unescapeHTML(@attr[:others])}") if @attr[:others] != ""
270     
271     return buf.join("\n")
272   end
273 end
274
275 # = Entryクラス
276 #
277 # Entryの基礎情報を保有するクラスです
278 class Entry < AbstractEntry
279   # 初期化メソッドです
280   #
281   # _hash_ :: 値を格納したhash配列。entry.new(CGI::Session.new(new CGI).params)のようにして使います。
282   def initialize(hash)
283     # 内部データ保持用のハッシュ配列です
284     @attr = {}
285     
286     # 内部データの項目名を格納する配列です
287     @paramlist = ["entryid", "title", "summary", "published", "updated", "url", "content", "others"]
288     
289     # AbstractEntryのinitializeメソッドを呼び出し、値を@attr配列に格納します
290     super(hash)
291     
292     # 可視・不可視を示すハッシュキーを格納する配列です
293     @display = {"entryid" => "none", "title" => "",
294       "summary" => "", "published" => "none",
295       "updated" => "none", "url" => "none",
296       "content" => "", "others"=>"none"}
297     
298     # デバッグモードの場合、全ての入力要素を表示します
299     if DEBUG == true
300       @display.each do |key, val|
301         @display[key] = ""
302       end
303     end
304     
305     # ハッシュキーの日本語説明を格納する配列です
306     @name = {"entryid" => "記事固有のID", "title" => "記事のタイトル",
307       "summary" => "記事の簡単な説明", "published" => "記事の出版時刻",
308       "updated" => "記事の更新時刻", "url" => "記事へのURLアドレス",
309       "content" => "記事の本文", "others"=>"その他の項目"}
310   end
311   
312   # Atom XMLファイルを読み込んで解析し、等価なEntryオブジェクト配列を返却するメソッドです
313   #
314   # _path_ :: Atom XMLファイルのパス
315   def Entry.readxml(path)
316     
317     # ファイルを読み込みます
318     doc = REXML::Document.new(myopen(path, "r:utf-8"){|f|f.read})
319     entrylist = []
320     xml = {}
321     
322     # XML解析部分です。各element毎に判定を行います
323     doc.elements.each("feed/entry") do |elm|
324       xml = {}
325       others = []
326       elm.elements.each do |child|
327         begin
328           case child.name
329             when "id"
330             xml[:entryid] = child.text
331             when "link"
332             xml[:url] = child.attributes["href"]
333             when "title", "summary", "summary", "published", "updated", "content"
334             xml[child.name.to_sym] = child.text
335           else
336             # 上記判定以外の全要素は配列に格納します
337             others.push(child.to_s)
338           end
339         rescue NoMethodError
340         end
341       end
342       # Others要素を結合して代入します
343       xml[:others] = others.join("\n")
344       entrylist.push(Entry.new(xml))
345     end
346     
347     return entrylist
348   end
349   
350   # データソースから読み取ったHTMLを、エディタで編集可能な形式に変換するメソッドです
351   def content_for_generator
352     str = @attr[:content].dup
353     # str.strip!
354     str.gsub!(/(&lt;\/(?:p|h\d|div)(?:&gt;|>))\n/i, '\1')
355     str.gsub!(/\n/, '&lt;br&gt;') if REPLACEBRTAG
356     str.gsub!(/(&lt;(?:(?!&gt;).)*?)#{Regexp.escape(FEEDXMLDIR)}/) { "#$1#{XMLPATH}" }
357     str
358   end
359   
360   # エディタで編集されたCONTENT要素を、データソースに書き込める形式に変換するメソッドです
361   def content_for_blog
362     str = @attr[:content].dup
363     str = CGI.unescapeHTML(str)
364     # str.strip!
365     str.gsub!(/(\r\n|\n)/, "")
366     str.gsub!(/<br>/i, "\n") if REPLACEBRTAG
367     str.gsub!(/(<br>|<\/p>|<\/h\d>|<\/div>)(?=[^\n])/i) { "#$1\n" } unless REPLACEBRTAG
368     str.gsub!(/(<[^>]*?)#{Regexp.escape(XMLPATH)}/) { "#$1#{FEEDXMLDIR}" }
369     CGI.escapeHTML(str)
370   end
371   
372   # 確認画面で表示されるCONTENT要素を生成するメソッドです
373   def content_for_view
374     str = @attr[:content].dup
375     str = CGI.unescapeHTML(str)
376     # str.strip!
377     str.gsub!(/<br>/i, "\n") if REPLACEBRTAG
378     str.gsub!(/(<[^>]*?)#{Regexp.escape(FEEDXMLDIR)}/) { "#$1#{XMLPATH}" }
379     str
380   end
381   
382   # 内部に保持している情報を、Atom Feed1.0形式の文字列に出力するメソッドです
383   def to_s
384     buf = []
385     buf.push("<entry>")
386     buf.push("<id>#{@attr[:entryid]}</id>")
387     buf.push("<title>#{@attr[:title]}</title>")
388     buf.push("<summary>#{@attr[:summary]}</summary>")
389     buf.push("<published>#{@attr[:published]}</published>")
390     buf.push("<updated>#{@attr[:updated]}</updated>")
391     buf.push("<link href=\"#{@attr[:url]}\" />")
392     buf.push("<content type=\"html\">#{@attr[:content]}</content>")
393     buf.push("#{CGI.unescapeHTML(@attr[:others])}") if @attr[:others] != ""
394     buf.push("</entry>")
395     
396     return buf.join("\n")
397   end
398 end
399
400 # = FeedWriterクラス
401 #
402 # Atom Feed 1.0 XMLファイルに書き込みを行うクラスです
403 class FeedWriter
404   # 初期化メソッドです
405   #
406   # _path_ :: Atom Feed 1.0 XMLファイルの書き込みパス
407   def initialize(path)
408     @path = path
409   end
410   
411   # XMLファイル出力用メソッドです
412   #
413   # _feed_ :: Feedオブジェクト
414   # _entry_ :: Entryオブジェクトの配列
415   def to_xml(feed, entrylist)
416     buf = []
417     buf.push("<?xml version=\"1.0\" encoding=\"utf-8\"?>")
418     buf.push("#{feed.to_s}\n")
419     entrylist.each { |entry|
420       buf.push("#{entry.to_s}\n")
421     }
422     buf.push("</feed>")
423     
424     myopen(@path, "w") do |f|
425       f.print buf.join("\n")
426     end
427   end
428 end
429
430 # = HtmlWriterクラス
431
432 # テンプレートファイル(*.erb)を読み込み、管理するクラスです
433 class HtmlWriter
434   # 初期化メソッドです
435   #
436   # _template_ :: テンプレートファイル(*.erb)のパス
437   # _binding_ :: binding変数
438   def initialize(template, binding)
439     @erb = ERB.new(myopen(template, "r:utf-8") {|f| f.read}, nil, "-")
440     @binding = binding
441   end
442   
443   # テンプレートファイルの文字列を返却するメソッドです
444   def to_code
445     @erb.result(@binding)
446   end
447 end
448
449 # = LogListクラス
450 #
451 # loglist.xmlにかかわる入出力を行います
452 class LogList
453   # 初期化メソッドです
454   #
455   # _display_ :: 画面表示用文字の配列
456   # _path_ :: XMLファイルパスの配列
457   # _logpath_ :: loglist.xmlのパス
458   def initialize(display, path, logpath)
459     @display = display
460     @path = path
461     @logpath = logpath
462   end
463   
464   attr_accessor :display, :path, :logpath
465   
466   # loglist.xmlファイルを読み込んで解析し、LogListオブジェクトを返却するメソッドです
467   #
468   # _logpath_ :: loglist.xmlへのパス
469   def LogList.readxml(logpath)
470     
471     # ファイルを読み込みます
472     lines = []
473     myopen(logpath, "r:utf-8") { |f|
474       lines = f.readlines
475     }
476     
477     doc = REXML::Document.new(lines.join("\n"))
478     @display = []
479     @path = []
480     
481     doc.elements.each("list/file") { |elm|
482       elm.elements.each {|child|
483         case child.name
484           when "display"
485           @display.push(child.text)
486           when "path"
487           @path.push(child.text)
488         else 
489           # With no action
490         end
491       }
492     }
493     
494     return LogList.new(@display, @path, logpath)
495   end
496   
497   # loglist.xmlにオブジェクトの内容を反映するメソッドです
498   def to_xml
499     buf = []
500     buf.push("<list>")
501     path.each_with_index do |path, i|
502       buf.push("<file>")
503       buf.push("<display>#{@display[i]}</display>")
504       buf.push("<path>#{@path[i]}</path>")
505       buf.push("</file>")
506     end
507     buf.push("</list>")
508     
509     myopen(@logpath, "w") do |f|
510       f.print buf.join("\n")
511     end
512   end
513   
514 end
515
516 # = FileUploaderクラス
517 #
518 # 画像の管理を行うクラスです
519 class FileUploader
520   # 初期化メソッドです
521   def initialize
522   end
523   
524   # ファイルの一覧を取得し、ファイル名称を格納した配列を返却します
525   def filelist
526     arr = Dir::entries(XMLPATH).sort
527     arr.delete(".")
528     arr.delete("..")
529     
530     return arr
531   end
532   
533   # ファイルの消去を実行します
534   #
535   # _name_ :: 消去するファイル名
536   def delete(name)
537     File.delete(XMLPATH + name.match(/[^\/]*?$/).to_a[0])
538   end
539   
540   # ファイルのアップロードを実行します
541   #
542   # _name_ :: アップロードファイルの名称
543   # _img_ :: アップロードファイル
544   def upload(name, img)
545     open(XMLPATH + File.basename(name), "w") do |f|
546       f.binmode
547       f.write img
548     end
549   end
550 end
551
552 # = Controllerクラス
553 #
554 # コントローラ部分に相当する処理を受け持つクラスです
555 class Controller
556   def Controller.NormalForm(cgi, session, params, db)
557     db.transaction do
558       # modeとactionの相関図は以下のようになります。
559       # [mode] + [action]
560       #        + [action]
561       #        + [action]
562       #        + ... and more
563       case params["mode"]
564         # ログ選択画面
565         when "logselect"
566         session["filepath"] = params["logpath"]
567         db["loglist"] = LogList.readxml(LISTXMLPATH)
568         # 初期状態で選択されるログは「loglist.xml」の最上に位置するログになります
569         session["filepath"] = db["loglist"].path[0] if session["filepath"] == nil
570         db["feed"] = Feed.readxml(XMLPATH + session["filepath"].match(/[^\/]*?$/).to_a[0])
571         db["entry"] = Entry.readxml(XMLPATH + session["filepath"].match(/[^\/]*?$/).to_a[0])
572         
573         # 新規記事追加部分
574         when "newentry"
575         case params["action"]
576           # 確認画面
577           when "confirm"
578           db["newentry"] = Entry.new(params)
579           db["newentry"].content = db["newentry"].content_for_blog
580           # 記事の追記を実際にファイルに反映
581           when "exec"
582           db["entry"].unshift db["newentry"]
583           feedwriter = FeedWriter.new(XMLPATH + session["filepath"].match(/[^\/]*?$/).to_a[0])
584           feedwriter.to_xml(db["feed"], db["entry"])
585         else 
586           # New Diary - Default
587           db["feed"] = Feed.readxml(XMLPATH + session["filepath"].match(/[^\/]*?$/).to_a[0])
588           db["entry"] = Entry.readxml(XMLPATH + session["filepath"].match(/[^\/]*?$/).to_a[0])
589         end
590         
591         # 記事編集部分
592         when "editentry"
593         case params["action"]
594           # 編集画面
595           when "edit"
596           session["editid"] = cgi["editid"].to_s
597           # 確認画面
598           when "confirm"
599           session["editid"] = cgi["editid"].to_s
600           db["editentry"] = Entry.new(params)
601           db["editentry"].content = db["editentry"].content_for_blog
602           # 記事の変更を実際にファイルに反映
603           when "exec"
604           db["entry"].each_with_index { |e, i|
605             db["entry"][i] = db["editentry"] if e.entryid == cgi["editid"].to_s
606           }
607           feedwriter = FeedWriter.new(XMLPATH + session["filepath"].match(/[^\/]*?$/).to_a[0])
608           feedwriter.to_xml(db["feed"], db["entry"])
609         else
610           # Edit Diary - Default
611           db["feed"] = Feed.readxml(XMLPATH + session["filepath"].match(/[^\/]*?$/).to_a[0])
612           db["entry"] = Entry.readxml(XMLPATH + session["filepath"].match(/[^\/]*?$/).to_a[0])
613         end
614         
615         # 記事削除部分
616         when "delentry"
617         case params["action"]
618           # 確認画面
619           when "confirm"
620           session["delid"] = cgi["delid"].to_s
621           # 記事の削除を実際にファイルに反映
622           when "exec"
623           delindex = nil
624           db["entry"].each_with_index { |e, i|
625             delindex = i if e.entryid == cgi["delid"].to_s
626           }
627           db["entry"].delete_at(delindex) unless delindex == nil
628           feedwriter = FeedWriter.new(XMLPATH + session["filepath"].match(/[^\/]*?$/).to_a[0])
629           feedwriter.to_xml(db["feed"], db["entry"])
630         else
631           # Delete Diary - Default
632           db["feed"] = Feed.readxml(XMLPATH + session["filepath"].match(/[^\/]*?$/).to_a[0])
633           db["entry"] = Entry.readxml(XMLPATH + session["filepath"].match(/[^\/]*?$/).to_a[0])
634         end
635         
636         # Feed情報変更部分
637         when "editfeed"
638         case params["action"]
639           # 確認画面
640           when "confirm"
641           db["feed"] = Feed.new(params)
642           # 実際にFeed情報の変更をファイルに反映
643           when "exec"
644           feedwriter = FeedWriter.new(XMLPATH + session["filepath"].match(/[^\/]*?$/).to_a[0])
645           feedwriter.to_xml(db["feed"], db["entry"])
646         else
647           
648         end
649         
650         # ログ編集モード
651         when "log"
652         case params["action"]
653           # ログファイルの編集を実際にファイルに反映
654           when "addexec"
655           # 入力された内容を保持する配列に追加
656           db["loglist"].path[1, 0] = db["logpath"]
657           db["loglist"].display[1, 0] = db["logdisplay"]
658           db["loglist"].to_xml
659           # 既存のdiary.xmlを指定された名称でコピーして保存
660           FileUtils.copy_file(XMLPATH + db["loglist"].path[0].match(/[^\/]*?$/).to_a[0], XMLPATH + db["logpath"].match(/[^\/]*?$/).to_a[0], true)
661           # 保持している情報を更新する
662           db["loglist"] = LogList.readxml(LISTXMLPATH)
663           db["entry"] = []
664           # 新たなdiary.xmlを生成。この際保持する情報は同一のFeedオブジェクト
665           writer = FeedWriter.new(XMLPATH + db["loglist"].path[0].match(/[^\/]*?$/).to_a[0])
666           writer.to_xml(db["feed"], [])
667           # 確認画面
668           when "addconfirm"
669           # 入力されたログが既に存在するかを確認
670           db["loglist"].path.each do |val|
671             if val == cgi["logpath"]
672               # 重複していた場合エラーメッセージを表示し、処理を行わない
673               params["action"] = ""
674               error = "<span style='color: #ff0000'>同一のファイルが存在します!別の名前を指定してください。</span><br>"
675               break;
676             end
677           end
678           
679           db["logpath"] = cgi["logpath"]
680           db["logdisplay"] = cgi["logdisplay"]
681           
682           if db["logpath"].blank? || db["logdisplay"].blank?
683             params["action"] = ""
684           end
685           when "back"
686           # 削除確認画面
687           when "delconfirm"
688           db["logdelindex"] = params["logdelindex"].to_i
689           
690           if db["logdelindex"] == 0
691             params["action"] = ""
692           end
693           # 削除処理
694           when "delexec"
695           if db["logdelindex"] == 0
696             params["action"] = ""
697           else
698             # 記事ファイルを削除します
699             File.delete(XMLPATH + db["loglist"].path[db["logdelindex"]].match(/[^\/]*?$/).to_s)
700             # ログリストから削除します
701             db["loglist"].path.delete_at(db["logdelindex"])
702             db["loglist"].display.delete_at(db["logdelindex"])
703             db["loglist"].to_xml
704             # 保持している情報を更新する
705             db["loglist"] = LogList.readxml(LISTXMLPATH)
706             db["entry"] = []
707           end
708           # 編集画面
709           when "edit"
710           db["logeditindex"] = params["logdelindex"].to_i
711           
712           db["logpath"] = db["loglist"].path[db["logeditindex"]]
713           db["logdisplay"] = db["loglist"].display[db["logeditindex"]]
714           
715           if db["logeditindex"] == 0
716             params["action"] = ""
717           end
718           # 編集確認画面
719           when "editconfirm"
720           
721           checkflag = true
722           db["loglist"].path.each_with_index do |val, i|
723             if db["logeditindex"] != i
724               if params["logpath"].to_s == db["loglist"].path[i].to_s
725                 checkflag = false
726               end
727             end
728           end
729           
730           if checkflag == false
731             params["action"] = "edit"
732             db["error"] = "<span style='color: #ff0000'>同一のファイルが存在します!別の名前を指定してください。</span><br>"
733           else
734             db["loginsertindex"] = params["loginsertindex"].to_i
735             
736             db["logpath"] = params["logpath"].to_s
737             db["logdisplay"] = params["logdisplay"].to_s
738           end
739           # 編集実行
740           when "editexec"
741           # ファイルを移動します
742           if XMLPATH + db["loglist"].path[db["logeditindex"]].match(/[^\/]*?$/).to_s != XMLPATH + db["logpath"].match(/[^\/]*?$/).to_s
743             FileUtils.move(XMLPATH + db["loglist"].path[db["logeditindex"]].match(/[^\/]*?$/).to_s, XMLPATH + db["logpath"].match(/[^\/]*?$/).to_s)
744           end
745           # ログリストを更新します
746           db["loglist"].path.delete_at(db["logeditindex"])
747           db["loglist"].display.delete_at(db["logeditindex"])
748           db["loglist"].path.insert(db["loginsertindex"] + 1, db["logpath"])
749           db["loglist"].display.insert(db["loginsertindex"] + 1, db["logdisplay"])
750           db["loglist"].to_xml
751           # 初期表示画面
752         else
753           # 初期表示の際に内部保持データをリフレッシュ
754           db["loglist"] = LogList.readxml(LISTXMLPATH)
755           
756           # 現在編集中のデータを強制的に最上のファイルパスに変更
757           session["filepath"] = db["loglist"].path[0]
758           
759           # 前月の時刻を作成します
760           prevmonth = (DateTime.now << 1)
761           
762           # 前月の時刻を元に、ディフォルト書式を生成します
763           db["logpath"] = FEEDXMLDIR + prevmonth.strftime("%Y%m") + ".xml"
764           db["logdisplay"] = prevmonth.strftime("%Y年%m月").gsub("年0", "年")
765         end
766         
767         # インポートモードの場合の処理
768         when "import"
769         db["loglist"] = LogList.readxml(LISTXMLPATH)
770         
771         # リセットモードの場合の処理
772         when "reset"
773         case params["action"]
774           # リセット実行時の処理
775           when "exec"
776           file = FileUploader.new
777           file.filelist().each { |fname|
778             file.delete(fname) if File.ftype(XMLPATH + fname) == "file"
779           }
780           
781           # 全ファイルの初期化を実行する
782           # loglist.xmlの初期化
783           loglist = LogList.new(["最新の記事"], ["#{FEEDXMLDIR}diary.xml"], LISTXMLPATH)
784           loglist.to_xml
785           writer = FeedWriter.new(XMLPATH + loglist.path[0].match(/[^\/]*?$/).to_a[0])
786           writer.to_xml(Feed.new({}), [])
787           
788           db["loglist"] = LogList.readxml(LISTXMLPATH)
789           
790           # diary.xmlの初期化
791           writer = FeedWriter.new(XMLPATH + db["loglist"].path[0].match(/[^\/]*?$/).to_a[0])
792           writer.to_xml(Feed.new({}), [])
793           
794           # 初期の編集ファイルはloglist.xml上の最も上のファイル
795           session["filepath"] = db["loglist"].path[0]
796           db["feed"] = Feed.readxml(XMLPATH + session["filepath"].match(/[^\/]*?$/).to_a[0].to_s)
797           db["entry"] = Entry.readxml(XMLPATH + session["filepath"].match(/[^\/]*?$/).to_a[0].to_s)
798           
799           # 画面の遷移先をトップページにする
800           params["mode"] = ""
801           params["action"] = ""
802           # パラメタ無しの処理
803         else
804           # ファイル一覧を取得する
805           filelist = FileUploader.new.filelist()
806           
807           # タイプがファイルのみリストとして取得する
808           db["filelist"] = []
809           filelist.each { |fname| db["filelist"] << fname if File.ftype(XMLPATH + fname) == "file"}
810         end
811         
812         # ログイン時の画面
813       else
814         # loglist.xmlが存在するかチェック
815         if File.exist?(LISTXMLPATH) == false
816           # なかった場合はloglist.xmlを自動生成
817           loglist = LogList.new(["最新の記事"], ["#{FEEDXMLDIR}diary.xml"], LISTXMLPATH)
818           loglist.to_xml
819           writer = FeedWriter.new(XMLPATH + loglist.path[0].match(/[^\/]*?$/).to_a[0])
820           writer.to_xml(Feed.new({}), [])
821         end
822         
823         db["loglist"] = LogList.readxml(LISTXMLPATH)
824         
825         # diary.xmlが存在するかチェック
826         if File.exist?(XMLPATH + db["loglist"].path[0].match(/[^\/]*?$/).to_a[0]) == false
827           # なかった場合はdiary.xmlを自動生成
828           writer = FeedWriter.new(XMLPATH + db["loglist"].path[0].match(/[^\/]*?$/).to_a[0])
829           writer.to_xml(Feed.new({}), [])
830         end
831         
832         # 初期の編集ファイルはloglist.xml上の最も上のファイル
833         session["filepath"] = db["loglist"].path[0] if session["filepath"] == nil
834         db["feed"] = Feed.readxml(XMLPATH + session["filepath"].match(/[^\/]*?$/).to_a[0].to_s)
835         db["entry"] = Entry.readxml(XMLPATH + session["filepath"].match(/[^\/]*?$/).to_a[0].to_s)
836       end
837     end
838   end
839   
840   # マルチパートフォームの場合の処理です
841   def Controller.MultiForm(cgi, session, params, db)
842     db.transaction do
843       case params["mode"]
844         # 特定位置に挿入する場合の処理
845         when "insert"
846         case params["action"]
847           when "exec"
848           # ファイルを実際にアップロードします
849           file = FileUploader.new
850           file.upload(db["logpath"], db["importxml"])
851           
852           # loglist.xmlを更新します
853           db["loglist"].path[db["loginsertindex"], 0] = db["logpath"]
854           db["loglist"].display[db["loginsertindex"], 0] = db["logdisplay"]
855           db["loglist"].to_xml
856           
857           # 情報を更新します
858           db["loglist"] = LogList.readxml(LISTXMLPATH)
859           session["filepath"] = db["loglist"].path[0] if session["filepath"] == nil
860           db["feed"] = Feed.readxml(XMLPATH + session["filepath"].match(/[^\/]*?$/).to_a[0].to_s)
861           db["entry"] = Entry.readxml(XMLPATH + session["filepath"].match(/[^\/]*?$/).to_a[0].to_s)
862           
863           when "confirm"
864           # 入力されたログファイルパスが既に存在するかを確認
865           checkflag = true
866           db["logpath"] = Controller.get_mpart_value(cgi["logpath"])
867           db["loglist"].path.each do |val|
868             if val == db["logpath"]
869               # 重複していた場合エラーメッセージを表示し、処理を行わない
870               db["logpath"] = ""
871               params["action"] = ""
872               db["error"] = "<br><span style='color: #ff0000'>同一のファイルが存在します!別の名前を指定してください。</span><br>"
873               checkflag = false
874               break;
875             end
876           end
877           
878           if checkflag == true
879             db["loginsertindex"] = cgi["loginsertindex"].read.to_i
880             db["logdisplay"] = Controller.get_mpart_value(cgi["logdisplay"])
881             db["importxml"] = cgi["updata"].read
882             
883             # XMLの整合性をチェックします
884             begin
885               REXML::Document.new(Controller.fix_updata_enc(db["importxml"].to_s))
886             rescue => exception
887               db["error"] = "<br><span style='color: #ff0000'>不正な整形のXMLです!ファイルを見直してください。</span><br>"
888               db["error"] << "<div class='divstyle' style='width: 560px; color: #ff0000; text-align: left;'>"
889               db["error"] << CGI.escapeHTML(exception.to_s).gsub("\n", "<br>")
890               db["error"] << "</div>"
891               params["action"] = ""
892             end
893           end
894           
895           # 0位置への挿入防止
896           if db["loginsertindex"] == 0
897             params["action"] = ""
898             db["error"] = "<br><span style='color: #ff0000'>ラジオボックスでログの挿入位置を選択してください!</span><br>"
899           end
900           
901           if db["logpath"] == FEEDXMLDIR || db["logdisplay"].blank?
902             params["action"] = ""
903             db["error"] = "<br><span style='color: #ff0000'>インポートファイル、及び、ログの表示名は空欄にできません。</span><br>"
904           end
905         else
906           # loglist.xmlをロードします
907           db["loglist"] = LogList.readxml(LISTXMLPATH)
908         end
909         
910         # diary.xmlを入れ替える処理
911         when "replace"
912         case params["action"]
913           when "exec"
914           # diary.xmlを移動します
915           FileUtils.move(XMLPATH + "diary.xml", XMLPATH + db["logpath"].match(/[^\/]*?$/).to_s)
916           # ファイルをアップロードします
917           file = FileUploader.new
918           file.upload("diary.xml", db["importxml"])
919           
920           # loglist.xmlを更新します
921           db["loglist"].path[1, 0] = db["logpath"]
922           db["loglist"].display[1, 0] = db["logdisplay"]
923           db["loglist"].to_xml
924           
925           # 情報を更新します
926           db["loglist"] = LogList.readxml(LISTXMLPATH)
927           session["filepath"] = db["loglist"].path[0] if session["filepath"] == nil
928           db["feed"] = Feed.readxml(XMLPATH + session["filepath"].match(/[^\/]*?$/).to_a[0].to_s)
929           db["entry"] = Entry.readxml(XMLPATH + session["filepath"].match(/[^\/]*?$/).to_a[0].to_s)
930           
931           when "confirm"
932           # 入力されたログファイルパスが既に存在するかを確認
933           checkflag = true
934           db["logpath"] = Controller.get_mpart_value(cgi["logpath"])
935           db["loglist"].path.each do |val|
936             if val == db["logpath"]
937               # 重複していた場合エラーメッセージを表示し、処理を行わない
938               params["action"] = ""
939               db["error"] = "<br><span style='color: #ff0000'>同一のファイルが存在します!別の名前を指定してください。</span><br>"
940               checkflag = false
941               break;
942             end
943           end
944           
945           if checkflag == true
946             db["logdisplay"] = Controller.get_mpart_value(cgi["logdisplay"])
947             db["importxml"] = cgi["updata"].read
948             
949             # XMLの整合性をチェックします
950             begin
951               REXML::Document.new(Controller.fix_updata_enc(db["importxml"].to_s))
952             rescue => exception
953               db["error"] = "<br><span style='color: #ff0000'>不正な整形のXMLです!ファイルを見直してください。</span><br>"
954               db["error"] << "<div class='divstyle' style='width: 560px; color: #ff0000; text-align: left;'>"
955               db["error"] << CGI.escapeHTML(exception.to_s).gsub("\n", "<br>")
956               db["error"] << "</div>"
957               params["action"] = ""
958             end
959           end
960           
961           if db["logpath"].blank? || db["logdisplay"].blank? || db["importxml"].blank?
962             params["action"] = ""
963             db["error"] = "<span style='color: #ff0000'>インポートファイル、及び、ログの表示名、ログのパスは空欄にできません。</span><br>"
964           end
965         else
966           # loglist.xmlをロードします
967           db["loglist"] = LogList.readxml(LISTXMLPATH)
968         end
969       else
970         # loglist.xmlをロードします
971         db["loglist"] = LogList.readxml(LISTXMLPATH)
972       end
973     end
974   end
975   
976   # Formのenctypeがmultypartだった場合、入力された値をrubyバージョンに左右されずに読み取るメソッドです。
977   def Controller.get_mpart_value(cgi_param)
978     if RUBY_VERSION >= "1.9.0"
979       cgi_param[0..cgi_param.length].to_s
980     else
981       cgi_param.read.to_s
982     end
983   end
984   
985   # アップロードされたXMLをバージョンに左右されずに読み取るために、ruby-1.9.1以上ではエンコーディングを強制指定します
986   def Controller.fix_updata_enc(file_to_s)
987     if RUBY_VERSION >= "1.9.0"
988       file_to_s.force_encoding("UTF-8")
989     else
990       file_to_s
991     end
992   end
993   
994 end
995
996 def main
997   # SESSION変数、パラメータなどを取得します
998   cgi = CGI.new
999   session = CGI::Session.new(cgi)
1000   params = Hash[*cgi.params.to_a.map{|k, v| [k, v[0].to_s]}.flatten]
1001   
1002   # コントローラー部分
1003   # セッション管理
1004   if session["login"] != "true"
1005     if (cgi["loginid"] == LOGINID && cgi["password"] == PASSWORD)
1006       session["login"] = "true"
1007       
1008       # ワークフォルダの中をクリーンアップします
1009       filelist = Dir::entries("./work")
1010       # 削除条件 : 最終変更日時から1日(60*60*24sec)かつ、ファイルタイプがファイルの場合
1011       filelist.each do |file|
1012         File.delete("./work/#{file}") if Time.now - File.ctime("./work/#{file}") > 86400 && File.ftype("./work/#{file}") == "file"
1013       end
1014     end
1015   end
1016   
1017   # ログアウト処理
1018   if params["mode"] == "logout"
1019     session["login"] = nil
1020     session["logini"] = nil
1021     session["password"] = nil
1022     session.delete
1023   end
1024   
1025   begin
1026     # メインコントローラー
1027     # セッションが有効な場合のも実行します
1028     if session["login"] == "true"
1029       # PStore破損チェック!
1030       begin
1031         db = PStore.new("./work/#{session.session_id}.dat")
1032         db.transaction do
1033           db["error"] = ""
1034         end
1035       rescue
1036         # PStoreファイルを破棄する
1037         File.delete("./work/#{session.session_id}.dat")
1038         # PStoreが破損していた場合はセッション情報を破棄する
1039         session["login"] = nil
1040         session["logini"] = nil
1041         session["password"] = nil
1042         session.delete
1043       end
1044       
1045       # フォームによって挙動を変更します
1046       if cgi["mode"].respond_to?(:read) && cgi["action"].respond_to?(:read)
1047         params["mode"] = cgi["mode"].read
1048         params["action"] = cgi["action"].read
1049         Controller.MultiForm(cgi, session, params, db)
1050       else
1051         Controller.NormalForm(cgi, session, params, db)
1052       end
1053     end
1054     
1055     # メニューとして表示されるHTML文字列です
1056 =begin
1057     if USEFILEMANAGER == true
1058       menu = "<div class=\"divstyle\" style=\"width: #{TABLEWIDTH}px;\"><div class=\"divstyle\" align=\"center\" style=\"margin-bottom: 5px;\">現在編集中のファイル : #{session["filepath"]} [ <a href=\"#{cgi.script_name}?mode=selectlog\">他のファイルを選択</a> ]</div>[ <a href=\"#{cgi.script_name}\">トップページ</a> | 記事管理 ( <a href=\"#{cgi.script_name}?mode=newentry\">作成</a> | <a href=\"#{cgi.script_name}?mode=editentry\">編集</a> | <a href=\"#{cgi.script_name}?mode=delentry\">消去</a> )  | <a href=\"#{cgi.script_name}?mode=editfeed\">XML情報編集</a> | <a href=\"#{cgi.script_name}?mode=log\">ログ管理</a> | <a href=\"#{cgi.script_name}?mode=import\">インポート</a> | <a href=\"#{FILEMANAGER}\" target=\"_blank\">ファイル管理</a> | <a href=\"#{cgi.script_name}?mode=reset\">初期化</a> | <a href=\"#{cgi.script_name}?mode=logout\">ログアウト</a> ]</div>"
1059     else
1060       menu = "<div class=\"divstyle\" style=\"width: #{TABLEWIDTH}px;\"><div class=\"divstyle\" align=\"center\" style=\"margin-bottom: 5px;\">現在編集中のファイル : #{session["filepath"]} [ <a href=\"#{cgi.script_name}?mode=selectlog\">他のファイルを選択</a> ]</div>[ <a href=\"#{cgi.script_name}\">トップページ</a> | 記事管理 ( <a href=\"#{cgi.script_name}?mode=newentry\">作成</a> | <a href=\"#{cgi.script_name}?mode=editentry\">編集</a> | <a href=\"#{cgi.script_name}?mode=delentry\">消去</a> )  | <a href=\"#{cgi.script_name}?mode=editfeed\">XML情報編集</a> | <a href=\"#{cgi.script_name}?mode=log\">ログ管理</a> | <a href=\"#{cgi.script_name}?mode=import\">インポート</a> | <a href=\"#{cgi.script_name}?mode=reset\">初期化</a> | <a href=\"#{cgi.script_name}?mode=logout\">ログアウト</a> ]</div>"
1061     end
1062 =end
1063
1064     # メニューとして表示されるHTML文字列です
1065     if USEFILEMANAGER == true
1066       menu = "<div class=\"divstyle\" style=\"width: #{TABLEWIDTH}px;\"><div class=\"divstyle\" align=\"center\" style=\"margin-bottom: 5px;\">現在編集中のファイル : #{session["filepath"]} [ <a class=\"menu_link\" onclick=\"location.href ='#{cgi.script_name}?mode=selectlog'\">他のファイルを選択</a> ]</div>[ <a class=\"menu_link\" onclick=\"location.href ='#{cgi.script_name}'\">トップページ</a> | 記事管理 ( <a class=\"menu_link\" onclick=\"location.href ='#{cgi.script_name}?mode=newentry'\">作成</a> | <a class=\"menu_link\" onclick=\"location.href ='#{cgi.script_name}?mode=editentry'\">編集</a> | <a class=\"menu_link\" onclick=\"location.href ='#{cgi.script_name}?mode=delentry'\">消去</a> )  | <a class=\"menu_link\" onclick=\"location.href ='#{cgi.script_name}?mode=editfeed'\">XML情報編集</a> | <a class=\"menu_link\" onclick=\"location.href ='#{cgi.script_name}?mode=log'\">ログ管理</a> | <a class=\"menu_link\" onclick=\"location.href ='#{cgi.script_name}?mode=import'\">インポート</a> | <a href=\"#{FILEMANAGER}\" target=\"_blank\">ファイル管理</a> | <a class=\"menu_link\" onclick=\"location.href ='#{cgi.script_name}?mode=reset'\">初期化</a> | <a class=\"menu_link\" onclick=\"location.href ='#{cgi.script_name}?mode=logout'\">ログアウト</a> ]</div>"
1067     else
1068       menu = "<div class=\"divstyle\" style=\"width: #{TABLEWIDTH}px;\"><div class=\"divstyle\" align=\"center\" style=\"margin-bottom: 5px;\">現在編集中のファイル : #{session["filepath"]} [ <a class=\"menu_link\" onclick=\"location.href ='#{cgi.script_name}?mode=selectlog'\">他のファイルを選択</a> ]</div>[ <a class=\"menu_link\" onclick=\"location.href ='#{cgi.script_name}'\">トップページ</a> | 記事管理 ( <a class=\"menu_link\" onclick=\"location.href ='#{cgi.script_name}?mode=newentry'\">作成</a> | <a class=\"menu_link\" onclick=\"location.href ='#{cgi.script_name}?mode=editentry'\">編集</a> | <a class=\"menu_link\" onclick=\"location.href ='#{cgi.script_name}?mode=delentry'\">消去</a> )  | <a class=\"menu_link\" onclick=\"location.href ='#{cgi.script_name}?mode=editfeed'\">XML情報編集</a> | <a class=\"menu_link\" onclick=\"location.href ='#{cgi.script_name}?mode=log'\">ログ管理</a> | <a class=\"menu_link\" onclick=\"location.href ='#{cgi.script_name}?mode=import'\">インポート</a> | <a class=\"menu_link\" onclick=\"location.href ='#{cgi.script_name}?mode=reset'\">初期化</a> | <a class=\"menu_link\" onclick=\"location.href ='#{cgi.script_name}?mode=logout\">ログアウト</a> ]</div>"
1069     end
1070     
1071     # ビュー部分
1072     # modeとactionの相関図は以下のようになります。
1073     # [mode] + [action]
1074     #        + [action]
1075     #        + [action]
1076     #        + ... and more
1077     if session["login"] != "true"
1078       # セッションが存在しない場合は強制的にトップページに遷移
1079       htmlwriter = HtmlWriter.new("./erbtemp/login.html.erb", binding)
1080     else
1081       case params["mode"]
1082         when "selectlog"
1083         htmlwriter = HtmlWriter.new("./erbtemp/select.html.erb", binding)
1084         when "newentry"
1085         htmlwriter = HtmlWriter.new("./erbtemp/newentry.html.erb", binding)
1086         when "editentry"
1087         htmlwriter = HtmlWriter.new("./erbtemp/editentry.html.erb", binding)
1088         when "delentry"
1089         htmlwriter = HtmlWriter.new("./erbtemp/delentry.html.erb", binding)
1090         when "editfeed"
1091         htmlwriter = HtmlWriter.new("./erbtemp/editfeed.html.erb", binding)
1092         when "log"
1093         htmlwriter = HtmlWriter.new("./erbtemp/log.html.erb", binding)
1094         when "insert"
1095         htmlwriter = HtmlWriter.new("./erbtemp/insertfeed.html.erb", binding)
1096         when "replace"
1097         htmlwriter = HtmlWriter.new("./erbtemp/replacefeed.html.erb", binding)
1098         when "import"
1099         htmlwriter = HtmlWriter.new("./erbtemp/indeximport.html.erb", binding)
1100         when "reset"
1101         htmlwriter = HtmlWriter.new("./erbtemp/reset.html.erb", binding)
1102       else
1103         htmlwriter = HtmlWriter.new("./erbtemp/index.html.erb", binding)
1104       end
1105     end
1106   rescue => exception
1107     # エラーが発生した場合、それを画面に表示します
1108     htmlwriter = HtmlWriter.new("./erbtemp/exception.html.erb", binding)
1109   end
1110   
1111   # 実際にHTMLを出力します
1112   begin
1113     cgi.out{htmlwriter.to_code}
1114   rescue => exception
1115     # エラーが発生した場合、それを画面に表示します
1116     htmlwriter = HtmlWriter.new("./erbtemp/exception.html.erb", binding)
1117     cgi.out{htmlwriter.to_code}
1118   end
1119 end
1120
1121 begin
1122   main
1123 rescue => evar
1124   # エラーが発生した場合、それを画面に表示します
1125   detail = ("%s: %s (%s)\n" %
1126   [evar.backtrace[0], evar.message, evar.send('class')]) +
1127   evar.backtrace[1..-1].join("\n")
1128   puts "content-type: text/html\n\n<plaintext>\n" + detail
1129 end