~ubuntu-branches/ubuntu/hardy/ruby1.8/hardy-updates

« back to all changes in this revision

Viewing changes to lib/cgi/session.rb

  • Committer: Bazaar Package Importer
  • Author(s): akira yamada
  • Date: 2007-03-13 22:11:58 UTC
  • mfrom: (1.1.5 upstream)
  • Revision ID: james.westby@ubuntu.com-20070313221158-h3oql37brlaf2go2
Tags: 1.8.6-1
* new upstream version, 1.8.6.
* libruby1.8 conflicts with libopenssl-ruby1.8 (< 1.8.6) (closes: #410018)
* changed packaging style to cdbs from dbs.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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 (william@williamwebber.com)
 
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
 
 
26
require 'cgi'
 
27
require 'tmpdir'
 
28
 
 
29
class 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
 
484
end