#!/usr/local/bin/ruby
# -*- coding: utf-8 -*-
#
#= FeedGeneratorから起動するファイルマネージャWEBアプリケーション
#
#Autohr::    Kureha Hisame (http://lunardial.sakura.ne.jp/) & Yui Naruse (http://airemix.com/)
#Version::   2.0.0.0
#Copyright:: Copyright 2009 FeedBlog Project (http://sourceforge.jp/projects/feedblog/)
#License::   GPLv3

require "cgi"
require "cgi/session"
require "erb"
require "rexml/document"
require "pstore"
require "fileutils"

# インターフェースのテーブルの幅
TABLEWIDTH = 800
# 画像フォルダの場所を定義
IMGPATH = "./../lunardial/xml/img/"

# バージョン情報を示す文字列です
APPVERSION = "- FileManager 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>)"
# タイトル領域に表示される文字列です
APPTITLE = "FileManager for Ruby version 2.0.0.0"

# アップロード可能な最大ファイルサイズ
UPLOADLIMIT = 2 * 1024 * 1024

# = Objectクラス
#
# 基本クラスのオーバーライドを行います
class Object
  # myopenメソッド
  #
  # ruby-1.9.x以降ではファイルを開いた際、エンコードの指定を行わないとエラーの原因になります。
  # ただしruby-1.8.6以前はエンコードの指定に対応していないため、独自メソッドを定義してファイルの入出力を行います。
  #
  # _arg[0]_ :: 入出力を行うファイルのパス
  # _arg[1]_ :: モードの指定。例 : w:utf-8(書き込みモード・UTF-8エンコードでファイルを開く)
  def myopen(*arg)
    mode = arg[1]
    rdonly_p = true
    case mode
    when String
      arg[1] = mode[/[^:]+/] if RUBY_VERSION < "1.8.7" && mode.include?(':')
      rdonly_p = /\A[^:]*[wa+]/ !~ mode
    when Numeric
      rdonly_p = !(mode & (IO::WRONY | IO::RDWR))
    end
    open(*arg) do |f|
      f.flock(rdonly_p ? File::LOCK_SH : File::LOCK_EX)
      return yield(f)
    end
  end
end

class NilClass
  def blank?
    nil?
  end
end

class Array
  def blank?
    empty?
  end
end

class String
  def blank?
    empty?
  end
end

# = WebSecurityExceptionクラス
#
# 独自定義の例外クラスです。WebFilerインスタンス内の処理でセキュリティ違反が発生した場合にthrowされます。
class WebSecurityException < Exception
end

# = FileExistedExceptionクラス
#
# 独自定義の例外クラスです。WebFileインスタンスの処理で、既に存在するファイルに上書きしようとした場合にthrowされます。
class FileExistedException < Exception
end

# = HtmlWriterクラス
# 
# テンプレートファイル(*.erb)を読み込み、管理するクラスです
class HtmlWriter
  # 初期化メソッドです
  #
  # _template_ :: テンプレートファイル(*.erb)のパス
  # _binding_ :: binding変数
  def initialize(template, binding)
    @erb = ERB.new(myopen(template, "r:utf-8") {|f| f.read}, nil, "-")
    @binding = binding
  end
  
  # テンプレートファイルの文字列を返却するメソッドです
  def to_code
    @erb.result(@binding)
  end
end

# = WebFilerクラス
#
# Web上にローカルと同じフォルダ管理機能を利用できるクラスです
class WebFiler
  # 初期化メソッドです
  #
  # _basepath_ :: クラス内で扱う最上位のフォルダ(root)とする実パス
  def initialize(basepath)
    @basepath = basepath
    @basepath << "/" unless @basepath[-1..-1] == "/"
    @relpath_list = []
    
    raise "Target dir not found." unless File.directory?(pwd)
  end
  
  # ファイルに使用できない文字列を調べるための正規表現を定義します
  FILEREGEXP = /\A[-_+~^0-9a-zA-Z]+(\.[-_+~^0-9a-zA-Z]+)*\z/
  
  attr_reader :basepath
  attr_accessor :relpath_list
  
  # ディレクトリ内部のファイル・フォルダ一覧を取得するメソッド
  def ls
    filelist = Dir::entries(pwd)
    filelist.delete(".")
    filelist.delete("..")
    
    filelist.sort!
  end
  
  # ディレクトリ内部のファイル・フォルダ一覧の詳細情報を取得するメソッド
  def lsinfo
    filelist = Dir::entries(pwd)
    filelist.delete(".")
    filelist.delete("..")
    
    fileinfo = {}
    filelist.each { |fname| 
      fileinfo[fname] = {}
      fileinfo[fname][:ftype] = File.ftype(pwd + "/" + fname)
      fileinfo[fname][:atime] = File.atime(pwd + "/" + fname)
      fileinfo[fname][:ctime] = File.ctime(pwd + "/" + fname)
      fileinfo[fname][:mtime] = File.mtime(pwd + "/" + fname)
      fileinfo[fname][:size] = File.size(pwd + "/" + fname) / 1000
    }
    
    fileinfo
  end
  
  # ファイルタイプを取得するメソッド
  def ftype(fname)
    File.ftype(pwd + File.basename(fname))
  end
  
  # ディレクトリを移動するメソッド
  def cd(pathname)
    if pathname == ".."
      @relpath_list.delete_at(-1) unless @relpath_list.length == 0
    elsif pathname.match(FILEREGEXP)
      if File.directory?(pwd + "/" + File.basename(pathname))
        @relpath_list << File.basename(pathname)
      else
        raise FileExistedException
      end
    else
      raise WebSecurityException
    end
  end
  
  # ディレクトリを絶対指定で移動するメソッド
  def cd_abs(relpath_arr)
    relpath_arr.each { |name|
      unless name.match(FILEREGEXP)
        raise WebSecurityException
      end
    }
    @relpath_list = relpath_arr
  end
  
  # 現在のディレクトリを表示するメソッド
  def pwd
    @basepath + relpath
  end
  
  # 相対パスを算出するメソッド
  def relpath
    if @relpath_list.length == 0
      ""
    else
      @relpath_list.join("/") << "/"
    end
  end
  
  # ファイルをアップロードするメソッド
  def upload(file, fname)
    fname.gsub!(/( |　)/, "_")
    if File.exist?(pwd + File.basename(fname))
      raise FileExistedException
    elsif File.basename(fname).match(FILEREGEXP)
      open(pwd + File.basename(fname), "w") do |f|
        f.binmode
        f.write file.read
      end
    else
      raise WebSecurityException
    end
  end
  
  # ファイルを消去するメソッド
  def delete(fname)
    if File.exist?(pwd + File.basename(fname)) && fname.match(FILEREGEXP)
      File.delete(pwd + File.basename(fname))
    else
      raise WebSecurityException
    end
  end
  
  # ディレクトリを製作するメソッド
  def mkdir(dirname)
    dirname.gsub!(/( |　)/, "_")
    if File.exist?(pwd + File.basename(dirname))
      raise FileExistedException
    elsif dirname.match(FILEREGEXP)
      Dir.mkdir(pwd + File.basename(dirname))
    else
      raise WebSecurityException
    end
  end
  
  # 内部が空のディレクトリを消去するメソッド
  def rmdir(dirname)
    if File.exist?(pwd + File.basename(dirname)) && dirname.match(FILEREGEXP)
      Dir.rmdir(pwd + File.basename(dirname))
    else
      raise WebSecurityException
    end
  end
  
end

# = Controllerクラス
#
# コントローラ部分に相当する処理を受け持つクラスです
class Controller
  def self.update_session(session, filer)
    session["filelist"] = filer.ls.reverse
    session["fileinfo"] = filer.lsinfo
    session["pwd"] = filer.pwd
    session["relpath_list"] = filer.relpath_list.join("/")
  end
  
  def Controller.MultiForm(cgi, session, params, db)
    db.transaction do
      case params["action"]
        # アップロード時
        when "upload"
        filer = WebFiler.new(IMGPATH)
        filer.relpath_list = params["relpath_list"].split("/")
        Controller.update_session(session, filer);
        
        if (cgi["updata"].size <= UPLOADLIMIT)
          begin
            filer.upload(cgi["updata"], cgi["updata"].original_filename)
          rescue FileExistedException
            session["error"] = "既に同名のファイルが存在します！"
          rescue WebSecurityException
            session["error"] = "ファイル名に使用できない文字列が含まれています！"
          end
        else
          session["error"] = "ファイルの容量が大きすぎます！"
        end
        
        Controller.update_session(session, filer);
        
        session["info"] = "正常にアップロードが完了しました。" if session["error"] == ""
        
        # 削除時
        when "delete"
        filer = WebFiler.new(IMGPATH)
        filer.relpath_list = params["relpath_list"].split("/")
        Controller.update_session(session, filer);
        
        session["dellist"] = []
        count = 0
        session["filelist"].each do |file|
          if params["filename_" + file] == "delete" && filer.ftype(file) == "file"
            filer.delete(file) 
            count = count + 1
          elsif params["filename_" + file] == "delete" && filer.ftype(file) == "directory"
            begin
              filer.rmdir(file)
              count = count + 1
            rescue
              mes = "対象のディレクトリは空ではありません！<br>"
              mes << "ディレクトリを削除する場合は、事前にディレクトリ内部の全てのファイルを削除してください。"
              session["error"] = mes
            end
          end
        end
        
        Controller.update_session(session, filer);
        
        session["info"] = "正常にファイルの削除が完了しました。" if session["error"] == "" && count != 0
        
        # ディレクトリ製作時
        when "mkdir"
        filer = WebFiler.new(IMGPATH)
        filer.relpath_list = params["relpath_list"].split("/")
        Controller.update_session(session, filer);
        
        begin
          filer.mkdir(params["dirname"])
        rescue FileExistedException
          session["error"] = "既に同名のディレクトリが存在します！"
        rescue WebSecurityException
          session["error"] = "ディレクトリ名に使用できない文字列が含まれています！"
        end
        
        Controller.update_session(session, filer);
        
        session["info"] = "正常にディレクトリの作成が完了しました。" if session["error"] == ""
        
        # ディレクトリ移動時
        when "cd"
        filer = WebFiler.new(IMGPATH)
        filer.relpath_list = params["relpath_list"].split("/")
        Controller.update_session(session, filer);
        
        begin
          filer.cd(params["arg"])
        rescue
          session["error"] = "移動先のディレクトリが見つかりません！"
        end
        
        Controller.update_session(session, filer);
        
        # 絶対位置でのディレクトリ移動時
        when "cd_abs"
        filer = WebFiler.new(IMGPATH)
        filer.relpath_list = params["relpath_list"].split("/")
        Controller.update_session(session, filer);
        
        if params["arg"].to_i >= 0
          begin
            movepath = []
            params["arg"].to_i.times { |i|
              movepath << params["relpath_list"].split("/")[i]
            }
            filer.cd_abs(movepath)
          rescue
            session["error"] = "移動先のディレクトリが見つかりません！"
          end
        else
          session["error"] = "移動先のディレクトリが見つかりません！"
        end
        
        Controller.update_session(session, filer);
        
        # 表示更新時
        when "refresh"
        filer = WebFiler.new(IMGPATH)
        filer.relpath_list = params["relpath_list"].split("/")
        Controller.update_session(session, filer);
        
        # 初期表示
      else
        filer = WebFiler.new(IMGPATH)
        Controller.update_session(session, filer);
      end
    end
  end
end

def main
  # SESSION変数、パラメータなどを取得します
  cgi = CGI.new
  session = CGI::Session.new(cgi)
  params = Hash[*cgi.params.to_a.map{|k, v| [k, v[0].to_s]}.flatten]
  params.each { |k, v| params[k] = cgi[k].read if cgi[k].respond_to?(:read) && k != "updata"}
  
  # ログアウト処理
  if params["mode"] == "logout"
    session["login"] = nil
    session["logini"] = nil
    session["password"] = nil
    session.delete
  end
  
  # メインコントローラー
  # セッションが有効な場合のも実行します
  if session["login"] == "true"
    db = PStore.new("./work/#{session.session_id}_file.dat")
    session["info"] = ""
    session["error"] = ""
    
    # コントローラ部分
    Controller.MultiForm(cgi, session, params, db)
  end
  
  # ビュー部分
  if session["login"] != "true"
    # if session["login"] == "true"
    # セッションが存在しない場合は強制的にエラーページに遷移
    htmlwriter = HtmlWriter.new("./erbtemp/fileindex.html.erb", binding)
  else
    htmlwriter = HtmlWriter.new("./erbtemp/filemanager.html.erb", binding)
  end
  
  # 実際にHTMLを出力します
  begin
    cgi.out{htmlwriter.to_code}
  rescue => exception
    # エラーが発生した場合、それを画面に表示します
    htmlwriter = HtmlWriter.new("./erbtemp/exception.html.erb", binding)
    cgi.out{ htmlwriter.to_code }
  end
end

begin
  main
rescue => evar
  # エラーが発生した場合、それを画面に表示します
  detail = ("%s: %s (%s)\n" %
  [evar.backtrace[0], evar.message, evar.send('class')]) +
  evar.backtrace[1..-1].join("\n")
  puts "content-type: text/html\n\n<plaintext>\n" + detail
end
