source: extensions/gsdl-video/trunk/installed/cmdline/lib/ruby/1.8/cgi/session.rb@ 18425

Last change on this file since 18425 was 18425, checked in by davidb, 15 years ago

Video extension to Greenstone

File size: 16.7 KB
Line 
1#
2# cgi/session.rb - session support for cgi scripts
3#
4# Copyright (C) 2001 Yukihiro "Matz" Matsumoto
5# Copyright (C) 2000 Network Applied Communication Laboratory, Inc.
6# Copyright (C) 2000 Information-technology Promotion Agency, Japan
7#
8# Author: Yukihiro "Matz" Matsumoto
9#
10# Documentation: William Webber ([email protected])
11#
12# == Overview
13#
14# This file provides the +CGI::Session+ class, which provides session
15# support for CGI scripts. A session is a sequence of HTTP requests
16# and responses linked together and associated with a single client.
17# Information associated with the session is stored
18# on the server between requests. A session id is passed between client
19# and server with every request and response, transparently
20# to the user. This adds state information to the otherwise stateless
21# HTTP request/response protocol.
22#
23# See the documentation to the +CGI::Session+ class for more details
24# and examples of usage. See cgi.rb for the +CGI+ class itself.
25
26require 'cgi'
27require 'tmpdir'
28
29class CGI
30
31 # Class representing an HTTP session. See documentation for the file
32 # cgi/session.rb for an introduction to HTTP sessions.
33 #
34 # == Lifecycle
35 #
36 # A CGI::Session instance is created from a CGI object. By default,
37 # this CGI::Session instance will start a new session if none currently
38 # exists, or continue the current session for this client if one does
39 # exist. The +new_session+ option can be used to either always or
40 # never create a new session. See #new() for more details.
41 #
42 # #delete() deletes a session from session storage. It
43 # does not however remove the session id from the client. If the client
44 # makes another request with the same id, the effect will be to start
45 # a new session with the old session's id.
46 #
47 # == Setting and retrieving session data.
48 #
49 # The Session class associates data with a session as key-value pairs.
50 # This data can be set and retrieved by indexing the Session instance
51 # using '[]', much the same as hashes (although other hash methods
52 # are not supported).
53 #
54 # When session processing has been completed for a request, the
55 # session should be closed using the close() method. This will
56 # store the session's state to persistent storage. If you want
57 # to store the session's state to persistent storage without
58 # finishing session processing for this request, call the update()
59 # method.
60 #
61 # == Storing session state
62 #
63 # The caller can specify what form of storage to use for the session's
64 # data with the +database_manager+ option to CGI::Session::new. The
65 # following storage classes are provided as part of the standard library:
66 #
67 # CGI::Session::FileStore:: stores data as plain text in a flat file. Only
68 # works with String data. This is the default
69 # storage type.
70 # CGI::Session::MemoryStore:: stores data in an in-memory hash. The data
71 # only persists for as long as the current ruby
72 # interpreter instance does.
73 # CGI::Session::PStore:: stores data in Marshalled format. Provided by
74 # cgi/session/pstore.rb. Supports data of any type,
75 # and provides file-locking and transaction support.
76 #
77 # Custom storage types can also be created by defining a class with
78 # the following methods:
79 #
80 # new(session, options)
81 # restore # returns hash of session data.
82 # update
83 # close
84 # delete
85 #
86 # Changing storage type mid-session does not work. Note in particular
87 # that by default the FileStore and PStore session data files have the
88 # same name. If your application switches from one to the other without
89 # making sure that filenames will be different
90 # and clients still have old sessions lying around in cookies, then
91 # things will break nastily!
92 #
93 # == Maintaining the session id.
94 #
95 # Most session state is maintained on the server. However, a session
96 # id must be passed backwards and forwards between client and server
97 # to maintain a reference to this session state.
98 #
99 # The simplest way to do this is via cookies. The CGI::Session class
100 # provides transparent support for session id communication via cookies
101 # if the client has cookies enabled.
102 #
103 # If the client has cookies disabled, the session id must be included
104 # as a parameter of all requests sent by the client to the server. The
105 # CGI::Session class in conjunction with the CGI class will transparently
106 # add the session id as a hidden input field to all forms generated
107 # using the CGI#form() HTML generation method. No built-in support is
108 # provided for other mechanisms, such as URL re-writing. The caller is
109 # responsible for extracting the session id from the session_id
110 # attribute and manually encoding it in URLs and adding it as a hidden
111 # input to HTML forms created by other mechanisms. Also, session expiry
112 # is not automatically handled.
113 #
114 # == Examples of use
115 #
116 # === Setting the user's name
117 #
118 # require 'cgi'
119 # require 'cgi/session'
120 # require 'cgi/session/pstore' # provides CGI::Session::PStore
121 #
122 # cgi = CGI.new("html4")
123 #
124 # session = CGI::Session.new(cgi,
125 # 'database_manager' => CGI::Session::PStore, # use PStore
126 # 'session_key' => '_rb_sess_id', # custom session key
127 # 'session_expires' => Time.now + 30 * 60, # 30 minute timeout
128 # 'prefix' => 'pstore_sid_') # PStore option
129 # if cgi.has_key?('user_name') and cgi['user_name'] != ''
130 # # coerce to String: cgi[] returns the
131 # # string-like CGI::QueryExtension::Value
132 # session['user_name'] = cgi['user_name'].to_s
133 # elsif !session['user_name']
134 # session['user_name'] = "guest"
135 # end
136 # session.close
137 #
138 # === Creating a new session safely
139 #
140 # require 'cgi'
141 # require 'cgi/session'
142 #
143 # cgi = CGI.new("html4")
144 #
145 # # We make sure to delete an old session if one exists,
146 # # not just to free resources, but to prevent the session
147 # # from being maliciously hijacked later on.
148 # begin
149 # session = CGI::Session.new(cgi, 'new_session' => false)
150 # session.delete
151 # rescue ArgumentError # if no old session
152 # end
153 # session = CGI::Session.new(cgi, 'new_session' => true)
154 # session.close
155 #
156 class Session
157
158 class NoSession < RuntimeError #:nodoc:
159 end
160
161 # The id of this session.
162 attr_reader :session_id, :new_session
163
164 def Session::callback(dbman) #:nodoc:
165 Proc.new{
166 dbman[0].close unless dbman.empty?
167 }
168 end
169
170 # Create a new session id.
171 #
172 # The session id is an MD5 hash based upon the time,
173 # a random number, and a constant string. This routine
174 # is used internally for automatically generated
175 # session ids.
176 def create_new_id
177 require 'digest/md5'
178 md5 = Digest::MD5::new
179 now = Time::now
180 md5.update(now.to_s)
181 md5.update(String(now.usec))
182 md5.update(String(rand(0)))
183 md5.update(String($$))
184 md5.update('foobar')
185 @new_session = true
186 md5.hexdigest
187 end
188 private :create_new_id
189
190 # Create a new CGI::Session object for +request+.
191 #
192 # +request+ is an instance of the +CGI+ class (see cgi.rb).
193 # +option+ is a hash of options for initialising this
194 # CGI::Session instance. The following options are
195 # recognised:
196 #
197 # session_key:: the parameter name used for the session id.
198 # Defaults to '_session_id'.
199 # session_id:: the session id to use. If not provided, then
200 # it is retrieved from the +session_key+ parameter
201 # of the request, or automatically generated for
202 # a new session.
203 # new_session:: if true, force creation of a new session. If not set,
204 # a new session is only created if none currently
205 # exists. If false, a new session is never created,
206 # and if none currently exists and the +session_id+
207 # option is not set, an ArgumentError is raised.
208 # database_manager:: the name of the class providing storage facilities
209 # for session state persistence. Built-in support
210 # is provided for +FileStore+ (the default),
211 # +MemoryStore+, and +PStore+ (from
212 # cgi/session/pstore.rb). See the documentation for
213 # these classes for more details.
214 #
215 # The following options are also recognised, but only apply if the
216 # session id is stored in a cookie.
217 #
218 # session_expires:: the time the current session expires, as a
219 # +Time+ object. If not set, the session will terminate
220 # when the user's browser is closed.
221 # session_domain:: the hostname domain for which this session is valid.
222 # If not set, defaults to the hostname of the server.
223 # session_secure:: if +true+, this session will only work over HTTPS.
224 # session_path:: the path for which this session applies. Defaults
225 # to the directory of the CGI script.
226 #
227 # +option+ is also passed on to the session storage class initialiser; see
228 # the documentation for each session storage class for the options
229 # they support.
230 #
231 # The retrieved or created session is automatically added to +request+
232 # as a cookie, and also to its +output_hidden+ table, which is used
233 # to add hidden input elements to forms.
234 #
235 # *WARNING* the +output_hidden+
236 # fields are surrounded by a <fieldset> tag in HTML 4 generation, which
237 # is _not_ invisible on many browsers; you may wish to disable the
238 # use of fieldsets with code similar to the following
239 # (see http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-list/37805)
240 #
241 # cgi = CGI.new("html4")
242 # class << cgi
243 # undef_method :fieldset
244 # end
245 #
246 def initialize(request, option={})
247 @new_session = false
248 session_key = option['session_key'] || '_session_id'
249 session_id = option['session_id']
250 unless session_id
251 if option['new_session']
252 session_id = create_new_id
253 end
254 end
255 unless session_id
256 if request.key?(session_key)
257 session_id = request[session_key]
258 session_id = session_id.read if session_id.respond_to?(:read)
259 end
260 unless session_id
261 session_id, = request.cookies[session_key]
262 end
263 unless session_id
264 unless option.fetch('new_session', true)
265 raise ArgumentError, "session_key `%s' should be supplied"%session_key
266 end
267 session_id = create_new_id
268 end
269 end
270 @session_id = session_id
271 dbman = option['database_manager'] || FileStore
272 begin
273 @dbman = dbman::new(self, option)
274 rescue NoSession
275 unless option.fetch('new_session', true)
276 raise ArgumentError, "invalid session_id `%s'"%session_id
277 end
278 session_id = @session_id = create_new_id
279 retry
280 end
281 request.instance_eval do
282 @output_hidden = {session_key => session_id} unless option['no_hidden']
283 @output_cookies = [
284 Cookie::new("name" => session_key,
285 "value" => session_id,
286 "expires" => option['session_expires'],
287 "domain" => option['session_domain'],
288 "secure" => option['session_secure'],
289 "path" => if option['session_path'] then
290 option['session_path']
291 elsif ENV["SCRIPT_NAME"] then
292 File::dirname(ENV["SCRIPT_NAME"])
293 else
294 ""
295 end)
296 ] unless option['no_cookies']
297 end
298 @dbprot = [@dbman]
299 ObjectSpace::define_finalizer(self, Session::callback(@dbprot))
300 end
301
302 # Retrieve the session data for key +key+.
303 def [](key)
304 @data ||= @dbman.restore
305 @data[key]
306 end
307
308 # Set the session date for key +key+.
309 def []=(key, val)
310 @write_lock ||= true
311 @data ||= @dbman.restore
312 @data[key] = val
313 end
314
315 # Store session data on the server. For some session storage types,
316 # this is a no-op.
317 def update
318 @dbman.update
319 end
320
321 # Store session data on the server and close the session storage.
322 # For some session storage types, this is a no-op.
323 def close
324 @dbman.close
325 @dbprot.clear
326 end
327
328 # Delete the session from storage. Also closes the storage.
329 #
330 # Note that the session's data is _not_ automatically deleted
331 # upon the session expiring.
332 def delete
333 @dbman.delete
334 @dbprot.clear
335 end
336
337 # File-based session storage class.
338 #
339 # Implements session storage as a flat file of 'key=value' values.
340 # This storage type only works directly with String values; the
341 # user is responsible for converting other types to Strings when
342 # storing and from Strings when retrieving.
343 class FileStore
344 # Create a new FileStore instance.
345 #
346 # This constructor is used internally by CGI::Session. The
347 # user does not generally need to call it directly.
348 #
349 # +session+ is the session for which this instance is being
350 # created. The session id must only contain alphanumeric
351 # characters; automatically generated session ids observe
352 # this requirement.
353 #
354 # +option+ is a hash of options for the initialiser. The
355 # following options are recognised:
356 #
357 # tmpdir:: the directory to use for storing the FileStore
358 # file. Defaults to Dir::tmpdir (generally "/tmp"
359 # on Unix systems).
360 # prefix:: the prefix to add to the session id when generating
361 # the filename for this session's FileStore file.
362 # Defaults to the empty string.
363 # suffix:: the prefix to add to the session id when generating
364 # the filename for this session's FileStore file.
365 # Defaults to the empty string.
366 #
367 # This session's FileStore file will be created if it does
368 # not exist, or opened if it does.
369 def initialize(session, option={})
370 dir = option['tmpdir'] || Dir::tmpdir
371 prefix = option['prefix'] || ''
372 suffix = option['suffix'] || ''
373 id = session.session_id
374 require 'digest/md5'
375 md5 = Digest::MD5.hexdigest(id)[0,16]
376 @path = dir+"/"+prefix+md5+suffix
377 if File::exist? @path
378 @hash = nil
379 else
380 unless session.new_session
381 raise CGI::Session::NoSession, "uninitialized session"
382 end
383 @hash = {}
384 end
385 end
386
387 # Restore session state from the session's FileStore file.
388 #
389 # Returns the session state as a hash.
390 def restore
391 unless @hash
392 @hash = {}
393 begin
394 f = File.open(@path, 'r')
395 f.flock File::LOCK_SH
396 for line in f
397 line.chomp!
398 k, v = line.split('=',2)
399 @hash[CGI::unescape(k)] = CGI::unescape(v)
400 end
401 ensure
402 f.close unless f.nil?
403 end
404 end
405 @hash
406 end
407
408 # Save session state to the session's FileStore file.
409 def update
410 return unless @hash
411 begin
412 f = File.open(@path, File::CREAT|File::TRUNC|File::RDWR, 0600)
413 f.flock File::LOCK_EX
414 for k,v in @hash
415 f.printf "%s=%s\n", CGI::escape(k), CGI::escape(String(v))
416 end
417 ensure
418 f.close unless f.nil?
419 end
420 end
421
422 # Update and close the session's FileStore file.
423 def close
424 update
425 end
426
427 # Close and delete the session's FileStore file.
428 def delete
429 File::unlink @path
430 rescue Errno::ENOENT
431 end
432 end
433
434 # In-memory session storage class.
435 #
436 # Implements session storage as a global in-memory hash. Session
437 # data will only persist for as long as the ruby interpreter
438 # instance does.
439 class MemoryStore
440 GLOBAL_HASH_TABLE = {} #:nodoc:
441
442 # Create a new MemoryStore instance.
443 #
444 # +session+ is the session this instance is associated with.
445 # +option+ is a list of initialisation options. None are
446 # currently recognised.
447 def initialize(session, option=nil)
448 @session_id = session.session_id
449 unless GLOBAL_HASH_TABLE.key?(@session_id)
450 unless session.new_session
451 raise CGI::Session::NoSession, "uninitialized session"
452 end
453 GLOBAL_HASH_TABLE[@session_id] = {}
454 end
455 end
456
457 # Restore session state.
458 #
459 # Returns session data as a hash.
460 def restore
461 GLOBAL_HASH_TABLE[@session_id]
462 end
463
464 # Update session state.
465 #
466 # A no-op.
467 def update
468 # don't need to update; hash is shared
469 end
470
471 # Close session storage.
472 #
473 # A no-op.
474 def close
475 # don't need to close
476 end
477
478 # Delete the session state.
479 def delete
480 GLOBAL_HASH_TABLE.delete(@session_id)
481 end
482 end
483 end
484end
Note: See TracBrowser for help on using the repository browser.