summaryrefslogtreecommitdiff
path: root/lib/cgi/session.rb
blob: 0ce82b39200d4ea06330bf20c7cb03ef1abea74d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# 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