#--
# Copyright (c) 2004 David Heinemeier Hansson
# Copyright (c) 2006 Yugui <yugui@yugui.sakura.ne.jp>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; version 2.1
# of the License
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#--
#
# AjpRailsRequest is based on lib/action_controller/cgi_process.rb which is
# distributed as a port of actionpack 1.1.12. 
# The original file is avaiable here;
#   http://rubyforge.org/frs/?group_id=249&release_id=3791
#
# The following is the permission notice of the original
# lib/action_controller/cgi_process.rb, not for this script.
#
# --
# Copyright (c) 2004 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++

require 'forwardable'
require 'action_controller/cgi_ext/cgi_methods'

class Net::AJP13::Request
  def cookies
    unless @cookies
      @cookies = {}
      cookie_strings = self.get_fields('cookie')
      cookie_strings.each do |cookie_string|
        @cookies.update(CGI::Cookie::parse(cookie_string))
      end if cookie_strings
      @cookies.freeze
    end
    @cookies
  end

  attr_reader :output_cookies
end

# Wraps Net::AJP13::Request to adapt it to request object in rails.
class AjpRailsRequest < ActionController::AbstractRequest
  DEFAULT_SESSION_OPTIONS = ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS.merge('session_key' => 'JSESSIONID')

  extend Forwardable

  def initialize(ajp_request, session_options, server_environments)
    @req = ajp_request
    @session_options = session_options
    @server_environments = server_environments
    @session_options['session_path'] ||= @server_environments['APP_LOCATION']

    if server_environments['LOAD_BALANCE_ID'] and @req.cookies['JSESSIONID']
      balance_id = server_environments['LOAD_BALANCE_ID']
      @req.cookies['JSESSIONID'].each do |value|
        case value
	when String
	  value.gsub!(/\.#{Regexp.escape(balance_id)}\Z/, '')
	when Array
	  value.each do |entry|
            entry.gsub!(/\.#{Regexp.escape(balance_id)}\Z/, '')
	  end
	end
      end
    end

    # simulates environment hash.
    @env = self.class.instance_method(:environment).bind(self)
    # Object#method is overridden by AbstractRequest#method

    class << @env
      def include?(key) !call(key).nil? end
      alias :key? :include?
    end
  end
  attr_reader :env

  attr_accessor :session_options

  def method
    @req.method.downcase.to_sym
  end

  def environment(name)
    case name
    when /^\AHTTP_(\w+)\Z/
      name = $1.tr('_', '-')
      @req[name]
    else
      @req[name] or @server_environments[name]
    end
  end

  def raw_post
    if @req.body
      @req.body
    elsif @req.body_stream
      stream = @req.body_stream
      @req.body_stream = nil
      @req.body = stream.read
    else
      ''
    end
  end

  def path
    path = @req.path
    path = path.sub(%r!\A#{Regexp.escape(@server_environments['DISPATCHER_PREFIX'])}!, '') if @server_environments['DISPATCHER_PREFIX']
    path = path.sub(%r!#{Regexp.escape(@server_environments['DISPATCHER_SUFFIX'])}\Z!, '') if @server_environments['DISPATCHER_SUFFIX']
    path
  end

  def request_uri
    if @request_uri
      @request_uri
    else
      uri = path
      qs = query_string
      uri << '?' << qs[0] if qs
      @request_uri = uri
    end
  end

  def_delegators :@req, :ssl?, :cookies

  def server_software
    val = @req.get_attributes('server_software')
    val and val[0] and /([A-Za-z]+)/ =~ val[0] and $1.downcase
  end

  def query_string
    val = @req.get_attributes('query_string')
    val and val[0]
  end

  def query_parameters
    (qs = self.query_string).blank? ? {} : CGIMethods.parse_query_parameters(qs)
  end

  def post_params
    @post_params ||= CGI.parse(raw_post)
    @post_params
  end

  def request_parameters
    if formatted_post?
      CGIMethods.parse_formatted_request_parameters(post_format, raw_post)
    else
      CGIMethods.parse_request_parameters(self.post_params)
    end
  end

  def host
    @req['x-forwarded-host'] ||
      ($1 if @req.server_name and /\A(.*):\d+\Z/ =~ @req.server_name) ||
      (@req.server_name and @req.server_name.split(':').first) ||
      ($1 if @req['host'] and /\A(.*):\d+\Z/ =~ @req['host']) ||
      (@req['host'] and @req['host'].split(':').first) ||
      ''
  end
  def port
    @req['x-forwarded-host'] ? standard_port : (port_from_http_host || @req.server_port)
  end

  def port_from_http_host
    $1.to_i if @req['host'] && /:(\d+)$/ =~ @req['host']
  end

  def session
    unless @session
      if @session_options == false
        @session = Hash.new
      else
        stale_session_check! do
	  if session_options_with_string_keys['new_session'] == true
	    @session = new_session
	  else
            @session = CGI::Session.new(@req, session_options_with_string_keys)
	  end
	  @session['__valid_session']
	end
      end
    end
    @session
  end

  def reset_session
    @session.delete if CGI::Session === @session
    @session = new_session
  end

  private
  # Delete an old session if it exists then create a new one.
  def new_session
    if @session_options == false
      Hash.new
    else
      CGI::Session.new(@req, session_options_with_string_keys.merge("new_session" => false)).delete rescue nil
      CGI::Session.new(@req, session_options_with_string_keys.merge("new_session" => true))
    end
  end
  
  def stale_session_check!
    yield
  rescue ArgumentError => argument_error
    if argument_error.message =~ %r{undefined class/module (\w+)}
      begin
        Module.const_missing($1)
      rescue LoadError, NameError => const_error
        raise ActionController::SessionRestoreError, <<end_msg
Session contains objects whose class definition isn't available.
Remember to require the classes for all objects kept in the session.
(Original exception: #{const_error.message} [#{const_error.class}])
end_msg
      end
      retry
    else
      raise
    end
  end
  def session_options_with_string_keys
    @session_options_with_string_keys ||= DEFAULT_SESSION_OPTIONS.merge(@session_options).inject({}) { |options, (k,v)| options[k.to_s] = v; options }
  end
end

class AjpRailsResponse < ActionController::AbstractResponse
  def to_ajp_response(extra_cookies = nil)
    raise "Unrecognized status line #{headers['Status']}" unless
      md = /\A(\d{3})(?: (.+))?/.match(headers['Status'])

    res = md[2] ?
      Net::AJP13::Response.new(md[1].to_i, :reason_phrase => md[2]) :
      Net::AJP13::Response.new(md[1])

    @headers.each do |key, value|
      case key.downcase
      when 'status'
        # do nothing
      when 'expires'
        res.add_field(key, CGI::rfc1123_date(options.delete(key)))
      when 'cookie'
        case value
        when String
          res.add_field('Set-Cookie', value)
        when CGI::Cookie
          res.add_field('Set-Cookie', value.to_s)
        when Array
          value.each {|cookie| res.add_field('Set-Cookie', cookie) }
        when Hash
          value.each_value {|cookie| res.add_field('Set-Cookie', cookie) }
        end
      else
        res.add_field(key, value.to_s) if key != 'Status'
      end
    end
    res['content-type'] ||= 'text/html'
    if extra_cookies
      extra_cookies.each do |cookie|
        res.add_field('Set-Cookie', cookie.to_s)
      end
    end
    
    if @body.respond_to?(:call)
      buf = ''
      @body.call(self, StringIO.new(buf))
      res.body = buf
    else
      res.body = self.body
    end

    return res
  end
end