diff options
Diffstat (limited to 'lib/cgi')
| -rw-r--r-- | lib/cgi/cgi.gemspec | 42 | ||||
| -rw-r--r-- | lib/cgi/cookie.rb | 174 | ||||
| -rw-r--r-- | lib/cgi/core.rb | 219 | ||||
| -rw-r--r-- | lib/cgi/html.rb | 303 | ||||
| -rw-r--r-- | lib/cgi/session.rb | 107 | ||||
| -rw-r--r-- | lib/cgi/session/pstore.rb | 33 | ||||
| -rw-r--r-- | lib/cgi/util.rb | 202 |
7 files changed, 622 insertions, 458 deletions
diff --git a/lib/cgi/cgi.gemspec b/lib/cgi/cgi.gemspec new file mode 100644 index 0000000000..381c55a5ca --- /dev/null +++ b/lib/cgi/cgi.gemspec @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +name = File.basename(__FILE__, ".gemspec") +version = ["lib", Array.new(name.count("-")+1, "..").join("/")].find do |dir| + break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line| + /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1 + end rescue nil +end + +Gem::Specification.new do |spec| + spec.name = name + spec.version = version + spec.authors = ["Yukihiro Matsumoto"] + spec.email = ["matz@ruby-lang.org"] + + spec.summary = %q{Support for the Common Gateway Interface protocol.} + spec.description = %q{Support for the Common Gateway Interface protocol.} + spec.homepage = "https://github.com/ruby/cgi" + spec.licenses = ["Ruby", "BSD-2-Clause"] + spec.required_ruby_version = ">= 2.5.0" + + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = spec.homepage + + spec.executables = [] + + spec.files = [ + "LICENSE.txt", + "README.md", + *Dir["lib{.rb,/**/*.rb}", "bin/*"] ] + + spec.require_paths = ["lib"] + + if Gem::Platform === spec.platform and spec.platform =~ 'java' or RUBY_ENGINE == 'jruby' + spec.platform = 'java' + spec.require_paths << "ext/java/org/jruby/ext/cgi/escape/lib" + spec.files += Dir["ext/java/**/*.{rb}", "lib/cgi/escape.jar"] + else + spec.files += Dir["ext/cgi/**/*.{rb,c,h,sh}", "ext/cgi/escape/depend", "lib/cgi/escape.so"] + spec.extensions = ["ext/cgi/escape/extconf.rb"] + end +end diff --git a/lib/cgi/cookie.rb b/lib/cgi/cookie.rb index c2526931d5..1c4ef6a600 100644 --- a/lib/cgi/cookie.rb +++ b/lib/cgi/cookie.rb @@ -1,5 +1,6 @@ +# frozen_string_literal: true +require_relative 'util' class CGI - @@accept_charset="UTF-8" unless defined?(@@accept_charset) # Class representing an HTTP cookie. # # In addition to its specific fields and methods, a Cookie instance @@ -8,32 +9,40 @@ class CGI # 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 + # 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 + # 'httponly' => 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 + # name = cookie1.name + # values = cookie1.value + # path = cookie1.path + # domain = cookie1.domain + # expires = cookie1.expires + # secure = cookie1.secure + # httponly = cookie1.httponly # - # cookie1.name = 'name' - # cookie1.value = ['value1', 'value2', ...] - # cookie1.path = 'path' - # cookie1.domain = 'domain' - # cookie1.expires = Time.now + 30 - # cookie1.secure = true + # cookie1.name = 'name' + # cookie1.value = ['value1', 'value2', ...] + # cookie1.path = 'path' + # cookie1.domain = 'domain' + # cookie1.expires = Time.now + 30 + # cookie1.secure = true + # cookie1.httponly = true class Cookie < Array + @@accept_charset="UTF-8" unless defined?(@@accept_charset) + + TOKEN_RE = %r"\A[[!-~]&&[^()<>@,;:\\\"/?=\[\]{}]]+\z" + PATH_VALUE_RE = %r"\A[[ -~]&&[^;]]*\z" + DOMAIN_VALUE_RE = %r"\A\.?(?<label>(?!-)[-A-Za-z0-9]+(?<!-))(?:\.\g<label>)*\z" # Create a new CGI::Cookie object. # @@ -52,23 +61,25 @@ class CGI # # 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 + # path:: the path for which this cookie applies. Defaults to # the value of the +SCRIPT_NAME+ environment variable. # 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. + # httponly:: whether this cookie is a HttpOnly cookie or not (default to + # false). HttpOnly cookies are not available to javascript. # # These keywords correspond to attributes of the cookie object. def initialize(name = "", *value) @domain = nil @expires = nil if name.kind_of?(String) - @name = name - %r|^(.*/)|.match(ENV["SCRIPT_NAME"]) - @path = ($1 or "") + self.name = name + self.path = (%r|\A(.*/)| =~ ENV["SCRIPT_NAME"] ? $1 : "") @secure = false + @httponly = false return super(value) end @@ -77,32 +88,54 @@ class CGI raise ArgumentError, "`name' required" end - @name = options["name"] + self.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"] + self.path = options["path"] || (%r|\A(.*/)| =~ ENV["SCRIPT_NAME"] ? $1 : "") + self.domain = options["domain"] @expires = options["expires"] - @secure = options["secure"] == true ? true : false + @secure = options["secure"] == true + @httponly = options["httponly"] == true super(value) end # Name of this cookie, as a +String+ - attr_accessor :name + attr_reader :name + # Set name of this cookie + def name=(str) + if str and !TOKEN_RE.match?(str) + raise ArgumentError, "invalid name: #{str.dump}" + end + @name = str + end + # Path for which this cookie applies, as a +String+ - attr_accessor :path + attr_reader :path + # Set path for which this cookie applies + def path=(str) + if str and !PATH_VALUE_RE.match?(str) + raise ArgumentError, "invalid path: #{str.dump}" + end + @path = str + end + # Domain for which this cookie applies, as a +String+ - attr_accessor :domain + attr_reader :domain + # Set domain for which this cookie applies + def domain=(str) + if str and ((str = str.b).bytesize > 255 or !DOMAIN_VALUE_RE.match?(str)) + raise ArgumentError, "invalid domain: #{str.dump}" + end + @domain = str + end + # Time at which this cookie expires, as a +Time+ attr_accessor :expires # True if this cookie is secure; false otherwise - attr_reader("secure") + attr_reader :secure + # True if this cookie is httponly; false otherwise + attr_reader :httponly # Returns the value or list of values for this cookie. def value @@ -122,43 +155,56 @@ class CGI @secure end + # Set whether the Cookie is a httponly cookie or not. + # + # +val+ must be a boolean. + def httponly=(val) + @httponly = !!val + end + # Convert the Cookie to its string representation. def to_s - val = collect{|v| CGI::escape(v) }.join("&") - buf = "#{@name}=#{val}" + val = collect{|v| CGI.escape(v) }.join("&") + buf = "#{@name}=#{val}".dup buf << "; domain=#{@domain}" if @domain buf << "; path=#{@path}" if @path - buf << "; expires=#{CGI::rfc1123_date(@expires)}" if @expires - buf << "; secure" if @secure == true + buf << "; expires=#{CGI.rfc1123_date(@expires)}" if @expires + buf << "; secure" if @secure + buf << "; HttpOnly" if @httponly 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 self.parse(raw_cookie) + cookies = Hash.new([]) + return cookies unless raw_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,@@accept_charset) } - if cookies.has_key?(name) - values = cookies[name].value + values + raw_cookie.split(/;\s?/).each do |pairs| + name, values = pairs.split('=',2) + next unless name and values + values ||= "" + values = values.split('&').collect{|v| CGI.unescape(v,@@accept_charset) } + if cookies.has_key?(name) + cookies[name].concat(values) + else + cookies[name] = Cookie.new(name, *values) + end end - cookies[name] = Cookie::new(name, *values) + + cookies end - cookies - end + # A summary of cookie string. + def inspect + "#<CGI::Cookie: #{self.to_s.inspect}>" + end + + end # class Cookie end diff --git a/lib/cgi/core.rb b/lib/cgi/core.rb index f1e8d3467a..62e606837a 100644 --- a/lib/cgi/core.rb +++ b/lib/cgi/core.rb @@ -1,8 +1,16 @@ +# frozen_string_literal: true #-- # Methods for generating HTML, parsing CGI-related parameters, and # generating HTTP responses. #++ class CGI + unless const_defined?(:Util) + module Util + @@accept_charset = "UTF-8" # :nodoc: + end + include Util + extend Util + end $CGI_ENV = ENV # for FCGI support @@ -67,8 +75,8 @@ class CGI # Create an HTTP header block as a string. # # :call-seq: - # header(content_type_string="text/html") - # header(headers_hash) + # http_header(content_type_string="text/html") + # http_header(headers_hash) # # Includes the empty line that ends the header block. # @@ -127,29 +135,29 @@ class CGI # # Examples: # - # header + # http_header # # Content-Type: text/html # - # header("text/plain") + # http_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") + # http_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") # # This method does not perform charset conversion. - def header(options='text/html') + def http_header(options='text/html') if options.is_a?(String) content_type = options buf = _header_for_string(content_type) @@ -170,26 +178,45 @@ class CGI buf << EOL # empty line of separator return buf end - end # header() + end # http_header() + + # This method is an alias for #http_header, when HTML5 tag maker is inactive. + # + # NOTE: use #http_header to create HTTP header blocks, this alias is only + # provided for backwards compatibility. + # + # Using #header with the HTML5 tag maker will create a <header> element. + alias :header :http_header + + def _no_crlf_check(str) + if str + str = str.to_s + raise "A HTTP status or header field must not include CR and LF" if str =~ /[\r\n]/ + str + else + nil + end + end + private :_no_crlf_check def _header_for_string(content_type) #:nodoc: - buf = '' + buf = ''.dup if nph?() - buf << "#{$CGI_ENV['SERVER_PROTOCOL'] || 'HTTP/1.0'} 200 OK#{EOL}" + buf << "#{_no_crlf_check($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 << "Server: #{_no_crlf_check($CGI_ENV['SERVER_SOFTWARE'])}#{EOL}" buf << "Connection: close#{EOL}" end - buf << "Content-Type: #{content_type}#{EOL}" + buf << "Content-Type: #{_no_crlf_check(content_type)}#{EOL}" if @output_cookies - @output_cookies.each {|cookie| buf << "Set-Cookie: #{cookie}#{EOL}" } + @output_cookies.each {|cookie| buf << "Set-Cookie: #{_no_crlf_check(cookie)}#{EOL}" } end return buf end # _header_for_string private :_header_for_string def _header_for_hash(options) #:nodoc: - buf = '' + buf = ''.dup ## add charset to option['type'] options['type'] ||= 'text/html' charset = options.delete('charset') @@ -197,9 +224,9 @@ class CGI ## NPH options.delete('nph') if defined?(MOD_RUBY) if options.delete('nph') || nph?() - protocol = $CGI_ENV['SERVER_PROTOCOL'] || 'HTTP/1.0' + protocol = _no_crlf_check($CGI_ENV['SERVER_PROTOCOL']) || 'HTTP/1.0' status = options.delete('status') - status = HTTP_STATUS[status] || status || '200 OK' + status = HTTP_STATUS[status] || _no_crlf_check(status) || '200 OK' buf << "#{protocol} #{status}#{EOL}" buf << "Date: #{CGI.rfc1123_date(Time.now)}#{EOL}" options['server'] ||= $CGI_ENV['SERVER_SOFTWARE'] || '' @@ -207,51 +234,51 @@ class CGI end ## common headers status = options.delete('status') - buf << "Status: #{HTTP_STATUS[status] || status}#{EOL}" if status + buf << "Status: #{HTTP_STATUS[status] || _no_crlf_check(status)}#{EOL}" if status server = options.delete('server') - buf << "Server: #{server}#{EOL}" if server + buf << "Server: #{_no_crlf_check(server)}#{EOL}" if server connection = options.delete('connection') - buf << "Connection: #{connection}#{EOL}" if connection + buf << "Connection: #{_no_crlf_check(connection)}#{EOL}" if connection type = options.delete('type') - buf << "Content-Type: #{type}#{EOL}" #if type + buf << "Content-Type: #{_no_crlf_check(type)}#{EOL}" #if type length = options.delete('length') - buf << "Content-Length: #{length}#{EOL}" if length + buf << "Content-Length: #{_no_crlf_check(length)}#{EOL}" if length language = options.delete('language') - buf << "Content-Language: #{language}#{EOL}" if language + buf << "Content-Language: #{_no_crlf_check(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}" + buf << "Set-Cookie: #{_no_crlf_check(cookie)}#{EOL}" when Array arr = cookie - arr.each {|c| buf << "Set-Cookie: #{c}#{EOL}" } + arr.each {|c| buf << "Set-Cookie: #{_no_crlf_check(c)}#{EOL}" } when Hash hash = cookie - hash.each {|name, c| buf << "Set-Cookie: #{c}#{EOL}" } + hash.each_value {|c| buf << "Set-Cookie: #{_no_crlf_check(c)}#{EOL}" } end end if @output_cookies - @output_cookies.each {|c| buf << "Set-Cookie: #{c}#{EOL}" } + @output_cookies.each {|c| buf << "Set-Cookie: #{_no_crlf_check(c)}#{EOL}" } end ## other headers options.each do |key, value| - buf << "#{key}: #{value}#{EOL}" + buf << "#{_no_crlf_check(key)}: #{_no_crlf_check(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 + return /IIS\/(\d+)/ =~ $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 + $stderr.printf("name:%s value:%s\n", name, value) if $DEBUG case name when 'Set-Cookie' request.headers_out.add(name, value) @@ -283,7 +310,7 @@ class CGI # +content_type_string+:: # If a string is passed, it is assumed to be the content type. # +headers_hash+:: - # This is a Hash of headers, similar to that used by #header. + # This is a Hash of headers, similar to that used by #http_header. # +block+:: # A block is required and should evaluate to the body of the response. # @@ -344,7 +371,7 @@ class CGI options["length"] = content.bytesize.to_s output = stdoutput output.binmode if defined? output.binmode - output.print header(options) + output.print http_header(options) output.print content unless "HEAD" == env_table['REQUEST_METHOD'] end @@ -359,14 +386,14 @@ class CGI # Parse an HTTP query string into a hash of key=>value pairs. # - # params = CGI::parse("query_string") + # params = CGI.parse("query_string") # # {"name1" => ["value1", "value2", ...], # # "name2" => ["value1", "value2", ...], ... } # - def CGI::parse(query) + def self.parse(query) params = {} query.split(/[&;]/).each do |pairs| - key, value = pairs.split('=',2).collect{|v| CGI::unescape(v) } + key, value = pairs.split('=',2).collect{|v| CGI.unescape(v) } next unless key @@ -381,9 +408,6 @@ class CGI # 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 @@ -408,7 +432,7 @@ class CGI module QueryExtension %w[ CONTENT_LENGTH SERVER_PORT ].each do |env| - define_method(env.sub(/^HTTP_/, '').downcase) do + define_method(env.delete_prefix('HTTP_').downcase) do (val = env_table[env]) && Integer(val) end end @@ -421,7 +445,7 @@ class CGI 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 + define_method(env.delete_prefix('HTTP_').downcase) do env_table[env] end end @@ -474,15 +498,16 @@ class CGI @files = {} boundary_rexp = /--#{Regexp.quote(boundary)}(#{EOL}|--)/ boundary_size = "#{EOL}--#{boundary}#{EOL}".bytesize - boundary_end = nil - buf = '' + buf = ''.dup bufsize = 10 * 1024 max_count = MAX_MULTIPART_COUNT n = 0 + tempfiles = [] while true (n += 1) < max_count or raise StandardError.new("too many parameters.") ## create body (StringIO or Tempfile) body = create_body(bufsize < content_length) + tempfiles << body if defined?(Tempfile) && body.kind_of?(Tempfile) class << body if method_defined?(:path) alias local_path path @@ -528,18 +553,19 @@ class CGI body.rewind ## original filename /Content-Disposition:.* filename=(?:"(.*?)"|([^;\r\n]*))/i.match(head) - filename = $1 || $2 || '' + filename = $1 || $2 || ''.dup filename = CGI.unescape(filename) if unescape_filename?() - body.instance_variable_set('@original_filename', filename.taint) + body.instance_variable_set(:@original_filename, filename) ## content type /Content-Type: (.*)/i.match(head) - (content_type = $1 || '').chomp! - body.instance_variable_set('@content_type', content_type.taint) + (content_type = $1 || ''.dup).chomp! + body.instance_variable_set(:@content_type, content_type) ## 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) + body.close! if defined?(Tempfile) && body.kind_of?(Tempfile) (params[name] ||= []) << value unless value.valid_encoding? if @accept_charset_error_block @@ -563,19 +589,28 @@ class CGI raise EOFError, "bad boundary end of body part" unless boundary_end =~ /--/ params.default = [] params + rescue Exception + if tempfiles + tempfiles.each {|t| + if t.path + t.close! + end + } + end + raise end # read_multipart private :read_multipart def create_body(is_large) #:nodoc: if is_large require 'tempfile' - body = Tempfile.new('CGI', encoding: "ascii-8bit") + body = Tempfile.new('CGI', encoding: Encoding::ASCII_8BIT) else begin require 'stringio' - body = StringIO.new("".force_encoding("ascii-8bit")) + body = StringIO.new("".b) rescue LoadError require 'tempfile' - body = Tempfile.new('CGI', encoding: "ascii-8bit") + body = Tempfile.new('CGI', encoding: Encoding::ASCII_8BIT) end end body.binmode if defined? body.binmode @@ -583,6 +618,7 @@ class CGI end def unescape_filename? #:nodoc: user_agent = $CGI_ENV['HTTP_USER_AGENT'] + return false unless user_agent return /Mac/i.match(user_agent) && /Mozilla/i.match(user_agent) && !/MSIE/i.match(user_agent) end @@ -624,14 +660,15 @@ class CGI # 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 + %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?| =~ env_table['CONTENT_TYPE'] + current_max_multipart_length = @max_multipart_length.respond_to?(:call) ? @max_multipart_length.call : @max_multipart_length + raise StandardError.new("too large multipart data.") if env_table['CONTENT_LENGTH'].to_i > current_max_multipart_length boundary = $1.dup @multipart = true @params = read_multipart(boundary, Integer(env_table['CONTENT_LENGTH'])) else @multipart = false - @params = CGI::parse( + @params = CGI.parse( case env_table['REQUEST_METHOD'] when "GET", "HEAD" if defined?(MOD_RUBY) @@ -661,7 +698,7 @@ class CGI end end - @cookies = CGI::Cookie::parse((env_table['HTTP_COOKIE'] or env_table['COOKIE'])) + @cookies = CGI::Cookie.parse((env_table['HTTP_COOKIE'] or env_table['COOKIE'])) end private :initialize_query @@ -682,9 +719,9 @@ class CGI if value return value elsif defined? StringIO - StringIO.new("".force_encoding("ascii-8bit")) + StringIO.new("".b) else - Tempfile.new("CGI",encoding:"ascii-8bit") + Tempfile.new("CGI",encoding: Encoding::ASCII_8BIT) end else str = if value then value.dup else "" end @@ -716,7 +753,7 @@ class CGI # # CGI.accept_charset = "EUC-JP" # - @@accept_charset="UTF-8" + @@accept_charset="UTF-8" if false # needed for rdoc? # Return the accept character set for all new CGI instances. def self.accept_charset @@ -731,6 +768,16 @@ class CGI # Return the accept character set for this CGI instance. attr_reader :accept_charset + # @@max_multipart_length is the maximum length of multipart data. + # The default value is 128 * 1024 * 1024 bytes + # + # The default can be set to something else in the CGI constructor, + # via the :max_multipart_length key in the option hash. + # + # See CGI.new documentation. + # + @@max_multipart_length= 128 * 1024 * 1024 + # Create a new CGI instance. # # :call-seq: @@ -744,7 +791,7 @@ class CGI # +options_hash+ form, since it also allows you specify the charset you # will accept. # <tt>options_hash</tt>:: - # A Hash that recognizes two options: + # A Hash that recognizes three options: # # <tt>:accept_charset</tt>:: # specifies encoding of received query string. If omitted, @@ -773,6 +820,18 @@ class CGI # "html4Fr":: HTML 4.0 with Framesets # "html5":: HTML 5 # + # <tt>:max_multipart_length</tt>:: + # Specifies maximum length of multipart data. Can be an Integer scalar or + # a lambda, that will be evaluated when the request is parsed. This + # allows more complex logic to be set when determining whether to accept + # multipart data (e.g. consult a registered users upload allowance) + # + # Default is 128 * 1024 * 1024 bytes + # + # cgi=CGI.new(:max_multipart_length => 268435456) # simple scalar + # + # cgi=CGI.new(:max_multipart_length => -> {check_filesystem}) # lambda + # # <tt>block</tt>:: # If provided, the block is called when an invalid encoding is # encountered. For example: @@ -790,7 +849,10 @@ class CGI # CGI locations, which varies according to the REQUEST_METHOD. def initialize(options = {}, &block) # :yields: name, value @accept_charset_error_block = block_given? ? block : nil - @options={:accept_charset=>@@accept_charset} + @options={ + :accept_charset=>@@accept_charset, + :max_multipart_length=>@@max_multipart_length + } case options when Hash @options.merge!(options) @@ -798,6 +860,7 @@ class CGI @options[:tag_maker]=options end @accept_charset=@options[:accept_charset] + @max_multipart_length=@options[:max_multipart_length] if defined?(MOD_RUBY) && !ENV.key?("GATEWAY_INTERFACE") Apache.request.setup_cgi_env end @@ -811,35 +874,27 @@ class CGI case @options[:tag_maker] when "html3" - require 'cgi/html' + require_relative 'html' extend Html3 - element_init() extend HtmlExtension when "html4" - require 'cgi/html' + require_relative 'html' extend Html4 - element_init() extend HtmlExtension when "html4Tr" - require 'cgi/html' + require_relative 'html' extend Html4Tr - element_init() extend HtmlExtension when "html4Fr" - require 'cgi/html' + require_relative 'html' extend Html4Tr - element_init() extend Html4Fr - element_init() extend HtmlExtension when "html5" - require 'cgi/html' + require_relative 'html' extend Html5 - element_init() extend HtmlExtension end end end # class CGI - - diff --git a/lib/cgi/html.rb b/lib/cgi/html.rb index 3054279b54..1543943320 100644 --- a/lib/cgi/html.rb +++ b/lib/cgi/html.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class CGI # Base module for HTML-generation mixins. # @@ -8,55 +9,62 @@ class CGI # 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 + def nn_element(element, attributes = {}) + s = nOE_element(element, attributes) + if block_given? + s << yield.to_s + end + s << "</#{element.upcase}>" + end + + def nn_element_def(attributes = {}, &block) + nn_element(__callee__, attributes, &block) 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 + def nOE_element(element, attributes = {}) + attributes={attributes=>nil} if attributes.kind_of?(String) + s = "<#{element.upcase}".dup + attributes.each do|name, value| + next unless value + s << " " + s << CGI.escapeHTML(name.to_s) + if value != true + s << '="' + s << CGI.escapeHTML(value.to_s) + s << '"' + end + end + s << ">" + end + + def nOE_element_def(attributes = {}, &block) + nOE_element(__callee__, attributes, &block) 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 + def nO_element(element, attributes = {}) + s = nOE_element(element, attributes) + if block_given? + s << yield.to_s + s << "</#{element.upcase}>" + end + s + end + + def nO_element_def(attributes = {}, &block) + nO_element(__callee__, attributes, &block) end end # TagMaker - # # Mixin module providing HTML generation methods. # # For example, @@ -92,11 +100,7 @@ class CGI else href end - if block_given? - super(attributes){ yield } - else - super(attributes) - end + super(attributes) end # Generate a Document Base URI element as a String. @@ -114,11 +118,7 @@ class CGI else href end - if block_given? - super(attributes){ yield } - else - super(attributes) - end + super(attributes) end # Generate a BlockQuote element as a string. @@ -137,11 +137,7 @@ class CGI else cite end - if block_given? - super(attributes){ yield } - else - super(attributes) - end + super(attributes) end @@ -161,11 +157,7 @@ class CGI else align end - if block_given? - super(attributes){ yield } - else - super(attributes) - end + super(attributes) end @@ -416,7 +408,7 @@ class CGI end pretty = attributes.delete("PRETTY") pretty = " " if true == pretty - buf = "" + buf = "".dup if attributes.has_key?("DOCTYPE") if attributes["DOCTYPE"] @@ -428,14 +420,10 @@ class CGI buf << doctype end - if block_given? - buf << super(attributes){ yield } - else - buf << super(attributes) - end + buf << super(attributes) if pretty - CGI::pretty(buf, pretty) + CGI.pretty(buf, pretty) else buf end @@ -824,11 +812,7 @@ class CGI else name end - if block_given? - super(attributes){ yield } - else - super(attributes) - end + super(attributes) end end # HtmlExtension @@ -836,50 +820,38 @@ class CGI # Mixin module for HTML version 3 generation methods. module Html3 # :nodoc: + include TagMaker # 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 - return if defined?(html) - methods = "" + instance_method(:nn_element_def).tap do |m| # - - 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 + define_method(element.downcase, m) end + end + instance_method(:nOE_element_def).tap do |m| # - 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 + define_method(element.downcase, m) end + end + instance_method(:nO_element_def).tap do |m| # 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 + define_method(element.downcase, m) end - eval(methods) end end # Html3 @@ -887,49 +859,38 @@ class CGI # Mixin module for HTML version 4 generation methods. module Html4 # :nodoc: + include TagMaker # 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 - return if defined?(html) - methods = "" - # - - + # Initialize the HTML generation methods for this version. + # - - + instance_method(:nn_element_def).tap do |m| 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 + define_method(element.downcase, m) end + end - # - O EMPTY + # - O EMPTY + instance_method(:nOE_element_def).tap do |m| 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 + define_method(element.downcase, m) end + end - # O O or - O + # O O or - O + instance_method(:nO_element_def).tap do |m| 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 + define_method(element.downcase, m) end - eval(methods) end end # Html4 @@ -937,6 +898,7 @@ class CGI # Mixin module for HTML version 4 transitional generation methods. module Html4Tr # :nodoc: + include TagMaker # The DOCTYPE declaration for this version of HTML def doctype @@ -944,44 +906,32 @@ class CGI end # Initialise the HTML generation methods for this version. - def element_init - extend TagMaker - return if defined?(html) - methods = "" - # - - + # - - + instance_method(:nn_element_def).tap do |m| 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 + define_method(element.downcase, m) end + end - # - O EMPTY + # - O EMPTY + instance_method(:nOE_element_def).tap do |m| 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 + define_method(element.downcase, m) end + end - # O O or - O + # O O or - O + instance_method(:nO_element_def).tap do |m| 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 + define_method(element.downcase, m) end - eval(methods) end end # Html4Tr @@ -989,6 +939,7 @@ class CGI # Mixin module for generating HTML version 4 with framesets. module Html4Fr # :nodoc: + include TagMaker # The DOCTYPE declaration for this version of HTML def doctype @@ -996,27 +947,18 @@ class CGI end # Initialise the HTML generation methods for this version. - def element_init - return if defined?(frameset) - methods = "" - # - - + # - - + instance_method(:nn_element_def).tap do |m| for element in %w[ FRAMESET ] - methods << <<-BEGIN + nn_element_def(element) + <<-END - def #{element.downcase}(attributes = {}) - BEGIN - end - END + define_method(element.downcase, m) end + end - # - O EMPTY + # - O EMPTY + instance_method(:nOE_element_def).tap do |m| for element in %w[ FRAME ] - methods << <<-BEGIN + nOE_element_def(element) + <<-END - def #{element.downcase}(attributes = {}) - BEGIN - end - END + define_method(element.downcase, m) end - eval(methods) end end # Html4Fr @@ -1024,6 +966,7 @@ class CGI # Mixin module for HTML version 5 generation methods. module Html5 # :nodoc: + include TagMaker # The DOCTYPE declaration for this version of HTML def doctype @@ -1031,11 +974,9 @@ class CGI end # Initialise the HTML generation methods for this version. - def element_init - extend TagMaker - methods = "" - # - - - for element in %w[ SECTION NAV ARTICLE ASIDE HGROUP + # - - + instance_method(:nn_element_def).tap do |m| + for element in %w[ SECTION NAV ARTICLE ASIDE HGROUP HEADER FOOTER FIGURE FIGCAPTION S TIME U MARK RUBY BDI IFRAME VIDEO AUDIO CANVAS DATALIST OUTPUT PROGRESS METER DETAILS SUMMARY MENU DIALOG I B SMALL EM STRONG DFN CODE SAMP KBD @@ -1043,34 +984,52 @@ class CGI H1 H2 H3 H4 H5 H6 PRE Q INS DEL DL OL UL LABEL SELECT 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 + define_method(element.downcase, m) end + end - # - O EMPTY + # - O EMPTY + instance_method(:nOE_element_def).tap do |m| for element in %w[ IMG BASE BR AREA LINK PARAM HR INPUT COL META COMMAND EMBED KEYGEN SOURCE TRACK WBR ] - methods += <<-BEGIN + nOE_element_def(element) + <<-END - def #{element.downcase}(attributes = {}) - BEGIN - end - END + define_method(element.downcase, m) end + end - # O O or - O + # O O or - O + instance_method(:nO_element_def).tap do |m| for element in %w[ HTML HEAD BODY P DT DD LI OPTION THEAD TFOOT TBODY OPTGROUP COLGROUP RT RP TR TH TD ] - methods += <<-BEGIN + nO_element_def(element) + <<-END - def #{element.downcase}(attributes = {}) - BEGIN - end - END + define_method(element.downcase, m) end - eval(methods) end end # Html5 + + class HTML3 + include Html3 + include HtmlExtension + end + + class HTML4 + include Html4 + include HtmlExtension + end + + class HTML4Tr + include Html4Tr + include HtmlExtension + end + + class HTML4Fr + include Html4Tr + include Html4Fr + include HtmlExtension + end + + class HTML5 + include Html5 + include HtmlExtension + end + end diff --git a/lib/cgi/session.rb b/lib/cgi/session.rb index 42b5ead81a..70c7ebca42 100644 --- a/lib/cgi/session.rb +++ b/lib/cgi/session.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # # cgi/session.rb - session support for cgi scripts # @@ -62,7 +63,7 @@ class CGI # 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 + # 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, @@ -163,29 +164,72 @@ class CGI # 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. + # The session id is a secure random number by SecureRandom + # if possible, otherwise an SHA512 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 + # by OpenSSL, or system provided entropy pool session_id = SecureRandom.hex(16) rescue NotImplementedError - require 'digest/md5' - md5 = Digest::MD5::new + # never happens on modern systems + require 'digest' + d = Digest('SHA512').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 + d.update(now.to_s) + d.update(String(now.usec)) + d.update(String(rand(0))) + d.update(String($$)) + d.update('foobar') + session_id = d.hexdigest[0, 32] end session_id end private :create_new_id + + # Create a new file to store the session data. + # + # This file will be created if it does not exist, or opened if it + # does. + # + # This path is generated under _tmpdir_ from _prefix_, the + # digested session id, and _suffix_. + # + # +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. + def new_store_file(option={}) # :nodoc: + dir = option['tmpdir'] || Dir::tmpdir + prefix = option['prefix'] + suffix = option['suffix'] + require 'digest/md5' + md5 = Digest::MD5.hexdigest(session_id)[0,16] + path = dir+"/" + path << prefix if prefix + path << md5 + path << suffix if suffix + if File::exist? path + hash = nil + elsif new_session + hash = {} + else + raise NoSession, "uninitialized session" + end + return path, hash + end + # Create a new CGI::Session object for +request+. # # +request+ is an instance of the +CGI+ class (see cgi.rb). @@ -308,7 +352,7 @@ class CGI @data[key] end - # Set the session date for key +key+. + # Set the session data for key +key+. def []=(key, val) @write_lock ||= true @data ||= @dbman.restore @@ -370,21 +414,8 @@ class CGI # 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 + option = {'prefix' => 'cgi_sid_'}.update(option) + @path, @hash = session.new_store_file(option) end # Restore session state from the session's FileStore file. @@ -400,11 +431,11 @@ class CGI for line in f line.chomp! k, v = line.split('=',2) - @hash[CGI::unescape(k)] = Marshal.restore(CGI::unescape(v)) + @hash[CGI.unescape(k)] = Marshal.restore(CGI.unescape(v)) end ensure - f.close unless f.nil? - lockf.close if lockf + f&.close + lockf&.close end end @hash @@ -418,13 +449,13 @@ class CGI 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))) + 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 + f&.close + lockf&.close end end @@ -437,14 +468,14 @@ class CGI def delete File::unlink @path+".lock" rescue nil File::unlink @path+".new" rescue nil - File::unlink @path rescue Errno::ENOENT + File::unlink @path rescue nil 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 + # data will only persist for as long as the Ruby interpreter # instance does. class MemoryStore GLOBAL_HASH_TABLE = {} #:nodoc: @@ -453,7 +484,7 @@ class CGI # # +session+ is the session this instance is associated with. # +option+ is a list of initialisation options. None are - # currently recognised. + # currently recognized. def initialize(session, option=nil) @session_id = session.session_id unless GLOBAL_HASH_TABLE.key?(@session_id) diff --git a/lib/cgi/session/pstore.rb b/lib/cgi/session/pstore.rb index a63d7d3984..45d0d8ae2c 100644 --- a/lib/cgi/session/pstore.rb +++ b/lib/cgi/session/pstore.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # # cgi/session/pstore.rb - persistent storage of marshalled session data # @@ -9,7 +10,7 @@ # 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_relative '../session' require 'pstore' class CGI @@ -43,21 +44,8 @@ class CGI # 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 + option = {'suffix'=>''}.update(option) + path, @hash = session.new_store_file(option) @p = ::PStore.new(path) @p.transaction do |p| File.chmod(0600, p.path) @@ -97,15 +85,4 @@ class CGI 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 +# :enddoc: diff --git a/lib/cgi/util.rb b/lib/cgi/util.rb index 41ae724c8c..ce77a0ccd5 100644 --- a/lib/cgi/util.rb +++ b/lib/cgi/util.rb @@ -1,22 +1,61 @@ +# frozen_string_literal: true class CGI - @@accept_charset="UTF-8" unless defined?(@@accept_charset) - # URL-encode a string. - # url_encoded_string = CGI::escape("'Stop!' said Fred") + module Util; end + include Util + extend Util +end +module CGI::Util + @@accept_charset = Encoding::UTF_8 unless defined?(@@accept_charset) + + # URL-encode a string into application/x-www-form-urlencoded. + # Space characters (+" "+) are encoded with plus signs (+"+"+) + # url_encoded_string = CGI.escape("'Stop!' said Fred") # # => "%27Stop%21%27+said+Fred" - def CGI::escape(string) + def escape(string) encoding = string.encoding - string.dup.force_encoding('ASCII-8BIT').gsub(/([^ a-zA-Z0-9_.-]+)/) do - '%' + $1.unpack('H2' * $1.bytesize).join('%').upcase - end.tr(' ', '+').force_encoding(encoding) + buffer = string.b + buffer.gsub!(/([^ a-zA-Z0-9_.\-~]+)/) do |m| + '%' + m.unpack('H2' * m.bytesize).join('%').upcase + end + buffer.tr!(' ', '+') + buffer.force_encoding(encoding) end - # URL-decode a string with encoding(optional). - # string = CGI::unescape("%27Stop%21%27+said+Fred") + # URL-decode an application/x-www-form-urlencoded string with encoding(optional). + # string = CGI.unescape("%27Stop%21%27+said+Fred") # # => "'Stop!' said Fred" - def CGI::unescape(string,encoding=@@accept_charset) - str=string.tr('+', ' ').force_encoding(Encoding::ASCII_8BIT).gsub(/((?:%[0-9a-fA-F]{2})+)/) do - [$1.delete('%')].pack('H*') - end.force_encoding(encoding) + def unescape(string, encoding = @@accept_charset) + str = string.tr('+', ' ') + str = str.b + str.gsub!(/((?:%[0-9a-fA-F]{2})+)/) do |m| + [m.delete('%')].pack('H*') + end + str.force_encoding(encoding) + str.valid_encoding? ? str : str.force_encoding(string.encoding) + end + + # URL-encode a string following RFC 3986 + # Space characters (+" "+) are encoded with (+"%20"+) + # url_encoded_string = CGI.escape("'Stop!' said Fred") + # # => "%27Stop%21%27%20said%20Fred" + def escapeURIComponent(string) + encoding = string.encoding + buffer = string.b + buffer.gsub!(/([^a-zA-Z0-9_.\-~]+)/) do |m| + '%' + m.unpack('H2' * m.bytesize).join('%').upcase + end + buffer.force_encoding(encoding) + end + + # URL-decode a string following RFC 3986 with encoding(optional). + # string = CGI.unescape("%27Stop%21%27+said%20Fred") + # # => "'Stop!'+said Fred" + def unescapeURIComponent(string, encoding = @@accept_charset) + str = string.b + str.gsub!(/((?:%[0-9a-fA-F]{2})+)/) do |m| + [m.delete('%')].pack('H*') + end + str.force_encoding(encoding) str.valid_encoding? ? str : str.force_encoding(string.encoding) end @@ -29,21 +68,46 @@ class CGI '>' => '>', } - # Escape special characters in HTML, namely &\"<> - # CGI::escapeHTML('Usage: foo "bar" <baz>') + # 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__) + def escapeHTML(string) + enc = string.encoding + unless enc.ascii_compatible? + if enc.dummy? + origenc = enc + enc = Encoding::Converter.asciicompat_encoding(enc) + string = enc ? string.encode(enc) : string.b + end + table = Hash[TABLE_FOR_ESCAPE_HTML__.map {|pair|pair.map {|s|s.encode(enc)}}] + string = string.gsub(/#{"['&\"<>]".encode(enc)}/, table) + string.encode!(origenc) if origenc + string + else + string = string.b + string.gsub!(/['&\"<>]/, TABLE_FOR_ESCAPE_HTML__) + string.force_encoding(enc) + end + end + + begin + require 'cgi/escape' + rescue LoadError end # Unescape a string that has been HTML-escaped - # CGI::unescapeHTML("Usage: foo "bar" <baz>") + # CGI.unescapeHTML("Usage: foo "bar" <baz>") # # => "Usage: foo \"bar\" <baz>" - def CGI::unescapeHTML(string) + def 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('&(apos|amp|quot|gt|lt|#[0-9]+|#x[0-9A-Fa-f]+);'.encode(enc))) do - case $1.encode("US-ASCII") + unless enc.ascii_compatible? + if enc.dummy? + origenc = enc + enc = Encoding::Converter.asciicompat_encoding(enc) + string = enc ? string.encode(enc) : string.b + end + string = string.gsub(Regexp.new('&(apos|amp|quot|gt|lt|#[0-9]+|#x[0-9A-Fa-f]+);'.encode(enc))) do + case $1.encode(Encoding::US_ASCII) when 'apos' then "'".encode(enc) when 'amp' then '&'.encode(enc) when 'quot' then '"'.encode(enc) @@ -53,9 +117,17 @@ class CGI when /\A#x([0-9a-f]+)\z/i then $1.hex.chr(enc) end end + string.encode!(origenc) if origenc + return string end - asciicompat = Encoding.compatible?(string, "a") - string.gsub(/&(apos|amp|quot|gt|lt|\#[0-9]+|\#x[0-9A-Fa-f]+);/) do + return string unless string.include? '&' + charlimit = case enc + when Encoding::UTF_8; 0x10ffff + when Encoding::ISO_8859_1; 256 + else 128 + end + string = string.b + string.gsub!(/&(apos|amp|quot|gt|lt|\#[0-9]+|\#[xX][0-9A-Fa-f]+);/) do match = $1.dup case match when 'apos' then "'" @@ -65,18 +137,14 @@ class CGI 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 + if n < charlimit 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 + if n < charlimit n.chr(enc) else "&#x#{$1};" @@ -85,17 +153,14 @@ class CGI "&#{match};" end end + string.force_encoding enc end - # Synonym for CGI::escapeHTML(str) - def CGI::escape_html(str) - escapeHTML(str) - end + # Synonym for CGI.escapeHTML(str) + alias escape_html escapeHTML - # Synonym for CGI::unescapeHTML(str) - def CGI::unescape_html(str) - unescapeHTML(str) - end + # Synonym for CGI.unescapeHTML(str) + alias unescape_html unescapeHTML # Escape only the tags of certain HTML elements in +string+. # @@ -105,67 +170,54 @@ class CGI # 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") + # 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"]) + # print CGI.escapeElement('<BR><A HREF="url"></A>', ["A", "IMG"]) # # "<BR><A HREF="url"></A>" - def CGI::escapeElement(string, *elements) + def 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($&) + string.gsub(/<\/?(?:#{elements.join("|")})\b[^<>]*+>?/im) do + CGI.escapeHTML($&) end else string end end - # Undo escaping such as that done by CGI::escapeElement() + # Undo escaping such as that done by CGI.escapeElement() # - # print CGI::unescapeElement( - # CGI::escapeHTML('<BR><A HREF="url"></A>'), "A", "IMG") + # 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"]) + # print CGI.unescapeElement( + # CGI.escapeHTML('<BR><A HREF="url"></A>'), ["A", "IMG"]) # # "<BR><A HREF="url"></A>" - def CGI::unescapeElement(string, *elements) + def 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($&) + string.gsub(/<\/?(?:#{elements.join("|")})\b(?>[^&]+|&(?![gl]t;)\w+;)*(?:>)?/im) do + unescapeHTML($&) end else string end end - # Synonym for CGI::escapeElement(str) - def CGI::escape_element(str) - escapeElement(str) - end - - # Synonym for CGI::unescapeElement(str) - def CGI::unescape_element(str) - unescapeElement(str) - end - - # Abbreviated day-of-week names specified by RFC 822 - RFC822_DAYS = %w[ Sun Mon Tue Wed Thu Fri Sat ] + # Synonym for CGI.escapeElement(str) + alias escape_element escapeElement - # Abbreviated month names specified by RFC 822 - RFC822_MONTHS = %w[ Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec ] + # Synonym for CGI.unescapeElement(str) + alias unescape_element unescapeElement # Format a +Time+ object as a String using the format specified by RFC 1123. # - # CGI::rfc1123_date(Time.now) + # 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) + def rfc1123_date(time) + time.getgm.strftime("%a, %d %b %Y %T GMT") end # Prettify (indent) an HTML string. @@ -173,19 +225,19 @@ class CGI # +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>") + # print CGI.pretty("<HTML><BODY></BODY></HTML>") # # <HTML> # # <BODY> # # </BODY> # # </HTML> # - # print CGI::pretty("<HTML><BODY></BODY></HTML>", "\t") + # print CGI.pretty("<HTML><BODY></BODY></HTML>", "\t") # # <HTML> # # <BODY> # # </BODY> # # </HTML> # - def CGI::pretty(string, shift = " ") + def pretty(string, shift = " ") lines = string.gsub(/(?!\A)<.*?>/m, "\n\\0").gsub(/<.*?>(?!\n)/m, "\\0\n") end_pos = 0 while end_pos = lines.index(/^<\/(\w+)/, end_pos) @@ -195,4 +247,6 @@ class CGI end lines.gsub(/^((?:#{Regexp::quote(shift)})*)__(?=<\/?\w)/, '\1') end + + alias h escapeHTML end |
