Imported File#ftype spec from rubyspecs.
[rbx.git] / lib / cgi / session.rb
blob82eb7534d8faf53dbf1f8af99b7f134d5702450c
2 # cgi/session.rb - session support for cgi scripts
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
8 # Author: Yukihiro "Matz" Matsumoto
10 # Documentation: William Webber (william@williamwebber.com)
12 # == Overview
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.
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.
26 require 'cgi'
27 require 'tmpdir'
29 class CGI
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
158     class NoSession < RuntimeError #:nodoc:
159     end
161     # The id of this session.
162     attr_reader :session_id, :new_session
164     def Session::callback(dbman)  #:nodoc:
165       Proc.new{
166         dbman[0].close unless dbman.empty?
167       }
168     end
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
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
302     # Retrieve the session data for key +key+.
303     def [](key)
304       @data ||= @dbman.restore
305       @data[key]
306     end
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
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
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
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
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
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
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
422       # Update and close the session's FileStore file.
423       def close
424         update
425       end
427       # Close and delete the session's FileStore file.
428       def delete
429         File::unlink @path
430       rescue Errno::ENOENT
431       end
432     end
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:
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
457       # Restore session state.
458       #
459       # Returns session data as a hash.
460       def restore
461         GLOBAL_HASH_TABLE[@session_id]
462       end
464       # Update session state.
465       #
466       # A no-op.
467       def update
468         # don't need to update; hash is shared
469       end
471       # Close session storage.
472       #
473       # A no-op.
474       def close
475         # don't need to close
476       end
478       # Delete the session state.
479       def delete
480         GLOBAL_HASH_TABLE.delete(@session_id)
481       end
482     end
483   end