├── .document ├── ext ├── cgi │ └── escape │ │ ├── depend │ │ ├── extconf.rb │ │ └── escape.c └── java │ └── org │ └── jruby │ └── ext │ └── cgi │ └── escape │ ├── lib │ └── cgi │ │ └── escape.rb │ └── CGIEscape.java ├── test ├── cgi │ ├── testdata │ │ ├── large.png │ │ ├── small.png │ │ └── file1.html │ ├── update_env.rb │ ├── test_cgi_util.rb │ ├── test_cgi_modruby.rb │ ├── test_cgi_session.rb │ ├── test_cgi_header.rb │ ├── test_cgi_cookie.rb │ ├── test_cgi_new.rb │ ├── test_cgi_core.rb │ ├── test_cgi_multipart.rb │ ├── test_cgi_escape.rb │ └── test_cgi_tag_helper.rb └── lib │ └── helper.rb ├── .github ├── release.yml ├── dependabot.yml └── workflows │ ├── sync-ruby.yml │ ├── test.yml │ └── push_gem.yml ├── rakelib ├── epoch.rake ├── version.rake ├── changelogs.rake └── check.rake ├── Gemfile ├── .gitignore ├── .git-blame-ignore-revs ├── Rakefile ├── BSDL ├── lib ├── cgi │ ├── util.rb │ ├── session │ │ └── pstore.rb │ ├── cookie.rb │ ├── escape.rb │ └── session.rb └── cgi.rb ├── cgi.gemspec ├── COPYING └── README.md /.document: -------------------------------------------------------------------------------- 1 | BSDL 2 | COPYING 3 | README.md 4 | ext/ 5 | lib/ 6 | -------------------------------------------------------------------------------- /ext/cgi/escape/depend: -------------------------------------------------------------------------------- 1 | escape.o: $(RUBY_EXTCONF_H) 2 | escape.o: escape.c 3 | -------------------------------------------------------------------------------- /test/cgi/testdata/large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruby/cgi/master/test/cgi/testdata/large.png -------------------------------------------------------------------------------- /test/cgi/testdata/small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruby/cgi/master/test/cgi/testdata/small.png -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - dependencies # Added by Dependabot 5 | -------------------------------------------------------------------------------- /test/lib/helper.rb: -------------------------------------------------------------------------------- 1 | require "test/unit" 2 | require "core_assertions" 3 | 4 | Test::Unit::TestCase.include Test::Unit::CoreAssertions 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /rakelib/epoch.rake: -------------------------------------------------------------------------------- 1 | task "build" => "date_epoch" 2 | 3 | task "date_epoch" do 4 | ENV["SOURCE_DATE_EPOCH"] = IO.popen(%W[git -C #{__dir__} log -1 --format=%ct], &:read).chomp 5 | end 6 | -------------------------------------------------------------------------------- /ext/cgi/escape/extconf.rb: -------------------------------------------------------------------------------- 1 | require 'mkmf' 2 | 3 | if RUBY_ENGINE == 'truffleruby' 4 | File.write("Makefile", dummy_makefile($srcdir).join("")) 5 | else 6 | create_makefile 'cgi/escape' 7 | end 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | group :development do 4 | gem "bundler" 5 | gem "rake" 6 | gem "rake-compiler" 7 | gem "test-unit" 8 | gem "test-unit-ruby-core" 9 | end 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /logs/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | /ChangeLog 11 | /Gemfile.lock 12 | *.jar 13 | *.bundle 14 | *.so 15 | *.dll 16 | -------------------------------------------------------------------------------- /ext/java/org/jruby/ext/cgi/escape/lib/cgi/escape.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Load built-in cgi/escape library 4 | require 'cgi/escape.jar' 5 | JRuby::Util.load_ext("org.jruby.ext.cgi.escape.CGIEscape") 6 | -------------------------------------------------------------------------------- /test/cgi/update_env.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | module UpdateEnv 3 | def update_env(environ) 4 | environ.each do |key, val| 5 | @environ[key] = ENV[key] unless @environ.key?(key) 6 | ENV[key] = val 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # This is a file used by GitHub to ignore the following commits on `git blame`. 2 | # 3 | # You can also do the same thing in your local repository with: 4 | # $ git config --local blame.ignoreRevsFile .git-blame-ignore-revs 5 | 6 | # Expand tabs 7 | 4d987cf78f7f3eaeb67ef0463d3c0d07aaa3de09 8 | -------------------------------------------------------------------------------- /test/cgi/testdata/file1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ムスカ大佐のひとりごと 5 | 6 | 7 | 8 |

バカどもにはちょうどいい目くらましだ。

9 | 10 | 11 | -------------------------------------------------------------------------------- /test/cgi/test_cgi_util.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'test/unit' 3 | require 'cgi/util' 4 | 5 | class CGIUtilTest < Test::Unit::TestCase 6 | 7 | def test_cgi_pretty 8 | assert_equal("\n \n \n\n",CGI.pretty("")) 9 | assert_equal("\n\t\n\t\n\n",CGI.pretty("","\t")) 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | require 'rake/javaextensiontask' 5 | Rake::JavaExtensionTask.new("escape") do |ext| 6 | ext.source_version = '1.8' 7 | ext.target_version = '1.8' 8 | ext.ext_dir = 'ext/java' 9 | ext.lib_dir = 'lib/cgi' 10 | 11 | task :build => :compile 12 | end 13 | 14 | if RUBY_ENGINE == 'ruby' 15 | require 'rake/extensiontask' 16 | extask = Rake::ExtensionTask.new("cgi/escape") do |x| 17 | x.lib_dir.sub!(%r[(?=/|\z)], "/#{RUBY_VERSION}/#{x.platform}") 18 | end 19 | end 20 | 21 | Rake::TestTask.new(:test) do |t| 22 | t.libs << "test/lib" 23 | if RUBY_ENGINE == 'jruby' 24 | t.libs << "ext/java/org/jruby/ext/cgi/escape/lib" 25 | elsif RUBY_ENGINE == 'ruby' 26 | t.libs << "lib/#{RUBY_VERSION}/#{extask.platform}" 27 | end 28 | t.ruby_opts << "-rhelper" 29 | t.test_files = FileList['test/**/test_*.rb'] 30 | end 31 | 32 | task :default => :test 33 | task :test => :compile 34 | -------------------------------------------------------------------------------- /rakelib/version.rake: -------------------------------------------------------------------------------- 1 | class << (helper = Bundler::GemHelper.instance) 2 | def update_gemspec 3 | path = gemspec.loaded_from 4 | File.open(path, "r+b") do |f| 5 | d = f.read 6 | if d.sub!(/^(_VERSION\s*=\s*)".*"/) {$1 + gemspec.version.to_s.dump} 7 | f.rewind 8 | f.truncate(0) 9 | f.print(d) 10 | end 11 | end 12 | end 13 | 14 | def commit_bump 15 | sh(%W[git commit -m bump\ up\ to\ #{gemspec.version} 16 | #{gemspec.loaded_from}]) 17 | end 18 | 19 | def version=(v) 20 | gemspec.version = v 21 | update_gemspec 22 | commit_bump 23 | end 24 | end 25 | 26 | major, minor, teeny = helper.gemspec.version.segments 27 | 28 | task "bump:teeny" do 29 | helper.version = Gem::Version.new("#{major}.#{minor}.#{teeny+1}") 30 | end 31 | 32 | task "bump:minor" do 33 | helper.version = Gem::Version.new("#{major}.#{minor+1}.0") 34 | end 35 | 36 | task "bump:major" do 37 | helper.version = Gem::Version.new("#{major+1}.0.0") 38 | end 39 | 40 | task "bump" => "bump:teeny" 41 | 42 | task "tag" do 43 | helper.__send__(:tag_version) 44 | end 45 | -------------------------------------------------------------------------------- /.github/workflows/sync-ruby.yml: -------------------------------------------------------------------------------- 1 | name: Sync ruby 2 | on: 3 | push: 4 | branches: [master] 5 | jobs: 6 | sync: 7 | name: Sync ruby 8 | runs-on: ubuntu-latest 9 | if: ${{ github.repository_owner == 'ruby' }} 10 | steps: 11 | - uses: actions/checkout@v6 12 | 13 | - name: Create GitHub App token 14 | id: app-token 15 | uses: actions/create-github-app-token@v2 16 | with: 17 | app-id: 2060836 18 | private-key: ${{ secrets.RUBY_SYNC_DEFAULT_GEMS_PRIVATE_KEY }} 19 | owner: ruby 20 | repositories: ruby 21 | 22 | - name: Sync to ruby/ruby 23 | uses: convictional/trigger-workflow-and-wait@v1.6.5 24 | with: 25 | owner: ruby 26 | repo: ruby 27 | workflow_file_name: sync_default_gems.yml 28 | github_token: ${{ steps.app-token.outputs.token }} 29 | ref: master 30 | client_payload: | 31 | {"gem":"${{ github.event.repository.name }}","before":"${{ github.event.before }}","after":"${{ github.event.after }}"} 32 | propagate_failure: true 33 | wait_interval: 10 34 | -------------------------------------------------------------------------------- /rakelib/changelogs.rake: -------------------------------------------------------------------------------- 1 | task "build" => "changelogs" 2 | 3 | changelog = proc do |output, ver = nil, prev = nil| 4 | ver &&= Gem::Version.new(ver) 5 | range = [[prev], [ver, "HEAD"]].map {|ver, branch| ver ? "v#{ver.to_s}" : branch}.compact.join("..") 6 | IO.popen(%W[git log --format=fuller --topo-order --no-merges #{range}]) do |log| 7 | line = log.gets 8 | FileUtils.mkpath(File.dirname(output)) 9 | File.open(output, "wb") do |f| 10 | f.print "-*- coding: utf-8 -*-\n\n", line 11 | log.each_line do |line| 12 | line.sub!(/^(?!:)(?:Author|Commit)?(?:Date)?: /, ' \&') 13 | line.sub!(/ +$/, '') 14 | f.print(line) 15 | end 16 | end 17 | end 18 | end 19 | 20 | tags = IO.popen(%w[git tag -l v[0-9]*]).grep(/v(.*)/) {$1} 21 | tags.sort_by! {|tag| tag.scan(/\d+/).map(&:to_i)} 22 | tags.inject(nil) do |prev, tag| 23 | task("logs/ChangeLog-#{tag}") {|t| changelog[t.name, tag, prev]} 24 | tag 25 | end 26 | 27 | desc "Make ChangeLog" 28 | task "ChangeLog", [:ver, :prev] do |t, ver: nil, prev: tags.last| 29 | changelog[t.name, ver, prev] 30 | end 31 | 32 | changelogs = ["ChangeLog", *tags.map {|tag| "logs/ChangeLog-#{tag}"}] 33 | task "changelogs" => changelogs 34 | CLOBBER.concat(changelogs) << "logs" 35 | -------------------------------------------------------------------------------- /rakelib/check.rake: -------------------------------------------------------------------------------- 1 | task :check do 2 | Bundler.with_unbundled_env do 3 | spec = Gem::Specification::load("cgi.gemspec") 4 | version = spec.version.to_s 5 | 6 | gem = "pkg/cgi-#{version}#{"-java" if RUBY_ENGINE == "jruby"}.gem" 7 | File.size?(gem) or abort "gem not built!" 8 | 9 | sh "gem", "install", gem 10 | 11 | require_relative "../test/lib/envutil" 12 | 13 | _, _, status = EnvUtil.invoke_ruby([], <<~EOS) 14 | version = #{version.dump} 15 | gem "cgi", version 16 | loaded_version = Gem.loaded_specs["cgi"].version.to_s 17 | if loaded_version == version 18 | puts "cgi \#{loaded_version} is loaded." 19 | else 20 | abort "cgi \#{loaded_version} is loaded instead of \#{version}!" 21 | end 22 | require "cgi/escape" 23 | 24 | string = "&<>" 25 | actual = CGI.escape(string) 26 | expected = "%26%3C%3E" 27 | puts "CGI.escape(\#{string.dump}) = \#{actual.dump}" 28 | if actual != expected 29 | abort "no! expected to be \#{expected.dump}!" 30 | end 31 | EOS 32 | 33 | if status.success? 34 | puts "check succeeded!" 35 | else 36 | warn "check failed!" 37 | exit status.exitstatus 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /BSDL: -------------------------------------------------------------------------------- 1 | Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 18 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 20 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 21 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 22 | SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ruby-versions: 7 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 8 | with: 9 | min_version: 2.5 10 | test: 11 | needs: ruby-versions 12 | name: build (${{ matrix.ruby }} / ${{ matrix.os }}) 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 17 | os: [ ubuntu-latest, macos-latest, windows-latest ] 18 | exclude: 19 | - { os: macos-latest, ruby: 2.5 } 20 | - { os: windows-latest, ruby: head } 21 | - { os: macos-latest, ruby: jruby } 22 | - { os: windows-latest, ruby: jruby } 23 | - { os: macos-latest, ruby: jruby-head } 24 | - { os: windows-latest, ruby: jruby-head } 25 | - { os: windows-latest, ruby: truffleruby } 26 | - { os: windows-latest, ruby: truffleruby-head } 27 | include: 28 | - { os: windows-latest, ruby: mingw } 29 | - { os: windows-latest, ruby: mswin } 30 | runs-on: ${{ matrix.os }} 31 | steps: 32 | - uses: actions/checkout@v6 33 | - name: Set up Ruby 34 | uses: ruby/setup-ruby@v1 35 | with: 36 | ruby-version: ${{ matrix.ruby }} 37 | - name: Install dependencies 38 | run: bundle install 39 | - name: Build 40 | run: rake compile 41 | - name: Run test 42 | run: bundle exec rake test 43 | -------------------------------------------------------------------------------- /lib/cgi/util.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class CGI 3 | module Util; end 4 | include Util 5 | extend Util 6 | end 7 | 8 | # Utility methods for CGI. 9 | module CGI::Util 10 | # Format a +Time+ object as a String using the format specified by RFC 1123. 11 | # 12 | # CGI.rfc1123_date(Time.now) 13 | # # Sat, 01 Jan 2000 00:00:00 GMT 14 | def rfc1123_date(time) 15 | time.getgm.strftime("%a, %d %b %Y %T GMT") 16 | end 17 | 18 | # Prettify (indent) an HTML string. 19 | # 20 | # +string+ is the HTML string to indent. +shift+ is the indentation 21 | # unit to use; it defaults to two spaces. 22 | # 23 | # print CGI.pretty("") 24 | # # 25 | # # 26 | # # 27 | # # 28 | # 29 | # print CGI.pretty("", "\t") 30 | # # 31 | # # 32 | # # 33 | # # 34 | # 35 | def pretty(string, shift = " ") 36 | lines = string.gsub(/(?!\A)<.*?>/m, "\n\\0").gsub(/<.*?>(?!\n)/m, "\\0\n") 37 | end_pos = 0 38 | while end_pos = lines.index(/^<\/(\w+)/, end_pos) 39 | element = $1.dup 40 | start_pos = lines.rindex(/^\s*<#{element}/i, end_pos) 41 | lines[start_pos ... end_pos] = "__" + lines[start_pos ... end_pos].gsub(/\n(?!\z)/, "\n" + shift) + "__" 42 | end 43 | lines.gsub(/^((?:#{Regexp::quote(shift)})*)__(?=<\/?\w)/, '\1') 44 | end 45 | end 46 | 47 | # For backward compatibility 48 | require 'cgi/escape' unless defined?(CGI::EscapeExt) 49 | -------------------------------------------------------------------------------- /cgi.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | name = File.basename(__FILE__, ".gemspec") 4 | version = ["lib", Array.new(name.count("-")+1, "..").join("/")].find do |dir| 5 | break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line| 6 | /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1 7 | end rescue nil 8 | end 9 | 10 | Gem::Specification.new do |spec| 11 | spec.name = name 12 | spec.version = version 13 | spec.authors = ["Yukihiro Matsumoto"] 14 | spec.email = ["matz@ruby-lang.org"] 15 | 16 | spec.summary = %q{Support for the Common Gateway Interface protocol.} 17 | spec.description = %q{Support for the Common Gateway Interface protocol.} 18 | spec.homepage = "https://github.com/ruby/cgi" 19 | spec.licenses = ["Ruby", "BSD-2-Clause"] 20 | spec.required_ruby_version = ">= 2.5.0" 21 | 22 | spec.metadata["homepage_uri"] = spec.homepage 23 | spec.metadata["source_code_uri"] = spec.homepage 24 | 25 | spec.executables = [] 26 | 27 | spec.files = [ 28 | "COPYING", 29 | "BSDL", 30 | "README.md", 31 | *Dir["lib{.rb,/**/*.rb}", "bin/*"] ] 32 | 33 | spec.require_paths = ["lib"] 34 | 35 | if Gem::Platform === spec.platform and spec.platform =~ 'java' or RUBY_ENGINE == 'jruby' 36 | spec.platform = 'java' 37 | spec.require_paths << "ext/java/org/jruby/ext/cgi/escape/lib" 38 | spec.files += Dir["ext/java/**/*.{rb}", "lib/cgi/escape.jar"] 39 | else 40 | spec.files += Dir["ext/cgi/**/*.{rb,c,h,sh}", "ext/cgi/escape/depend", "lib/cgi/escape.so"] 41 | spec.extensions = ["ext/cgi/escape/extconf.rb"] 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /.github/workflows/push_gem.yml: -------------------------------------------------------------------------------- 1 | name: Publish gem to rubygems.org 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | push: 13 | if: github.repository == 'ruby/cgi' 14 | runs-on: ubuntu-latest 15 | 16 | environment: 17 | name: rubygems.org 18 | url: https://rubygems.org/gems/cgi 19 | 20 | permissions: 21 | contents: write 22 | id-token: write 23 | 24 | strategy: 25 | matrix: 26 | ruby: ["ruby", "jruby"] 27 | 28 | steps: 29 | - name: Harden Runner 30 | uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 31 | with: 32 | egress-policy: audit 33 | 34 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 35 | 36 | - name: Set up Ruby 37 | uses: ruby/setup-ruby@13e7a03dc3ac6c3798f4570bfead2aed4d96abfb # v1.244.0 38 | with: 39 | ruby-version: ${{ matrix.ruby }} 40 | 41 | # https://github.com/rubygems/rubygems/issues/5882 42 | - name: Install dependencies and build for JRuby 43 | run: | 44 | sudo apt install default-jdk maven 45 | gem update --system 46 | gem install ruby-maven rake-compiler --no-document 47 | rake compile 48 | if: matrix.ruby == 'jruby' 49 | 50 | - name: Install dependencies 51 | run: bundle install --jobs 4 --retry 3 52 | 53 | - name: Publish to RubyGems 54 | uses: rubygems/release-gem@1c162a739e8b4cb21a676e97b087e8268d8fc40b # v1.1.2 55 | 56 | - name: Create GitHub release 57 | run: | 58 | tag_name="$(git describe --tags --abbrev=0)" 59 | gh release create "${tag_name}" --verify-tag --generate-notes 60 | env: 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | if: matrix.ruby == 'ruby' 63 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Ruby is copyrighted free software by Yukihiro Matsumoto . 2 | You can redistribute it and/or modify it under either the terms of the 3 | 2-clause BSDL (see the file BSDL), or the conditions below: 4 | 5 | 1. You may make and give away verbatim copies of the source form of the 6 | software without restriction, provided that you duplicate all of the 7 | original copyright notices and associated disclaimers. 8 | 9 | 2. You may modify your copy of the software in any way, provided that 10 | you do at least ONE of the following: 11 | 12 | a. place your modifications in the Public Domain or otherwise 13 | make them Freely Available, such as by posting said 14 | modifications to Usenet or an equivalent medium, or by allowing 15 | the author to include your modifications in the software. 16 | 17 | b. use the modified software only within your corporation or 18 | organization. 19 | 20 | c. give non-standard binaries non-standard names, with 21 | instructions on where to get the original software distribution. 22 | 23 | d. make other distribution arrangements with the author. 24 | 25 | 3. You may distribute the software in object code or binary form, 26 | provided that you do at least ONE of the following: 27 | 28 | a. distribute the binaries and library files of the software, 29 | together with instructions (in the manual page or equivalent) 30 | on where to get the original distribution. 31 | 32 | b. accompany the distribution with the machine-readable source of 33 | the software. 34 | 35 | c. give non-standard binaries non-standard names, with 36 | instructions on where to get the original software distribution. 37 | 38 | d. make other distribution arrangements with the author. 39 | 40 | 4. You may modify and include the part of the software into any other 41 | software (possibly commercial). But some files in the distribution 42 | are not written by the author, so that they are not under these terms. 43 | 44 | For the list of those files and their copying conditions, see the 45 | file LEGAL. 46 | 47 | 5. The scripts and library files supplied as input to or produced as 48 | output from the software do not automatically fall under the 49 | copyright of the software, but belong to whomever generated them, 50 | and may be sold commercially, and may be aggregated with this 51 | software. 52 | 53 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 54 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 55 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 56 | PURPOSE. 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Introduction 2 | 3 | CGI is a large class, providing several categories of methods, many of which 4 | are mixed in from other modules. Some of the documentation is in this class, 5 | some in the modules CGI::QueryExtension and CGI::HtmlExtension. See 6 | CGI::Cookie for specific information on handling cookies, and cgi/session.rb 7 | (CGI::Session) for information on sessions. 8 | 9 | For queries, CGI provides methods to get at environmental variables, 10 | parameters, cookies, and multipart request data. For responses, CGI provides 11 | methods for writing output and generating HTML. 12 | 13 | Read on for more details. Examples are provided at the bottom. 14 | 15 | ## Installation 16 | 17 | Add this line to your application's Gemfile: 18 | 19 | ```ruby 20 | gem 'cgi' 21 | ``` 22 | 23 | And then execute: 24 | 25 | ```bash 26 | bundle 27 | ``` 28 | 29 | Or install it yourself as: 30 | 31 | ```bash 32 | gem install cgi 33 | ``` 34 | 35 | ## Usage 36 | 37 | ### Get form values 38 | 39 | Given a form with the content `field_name=123`: 40 | 41 | ```ruby 42 | require "cgi" 43 | cgi = CGI.new 44 | value = cgi['field_name'] # => "123" 45 | cgi['flowerpot'] # => "" 46 | fields = cgi.keys # => [ "field_name" ] 47 | 48 | cgi.has_key?('field_name') # => true 49 | cgi.include?('field_name') # => true 50 | cgi.include?('flowerpot') # => false 51 | ``` 52 | 53 | ### Get form values as hash 54 | 55 | ```ruby 56 | require "cgi" 57 | cgi = CGI.new 58 | params = cgi.params 59 | ``` 60 | 61 | cgi.params is a hash. 62 | 63 | ```ruby 64 | cgi.params['new_field_name'] = ["value"] # add new param 65 | cgi.params['field_name'] = ["new_value"] # change value 66 | cgi.params.delete('field_name') # delete param 67 | cgi.params.clear # delete all params 68 | ``` 69 | 70 | ### Save form values to file 71 | 72 | ```ruby 73 | require "pstore" 74 | db = PStore.new("query.db") 75 | db.transaction do 76 | db["params"] = cgi.params 77 | end 78 | ``` 79 | 80 | ### Restore form values from file 81 | 82 | ```ruby 83 | require "pstore" 84 | db = PStore.new("query.db") 85 | db.transaction do 86 | cgi.params = db["params"] 87 | end 88 | ``` 89 | 90 | ## Development 91 | 92 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 93 | 94 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 95 | 96 | ## Contributing 97 | 98 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/cgi. 99 | -------------------------------------------------------------------------------- /lib/cgi/session/pstore.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # cgi/session/pstore.rb - persistent storage of marshalled session data 4 | # 5 | # Documentation: William Webber (william@williamwebber.com) 6 | # 7 | # == Overview 8 | # 9 | # This file provides the CGI::Session::PStore class, which builds 10 | # persistent of session data on top of the pstore library. See 11 | # cgi/session.rb for more details on session storage managers. 12 | 13 | require_relative '../session' 14 | begin 15 | require 'pstore' 16 | rescue LoadError 17 | end 18 | 19 | class CGI 20 | class Session 21 | # PStore-based session storage class. 22 | # 23 | # This builds upon the top-level PStore class provided by the 24 | # library file pstore.rb. Session data is marshalled and stored 25 | # in a file. File locking and transaction services are provided. 26 | class PStore 27 | # Create a new CGI::Session::PStore instance 28 | # 29 | # This constructor is used internally by CGI::Session. The 30 | # user does not generally need to call it directly. 31 | # 32 | # +session+ is the session for which this instance is being 33 | # created. The session id must only contain alphanumeric 34 | # characters; automatically generated session ids observe 35 | # this requirement. 36 | # 37 | # +option+ is a hash of options for the initializer. The 38 | # following options are recognised: 39 | # 40 | # tmpdir:: the directory to use for storing the PStore 41 | # file. Defaults to Dir::tmpdir (generally "/tmp" 42 | # on Unix systems). 43 | # prefix:: the prefix to add to the session id when generating 44 | # the filename for this session's PStore file. 45 | # Defaults to the empty string. 46 | # 47 | # This session's PStore file will be created if it does 48 | # not exist, or opened if it does. 49 | def initialize(session, option={}) 50 | option = {'suffix'=>''}.update(option) 51 | path, @hash = session.new_store_file(option) 52 | @p = ::PStore.new(path) 53 | @p.transaction do |p| 54 | File.chmod(0600, p.path) 55 | end 56 | end 57 | 58 | # Restore session state from the session's PStore file. 59 | # 60 | # Returns the session state as a hash. 61 | def restore 62 | unless @hash 63 | @p.transaction do 64 | @hash = @p['hash'] || {} 65 | end 66 | end 67 | @hash 68 | end 69 | 70 | # Save session state to the session's PStore file. 71 | def update 72 | @p.transaction do 73 | @p['hash'] = @hash 74 | end 75 | end 76 | 77 | # Update and close the session's PStore file. 78 | def close 79 | update 80 | end 81 | 82 | # Close and delete the session's PStore file. 83 | def delete 84 | path = @p.path 85 | File::unlink path 86 | end 87 | 88 | end if defined?(::PStore) 89 | end 90 | end 91 | # :enddoc: 92 | -------------------------------------------------------------------------------- /test/cgi/test_cgi_modruby.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'test/unit' 3 | require 'cgi' 4 | require_relative 'update_env' 5 | 6 | 7 | class CGIModrubyTest < Test::Unit::TestCase 8 | include UpdateEnv 9 | 10 | 11 | def setup 12 | @environ = {} 13 | update_env( 14 | 'SERVER_PROTOCOL' => 'HTTP/1.1', 15 | 'REQUEST_METHOD' => 'GET', 16 | #'QUERY_STRING' => 'a=foo&b=bar', 17 | ) 18 | CGI.class_eval { const_set(:MOD_RUBY, true) } 19 | Apache._reset() 20 | #@cgi = CGI.new 21 | #@req = Apache.request 22 | end 23 | 24 | 25 | def teardown 26 | ENV.update(@environ) 27 | CGI.class_eval { remove_const(:MOD_RUBY) } 28 | end 29 | 30 | 31 | def test_cgi_modruby_simple 32 | req = Apache.request 33 | cgi = CGI.new 34 | assert(req._setup_cgi_env_invoked?) 35 | assert(! req._send_http_header_invoked?) 36 | actual = cgi.http_header 37 | assert_equal('', actual) 38 | assert_equal('text/html', req.content_type) 39 | assert(req._send_http_header_invoked?) 40 | end 41 | 42 | 43 | def test_cgi_modruby_complex 44 | req = Apache.request 45 | cgi = CGI.new 46 | options = { 47 | 'status' => 'FORBIDDEN', 48 | 'location' => 'http://www.example.com/', 49 | 'type' => 'image/gif', 50 | 'content-encoding' => 'deflate', 51 | 'cookie' => [ CGI::Cookie.new('name1', 'abc', '123'), 52 | CGI::Cookie.new('name'=>'name2', 'value'=>'value2', 'secure'=>true), 53 | ], 54 | } 55 | assert(req._setup_cgi_env_invoked?) 56 | assert(! req._send_http_header_invoked?) 57 | actual = cgi.http_header(options) 58 | assert_equal('', actual) 59 | assert_equal('image/gif', req.content_type) 60 | assert_equal('403 Forbidden', req.status_line) 61 | assert_equal(403, req.status) 62 | assert_equal('deflate', req.content_encoding) 63 | assert_equal('http://www.example.com/', req.headers_out['location']) 64 | assert_equal(["name1=abc&123; path=", "name2=value2; path=; secure"], 65 | req.headers_out['Set-Cookie']) 66 | assert(req._send_http_header_invoked?) 67 | end 68 | 69 | 70 | def test_cgi_modruby_location 71 | req = Apache.request 72 | cgi = CGI.new 73 | options = { 74 | 'status' => '200 OK', 75 | 'location' => 'http://www.example.com/', 76 | } 77 | cgi.http_header(options) 78 | assert_equal('200 OK', req.status_line) # should be '302 Found' ? 79 | assert_equal(302, req.status) 80 | assert_equal('http://www.example.com/', req.headers_out['location']) 81 | end 82 | 83 | 84 | def test_cgi_modruby_requestparams 85 | req = Apache.request 86 | req.args = 'a=foo&b=bar' 87 | cgi = CGI.new 88 | assert_equal('foo', cgi['a']) 89 | assert_equal('bar', cgi['b']) 90 | end 91 | 92 | 93 | instance_methods.each do |method| 94 | private method if method =~ /^test_(.*)/ && $1 != ENV['TEST'] 95 | end if ENV['TEST'] 96 | 97 | end 98 | 99 | 100 | 101 | ## dummy class for mod_ruby 102 | class Apache #:nodoc: 103 | 104 | def self._reset 105 | @request = Request.new 106 | end 107 | 108 | def self.request 109 | return @request 110 | end 111 | 112 | class Request 113 | 114 | def initialize 115 | hash = {} 116 | def hash.add(name, value) 117 | (self[name] ||= []) << value 118 | end 119 | @http_header = nil 120 | @headers_out = hash 121 | @status_line = nil 122 | @status = nil 123 | @content_type = nil 124 | @content_encoding = nil 125 | end 126 | attr_accessor :headers_out, :status_line, :status, :content_type, :content_encoding 127 | 128 | attr_accessor :args 129 | #def args 130 | # return ENV['QUERY_STRING'] 131 | #end 132 | 133 | def send_http_header 134 | @http_header = '*invoked*' 135 | end 136 | def _send_http_header_invoked? 137 | @http_header ? true : false 138 | end 139 | 140 | def setup_cgi_env 141 | @cgi_env = '*invoked*' 142 | end 143 | def _setup_cgi_env_invoked? 144 | @cgi_env ? true : false 145 | end 146 | 147 | end 148 | 149 | end 150 | -------------------------------------------------------------------------------- /test/cgi/test_cgi_session.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'test/unit' 3 | require 'cgi' 4 | require 'cgi/session' 5 | require 'cgi/session/pstore' 6 | require 'stringio' 7 | require 'tmpdir' 8 | require_relative 'update_env' 9 | 10 | class CGISessionTest < Test::Unit::TestCase 11 | include UpdateEnv 12 | 13 | def setup 14 | @environ = {} 15 | @session_dir = Dir.mktmpdir(%w'session dir') 16 | end 17 | 18 | def teardown 19 | ENV.update(@environ) 20 | $stdout = STDOUT 21 | FileUtils.rm_rf(@session_dir) 22 | end 23 | 24 | def test_cgi_session_filestore 25 | update_env( 26 | 'REQUEST_METHOD' => 'GET', 27 | # 'QUERY_STRING' => 'id=123&id=456&id=&str=%40h+%3D%7E+%2F%5E%24%2F', 28 | # 'HTTP_COOKIE' => '_session_id=12345; name1=val1&val2;', 29 | 'SERVER_SOFTWARE' => 'Apache 2.2.0', 30 | 'SERVER_PROTOCOL' => 'HTTP/1.1', 31 | ) 32 | value1="value1" 33 | value2="\x8F\xBC\x8D]".dup 34 | value2.force_encoding("SJIS") if defined?(::Encoding) 35 | cgi = CGI.new 36 | session = CGI::Session.new(cgi,"tmpdir"=>@session_dir) 37 | session["key1"]=value1 38 | session["key2"]=value2 39 | assert_equal(value1,session["key1"]) 40 | assert_equal(value2,session["key2"]) 41 | session.close 42 | $stdout = StringIO.new 43 | cgi.out{""} 44 | 45 | update_env( 46 | 'REQUEST_METHOD' => 'GET', 47 | # 'HTTP_COOKIE' => "_session_id=#{session_id}", 48 | 'QUERY_STRING' => "_session_id=#{session.session_id}", 49 | 'SERVER_SOFTWARE' => 'Apache 2.2.0', 50 | 'SERVER_PROTOCOL' => 'HTTP/1.1', 51 | ) 52 | cgi = CGI.new 53 | session = CGI::Session.new(cgi,"tmpdir"=>@session_dir) 54 | $stdout = StringIO.new 55 | assert_equal(value1,session["key1"]) 56 | assert_equal(value2,session["key2"]) 57 | session.close 58 | 59 | end 60 | def test_cgi_session_pstore 61 | update_env( 62 | 'REQUEST_METHOD' => 'GET', 63 | # 'QUERY_STRING' => 'id=123&id=456&id=&str=%40h+%3D%7E+%2F%5E%24%2F', 64 | # 'HTTP_COOKIE' => '_session_id=12345; name1=val1&val2;', 65 | 'SERVER_SOFTWARE' => 'Apache 2.2.0', 66 | 'SERVER_PROTOCOL' => 'HTTP/1.1', 67 | ) 68 | value1="value1" 69 | value2="\x8F\xBC\x8D]".dup 70 | value2.force_encoding("SJIS") if defined?(::Encoding) 71 | cgi = CGI.new 72 | session = CGI::Session.new(cgi,"tmpdir"=>@session_dir,"database_manager"=>CGI::Session::PStore) 73 | session["key1"]=value1 74 | session["key2"]=value2 75 | assert_equal(value1,session["key1"]) 76 | assert_equal(value2,session["key2"]) 77 | session.close 78 | $stdout = StringIO.new 79 | cgi.out{""} 80 | 81 | update_env( 82 | 'REQUEST_METHOD' => 'GET', 83 | # 'HTTP_COOKIE' => "_session_id=#{session_id}", 84 | 'QUERY_STRING' => "_session_id=#{session.session_id}", 85 | 'SERVER_SOFTWARE' => 'Apache 2.2.0', 86 | 'SERVER_PROTOCOL' => 'HTTP/1.1', 87 | ) 88 | cgi = CGI.new 89 | session = CGI::Session.new(cgi,"tmpdir"=>@session_dir,"database_manager"=>CGI::Session::PStore) 90 | $stdout = StringIO.new 91 | assert_equal(value1,session["key1"]) 92 | assert_equal(value2,session["key2"]) 93 | session.close 94 | end if defined?(::PStore) 95 | def test_cgi_session_specify_session_id 96 | update_env( 97 | 'REQUEST_METHOD' => 'GET', 98 | # 'QUERY_STRING' => 'id=123&id=456&id=&str=%40h+%3D%7E+%2F%5E%24%2F', 99 | # 'HTTP_COOKIE' => '_session_id=12345; name1=val1&val2;', 100 | 'SERVER_SOFTWARE' => 'Apache 2.2.0', 101 | 'SERVER_PROTOCOL' => 'HTTP/1.1', 102 | ) 103 | value1="value1" 104 | value2="\x8F\xBC\x8D]".dup 105 | value2.force_encoding("SJIS") if defined?(::Encoding) 106 | cgi = CGI.new 107 | session = CGI::Session.new(cgi,"tmpdir"=>@session_dir,"session_id"=>"foo") 108 | session["key1"]=value1 109 | session["key2"]=value2 110 | assert_equal(value1,session["key1"]) 111 | assert_equal(value2,session["key2"]) 112 | assert_equal("foo",session.session_id) 113 | #session_id=session.session_id 114 | session.close 115 | $stdout = StringIO.new 116 | cgi.out{""} 117 | 118 | update_env( 119 | 'REQUEST_METHOD' => 'GET', 120 | # 'HTTP_COOKIE' => "_session_id=#{session_id}", 121 | 'QUERY_STRING' => "_session_id=#{session.session_id}", 122 | 'SERVER_SOFTWARE' => 'Apache 2.2.0', 123 | 'SERVER_PROTOCOL' => 'HTTP/1.1', 124 | ) 125 | cgi = CGI.new 126 | session = CGI::Session.new(cgi,"tmpdir"=>@session_dir) 127 | $stdout = StringIO.new 128 | assert_equal(value1,session["key1"]) 129 | assert_equal(value2,session["key2"]) 130 | assert_equal("foo",session.session_id) 131 | session.close 132 | end 133 | def test_cgi_session_specify_session_key 134 | update_env( 135 | 'REQUEST_METHOD' => 'GET', 136 | # 'QUERY_STRING' => 'id=123&id=456&id=&str=%40h+%3D%7E+%2F%5E%24%2F', 137 | # 'HTTP_COOKIE' => '_session_id=12345; name1=val1&val2;', 138 | 'SERVER_SOFTWARE' => 'Apache 2.2.0', 139 | 'SERVER_PROTOCOL' => 'HTTP/1.1', 140 | ) 141 | value1="value1" 142 | value2="\x8F\xBC\x8D]".dup 143 | value2.force_encoding("SJIS") if defined?(::Encoding) 144 | cgi = CGI.new 145 | session = CGI::Session.new(cgi,"tmpdir"=>@session_dir,"session_key"=>"bar") 146 | session["key1"]=value1 147 | session["key2"]=value2 148 | assert_equal(value1,session["key1"]) 149 | assert_equal(value2,session["key2"]) 150 | session_id=session.session_id 151 | session.close 152 | $stdout = StringIO.new 153 | cgi.out{""} 154 | 155 | update_env( 156 | 'REQUEST_METHOD' => 'GET', 157 | 'HTTP_COOKIE' => "bar=#{session_id}", 158 | # 'QUERY_STRING' => "bar=#{session.session_id}", 159 | 'SERVER_SOFTWARE' => 'Apache 2.2.0', 160 | 'SERVER_PROTOCOL' => 'HTTP/1.1', 161 | ) 162 | cgi = CGI.new 163 | session = CGI::Session.new(cgi,"tmpdir"=>@session_dir,"session_key"=>"bar") 164 | $stdout = StringIO.new 165 | assert_equal(value1,session["key1"]) 166 | assert_equal(value2,session["key2"]) 167 | session.close 168 | end 169 | end 170 | -------------------------------------------------------------------------------- /test/cgi/test_cgi_header.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'test/unit' 3 | require 'cgi' 4 | require 'time' 5 | require_relative 'update_env' 6 | 7 | 8 | class CGIHeaderTest < Test::Unit::TestCase 9 | include UpdateEnv 10 | 11 | 12 | def setup 13 | @environ = {} 14 | update_env( 15 | 'SERVER_PROTOCOL' => 'HTTP/1.1', 16 | 'REQUEST_METHOD' => 'GET', 17 | 'SERVER_SOFTWARE' => 'Apache 2.2.0', 18 | ) 19 | end 20 | 21 | 22 | def teardown 23 | ENV.update(@environ) 24 | end 25 | 26 | 27 | def test_cgi_http_header_simple 28 | cgi = CGI.new 29 | ## default content type 30 | expected = "Content-Type: text/html\r\n\r\n" 31 | actual = cgi.http_header 32 | assert_equal(expected, actual) 33 | ## content type specified as string 34 | expected = "Content-Type: text/xhtml; charset=utf8\r\n\r\n" 35 | actual = cgi.http_header('text/xhtml; charset=utf8') 36 | assert_equal(expected, actual) 37 | ## content type specified as hash 38 | expected = "Content-Type: image/png\r\n\r\n" 39 | actual = cgi.http_header('type'=>'image/png') 40 | assert_equal(expected, actual) 41 | ## charset specified 42 | expected = "Content-Type: text/html; charset=utf8\r\n\r\n" 43 | actual = cgi.http_header('charset'=>'utf8') 44 | assert_equal(expected, actual) 45 | end 46 | 47 | 48 | def test_cgi_http_header_complex 49 | cgi = CGI.new 50 | options = { 51 | 'type' => 'text/xhtml', 52 | 'charset' => 'utf8', 53 | 'status' => 'REDIRECT', 54 | 'server' => 'webrick', 55 | 'connection' => 'close', 56 | 'length' => 123, 57 | 'language' => 'ja', 58 | 'expires' => Time.gm(2000, 1, 23, 12, 34, 56), 59 | 'location' => 'http://www.ruby-lang.org/', 60 | } 61 | expected = "Status: 302 Found\r\n".dup 62 | expected << "Server: webrick\r\n" 63 | expected << "Connection: close\r\n" 64 | expected << "Content-Type: text/xhtml; charset=utf8\r\n" 65 | expected << "Content-Length: 123\r\n" 66 | expected << "Content-Language: ja\r\n" 67 | expected << "Expires: Sun, 23 Jan 2000 12:34:56 GMT\r\n" 68 | expected << "location: http://www.ruby-lang.org/\r\n" 69 | expected << "\r\n" 70 | actual = cgi.http_header(options) 71 | assert_equal(expected, actual) 72 | end 73 | 74 | 75 | def test_cgi_http_header_argerr 76 | cgi = CGI.new 77 | expected = ArgumentError 78 | 79 | assert_raise(expected) do 80 | cgi.http_header(nil) 81 | end 82 | end 83 | 84 | 85 | def test_cgi_http_header_cookie 86 | cgi = CGI.new 87 | cookie1 = CGI::Cookie.new('name1', 'abc', '123') 88 | cookie2 = CGI::Cookie.new('name'=>'name2', 'value'=>'value2', 'secure'=>true) 89 | ctype = "Content-Type: text/html\r\n" 90 | sep = "\r\n" 91 | c1 = "Set-Cookie: name1=abc&123; path=\r\n" 92 | c2 = "Set-Cookie: name2=value2; path=; secure\r\n" 93 | ## CGI::Cookie object 94 | actual = cgi.http_header('cookie'=>cookie1) 95 | expected = ctype + c1 + sep 96 | assert_equal(expected, actual) 97 | ## String 98 | actual = cgi.http_header('cookie'=>cookie2.to_s) 99 | expected = ctype + c2 + sep 100 | assert_equal(expected, actual) 101 | ## Array 102 | actual = cgi.http_header('cookie'=>[cookie1, cookie2]) 103 | expected = ctype + c1 + c2 + sep 104 | assert_equal(expected, actual) 105 | ## Hash 106 | actual = cgi.http_header('cookie'=>{'name1'=>cookie1, 'name2'=>cookie2}) 107 | expected = ctype + c1 + c2 + sep 108 | assert_equal(expected, actual) 109 | end 110 | 111 | 112 | def test_cgi_http_header_output_cookies 113 | cgi = CGI.new 114 | ## output cookies 115 | cookies = [ CGI::Cookie.new('name1', 'abc', '123'), 116 | CGI::Cookie.new('name'=>'name2', 'value'=>'value2', 'secure'=>true), 117 | ] 118 | cgi.instance_variable_set('@output_cookies', cookies) 119 | expected = "Content-Type: text/html; charset=utf8\r\n".dup 120 | expected << "Set-Cookie: name1=abc&123; path=\r\n" 121 | expected << "Set-Cookie: name2=value2; path=; secure\r\n" 122 | expected << "\r\n" 123 | ## header when string 124 | actual = cgi.http_header('text/html; charset=utf8') 125 | assert_equal(expected, actual) 126 | ## _header_for_string 127 | actual = cgi.http_header('type'=>'text/html', 'charset'=>'utf8') 128 | assert_equal(expected, actual) 129 | end 130 | 131 | 132 | def test_cgi_http_header_nph 133 | time_start = Time.now.to_i 134 | cgi = CGI.new 135 | ## 'nph' is true 136 | ENV['SERVER_SOFTWARE'] = 'Apache 2.2.0' 137 | actual1 = cgi.http_header('nph'=>true) 138 | ## when old IIS, NPH-mode is forced 139 | ENV['SERVER_SOFTWARE'] = 'IIS/4.0' 140 | actual2 = cgi.http_header 141 | actual3 = cgi.http_header('status'=>'REDIRECT', 'location'=>'http://www.example.com/') 142 | ## newer IIS doesn't require NPH-mode ## [ruby-dev:30537] 143 | ENV['SERVER_SOFTWARE'] = 'IIS/5.0' 144 | actual4 = cgi.http_header 145 | actual5 = cgi.http_header('status'=>'REDIRECT', 'location'=>'http://www.example.com/') 146 | time_end = Time.now.to_i 147 | date = /^Date: ([A-Z][a-z]{2}, \d{2} [A-Z][a-z]{2} \d{4} \d\d:\d\d:\d\d GMT)\r\n/ 148 | [actual1, actual2, actual3].each do |actual| 149 | assert_match(date, actual) 150 | assert_include(time_start..time_end, date =~ actual && Time.parse($1).to_i) 151 | actual.sub!(date, "Date: DATE_IS_REMOVED\r\n") 152 | end 153 | ## assertion 154 | expected = "HTTP/1.1 200 OK\r\n".dup 155 | expected << "Date: DATE_IS_REMOVED\r\n" 156 | expected << "Server: Apache 2.2.0\r\n" 157 | expected << "Connection: close\r\n" 158 | expected << "Content-Type: text/html\r\n" 159 | expected << "\r\n" 160 | assert_equal(expected, actual1) 161 | expected.sub!(/^Server: .*?\r\n/, "Server: IIS/4.0\r\n") 162 | assert_equal(expected, actual2) 163 | expected.sub!(/^HTTP\/1.1 200 OK\r\n/, "HTTP/1.1 302 Found\r\n") 164 | expected.sub!(/\r\n\r\n/, "\r\nlocation: http://www.example.com/\r\n\r\n") 165 | assert_equal(expected, actual3) 166 | expected = "Content-Type: text/html\r\n".dup 167 | expected << "\r\n" 168 | assert_equal(expected, actual4) 169 | expected = "Status: 302 Found\r\n".dup 170 | expected << "Content-Type: text/html\r\n" 171 | expected << "location: http://www.example.com/\r\n" 172 | expected << "\r\n" 173 | assert_equal(expected, actual5) 174 | ensure 175 | ENV.delete('SERVER_SOFTWARE') 176 | end 177 | 178 | 179 | def test_cgi_http_header_crlf_injection 180 | cgi = CGI.new 181 | assert_raise(RuntimeError) { cgi.http_header("text/xhtml\r\nBOO") } 182 | assert_raise(RuntimeError) { cgi.http_header("type" => "text/xhtml\r\nBOO") } 183 | assert_raise(RuntimeError) { cgi.http_header("status" => "200 OK\r\nBOO") } 184 | assert_raise(RuntimeError) { cgi.http_header("location" => "text/xhtml\r\nBOO") } 185 | end 186 | 187 | 188 | instance_methods.each do |method| 189 | private method if method =~ /^test_(.*)/ && $1 != ENV['TEST'] 190 | end if ENV['TEST'] 191 | 192 | end 193 | -------------------------------------------------------------------------------- /test/cgi/test_cgi_cookie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'test/unit' 3 | require 'cgi' 4 | require 'stringio' 5 | require_relative 'update_env' 6 | 7 | 8 | class CGICookieTest < Test::Unit::TestCase 9 | include UpdateEnv 10 | 11 | 12 | def setup 13 | @environ = {} 14 | update_env( 15 | 'REQUEST_METHOD' => 'GET', 16 | 'SCRIPT_NAME' => nil, 17 | ) 18 | @str1="\xE3\x82\x86\xE3\x82\x93\xE3\x82\x86\xE3\x82\x93".dup 19 | @str1.force_encoding("UTF-8") if defined?(::Encoding) 20 | end 21 | 22 | def teardown 23 | ENV.update(@environ) 24 | end 25 | 26 | 27 | def test_cgi_cookie_new_simple 28 | cookie = CGI::Cookie.new('name1', 'val1', '&<>"', @str1) 29 | assert_equal('name1', cookie.name) 30 | assert_equal(['val1', '&<>"', @str1], cookie.value) 31 | assert_nil(cookie.domain) 32 | assert_nil(cookie.expires) 33 | assert_equal('', cookie.path) 34 | assert_equal(false, cookie.secure) 35 | assert_equal(false, cookie.httponly) 36 | assert_equal("name1=val1&%26%3C%3E%22&%E3%82%86%E3%82%93%E3%82%86%E3%82%93; path=", cookie.to_s) 37 | end 38 | 39 | 40 | def test_cgi_cookie_new_complex 41 | t = Time.gm(2030, 12, 31, 23, 59, 59) 42 | value = ['val1', '&<>"', "\xA5\xE0\xA5\xB9\xA5\xAB".dup] 43 | value[2].force_encoding("EUC-JP") if defined?(::Encoding) 44 | cookie = CGI::Cookie.new('name'=>'name1', 45 | 'value'=>value, 46 | 'path'=>'/cgi-bin/myapp/', 47 | 'domain'=>'www.example.com', 48 | 'expires'=>t, 49 | 'secure'=>true, 50 | 'httponly'=>true 51 | ) 52 | assert_equal('name1', cookie.name) 53 | assert_equal(value, cookie.value) 54 | assert_equal('www.example.com', cookie.domain) 55 | assert_equal(t, cookie.expires) 56 | assert_equal('/cgi-bin/myapp/', cookie.path) 57 | assert_equal(true, cookie.secure) 58 | assert_equal(true, cookie.httponly) 59 | assert_equal('name1=val1&%26%3C%3E%22&%A5%E0%A5%B9%A5%AB; domain=www.example.com; path=/cgi-bin/myapp/; expires=Tue, 31 Dec 2030 23:59:59 GMT; secure; HttpOnly', cookie.to_s) 60 | end 61 | 62 | 63 | def test_cgi_cookie_new_with_domain 64 | h = {'name'=>'name1', 'value'=>'value1'} 65 | cookie = CGI::Cookie.new(h.merge('domain'=>'a.example.com')) 66 | assert_equal('a.example.com', cookie.domain) 67 | 68 | cookie = CGI::Cookie.new(h.merge('domain'=>'.example.com')) 69 | assert_equal('.example.com', cookie.domain) 70 | 71 | cookie = CGI::Cookie.new(h.merge('domain'=>'1.example.com')) 72 | assert_equal('1.example.com', cookie.domain, 'enhanced by RFC 1123') 73 | 74 | assert_raise(ArgumentError) { 75 | CGI::Cookie.new(h.merge('domain'=>'-a.example.com')) 76 | } 77 | 78 | assert_raise(ArgumentError) { 79 | CGI::Cookie.new(h.merge('domain'=>'a-.example.com')) 80 | } 81 | end 82 | 83 | 84 | def test_cgi_cookie_scriptname 85 | cookie = CGI::Cookie.new('name1', 'value1') 86 | assert_equal('', cookie.path) 87 | cookie = CGI::Cookie.new('name'=>'name1', 'value'=>'value1') 88 | assert_equal('', cookie.path) 89 | ## when ENV['SCRIPT_NAME'] is set, cookie.path is set automatically 90 | ENV['SCRIPT_NAME'] = '/cgi-bin/app/example.cgi' 91 | cookie = CGI::Cookie.new('name1', 'value1') 92 | assert_equal('/cgi-bin/app/', cookie.path) 93 | cookie = CGI::Cookie.new('name'=>'name1', 'value'=>'value1') 94 | assert_equal('/cgi-bin/app/', cookie.path) 95 | end 96 | 97 | 98 | def test_cgi_cookie_parse 99 | ## ';' separator 100 | cookie_str = 'name1=val1&val2; name2=val2&%26%3C%3E%22&%E3%82%86%E3%82%93%E3%82%86%E3%82%93;_session_id=12345' 101 | cookies = CGI::Cookie.parse(cookie_str) 102 | list = [ 103 | ['name1', ['val1', 'val2']], 104 | ['name2', ['val2', '&<>"',@str1]], 105 | ['_session_id', ['12345']], 106 | ] 107 | list.each do |name, value| 108 | cookie = cookies[name] 109 | assert_equal(name, cookie.name) 110 | assert_equal(value, cookie.value) 111 | end 112 | ## don't allow ',' separator 113 | cookie_str = 'name1=val1&val2, name2=val2' 114 | cookies = CGI::Cookie.parse(cookie_str) 115 | list = [ 116 | ['name1', ['val1', 'val2, name2=val2']], 117 | ] 118 | list.each do |name, value| 119 | cookie = cookies[name] 120 | assert_equal(name, cookie.name) 121 | assert_equal(value, cookie.value) 122 | end 123 | end 124 | 125 | def test_cgi_cookie_parse_not_decode_name 126 | cookie_str = "%66oo=baz;foo=bar" 127 | cookies = CGI::Cookie.parse(cookie_str) 128 | assert_equal({"%66oo" => ["baz"], "foo" => ["bar"]}, cookies) 129 | end 130 | 131 | def test_cgi_cookie_arrayinterface 132 | cookie = CGI::Cookie.new('name1', 'a', 'b', 'c') 133 | assert_equal('a', cookie[0]) 134 | assert_equal('c', cookie[2]) 135 | assert_nil(cookie[3]) 136 | assert_equal('a', cookie.first) 137 | assert_equal('c', cookie.last) 138 | assert_equal(['A', 'B', 'C'], cookie.collect{|e| e.upcase}) 139 | end 140 | 141 | 142 | def test_cgi_cookie_domain_injection_into_name 143 | name = "a=b; domain=example.com;" 144 | path = "/" 145 | domain = "example.jp" 146 | assert_raise(ArgumentError) do 147 | CGI::Cookie.new('name' => name, 148 | 'value' => "value", 149 | 'domain' => domain, 150 | 'path' => path) 151 | end 152 | end 153 | 154 | 155 | def test_cgi_cookie_newline_injection_into_name 156 | name = "a=b;\r\nLocation: http://example.com#" 157 | path = "/" 158 | domain = "example.jp" 159 | assert_raise(ArgumentError) do 160 | CGI::Cookie.new('name' => name, 161 | 'value' => "value", 162 | 'domain' => domain, 163 | 'path' => path) 164 | end 165 | end 166 | 167 | 168 | def test_cgi_cookie_multibyte_injection_into_name 169 | name = "a=b;\u3042" 170 | path = "/" 171 | domain = "example.jp" 172 | assert_raise(ArgumentError) do 173 | CGI::Cookie.new('name' => name, 174 | 'value' => "value", 175 | 'domain' => domain, 176 | 'path' => path) 177 | end 178 | end 179 | 180 | 181 | def test_cgi_cookie_injection_into_path 182 | name = "name" 183 | path = "/; samesite=none" 184 | domain = "example.jp" 185 | assert_raise(ArgumentError) do 186 | CGI::Cookie.new('name' => name, 187 | 'value' => "value", 188 | 'domain' => domain, 189 | 'path' => path) 190 | end 191 | end 192 | 193 | 194 | def test_cgi_cookie_injection_into_domain 195 | name = "name" 196 | path = "/" 197 | domain = "example.jp; samesite=none" 198 | assert_raise(ArgumentError) do 199 | CGI::Cookie.new('name' => name, 200 | 'value' => "value", 201 | 'domain' => domain, 202 | 'path' => path) 203 | end 204 | end 205 | 206 | 207 | instance_methods.each do |method| 208 | private method if method =~ /^test_(.*)/ && $1 != ENV['TEST'] 209 | end if ENV['TEST'] 210 | 211 | end 212 | -------------------------------------------------------------------------------- /lib/cgi/cookie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require_relative 'util' 3 | class CGI 4 | # Class representing an HTTP cookie. 5 | # 6 | # In addition to its specific fields and methods, a Cookie instance 7 | # is a delegator to the array of its values. 8 | # 9 | # See RFC 2965. 10 | # 11 | # == Examples of use 12 | # cookie1 = CGI::Cookie.new("name", "value1", "value2", ...) 13 | # cookie1 = CGI::Cookie.new("name" => "name", "value" => "value") 14 | # cookie1 = CGI::Cookie.new('name' => 'name', 15 | # 'value' => ['value1', 'value2', ...], 16 | # 'path' => 'path', # optional 17 | # 'domain' => 'domain', # optional 18 | # 'expires' => Time.now, # optional 19 | # 'secure' => true, # optional 20 | # 'httponly' => true # optional 21 | # ) 22 | # 23 | # cgi.out("cookie" => [cookie1, cookie2]) { "string" } 24 | # 25 | # name = cookie1.name 26 | # values = cookie1.value 27 | # path = cookie1.path 28 | # domain = cookie1.domain 29 | # expires = cookie1.expires 30 | # secure = cookie1.secure 31 | # httponly = cookie1.httponly 32 | # 33 | # cookie1.name = 'name' 34 | # cookie1.value = ['value1', 'value2', ...] 35 | # cookie1.path = 'path' 36 | # cookie1.domain = 'domain' 37 | # cookie1.expires = Time.now + 30 38 | # cookie1.secure = true 39 | # cookie1.httponly = true 40 | class Cookie < Array 41 | @@accept_charset="UTF-8" unless defined?(@@accept_charset) 42 | 43 | # :stopdoc: 44 | TOKEN_RE = %r"\A[[!-~]&&[^()<>@,;:\\\"/?=\[\]{}]]+\z" 45 | PATH_VALUE_RE = %r"\A[[ -~]&&[^;]]*\z" 46 | DOMAIN_VALUE_RE = %r"\A\.?(?