diff options
Diffstat (limited to 'lib/cgi')
| -rw-r--r-- | lib/cgi/cgi.gemspec | 42 | ||||
| -rw-r--r-- | lib/cgi/cookie.rb | 126 | ||||
| -rw-r--r-- | lib/cgi/core.rb | 106 | ||||
| -rw-r--r-- | lib/cgi/html.rb | 11 | ||||
| -rw-r--r-- | lib/cgi/session.rb | 97 | ||||
| -rw-r--r-- | lib/cgi/session/pstore.rb | 20 | ||||
| -rw-r--r-- | lib/cgi/util.rb | 164 |
7 files changed, 373 insertions, 193 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 3ec884dffb..1c4ef6a600 100644 --- a/lib/cgi/cookie.rb +++ b/lib/cgi/cookie.rb @@ -1,4 +1,5 @@ -require 'cgi/util' +# frozen_string_literal: true +require_relative 'util' class CGI # Class representing an HTTP cookie. # @@ -10,32 +11,39 @@ class CGI # == 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' => '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. # # :call-seq: @@ -53,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 @@ -78,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 @@ -123,14 +155,22 @@ 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}" + 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 @@ -144,16 +184,16 @@ class CGI cookies = Hash.new([]) return cookies unless raw_cookie - raw_cookie.split(/[;,]\s?/).each do |pairs| + 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 + cookies[name].concat(values) + else + cookies[name] = Cookie.new(name, *values) end - cookies[name] = Cookie.new(name, *values) end cookies diff --git a/lib/cgi/core.rb b/lib/cgi/core.rb index b81f915379..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 @@ -145,7 +153,7 @@ class CGI # "language" => "ja", # "expires" => Time.now + 30, # "cookie" => [cookie1, cookie2], - # "my_header1" => "my_value" + # "my_header1" => "my_value", # "my_header2" => "my_value") # # This method does not perform charset conversion. @@ -180,24 +188,35 @@ class CGI # 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') @@ -205,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'] || '' @@ -215,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_value {|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) @@ -367,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 @@ -413,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 @@ -426,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 @@ -479,7 +498,7 @@ class CGI @files = {} boundary_rexp = /--#{Regexp.quote(boundary)}(#{EOL}|--)/ boundary_size = "#{EOL}--#{boundary}#{EOL}".bytesize - buf = '' + buf = ''.dup bufsize = 10 * 1024 max_count = MAX_MULTIPART_COUNT n = 0 @@ -534,13 +553,13 @@ 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 || '' @@ -588,7 +607,7 @@ class CGI else begin require 'stringio' - body = StringIO.new("".force_encoding(Encoding::ASCII_8BIT)) + body = StringIO.new("".b) rescue LoadError require 'tempfile' body = Tempfile.new('CGI', encoding: Encoding::ASCII_8BIT) @@ -599,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 @@ -640,7 +660,7 @@ 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']) + %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 @@ -648,7 +668,7 @@ class CGI @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) @@ -678,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 @@ -699,7 +719,7 @@ class CGI if value return value elsif defined? StringIO - StringIO.new("".force_encoding(Encoding::ASCII_8BIT)) + StringIO.new("".b) else Tempfile.new("CGI",encoding: Encoding::ASCII_8BIT) end @@ -733,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 @@ -854,24 +874,24 @@ class CGI case @options[:tag_maker] when "html3" - require 'cgi/html' + require_relative 'html' extend Html3 extend HtmlExtension when "html4" - require 'cgi/html' + require_relative 'html' extend Html4 extend HtmlExtension when "html4Tr" - require 'cgi/html' + require_relative 'html' extend Html4Tr extend HtmlExtension when "html4Fr" - require 'cgi/html' + require_relative 'html' extend Html4Tr extend Html4Fr extend HtmlExtension when "html5" - require 'cgi/html' + require_relative 'html' extend Html5 extend HtmlExtension end diff --git a/lib/cgi/html.rb b/lib/cgi/html.rb index db47bb8266..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. # @@ -25,14 +26,14 @@ class CGI # - O EMPTY def nOE_element(element, attributes = {}) attributes={attributes=>nil} if attributes.kind_of?(String) - s = "<#{element.upcase}" + s = "<#{element.upcase}".dup attributes.each do|name, value| next unless value s << " " - s << CGI::escapeHTML(name.to_s) + s << CGI.escapeHTML(name.to_s) if value != true s << '="' - s << CGI::escapeHTML(value.to_s) + s << CGI.escapeHTML(value.to_s) s << '"' end end @@ -407,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"] @@ -422,7 +423,7 @@ class CGI buf << super(attributes) if pretty - CGI::pretty(buf, pretty) + CGI.pretty(buf, pretty) else buf end diff --git a/lib/cgi/session.rb b/lib/cgi/session.rb index 63c5003526..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 # @@ -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). @@ -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 diff --git a/lib/cgi/session/pstore.rb b/lib/cgi/session/pstore.rb index 75343149e1..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) diff --git a/lib/cgi/util.rb b/lib/cgi/util.rb index 3d7db8f2c8..ce77a0ccd5 100644 --- a/lib/cgi/util.rb +++ b/lib/cgi/util.rb @@ -1,23 +1,61 @@ -class CGI; module Util; end; extend Util; end +# frozen_string_literal: true +class CGI + module Util; end + include Util + extend Util +end module CGI::Util - @@accept_charset="UTF-8" unless defined?(@@accept_charset) - # URL-encode a string. - # url_encoded_string = CGI::escape("'Stop!' said Fred") + @@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 escape(string) encoding = string.encoding - string.b.gsub(/([^ a-zA-Z0-9_.-]+)/) do |m| + buffer = string.b + buffer.gsub!(/([^ a-zA-Z0-9_.\-~]+)/) do |m| '%' + m.unpack('H2' * m.bytesize).join('%').upcase - end.tr(' ', '+').force_encoding(encoding) + 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 unescape(string,encoding=@@accept_charset) - str=string.tr('+', ' ').b.gsub(/((?:%[0-9a-fA-F]{2})+)/) do |m| + 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.force_encoding(encoding) + end + str.force_encoding(encoding) str.valid_encoding? ? str : str.force_encoding(string.encoding) end @@ -30,21 +68,45 @@ module CGI::Util '>' => '>', } - # 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 escapeHTML(string) - string.gsub(/['&\"<>]/, TABLE_FOR_ESCAPE_HTML__) + 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 unescapeHTML(string) - return string unless string.include? '&' enc = string.encoding - if enc != Encoding::UTF_8 && [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 + 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) @@ -55,9 +117,17 @@ module CGI::Util 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]+|\#[xX][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 "'" @@ -67,18 +137,14 @@ module CGI::Util 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};" @@ -87,12 +153,13 @@ module CGI::Util "&#{match};" end end + string.force_encoding enc end - # Synonym for CGI::escapeHTML(str) + # Synonym for CGI.escapeHTML(str) alias escape_html escapeHTML - # Synonym for CGI::unescapeHTML(str) + # Synonym for CGI.unescapeHTML(str) alias unescape_html unescapeHTML # Escape only the tags of certain HTML elements in +string+. @@ -103,35 +170,35 @@ module CGI::Util # 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 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 unescapeElement(string, *elements) elements = elements[0] if elements[0].kind_of?(Array) unless elements.empty? - string.gsub(/<\/?(?:#{elements.join("|")})(?!\w)(?:.|\n)*?>/i) do + string.gsub(/<\/?(?:#{elements.join("|")})\b(?>[^&]+|&(?![gl]t;)\w+;)*(?:>)?/im) do unescapeHTML($&) end else @@ -139,27 +206,18 @@ module CGI::Util end end - # Synonym for CGI::escapeElement(str) + # Synonym for CGI.escapeElement(str) alias escape_element escapeElement - # Synonym for CGI::unescapeElement(str) + # Synonym for CGI.unescapeElement(str) alias unescape_element unescapeElement - # 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 ] - # 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 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) + time.getgm.strftime("%a, %d %b %Y %T GMT") end # Prettify (indent) an HTML string. @@ -167,13 +225,13 @@ module CGI::Util # +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> |
