# $Id: session.rb,v 1.73 2004/04/28 13:17:58 eto Exp $
# Copyright (C) 2003-2004 Kouichirou Eto, All rights reserved.
# This is free software with ABSOLUTELY NO WARRANTY.
# You can redistribute it and/or modify it under the terms of the GNU GPL2.

require 'webrick'
require 'net/smtp'
require 'qwik/command'
require 'qwik/site'
require 'qwik/qxml'
require 'qwik/util'
require 'qwik/mail'
require 'qwik/generator'
require 'qwik/ext_member'

module Qwik
  class Servlet < WEBrick::HTTPServlet::AbstractServlet
    def do_GET(request, response)
      config = @server[:QwikConfig]
      memory = @server[:QwikMemory]
      req = Request.new(config, request)
      res = Response.new(config, response)
      res.domain    = req.domain
      res.sitename  = req.sitename
      session = Session.new(config, memory, req, res)
      runner = SessionRunner.new(session)
      runner.run
      res.setback(response)
    end
    alias do_POST do_GET
  end

  class Request
    def initialize(config, request=nil)
      @config = config
      @default_hostname = @config.default_hostname.dup
      @frontpage_html = @config.frontpage+'.html'

      @domain = @sitename = @path = nil
      @file = nil
      @request_method = nil
      @cookies = {}
      @header = {}
      @query = {}
      @user = @pass = nil
      parse_webrick(request) if request.is_a? WEBrick::HTTPRequest
    end
    attr_reader :sitename, :path, :file
    attr_reader :cookies, :query
    attr_accessor :domain, :user, :pass
    attr_accessor :request_method # for test

    def parse_host_path(host, path)
      @domain, @sitename, @path, @file = parse_req(host, path)
    end

    def parse_webrick(request)
      uri = request.request_uri
      host = uri ? uri.host : @default_hostname
      path = WEBrick::HTTPUtils.normalize_path(request.path)
      parse_host_path(host, path)

      @header = request.header.dup if request.header

      request.cookies.each {|c| @cookies[c.name] = c.value }

      @query = query_to_utf8(request.query)

      @request_method = request.request_method

      @user = @pass = nil
    end

    def query_to_utf8(oq)
      nq = Hash.new
      oq.each {|k, v|
	nq[k] = v.sjistou8
      }
      nq
    end

    def [](k)
      return nil if !@header
      v = @header[k.downcase]
      (v == nil || v.empty?) ? nil : v.join(", ")
    end

    def parse_host(h)
      h = @default_hostname if h =~ /^([.0-9]+)$/ || !h.include?(".") # IP address or host name?
      h.downcase
    end

    def base()
      File.basename(@file, ".*")
    end

    def ext()
      File.extname(@file).sub(/^\./, '')
    end

    def parse_path(u)
      if u =~ %r|/$|
	u += @frontpage_html
      end
      pa = u.path
      path = pa.dirname.to_s
      file = pa.basename.to_s
      [path, file]
    end

    def parse_req(h, u)
      domain = parse_host(h)
      path, file = parse_path(u)
      sitename = 'www'
      if path != '/'
	ar = path.split('/')
	ar.shift
	sitename = ar.shift
      end
      [domain, sitename, path, file]
    end
  end

  class Response
    COOKIE_EXP = 60*60*24*30 # 30 days
    def initialize(config, response=nil)
      @config = config
      @body = nil
      @status = 200
      @cookies = []
      @headers = {}
      @sitename = ""
      @domain = '' # for test
      @mimetypes = {}
      if response && response.is_a?(WEBrick::HTTPResponse)
	@mimetypes = response.config[:MimeTypes]
      end
    end
    attr_reader :mimetypes
    attr_accessor :body, :status
    attr_accessor :sitename
    attr_writer :domain # for test
    attr_reader :headers # for test

    def [](k)     @headers[k];     end
    def []=(k, v) @headers[k] = v; end

    def set_cookies(user, pass)
      @cookies << make_cookie('user', user)
      @cookies << make_cookie('pass', pass)
    end

    def clear_cookies()
      # Set cookies to 60 sec ago, to clear cookies from browser.
      @cookies << make_cookie('user', '', -60)
      @cookies << make_cookie('pass', '', -60)
    end

    def make_cookie(k, v, exp=COOKIE_EXP)
      c = WEBrick::Cookie.new(k, v)
#      dom = @domain
#      dom = @sitename if @sitename =~ /\./ # if the sitename contains a dot, treat the sitename as domain
#      dom = '127.0.0.1' if @localmode
#      c.domain = dom
      c.path = '/'
      c.expires = Time.now + exp
      c
    end

    def setback(response)
      body = ''
      if @body.nil?
	body = ''
      elsif @body.is_a? String
	body = @body
      elsif @body.is_a? IO
	body = @body
      else
	body = @body.to_xhtml(0)
      end

      if @headers['Content-Type'] =~ %r|text/html; charset=Shift_JIS|i
	body = body.u8tosjis
      end

      response.body = body
      response.status = @status
      @cookies.each {|c| response.cookies << c }
      @headers.each {|k, v| response[k] = v }
    end
  end

  class Session
    def initialize(config, memory, req, res)
      @config = config
      @req    = req
      @res    = res
      @memory = memory
      @site   = nil
      @xml    = nil
    end
    attr_reader :config, :req, :res, :memory
    attr_accessor :site, :xml

    def run()
      runner = SessionRunner.new(self)
      runner.run
    end
  end

  class InvalidMailError < StandardError; end
  class InvalidUserError < StandardError; end
  class SetCookieRetry   < StandardError; end

  class SessionRunner
    def initialize(session)
      @session = session
      @config  = @session.config
      @memory  = @session.memory
      @req     = @session.req
      @res     = @session.res
    end

    def run()
      process_session()
    end

    private
    def process_session()
      return send_theme if @req.path =~ %r|/theme|

      @session.site = @memory.farm.get_site(@req.sitename)
      return no_such_site(@req.sitename) if @session.site.nil?

      # no need for authentication
      if @req.file == @config.getpass+'.html'
	mail = @req.query['mail']
	return sendpass(mail) if mail
	return getpass()
      end

      begin
	authenticate_user()
      rescue InvalidMailError
	return invalid_mail()
      rescue InvalidUserError
	return invalid_user()
      rescue SetCookieRetry
	@res.set_cookies(@req.user, @req.pass)
	# If the method was GET, force redirect (for security reason).
	if @req.request_method == 'GET'
	  return redirect(@session.site)
	end
      end

      require_mail   = true
      require_member = true
      if @session.site.open?
	require_mail   = false
	require_member = false
      end

      if require_mail && ! @req.user
	return login()
      end

      if require_member && !@session.site.member.exist?(@req.user)
	return not_member(@session.site)
      end

      if @req.ext == "html"
	command_executor = CommandExecutor.new(@session)
	@session.xml = command_executor.dispatch
	@res.body = @session.xml
	@res['Content-Type'] = "text/html; charset=Shift_JIS"
#	if !@req.cmd
	  @res['Pragma'] = 'no-cache'
	  @res['Cache-Control'] = 'no-cache'
#	end

      elsif @req.base == @req.sitename
	if @req.ext == 'rss'
	  @res['Content-Type'] = "application/xml"
	  rssgen = RSS10Generator.new(@config, @session.site)
	  @session.xml = rssgen.generate
	  @res.body = @session.xml.to_s(0)
	  return
	end

      elsif @res.mimetypes[@req.ext]
	file = @session.site.pages.path(@req.file).to_s
	type = @res.mimetypes[@req.ext]
	return

      else
	return error_page("unknown error: [#{@req.ext}]")
      end
    end

    def authenticate_user()
      @req.user, @req.pass = @req.query['user'], @req.query['pass']
      if @req.user
	check_user(@req.user, @req.pass)
	raise SetCookieRetry
      end

      @req.user, @req.pass = @req.cookies['user'], @req.cookies['pass']
      if @req.user
	check_user(@req.user, @req.pass)
      end
    end

    def check_user(user, pass)
      raise InvalidUserError if user.nil? || user == ""
      raise InvalidMailError unless MailAddress.new(user).valid?
      gen = PasswordGenerator.new(@config)
      raise InvalidUserError if gen.generate(user, 0) != pass
    end

    #--------------------------------------------------- method for Login
    def no_such_site(sitename)
      error_page("対応するサイトがありません"){|g| [
	  g.p{sitename+"という、指定のURLに対応するqwikWebサイトはありません。"},
	  g.p{"qwikWebのサイトを作るには、まずメールを送る必要があります。"},
	  g.p{[g.a("http://qwik.jp/"){'qwik.jp'}, "にアクセスして、使い方をご覧下さい。"]}
	]}
    end

    def invalid_mail()
      error_page("ログイン: "+@session.site.url){|g| [
	  g.p.style('color: red'){"メールアドレスの形式が違います。"},
	  login_form(g),
	  goto_getpass(g, "パスワードを忘れましたか?")
	]}
    end

    def invalid_user()
      error_page("ログイン: "+@session.site.url){|g| [
	  g.p.style('color: red'){"ID(E-mail)もしくはパスワードが間違っております。"},
	  g.p{"入力に間違いがないかどうかもう一度メールをご確認ください。"},
	  g.p{"(コピー&ペーストだとエラーになる場合があります。その場合はお手数ですがキーボードから入力してみてください)"},
	  login_form(g),
	  goto_getpass(g, "パスワードを忘れましたか?")
	]}
    end

    def redirect()
      @res.status = 302
      url = @session.site.url+@req.file
      @res['Location'] = url
    end

    def login()
      error_page("ログイン: "+@session.site.url){|g| [
	  login_form(g),
	  goto_getpass(g, "パスワードをまだ持ってない?")
	]}
    end

    def not_member(site)
      error_page(site.url+"のメンバーですか?"){|g| [
	  g.p { "あなたはこのグループのメンバーではないようです。" },
	  g.p{["一旦", g.a("/"+@config.logoutpage+".html"){"ログアウト"}, "してから再度アクセスしてください。"]}
	]}
    end

    def goto_getpass(g, msg)
      g.div{[
	  g.h3{msg},
	  g.p{"アクセスにはパスワードが必要です。"},
	  g.p{[g.a(@config.getpass+".html"){"パスワード入手ページ"},"をご覧ください。"]}
	]}
    end

    def login_form(g)
      @res.clear_cookies()
      g.form('method'=>'POST'){[
	  g.p{"ID(E-mail)とパスワードを入力してください。"},
	  "ID(E-mail): ", g.text('user'), g.br,
	  "Password: ", g.password('pass'), g.br,
	  g.submit(' ログイン ')
	]}
    end

    def getpass()
      make_page("パスワード入手"){|g| mail_form(g) }
    end

    def getpass_invalid_mail()
      make_page("invalid mail address"){|g| [
	  g.p{[
	      g.b{mail}, "は正常なメールアドレスではありません。もう一度入力を確認してください。"
	    ]}, mail_form(g)
	]}
    end

    def mail_form(g)
      g.form('method'=>'POST'){[
	  g.p{"下記エリアにあなたのメールアドレスを入力してください。"},
	  g.p{"メールにてパスワードが送られます。"},
	  g.text('mail', nil, 40),
	  g.submit(" パスワードを入手 ")
	]}
    end

    def sendpass(mail)
      return getpass_invalid_mail() unless MailAddress.new(mail).valid?
      pmgen = PasswordMailGenerator.new(@config, @session.site)
      passmail = pmgen.generate(mail)
      sendmail = Sendmail.new(@config)
      sendmail.sendmail(passmail)

      make_page("Send Password"){|g| [
	  g.p {[ g.b{mail}, "に、対応するパスワードを送付いたしました。" ]},
	  g.p { "パスワードを入手しましたら、再度アクセスしてみてください。" }
	]}
    end

    def error_page(msg, &block)
      make_page(msg, &block)
    end

    def make_page(msg, &block)
      g = Qxhtml.new
      body = g.body
      body << g.h2{msg}
      body << g.div{block.call(g)} if block_given?
      html = g.html{[
	  g.head{[
	      g.make('title'){msg+' - qwik.jp'},
	      g.stylesheet("/theme/base.css"),
	      g.stylesheet("/theme/hiki/hiki.css")
	    ]},
	  body
	]}
      doc = REXML::Document.new
      doc << html
      @res.body = doc
      @res['Content-Type'] = "text/html; charset=Shift_JIS"
    end

    #----------------------------------------------------------------------
    def send_theme
      file = @config.lib_dir+@req.path+'/'+@req.file
      type = @res.mimetypes[@req.ext]
      simple_send(file, type)
    end

    def simple_send(local_path, mtype)
      return if !local_path.path.exist?
      st = local_path.path.stat
      mtime = st.mtime
      @res['etag'] = sprintf("%x-%x-%x", st.ino, st.size, st.mtime.to_i)
      if not_modified?(mtime)
	@res.body = ''
	raise WEBrick::HTTPStatus::NotModified
      else
	@res['content-type']   = mtype
	@res['content-length'] = st.size
	@res['last-modified']  = mtime.httpdate
	if @config.ssl
	  @res.body = open(local_path, "rb"){|f| f.read}
	else
	  @res.body = open(local_path, "rb")
	end
      end
    end

    def not_modified?(mtime)
      if ir = @req['if-range']
	begin
	  return true if ::Time.httpdate(ir) >= mtime
	rescue
	  return true if WEBrick::HTTPUtils::split_header_valie(ir).member?(@res['etag'])
	end
      end

      return true if (ims = @req['if-modified-since']) &&
	::Time.parse(ims) >= mtime

      return true if (inm = @req['if-none-match']) &&
	WEBrick::HTTPUtils::split_header_value(inm).member?(@res['etag'])

      false
    end
  end
end
