# Copyright (C) 2001  Yukihiro "Matz" Matsumoto
# Copyright (C) 2000  Network Applied Communication Laboratory, Inc.
# Copyright (C) 2000  Information-technology Promotion Agency, Japan

require 'cgi'
require 'tmpdir'

class CGI
  class Session

    attr_reader :session_id

    def Session::callback(dbman)
      lambda{
	dbman[0].close unless dbman.empty?
      }
    end

    def Session::create_new_id
      require 'digest/md5'
      md5 = Digest::MD5::new
      md5.update(String(Time::now))
      md5.update(String(rand(0)))
      md5.update(String($$))
      md5.update('foobar')
      md5.hexdigest[0,16]
    end

    def initialize(request, option={})
      session_key = option['session_key'] || '_session_id'
      id = option['session_id']
      unless id
	if option['new_session']
	  id = Session::create_new_id
	end
      end
      unless id
	if request.key?(session_key)
	  id = request[session_key] 
	  id = id.read if id.respond_to?(:read)
	end
	unless id
	  id, = request.cookies[session_key]
	end
	unless id
	  if option.key?('new_session') and not option['new_session']
	    raise ArgumentError, "session_key `%s' should be supplied"%session_key
	  end
	  id = Session::create_new_id
	end
      end
      @session_id = id
      dbman = option['database_manager'] || FileStore
      @dbman = dbman::new(self, option)
      request.instance_eval do
	@output_hidden = {session_key => id}
	@output_cookies =  [
          Cookie::new("name" => session_key,
		      "value" => id,
		      "expires" => option['session_expires'],
		      "domain" => option['session_domain'],
		      "secure" => option['session_secure'],
		      "path" => if option['session_path'] then
				  option['session_path']
		                elsif ENV["SCRIPT_NAME"] then
				  File::dirname(ENV["SCRIPT_NAME"])
				else
				  ""
				end)
        ]
      end
      @dbprot = [@dbman]
      ObjectSpace::define_finalizer(self, Session::callback(@dbprot))
    end

    def [](key)
      unless @data
	@data = @dbman.restore
      end
      @data[key]
    end

    def []=(key, val)
      unless @write_lock
	@write_lock = true
      end
      unless @data
	@data = @dbman.restore
      end
      @data[key] = val
    end

    def update
      @dbman.update
    end

    def close
      @dbman.close
      @dbprot.clear
    end

    def delete
      @dbman.delete
      @dbprot.clear
    end

    class FileStore
      def check_id(id)
	/[^0-9a-zA-Z]/ =~ id.to_s ? false : true
      end

      def initialize(session, option={})
	dir = option['tmpdir'] || Dir::tmpdir
	prefix = option['prefix'] || ''
	id = session.session_id
	unless check_id(id)
	  raise ArgumentError, "session_id `%s' is invalid" % id
	end
	path = dir+"/"+prefix+id
	path.untaint
	unless File::exist? path
	  @hash = {}
	end
	begin
	  @f = open(path, "r+")
	rescue Errno::ENOENT
	  @f = open(path, "w+")
	end
      end

      def restore
	unless @hash
	  @hash = {}
	  @f.flock File::LOCK_EX
	  @f.rewind
	  for line in @f
	    line.chomp!
	    k, v = line.split('=',2)
	    @hash[CGI::unescape(k)] = CGI::unescape(v)
	  end
	end
	@hash
      end

      def update
	return unless @hash
	@f.rewind
	for k,v in @hash
	  @f.printf "%s=%s\n", CGI::escape(k), CGI::escape(String(v))
	end
	@f.truncate @f.tell
      end

      def close
	return if @f.closed?
	update
	@f.close
      end

      def delete
	path = @f.path
	@f.close
	File::unlink path
      end
    end

    class MemoryStore
      GLOBAL_HASH_TABLE = {}

      def initialize(session, option=nil)
	@session_id = session.session_id
	GLOBAL_HASH_TABLE[@session_id] ||= {}
      end

      def restore
	GLOBAL_HASH_TABLE[@session_id]
      end

      def update
	# don't need to update; hash is shared
      end

      def close
	# don't need to close
      end

      def delete
	GLOBAL_HASH_TABLE.delete(@session_id)
      end
    end
  end
end
