diff options
Diffstat (limited to 'ruby/lib/cgi')
-rw-r--r-- | ruby/lib/cgi/cookie.rb | 144 | ||||
-rw-r--r-- | ruby/lib/cgi/core.rb | 786 | ||||
-rw-r--r-- | ruby/lib/cgi/html.rb | 1021 | ||||
-rw-r--r-- | ruby/lib/cgi/session.rb | 537 | ||||
-rw-r--r-- | ruby/lib/cgi/session/pstore.rb | 111 | ||||
-rw-r--r-- | ruby/lib/cgi/util.rb | 181 |
6 files changed, 2780 insertions, 0 deletions
diff --git a/ruby/lib/cgi/cookie.rb b/ruby/lib/cgi/cookie.rb new file mode 100644 index 0000000..4e8d3e2 --- /dev/null +++ b/ruby/lib/cgi/cookie.rb @@ -0,0 +1,144 @@ +# Class representing an HTTP cookie. +# +# In addition to its specific fields and methods, a Cookie instance +# is a delegator to the array of its values. +# +# See RFC 2965. +# +# == Examples of use +# cookie1 = CGI::Cookie::new("name", "value1", "value2", ...) +# cookie1 = CGI::Cookie::new("name" => "name", "value" => "value") +# cookie1 = CGI::Cookie::new('name' => 'name', +# 'value' => ['value1', 'value2', ...], +# 'path' => 'path', # optional +# 'domain' => 'domain', # optional +# 'expires' => Time.now, # optional +# 'secure' => true # optional +# ) +# +# cgi.out("cookie" => [cookie1, cookie2]) { "string" } +# +# name = cookie1.name +# values = cookie1.value +# path = cookie1.path +# domain = cookie1.domain +# expires = cookie1.expires +# secure = cookie1.secure +# +# cookie1.name = 'name' +# cookie1.value = ['value1', 'value2', ...] +# cookie1.path = 'path' +# cookie1.domain = 'domain' +# cookie1.expires = Time.now + 30 +# cookie1.secure = true +class CGI + class Cookie < Array + + # Create a new CGI::Cookie object. + # + # The contents of the cookie can be specified as a +name+ and one + # or more +value+ arguments. Alternatively, the contents can + # be specified as a single hash argument. The possible keywords of + # this hash are as follows: + # + # name:: the name of the cookie. Required. + # value:: the cookie's value or list of values. + # path:: the path for which this cookie applies. Defaults to the + # base directory of the CGI script. + # domain:: the domain for which this cookie applies. + # expires:: the time at which this cookie expires, as a +Time+ object. + # secure:: whether this cookie is a secure cookie or not (default to + # false). Secure cookies are only transmitted to HTTPS + # servers. + # + # These keywords correspond to attributes of the cookie object. + def initialize(name = "", *value) + if name.kind_of?(String) + @name = name + %r|^(.*/)|.match(ENV["SCRIPT_NAME"]) + @path = ($1 or "") + @secure = false + return super(value) + end + + options = name + unless options.has_key?("name") + raise ArgumentError, "`name' required" + end + + @name = options["name"] + value = Array(options["value"]) + # simple support for IE + if options["path"] + @path = options["path"] + else + %r|^(.*/)|.match(ENV["SCRIPT_NAME"]) + @path = ($1 or "") + end + @domain = options["domain"] + @expires = options["expires"] + @secure = options["secure"] == true ? true : false + + super(value) + end + + attr_accessor("name", "path", "domain", "expires") + attr_reader("secure") + + def value + self + end + + def value=(val) + replace(Array(val)) + end + + # Set whether the Cookie is a secure cookie or not. + # + # +val+ must be a boolean. + def secure=(val) + @secure = val if val == true or val == false + @secure + end + + # Convert the Cookie to its string representation. + def to_s + val = collect{|v| CGI::escape(v) }.join("&") + buf = "#{@name}=#{val}" + buf << "; domain=#{@domain}" if @domain + buf << "; path=#{@path}" if @path + buf << "; expires=#{CGI::rfc1123_date(@expires)}" if @expires + buf << "; secure" if @secure == true + buf + end + + end # class Cookie + + + # Parse a raw cookie string into a hash of cookie-name=>Cookie + # pairs. + # + # cookies = CGI::Cookie::parse("raw_cookie_string") + # # { "name1" => cookie1, "name2" => cookie2, ... } + # + def Cookie::parse(raw_cookie) + cookies = Hash.new([]) + return cookies unless raw_cookie + + raw_cookie.split(/[;,]\s?/).each do |pairs| + name, values = pairs.split('=',2) + next unless name and values + name = CGI::unescape(name) + values ||= "" + values = values.split('&').collect{|v| CGI::unescape(v) } + if cookies.has_key?(name) + values = cookies[name].value + values + end + cookies[name] = Cookie::new(name, *values) + end + + cookies + end +end + + diff --git a/ruby/lib/cgi/core.rb b/ruby/lib/cgi/core.rb new file mode 100644 index 0000000..4521d87 --- /dev/null +++ b/ruby/lib/cgi/core.rb @@ -0,0 +1,786 @@ +class CGI + + $CGI_ENV = ENV # for FCGI support + + # String for carriage return + CR = "\015" + + # String for linefeed + LF = "\012" + + # Standard internet newline sequence + EOL = CR + LF + + REVISION = '$Id: core.rb 23760 2009-06-20 09:06:49Z yugui $' #:nodoc: + + NEEDS_BINMODE = true if /WIN/i.match(RUBY_PLATFORM) + + # Path separators in different environments. + PATH_SEPARATOR = {'UNIX'=>'/', 'WINDOWS'=>'\\', 'MACINTOSH'=>':'} + + # HTTP status codes. + HTTP_STATUS = { + "OK" => "200 OK", + "PARTIAL_CONTENT" => "206 Partial Content", + "MULTIPLE_CHOICES" => "300 Multiple Choices", + "MOVED" => "301 Moved Permanently", + "REDIRECT" => "302 Found", + "NOT_MODIFIED" => "304 Not Modified", + "BAD_REQUEST" => "400 Bad Request", + "AUTH_REQUIRED" => "401 Authorization Required", + "FORBIDDEN" => "403 Forbidden", + "NOT_FOUND" => "404 Not Found", + "METHOD_NOT_ALLOWED" => "405 Method Not Allowed", + "NOT_ACCEPTABLE" => "406 Not Acceptable", + "LENGTH_REQUIRED" => "411 Length Required", + "PRECONDITION_FAILED" => "412 Precondition Failed", + "SERVER_ERROR" => "500 Internal Server Error", + "NOT_IMPLEMENTED" => "501 Method Not Implemented", + "BAD_GATEWAY" => "502 Bad Gateway", + "VARIANT_ALSO_VARIES" => "506 Variant Also Negotiates" + } + + # Abbreviated day-of-week names specified by RFC 822 + RFC822_DAYS = %w[ Sun Mon Tue Wed Thu Fri Sat ] + + # Abbreviated month names specified by RFC 822 + RFC822_MONTHS = %w[ Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec ] + + # :startdoc: + + def env_table + ENV + end + + def stdinput + $stdin + end + + def stdoutput + $stdout + end + + private :env_table, :stdinput, :stdoutput + + + # Create an HTTP header block as a string. + # + # Includes the empty line that ends the header block. + # + # +options+ can be a string specifying the Content-Type (defaults + # to text/html), or a hash of header key/value pairs. The following + # header keys are recognized: + # + # type:: the Content-Type header. Defaults to "text/html" + # charset:: the charset of the body, appended to the Content-Type header. + # nph:: a boolean value. If true, prepend protocol string and status code, and + # date; and sets default values for "server" and "connection" if not + # explicitly set. + # status:: the HTTP status code, returned as the Status header. See the + # list of available status codes below. + # server:: the server software, returned as the Server header. + # connection:: the connection type, returned as the Connection header (for + # instance, "close". + # length:: the length of the content that will be sent, returned as the + # Content-Length header. + # language:: the language of the content, returned as the Content-Language + # header. + # expires:: the time on which the current content expires, as a +Time+ + # object, returned as the Expires header. + # cookie:: a cookie or cookies, returned as one or more Set-Cookie headers. + # The value can be the literal string of the cookie; a CGI::Cookie + # object; an Array of literal cookie strings or Cookie objects; or a + # hash all of whose values are literal cookie strings or Cookie objects. + # These cookies are in addition to the cookies held in the + # @output_cookies field. + # + # Other header lines can also be set; they are appended as key: value. + # + # header + # # Content-Type: text/html + # + # header("text/plain") + # # Content-Type: text/plain + # + # header("nph" => true, + # "status" => "OK", # == "200 OK" + # # "status" => "200 GOOD", + # "server" => ENV['SERVER_SOFTWARE'], + # "connection" => "close", + # "type" => "text/html", + # "charset" => "iso-2022-jp", + # # Content-Type: text/html; charset=iso-2022-jp + # "length" => 103, + # "language" => "ja", + # "expires" => Time.now + 30, + # "cookie" => [cookie1, cookie2], + # "my_header1" => "my_value" + # "my_header2" => "my_value") + # + # The status codes are: + # + # "OK" --> "200 OK" + # "PARTIAL_CONTENT" --> "206 Partial Content" + # "MULTIPLE_CHOICES" --> "300 Multiple Choices" + # "MOVED" --> "301 Moved Permanently" + # "REDIRECT" --> "302 Found" + # "NOT_MODIFIED" --> "304 Not Modified" + # "BAD_REQUEST" --> "400 Bad Request" + # "AUTH_REQUIRED" --> "401 Authorization Required" + # "FORBIDDEN" --> "403 Forbidden" + # "NOT_FOUND" --> "404 Not Found" + # "METHOD_NOT_ALLOWED" --> "405 Method Not Allowed" + # "NOT_ACCEPTABLE" --> "406 Not Acceptable" + # "LENGTH_REQUIRED" --> "411 Length Required" + # "PRECONDITION_FAILED" --> "412 Precondition Failed" + # "SERVER_ERROR" --> "500 Internal Server Error" + # "NOT_IMPLEMENTED" --> "501 Method Not Implemented" + # "BAD_GATEWAY" --> "502 Bad Gateway" + # "VARIANT_ALSO_VARIES" --> "506 Variant Also Negotiates" + # + # This method does not perform charset conversion. + def header(options='text/html') + if options.is_a?(String) + content_type = options + buf = _header_for_string(content_type) + elsif options.is_a?(Hash) + if options.size == 1 && options.has_key?('type') + content_type = options['type'] + buf = _header_for_string(content_type) + else + buf = _header_for_hash(options.dup) + end + else + raise ArgumentError.new("expected String or Hash but got #{options.class}") + end + if defined?(MOD_RUBY) + _header_for_modruby(buf) + return '' + else + buf << EOL # empty line of separator + return buf + end + end # header() + + def _header_for_string(content_type) #:nodoc: + buf = '' + if nph?() + buf << "#{$CGI_ENV['SERVER_PROTOCOL'] || 'HTTP/1.0'} 200 OK#{EOL}" + buf << "Date: #{CGI.rfc1123_date(Time.now)}#{EOL}" + buf << "Server: #{$CGI_ENV['SERVER_SOFTWARE']}#{EOL}" + buf << "Connection: close#{EOL}" + end + buf << "Content-Type: #{content_type}#{EOL}" + if @output_cookies + @output_cookies.each {|cookie| buf << "Set-Cookie: #{cookie}#{EOL}" } + end + return buf + end # _header_for_string + private :_header_for_string + + def _header_for_hash(options) #:nodoc: + buf = '' + ## add charset to option['type'] + options['type'] ||= 'text/html' + charset = options.delete('charset') + options['type'] += "; charset=#{charset}" if charset + ## NPH + options.delete('nph') if defined?(MOD_RUBY) + if options.delete('nph') || nph?() + protocol = $CGI_ENV['SERVER_PROTOCOL'] || 'HTTP/1.0' + status = options.delete('status') + status = HTTP_STATUS[status] || status || '200 OK' + buf << "#{protocol} #{status}#{EOL}" + buf << "Date: #{CGI.rfc1123_date(Time.now)}#{EOL}" + options['server'] ||= $CGI_ENV['SERVER_SOFTWARE'] || '' + options['connection'] ||= 'close' + end + ## common headers + status = options.delete('status') + buf << "Status: #{HTTP_STATUS[status] || status}#{EOL}" if status + server = options.delete('server') + buf << "Server: #{server}#{EOL}" if server + connection = options.delete('connection') + buf << "Connection: #{connection}#{EOL}" if connection + type = options.delete('type') + buf << "Content-Type: #{type}#{EOL}" #if type + length = options.delete('length') + buf << "Content-Length: #{length}#{EOL}" if length + language = options.delete('language') + buf << "Content-Language: #{language}#{EOL}" if language + expires = options.delete('expires') + buf << "Expires: #{CGI.rfc1123_date(expires)}#{EOL}" if expires + ## cookie + if cookie = options.delete('cookie') + case cookie + when String, Cookie + buf << "Set-Cookie: #{cookie}#{EOL}" + when Array + arr = cookie + arr.each {|c| buf << "Set-Cookie: #{c}#{EOL}" } + when Hash + hash = cookie + hash.each {|name, c| buf << "Set-Cookie: #{c}#{EOL}" } + end + end + if @output_cookies + @output_cookies.each {|c| buf << "Set-Cookie: #{c}#{EOL}" } + end + ## other headers + options.each do |key, value| + buf << "#{key}: #{value}#{EOL}" + end + return buf + end # _header_for_hash + private :_header_for_hash + + def nph? #:nodoc: + return /IIS\/(\d+)/.match($CGI_ENV['SERVER_SOFTWARE']) && $1.to_i < 5 + end + + def _header_for_modruby(buf) #:nodoc: + request = Apache::request + buf.scan(/([^:]+): (.+)#{EOL}/o) do |name, value| + warn sprintf("name:%s value:%s\n", name, value) if $DEBUG + case name + when 'Set-Cookie' + request.headers_out.add(name, value) + when /^status$/i + request.status_line = value + request.status = value.to_i + when /^content-type$/i + request.content_type = value + when /^content-encoding$/i + request.content_encoding = value + when /^location$/i + request.status = 302 if request.status == 200 + request.headers_out[name] = value + else + request.headers_out[name] = value + end + end + request.send_http_header + return '' + end + private :_header_for_modruby + # + + # Print an HTTP header and body to $DEFAULT_OUTPUT ($>) + # + # The header is provided by +options+, as for #header(). + # The body of the document is that returned by the passed- + # in block. This block takes no arguments. It is required. + # + # cgi = CGI.new + # cgi.out{ "string" } + # # Content-Type: text/html + # # Content-Length: 6 + # # + # # string + # + # cgi.out("text/plain") { "string" } + # # Content-Type: text/plain + # # Content-Length: 6 + # # + # # string + # + # cgi.out("nph" => true, + # "status" => "OK", # == "200 OK" + # "server" => ENV['SERVER_SOFTWARE'], + # "connection" => "close", + # "type" => "text/html", + # "charset" => "iso-2022-jp", + # # Content-Type: text/html; charset=iso-2022-jp + # "language" => "ja", + # "expires" => Time.now + (3600 * 24 * 30), + # "cookie" => [cookie1, cookie2], + # "my_header1" => "my_value", + # "my_header2" => "my_value") { "string" } + # + # Content-Length is automatically calculated from the size of + # the String returned by the content block. + # + # If ENV['REQUEST_METHOD'] == "HEAD", then only the header + # is outputted (the content block is still required, but it + # is ignored). + # + # If the charset is "iso-2022-jp" or "euc-jp" or "shift_jis" then + # the content is converted to this charset, and the language is set + # to "ja". + def out(options = "text/html") # :yield: + + options = { "type" => options } if options.kind_of?(String) + content = yield + options["length"] = content.bytesize.to_s + output = stdoutput + output.binmode if defined? output.binmode + output.print header(options) + output.print content unless "HEAD" == env_table['REQUEST_METHOD'] + end + + + # Print an argument or list of arguments to the default output stream + # + # cgi = CGI.new + # cgi.print # default: cgi.print == $DEFAULT_OUTPUT.print + def print(*options) + stdoutput.print(*options) + end + + # Parse an HTTP query string into a hash of key=>value pairs. + # + # params = CGI::parse("query_string") + # # {"name1" => ["value1", "value2", ...], + # # "name2" => ["value1", "value2", ...], ... } + # + def CGI::parse(query) + params = {} + query.split(/[&;]/).each do |pairs| + key, value = pairs.split('=',2).collect{|v| CGI::unescape(v) } + if key && value + params.has_key?(key) ? params[key].push(value) : params[key] = [value] + elsif key + params[key]=[] + end + end + params.default=[].freeze + params + end + + # Maximum content length of post data + ##MAX_CONTENT_LENGTH = 2 * 1024 * 1024 + + # Maximum content length of multipart data + MAX_MULTIPART_LENGTH = 128 * 1024 * 1024 + + # Maximum number of request parameters when multipart + MAX_MULTIPART_COUNT = 128 + + # Mixin module. It provides the follow functionality groups: + # + # 1. Access to CGI environment variables as methods. See + # documentation to the CGI class for a list of these variables. + # + # 2. Access to cookies, including the cookies attribute. + # + # 3. Access to parameters, including the params attribute, and overloading + # [] to perform parameter value lookup by key. + # + # 4. The initialize_query method, for initialising the above + # mechanisms, handling multipart forms, and allowing the + # class to be used in "offline" mode. + # + module QueryExtension + + %w[ CONTENT_LENGTH SERVER_PORT ].each do |env| + define_method(env.sub(/^HTTP_/, '').downcase) do + (val = env_table[env]) && Integer(val) + end + end + + %w[ AUTH_TYPE CONTENT_TYPE GATEWAY_INTERFACE PATH_INFO + PATH_TRANSLATED QUERY_STRING REMOTE_ADDR REMOTE_HOST + REMOTE_IDENT REMOTE_USER REQUEST_METHOD SCRIPT_NAME + SERVER_NAME SERVER_PROTOCOL SERVER_SOFTWARE + + HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING + HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM HTTP_HOST + HTTP_NEGOTIATE HTTP_PRAGMA HTTP_REFERER HTTP_USER_AGENT ].each do |env| + define_method(env.sub(/^HTTP_/, '').downcase) do + env_table[env] + end + end + + # Get the raw cookies as a string. + def raw_cookie + env_table["HTTP_COOKIE"] + end + + # Get the raw RFC2965 cookies as a string. + def raw_cookie2 + env_table["HTTP_COOKIE2"] + end + + # Get the cookies as a hash of cookie-name=>Cookie pairs. + attr_accessor :cookies + + # Get the parameters as a hash of name=>values pairs, where + # values is an Array. + attr_reader :params + + # Get the uploaed files as a hash of name=>values pairs + attr_reader :files + + # Set all the parameters. + def params=(hash) + @params.clear + @params.update(hash) + end + + def read_multipart(boundary, content_length) + ## read first boundary + stdin = $stdin + first_line = "--#{boundary}#{EOL}" + content_length -= first_line.bytesize + status = stdin.read(first_line.bytesize) + raise EOFError.new("no content body") unless status + raise EOFError.new("bad content body") unless first_line == status + ## parse and set params + params = {} + @files = {} + boundary_rexp = /--#{Regexp.quote(boundary)}(#{EOL}|--)/ + boundary_size = "#{EOL}--#{boundary}#{EOL}".bytesize + boundary_end = nil + buf = '' + bufsize = 10 * 1024 + max_count = MAX_MULTIPART_COUNT + n = 0 + while true + (n += 1) < max_count or raise StandardError.new("too many parameters.") + ## create body (StringIO or Tempfile) + body = create_body(bufsize < content_length) + class << body + alias local_path path + attr_reader :original_filename, :content_type + end + ## find head and boundary + head = nil + separator = EOL * 2 + until head && matched = boundary_rexp.match(buf) + if !head && pos = buf.index(separator) + len = pos + EOL.bytesize + head = buf[0, len] + buf = buf[(pos+separator.bytesize)..-1] + else + if head && buf.size > boundary_size + len = buf.size - boundary_size + body.print(buf[0, len]) + buf[0, len] = '' + end + c = stdin.read(bufsize < content_length ? bufsize : content_length) + raise EOFError.new("bad content body") if c.nil? || c.empty? + buf << c + content_length -= c.bytesize + end + end + ## read to end of boundary + m = matched + len = m.begin(0) + s = buf[0, len] + if s =~ /(\r?\n)\z/ + s = buf[0, len - $1.bytesize] + end + body.print(s) + buf = buf[m.end(0)..-1] + boundary_end = m[1] + content_length = -1 if boundary_end == '--' + ## reset file cursor position + body.rewind + ## original filename + /Content-Disposition:.* filename=(?:"(.*?)"|([^;\r\n]*))/i.match(head) + filename = $1 || $2 || '' + filename = CGI.unescape(filename) if unescape_filename?() + body.instance_variable_set('@original_filename', filename.taint) + ## content type + /Content-Type: (.*)/i.match(head) + (content_type = $1 || '').chomp! + body.instance_variable_set('@content_type', content_type.taint) + ## query parameter name + /Content-Disposition:.* name=(?:"(.*?)"|([^;\r\n]*))/i.match(head) + name = $1 || $2 || '' + if body.original_filename.empty? + value=body.read.dup.force_encoding(@accept_charset) + (params[name] ||= []) << value + unless value.valid_encoding? + if @accept_charset_error_block + @accept_charset_error_block.call(name,value) + else + raise InvalidEncoding,"Accept-Charset encoding error" + end + end + class << params[name].last;self;end.class_eval do + define_method(:read){self} + define_method(:original_filename){""} + define_method(:content_type){""} + end + else + (params[name] ||= []) << body + @files[name]=body + end + ## break loop + break if buf.size == 0 + break if content_length == -1 + end + raise EOFError, "bad boundary end of body part" unless boundary_end =~ /--/ + params.default = [] + params + end # read_multipart + private :read_multipart + def create_body(is_large) #:nodoc: + if is_large + require 'tempfile' + body = Tempfile.new('CGI', encoding: "ascii-8bit") + else + begin + require 'stringio' + body = StringIO.new("".force_encoding("ascii-8bit")) + rescue LoadError + require 'tempfile' + body = Tempfile.new('CGI', encoding: "ascii-8bit") + end + end + body.binmode if defined? body.binmode + return body + end + def unescape_filename? #:nodoc: + user_agent = $CGI_ENV['HTTP_USER_AGENT'] + return /Mac/i.match(user_agent) && /Mozilla/i.match(user_agent) && !/MSIE/i.match(user_agent) + end + + # offline mode. read name=value pairs on standard input. + def read_from_cmdline + require "shellwords" + + string = unless ARGV.empty? + ARGV.join(' ') + else + if STDIN.tty? + STDERR.print( + %|(offline mode: enter name=value pairs on standard input)\n| + ) + end + readlines.join(' ').gsub(/\n/, '') + end.gsub(/\\=/, '%3D').gsub(/\\&/, '%26') + + words = Shellwords.shellwords(string) + + if words.find{|x| /=/.match(x) } + words.join('&') + else + words.join('+') + end + end + private :read_from_cmdline + + # A wrapper class to use a StringIO object as the body and switch + # to a TempFile when the passed threshold is passed. + # Initialize the data from the query. + # + # Handles multipart forms (in particular, forms that involve file uploads). + # Reads query parameters in the @params field, and cookies into @cookies. + def initialize_query() + if ("POST" == env_table['REQUEST_METHOD']) and + %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|.match(env_table['CONTENT_TYPE']) + raise StandardError.new("too large multipart data.") if env_table['CONTENT_LENGTH'].to_i > MAX_MULTIPART_LENGTH + boundary = $1.dup + @multipart = true + @params = read_multipart(boundary, Integer(env_table['CONTENT_LENGTH'])) + else + @multipart = false + @params = CGI::parse( + case env_table['REQUEST_METHOD'] + when "GET", "HEAD" + if defined?(MOD_RUBY) + Apache::request.args or "" + else + env_table['QUERY_STRING'] or "" + end + when "POST" + stdinput.binmode if defined? stdinput.binmode + stdinput.read(Integer(env_table['CONTENT_LENGTH'])) or '' + else + read_from_cmdline + end.dup.force_encoding(@accept_charset) + ) + unless Encoding.find(@accept_charset) == Encoding::ASCII_8BIT + @params.each do |key,values| + values.each do |value| + unless value.valid_encoding? + if @accept_charset_error_block + @accept_charset_error_block.call(key,value) + else + raise InvalidEncoding,"Accept-Charset encoding error" + end + end + end + end + end + end + + @cookies = CGI::Cookie::parse((env_table['HTTP_COOKIE'] or env_table['COOKIE'])) + end + private :initialize_query + + def multipart? + @multipart + end + + # Get the value for the parameter with a given key. + # + # If the parameter has multiple values, only the first will be + # retrieved; use #params() to get the array of values. + def [](key) + params = @params[key] + return '' unless params + value = params[0] + if @multipart + if value + return value + elsif defined? StringIO + StringIO.new("".force_encoding("ascii-8bit")) + else + Tempfile.new("CGI",encoding:"ascii-8bit") + end + else + str = if value then value.dup else "" end + str + end + end + + # Return all parameter keys as an array. + def keys(*args) + @params.keys(*args) + end + + # Returns true if a given parameter key exists in the query. + def has_key?(*args) + @params.has_key?(*args) + end + alias key? has_key? + alias include? has_key? + + end # QueryExtension + + # InvalidEncoding Exception class + class InvalidEncoding < Exception; end + + # @@accept_charset is default accept character set. + # This default value default is "UTF-8" + # If you want to change the default accept character set + # when create a new CGI instance, set this: + # + # CGI.accept_charset = "EUC-JP" + # + + @@accept_charset="UTF-8" + + def self.accept_charset + @@accept_charset + end + + def self.accept_charset=(accept_charset) + @@accept_charset=accept_charset + end + + # Create a new CGI instance. + # + # CGI accept constructor parameters either in a hash, string as a block. + # But string is as same as using :tag_maker of hash. + # + # CGI.new("html3") #=> CGI.new(:tag_maker=>"html3") + # + # And, if you specify string, @accept_charset cannot be changed. + # Instead, please use hash parameter. + # + # == accept_charset + # + # :accept_charset specifies encoding of received query string. + # ( Default value is @@accept_charset. ) + # If not valid, raise CGI::InvalidEncoding + # + # Example. Suppose @@accept_charset # => "UTF-8" + # + # when not specified: + # + # cgi=CGI.new # @accept_charset # => "UTF-8" + # + # when specified "EUC-JP": + # + # cgi=CGI.new(:accept_charset => "EUC-JP") # => "EUC-JP" + # + # == block + # + # When you use a block, you can write a process + # that query encoding is invalid. Example: + # + # encoding_error={} + # cgi=CGI.new(:accept_charset=>"EUC-JP") do |name,value| + # encoding_error[key] = value + # end + # + # == tag_maker + # + # :tag_maker specifies which version of HTML to load the HTML generation + # methods for. The following versions of HTML are supported: + # + # html3:: HTML 3.x + # html4:: HTML 4.0 + # html4Tr:: HTML 4.0 Transitional + # html4Fr:: HTML 4.0 with Framesets + # + # If not specified, no HTML generation methods will be loaded. + # + # If the CGI object is not created in a standard CGI call environment + # (that is, it can't locate REQUEST_METHOD in its environment), then + # it will run in "offline" mode. In this mode, it reads its parameters + # from the command line or (failing that) from standard input. Otherwise, + # cookies and other parameters are parsed automatically from the standard + # CGI locations, which varies according to the REQUEST_METHOD. It works this: + # + # CGI.new(:tag_maker=>"html3") + # + # This will be obsolete: + # + # CGI.new("html3") + # + attr_reader :accept_charset + def initialize(options = {},&block) + @accept_charset_error_block=block if block_given? + @options={:accept_charset=>@@accept_charset} + case options + when Hash + @options.merge!(options) + when String + @options[:tag_maker]=options + end + @accept_charset=@options[:accept_charset] + if defined?(MOD_RUBY) && !ENV.key?("GATEWAY_INTERFACE") + Apache.request.setup_cgi_env + end + + extend QueryExtension + @multipart = false + + initialize_query() # set @params, @cookies + @output_cookies = nil + @output_hidden = nil + + case @options[:tag_maker] + when "html3" + require 'cgi/html' + extend Html3 + element_init() + extend HtmlExtension + when "html4" + require 'cgi/html' + extend Html4 + element_init() + extend HtmlExtension + when "html4Tr" + require 'cgi/html' + extend Html4Tr + element_init() + extend HtmlExtension + when "html4Fr" + require 'cgi/html' + extend Html4Tr + element_init() + extend Html4Fr + element_init() + extend HtmlExtension + end + end + +end # class CGI + + diff --git a/ruby/lib/cgi/html.rb b/ruby/lib/cgi/html.rb new file mode 100644 index 0000000..62f1fc1 --- /dev/null +++ b/ruby/lib/cgi/html.rb @@ -0,0 +1,1021 @@ + # Base module for HTML-generation mixins. + # + # Provides methods for code generation for tags following + # the various DTD element types. +class CGI + module TagMaker # :nodoc: + + # Generate code for an element with required start and end tags. + # + # - - + def nn_element_def(element) + nOE_element_def(element, <<-END) + if block_given? + yield.to_s + else + "" + end + + "</#{element.upcase}>" + END + end + + # Generate code for an empty element. + # + # - O EMPTY + def nOE_element_def(element, append = nil) + s = <<-END + attributes={attributes=>nil} if attributes.kind_of?(String) + "<#{element.upcase}" + attributes.collect{|name, value| + next unless value + " " + CGI::escapeHTML(name.to_s) + + if true == value + "" + else + '="' + CGI::escapeHTML(value.to_s) + '"' + end + }.join + ">" + END + s.sub!(/\Z/, " +") << append if append + s + end + + # Generate code for an element for which the end (and possibly the + # start) tag is optional. + # + # O O or - O + def nO_element_def(element) + nOE_element_def(element, <<-END) + if block_given? + yield.to_s + "</#{element.upcase}>" + else + "" + end + END + end + + end # TagMaker + + + # + # Mixin module providing HTML generation methods. + # + # For example, + # cgi.a("http://www.example.com") { "Example" } + # # => "<A HREF=\"http://www.example.com\">Example</A>" + # + # Modules Http3, Http4, etc., contain more basic HTML-generation methods + # (:title, :center, etc.). + # + # See class CGI for a detailed example. + # + module HtmlExtension + + + # Generate an Anchor element as a string. + # + # +href+ can either be a string, giving the URL + # for the HREF attribute, or it can be a hash of + # the element's attributes. + # + # The body of the element is the string returned by the no-argument + # block passed in. + # + # a("http://www.example.com") { "Example" } + # # => "<A HREF=\"http://www.example.com\">Example</A>" + # + # a("HREF" => "http://www.example.com", "TARGET" => "_top") { "Example" } + # # => "<A HREF=\"http://www.example.com\" TARGET=\"_top\">Example</A>" + # + def a(href = "") # :yield: + attributes = if href.kind_of?(String) + { "HREF" => href } + else + href + end + if block_given? + super(attributes){ yield } + else + super(attributes) + end + end + + # Generate a Document Base URI element as a String. + # + # +href+ can either by a string, giving the base URL for the HREF + # attribute, or it can be a has of the element's attributes. + # + # The passed-in no-argument block is ignored. + # + # base("http://www.example.com/cgi") + # # => "<BASE HREF=\"http://www.example.com/cgi\">" + def base(href = "") # :yield: + attributes = if href.kind_of?(String) + { "HREF" => href } + else + href + end + if block_given? + super(attributes){ yield } + else + super(attributes) + end + end + + # Generate a BlockQuote element as a string. + # + # +cite+ can either be a string, give the URI for the source of + # the quoted text, or a hash, giving all attributes of the element, + # or it can be omitted, in which case the element has no attributes. + # + # The body is provided by the passed-in no-argument block + # + # blockquote("http://www.example.com/quotes/foo.html") { "Foo!" } + # #=> "<BLOCKQUOTE CITE=\"http://www.example.com/quotes/foo.html\">Foo!</BLOCKQUOTE> + def blockquote(cite = {}) # :yield: + attributes = if cite.kind_of?(String) + { "CITE" => cite } + else + cite + end + if block_given? + super(attributes){ yield } + else + super(attributes) + end + end + + + # Generate a Table Caption element as a string. + # + # +align+ can be a string, giving the alignment of the caption + # (one of top, bottom, left, or right). It can be a hash of + # all the attributes of the element. Or it can be omitted. + # + # The body of the element is provided by the passed-in no-argument block. + # + # caption("left") { "Capital Cities" } + # # => <CAPTION ALIGN=\"left\">Capital Cities</CAPTION> + def caption(align = {}) # :yield: + attributes = if align.kind_of?(String) + { "ALIGN" => align } + else + align + end + if block_given? + super(attributes){ yield } + else + super(attributes) + end + end + + + # Generate a Checkbox Input element as a string. + # + # The attributes of the element can be specified as three arguments, + # +name+, +value+, and +checked+. +checked+ is a boolean value; + # if true, the CHECKED attribute will be included in the element. + # + # Alternatively, the attributes can be specified as a hash. + # + # checkbox("name") + # # = checkbox("NAME" => "name") + # + # checkbox("name", "value") + # # = checkbox("NAME" => "name", "VALUE" => "value") + # + # checkbox("name", "value", true) + # # = checkbox("NAME" => "name", "VALUE" => "value", "CHECKED" => true) + def checkbox(name = "", value = nil, checked = nil) + attributes = if name.kind_of?(String) + { "TYPE" => "checkbox", "NAME" => name, + "VALUE" => value, "CHECKED" => checked } + else + name["TYPE"] = "checkbox" + name + end + input(attributes) + end + + # Generate a sequence of checkbox elements, as a String. + # + # The checkboxes will all have the same +name+ attribute. + # Each checkbox is followed by a label. + # There will be one checkbox for each value. Each value + # can be specified as a String, which will be used both + # as the value of the VALUE attribute and as the label + # for that checkbox. A single-element array has the + # same effect. + # + # Each value can also be specified as a three-element array. + # The first element is the VALUE attribute; the second is the + # label; and the third is a boolean specifying whether this + # checkbox is CHECKED. + # + # Each value can also be specified as a two-element + # array, by omitting either the value element (defaults + # to the same as the label), or the boolean checked element + # (defaults to false). + # + # checkbox_group("name", "foo", "bar", "baz") + # # <INPUT TYPE="checkbox" NAME="name" VALUE="foo">foo + # # <INPUT TYPE="checkbox" NAME="name" VALUE="bar">bar + # # <INPUT TYPE="checkbox" NAME="name" VALUE="baz">baz + # + # checkbox_group("name", ["foo"], ["bar", true], "baz") + # # <INPUT TYPE="checkbox" NAME="name" VALUE="foo">foo + # # <INPUT TYPE="checkbox" CHECKED NAME="name" VALUE="bar">bar + # # <INPUT TYPE="checkbox" NAME="name" VALUE="baz">baz + # + # checkbox_group("name", ["1", "Foo"], ["2", "Bar", true], "Baz") + # # <INPUT TYPE="checkbox" NAME="name" VALUE="1">Foo + # # <INPUT TYPE="checkbox" CHECKED NAME="name" VALUE="2">Bar + # # <INPUT TYPE="checkbox" NAME="name" VALUE="Baz">Baz + # + # checkbox_group("NAME" => "name", + # "VALUES" => ["foo", "bar", "baz"]) + # + # checkbox_group("NAME" => "name", + # "VALUES" => [["foo"], ["bar", true], "baz"]) + # + # checkbox_group("NAME" => "name", + # "VALUES" => [["1", "Foo"], ["2", "Bar", true], "Baz"]) + def checkbox_group(name = "", *values) + if name.kind_of?(Hash) + values = name["VALUES"] + name = name["NAME"] + end + values.collect{|value| + if value.kind_of?(String) + checkbox(name, value) + value + else + if value[-1] == true || value[-1] == false + checkbox(name, value[0], value[-1]) + + value[-2] + else + checkbox(name, value[0]) + + value[-1] + end + end + }.join + end + + + # Generate an File Upload Input element as a string. + # + # The attributes of the element can be specified as three arguments, + # +name+, +size+, and +maxlength+. +maxlength+ is the maximum length + # of the file's _name_, not of the file's _contents_. + # + # Alternatively, the attributes can be specified as a hash. + # + # See #multipart_form() for forms that include file uploads. + # + # file_field("name") + # # <INPUT TYPE="file" NAME="name" SIZE="20"> + # + # file_field("name", 40) + # # <INPUT TYPE="file" NAME="name" SIZE="40"> + # + # file_field("name", 40, 100) + # # <INPUT TYPE="file" NAME="name" SIZE="40" MAXLENGTH="100"> + # + # file_field("NAME" => "name", "SIZE" => 40) + # # <INPUT TYPE="file" NAME="name" SIZE="40"> + def file_field(name = "", size = 20, maxlength = nil) + attributes = if name.kind_of?(String) + { "TYPE" => "file", "NAME" => name, + "SIZE" => size.to_s } + else + name["TYPE"] = "file" + name + end + attributes["MAXLENGTH"] = maxlength.to_s if maxlength + input(attributes) + end + + + # Generate a Form element as a string. + # + # +method+ should be either "get" or "post", and defaults to the latter. + # +action+ defaults to the current CGI script name. +enctype+ + # defaults to "application/x-www-form-urlencoded". + # + # Alternatively, the attributes can be specified as a hash. + # + # See also #multipart_form() for forms that include file uploads. + # + # form{ "string" } + # # <FORM METHOD="post" ENCTYPE="application/x-www-form-urlencoded">string</FORM> + # + # form("get") { "string" } + # # <FORM METHOD="get" ENCTYPE="application/x-www-form-urlencoded">string</FORM> + # + # form("get", "url") { "string" } + # # <FORM METHOD="get" ACTION="url" ENCTYPE="application/x-www-form-urlencoded">string</FORM> + # + # form("METHOD" => "post", "ENCTYPE" => "enctype") { "string" } + # # <FORM METHOD="post" ENCTYPE="enctype">string</FORM> + def form(method = "post", action = script_name, enctype = "application/x-www-form-urlencoded") + attributes = if method.kind_of?(String) + { "METHOD" => method, "ACTION" => action, + "ENCTYPE" => enctype } + else + unless method.has_key?("METHOD") + method["METHOD"] = "post" + end + unless method.has_key?("ENCTYPE") + method["ENCTYPE"] = enctype + end + method + end + if block_given? + body = yield + else + body = "" + end + if @output_hidden + body += @output_hidden.collect{|k,v| + "<INPUT TYPE=\"HIDDEN\" NAME=\"#{k}\" VALUE=\"#{v}\">" + }.join + end + super(attributes){body} + end + + # Generate a Hidden Input element as a string. + # + # The attributes of the element can be specified as two arguments, + # +name+ and +value+. + # + # Alternatively, the attributes can be specified as a hash. + # + # hidden("name") + # # <INPUT TYPE="hidden" NAME="name"> + # + # hidden("name", "value") + # # <INPUT TYPE="hidden" NAME="name" VALUE="value"> + # + # hidden("NAME" => "name", "VALUE" => "reset", "ID" => "foo") + # # <INPUT TYPE="hidden" NAME="name" VALUE="value" ID="foo"> + def hidden(name = "", value = nil) + attributes = if name.kind_of?(String) + { "TYPE" => "hidden", "NAME" => name, "VALUE" => value } + else + name["TYPE"] = "hidden" + name + end + input(attributes) + end + + # Generate a top-level HTML element as a string. + # + # The attributes of the element are specified as a hash. The + # pseudo-attribute "PRETTY" can be used to specify that the generated + # HTML string should be indented. "PRETTY" can also be specified as + # a string as the sole argument to this method. The pseudo-attribute + # "DOCTYPE", if given, is used as the leading DOCTYPE SGML tag; it + # should include the entire text of this tag, including angle brackets. + # + # The body of the html element is supplied as a block. + # + # html{ "string" } + # # <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"><HTML>string</HTML> + # + # html("LANG" => "ja") { "string" } + # # <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"><HTML LANG="ja">string</HTML> + # + # html("DOCTYPE" => false) { "string" } + # # <HTML>string</HTML> + # + # html("DOCTYPE" => '<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">') { "string" } + # # <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN"><HTML>string</HTML> + # + # html("PRETTY" => " ") { "<BODY></BODY>" } + # # <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> + # # <HTML> + # # <BODY> + # # </BODY> + # # </HTML> + # + # html("PRETTY" => "\t") { "<BODY></BODY>" } + # # <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> + # # <HTML> + # # <BODY> + # # </BODY> + # # </HTML> + # + # html("PRETTY") { "<BODY></BODY>" } + # # = html("PRETTY" => " ") { "<BODY></BODY>" } + # + # html(if $VERBOSE then "PRETTY" end) { "HTML string" } + # + def html(attributes = {}) # :yield: + if nil == attributes + attributes = {} + elsif "PRETTY" == attributes + attributes = { "PRETTY" => true } + end + pretty = attributes.delete("PRETTY") + pretty = " " if true == pretty + buf = "" + + if attributes.has_key?("DOCTYPE") + if attributes["DOCTYPE"] + buf += attributes.delete("DOCTYPE") + else + attributes.delete("DOCTYPE") + end + else + buf += doctype + end + + if block_given? + buf += super(attributes){ yield } + else + buf += super(attributes) + end + + if pretty + CGI::pretty(buf, pretty) + else + buf + end + + end + + # Generate an Image Button Input element as a string. + # + # +src+ is the URL of the image to use for the button. +name+ + # is the input name. +alt+ is the alternative text for the image. + # + # Alternatively, the attributes can be specified as a hash. + # + # image_button("url") + # # <INPUT TYPE="image" SRC="url"> + # + # image_button("url", "name", "string") + # # <INPUT TYPE="image" SRC="url" NAME="name" ALT="string"> + # + # image_button("SRC" => "url", "ATL" => "strng") + # # <INPUT TYPE="image" SRC="url" ALT="string"> + def image_button(src = "", name = nil, alt = nil) + attributes = if src.kind_of?(String) + { "TYPE" => "image", "SRC" => src, "NAME" => name, + "ALT" => alt } + else + src["TYPE"] = "image" + src["SRC"] ||= "" + src + end + input(attributes) + end + + + # Generate an Image element as a string. + # + # +src+ is the URL of the image. +alt+ is the alternative text for + # the image. +width+ is the width of the image, and +height+ is + # its height. + # + # Alternatively, the attributes can be specified as a hash. + # + # img("src", "alt", 100, 50) + # # <IMG SRC="src" ALT="alt" WIDTH="100" HEIGHT="50"> + # + # img("SRC" => "src", "ALT" => "alt", "WIDTH" => 100, "HEIGHT" => 50) + # # <IMG SRC="src" ALT="alt" WIDTH="100" HEIGHT="50"> + def img(src = "", alt = "", width = nil, height = nil) + attributes = if src.kind_of?(String) + { "SRC" => src, "ALT" => alt } + else + src + end + attributes["WIDTH"] = width.to_s if width + attributes["HEIGHT"] = height.to_s if height + super(attributes) + end + + + # Generate a Form element with multipart encoding as a String. + # + # Multipart encoding is used for forms that include file uploads. + # + # +action+ is the action to perform. +enctype+ is the encoding + # type, which defaults to "multipart/form-data". + # + # Alternatively, the attributes can be specified as a hash. + # + # multipart_form{ "string" } + # # <FORM METHOD="post" ENCTYPE="multipart/form-data">string</FORM> + # + # multipart_form("url") { "string" } + # # <FORM METHOD="post" ACTION="url" ENCTYPE="multipart/form-data">string</FORM> + def multipart_form(action = nil, enctype = "multipart/form-data") + attributes = if action == nil + { "METHOD" => "post", "ENCTYPE" => enctype } + elsif action.kind_of?(String) + { "METHOD" => "post", "ACTION" => action, + "ENCTYPE" => enctype } + else + unless action.has_key?("METHOD") + action["METHOD"] = "post" + end + unless action.has_key?("ENCTYPE") + action["ENCTYPE"] = enctype + end + action + end + if block_given? + form(attributes){ yield } + else + form(attributes) + end + end + + + # Generate a Password Input element as a string. + # + # +name+ is the name of the input field. +value+ is its default + # value. +size+ is the size of the input field display. +maxlength+ + # is the maximum length of the inputted password. + # + # Alternatively, attributes can be specified as a hash. + # + # password_field("name") + # # <INPUT TYPE="password" NAME="name" SIZE="40"> + # + # password_field("name", "value") + # # <INPUT TYPE="password" NAME="name" VALUE="value" SIZE="40"> + # + # password_field("password", "value", 80, 200) + # # <INPUT TYPE="password" NAME="name" VALUE="value" SIZE="80" MAXLENGTH="200"> + # + # password_field("NAME" => "name", "VALUE" => "value") + # # <INPUT TYPE="password" NAME="name" VALUE="value"> + def password_field(name = "", value = nil, size = 40, maxlength = nil) + attributes = if name.kind_of?(String) + { "TYPE" => "password", "NAME" => name, + "VALUE" => value, "SIZE" => size.to_s } + else + name["TYPE"] = "password" + name + end + attributes["MAXLENGTH"] = maxlength.to_s if maxlength + input(attributes) + end + + # Generate a Select element as a string. + # + # +name+ is the name of the element. The +values+ are the options that + # can be selected from the Select menu. Each value can be a String or + # a one, two, or three-element Array. If a String or a one-element + # Array, this is both the value of that option and the text displayed for + # it. If a three-element Array, the elements are the option value, displayed + # text, and a boolean value specifying whether this option starts as selected. + # The two-element version omits either the option value (defaults to the same + # as the display text) or the boolean selected specifier (defaults to false). + # + # The attributes and options can also be specified as a hash. In this + # case, options are specified as an array of values as described above, + # with the hash key of "VALUES". + # + # popup_menu("name", "foo", "bar", "baz") + # # <SELECT NAME="name"> + # # <OPTION VALUE="foo">foo</OPTION> + # # <OPTION VALUE="bar">bar</OPTION> + # # <OPTION VALUE="baz">baz</OPTION> + # # </SELECT> + # + # popup_menu("name", ["foo"], ["bar", true], "baz") + # # <SELECT NAME="name"> + # # <OPTION VALUE="foo">foo</OPTION> + # # <OPTION VALUE="bar" SELECTED>bar</OPTION> + # # <OPTION VALUE="baz">baz</OPTION> + # # </SELECT> + # + # popup_menu("name", ["1", "Foo"], ["2", "Bar", true], "Baz") + # # <SELECT NAME="name"> + # # <OPTION VALUE="1">Foo</OPTION> + # # <OPTION SELECTED VALUE="2">Bar</OPTION> + # # <OPTION VALUE="Baz">Baz</OPTION> + # # </SELECT> + # + # popup_menu("NAME" => "name", "SIZE" => 2, "MULTIPLE" => true, + # "VALUES" => [["1", "Foo"], ["2", "Bar", true], "Baz"]) + # # <SELECT NAME="name" MULTIPLE SIZE="2"> + # # <OPTION VALUE="1">Foo</OPTION> + # # <OPTION SELECTED VALUE="2">Bar</OPTION> + # # <OPTION VALUE="Baz">Baz</OPTION> + # # </SELECT> + def popup_menu(name = "", *values) + + if name.kind_of?(Hash) + values = name["VALUES"] + size = name["SIZE"].to_s if name["SIZE"] + multiple = name["MULTIPLE"] + name = name["NAME"] + else + size = nil + multiple = nil + end + + select({ "NAME" => name, "SIZE" => size, + "MULTIPLE" => multiple }){ + values.collect{|value| + if value.kind_of?(String) + option({ "VALUE" => value }){ value } + else + if value[value.size - 1] == true + option({ "VALUE" => value[0], "SELECTED" => true }){ + value[value.size - 2] + } + else + option({ "VALUE" => value[0] }){ + value[value.size - 1] + } + end + end + }.join + } + + end + + # Generates a radio-button Input element. + # + # +name+ is the name of the input field. +value+ is the value of + # the field if checked. +checked+ specifies whether the field + # starts off checked. + # + # Alternatively, the attributes can be specified as a hash. + # + # radio_button("name", "value") + # # <INPUT TYPE="radio" NAME="name" VALUE="value"> + # + # radio_button("name", "value", true) + # # <INPUT TYPE="radio" NAME="name" VALUE="value" CHECKED> + # + # radio_button("NAME" => "name", "VALUE" => "value", "ID" => "foo") + # # <INPUT TYPE="radio" NAME="name" VALUE="value" ID="foo"> + def radio_button(name = "", value = nil, checked = nil) + attributes = if name.kind_of?(String) + { "TYPE" => "radio", "NAME" => name, + "VALUE" => value, "CHECKED" => checked } + else + name["TYPE"] = "radio" + name + end + input(attributes) + end + + # Generate a sequence of radio button Input elements, as a String. + # + # This works the same as #checkbox_group(). However, it is not valid + # to have more than one radiobutton in a group checked. + # + # radio_group("name", "foo", "bar", "baz") + # # <INPUT TYPE="radio" NAME="name" VALUE="foo">foo + # # <INPUT TYPE="radio" NAME="name" VALUE="bar">bar + # # <INPUT TYPE="radio" NAME="name" VALUE="baz">baz + # + # radio_group("name", ["foo"], ["bar", true], "baz") + # # <INPUT TYPE="radio" NAME="name" VALUE="foo">foo + # # <INPUT TYPE="radio" CHECKED NAME="name" VALUE="bar">bar + # # <INPUT TYPE="radio" NAME="name" VALUE="baz">baz + # + # radio_group("name", ["1", "Foo"], ["2", "Bar", true], "Baz") + # # <INPUT TYPE="radio" NAME="name" VALUE="1">Foo + # # <INPUT TYPE="radio" CHECKED NAME="name" VALUE="2">Bar + # # <INPUT TYPE="radio" NAME="name" VALUE="Baz">Baz + # + # radio_group("NAME" => "name", + # "VALUES" => ["foo", "bar", "baz"]) + # + # radio_group("NAME" => "name", + # "VALUES" => [["foo"], ["bar", true], "baz"]) + # + # radio_group("NAME" => "name", + # "VALUES" => [["1", "Foo"], ["2", "Bar", true], "Baz"]) + def radio_group(name = "", *values) + if name.kind_of?(Hash) + values = name["VALUES"] + name = name["NAME"] + end + values.collect{|value| + if value.kind_of?(String) + radio_button(name, value) + value + else + if value[-1] == true || value[-1] == false + radio_button(name, value[0], value[-1]) + + value[-2] + else + radio_button(name, value[0]) + + value[-1] + end + end + }.join + end + + # Generate a reset button Input element, as a String. + # + # This resets the values on a form to their initial values. +value+ + # is the text displayed on the button. +name+ is the name of this button. + # + # Alternatively, the attributes can be specified as a hash. + # + # reset + # # <INPUT TYPE="reset"> + # + # reset("reset") + # # <INPUT TYPE="reset" VALUE="reset"> + # + # reset("VALUE" => "reset", "ID" => "foo") + # # <INPUT TYPE="reset" VALUE="reset" ID="foo"> + def reset(value = nil, name = nil) + attributes = if (not value) or value.kind_of?(String) + { "TYPE" => "reset", "VALUE" => value, "NAME" => name } + else + value["TYPE"] = "reset" + value + end + input(attributes) + end + + alias scrolling_list popup_menu + + # Generate a submit button Input element, as a String. + # + # +value+ is the text to display on the button. +name+ is the name + # of the input. + # + # Alternatively, the attributes can be specified as a hash. + # + # submit + # # <INPUT TYPE="submit"> + # + # submit("ok") + # # <INPUT TYPE="submit" VALUE="ok"> + # + # submit("ok", "button1") + # # <INPUT TYPE="submit" VALUE="ok" NAME="button1"> + # + # submit("VALUE" => "ok", "NAME" => "button1", "ID" => "foo") + # # <INPUT TYPE="submit" VALUE="ok" NAME="button1" ID="foo"> + def submit(value = nil, name = nil) + attributes = if (not value) or value.kind_of?(String) + { "TYPE" => "submit", "VALUE" => value, "NAME" => name } + else + value["TYPE"] = "submit" + value + end + input(attributes) + end + + # Generate a text field Input element, as a String. + # + # +name+ is the name of the input field. +value+ is its initial + # value. +size+ is the size of the input area. +maxlength+ + # is the maximum length of input accepted. + # + # Alternatively, the attributes can be specified as a hash. + # + # text_field("name") + # # <INPUT TYPE="text" NAME="name" SIZE="40"> + # + # text_field("name", "value") + # # <INPUT TYPE="text" NAME="name" VALUE="value" SIZE="40"> + # + # text_field("name", "value", 80) + # # <INPUT TYPE="text" NAME="name" VALUE="value" SIZE="80"> + # + # text_field("name", "value", 80, 200) + # # <INPUT TYPE="text" NAME="name" VALUE="value" SIZE="80" MAXLENGTH="200"> + # + # text_field("NAME" => "name", "VALUE" => "value") + # # <INPUT TYPE="text" NAME="name" VALUE="value"> + def text_field(name = "", value = nil, size = 40, maxlength = nil) + attributes = if name.kind_of?(String) + { "TYPE" => "text", "NAME" => name, "VALUE" => value, + "SIZE" => size.to_s } + else + name["TYPE"] = "text" + name + end + attributes["MAXLENGTH"] = maxlength.to_s if maxlength + input(attributes) + end + + # Generate a TextArea element, as a String. + # + # +name+ is the name of the textarea. +cols+ is the number of + # columns and +rows+ is the number of rows in the display. + # + # Alternatively, the attributes can be specified as a hash. + # + # The body is provided by the passed-in no-argument block + # + # textarea("name") + # # = textarea("NAME" => "name", "COLS" => 70, "ROWS" => 10) + # + # textarea("name", 40, 5) + # # = textarea("NAME" => "name", "COLS" => 40, "ROWS" => 5) + def textarea(name = "", cols = 70, rows = 10) # :yield: + attributes = if name.kind_of?(String) + { "NAME" => name, "COLS" => cols.to_s, + "ROWS" => rows.to_s } + else + name + end + if block_given? + super(attributes){ yield } + else + super(attributes) + end + end + + end # HtmlExtension + + + # Mixin module for HTML version 3 generation methods. + module Html3 # :nodoc: + + # The DOCTYPE declaration for this version of HTML + def doctype + %|<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">| + end + + # Initialise the HTML generation methods for this version. + def element_init + extend TagMaker + methods = "" + # - - + for element in %w[ A TT I B U STRIKE BIG SMALL SUB SUP EM STRONG + DFN CODE SAMP KBD VAR CITE FONT ADDRESS DIV center MAP + APPLET PRE XMP LISTING DL OL UL DIR MENU SELECT table TITLE + STYLE SCRIPT H1 H2 H3 H4 H5 H6 TEXTAREA FORM BLOCKQUOTE + CAPTION ] + methods += <<-BEGIN + nn_element_def(element) + <<-END + def #{element.downcase}(attributes = {}) + BEGIN + end + END + end + + # - O EMPTY + for element in %w[ IMG BASE BASEFONT BR AREA LINK PARAM HR INPUT + ISINDEX META ] + methods += <<-BEGIN + nOE_element_def(element) + <<-END + def #{element.downcase}(attributes = {}) + BEGIN + end + END + end + + # O O or - O + for element in %w[ HTML HEAD BODY P PLAINTEXT DT DD LI OPTION tr + th td ] + methods += <<-BEGIN + nO_element_def(element) + <<-END + def #{element.downcase}(attributes = {}) + BEGIN + end + END + end + eval(methods) + end + + end # Html3 + + + # Mixin module for HTML version 4 generation methods. + module Html4 # :nodoc: + + # The DOCTYPE declaration for this version of HTML + def doctype + %|<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">| + end + + # Initialise the HTML generation methods for this version. + def element_init + extend TagMaker + methods = "" + # - - + for element in %w[ TT I B BIG SMALL EM STRONG DFN CODE SAMP KBD + VAR CITE ABBR ACRONYM SUB SUP SPAN BDO ADDRESS DIV MAP OBJECT + H1 H2 H3 H4 H5 H6 PRE Q INS DEL DL OL UL LABEL SELECT OPTGROUP + FIELDSET LEGEND BUTTON TABLE TITLE STYLE SCRIPT NOSCRIPT + TEXTAREA FORM A BLOCKQUOTE CAPTION ] + methods += <<-BEGIN + nn_element_def(element) + <<-END + def #{element.downcase}(attributes = {}) + BEGIN + end + END + end + + # - O EMPTY + for element in %w[ IMG BASE BR AREA LINK PARAM HR INPUT COL META ] + methods += <<-BEGIN + nOE_element_def(element) + <<-END + def #{element.downcase}(attributes = {}) + BEGIN + end + END + end + + # O O or - O + for element in %w[ HTML BODY P DT DD LI OPTION THEAD TFOOT TBODY + COLGROUP TR TH TD HEAD] + methods += <<-BEGIN + nO_element_def(element) + <<-END + def #{element.downcase}(attributes = {}) + BEGIN + end + END + end + eval(methods) + end + + end # Html4 + + + # Mixin module for HTML version 4 transitional generation methods. + module Html4Tr # :nodoc: + + # The DOCTYPE declaration for this version of HTML + def doctype + %|<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">| + end + + # Initialise the HTML generation methods for this version. + def element_init + extend TagMaker + methods = "" + # - - + for element in %w[ TT I B U S STRIKE BIG SMALL EM STRONG DFN + CODE SAMP KBD VAR CITE ABBR ACRONYM FONT SUB SUP SPAN BDO + ADDRESS DIV CENTER MAP OBJECT APPLET H1 H2 H3 H4 H5 H6 PRE Q + INS DEL DL OL UL DIR MENU LABEL SELECT OPTGROUP FIELDSET + LEGEND BUTTON TABLE IFRAME NOFRAMES TITLE STYLE SCRIPT + NOSCRIPT TEXTAREA FORM A BLOCKQUOTE CAPTION ] + methods += <<-BEGIN + nn_element_def(element) + <<-END + def #{element.downcase}(attributes = {}) + BEGIN + end + END + end + + # - O EMPTY + for element in %w[ IMG BASE BASEFONT BR AREA LINK PARAM HR INPUT + COL ISINDEX META ] + methods += <<-BEGIN + nOE_element_def(element) + <<-END + def #{element.downcase}(attributes = {}) + BEGIN + end + END + end + + # O O or - O + for element in %w[ HTML BODY P DT DD LI OPTION THEAD TFOOT TBODY + COLGROUP TR TH TD HEAD ] + methods += <<-BEGIN + nO_element_def(element) + <<-END + def #{element.downcase}(attributes = {}) + BEGIN + end + END + end + eval(methods) + end + + end # Html4Tr + + + # Mixin module for generating HTML version 4 with framesets. + module Html4Fr # :nodoc: + + # The DOCTYPE declaration for this version of HTML + def doctype + %|<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">| + end + + # Initialise the HTML generation methods for this version. + def element_init + methods = "" + # - - + for element in %w[ FRAMESET ] + methods += <<-BEGIN + nn_element_def(element) + <<-END + def #{element.downcase}(attributes = {}) + BEGIN + end + END + end + + # - O EMPTY + for element in %w[ FRAME ] + methods += <<-BEGIN + nOE_element_def(element) + <<-END + def #{element.downcase}(attributes = {}) + BEGIN + end + END + end + eval(methods) + end + + end # Html4Fr +end + + diff --git a/ruby/lib/cgi/session.rb b/ruby/lib/cgi/session.rb new file mode 100644 index 0000000..2b5aa84 --- /dev/null +++ b/ruby/lib/cgi/session.rb @@ -0,0 +1,537 @@ +# +# cgi/session.rb - session support for cgi scripts +# +# Copyright (C) 2001 Yukihiro "Matz" Matsumoto +# Copyright (C) 2000 Network Applied Communication Laboratory, Inc. +# Copyright (C) 2000 Information-technology Promotion Agency, Japan +# +# Author: Yukihiro "Matz" Matsumoto +# +# Documentation: William Webber (william@williamwebber.com) +# +# == Overview +# +# This file provides the +CGI::Session+ class, which provides session +# support for CGI scripts. A session is a sequence of HTTP requests +# and responses linked together and associated with a single client. +# Information associated with the session is stored +# on the server between requests. A session id is passed between client +# and server with every request and response, transparently +# to the user. This adds state information to the otherwise stateless +# HTTP request/response protocol. +# +# See the documentation to the +CGI::Session+ class for more details +# and examples of usage. See cgi.rb for the +CGI+ class itself. + +require 'cgi' +require 'tmpdir' + +class CGI + + # Class representing an HTTP session. See documentation for the file + # cgi/session.rb for an introduction to HTTP sessions. + # + # == Lifecycle + # + # A CGI::Session instance is created from a CGI object. By default, + # this CGI::Session instance will start a new session if none currently + # exists, or continue the current session for this client if one does + # exist. The +new_session+ option can be used to either always or + # never create a new session. See #new() for more details. + # + # #delete() deletes a session from session storage. It + # does not however remove the session id from the client. If the client + # makes another request with the same id, the effect will be to start + # a new session with the old session's id. + # + # == Setting and retrieving session data. + # + # The Session class associates data with a session as key-value pairs. + # This data can be set and retrieved by indexing the Session instance + # using '[]', much the same as hashes (although other hash methods + # are not supported). + # + # When session processing has been completed for a request, the + # session should be closed using the close() method. This will + # store the session's state to persistent storage. If you want + # to store the session's state to persistent storage without + # finishing session processing for this request, call the update() + # method. + # + # == Storing session state + # + # The caller can specify what form of storage to use for the session's + # data with the +database_manager+ option to CGI::Session::new. The + # following storage classes are provided as part of the standard library: + # + # CGI::Session::FileStore:: stores data as plain text in a flat file. Only + # works with String data. This is the default + # storage type. + # CGI::Session::MemoryStore:: stores data in an in-memory hash. The data + # only persists for as long as the current ruby + # interpreter instance does. + # CGI::Session::PStore:: stores data in Marshalled format. Provided by + # cgi/session/pstore.rb. Supports data of any type, + # and provides file-locking and transaction support. + # + # Custom storage types can also be created by defining a class with + # the following methods: + # + # new(session, options) + # restore # returns hash of session data. + # update + # close + # delete + # + # Changing storage type mid-session does not work. Note in particular + # that by default the FileStore and PStore session data files have the + # same name. If your application switches from one to the other without + # making sure that filenames will be different + # and clients still have old sessions lying around in cookies, then + # things will break nastily! + # + # == Maintaining the session id. + # + # Most session state is maintained on the server. However, a session + # id must be passed backwards and forwards between client and server + # to maintain a reference to this session state. + # + # The simplest way to do this is via cookies. The CGI::Session class + # provides transparent support for session id communication via cookies + # if the client has cookies enabled. + # + # If the client has cookies disabled, the session id must be included + # as a parameter of all requests sent by the client to the server. The + # CGI::Session class in conjunction with the CGI class will transparently + # add the session id as a hidden input field to all forms generated + # using the CGI#form() HTML generation method. No built-in support is + # provided for other mechanisms, such as URL re-writing. The caller is + # responsible for extracting the session id from the session_id + # attribute and manually encoding it in URLs and adding it as a hidden + # input to HTML forms created by other mechanisms. Also, session expiry + # is not automatically handled. + # + # == Examples of use + # + # === Setting the user's name + # + # require 'cgi' + # require 'cgi/session' + # require 'cgi/session/pstore' # provides CGI::Session::PStore + # + # cgi = CGI.new("html4") + # + # session = CGI::Session.new(cgi, + # 'database_manager' => CGI::Session::PStore, # use PStore + # 'session_key' => '_rb_sess_id', # custom session key + # 'session_expires' => Time.now + 30 * 60, # 30 minute timeout + # 'prefix' => 'pstore_sid_') # PStore option + # if cgi.has_key?('user_name') and cgi['user_name'] != '' + # # coerce to String: cgi[] returns the + # # string-like CGI::QueryExtension::Value + # session['user_name'] = cgi['user_name'].to_s + # elsif !session['user_name'] + # session['user_name'] = "guest" + # end + # session.close + # + # === Creating a new session safely + # + # require 'cgi' + # require 'cgi/session' + # + # cgi = CGI.new("html4") + # + # # We make sure to delete an old session if one exists, + # # not just to free resources, but to prevent the session + # # from being maliciously hijacked later on. + # begin + # session = CGI::Session.new(cgi, 'new_session' => false) + # session.delete + # rescue ArgumentError # if no old session + # end + # session = CGI::Session.new(cgi, 'new_session' => true) + # session.close + # + class Session + + class NoSession < RuntimeError #:nodoc: + end + + # The id of this session. + attr_reader :session_id, :new_session + + def Session::callback(dbman) #:nodoc: + Proc.new{ + dbman[0].close unless dbman.empty? + } + end + + # Create a new session id. + # + # The session id is an MD5 hash based upon the time, + # a random number, and a constant string. This routine + # is used internally for automatically generated + # session ids. + def create_new_id + require 'securerandom' + begin + session_id = SecureRandom.hex(16) + rescue NotImplementedError + require 'digest/md5' + md5 = Digest::MD5::new + now = Time::now + md5.update(now.to_s) + md5.update(String(now.usec)) + md5.update(String(rand(0))) + md5.update(String($$)) + md5.update('foobar') + session_id = md5.hexdigest + end + session_id + end + private :create_new_id + + # Create a new CGI::Session object for +request+. + # + # +request+ is an instance of the +CGI+ class (see cgi.rb). + # +option+ is a hash of options for initialising this + # CGI::Session instance. The following options are + # recognised: + # + # session_key:: the parameter name used for the session id. + # Defaults to '_session_id'. + # session_id:: the session id to use. If not provided, then + # it is retrieved from the +session_key+ parameter + # of the request, or automatically generated for + # a new session. + # new_session:: if true, force creation of a new session. If not set, + # a new session is only created if none currently + # exists. If false, a new session is never created, + # and if none currently exists and the +session_id+ + # option is not set, an ArgumentError is raised. + # database_manager:: the name of the class providing storage facilities + # for session state persistence. Built-in support + # is provided for +FileStore+ (the default), + # +MemoryStore+, and +PStore+ (from + # cgi/session/pstore.rb). See the documentation for + # these classes for more details. + # + # The following options are also recognised, but only apply if the + # session id is stored in a cookie. + # + # session_expires:: the time the current session expires, as a + # +Time+ object. If not set, the session will terminate + # when the user's browser is closed. + # session_domain:: the hostname domain for which this session is valid. + # If not set, defaults to the hostname of the server. + # session_secure:: if +true+, this session will only work over HTTPS. + # session_path:: the path for which this session applies. Defaults + # to the directory of the CGI script. + # + # +option+ is also passed on to the session storage class initializer; see + # the documentation for each session storage class for the options + # they support. + # + # The retrieved or created session is automatically added to +request+ + # as a cookie, and also to its +output_hidden+ table, which is used + # to add hidden input elements to forms. + # + # *WARNING* the +output_hidden+ + # fields are surrounded by a <fieldset> tag in HTML 4 generation, which + # is _not_ invisible on many browsers; you may wish to disable the + # use of fieldsets with code similar to the following + # (see http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-list/37805) + # + # cgi = CGI.new("html4") + # class << cgi + # undef_method :fieldset + # end + # + def initialize(request, option={}) + @new_session = false + session_key = option['session_key'] || '_session_id' + session_id = option['session_id'] + unless session_id + if option['new_session'] + session_id = create_new_id + @new_session = true + end + end + unless session_id + if request.key?(session_key) + session_id = request[session_key] + session_id = session_id.read if session_id.respond_to?(:read) + end + unless session_id + session_id, = request.cookies[session_key] + end + unless session_id + unless option.fetch('new_session', true) + raise ArgumentError, "session_key `%s' should be supplied"%session_key + end + session_id = create_new_id + @new_session = true + end + end + @session_id = session_id + dbman = option['database_manager'] || FileStore + begin + @dbman = dbman::new(self, option) + rescue NoSession + unless option.fetch('new_session', true) + raise ArgumentError, "invalid session_id `%s'"%session_id + end + session_id = @session_id = create_new_id unless session_id + @new_session=true + retry + end + request.instance_eval do + @output_hidden = {session_key => session_id} unless option['no_hidden'] + @output_cookies = [ + Cookie::new("name" => session_key, + "value" => session_id, + "expires" => option['session_expires'], + "domain" => option['session_domain'], + "secure" => option['session_secure'], + "path" => + if option['session_path'] + option['session_path'] + elsif ENV["SCRIPT_NAME"] + File::dirname(ENV["SCRIPT_NAME"]) + else + "" + end) + ] unless option['no_cookies'] + end + @dbprot = [@dbman] + ObjectSpace::define_finalizer(self, Session::callback(@dbprot)) + end + + # Retrieve the session data for key +key+. + def [](key) + @data ||= @dbman.restore + @data[key] + end + + # Set the session date for key +key+. + def []=(key, val) + @write_lock ||= true + @data ||= @dbman.restore + @data[key] = val + end + + # Store session data on the server. For some session storage types, + # this is a no-op. + def update + @dbman.update + end + + # Store session data on the server and close the session storage. + # For some session storage types, this is a no-op. + def close + @dbman.close + @dbprot.clear + end + + # Delete the session from storage. Also closes the storage. + # + # Note that the session's data is _not_ automatically deleted + # upon the session expiring. + def delete + @dbman.delete + @dbprot.clear + end + + # File-based session storage class. + # + # Implements session storage as a flat file of 'key=value' values. + # This storage type only works directly with String values; the + # user is responsible for converting other types to Strings when + # storing and from Strings when retrieving. + class FileStore + # Create a new FileStore instance. + # + # This constructor is used internally by CGI::Session. The + # user does not generally need to call it directly. + # + # +session+ is the session for which this instance is being + # created. The session id must only contain alphanumeric + # characters; automatically generated session ids observe + # this requirement. + # + # +option+ is a hash of options for the initializer. The + # following options are recognised: + # + # tmpdir:: the directory to use for storing the FileStore + # file. Defaults to Dir::tmpdir (generally "/tmp" + # on Unix systems). + # prefix:: the prefix to add to the session id when generating + # the filename for this session's FileStore file. + # Defaults to "cgi_sid_". + # suffix:: the prefix to add to the session id when generating + # the filename for this session's FileStore file. + # Defaults to the empty string. + # + # This session's FileStore file will be created if it does + # not exist, or opened if it does. + def initialize(session, option={}) + dir = option['tmpdir'] || Dir::tmpdir + prefix = option['prefix'] || 'cgi_sid_' + suffix = option['suffix'] || '' + id = session.session_id + require 'digest/md5' + md5 = Digest::MD5.hexdigest(id)[0,16] + @path = dir+"/"+prefix+md5+suffix + if File::exist? @path + @hash = nil + else + unless session.new_session + raise CGI::Session::NoSession, "uninitialized session" + end + @hash = {} + end + end + + # Restore session state from the session's FileStore file. + # + # Returns the session state as a hash. + def restore + unless @hash + @hash = {} + begin + lockf = File.open(@path+".lock", "r") + lockf.flock File::LOCK_SH + f = File.open(@path, 'r') + for line in f + line.chomp! + k, v = line.split('=',2) + @hash[CGI::unescape(k)] = Marshal.restore(CGI::unescape(v)) + end + ensure + f.close unless f.nil? + lockf.close if lockf + end + end + @hash + end + + # Save session state to the session's FileStore file. + def update + return unless @hash + begin + lockf = File.open(@path+".lock", File::CREAT|File::RDWR, 0600) + lockf.flock File::LOCK_EX + f = File.open(@path+".new", File::CREAT|File::TRUNC|File::WRONLY, 0600) + for k,v in @hash + f.printf "%s=%s\n", CGI::escape(k), CGI::escape(String(Marshal.dump(v))) + end + f.close + File.rename @path+".new", @path + ensure + f.close if f and !f.closed? + lockf.close if lockf + end + end + + # Update and close the session's FileStore file. + def close + update + end + + # Close and delete the session's FileStore file. + def delete + File::unlink @path+".lock" rescue nil + File::unlink @path+".new" rescue nil + File::unlink @path rescue Errno::ENOENT + end + end + + # In-memory session storage class. + # + # Implements session storage as a global in-memory hash. Session + # data will only persist for as long as the ruby interpreter + # instance does. + class MemoryStore + GLOBAL_HASH_TABLE = {} #:nodoc: + + # Create a new MemoryStore instance. + # + # +session+ is the session this instance is associated with. + # +option+ is a list of initialisation options. None are + # currently recognised. + def initialize(session, option=nil) + @session_id = session.session_id + unless GLOBAL_HASH_TABLE.key?(@session_id) + unless session.new_session + raise CGI::Session::NoSession, "uninitialized session" + end + GLOBAL_HASH_TABLE[@session_id] = {} + end + end + + # Restore session state. + # + # Returns session data as a hash. + def restore + GLOBAL_HASH_TABLE[@session_id] + end + + # Update session state. + # + # A no-op. + def update + # don't need to update; hash is shared + end + + # Close session storage. + # + # A no-op. + def close + # don't need to close + end + + # Delete the session state. + def delete + GLOBAL_HASH_TABLE.delete(@session_id) + end + end + + # Dummy session storage class. + # + # Implements session storage place holder. No actual storage + # will be done. + class NullStore + # Create a new NullStore instance. + # + # +session+ is the session this instance is associated with. + # +option+ is a list of initialisation options. None are + # currently recognised. + def initialize(session, option=nil) + end + + # Restore (empty) session state. + def restore + {} + end + + # Update session state. + # + # A no-op. + def update + end + + # Close session storage. + # + # A no-op. + def close + end + + # Delete the session state. + # + # A no-op. + def delete + end + end + end +end diff --git a/ruby/lib/cgi/session/pstore.rb b/ruby/lib/cgi/session/pstore.rb new file mode 100644 index 0000000..3cd3e46 --- /dev/null +++ b/ruby/lib/cgi/session/pstore.rb @@ -0,0 +1,111 @@ +# +# cgi/session/pstore.rb - persistent storage of marshalled session data +# +# Documentation: William Webber (william@williamwebber.com) +# +# == Overview +# +# This file provides the CGI::Session::PStore class, which builds +# persistent of session data on top of the pstore library. See +# cgi/session.rb for more details on session storage managers. + +require 'cgi/session' +require 'pstore' + +class CGI + class Session + # PStore-based session storage class. + # + # This builds upon the top-level PStore class provided by the + # library file pstore.rb. Session data is marshalled and stored + # in a file. File locking and transaction services are provided. + class PStore + # Create a new CGI::Session::PStore instance + # + # This constructor is used internally by CGI::Session. The + # user does not generally need to call it directly. + # + # +session+ is the session for which this instance is being + # created. The session id must only contain alphanumeric + # characters; automatically generated session ids observe + # this requirement. + # + # +option+ is a hash of options for the initializer. The + # following options are recognised: + # + # tmpdir:: the directory to use for storing the PStore + # file. Defaults to Dir::tmpdir (generally "/tmp" + # on Unix systems). + # prefix:: the prefix to add to the session id when generating + # the filename for this session's PStore file. + # Defaults to the empty string. + # + # This session's PStore file will be created if it does + # not exist, or opened if it does. + def initialize(session, option={}) + dir = option['tmpdir'] || Dir::tmpdir + prefix = option['prefix'] || '' + id = session.session_id + require 'digest/md5' + md5 = Digest::MD5.hexdigest(id)[0,16] + path = dir+"/"+prefix+md5 + path.untaint + if File::exist?(path) + @hash = nil + else + unless session.new_session + raise CGI::Session::NoSession, "uninitialized session" + end + @hash = {} + end + @p = ::PStore.new(path) + @p.transaction do |p| + File.chmod(0600, p.path) + end + end + + # Restore session state from the session's PStore file. + # + # Returns the session state as a hash. + def restore + unless @hash + @p.transaction do + @hash = @p['hash'] || {} + end + end + @hash + end + + # Save session state to the session's PStore file. + def update + @p.transaction do + @p['hash'] = @hash + end + end + + # Update and close the session's PStore file. + def close + update + end + + # Close and delete the session's PStore file. + def delete + path = @p.path + File::unlink path + end + + end + end +end + +if $0 == __FILE__ + # :enddoc: + STDIN.reopen("/dev/null") + cgi = CGI.new + session = CGI::Session.new(cgi, 'database_manager' => CGI::Session::PStore) + session['key'] = {'k' => 'v'} + puts session['key'].class + fail unless Hash === session['key'] + puts session['key'].inspect + fail unless session['key'].inspect == '{"k"=>"v"}' +end diff --git a/ruby/lib/cgi/util.rb b/ruby/lib/cgi/util.rb new file mode 100644 index 0000000..991b68c --- /dev/null +++ b/ruby/lib/cgi/util.rb @@ -0,0 +1,181 @@ +class CGI + # URL-encode a string. + # url_encoded_string = CGI::escape("'Stop!' said Fred") + # # => "%27Stop%21%27+said+Fred" + def CGI::escape(string) + string.gsub(/([^ a-zA-Z0-9_.-]+)/) do + '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase + end.tr(' ', '+') + end + + + # URL-decode a string. + # string = CGI::unescape("%27Stop%21%27+said+Fred") + # # => "'Stop!' said Fred" + def CGI::unescape(string) + enc = string.encoding + string.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/) do + [$1.delete('%')].pack('H*').force_encoding(enc) + end + end + + TABLE_FOR_ESCAPE_HTML__ = { + '&' => '&', + '"' => '"', + '<' => '<', + '>' => '>', + } + + # Escape special characters in HTML, namely &\"<> + # CGI::escapeHTML('Usage: foo "bar" <baz>') + # # => "Usage: foo "bar" <baz>" + def CGI::escapeHTML(string) + string.gsub(/[&\"<>]/, TABLE_FOR_ESCAPE_HTML__) + end + + + # Unescape a string that has been HTML-escaped + # CGI::unescapeHTML("Usage: foo "bar" <baz>") + # # => "Usage: foo \"bar\" <baz>" + def CGI::unescapeHTML(string) + enc = string.encoding + if [Encoding::UTF_16BE, Encoding::UTF_16LE, Encoding::UTF_32BE, Encoding::UTF_32LE].include?(enc) + return string.gsub(Regexp.new('&(amp|quot|gt|lt|#[0-9]+|#x[0-9A-Fa-f]+);'.encode(enc))) do + case $1.encode("US-ASCII") + when 'amp' then '&'.encode(enc) + when 'quot' then '"'.encode(enc) + when 'gt' then '>'.encode(enc) + when 'lt' then '<'.encode(enc) + when /\A#0*(\d+)\z/ then $1.to_i.chr(enc) + when /\A#x([0-9a-f]+)\z/i then $1.hex.chr(enc) + end + end + end + asciicompat = Encoding.compatible?(string, "a") + string.gsub(/&(amp|quot|gt|lt|\#[0-9]+|\#x[0-9A-Fa-f]+);/) do + match = $1.dup + case match + when 'amp' then '&' + when 'quot' then '"' + when 'gt' then '>' + when 'lt' then '<' + when /\A#0*(\d+)\z/ + n = $1.to_i + if enc == Encoding::UTF_8 or + enc == Encoding::ISO_8859_1 && n < 256 or + asciicompat && n < 128 + n.chr(enc) + else + "&##{$1};" + end + when /\A#x([0-9a-f]+)\z/i + n = $1.hex + if enc == Encoding::UTF_8 or + enc == Encoding::ISO_8859_1 && n < 256 or + asciicompat && n < 128 + n.chr(enc) + else + "&#x#{$1};" + end + else + "&#{match};" + end + end + end + def CGI::escape_html(str) + escapeHTML(str) + end + def CGI::unescape_html(str) + unescapeHTML(str) + end + + # Escape only the tags of certain HTML elements in +string+. + # + # Takes an element or elements or array of elements. Each element + # is specified by the name of the element, without angle brackets. + # This matches both the start and the end tag of that element. + # The attribute list of the open tag will also be escaped (for + # instance, the double-quotes surrounding attribute values). + # + # print CGI::escapeElement('<BR><A HREF="url"></A>', "A", "IMG") + # # "<BR><A HREF="url"></A>" + # + # print CGI::escapeElement('<BR><A HREF="url"></A>', ["A", "IMG"]) + # # "<BR><A HREF="url"></A>" + def CGI::escapeElement(string, *elements) + elements = elements[0] if elements[0].kind_of?(Array) + unless elements.empty? + string.gsub(/<\/?(?:#{elements.join("|")})(?!\w)(?:.|\n)*?>/i) do + CGI::escapeHTML($&) + end + else + string + end + end + + + # Undo escaping such as that done by CGI::escapeElement() + # + # print CGI::unescapeElement( + # CGI::escapeHTML('<BR><A HREF="url"></A>'), "A", "IMG") + # # "<BR><A HREF="url"></A>" + # + # print CGI::unescapeElement( + # CGI::escapeHTML('<BR><A HREF="url"></A>'), ["A", "IMG"]) + # # "<BR><A HREF="url"></A>" + def CGI::unescapeElement(string, *elements) + elements = elements[0] if elements[0].kind_of?(Array) + unless elements.empty? + string.gsub(/<\/?(?:#{elements.join("|")})(?!\w)(?:.|\n)*?>/i) do + CGI::unescapeHTML($&) + end + else + string + end + end + def CGI::escape_element(str) + escapeElement(str) + end + def CGI::unescape_element(str) + unescapeElement(str) + end + + # Format a +Time+ object as a String using the format specified by RFC 1123. + # + # CGI::rfc1123_date(Time.now) + # # Sat, 01 Jan 2000 00:00:00 GMT + def CGI::rfc1123_date(time) + t = time.clone.gmtime + return format("%s, %.2d %s %.4d %.2d:%.2d:%.2d GMT", + RFC822_DAYS[t.wday], t.day, RFC822_MONTHS[t.month-1], t.year, + t.hour, t.min, t.sec) + end + + # Prettify (indent) an HTML string. + # + # +string+ is the HTML string to indent. +shift+ is the indentation + # unit to use; it defaults to two spaces. + # + # print CGI::pretty("<HTML><BODY></BODY></HTML>") + # # <HTML> + # # <BODY> + # # </BODY> + # # </HTML> + # + # print CGI::pretty("<HTML><BODY></BODY></HTML>", "\t") + # # <HTML> + # # <BODY> + # # </BODY> + # # </HTML> + # + def CGI::pretty(string, shift = " ") + lines = string.gsub(/(?!\A)<(?:.|\n)*?>/, "\n\\0").gsub(/<(?:.|\n)*?>(?!\n)/, "\\0\n") + end_pos = 0 + while end_pos = lines.index(/^<\/(\w+)/, end_pos) + element = $1.dup + start_pos = lines.rindex(/^\s*<#{element}/i, end_pos) + lines[start_pos ... end_pos] = "__" + lines[start_pos ... end_pos].gsub(/\n(?!\z)/, "\n" + shift) + "__" + end + lines.gsub(/^((?:#{Regexp::quote(shift)})*)__(?=<\/?\w)/, '\1') + end +end |