#!/usr/local/bin/ruby
# -*- coding: utf-8 -*-
#
#= Galleryマネージャ用アプリケーション
#
#Autohr::    Kureha Hisame (http://lunardial.sakura.ne.jp/)
#Version::   1.0.0.0
#Copyright:: Copyright 2013 Kureha Hisame (http://lunardial.sakura.ne.jp/)
#License::   GPLv3

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

require "./common.rb"
require "./define.rb"

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

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

# = FileNotExistedExceptionクラス
#
# 独自定義の例外クラスです。WebFileインスタンスの処理で、対象ファイルが存在しない場合にthrowされます。
class FileNotExistedException < 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 = []
    @sort_type = ''
    @sort_reverse = ''

    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, :sort_type, :sort_reverse

  # ディレクトリ内部のファイル・フォルダ一覧を取得するメソッド
  def ls
    filelist = Dir::entries(pwd)
    filelist.delete(".")
    filelist.delete("..")

    # ファイルリスト一覧のソート処理。
    case @sort_type
    when "ftype"
      if @sort_reverse == "true"
        filelist.sort!{|a, b| 
          (File.ftype(pwd + b) <=> File.ftype(pwd + a)).nonzero? || 
          (File.basename(pwd + a) <=> File.basename(pwd + b))
        }
      else
        filelist.sort!{|a, b| 
          (File.ftype(pwd + a) <=> File.ftype(pwd + b)).nonzero? || 
          (File.basename(pwd + a) <=> File.basename(pwd + b))
        }
      end
    when "ctime"
      filelist.sort!{|a, b| File.ctime(pwd + b) <=> File.ctime(pwd + a) }
      # ファイルリストを逆順にするか
      filelist.reverse! if @sort_reverse == "true"
    when "mtime"
      filelist.sort!{|a, b| File.mtime(pwd + b) <=> File.mtime(pwd + a) }
      # ファイルリストを逆順にするか
      filelist.reverse! if @sort_reverse == "true"
    when "size"
      if @sort_reverse == "true"
        filelist.sort!{|a, b| 
          (File.ftype(pwd + a) <=> File.ftype(pwd + b)).nonzero? || 
          (File.size(pwd + a) <=> File.size(pwd + b)).nonzero? || 
          (File.basename(pwd + a) <=> File.basename(pwd + b))
        }
      else
        filelist.sort!{|a, b| 
          (File.ftype(pwd + a) <=> File.ftype(pwd + b)).nonzero? || 
          (File.size(pwd + b) <=> File.size(pwd + a)).nonzero? || 
          (File.basename(pwd + a) <=> File.basename(pwd + b))
        }
      end
    when "name"
      filelist.sort!{|a, b| File.basename(pwd + a) <=> File.basename(pwd + b) }
      # ファイルリストを逆順にするか
      filelist.reverse! if @sort_reverse == "true"
    else
      if @sort_reverse == "true"
        filelist.sort!{|a, b| 
          (File.ftype(pwd + b) <=> File.ftype(pwd + a)).nonzero? || 
          (File.basename(pwd + a) <=> File.basename(pwd + b))
        }
      else
        filelist.sort!{|a, b| 
          (File.ftype(pwd + a) <=> File.ftype(pwd + b)).nonzero? || 
          (File.basename(pwd + a) <=> File.basename(pwd + b))
        }
      end
    end
    
    filelist
  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[fname][:sort] = File.mtime(pwd + "/" + fname).to_i
    }
    
    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 move(from_name, dest_name)
    if File.exist?(pwd + File.basename(dest_name))
      raise FileExistedException
    end
    
    unless File.exist?(pwd + File.basename(from_name))
      raise FileNotExistedException
    end
    
    unless File.basename(from_name).match(FILEREGEXP)
      raise WebSecurityException
    end
    
    unless File.basename(dest_name).match(FILEREGEXP)
      raise WebSecurityException
    end
    
    File.rename(pwd + File.basename(from_name), pwd + File.basename(dest_name))
  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

# = PluginManagerクラス
#
# プラグインの処理を行うクラスです
class PluginManager
  def self.exec(mode, module_name, filename, filepath)
    Dir.foreach(PLUGINDIR) do |fn|
      begin 
        next unless File.extname(fn) == '.rb'
        require File.join(PLUGINDIR, fn)
        plugin_name = "#{module_name}::"
        plugin_name << File.basename(fn).gsub(/\.rb\Z/, "")
        plugin_ins = plugin_name.split(/::/).inject(Object) { |c,name| c.const_get(name) }
        plugin_ins.new.exec(mode, filename, filepath)
      rescue
      end
    end
  end
end

# = Controllerクラス
#
# コントローラ部分に相当する処理を受け持つクラスです
class Controller
  def self.update_session(session, filer)
    session["filelist"] = filer.ls
    session["fileinfo"] = filer.lsinfo
    session["sort_type"] = filer.sort_type
    session["sort_reverse"] = filer.sort_reverse
    session["pwd"] = filer.pwd
    session["relpath_list"] = filer.relpath_list.join("/")
  end

  def Controller.MultiForm(cgi, session, params, db)
    db.transaction do
      # 共通初期化処理
      filer = WebFiler.new(IMGPATH)
      begin
        filer.relpath_list = params["relpath_list"].split("/")
        filer.sort_type = params["sort_type"]
        filer.sort_reverse = params["sort_reverse"]
      rescue
      end
      filer.sort_type = "ftype" if filer.sort_type == nil or filer.sort_type.empty?
      Controller.update_session(session, filer);
      
      case params["action"]
        # アップロード時
      when "upload"
        if (cgi["updata"].size == 0)
          session["error"] = "アップロードするファイルが選択されていません！"
        elsif (cgi["updata"].size <= UPLOADLIMIT)
          begin
            # ファイル名称指定判定
            upload_filename = nil
            unless params["file_rename"].empty?
              upload_filename = File.basename(params["file_rename"])
              # 空白は自動変換する
              upload_filename.gsub!(/( |　)/, "_")
            else
              upload_filename = cgi["updata"].original_filename
            end
            
            filer.upload(cgi["updata"], upload_filename)
            
            # サムネイル作成開始
            if params["thumbs"] == "true" and File.extname(upload_filename) =~ /\.(jpg|JPG|png|PNG)\Z/
              begin
                require('./plugins/ResizeManager.rb')
                if filer.relpath_list.size == 0
                  ResizeManager::ResizeManager.create_thumbs(IMGPATH + upload_filename)
                else
                  ResizeManager::ResizeManager.create_thumbs(IMGPATH + filer.relpath_list.join("/") + "/" + upload_filename)
                end
              rescue LoadError
                # With no action
              rescue => evar
                # With no action
                session["error"] = "サムネイル作成に失敗しました。<br>"
                session["error"] << evar.to_s
              end
            end
            # 独自改造 終了
                        
          rescue FileExistedException
            session["error"] = "既に同名のファイルが存在します！"
          rescue WebSecurityException
            session["error"] = "ファイル名に使用できない文字列が含まれています！"
          end
          
          
        else
          session["error"] = "ファイルの容量が大きすぎます！"
        end
        
        # ファイル一覧を更新
        Controller.update_session(session, filer)
        
        session["info"] = "正常にファイル（#{upload_filename}）のアップロードが完了しました。" if session["error"] == ""
        
        # 移動時
      when "move"
        session["selectlist"] = []
        count = 0
        from_name = nil;
        dest_name = params["destname"]
        session["filelist"].each do |file|
          if params["filename_" + file] == "true"
            count = count + 1
            from_name = file
          end
        end
        
        if count == 0
          mes = "対象が選択されていません。"
          session["error"] = mes
        elsif count > 1
          mes = "一つ以上の対象がチェックされています。<br>"
          mes << "リネーム処理は一度に一つの対象にしか行えません。"
          session["error"] = mes
        elsif dest_name.empty?
          mes = "リネーム名称が入力されていません。"
          session["error"] = mes
        else
          begin
            filer.move(from_name, dest_name)
          rescue FileExistedException
            session["error"] = "既に同名のファイルが存在します！"
          rescue FileNotExistedException
            session["error"] = "選択したファイルは既に削除されました。"
          rescue WebSecurityException
            session["error"] = "リネーム名称に使用できない文字列が含まれています！"
          end
        end
        
        # ファイル一覧を更新
        Controller.update_session(session, filer);

        session["info"] = "正常にファイルのリネームが完了しました。" if session["error"] == ""

        # 削除時
      when "delete"
        session["dellist"] = []
        count = 0
        session["filelist"].each do |file|
          if params["filename_" + file] == "true" && filer.ftype(file) == "file"
            filer.delete(file) 
            count = count + 1
          elsif params["filename_" + file] == "true" && 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"
        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"
        begin
          filer.cd(params["arg"])
        rescue
          session["error"] = "移動先のフォルダが見つかりません！"
        end
        
        # ファイル一覧を更新
        Controller.update_session(session, filer);

        # 絶対位置でのディレクトリ移動時
      when "cd_abs"
        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"
        
        # 初期表示
      else
        
      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"}
  
  # ロガーを作成する
  logger = WebLogger.get_logger(cgi.script_name, LOG_DIR, LOG_RELEASE_MODE)
  
  # コントローラー部分
  # セッション管理
  session["info"] = ""
  session["error"] = ""
  if session["login"] != "true"
    # ログイン情報を確認
    LOGININFO.each {|h|
      if (cgi["loginid"] == h[:id] && cgi["password"] == h[:password])
        session["login"] = "true"
        session["name"] = h[:name]
        
        # ログを記録
        logger.info "ログインユーザ：#{h[:id]}, IPアドレス：#{cgi.remote_addr}"
        
        # ワークフォルダの中をクリーンアップします
        filelist = Dir::entries("./work")
        # 削除条件 : 最終変更日時から1日(60*60*24sec)かつ、ファイルタイプがファイルの場合
        filelist.each do |file|
          File.delete("./work/#{file}") if Time.now - File.ctime("./work/#{file}") > 86400 && File.ftype("./work/#{file}") == "file"
        end
        break
      end
    }
    
    # ログイン失敗ユーザを記録
    if (session["login"] != "true" and (cgi["loginid"] != "" or cgi["password"] != ""))
      session["error"] = "ログインに失敗しました。ユーザIDまたはパスワードを確認して下さい。"
      logger.info "次のアクセスがログインに失敗しました。ログインID：#{cgi["loginid"]}, IPアドレス：#{cgi.remote_addr}"
    end
  end

  # ログアウト処理
  if params["mode"] == "logout"
    session["login"] = nil
    session["logini"] = nil
    session["password"] = nil
    session.delete
  end

  
  begin
    # セッションが有効な場合のみコントローラを実行します
    if session["login"] == "true"
      db = PStore.new("./work/#{session.session_id}_file.dat")
      # コントローラ部分
      Controller.MultiForm(cgi, session, params, db)
    end

    # ビュー部分
    if session["login"] != "true"
      # セッションが存在しない場合は強制的にエラーページに遷移
      htmlwriter = HtmlWriter.new("./erbtemp/login.html.erb", binding)
    else
      htmlwriter = HtmlWriter.new("./erbtemp/filemanager.html.erb", binding)
    end

    # 実際にHTMLを出力します
    cgi.out{htmlwriter.to_code}
  rescue => exception
    # ログに記録を実施します
    logger.error(exception.to_s)
    logger.error(exception.backtrace.join "\n")
    
    # エラーが発生した場合、それを画面に表示します
    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
