Merge pull request #15954 from Bo98/vendor-cleanup

vendor/bundle/ruby: cleanup unneeded files
This commit is contained in:
Mike McQuaid 2023-09-27 09:08:20 +01:00 committed by GitHub
commit e5018531ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
530 changed files with 557 additions and 49151 deletions

View File

@ -22,6 +22,60 @@ permissions:
pull-requests: read
jobs:
check-vendor-version:
if: github.event_name == 'pull_request'
runs-on: ubuntu-22.04
steps:
- name: Set up Homebrew
id: set-up-homebrew
uses: Homebrew/actions/setup-homebrew@master
with:
core: false
cask: false
test-bot: false
- name: Install Bundler RubyGems
run: brew install-bundler-gems --groups=all
- name: Get Ruby ABI version
id: ruby-abi
run: echo "version=$(brew ruby -e "puts Gem.ruby_api_version")" >> "${GITHUB_OUTPUT}"
- name: Get gem info
id: gem-info
working-directory: ${{ steps.set-up-homebrew.outputs.gems-path }}/${{ steps.ruby-abi.outputs.version }}/gems
run: |
{
echo "vendor-version=$(cat ../.homebrew_vendor_version)"
echo "ignored<<EOS"
git check-ignore -- *
echo "EOS"
} >> "${GITHUB_OUTPUT}"
- name: Compare to base ref
working-directory: ${{ steps.set-up-homebrew.outputs.gems-path }}/${{ steps.ruby-abi.outputs.version }}
run: |
git checkout "origin/${GITHUB_BASE_REF}"
rm .homebrew_vendor_version
brew install-bundler-gems --groups=all
if [[ "$(cat .homebrew_vendor_version)" == "${{ steps.gem-info.outputs.vendor-version }}" ]]; then
ignored_gems="${{ steps.gem-info.outputs.ignored }}"
while IFS= read -r gem; do
gem_dir="./gems/${gem}"
[[ -d "${gem_dir}" ]] || continue
exit_code=0
git check-ignore --quiet "${gem_dir}" || exit_code=$?
if (( exit_code != 0 )); then
if (( exit_code == 1 )); then
echo "::error::VENDOR_VERSION needs bumping in utils/gems.rb" >&2
else
echo "::error::git check-ignore failed" >&2
fi
exit "${exit_code}"
fi
done <<< "${ignored_gems}"
fi
vendor-gems:
if: >
github.repository_owner == 'Homebrew' && (

221
.gitignore vendored
View File

@ -27,6 +27,7 @@
**/.bundle/bin
**/.bundle/cache
**/vendor/bundle/ruby/.homebrew_gem_groups
**/vendor/bundle/ruby/*/.homebrew_vendor_version
**/vendor/bundle/ruby/*/bundler.lock
**/vendor/bundle/ruby/*/bin
**/vendor/bundle/ruby/*/build_info/
@ -43,6 +44,7 @@
**/.yardoc
# Unignore vendored gems
!**/vendor/bundle/ruby/*/gems/*/*LICENSE*
!**/vendor/bundle/ruby/*/gems/*/lib
!**/vendor/bundle/ruby/*/gems/addressable-*/data
!**/vendor/bundle/ruby/*/gems/public_suffix-*/data
@ -51,181 +53,30 @@
!**/vendor/bundle/ruby/*/gems/rubocop-rspec-*/config
!**/vendor/bundle/ruby/*/gems/rubocop-sorbet-*/config
# Ignore activesupport files we don't need
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/cache/
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/array.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/array/conversions.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/array/extract.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/array/extract_options.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/array/grouping.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/array/inquiry.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/array/wrap.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/benchmark.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/big_decimal.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/big_decimal/conversions.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/class.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/class/attribute.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/class/attribute_accessors.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/class/subclasses.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/date.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/date/acts_like.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/date/blank.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/date/calculations.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/date/conversions.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/date/zones.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/date_and_time/calculations.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/date_and_time/compatibility.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/date_and_time/zones.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/date_time.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/date_time/acts_like.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/date_time/blank.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/date_time/calculations.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/date_time/compatibility.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/date_time/conversions.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/digest.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/digest/uuid.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/file.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/hash.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/hash/conversions.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/hash/indifferent_access.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/hash/reverse_merge.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/integer.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/integer/inflections.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/integer/multiple.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/integer/time.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/kernel.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/kernel/concern.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/kernel/reporting.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/kernel/singleton_class.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/load_error.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/marshal.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/module.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/name_error.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/numeric.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/object.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/object/acts_like.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/object/conversions.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/object/inclusion.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/object/instance_variables.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/object/json.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/object/to_param.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/object/to_query.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/object/with_options.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/range.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/range/compare_range.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/range/conversions.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/range/each.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/range/include_time_with_zone.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/range/overlaps.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/regexp.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/securerandom.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/string.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/string/access.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/string/behavior.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/string/conversions.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/string/inquiry.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/string/output_safety.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/string/starts_ends_with.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/string/strip.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/string/zones.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/symbol.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/symbol/starts_ends_with.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/time.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/time/acts_like.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/time/calculations.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/time/compatibility.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/time/conversions.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/time/zones.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/uri.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/concurrency/
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/current_attributes/
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/dependencies/
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/deprecation/
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/duration/
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/json/
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/locale/
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/log_subscriber/
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/messages/
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/multibyte/
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/notifications/
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/number_helper/
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/testing/
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/values/
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/xml_mini/
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/actionable_error.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/all.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/array_inquirer.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/backtrace_cleaner.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/benchmarkable.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/builder.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/cache.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/callbacks.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/concern.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/configurable.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/configuration_file.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/current_attributes.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/dependencies.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/deprecation.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/descendants_tracker.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/digest.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/duration.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/encrypted_configuration.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/encrypted_file.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/environment_inquirer.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/evented_file_update_checker.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/execution_wrapper.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/executor.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/file_update_checker.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/fork_tracker.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/gem_version.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/gzip.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/hash_with_indifferent_access.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/i18n_railtie.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/json.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/key_generator.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/log_subscriber.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/logger.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/logger_silence.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/logger_thread_safe_level.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/message_encryptor.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/message_verifier.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/notifications.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/number_helper.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/option_merger.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/ordered_hash.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/ordered_options.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/parameter_filter.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/per_thread_registry.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/proxy_object.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/rails.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/railtie.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/reloader.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/rescuable.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/secure_compare_rotator.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/security_utils.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/string_inquirer.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/subscriber.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/tagged_logging.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/test_case.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/time.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/time_with_zone.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/version.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/xml_mini.rb
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support.rb
# Ignore activesupport, except the ones we need.
**/vendor/bundle/ruby/*/gems/activesupport-*/lib/**/*
!**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/
!**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/
!**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/*/
!**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/array/access.rb
!**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/enumerable.rb
!**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/file/atomic.rb
!**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/hash/deep_merge.rb
!**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/hash/deep_transform_values.rb
!**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/hash/except.rb
!**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/hash/keys.rb
!**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/hash/slice.rb
!**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/object/blank.rb
!**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/object/deep_dup.rb
!**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/object/duplicable.rb
!**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/object/try.rb
!**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/string/exclude.rb
!**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/string/filters.rb
!**/vendor/bundle/ruby/*/gems/activesupport-*/lib/active_support/core_ext/string/indent.rb
# Ignore partially included gems where we don't need all files
**/vendor/bundle/ruby/*/gems/concurrent-ruby-*/lib/atomic/
**/vendor/bundle/ruby/*/gems/concurrent-ruby-*/lib/atomic_reference/
**/vendor/bundle/ruby/*/gems/concurrent-ruby-*/lib/collection/
**/vendor/bundle/ruby/*/gems/concurrent-ruby-*/lib/concern/
**/vendor/bundle/ruby/*/gems/concurrent-ruby-*/lib/executor/
**/vendor/bundle/ruby/*/gems/concurrent-ruby-*/lib/synchronization/
**/vendor/bundle/ruby/*/gems/concurrent-ruby-*/lib/thread_safe/
**/vendor/bundle/ruby/*/gems/concurrent-ruby-*/lib/utility/
**/vendor/bundle/ruby/*/gems/concurrent-ruby-*/lib/*/*.jar
**/vendor/bundle/ruby/*/gems/i18n-*/lib/i18n/tests*
**/vendor/bundle/ruby/*/gems/thread_safe-*/lib/thread_safe/util
**/vendor/gems/mechanize-*/.*
**/vendor/gems/mechanize-*/*.md
**/vendor/gems/mechanize-*/*.rdoc
@ -233,21 +84,11 @@
**/vendor/gems/mechanize-*/Gemfile
**/vendor/gems/mechanize-*/Rakefile
**/vendor/gems/mechanize-*/examples/
**/vendor/gems/mechanize-*/lib/*.rb
**/vendor/gems/mechanize-*/lib/*.rb
**/vendor/gems/mechanize-*/lib/mechanize/http/agent.rb
**/vendor/gems/mechanize-*/lib/mechanize/http/*auth*.rb
**/vendor/gems/mechanize-*/lib/mechanize/c*
**/vendor/gems/mechanize-*/lib/mechanize/d*
**/vendor/gems/mechanize-*/lib/mechanize/e*
**/vendor/gems/mechanize-*/lib/mechanize/f*
**/vendor/gems/mechanize-*/lib/mechanize/h*.rb
**/vendor/gems/mechanize-*/lib/mechanize/i*
**/vendor/gems/mechanize-*/lib/mechanize/p*
**/vendor/gems/mechanize-*/lib/mechanize/r*
**/vendor/gems/mechanize-*/lib/mechanize/t*
**/vendor/gems/mechanize-*/lib/mechanize/u*
**/vendor/gems/mechanize-*/lib/mechanize/x*
**/vendor/gems/mechanize-*/lib/**/*
!**/vendor/gems/mechanize-*/lib/mechanize/
!**/vendor/gems/mechanize-*/lib/mechanize/http/
!**/vendor/gems/mechanize-*/lib/mechanize/http/content_disposition_parser.rb
!**/vendor/gems/mechanize-*/lib/mechanize/version.rb
**/vendor/gems/mechanize-*/test/
# Ignore dependencies we don't wish to vendor
@ -281,6 +122,7 @@
**/vendor/bundle/ruby/*/gems/psych-*/
**/vendor/bundle/ruby/*/gems/pry-*/
**/vendor/bundle/ruby/*/gems/racc-*/
**/vendor/bundle/ruby/*/gems/rack-*/
**/vendor/bundle/ruby/*/gems/rainbow-*/
**/vendor/bundle/ruby/*/gems/rbi-*/
**/vendor/bundle/ruby/*/gems/rdiscount-*/
@ -299,9 +141,16 @@
**/vendor/bundle/ruby/*/gems/rspec-wait-*/
**/vendor/bundle/ruby/*/gems/rubocop-1*/
**/vendor/bundle/ruby/*/gems/rubocop-ast-*/
**/vendor/bundle/ruby/*/gems/rubocop-capybara-*/
**/vendor/bundle/ruby/*/gems/rubocop-performance-*/
**/vendor/bundle/ruby/*/gems/rubocop-rails-*/
**/vendor/bundle/ruby/*/gems/rubocop-rspec-*/
**/vendor/bundle/ruby/*/gems/rubocop-sorbet-*/
**/vendor/bundle/ruby/*/gems/ruby-prof-*/
**/vendor/bundle/ruby/*/gems/ruby-progressbar-*/
**/vendor/bundle/ruby/*/gems/simplecov-*/
**/vendor/bundle/ruby/*/gems/simplecov-html-*/
**/vendor/bundle/ruby/*/gems/simplecov_json_formatter-*/
**/vendor/bundle/ruby/*/gems/simpleidn-*/
**/vendor/bundle/ruby/*/gems/sorbet-*/
!**/vendor/bundle/ruby/*/gems/sorbet-runtime-*/

View File

@ -7,7 +7,6 @@ require "livecheck/livecheck_version"
require "livecheck/skip_conditions"
require "livecheck/strategy"
require "addressable"
require "ruby-progressbar"
require "uri"
module Homebrew
@ -208,6 +207,7 @@ module Homebrew
stderr.puts Formatter.headline("Running checks", color: :blue)
end
require "ruby-progressbar"
progress = ProgressBar.create(
total: formulae_and_casks_total,
progress_mark: "#",

View File

@ -46,3 +46,5 @@ module Kernel
sig { params(arg0: NilClass).returns(NilClass) }
def set_trace_func(arg0); end
end
class Gem::Security::Exception < Gem::Exception; end

View File

@ -12,9 +12,24 @@ module Homebrew
# After updating this, run `brew vendor-gems --update=--bundler`.
HOMEBREW_BUNDLER_VERSION = "2.4.18"
GEM_GROUPS_FILE = (HOMEBREW_LIBRARY_PATH/"vendor/bundle/ruby/.homebrew_gem_groups").freeze
# Bump this whenever a committed vendored gem is later added to gitignore.
# This will trigger it to reinstall properly if `brew install-bundler-gems` needs it.
VENDOR_VERSION = 1
private_constant :VENDOR_VERSION
RUBY_BUNDLE_VENDOR_DIRECTORY = (HOMEBREW_LIBRARY_PATH/"vendor/bundle/ruby").freeze
private_constant :RUBY_BUNDLE_VENDOR_DIRECTORY
# This is tracked across Ruby versions.
GEM_GROUPS_FILE = (RUBY_BUNDLE_VENDOR_DIRECTORY/".homebrew_gem_groups").freeze
private_constant :GEM_GROUPS_FILE
# This is tracked per Ruby version.
VENDOR_VERSION_FILE = (
RUBY_BUNDLE_VENDOR_DIRECTORY/"#{RbConfig::CONFIG["ruby_version"]}/.homebrew_vendor_version"
).freeze
private_constant :VENDOR_VERSION_FILE
module_function
# @api private
@ -22,6 +37,11 @@ module Homebrew
File.join(ENV.fetch("HOMEBREW_LIBRARY"), "Homebrew", "Gemfile")
end
# @api private
def bundler_definition
@bundler_definition ||= Bundler::Definition.build(Bundler.default_gemfile, Bundler.default_lockfile, false)
end
# @api private
def valid_gem_groups
install_bundler!
@ -29,7 +49,7 @@ module Homebrew
Bundler.with_unbundled_env do
ENV["BUNDLE_GEMFILE"] = gemfile
groups = Bundler::Definition.build(Bundler.default_gemfile, Bundler.default_lockfile, false).groups
groups = bundler_definition.groups
groups.delete(:default)
groups.map(&:to_s)
end
@ -71,7 +91,7 @@ module Homebrew
ENV["BUNDLER_NO_OLD_RUBYGEMS_WARNING"] = "1"
# Match where our bundler gems are.
gem_home = "#{HOMEBREW_LIBRARY_PATH}/vendor/bundle/ruby/#{RbConfig::CONFIG["ruby_version"]}"
gem_home = "#{RUBY_BUNDLE_VENDOR_DIRECTORY}/#{RbConfig::CONFIG["ruby_version"]}"
Gem.paths = {
"GEM_HOME" => gem_home,
"GEM_PATH" => gem_home,
@ -181,6 +201,14 @@ module Homebrew
end
end
def user_vendor_version
@user_vendor_version ||= if VENDOR_VERSION_FILE.exist?
VENDOR_VERSION_FILE.read.to_i
else
0
end
end
def install_bundler_gems!(only_warn_on_failure: false, setup_path: true, groups: [])
old_path = ENV.fetch("PATH", nil)
old_gem_path = ENV.fetch("GEM_PATH", nil)
@ -229,7 +257,49 @@ module Homebrew
bundle_check_failed = !$CHILD_STATUS.success?
# for some reason sometimes the exit code lies so check the output too.
bundle_installed = if bundle_check_failed || bundle_check_output.include?("Install missing gems")
bundle_install_required = bundle_check_failed || bundle_check_output.include?("Install missing gems")
if user_vendor_version != VENDOR_VERSION
# Check if the install is intact. This is useful if any gems are added to gitignore.
# We intentionally map over everything and then call `any?` so that we remove the spec of each bad gem.
specs = bundler_definition.resolve.materialize(bundler_definition.locked_dependencies)
vendor_reinstall_required = specs.map do |spec|
spec_file = "#{Gem.dir}/specifications/#{spec.full_name}.gemspec"
next false unless File.exist?(spec_file)
cache_file = "#{Gem.dir}/cache/#{spec.full_name}.gem"
if File.exist?(cache_file)
require "rubygems/package"
package = Gem::Package.new(cache_file)
package_install_intact = begin
contents = package.contents
# If the gem has contents, ensure we have every file installed it contains.
contents&.all? do |gem_file|
File.exist?("#{Gem.dir}/gems/#{spec.full_name}/#{gem_file}")
end
rescue Gem::Package::Error, Gem::Security::Exception
# Malformed, assume broken
File.unlink(cache_file)
false
end
next false if package_install_intact
end
# Mark gem for reinstallation
File.unlink(spec_file)
true
end.any?
VENDOR_VERSION_FILE.dirname.mkpath
VENDOR_VERSION_FILE.write(VENDOR_VERSION.to_s)
bundle_install_required ||= vendor_reinstall_required
end
bundle_installed = if bundle_install_required
if system bundle, "install"
true
else

View File

@ -0,0 +1,20 @@
Copyright (c) 2005-2022 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,31 +0,0 @@
# frozen_string_literal: true
class Module
# Allows you to make aliases for attributes, which includes
# getter, setter, and a predicate.
#
# class Content < ActiveRecord::Base
# # has a title attribute
# end
#
# class Email < Content
# alias_attribute :subject, :title
# end
#
# e = Email.find(1)
# e.title # => "Superstars"
# e.subject # => "Superstars"
# e.subject? # => true
# e.subject = "Megastars"
# e.title # => "Megastars"
def alias_attribute(new_name, old_name)
# The following reader methods use an explicit `self` receiver in order to
# support aliases that start with an uppercase letter. Otherwise, they would
# be resolved as constants instead.
module_eval <<-STR, __FILE__, __LINE__ + 1
def #{new_name}; self.#{old_name}; end # def subject; self.title; end
def #{new_name}?; self.#{old_name}?; end # def subject?; self.title?; end
def #{new_name}=(v); self.#{old_name} = v; end # def subject=(v); self.title = v; end
STR
end
end

View File

@ -1,30 +0,0 @@
# frozen_string_literal: true
class Module
# A module may or may not have a name.
#
# module M; end
# M.name # => "M"
#
# m = Module.new
# m.name # => nil
#
# +anonymous?+ method returns true if module does not have a name, false otherwise:
#
# Module.new.anonymous? # => true
#
# module M; end
# M.anonymous? # => false
#
# A module gets a name when it is first assigned to a constant. Either
# via the +module+ or +class+ keyword or by an explicit assignment:
#
# m = Module.new # creates an anonymous module
# m.anonymous? # => true
# M = m # m gets a name here as a side-effect
# m.name # => "M"
# m.anonymous? # => false
def anonymous?
name.nil?
end
end

View File

@ -1,38 +0,0 @@
# frozen_string_literal: true
class Module
# Declares an attribute reader backed by an internally-named instance variable.
def attr_internal_reader(*attrs)
attrs.each { |attr_name| attr_internal_define(attr_name, :reader) }
end
# Declares an attribute writer backed by an internally-named instance variable.
def attr_internal_writer(*attrs)
attrs.each { |attr_name| attr_internal_define(attr_name, :writer) }
end
# Declares an attribute reader and writer backed by an internally-named instance
# variable.
def attr_internal_accessor(*attrs)
attr_internal_reader(*attrs)
attr_internal_writer(*attrs)
end
alias_method :attr_internal, :attr_internal_accessor
class << self; attr_accessor :attr_internal_naming_format end
self.attr_internal_naming_format = "@_%s"
private
def attr_internal_ivar_name(attr)
Module.attr_internal_naming_format % attr
end
def attr_internal_define(attr_name, type)
internal_name = attr_internal_ivar_name(attr_name).delete_prefix("@")
# use native attr_* methods as they are faster on some Ruby implementations
public_send("attr_#{type}", internal_name)
attr_name, internal_name = "#{attr_name}=", "#{internal_name}=" if type == :writer
alias_method attr_name, internal_name
remove_method internal_name
end
end

View File

@ -1,206 +0,0 @@
# frozen_string_literal: true
# Extends the module object with class/module and instance accessors for
# class/module attributes, just like the native attr* accessors for instance
# attributes.
class Module
# Defines a class attribute and creates a class and instance reader methods.
# The underlying class variable is set to +nil+, if it is not previously
# defined. All class and instance methods created will be public, even if
# this method is called with a private or protected access modifier.
#
# module HairColors
# mattr_reader :hair_colors
# end
#
# HairColors.hair_colors # => nil
# HairColors.class_variable_set("@@hair_colors", [:brown, :black])
# HairColors.hair_colors # => [:brown, :black]
#
# The attribute name must be a valid method name in Ruby.
#
# module Foo
# mattr_reader :"1_Badname"
# end
# # => NameError: invalid attribute name: 1_Badname
#
# To omit the instance reader method, pass
# <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>.
#
# module HairColors
# mattr_reader :hair_colors, instance_reader: false
# end
#
# class Person
# include HairColors
# end
#
# Person.new.hair_colors # => NoMethodError
#
# You can set a default value for the attribute.
#
# module HairColors
# mattr_reader :hair_colors, default: [:brown, :black, :blonde, :red]
# end
#
# class Person
# include HairColors
# end
#
# Person.new.hair_colors # => [:brown, :black, :blonde, :red]
def mattr_reader(*syms, instance_reader: true, instance_accessor: true, default: nil, location: nil)
raise TypeError, "module attributes should be defined directly on class, not singleton" if singleton_class?
location ||= caller_locations(1, 1).first
definition = []
syms.each do |sym|
raise NameError.new("invalid attribute name: #{sym}") unless /\A[_A-Za-z]\w*\z/.match?(sym)
definition << "def self.#{sym}; @@#{sym}; end"
if instance_reader && instance_accessor
definition << "def #{sym}; @@#{sym}; end"
end
sym_default_value = (block_given? && default.nil?) ? yield : default
class_variable_set("@@#{sym}", sym_default_value) unless sym_default_value.nil? && class_variable_defined?("@@#{sym}")
end
module_eval(definition.join(";"), location.path, location.lineno)
end
alias :cattr_reader :mattr_reader
# Defines a class attribute and creates a class and instance writer methods to
# allow assignment to the attribute. All class and instance methods created
# will be public, even if this method is called with a private or protected
# access modifier.
#
# module HairColors
# mattr_writer :hair_colors
# end
#
# class Person
# include HairColors
# end
#
# HairColors.hair_colors = [:brown, :black]
# Person.class_variable_get("@@hair_colors") # => [:brown, :black]
# Person.new.hair_colors = [:blonde, :red]
# HairColors.class_variable_get("@@hair_colors") # => [:blonde, :red]
#
# To omit the instance writer method, pass
# <tt>instance_writer: false</tt> or <tt>instance_accessor: false</tt>.
#
# module HairColors
# mattr_writer :hair_colors, instance_writer: false
# end
#
# class Person
# include HairColors
# end
#
# Person.new.hair_colors = [:blonde, :red] # => NoMethodError
#
# You can set a default value for the attribute.
#
# module HairColors
# mattr_writer :hair_colors, default: [:brown, :black, :blonde, :red]
# end
#
# class Person
# include HairColors
# end
#
# Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red]
def mattr_writer(*syms, instance_writer: true, instance_accessor: true, default: nil, location: nil)
raise TypeError, "module attributes should be defined directly on class, not singleton" if singleton_class?
location ||= caller_locations(1, 1).first
definition = []
syms.each do |sym|
raise NameError.new("invalid attribute name: #{sym}") unless /\A[_A-Za-z]\w*\z/.match?(sym)
definition << "def self.#{sym}=(val); @@#{sym} = val; end"
if instance_writer && instance_accessor
definition << "def #{sym}=(val); @@#{sym} = val; end"
end
sym_default_value = (block_given? && default.nil?) ? yield : default
class_variable_set("@@#{sym}", sym_default_value) unless sym_default_value.nil? && class_variable_defined?("@@#{sym}")
end
module_eval(definition.join(";"), location.path, location.lineno)
end
alias :cattr_writer :mattr_writer
# Defines both class and instance accessors for class attributes.
# All class and instance methods created will be public, even if
# this method is called with a private or protected access modifier.
#
# module HairColors
# mattr_accessor :hair_colors
# end
#
# class Person
# include HairColors
# end
#
# HairColors.hair_colors = [:brown, :black, :blonde, :red]
# HairColors.hair_colors # => [:brown, :black, :blonde, :red]
# Person.new.hair_colors # => [:brown, :black, :blonde, :red]
#
# If a subclass changes the value then that would also change the value for
# parent class. Similarly if parent class changes the value then that would
# change the value of subclasses too.
#
# class Citizen < Person
# end
#
# Citizen.new.hair_colors << :blue
# Person.new.hair_colors # => [:brown, :black, :blonde, :red, :blue]
#
# To omit the instance writer method, pass <tt>instance_writer: false</tt>.
# To omit the instance reader method, pass <tt>instance_reader: false</tt>.
#
# module HairColors
# mattr_accessor :hair_colors, instance_writer: false, instance_reader: false
# end
#
# class Person
# include HairColors
# end
#
# Person.new.hair_colors = [:brown] # => NoMethodError
# Person.new.hair_colors # => NoMethodError
#
# Or pass <tt>instance_accessor: false</tt>, to omit both instance methods.
#
# module HairColors
# mattr_accessor :hair_colors, instance_accessor: false
# end
#
# class Person
# include HairColors
# end
#
# Person.new.hair_colors = [:brown] # => NoMethodError
# Person.new.hair_colors # => NoMethodError
#
# You can set a default value for the attribute.
#
# module HairColors
# mattr_accessor :hair_colors, default: [:brown, :black, :blonde, :red]
# end
#
# class Person
# include HairColors
# end
#
# Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red]
def mattr_accessor(*syms, instance_reader: true, instance_writer: true, instance_accessor: true, default: nil, &blk)
location = caller_locations(1, 1).first
mattr_reader(*syms, instance_reader: instance_reader, instance_accessor: instance_accessor, default: default, location: location, &blk)
mattr_writer(*syms, instance_writer: instance_writer, instance_accessor: instance_accessor, default: default, location: location)
end
alias :cattr_accessor :mattr_accessor
end

View File

@ -1,148 +0,0 @@
# frozen_string_literal: true
# Extends the module object with class/module and instance accessors for
# class/module attributes, just like the native attr* accessors for instance
# attributes, but does so on a per-thread basis.
#
# So the values are scoped within the Thread.current space under the class name
# of the module.
class Module
# Defines a per-thread class attribute and creates class and instance reader methods.
# The underlying per-thread class variable is set to +nil+, if it is not previously defined.
#
# module Current
# thread_mattr_reader :user
# end
#
# Current.user # => nil
# Thread.current[:attr_Current_user] = "DHH"
# Current.user # => "DHH"
#
# The attribute name must be a valid method name in Ruby.
#
# module Foo
# thread_mattr_reader :"1_Badname"
# end
# # => NameError: invalid attribute name: 1_Badname
#
# To omit the instance reader method, pass
# <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>.
#
# class Current
# thread_mattr_reader :user, instance_reader: false
# end
#
# Current.new.user # => NoMethodError
def thread_mattr_reader(*syms, instance_reader: true, instance_accessor: true, default: nil) # :nodoc:
syms.each do |sym|
raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym)
# The following generated method concatenates `name` because we want it
# to work with inheritance via polymorphism.
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
def self.#{sym}
Thread.current["attr_" + name + "_#{sym}"]
end
EOS
if instance_reader && instance_accessor
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
def #{sym}
self.class.#{sym}
end
EOS
end
Thread.current["attr_" + name + "_#{sym}"] = default unless default.nil?
end
end
alias :thread_cattr_reader :thread_mattr_reader
# Defines a per-thread class attribute and creates a class and instance writer methods to
# allow assignment to the attribute.
#
# module Current
# thread_mattr_writer :user
# end
#
# Current.user = "DHH"
# Thread.current[:attr_Current_user] # => "DHH"
#
# To omit the instance writer method, pass
# <tt>instance_writer: false</tt> or <tt>instance_accessor: false</tt>.
#
# class Current
# thread_mattr_writer :user, instance_writer: false
# end
#
# Current.new.user = "DHH" # => NoMethodError
def thread_mattr_writer(*syms, instance_writer: true, instance_accessor: true, default: nil) # :nodoc:
syms.each do |sym|
raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym)
# The following generated method concatenates `name` because we want it
# to work with inheritance via polymorphism.
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
def self.#{sym}=(obj)
Thread.current["attr_" + name + "_#{sym}"] = obj
end
EOS
if instance_writer && instance_accessor
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
def #{sym}=(obj)
self.class.#{sym} = obj
end
EOS
end
public_send("#{sym}=", default) unless default.nil?
end
end
alias :thread_cattr_writer :thread_mattr_writer
# Defines both class and instance accessors for class attributes.
#
# class Account
# thread_mattr_accessor :user
# end
#
# Account.user = "DHH"
# Account.user # => "DHH"
# Account.new.user # => "DHH"
#
# If a subclass changes the value, the parent class' value is not changed.
# Similarly, if the parent class changes the value, the value of subclasses
# is not changed.
#
# class Customer < Account
# end
#
# Customer.user = "Rafael"
# Customer.user # => "Rafael"
# Account.user # => "DHH"
#
# To omit the instance writer method, pass <tt>instance_writer: false</tt>.
# To omit the instance reader method, pass <tt>instance_reader: false</tt>.
#
# class Current
# thread_mattr_accessor :user, instance_writer: false, instance_reader: false
# end
#
# Current.new.user = "DHH" # => NoMethodError
# Current.new.user # => NoMethodError
#
# Or pass <tt>instance_accessor: false</tt>, to omit both instance methods.
#
# class Current
# thread_mattr_accessor :user, instance_accessor: false
# end
#
# Current.new.user = "DHH" # => NoMethodError
# Current.new.user # => NoMethodError
def thread_mattr_accessor(*syms, instance_reader: true, instance_writer: true, instance_accessor: true, default: nil)
thread_mattr_reader(*syms, instance_reader: instance_reader, instance_accessor: instance_accessor, default: default)
thread_mattr_writer(*syms, instance_writer: instance_writer, instance_accessor: instance_accessor)
end
alias :thread_cattr_accessor :thread_mattr_accessor
end

View File

@ -1,140 +0,0 @@
# frozen_string_literal: true
require "active_support/concern"
class Module
# = Bite-sized separation of concerns
#
# We often find ourselves with a medium-sized chunk of behavior that we'd
# like to extract, but only mix in to a single class.
#
# Extracting a plain old Ruby object to encapsulate it and collaborate or
# delegate to the original object is often a good choice, but when there's
# no additional state to encapsulate or we're making DSL-style declarations
# about the parent class, introducing new collaborators can obfuscate rather
# than simplify.
#
# The typical route is to just dump everything in a monolithic class, perhaps
# with a comment, as a least-bad alternative. Using modules in separate files
# means tedious sifting to get a big-picture view.
#
# = Dissatisfying ways to separate small concerns
#
# == Using comments:
#
# class Todo < ApplicationRecord
# # Other todo implementation
# # ...
#
# ## Event tracking
# has_many :events
#
# before_create :track_creation
#
# private
# def track_creation
# # ...
# end
# end
#
# == With an inline module:
#
# Noisy syntax.
#
# class Todo < ApplicationRecord
# # Other todo implementation
# # ...
#
# module EventTracking
# extend ActiveSupport::Concern
#
# included do
# has_many :events
# before_create :track_creation
# end
#
# private
# def track_creation
# # ...
# end
# end
# include EventTracking
# end
#
# == Mix-in noise exiled to its own file:
#
# Once our chunk of behavior starts pushing the scroll-to-understand-it
# boundary, we give in and move it to a separate file. At this size, the
# increased overhead can be a reasonable tradeoff even if it reduces our
# at-a-glance perception of how things work.
#
# class Todo < ApplicationRecord
# # Other todo implementation
# # ...
#
# include TodoEventTracking
# end
#
# = Introducing Module#concerning
#
# By quieting the mix-in noise, we arrive at a natural, low-ceremony way to
# separate bite-sized concerns.
#
# class Todo < ApplicationRecord
# # Other todo implementation
# # ...
#
# concerning :EventTracking do
# included do
# has_many :events
# before_create :track_creation
# end
#
# private
# def track_creation
# # ...
# end
# end
# end
#
# Todo.ancestors
# # => [Todo, Todo::EventTracking, ApplicationRecord, Object]
#
# This small step has some wonderful ripple effects. We can
# * grok the behavior of our class in one glance,
# * clean up monolithic junk-drawer classes by separating their concerns, and
# * stop leaning on protected/private for crude "this is internal stuff" modularity.
#
# === Prepending concerning
#
# <tt>concerning</tt> supports a <tt>prepend: true</tt> argument which will <tt>prepend</tt> the
# concern instead of using <tt>include</tt> for it.
module Concerning
# Define a new concern and mix it in.
def concerning(topic, prepend: false, &block)
method = prepend ? :prepend : :include
__send__(method, concern(topic, &block))
end
# A low-cruft shortcut to define a concern.
#
# concern :EventTracking do
# ...
# end
#
# is equivalent to
#
# module EventTracking
# extend ActiveSupport::Concern
#
# ...
# end
def concern(topic, &module_definition)
const_set topic, Module.new {
extend ::ActiveSupport::Concern
module_eval(&module_definition)
}
end
end
include Concerning
end

View File

@ -1,330 +0,0 @@
# frozen_string_literal: true
require "set"
class Module
# Error generated by +delegate+ when a method is called on +nil+ and +allow_nil+
# option is not used.
class DelegationError < NoMethodError; end
RUBY_RESERVED_KEYWORDS = %w(alias and BEGIN begin break case class def defined? do
else elsif END end ensure false for if in module next nil not or redo rescue retry
return self super then true undef unless until when while yield)
DELEGATION_RESERVED_KEYWORDS = %w(_ arg args block)
DELEGATION_RESERVED_METHOD_NAMES = Set.new(
RUBY_RESERVED_KEYWORDS + DELEGATION_RESERVED_KEYWORDS
).freeze
# Provides a +delegate+ class method to easily expose contained objects'
# public methods as your own.
#
# ==== Options
# * <tt>:to</tt> - Specifies the target object name as a symbol or string
# * <tt>:prefix</tt> - Prefixes the new method with the target name or a custom prefix
# * <tt>:allow_nil</tt> - If set to true, prevents a +Module::DelegationError+
# from being raised
# * <tt>:private</tt> - If set to true, changes method visibility to private
#
# The macro receives one or more method names (specified as symbols or
# strings) and the name of the target object via the <tt>:to</tt> option
# (also a symbol or string).
#
# Delegation is particularly useful with Active Record associations:
#
# class Greeter < ActiveRecord::Base
# def hello
# 'hello'
# end
#
# def goodbye
# 'goodbye'
# end
# end
#
# class Foo < ActiveRecord::Base
# belongs_to :greeter
# delegate :hello, to: :greeter
# end
#
# Foo.new.hello # => "hello"
# Foo.new.goodbye # => NoMethodError: undefined method `goodbye' for #<Foo:0x1af30c>
#
# Multiple delegates to the same target are allowed:
#
# class Foo < ActiveRecord::Base
# belongs_to :greeter
# delegate :hello, :goodbye, to: :greeter
# end
#
# Foo.new.goodbye # => "goodbye"
#
# Methods can be delegated to instance variables, class variables, or constants
# by providing them as a symbols:
#
# class Foo
# CONSTANT_ARRAY = [0,1,2,3]
# @@class_array = [4,5,6,7]
#
# def initialize
# @instance_array = [8,9,10,11]
# end
# delegate :sum, to: :CONSTANT_ARRAY
# delegate :min, to: :@@class_array
# delegate :max, to: :@instance_array
# end
#
# Foo.new.sum # => 6
# Foo.new.min # => 4
# Foo.new.max # => 11
#
# It's also possible to delegate a method to the class by using +:class+:
#
# class Foo
# def self.hello
# "world"
# end
#
# delegate :hello, to: :class
# end
#
# Foo.new.hello # => "world"
#
# Delegates can optionally be prefixed using the <tt>:prefix</tt> option. If the value
# is <tt>true</tt>, the delegate methods are prefixed with the name of the object being
# delegated to.
#
# Person = Struct.new(:name, :address)
#
# class Invoice < Struct.new(:client)
# delegate :name, :address, to: :client, prefix: true
# end
#
# john_doe = Person.new('John Doe', 'Vimmersvej 13')
# invoice = Invoice.new(john_doe)
# invoice.client_name # => "John Doe"
# invoice.client_address # => "Vimmersvej 13"
#
# It is also possible to supply a custom prefix.
#
# class Invoice < Struct.new(:client)
# delegate :name, :address, to: :client, prefix: :customer
# end
#
# invoice = Invoice.new(john_doe)
# invoice.customer_name # => 'John Doe'
# invoice.customer_address # => 'Vimmersvej 13'
#
# The delegated methods are public by default.
# Pass <tt>private: true</tt> to change that.
#
# class User < ActiveRecord::Base
# has_one :profile
# delegate :first_name, to: :profile
# delegate :date_of_birth, to: :profile, private: true
#
# def age
# Date.today.year - date_of_birth.year
# end
# end
#
# User.new.first_name # => "Tomas"
# User.new.date_of_birth # => NoMethodError: private method `date_of_birth' called for #<User:0x00000008221340>
# User.new.age # => 2
#
# If the target is +nil+ and does not respond to the delegated method a
# +Module::DelegationError+ is raised. If you wish to instead return +nil+,
# use the <tt>:allow_nil</tt> option.
#
# class User < ActiveRecord::Base
# has_one :profile
# delegate :age, to: :profile
# end
#
# User.new.age
# # => Module::DelegationError: User#age delegated to profile.age, but profile is nil
#
# But if not having a profile yet is fine and should not be an error
# condition:
#
# class User < ActiveRecord::Base
# has_one :profile
# delegate :age, to: :profile, allow_nil: true
# end
#
# User.new.age # nil
#
# Note that if the target is not +nil+ then the call is attempted regardless of the
# <tt>:allow_nil</tt> option, and thus an exception is still raised if said object
# does not respond to the method:
#
# class Foo
# def initialize(bar)
# @bar = bar
# end
#
# delegate :name, to: :@bar, allow_nil: true
# end
#
# Foo.new("Bar").name # raises NoMethodError: undefined method `name'
#
# The target method must be public, otherwise it will raise +NoMethodError+.
def delegate(*methods, to: nil, prefix: nil, allow_nil: nil, private: nil)
unless to
raise ArgumentError, "Delegation needs a target. Supply a keyword argument 'to' (e.g. delegate :hello, to: :greeter)."
end
if prefix == true && /^[^a-z_]/.match?(to)
raise ArgumentError, "Can only automatically set the delegation prefix when delegating to a method."
end
method_prefix = \
if prefix
"#{prefix == true ? to : prefix}_"
else
""
end
location = caller_locations(1, 1).first
file, line = location.path, location.lineno
to = to.to_s
to = "self.#{to}" if DELEGATION_RESERVED_METHOD_NAMES.include?(to)
method_def = []
method_names = []
methods.map do |method|
method_name = prefix ? "#{method_prefix}#{method}" : method
method_names << method_name.to_sym
# Attribute writer methods only accept one argument. Makes sure []=
# methods still accept two arguments.
definition = if /[^\]]=$/.match?(method)
"arg"
elsif RUBY_VERSION >= "2.7"
"..."
else
"*args, &block"
end
# The following generated method calls the target exactly once, storing
# the returned value in a dummy variable.
#
# Reason is twofold: On one hand doing less calls is in general better.
# On the other hand it could be that the target has side-effects,
# whereas conceptually, from the user point of view, the delegator should
# be doing one call.
if allow_nil
method = method.to_s
method_def <<
"def #{method_name}(#{definition})" <<
" _ = #{to}" <<
" if !_.nil? || nil.respond_to?(:#{method})" <<
" _.#{method}(#{definition})" <<
" end" <<
"end"
else
method = method.to_s
method_name = method_name.to_s
method_def <<
"def #{method_name}(#{definition})" <<
" _ = #{to}" <<
" _.#{method}(#{definition})" <<
"rescue NoMethodError => e" <<
" if _.nil? && e.name == :#{method}" <<
%( raise DelegationError, "#{self}##{method_name} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}") <<
" else" <<
" raise" <<
" end" <<
"end"
end
end
module_eval(method_def.join(";"), file, line)
private(*method_names) if private
method_names
end
# When building decorators, a common pattern may emerge:
#
# class Partition
# def initialize(event)
# @event = event
# end
#
# def person
# detail.person || creator
# end
#
# private
# def respond_to_missing?(name, include_private = false)
# @event.respond_to?(name, include_private)
# end
#
# def method_missing(method, *args, &block)
# @event.send(method, *args, &block)
# end
# end
#
# With <tt>Module#delegate_missing_to</tt>, the above is condensed to:
#
# class Partition
# delegate_missing_to :@event
#
# def initialize(event)
# @event = event
# end
#
# def person
# detail.person || creator
# end
# end
#
# The target can be anything callable within the object, e.g. instance
# variables, methods, constants, etc.
#
# The delegated method must be public on the target, otherwise it will
# raise +DelegationError+. If you wish to instead return +nil+,
# use the <tt>:allow_nil</tt> option.
#
# The <tt>marshal_dump</tt> and <tt>_dump</tt> methods are exempt from
# delegation due to possible interference when calling
# <tt>Marshal.dump(object)</tt>, should the delegation target method
# of <tt>object</tt> add or remove instance variables.
def delegate_missing_to(target, allow_nil: nil)
target = target.to_s
target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)
module_eval <<-RUBY, __FILE__, __LINE__ + 1
def respond_to_missing?(name, include_private = false)
# It may look like an oversight, but we deliberately do not pass
# +include_private+, because they do not get delegated.
return false if name == :marshal_dump || name == :_dump
#{target}.respond_to?(name) || super
end
def method_missing(method, *args, &block)
if #{target}.respond_to?(method)
#{target}.public_send(method, *args, &block)
else
begin
super
rescue NoMethodError
if #{target}.nil?
if #{allow_nil == true}
nil
else
raise DelegationError, "\#{method} delegated to #{target}, but #{target} is nil"
end
else
raise
end
end
end
end
ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
RUBY
end
end

View File

@ -1,25 +0,0 @@
# frozen_string_literal: true
class Module
# deprecate :foo
# deprecate bar: 'message'
# deprecate :foo, :bar, baz: 'warning!', qux: 'gone!'
#
# You can also use custom deprecator instance:
#
# deprecate :foo, deprecator: MyLib::Deprecator.new
# deprecate :foo, bar: "warning!", deprecator: MyLib::Deprecator.new
#
# \Custom deprecators must respond to <tt>deprecation_warning(deprecated_method_name, message, caller_backtrace)</tt>
# method where you can implement your custom warning behavior.
#
# class MyLib::Deprecator
# def deprecation_warning(deprecated_method_name, message, caller_backtrace = nil)
# message = "#{deprecated_method_name} is deprecated and will be removed from MyLibrary | #{message}"
# Kernel.warn message
# end
# end
def deprecate(*method_names)
ActiveSupport::Deprecation.deprecate_methods(self, *method_names)
end
end

View File

@ -1,63 +0,0 @@
# frozen_string_literal: true
require "active_support/core_ext/string/filters"
require "active_support/inflector"
class Module
# Returns the name of the module containing this one.
#
# M::N.module_parent_name # => "M"
def module_parent_name
if defined?(@parent_name)
@parent_name
else
parent_name = name =~ /::[^:]+\z/ ? -$` : nil
@parent_name = parent_name unless frozen?
parent_name
end
end
# Returns the module which contains this one according to its name.
#
# module M
# module N
# end
# end
# X = M::N
#
# M::N.module_parent # => M
# X.module_parent # => M
#
# The parent of top-level and anonymous modules is Object.
#
# M.module_parent # => Object
# Module.new.module_parent # => Object
def module_parent
module_parent_name ? ActiveSupport::Inflector.constantize(module_parent_name) : Object
end
# Returns all the parents of this module according to its name, ordered from
# nested outwards. The receiver is not contained within the result.
#
# module M
# module N
# end
# end
# X = M::N
#
# M.module_parents # => [Object]
# M::N.module_parents # => [M, Object]
# X.module_parents # => [M, Object]
def module_parents
parents = []
if module_parent_name
parts = module_parent_name.split("::")
until parts.empty?
parents << ActiveSupport::Inflector.constantize(parts * "::")
parts.pop
end
end
parents << Object unless parents.include? Object
parents
end
end

View File

@ -1,40 +0,0 @@
# frozen_string_literal: true
class Module
# Marks the named method as intended to be redefined, if it exists.
# Suppresses the Ruby method redefinition warning. Prefer
# #redefine_method where possible.
def silence_redefinition_of_method(method)
if method_defined?(method) || private_method_defined?(method)
# This suppresses the "method redefined" warning; the self-alias
# looks odd, but means we don't need to generate a unique name
alias_method method, method
end
end
# Replaces the existing method definition, if there is one, with the passed
# block as its body.
def redefine_method(method, &block)
visibility = method_visibility(method)
silence_redefinition_of_method(method)
define_method(method, &block)
send(visibility, method)
end
# Replaces the existing singleton method definition, if there is one, with
# the passed block as its body.
def redefine_singleton_method(method, &block)
singleton_class.redefine_method(method, &block)
end
def method_visibility(method) # :nodoc:
case
when private_method_defined?(method)
:private
when protected_method_defined?(method)
:protected
else
:public
end
end
end

View File

@ -1,17 +0,0 @@
# frozen_string_literal: true
require "active_support/core_ext/module/redefine_method"
class Module
# Removes the named method, if it exists.
def remove_possible_method(method)
if method_defined?(method) || private_method_defined?(method)
undef_method(method)
end
end
# Removes the named singleton method, if it exists.
def remove_possible_singleton_method(method)
singleton_class.remove_possible_method(method)
end
end

View File

@ -1,66 +0,0 @@
# frozen_string_literal: true
class Numeric
KILOBYTE = 1024
MEGABYTE = KILOBYTE * 1024
GIGABYTE = MEGABYTE * 1024
TERABYTE = GIGABYTE * 1024
PETABYTE = TERABYTE * 1024
EXABYTE = PETABYTE * 1024
# Enables the use of byte calculations and declarations, like 45.bytes + 2.6.megabytes
#
# 2.bytes # => 2
def bytes
self
end
alias :byte :bytes
# Returns the number of bytes equivalent to the kilobytes provided.
#
# 2.kilobytes # => 2048
def kilobytes
self * KILOBYTE
end
alias :kilobyte :kilobytes
# Returns the number of bytes equivalent to the megabytes provided.
#
# 2.megabytes # => 2_097_152
def megabytes
self * MEGABYTE
end
alias :megabyte :megabytes
# Returns the number of bytes equivalent to the gigabytes provided.
#
# 2.gigabytes # => 2_147_483_648
def gigabytes
self * GIGABYTE
end
alias :gigabyte :gigabytes
# Returns the number of bytes equivalent to the terabytes provided.
#
# 2.terabytes # => 2_199_023_255_552
def terabytes
self * TERABYTE
end
alias :terabyte :terabytes
# Returns the number of bytes equivalent to the petabytes provided.
#
# 2.petabytes # => 2_251_799_813_685_248
def petabytes
self * PETABYTE
end
alias :petabyte :petabytes
# Returns the number of bytes equivalent to the exabytes provided.
#
# 2.exabytes # => 2_305_843_009_213_693_952
def exabytes
self * EXABYTE
end
alias :exabyte :exabytes
end

View File

@ -1,140 +0,0 @@
# frozen_string_literal: true
require "active_support/core_ext/big_decimal/conversions"
require "active_support/number_helper"
module ActiveSupport
module NumericWithFormat
# Provides options for converting numbers into formatted strings.
# Options are provided for phone numbers, currency, percentage,
# precision, positional notation, file size and pretty printing.
#
# ==== Options
#
# For details on which formats use which options, see ActiveSupport::NumberHelper
#
# ==== Examples
#
# Phone Numbers:
# 5551234.to_s(:phone) # => "555-1234"
# 1235551234.to_s(:phone) # => "123-555-1234"
# 1235551234.to_s(:phone, area_code: true) # => "(123) 555-1234"
# 1235551234.to_s(:phone, delimiter: ' ') # => "123 555 1234"
# 1235551234.to_s(:phone, area_code: true, extension: 555) # => "(123) 555-1234 x 555"
# 1235551234.to_s(:phone, country_code: 1) # => "+1-123-555-1234"
# 1235551234.to_s(:phone, country_code: 1, extension: 1343, delimiter: '.')
# # => "+1.123.555.1234 x 1343"
#
# Currency:
# 1234567890.50.to_s(:currency) # => "$1,234,567,890.50"
# 1234567890.506.to_s(:currency) # => "$1,234,567,890.51"
# 1234567890.506.to_s(:currency, precision: 3) # => "$1,234,567,890.506"
# 1234567890.506.to_s(:currency, round_mode: :down) # => "$1,234,567,890.50"
# 1234567890.506.to_s(:currency, locale: :fr) # => "1 234 567 890,51 €"
# -1234567890.50.to_s(:currency, negative_format: '(%u%n)')
# # => "($1,234,567,890.50)"
# 1234567890.50.to_s(:currency, unit: '&pound;', separator: ',', delimiter: '')
# # => "&pound;1234567890,50"
# 1234567890.50.to_s(:currency, unit: '&pound;', separator: ',', delimiter: '', format: '%n %u')
# # => "1234567890,50 &pound;"
#
# Percentage:
# 100.to_s(:percentage) # => "100.000%"
# 100.to_s(:percentage, precision: 0) # => "100%"
# 1000.to_s(:percentage, delimiter: '.', separator: ',') # => "1.000,000%"
# 302.24398923423.to_s(:percentage, precision: 5) # => "302.24399%"
# 302.24398923423.to_s(:percentage, round_mode: :down) # => "302.243%"
# 1000.to_s(:percentage, locale: :fr) # => "1 000,000%"
# 100.to_s(:percentage, format: '%n %') # => "100.000 %"
#
# Delimited:
# 12345678.to_s(:delimited) # => "12,345,678"
# 12345678.05.to_s(:delimited) # => "12,345,678.05"
# 12345678.to_s(:delimited, delimiter: '.') # => "12.345.678"
# 12345678.to_s(:delimited, delimiter: ',') # => "12,345,678"
# 12345678.05.to_s(:delimited, separator: ' ') # => "12,345,678 05"
# 12345678.05.to_s(:delimited, locale: :fr) # => "12 345 678,05"
# 98765432.98.to_s(:delimited, delimiter: ' ', separator: ',')
# # => "98 765 432,98"
#
# Rounded:
# 111.2345.to_s(:rounded) # => "111.235"
# 111.2345.to_s(:rounded, precision: 2) # => "111.23"
# 111.2345.to_s(:rounded, precision: 2, round_mode: :up) # => "111.24"
# 13.to_s(:rounded, precision: 5) # => "13.00000"
# 389.32314.to_s(:rounded, precision: 0) # => "389"
# 111.2345.to_s(:rounded, significant: true) # => "111"
# 111.2345.to_s(:rounded, precision: 1, significant: true) # => "100"
# 13.to_s(:rounded, precision: 5, significant: true) # => "13.000"
# 111.234.to_s(:rounded, locale: :fr) # => "111,234"
# 13.to_s(:rounded, precision: 5, significant: true, strip_insignificant_zeros: true)
# # => "13"
# 389.32314.to_s(:rounded, precision: 4, significant: true) # => "389.3"
# 1111.2345.to_s(:rounded, precision: 2, separator: ',', delimiter: '.')
# # => "1.111,23"
#
# Human-friendly size in Bytes:
# 123.to_s(:human_size) # => "123 Bytes"
# 1234.to_s(:human_size) # => "1.21 KB"
# 12345.to_s(:human_size) # => "12.1 KB"
# 1234567.to_s(:human_size) # => "1.18 MB"
# 1234567890.to_s(:human_size) # => "1.15 GB"
# 1234567890123.to_s(:human_size) # => "1.12 TB"
# 1234567890123456.to_s(:human_size) # => "1.1 PB"
# 1234567890123456789.to_s(:human_size) # => "1.07 EB"
# 1234567.to_s(:human_size, precision: 2) # => "1.2 MB"
# 1234567.to_s(:human_size, precision: 2, round_mode: :up) # => "1.3 MB"
# 483989.to_s(:human_size, precision: 2) # => "470 KB"
# 1234567.to_s(:human_size, precision: 2, separator: ',') # => "1,2 MB"
# 1234567890123.to_s(:human_size, precision: 5) # => "1.1228 TB"
# 524288000.to_s(:human_size, precision: 5) # => "500 MB"
#
# Human-friendly format:
# 123.to_s(:human) # => "123"
# 1234.to_s(:human) # => "1.23 Thousand"
# 12345.to_s(:human) # => "12.3 Thousand"
# 1234567.to_s(:human) # => "1.23 Million"
# 1234567890.to_s(:human) # => "1.23 Billion"
# 1234567890123.to_s(:human) # => "1.23 Trillion"
# 1234567890123456.to_s(:human) # => "1.23 Quadrillion"
# 1234567890123456789.to_s(:human) # => "1230 Quadrillion"
# 489939.to_s(:human, precision: 2) # => "490 Thousand"
# 489939.to_s(:human, precision: 2, round_mode: :down) # => "480 Thousand"
# 489939.to_s(:human, precision: 4) # => "489.9 Thousand"
# 1234567.to_s(:human, precision: 4,
# significant: false) # => "1.2346 Million"
# 1234567.to_s(:human, precision: 1,
# separator: ',',
# significant: false) # => "1,2 Million"
def to_s(format = nil, options = nil)
case format
when nil
super()
when Integer, String
super(format)
when :phone
ActiveSupport::NumberHelper.number_to_phone(self, options || {})
when :currency
ActiveSupport::NumberHelper.number_to_currency(self, options || {})
when :percentage
ActiveSupport::NumberHelper.number_to_percentage(self, options || {})
when :delimited
ActiveSupport::NumberHelper.number_to_delimited(self, options || {})
when :rounded
ActiveSupport::NumberHelper.number_to_rounded(self, options || {})
when :human
ActiveSupport::NumberHelper.number_to_human(self, options || {})
when :human_size
ActiveSupport::NumberHelper.number_to_human_size(self, options || {})
when Symbol
super()
else
super(format)
end
end
end
end
Integer.prepend ActiveSupport::NumericWithFormat
Float.prepend ActiveSupport::NumericWithFormat
BigDecimal.prepend ActiveSupport::NumericWithFormat

View File

@ -1,66 +0,0 @@
# frozen_string_literal: true
require "active_support/duration"
require "active_support/core_ext/time/calculations"
require "active_support/core_ext/time/acts_like"
require "active_support/core_ext/date/calculations"
require "active_support/core_ext/date/acts_like"
class Numeric
# Returns a Duration instance matching the number of seconds provided.
#
# 2.seconds # => 2 seconds
def seconds
ActiveSupport::Duration.seconds(self)
end
alias :second :seconds
# Returns a Duration instance matching the number of minutes provided.
#
# 2.minutes # => 2 minutes
def minutes
ActiveSupport::Duration.minutes(self)
end
alias :minute :minutes
# Returns a Duration instance matching the number of hours provided.
#
# 2.hours # => 2 hours
def hours
ActiveSupport::Duration.hours(self)
end
alias :hour :hours
# Returns a Duration instance matching the number of days provided.
#
# 2.days # => 2 days
def days
ActiveSupport::Duration.days(self)
end
alias :day :days
# Returns a Duration instance matching the number of weeks provided.
#
# 2.weeks # => 2 weeks
def weeks
ActiveSupport::Duration.weeks(self)
end
alias :week :weeks
# Returns a Duration instance matching the number of fortnights provided.
#
# 2.fortnights # => 4 weeks
def fortnights
ActiveSupport::Duration.weeks(self * 2)
end
alias :fortnight :fortnights
# Returns the number of milliseconds equivalent to the seconds provided.
# Used with the standard time durations.
#
# 2.in_milliseconds # => 2000
# 1.hour.in_milliseconds # => 3600000
def in_milliseconds
self * 1000
end
end

View File

@ -1,293 +0,0 @@
# frozen_string_literal: true
require "active_support/inflector/methods"
require "active_support/inflector/transliterate"
# String inflections define new methods on the String class to transform names for different purposes.
# For instance, you can figure out the name of a table from the name of a class.
#
# 'ScaleScore'.tableize # => "scale_scores"
#
class String
# Returns the plural form of the word in the string.
#
# If the optional parameter +count+ is specified,
# the singular form will be returned if <tt>count == 1</tt>.
# For any other value of +count+ the plural will be returned.
#
# If the optional parameter +locale+ is specified,
# the word will be pluralized as a word of that language.
# By default, this parameter is set to <tt>:en</tt>.
# You must define your own inflection rules for languages other than English.
#
# 'post'.pluralize # => "posts"
# 'octopus'.pluralize # => "octopi"
# 'sheep'.pluralize # => "sheep"
# 'words'.pluralize # => "words"
# 'the blue mailman'.pluralize # => "the blue mailmen"
# 'CamelOctopus'.pluralize # => "CamelOctopi"
# 'apple'.pluralize(1) # => "apple"
# 'apple'.pluralize(2) # => "apples"
# 'ley'.pluralize(:es) # => "leyes"
# 'ley'.pluralize(1, :es) # => "ley"
#
# See ActiveSupport::Inflector.pluralize.
def pluralize(count = nil, locale = :en)
locale = count if count.is_a?(Symbol)
if count == 1
dup
else
ActiveSupport::Inflector.pluralize(self, locale)
end
end
# The reverse of +pluralize+, returns the singular form of a word in a string.
#
# If the optional parameter +locale+ is specified,
# the word will be singularized as a word of that language.
# By default, this parameter is set to <tt>:en</tt>.
# You must define your own inflection rules for languages other than English.
#
# 'posts'.singularize # => "post"
# 'octopi'.singularize # => "octopus"
# 'sheep'.singularize # => "sheep"
# 'word'.singularize # => "word"
# 'the blue mailmen'.singularize # => "the blue mailman"
# 'CamelOctopi'.singularize # => "CamelOctopus"
# 'leyes'.singularize(:es) # => "ley"
#
# See ActiveSupport::Inflector.singularize.
def singularize(locale = :en)
ActiveSupport::Inflector.singularize(self, locale)
end
# +constantize+ tries to find a declared constant with the name specified
# in the string. It raises a NameError when the name is not in CamelCase
# or is not initialized.
#
# 'Module'.constantize # => Module
# 'Class'.constantize # => Class
# 'blargle'.constantize # => NameError: wrong constant name blargle
#
# See ActiveSupport::Inflector.constantize.
def constantize
ActiveSupport::Inflector.constantize(self)
end
# +safe_constantize+ tries to find a declared constant with the name specified
# in the string. It returns +nil+ when the name is not in CamelCase
# or is not initialized.
#
# 'Module'.safe_constantize # => Module
# 'Class'.safe_constantize # => Class
# 'blargle'.safe_constantize # => nil
#
# See ActiveSupport::Inflector.safe_constantize.
def safe_constantize
ActiveSupport::Inflector.safe_constantize(self)
end
# By default, +camelize+ converts strings to UpperCamelCase. If the argument to camelize
# is set to <tt>:lower</tt> then camelize produces lowerCamelCase.
#
# +camelize+ will also convert '/' to '::' which is useful for converting paths to namespaces.
#
# 'active_record'.camelize # => "ActiveRecord"
# 'active_record'.camelize(:lower) # => "activeRecord"
# 'active_record/errors'.camelize # => "ActiveRecord::Errors"
# 'active_record/errors'.camelize(:lower) # => "activeRecord::Errors"
#
# +camelize+ is also aliased as +camelcase+.
#
# See ActiveSupport::Inflector.camelize.
def camelize(first_letter = :upper)
case first_letter
when :upper
ActiveSupport::Inflector.camelize(self, true)
when :lower
ActiveSupport::Inflector.camelize(self, false)
else
raise ArgumentError, "Invalid option, use either :upper or :lower."
end
end
alias_method :camelcase, :camelize
# Capitalizes all the words and replaces some characters in the string to create
# a nicer looking title. +titleize+ is meant for creating pretty output. It is not
# used in the Rails internals.
#
# The trailing '_id','Id'.. can be kept and capitalized by setting the
# optional parameter +keep_id_suffix+ to true.
# By default, this parameter is false.
#
# 'man from the boondocks'.titleize # => "Man From The Boondocks"
# 'x-men: the last stand'.titleize # => "X Men: The Last Stand"
# 'string_ending_with_id'.titleize(keep_id_suffix: true) # => "String Ending With Id"
#
# +titleize+ is also aliased as +titlecase+.
#
# See ActiveSupport::Inflector.titleize.
def titleize(keep_id_suffix: false)
ActiveSupport::Inflector.titleize(self, keep_id_suffix: keep_id_suffix)
end
alias_method :titlecase, :titleize
# The reverse of +camelize+. Makes an underscored, lowercase form from the expression in the string.
#
# +underscore+ will also change '::' to '/' to convert namespaces to paths.
#
# 'ActiveModel'.underscore # => "active_model"
# 'ActiveModel::Errors'.underscore # => "active_model/errors"
#
# See ActiveSupport::Inflector.underscore.
def underscore
ActiveSupport::Inflector.underscore(self)
end
# Replaces underscores with dashes in the string.
#
# 'puni_puni'.dasherize # => "puni-puni"
#
# See ActiveSupport::Inflector.dasherize.
def dasherize
ActiveSupport::Inflector.dasherize(self)
end
# Removes the module part from the constant expression in the string.
#
# 'ActiveSupport::Inflector::Inflections'.demodulize # => "Inflections"
# 'Inflections'.demodulize # => "Inflections"
# '::Inflections'.demodulize # => "Inflections"
# ''.demodulize # => ''
#
# See ActiveSupport::Inflector.demodulize.
#
# See also +deconstantize+.
def demodulize
ActiveSupport::Inflector.demodulize(self)
end
# Removes the rightmost segment from the constant expression in the string.
#
# 'Net::HTTP'.deconstantize # => "Net"
# '::Net::HTTP'.deconstantize # => "::Net"
# 'String'.deconstantize # => ""
# '::String'.deconstantize # => ""
# ''.deconstantize # => ""
#
# See ActiveSupport::Inflector.deconstantize.
#
# See also +demodulize+.
def deconstantize
ActiveSupport::Inflector.deconstantize(self)
end
# Replaces special characters in a string so that it may be used as part of a 'pretty' URL.
#
# If the optional parameter +locale+ is specified,
# the word will be parameterized as a word of that language.
# By default, this parameter is set to <tt>nil</tt> and it will use
# the configured <tt>I18n.locale</tt>.
#
# class Person
# def to_param
# "#{id}-#{name.parameterize}"
# end
# end
#
# @person = Person.find(1)
# # => #<Person id: 1, name: "Donald E. Knuth">
#
# <%= link_to(@person.name, person_path) %>
# # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a>
#
# To preserve the case of the characters in a string, use the +preserve_case+ argument.
#
# class Person
# def to_param
# "#{id}-#{name.parameterize(preserve_case: true)}"
# end
# end
#
# @person = Person.find(1)
# # => #<Person id: 1, name: "Donald E. Knuth">
#
# <%= link_to(@person.name, person_path) %>
# # => <a href="/person/1-Donald-E-Knuth">Donald E. Knuth</a>
#
# See ActiveSupport::Inflector.parameterize.
def parameterize(separator: "-", preserve_case: false, locale: nil)
ActiveSupport::Inflector.parameterize(self, separator: separator, preserve_case: preserve_case, locale: locale)
end
# Creates the name of a table like Rails does for models to table names. This method
# uses the +pluralize+ method on the last word in the string.
#
# 'RawScaledScorer'.tableize # => "raw_scaled_scorers"
# 'ham_and_egg'.tableize # => "ham_and_eggs"
# 'fancyCategory'.tableize # => "fancy_categories"
#
# See ActiveSupport::Inflector.tableize.
def tableize
ActiveSupport::Inflector.tableize(self)
end
# Creates a class name from a plural table name like Rails does for table names to models.
# Note that this returns a string and not a class. (To convert to an actual class
# follow +classify+ with +constantize+.)
#
# 'ham_and_eggs'.classify # => "HamAndEgg"
# 'posts'.classify # => "Post"
#
# See ActiveSupport::Inflector.classify.
def classify
ActiveSupport::Inflector.classify(self)
end
# Capitalizes the first word, turns underscores into spaces, and (by default)strips a
# trailing '_id' if present.
# Like +titleize+, this is meant for creating pretty output.
#
# The capitalization of the first word can be turned off by setting the
# optional parameter +capitalize+ to false.
# By default, this parameter is true.
#
# The trailing '_id' can be kept and capitalized by setting the
# optional parameter +keep_id_suffix+ to true.
# By default, this parameter is false.
#
# 'employee_salary'.humanize # => "Employee salary"
# 'author_id'.humanize # => "Author"
# 'author_id'.humanize(capitalize: false) # => "author"
# '_id'.humanize # => "Id"
# 'author_id'.humanize(keep_id_suffix: true) # => "Author Id"
#
# See ActiveSupport::Inflector.humanize.
def humanize(capitalize: true, keep_id_suffix: false)
ActiveSupport::Inflector.humanize(self, capitalize: capitalize, keep_id_suffix: keep_id_suffix)
end
# Converts just the first character to uppercase.
#
# 'what a Lovely Day'.upcase_first # => "What a Lovely Day"
# 'w'.upcase_first # => "W"
# ''.upcase_first # => ""
#
# See ActiveSupport::Inflector.upcase_first.
def upcase_first
ActiveSupport::Inflector.upcase_first(self)
end
# Creates a foreign key name from a class name.
# +separate_class_name_and_id_with_underscore+ sets whether
# the method should put '_' between the name and 'id'.
#
# 'Message'.foreign_key # => "message_id"
# 'Message'.foreign_key(false) # => "messageid"
# 'Admin::Post'.foreign_key # => "post_id"
#
# See ActiveSupport::Inflector.foreign_key.
def foreign_key(separate_class_name_and_id_with_underscore = true)
ActiveSupport::Inflector.foreign_key(self, separate_class_name_and_id_with_underscore)
end
end

View File

@ -1,58 +0,0 @@
# frozen_string_literal: true
require "active_support/multibyte"
class String
# == Multibyte proxy
#
# +mb_chars+ is a multibyte safe proxy for string methods.
#
# It creates and returns an instance of the ActiveSupport::Multibyte::Chars class which
# encapsulates the original string. A Unicode safe version of all the String methods are defined on this proxy
# class. If the proxy class doesn't respond to a certain method, it's forwarded to the encapsulated string.
#
# >> "lj".mb_chars.upcase.to_s
# => "LJ"
#
# NOTE: Ruby 2.4 and later support native Unicode case mappings:
#
# >> "lj".upcase
# => "LJ"
#
# == Method chaining
#
# All the methods on the Chars proxy which normally return a string will return a Chars object. This allows
# method chaining on the result of any of these methods.
#
# name.mb_chars.reverse.length # => 12
#
# == Interoperability and configuration
#
# The Chars object tries to be as interchangeable with String objects as possible: sorting and comparing between
# String and Char work like expected. The bang! methods change the internal string representation in the Chars
# object. Interoperability problems can be resolved easily with a +to_s+ call.
#
# For more information about the methods defined on the Chars proxy see ActiveSupport::Multibyte::Chars. For
# information about how to change the default Multibyte behavior see ActiveSupport::Multibyte.
def mb_chars
ActiveSupport::Multibyte.proxy_class.new(self)
end
# Returns +true+ if string has utf_8 encoding.
#
# utf_8_str = "some string".encode "UTF-8"
# iso_str = "some string".encode "ISO-8859-1"
#
# utf_8_str.is_utf8? # => true
# iso_str.is_utf8? # => false
def is_utf8?
case encoding
when Encoding::UTF_8, Encoding::US_ASCII
valid_encoding?
when Encoding::ASCII_8BIT
dup.force_encoding(Encoding::UTF_8).valid_encoding?
else
false
end
end
end

View File

@ -1,16 +0,0 @@
# frozen_string_literal: true
require "active_support/core_ext/hash/deep_merge"
require "active_support/core_ext/hash/except"
require "active_support/core_ext/hash/slice"
begin
require "i18n"
rescue LoadError => e
$stderr.puts "The i18n gem is not available. Please add it to your Gemfile and run bundle install"
raise e
end
require "active_support/lazy_load_hooks"
ActiveSupport.run_load_hooks(:i18n)
I18n.load_path << File.expand_path("locale/en.yml", __dir__)
I18n.load_path << File.expand_path("locale/en.rb", __dir__)

View File

@ -1,72 +0,0 @@
# frozen_string_literal: true
require "active_support/inflector/inflections"
#--
# Defines the standard inflection rules. These are the starting point for
# new projects and are not considered complete. The current set of inflection
# rules is frozen. This means, we do not change them to become more complete.
# This is a safety measure to keep existing applications from breaking.
#++
module ActiveSupport
Inflector.inflections(:en) do |inflect|
inflect.plural(/$/, "s")
inflect.plural(/s$/i, "s")
inflect.plural(/^(ax|test)is$/i, '\1es')
inflect.plural(/(octop|vir)us$/i, '\1i')
inflect.plural(/(octop|vir)i$/i, '\1i')
inflect.plural(/(alias|status)$/i, '\1es')
inflect.plural(/(bu)s$/i, '\1ses')
inflect.plural(/(buffal|tomat)o$/i, '\1oes')
inflect.plural(/([ti])um$/i, '\1a')
inflect.plural(/([ti])a$/i, '\1a')
inflect.plural(/sis$/i, "ses")
inflect.plural(/(?:([^f])fe|([lr])f)$/i, '\1\2ves')
inflect.plural(/(hive)$/i, '\1s')
inflect.plural(/([^aeiouy]|qu)y$/i, '\1ies')
inflect.plural(/(x|ch|ss|sh)$/i, '\1es')
inflect.plural(/(matr|vert|ind)(?:ix|ex)$/i, '\1ices')
inflect.plural(/^(m|l)ouse$/i, '\1ice')
inflect.plural(/^(m|l)ice$/i, '\1ice')
inflect.plural(/^(ox)$/i, '\1en')
inflect.plural(/^(oxen)$/i, '\1')
inflect.plural(/(quiz)$/i, '\1zes')
inflect.singular(/s$/i, "")
inflect.singular(/(ss)$/i, '\1')
inflect.singular(/(n)ews$/i, '\1ews')
inflect.singular(/([ti])a$/i, '\1um')
inflect.singular(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/i, '\1sis')
inflect.singular(/(^analy)(sis|ses)$/i, '\1sis')
inflect.singular(/([^f])ves$/i, '\1fe')
inflect.singular(/(hive)s$/i, '\1')
inflect.singular(/(tive)s$/i, '\1')
inflect.singular(/([lr])ves$/i, '\1f')
inflect.singular(/([^aeiouy]|qu)ies$/i, '\1y')
inflect.singular(/(s)eries$/i, '\1eries')
inflect.singular(/(m)ovies$/i, '\1ovie')
inflect.singular(/(x|ch|ss|sh)es$/i, '\1')
inflect.singular(/^(m|l)ice$/i, '\1ouse')
inflect.singular(/(bus)(es)?$/i, '\1')
inflect.singular(/(o)es$/i, '\1')
inflect.singular(/(shoe)s$/i, '\1')
inflect.singular(/(cris|test)(is|es)$/i, '\1is')
inflect.singular(/^(a)x[ie]s$/i, '\1xis')
inflect.singular(/(octop|vir)(us|i)$/i, '\1us')
inflect.singular(/(alias|status)(es)?$/i, '\1')
inflect.singular(/^(ox)en/i, '\1')
inflect.singular(/(vert|ind)ices$/i, '\1ex')
inflect.singular(/(matr)ices$/i, '\1ix')
inflect.singular(/(quiz)zes$/i, '\1')
inflect.singular(/(database)s$/i, '\1')
inflect.irregular("person", "people")
inflect.irregular("man", "men")
inflect.irregular("child", "children")
inflect.irregular("sex", "sexes")
inflect.irregular("move", "moves")
inflect.irregular("zombie", "zombies")
inflect.uncountable(%w(equipment information rice money species series fish sheep jeans police))
end
end

View File

@ -1,9 +0,0 @@
# frozen_string_literal: true
# in case active_support/inflector is required without the rest of active_support
require "active_support/inflector/inflections"
require "active_support/inflector/transliterate"
require "active_support/inflector/methods"
require "active_support/inflections"
require "active_support/core_ext/string/inflections"

View File

@ -1,255 +0,0 @@
# frozen_string_literal: true
require "concurrent/map"
require "active_support/i18n"
module ActiveSupport
module Inflector
extend self
# A singleton instance of this class is yielded by Inflector.inflections,
# which can then be used to specify additional inflection rules. If passed
# an optional locale, rules for other languages can be specified. The
# default locale is <tt>:en</tt>. Only rules for English are provided.
#
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.plural /^(ox)$/i, '\1\2en'
# inflect.singular /^(ox)en/i, '\1'
#
# inflect.irregular 'octopus', 'octopi'
#
# inflect.uncountable 'equipment'
# end
#
# New rules are added at the top. So in the example above, the irregular
# rule for octopus will now be the first of the pluralization and
# singularization rules that is runs. This guarantees that your rules run
# before any of the rules that may already have been loaded.
class Inflections
@__instance__ = Concurrent::Map.new
class Uncountables < Array
def initialize
@regex_array = []
super
end
def delete(entry)
super entry
@regex_array.delete(to_regex(entry))
end
def <<(*word)
add(word)
end
def add(words)
words = words.flatten.map(&:downcase)
concat(words)
@regex_array += words.map { |word| to_regex(word) }
self
end
def uncountable?(str)
@regex_array.any? { |regex| regex.match? str }
end
private
def to_regex(string)
/\b#{::Regexp.escape(string)}\Z/i
end
end
def self.instance(locale = :en)
@__instance__[locale] ||= new
end
attr_reader :plurals, :singulars, :uncountables, :humans, :acronyms
attr_reader :acronyms_camelize_regex, :acronyms_underscore_regex # :nodoc:
def initialize
@plurals, @singulars, @uncountables, @humans, @acronyms = [], [], Uncountables.new, [], {}
define_acronym_regex_patterns
end
# Private, for the test suite.
def initialize_dup(orig) # :nodoc:
%w(plurals singulars uncountables humans acronyms).each do |scope|
instance_variable_set("@#{scope}", orig.public_send(scope).dup)
end
define_acronym_regex_patterns
end
# Specifies a new acronym. An acronym must be specified as it will appear
# in a camelized string. An underscore string that contains the acronym
# will retain the acronym when passed to +camelize+, +humanize+, or
# +titleize+. A camelized string that contains the acronym will maintain
# the acronym when titleized or humanized, and will convert the acronym
# into a non-delimited single lowercase word when passed to +underscore+.
#
# acronym 'HTML'
# titleize 'html' # => 'HTML'
# camelize 'html' # => 'HTML'
# underscore 'MyHTML' # => 'my_html'
#
# The acronym, however, must occur as a delimited unit and not be part of
# another word for conversions to recognize it:
#
# acronym 'HTTP'
# camelize 'my_http_delimited' # => 'MyHTTPDelimited'
# camelize 'https' # => 'Https', not 'HTTPs'
# underscore 'HTTPS' # => 'http_s', not 'https'
#
# acronym 'HTTPS'
# camelize 'https' # => 'HTTPS'
# underscore 'HTTPS' # => 'https'
#
# Note: Acronyms that are passed to +pluralize+ will no longer be
# recognized, since the acronym will not occur as a delimited unit in the
# pluralized result. To work around this, you must specify the pluralized
# form as an acronym as well:
#
# acronym 'API'
# camelize(pluralize('api')) # => 'Apis'
#
# acronym 'APIs'
# camelize(pluralize('api')) # => 'APIs'
#
# +acronym+ may be used to specify any word that contains an acronym or
# otherwise needs to maintain a non-standard capitalization. The only
# restriction is that the word must begin with a capital letter.
#
# acronym 'RESTful'
# underscore 'RESTful' # => 'restful'
# underscore 'RESTfulController' # => 'restful_controller'
# titleize 'RESTfulController' # => 'RESTful Controller'
# camelize 'restful' # => 'RESTful'
# camelize 'restful_controller' # => 'RESTfulController'
#
# acronym 'McDonald'
# underscore 'McDonald' # => 'mcdonald'
# camelize 'mcdonald' # => 'McDonald'
def acronym(word)
@acronyms[word.downcase] = word
define_acronym_regex_patterns
end
# Specifies a new pluralization rule and its replacement. The rule can
# either be a string or a regular expression. The replacement should
# always be a string that may include references to the matched data from
# the rule.
def plural(rule, replacement)
@uncountables.delete(rule) if rule.is_a?(String)
@uncountables.delete(replacement)
@plurals.prepend([rule, replacement])
end
# Specifies a new singularization rule and its replacement. The rule can
# either be a string or a regular expression. The replacement should
# always be a string that may include references to the matched data from
# the rule.
def singular(rule, replacement)
@uncountables.delete(rule) if rule.is_a?(String)
@uncountables.delete(replacement)
@singulars.prepend([rule, replacement])
end
# Specifies a new irregular that applies to both pluralization and
# singularization at the same time. This can only be used for strings, not
# regular expressions. You simply pass the irregular in singular and
# plural form.
#
# irregular 'octopus', 'octopi'
# irregular 'person', 'people'
def irregular(singular, plural)
@uncountables.delete(singular)
@uncountables.delete(plural)
s0 = singular[0]
srest = singular[1..-1]
p0 = plural[0]
prest = plural[1..-1]
if s0.upcase == p0.upcase
plural(/(#{s0})#{srest}$/i, '\1' + prest)
plural(/(#{p0})#{prest}$/i, '\1' + prest)
singular(/(#{s0})#{srest}$/i, '\1' + srest)
singular(/(#{p0})#{prest}$/i, '\1' + srest)
else
plural(/#{s0.upcase}(?i)#{srest}$/, p0.upcase + prest)
plural(/#{s0.downcase}(?i)#{srest}$/, p0.downcase + prest)
plural(/#{p0.upcase}(?i)#{prest}$/, p0.upcase + prest)
plural(/#{p0.downcase}(?i)#{prest}$/, p0.downcase + prest)
singular(/#{s0.upcase}(?i)#{srest}$/, s0.upcase + srest)
singular(/#{s0.downcase}(?i)#{srest}$/, s0.downcase + srest)
singular(/#{p0.upcase}(?i)#{prest}$/, s0.upcase + srest)
singular(/#{p0.downcase}(?i)#{prest}$/, s0.downcase + srest)
end
end
# Specifies words that are uncountable and should not be inflected.
#
# uncountable 'money'
# uncountable 'money', 'information'
# uncountable %w( money information rice )
def uncountable(*words)
@uncountables.add(words)
end
# Specifies a humanized form of a string by a regular expression rule or
# by a string mapping. When using a regular expression based replacement,
# the normal humanize formatting is called after the replacement. When a
# string is used, the human form should be specified as desired (example:
# 'The name', not 'the_name').
#
# human /_cnt$/i, '\1_count'
# human 'legacy_col_person_name', 'Name'
def human(rule, replacement)
@humans.prepend([rule, replacement])
end
# Clears the loaded inflections within a given scope (default is
# <tt>:all</tt>). Give the scope as a symbol of the inflection type, the
# options are: <tt>:plurals</tt>, <tt>:singulars</tt>, <tt>:uncountables</tt>,
# <tt>:humans</tt>.
#
# clear :all
# clear :plurals
def clear(scope = :all)
case scope
when :all
@plurals, @singulars, @uncountables, @humans = [], [], Uncountables.new, []
else
instance_variable_set "@#{scope}", []
end
end
private
def define_acronym_regex_patterns
@acronym_regex = @acronyms.empty? ? /(?=a)b/ : /#{@acronyms.values.join("|")}/
@acronyms_camelize_regex = /^(?:#{@acronym_regex}(?=\b|[A-Z_])|\w)/
@acronyms_underscore_regex = /(?:(?<=([A-Za-z\d]))|\b)(#{@acronym_regex})(?=\b|[^a-z])/
end
end
# Yields a singleton instance of Inflector::Inflections so you can specify
# additional inflector rules. If passed an optional locale, rules for other
# languages can be specified. If not specified, defaults to <tt>:en</tt>.
# Only rules for English are provided.
#
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.uncountable 'rails'
# end
def inflections(locale = :en)
if block_given?
yield Inflections.instance(locale)
else
Inflections.instance(locale)
end
end
end
end

View File

@ -1,400 +0,0 @@
# frozen_string_literal: true
require "active_support/inflections"
require "active_support/core_ext/object/blank"
module ActiveSupport
# The Inflector transforms words from singular to plural, class names to table
# names, modularized class names to ones without, and class names to foreign
# keys. The default inflections for pluralization, singularization, and
# uncountable words are kept in inflections.rb.
#
# The Rails core team has stated patches for the inflections library will not
# be accepted in order to avoid breaking legacy applications which may be
# relying on errant inflections. If you discover an incorrect inflection and
# require it for your application or wish to define rules for languages other
# than English, please correct or add them yourself (explained below).
module Inflector
extend self
# Returns the plural form of the word in the string.
#
# If passed an optional +locale+ parameter, the word will be
# pluralized using rules defined for that language. By default,
# this parameter is set to <tt>:en</tt>.
#
# pluralize('post') # => "posts"
# pluralize('octopus') # => "octopi"
# pluralize('sheep') # => "sheep"
# pluralize('words') # => "words"
# pluralize('CamelOctopus') # => "CamelOctopi"
# pluralize('ley', :es) # => "leyes"
def pluralize(word, locale = :en)
apply_inflections(word, inflections(locale).plurals, locale)
end
# The reverse of #pluralize, returns the singular form of a word in a
# string.
#
# If passed an optional +locale+ parameter, the word will be
# singularized using rules defined for that language. By default,
# this parameter is set to <tt>:en</tt>.
#
# singularize('posts') # => "post"
# singularize('octopi') # => "octopus"
# singularize('sheep') # => "sheep"
# singularize('word') # => "word"
# singularize('CamelOctopi') # => "CamelOctopus"
# singularize('leyes', :es) # => "ley"
def singularize(word, locale = :en)
apply_inflections(word, inflections(locale).singulars, locale)
end
# Converts strings to UpperCamelCase.
# If the +uppercase_first_letter+ parameter is set to false, then produces
# lowerCamelCase.
#
# Also converts '/' to '::' which is useful for converting
# paths to namespaces.
#
# camelize('active_model') # => "ActiveModel"
# camelize('active_model', false) # => "activeModel"
# camelize('active_model/errors') # => "ActiveModel::Errors"
# camelize('active_model/errors', false) # => "activeModel::Errors"
#
# As a rule of thumb you can think of +camelize+ as the inverse of
# #underscore, though there are cases where that does not hold:
#
# camelize(underscore('SSLError')) # => "SslError"
def camelize(term, uppercase_first_letter = true)
string = term.to_s
if uppercase_first_letter
string = string.sub(/^[a-z\d]*/) { |match| inflections.acronyms[match] || match.capitalize }
else
string = string.sub(inflections.acronyms_camelize_regex) { |match| match.downcase }
end
string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" }
string.gsub!("/", "::")
string
end
# Makes an underscored, lowercase form from the expression in the string.
#
# Changes '::' to '/' to convert namespaces to paths.
#
# underscore('ActiveModel') # => "active_model"
# underscore('ActiveModel::Errors') # => "active_model/errors"
#
# As a rule of thumb you can think of +underscore+ as the inverse of
# #camelize, though there are cases where that does not hold:
#
# camelize(underscore('SSLError')) # => "SslError"
def underscore(camel_cased_word)
return camel_cased_word unless /[A-Z-]|::/.match?(camel_cased_word)
word = camel_cased_word.to_s.gsub("::", "/")
word.gsub!(inflections.acronyms_underscore_regex) { "#{$1 && '_' }#{$2.downcase}" }
word.gsub!(/([A-Z])(?=[A-Z][a-z])|([a-z\d])(?=[A-Z])/) { ($1 || $2) << "_" }
word.tr!("-", "_")
word.downcase!
word
end
# Tweaks an attribute name for display to end users.
#
# Specifically, performs these transformations:
#
# * Applies human inflection rules to the argument.
# * Deletes leading underscores, if any.
# * Removes a "_id" suffix if present.
# * Replaces underscores with spaces, if any.
# * Downcases all words except acronyms.
# * Capitalizes the first word.
# The capitalization of the first word can be turned off by setting the
# +:capitalize+ option to false (default is true).
#
# The trailing '_id' can be kept and capitalized by setting the
# optional parameter +keep_id_suffix+ to true (default is false).
#
# humanize('employee_salary') # => "Employee salary"
# humanize('author_id') # => "Author"
# humanize('author_id', capitalize: false) # => "author"
# humanize('_id') # => "Id"
# humanize('author_id', keep_id_suffix: true) # => "Author Id"
#
# If "SSL" was defined to be an acronym:
#
# humanize('ssl_error') # => "SSL error"
#
def humanize(lower_case_and_underscored_word, capitalize: true, keep_id_suffix: false)
result = lower_case_and_underscored_word.to_s.dup
inflections.humans.each { |(rule, replacement)| break if result.sub!(rule, replacement) }
result.sub!(/\A_+/, "")
unless keep_id_suffix
result.delete_suffix!("_id")
end
result.tr!("_", " ")
result.gsub!(/([a-z\d]*)/i) do |match|
"#{inflections.acronyms[match.downcase] || match.downcase}"
end
if capitalize
result.sub!(/\A\w/) { |match| match.upcase }
end
result
end
# Converts just the first character to uppercase.
#
# upcase_first('what a Lovely Day') # => "What a Lovely Day"
# upcase_first('w') # => "W"
# upcase_first('') # => ""
def upcase_first(string)
string.length > 0 ? string[0].upcase.concat(string[1..-1]) : ""
end
# Capitalizes all the words and replaces some characters in the string to
# create a nicer looking title. +titleize+ is meant for creating pretty
# output. It is not used in the Rails internals.
#
# The trailing '_id','Id'.. can be kept and capitalized by setting the
# optional parameter +keep_id_suffix+ to true.
# By default, this parameter is false.
#
# +titleize+ is also aliased as +titlecase+.
#
# titleize('man from the boondocks') # => "Man From The Boondocks"
# titleize('x-men: the last stand') # => "X Men: The Last Stand"
# titleize('TheManWithoutAPast') # => "The Man Without A Past"
# titleize('raiders_of_the_lost_ark') # => "Raiders Of The Lost Ark"
# titleize('string_ending_with_id', keep_id_suffix: true) # => "String Ending With Id"
def titleize(word, keep_id_suffix: false)
humanize(underscore(word), keep_id_suffix: keep_id_suffix).gsub(/\b(?<!\w['`()])[a-z]/) do |match|
match.capitalize
end
end
# Creates the name of a table like Rails does for models to table names.
# This method uses the #pluralize method on the last word in the string.
#
# tableize('RawScaledScorer') # => "raw_scaled_scorers"
# tableize('ham_and_egg') # => "ham_and_eggs"
# tableize('fancyCategory') # => "fancy_categories"
def tableize(class_name)
pluralize(underscore(class_name))
end
# Creates a class name from a plural table name like Rails does for table
# names to models. Note that this returns a string and not a Class (To
# convert to an actual class follow +classify+ with #constantize).
#
# classify('ham_and_eggs') # => "HamAndEgg"
# classify('posts') # => "Post"
#
# Singular names are not handled correctly:
#
# classify('calculus') # => "Calculu"
def classify(table_name)
# strip out any leading schema name
camelize(singularize(table_name.to_s.sub(/.*\./, "")))
end
# Replaces underscores with dashes in the string.
#
# dasherize('puni_puni') # => "puni-puni"
def dasherize(underscored_word)
underscored_word.tr("_", "-")
end
# Removes the module part from the expression in the string.
#
# demodulize('ActiveSupport::Inflector::Inflections') # => "Inflections"
# demodulize('Inflections') # => "Inflections"
# demodulize('::Inflections') # => "Inflections"
# demodulize('') # => ""
#
# See also #deconstantize.
def demodulize(path)
path = path.to_s
if i = path.rindex("::")
path[(i + 2)..-1]
else
path
end
end
# Removes the rightmost segment from the constant expression in the string.
#
# deconstantize('Net::HTTP') # => "Net"
# deconstantize('::Net::HTTP') # => "::Net"
# deconstantize('String') # => ""
# deconstantize('::String') # => ""
# deconstantize('') # => ""
#
# See also #demodulize.
def deconstantize(path)
path.to_s[0, path.rindex("::") || 0] # implementation based on the one in facets' Module#spacename
end
# Creates a foreign key name from a class name.
# +separate_class_name_and_id_with_underscore+ sets whether
# the method should put '_' between the name and 'id'.
#
# foreign_key('Message') # => "message_id"
# foreign_key('Message', false) # => "messageid"
# foreign_key('Admin::Post') # => "post_id"
def foreign_key(class_name, separate_class_name_and_id_with_underscore = true)
underscore(demodulize(class_name)) + (separate_class_name_and_id_with_underscore ? "_id" : "id")
end
# Tries to find a constant with the name specified in the argument string.
#
# constantize('Module') # => Module
# constantize('Foo::Bar') # => Foo::Bar
#
# The name is assumed to be the one of a top-level constant, no matter
# whether it starts with "::" or not. No lexical context is taken into
# account:
#
# C = 'outside'
# module M
# C = 'inside'
# C # => 'inside'
# constantize('C') # => 'outside', same as ::C
# end
#
# NameError is raised when the name is not in CamelCase or the constant is
# unknown.
def constantize(camel_cased_word)
if camel_cased_word.blank? || !camel_cased_word.include?("::")
Object.const_get(camel_cased_word)
else
names = camel_cased_word.split("::")
# Trigger a built-in NameError exception including the ill-formed constant in the message.
Object.const_get(camel_cased_word) if names.empty?
# Remove the first blank element in case of '::ClassName' notation.
names.shift if names.size > 1 && names.first.empty?
names.inject(Object) do |constant, name|
if constant == Object
constant.const_get(name)
else
candidate = constant.const_get(name)
next candidate if constant.const_defined?(name, false)
next candidate unless Object.const_defined?(name)
# Go down the ancestors to check if it is owned directly. The check
# stops when we reach Object or the end of ancestors tree.
constant = constant.ancestors.inject(constant) do |const, ancestor|
break const if ancestor == Object
break ancestor if ancestor.const_defined?(name, false)
const
end
# owner is in Object, so raise
constant.const_get(name, false)
end
end
end
end
# Tries to find a constant with the name specified in the argument string.
#
# safe_constantize('Module') # => Module
# safe_constantize('Foo::Bar') # => Foo::Bar
#
# The name is assumed to be the one of a top-level constant, no matter
# whether it starts with "::" or not. No lexical context is taken into
# account:
#
# C = 'outside'
# module M
# C = 'inside'
# C # => 'inside'
# safe_constantize('C') # => 'outside', same as ::C
# end
#
# +nil+ is returned when the name is not in CamelCase or the constant (or
# part of it) is unknown.
#
# safe_constantize('blargle') # => nil
# safe_constantize('UnknownModule') # => nil
# safe_constantize('UnknownModule::Foo::Bar') # => nil
def safe_constantize(camel_cased_word)
constantize(camel_cased_word)
rescue NameError => e
raise if e.name && !(camel_cased_word.to_s.split("::").include?(e.name.to_s) ||
e.name.to_s == camel_cased_word.to_s)
rescue LoadError => e
message = e.respond_to?(:original_message) ? e.original_message : e.message
raise unless /Unable to autoload constant #{const_regexp(camel_cased_word)}/.match?(message)
end
# Returns the suffix that should be added to a number to denote the position
# in an ordered sequence such as 1st, 2nd, 3rd, 4th.
#
# ordinal(1) # => "st"
# ordinal(2) # => "nd"
# ordinal(1002) # => "nd"
# ordinal(1003) # => "rd"
# ordinal(-11) # => "th"
# ordinal(-1021) # => "st"
def ordinal(number)
I18n.translate("number.nth.ordinals", number: number)
end
# Turns a number into an ordinal string used to denote the position in an
# ordered sequence such as 1st, 2nd, 3rd, 4th.
#
# ordinalize(1) # => "1st"
# ordinalize(2) # => "2nd"
# ordinalize(1002) # => "1002nd"
# ordinalize(1003) # => "1003rd"
# ordinalize(-11) # => "-11th"
# ordinalize(-1021) # => "-1021st"
def ordinalize(number)
I18n.translate("number.nth.ordinalized", number: number)
end
private
# Mounts a regular expression, returned as a string to ease interpolation,
# that will match part by part the given constant.
#
# const_regexp("Foo::Bar::Baz") # => "Foo(::Bar(::Baz)?)?"
# const_regexp("::") # => "::"
def const_regexp(camel_cased_word)
parts = camel_cased_word.split("::")
return Regexp.escape(camel_cased_word) if parts.blank?
last = parts.pop
parts.reverse!.inject(last) do |acc, part|
part.empty? ? acc : "#{part}(::#{acc})?"
end
end
# Applies inflection rules for +singularize+ and +pluralize+.
#
# If passed an optional +locale+ parameter, the uncountables will be
# found for that locale.
#
# apply_inflections('post', inflections.plurals, :en) # => "posts"
# apply_inflections('posts', inflections.singulars, :en) # => "post"
def apply_inflections(word, rules, locale = :en)
result = word.to_s.dup
if word.empty? || inflections(locale).uncountables.uncountable?(result)
result
else
rules.each { |(rule, replacement)| break if result.sub!(rule, replacement) }
result
end
end
end
end

View File

@ -1,147 +0,0 @@
# frozen_string_literal: true
require "active_support/core_ext/string/multibyte"
require "active_support/i18n"
module ActiveSupport
module Inflector
ALLOWED_ENCODINGS_FOR_TRANSLITERATE = [Encoding::UTF_8, Encoding::US_ASCII, Encoding::GB18030].freeze
# Replaces non-ASCII characters with an ASCII approximation, or if none
# exists, a replacement character which defaults to "?".
#
# transliterate('Ærøskøbing')
# # => "AEroskobing"
#
# Default approximations are provided for Western/Latin characters,
# e.g, "ø", "ñ", "é", "ß", etc.
#
# This method is I18n aware, so you can set up custom approximations for a
# locale. This can be useful, for example, to transliterate German's "ü"
# and "ö" to "ue" and "oe", or to add support for transliterating Russian
# to ASCII.
#
# In order to make your custom transliterations available, you must set
# them as the <tt>i18n.transliterate.rule</tt> i18n key:
#
# # Store the transliterations in locales/de.yml
# i18n:
# transliterate:
# rule:
# ü: "ue"
# ö: "oe"
#
# # Or set them using Ruby
# I18n.backend.store_translations(:de, i18n: {
# transliterate: {
# rule: {
# 'ü' => 'ue',
# 'ö' => 'oe'
# }
# }
# })
#
# The value for <tt>i18n.transliterate.rule</tt> can be a simple Hash that
# maps characters to ASCII approximations as shown above, or, for more
# complex requirements, a Proc:
#
# I18n.backend.store_translations(:de, i18n: {
# transliterate: {
# rule: ->(string) { MyTransliterator.transliterate(string) }
# }
# })
#
# Now you can have different transliterations for each locale:
#
# transliterate('Jürgen', locale: :en)
# # => "Jurgen"
#
# transliterate('Jürgen', locale: :de)
# # => "Juergen"
#
# Transliteration is restricted to UTF-8, US-ASCII and GB18030 strings
# Other encodings will raise an ArgumentError.
def transliterate(string, replacement = "?", locale: nil)
string = string.dup if string.frozen?
raise ArgumentError, "Can only transliterate strings. Received #{string.class.name}" unless string.is_a?(String)
raise ArgumentError, "Cannot transliterate strings with #{string.encoding} encoding" unless ALLOWED_ENCODINGS_FOR_TRANSLITERATE.include?(string.encoding)
input_encoding = string.encoding
# US-ASCII is a subset of UTF-8 so we'll force encoding as UTF-8 if
# US-ASCII is given. This way we can let tidy_bytes handle the string
# in the same way as we do for UTF-8
string.force_encoding(Encoding::UTF_8) if string.encoding == Encoding::US_ASCII
# GB18030 is Unicode compatible but is not a direct mapping so needs to be
# transcoded. Using invalid/undef :replace will result in loss of data in
# the event of invalid characters, but since tidy_bytes will replace
# invalid/undef with a "?" we're safe to do the same beforehand
string.encode!(Encoding::UTF_8, invalid: :replace, undef: :replace) if string.encoding == Encoding::GB18030
transliterated = I18n.transliterate(
ActiveSupport::Multibyte::Unicode.tidy_bytes(string).unicode_normalize(:nfc),
replacement: replacement,
locale: locale
)
# Restore the string encoding of the input if it was not UTF-8.
# Apply invalid/undef :replace as tidy_bytes does
transliterated.encode!(input_encoding, invalid: :replace, undef: :replace) if input_encoding != transliterated.encoding
transliterated
end
# Replaces special characters in a string so that it may be used as part of
# a 'pretty' URL.
#
# parameterize("Donald E. Knuth") # => "donald-e-knuth"
# parameterize("^très|Jolie-- ") # => "tres-jolie"
#
# To use a custom separator, override the +separator+ argument.
#
# parameterize("Donald E. Knuth", separator: '_') # => "donald_e_knuth"
# parameterize("^très|Jolie__ ", separator: '_') # => "tres_jolie"
#
# To preserve the case of the characters in a string, use the +preserve_case+ argument.
#
# parameterize("Donald E. Knuth", preserve_case: true) # => "Donald-E-Knuth"
# parameterize("^très|Jolie-- ", preserve_case: true) # => "tres-Jolie"
#
# It preserves dashes and underscores unless they are used as separators:
#
# parameterize("^très|Jolie__ ") # => "tres-jolie__"
# parameterize("^très|Jolie-- ", separator: "_") # => "tres_jolie--"
# parameterize("^très_Jolie-- ", separator: ".") # => "tres_jolie--"
#
# If the optional parameter +locale+ is specified,
# the word will be parameterized as a word of that language.
# By default, this parameter is set to <tt>nil</tt> and it will use
# the configured <tt>I18n.locale</tt>.
def parameterize(string, separator: "-", preserve_case: false, locale: nil)
# Replace accented chars with their ASCII equivalents.
parameterized_string = transliterate(string, locale: locale)
# Turn unwanted chars into the separator.
parameterized_string.gsub!(/[^a-z0-9\-_]+/i, separator)
unless separator.nil? || separator.empty?
if separator == "-"
re_duplicate_separator = /-{2,}/
re_leading_trailing_separator = /^-|-$/i
else
re_sep = Regexp.escape(separator)
re_duplicate_separator = /#{re_sep}{2,}/
re_leading_trailing_separator = /^#{re_sep}|#{re_sep}$/i
end
# No more than one of the separator in a row.
parameterized_string.gsub!(re_duplicate_separator, separator)
# Remove leading/trailing separator.
parameterized_string.gsub!(re_leading_trailing_separator, "")
end
parameterized_string.downcase! unless preserve_case
parameterized_string
end
end
end

View File

@ -1,81 +0,0 @@
# frozen_string_literal: true
module ActiveSupport
# lazy_load_hooks allows Rails to lazily load a lot of components and thus
# making the app boot faster. Because of this feature now there is no need to
# require <tt>ActiveRecord::Base</tt> at boot time purely to apply
# configuration. Instead a hook is registered that applies configuration once
# <tt>ActiveRecord::Base</tt> is loaded. Here <tt>ActiveRecord::Base</tt> is
# used as example but this feature can be applied elsewhere too.
#
# Here is an example where +on_load+ method is called to register a hook.
#
# initializer 'active_record.initialize_timezone' do
# ActiveSupport.on_load(:active_record) do
# self.time_zone_aware_attributes = true
# self.default_timezone = :utc
# end
# end
#
# When the entirety of +ActiveRecord::Base+ has been
# evaluated then +run_load_hooks+ is invoked. The very last line of
# +ActiveRecord::Base+ is:
#
# ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base)
module LazyLoadHooks
def self.extended(base) # :nodoc:
base.class_eval do
@load_hooks = Hash.new { |h, k| h[k] = [] }
@loaded = Hash.new { |h, k| h[k] = [] }
@run_once = Hash.new { |h, k| h[k] = [] }
end
end
# Declares a block that will be executed when a Rails component is fully
# loaded.
#
# Options:
#
# * <tt>:yield</tt> - Yields the object that run_load_hooks to +block+.
# * <tt>:run_once</tt> - Given +block+ will run only once.
def on_load(name, options = {}, &block)
@loaded[name].each do |base|
execute_hook(name, base, options, block)
end
@load_hooks[name] << [block, options]
end
def run_load_hooks(name, base = Object)
@loaded[name] << base
@load_hooks[name].each do |hook, options|
execute_hook(name, base, options, hook)
end
end
private
def with_execution_control(name, block, once)
unless @run_once[name].include?(block)
@run_once[name] << block if once
yield
end
end
def execute_hook(name, base, options, block)
with_execution_control(name, block, options[:run_once]) do
if options[:yield]
block.call(base)
else
if base.is_a?(Module)
base.class_eval(&block)
else
base.instance_eval(&block)
end
end
end
end
end
extend LazyLoadHooks
end

View File

@ -1,23 +0,0 @@
# frozen_string_literal: true
module ActiveSupport #:nodoc:
module Multibyte
autoload :Chars, "active_support/multibyte/chars"
autoload :Unicode, "active_support/multibyte/unicode"
# The proxy class returned when calling mb_chars. You can use this accessor
# to configure your own proxy class so you can support other encodings. See
# the ActiveSupport::Multibyte::Chars implementation for an example how to
# do this.
#
# ActiveSupport::Multibyte.proxy_class = CharsForUTF32
def self.proxy_class=(klass)
@proxy_class = klass
end
# Returns the current proxy class.
def self.proxy_class
@proxy_class ||= ActiveSupport::Multibyte::Chars
end
end
end

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,25 @@
BSD 2-Clause License
Copyright (c) 2007-2022, Dion Mendel
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,21 @@
Copyright (c) Jerry D'Antonio -- released under the MIT license.
http://www.opensource.org/licenses/mit-license.php
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,22 @@
Copyright (c) 2014-2016 Yuki Nishijima
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,20 @@
Copyright (c) 2008 The Ruby I18n team
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,20 @@
Copyright (c) 2006-2010, Ben Bleything and Patrick May
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,22 @@
Copyright (c) 2009-2023 Simone Carletti <weppos@weppos.net>
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,71 +0,0 @@
# frozen_string_literal: true
# Copyright (C) 2007-2019 Leah Neukirchen <http://leahneukirchen.org/infopage.html>
#
# Rack is freely distributable under the terms of an MIT-style license.
# See MIT-LICENSE or https://opensource.org/licenses/MIT.
# The Rack main module, serving as a namespace for all core Rack
# modules and classes.
#
# All modules meant for use in your application are <tt>autoload</tt>ed here,
# so it should be enough just to <tt>require 'rack'</tt> in your code.
require_relative 'rack/version'
require_relative 'rack/constants'
module Rack
autoload :Builder, "rack/builder"
autoload :BodyProxy, "rack/body_proxy"
autoload :Cascade, "rack/cascade"
autoload :Chunked, "rack/chunked"
autoload :CommonLogger, "rack/common_logger"
autoload :ConditionalGet, "rack/conditional_get"
autoload :Config, "rack/config"
autoload :ContentLength, "rack/content_length"
autoload :ContentType, "rack/content_type"
autoload :ETag, "rack/etag"
autoload :Events, "rack/events"
autoload :File, "rack/file"
autoload :Files, "rack/files"
autoload :Deflater, "rack/deflater"
autoload :Directory, "rack/directory"
autoload :ForwardRequest, "rack/recursive"
autoload :Handler, "rack/handler"
autoload :Head, "rack/head"
autoload :Headers, "rack/headers"
autoload :Lint, "rack/lint"
autoload :Lock, "rack/lock"
autoload :Logger, "rack/logger"
autoload :MediaType, "rack/media_type"
autoload :MethodOverride, "rack/method_override"
autoload :Mime, "rack/mime"
autoload :NullLogger, "rack/null_logger"
autoload :QueryParser, "rack/query_parser"
autoload :Recursive, "rack/recursive"
autoload :Reloader, "rack/reloader"
autoload :RewindableInput, "rack/rewindable_input"
autoload :Runtime, "rack/runtime"
autoload :Sendfile, "rack/sendfile"
autoload :Server, "rack/server"
autoload :ShowExceptions, "rack/show_exceptions"
autoload :ShowStatus, "rack/show_status"
autoload :Static, "rack/static"
autoload :TempfileReaper, "rack/tempfile_reaper"
autoload :URLMap, "rack/urlmap"
autoload :Utils, "rack/utils"
autoload :Multipart, "rack/multipart"
autoload :MockRequest, "rack/mock_request"
autoload :MockResponse, "rack/mock_response"
autoload :Request, "rack/request"
autoload :Response, "rack/response"
module Auth
autoload :Basic, "rack/auth/basic"
autoload :AbstractRequest, "rack/auth/abstract/request"
autoload :AbstractHandler, "rack/auth/abstract/handler"
autoload :Digest, "rack/auth/digest"
end
end

View File

@ -1,41 +0,0 @@
# frozen_string_literal: true
require_relative '../../constants'
module Rack
module Auth
# Rack::Auth::AbstractHandler implements common authentication functionality.
#
# +realm+ should be set for all handlers.
class AbstractHandler
attr_accessor :realm
def initialize(app, realm = nil, &authenticator)
@app, @realm, @authenticator = app, realm, authenticator
end
private
def unauthorized(www_authenticate = challenge)
return [ 401,
{ CONTENT_TYPE => 'text/plain',
CONTENT_LENGTH => '0',
'www-authenticate' => www_authenticate.to_s },
[]
]
end
def bad_request
return [ 400,
{ CONTENT_TYPE => 'text/plain',
CONTENT_LENGTH => '0' },
[]
]
end
end
end
end

View File

@ -1,49 +0,0 @@
# frozen_string_literal: true
require_relative '../../request'
module Rack
module Auth
class AbstractRequest
def initialize(env)
@env = env
end
def request
@request ||= Request.new(@env)
end
def provided?
!authorization_key.nil? && valid?
end
def valid?
!@env[authorization_key].nil?
end
def parts
@parts ||= @env[authorization_key].split(' ', 2)
end
def scheme
@scheme ||= parts.first&.downcase
end
def params
@params ||= parts.last
end
private
AUTHORIZATION_KEYS = ['HTTP_AUTHORIZATION', 'X-HTTP_AUTHORIZATION', 'X_HTTP_AUTHORIZATION']
def authorization_key
@authorization_key ||= AUTHORIZATION_KEYS.detect { |key| @env.has_key?(key) }
end
end
end
end

View File

@ -1,59 +0,0 @@
# frozen_string_literal: true
require_relative 'abstract/handler'
require_relative 'abstract/request'
require 'base64'
module Rack
module Auth
# Rack::Auth::Basic implements HTTP Basic Authentication, as per RFC 2617.
#
# Initialize with the Rack application that you want protecting,
# and a block that checks if a username and password pair are valid.
class Basic < AbstractHandler
def call(env)
auth = Basic::Request.new(env)
return unauthorized unless auth.provided?
return bad_request unless auth.basic?
if valid?(auth)
env['REMOTE_USER'] = auth.username
return @app.call(env)
end
unauthorized
end
private
def challenge
'Basic realm="%s"' % realm
end
def valid?(auth)
@authenticator.call(*auth.credentials)
end
class Request < Auth::AbstractRequest
def basic?
"basic" == scheme && credentials.length == 2
end
def credentials
@credentials ||= Base64.decode64(params).split(':', 2)
end
def username
credentials.first
end
end
end
end
end

View File

@ -1,256 +0,0 @@
# frozen_string_literal: true
require_relative 'abstract/handler'
require_relative 'abstract/request'
require 'digest/md5'
require 'base64'
module Rack
warn "Rack::Auth::Digest is deprecated and will be removed in Rack 3.1", uplevel: 1
module Auth
module Digest
# Rack::Auth::Digest::Nonce is the default nonce generator for the
# Rack::Auth::Digest::MD5 authentication handler.
#
# +private_key+ needs to set to a constant string.
#
# +time_limit+ can be optionally set to an integer (number of seconds),
# to limit the validity of the generated nonces.
class Nonce
class << self
attr_accessor :private_key, :time_limit
end
def self.parse(string)
new(*Base64.decode64(string).split(' ', 2))
end
def initialize(timestamp = Time.now, given_digest = nil)
@timestamp, @given_digest = timestamp.to_i, given_digest
end
def to_s
Base64.encode64("#{@timestamp} #{digest}").strip
end
def digest
::Digest::MD5.hexdigest("#{@timestamp}:#{self.class.private_key}")
end
def valid?
digest == @given_digest
end
def stale?
!self.class.time_limit.nil? && (Time.now.to_i - @timestamp) > self.class.time_limit
end
def fresh?
!stale?
end
end
class Params < Hash
def self.parse(str)
Params[*split_header_value(str).map do |param|
k, v = param.split('=', 2)
[k, dequote(v)]
end.flatten]
end
def self.dequote(str) # From WEBrick::HTTPUtils
ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
ret.gsub!(/\\(.)/, "\\1")
ret
end
def self.split_header_value(str)
str.scan(/\w+\=(?:"[^\"]+"|[^,]+)/n)
end
def initialize
super()
yield self if block_given?
end
def [](k)
super k.to_s
end
def []=(k, v)
super k.to_s, v.to_s
end
UNQUOTED = ['nc', 'stale']
def to_s
map do |k, v|
"#{k}=#{(UNQUOTED.include?(k) ? v.to_s : quote(v))}"
end.join(', ')
end
def quote(str) # From WEBrick::HTTPUtils
'"' + str.gsub(/[\\\"]/o, "\\\1") + '"'
end
end
class Request < Auth::AbstractRequest
def method
@env[RACK_METHODOVERRIDE_ORIGINAL_METHOD] || @env[REQUEST_METHOD]
end
def digest?
"digest" == scheme
end
def correct_uri?
request.fullpath == uri
end
def nonce
@nonce ||= Nonce.parse(params['nonce'])
end
def params
@params ||= Params.parse(parts.last)
end
def respond_to?(sym, *)
super or params.has_key? sym.to_s
end
def method_missing(sym, *args)
return super unless params.has_key?(key = sym.to_s)
return params[key] if args.size == 0
raise ArgumentError, "wrong number of arguments (#{args.size} for 0)"
end
end
# Rack::Auth::Digest::MD5 implements the MD5 algorithm version of
# HTTP Digest Authentication, as per RFC 2617.
#
# Initialize with the [Rack] application that you want protecting,
# and a block that looks up a plaintext password for a given username.
#
# +opaque+ needs to be set to a constant base64/hexadecimal string.
#
class MD5 < AbstractHandler
attr_accessor :opaque
attr_writer :passwords_hashed
def initialize(app, realm = nil, opaque = nil, &authenticator)
@passwords_hashed = nil
if opaque.nil? and realm.respond_to? :values_at
realm, opaque, @passwords_hashed = realm.values_at :realm, :opaque, :passwords_hashed
end
super(app, realm, &authenticator)
@opaque = opaque
end
def passwords_hashed?
!!@passwords_hashed
end
def call(env)
auth = Request.new(env)
unless auth.provided?
return unauthorized
end
if !auth.digest? || !auth.correct_uri? || !valid_qop?(auth)
return bad_request
end
if valid?(auth)
if auth.nonce.stale?
return unauthorized(challenge(stale: true))
else
env['REMOTE_USER'] = auth.username
return @app.call(env)
end
end
unauthorized
end
private
QOP = 'auth'
def params(hash = {})
Params.new do |params|
params['realm'] = realm
params['nonce'] = Nonce.new.to_s
params['opaque'] = H(opaque)
params['qop'] = QOP
hash.each { |k, v| params[k] = v }
end
end
def challenge(hash = {})
"Digest #{params(hash)}"
end
def valid?(auth)
valid_opaque?(auth) && valid_nonce?(auth) && valid_digest?(auth)
end
def valid_qop?(auth)
QOP == auth.qop
end
def valid_opaque?(auth)
H(opaque) == auth.opaque
end
def valid_nonce?(auth)
auth.nonce.valid?
end
def valid_digest?(auth)
pw = @authenticator.call(auth.username)
pw && Rack::Utils.secure_compare(digest(auth, pw), auth.response)
end
def md5(data)
::Digest::MD5.hexdigest(data)
end
alias :H :md5
def KD(secret, data)
H "#{secret}:#{data}"
end
def A1(auth, password)
"#{auth.username}:#{auth.realm}:#{password}"
end
def A2(auth)
"#{auth.method}:#{auth.uri}"
end
def digest(auth, password)
password_hash = passwords_hashed? ? password : H(A1(auth, password))
KD password_hash, "#{auth.nonce}:#{auth.nc}:#{auth.cnonce}:#{QOP}:#{H A2(auth)}"
end
end
end
end
end

View File

@ -1 +0,0 @@
require_relative '../digest'

View File

@ -1 +0,0 @@
require_relative '../digest'

View File

@ -1 +0,0 @@
require_relative '../digest'

View File

@ -1 +0,0 @@
require_relative '../digest'

View File

@ -1,47 +0,0 @@
# frozen_string_literal: true
module Rack
# Proxy for response bodies allowing calling a block when
# the response body is closed (after the response has been fully
# sent to the client).
class BodyProxy
# Set the response body to wrap, and the block to call when the
# response has been fully sent.
def initialize(body, &block)
@body = body
@block = block
@closed = false
end
# Return whether the wrapped body responds to the method.
def respond_to_missing?(method_name, include_all = false)
super or @body.respond_to?(method_name, include_all)
end
# If not already closed, close the wrapped body and
# then call the block the proxy was initialized with.
def close
return if @closed
@closed = true
begin
@body.close if @body.respond_to?(:close)
ensure
@block.call
end
end
# Whether the proxy is closed. The proxy starts as not closed,
# and becomes closed on the first call to close.
def closed?
@closed
end
# Delegate missing methods to the wrapped body.
def method_missing(method_name, *args, &block)
@body.__send__(method_name, *args, &block)
end
# :nocov:
ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
# :nocov:
end
end

View File

@ -1,277 +0,0 @@
# frozen_string_literal: true
require_relative 'urlmap'
module Rack
# Rack::Builder provides a domain-specific language (DSL) to construct Rack
# applications. It is primarily used to parse +config.ru+ files which
# instantiate several middleware and a final application which are hosted
# by a Rack-compatible web server.
#
# Example:
#
# app = Rack::Builder.new do
# use Rack::CommonLogger
# map "/ok" do
# run lambda { |env| [200, {'content-type' => 'text/plain'}, ['OK']] }
# end
# end
#
# run app
#
# Or
#
# app = Rack::Builder.app do
# use Rack::CommonLogger
# run lambda { |env| [200, {'content-type' => 'text/plain'}, ['OK']] }
# end
#
# run app
#
# +use+ adds middleware to the stack, +run+ dispatches to an application.
# You can use +map+ to construct a Rack::URLMap in a convenient way.
class Builder
# https://stackoverflow.com/questions/2223882/whats-the-difference-between-utf-8-and-utf-8-without-bom
UTF_8_BOM = '\xef\xbb\xbf'
# Parse the given config file to get a Rack application.
#
# If the config file ends in +.ru+, it is treated as a
# rackup file and the contents will be treated as if
# specified inside a Rack::Builder block.
#
# If the config file does not end in +.ru+, it is
# required and Rack will use the basename of the file
# to guess which constant will be the Rack application to run.
#
# Examples:
#
# Rack::Builder.parse_file('config.ru')
# # Rack application built using Rack::Builder.new
#
# Rack::Builder.parse_file('app.rb')
# # requires app.rb, which can be anywhere in Ruby's
# # load path. After requiring, assumes App constant
# # contains Rack application
#
# Rack::Builder.parse_file('./my_app.rb')
# # requires ./my_app.rb, which should be in the
# # process's current directory. After requiring,
# # assumes MyApp constant contains Rack application
def self.parse_file(path)
if path.end_with?('.ru')
return self.load_file(path)
else
require path
return Object.const_get(::File.basename(path, '.rb').split('_').map(&:capitalize).join(''))
end
end
# Load the given file as a rackup file, treating the
# contents as if specified inside a Rack::Builder block.
#
# Ignores content in the file after +__END__+, so that
# use of +__END__+ will not result in a syntax error.
#
# Example config.ru file:
#
# $ cat config.ru
#
# use Rack::ContentLength
# require './app.rb'
# run App
def self.load_file(path)
config = ::File.read(path)
config.slice!(/\A#{UTF_8_BOM}/) if config.encoding == Encoding::UTF_8
if config[/^#\\(.*)/]
fail "Parsing options from the first comment line is no longer supported: #{path}"
end
config.sub!(/^__END__\n.*\Z/m, '')
return new_from_string(config, path)
end
# Evaluate the given +builder_script+ string in the context of
# a Rack::Builder block, returning a Rack application.
def self.new_from_string(builder_script, file = "(rackup)")
# We want to build a variant of TOPLEVEL_BINDING with self as a Rack::Builder instance.
# We cannot use instance_eval(String) as that would resolve constants differently.
binding, builder = TOPLEVEL_BINDING.eval('Rack::Builder.new.instance_eval { [binding, self] }')
eval builder_script, binding, file
return builder.to_app
end
# Initialize a new Rack::Builder instance. +default_app+ specifies the
# default application if +run+ is not called later. If a block
# is given, it is evaluated in the context of the instance.
def initialize(default_app = nil, &block)
@use = []
@map = nil
@run = default_app
@warmup = nil
@freeze_app = false
instance_eval(&block) if block_given?
end
# Create a new Rack::Builder instance and return the Rack application
# generated from it.
def self.app(default_app = nil, &block)
self.new(default_app, &block).to_app
end
# Specifies middleware to use in a stack.
#
# class Middleware
# def initialize(app)
# @app = app
# end
#
# def call(env)
# env["rack.some_header"] = "setting an example"
# @app.call(env)
# end
# end
#
# use Middleware
# run lambda { |env| [200, { "content-type" => "text/plain" }, ["OK"]] }
#
# All requests through to this application will first be processed by the middleware class.
# The +call+ method in this example sets an additional environment key which then can be
# referenced in the application if required.
def use(middleware, *args, &block)
if @map
mapping, @map = @map, nil
@use << proc { |app| generate_map(app, mapping) }
end
@use << proc { |app| middleware.new(app, *args, &block) }
end
# :nocov:
ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true)
# :nocov:
# Takes a block or argument that is an object that responds to #call and
# returns a Rack response.
#
# You can use a block:
#
# run do |env|
# [200, { "content-type" => "text/plain" }, ["Hello World!"]]
# end
#
# You can also provide a lambda:
#
# run lambda { |env| [200, { "content-type" => "text/plain" }, ["OK"]] }
#
# You can also provide a class instance:
#
# class Heartbeat
# def call(env)
# [200, { "content-type" => "text/plain" }, ["OK"]]
# end
# end
#
# run Heartbeat.new
#
def run(app = nil, &block)
raise ArgumentError, "Both app and block given!" if app && block_given?
@run = app || block
end
# Takes a lambda or block that is used to warm-up the application. This block is called
# before the Rack application is returned by to_app.
#
# warmup do |app|
# client = Rack::MockRequest.new(app)
# client.get('/')
# end
#
# use SomeMiddleware
# run MyApp
def warmup(prc = nil, &block)
@warmup = prc || block
end
# Creates a route within the application. Routes under the mapped path will be sent to
# the Rack application specified by run inside the block. Other requests will be sent to the
# default application specified by run outside the block.
#
# class App
# def call(env)
# [200, {'content-type' => 'text/plain'}, ["Hello World"]]
# end
# end
#
# class Heartbeat
# def call(env)
# [200, { "content-type" => "text/plain" }, ["OK"]]
# end
# end
#
# app = Rack::Builder.app do
# map '/heartbeat' do
# run Heartbeat.new
# end
# run App.new
# end
#
# run app
#
# The +use+ method can also be used inside the block to specify middleware to run under a specific path:
#
# app = Rack::Builder.app do
# map '/heartbeat' do
# use Middleware
# run Heartbeat.new
# end
# run App.new
# end
#
# This example includes a piece of middleware which will run before +/heartbeat+ requests hit +Heartbeat+.
#
# Note that providing a +path+ of +/+ will ignore any default application given in a +run+ statement
# outside the block.
def map(path, &block)
@map ||= {}
@map[path] = block
end
# Freeze the app (set using run) and all middleware instances when building the application
# in to_app.
def freeze_app
@freeze_app = true
end
# Return the Rack application generated by this instance.
def to_app
app = @map ? generate_map(@run, @map) : @run
fail "missing run or map statement" unless app
app.freeze if @freeze_app
app = @use.reverse.inject(app) { |a, e| e[a].tap { |x| x.freeze if @freeze_app } }
@warmup.call(app) if @warmup
app
end
# Call the Rack application generated by this builder instance. Note that
# this rebuilds the Rack application and runs the warmup code (if any)
# every time it is called, so it should not be used if performance is important.
def call(env)
to_app.call(env)
end
private
# Generate a URLMap instance by generating new Rack applications for each
# map block in this instance.
def generate_map(default_app, mapping)
mapped = default_app ? { '/' => default_app } : {}
mapping.each { |r, b| mapped[r] = self.class.new(default_app, &b).to_app }
URLMap.new(mapped)
end
end
end

View File

@ -1,70 +0,0 @@
# frozen_string_literal: true
require_relative 'constants'
module Rack
# Rack::Cascade tries a request on several apps, and returns the
# first response that is not 404 or 405 (or in a list of configured
# status codes). If all applications tried return one of the configured
# status codes, return the last response.
class Cascade
# deprecated, no longer used
NotFound = [404, { CONTENT_TYPE => "text/plain" }, []]
# An array of applications to try in order.
attr_reader :apps
# Set the apps to send requests to, and what statuses result in
# cascading. Arguments:
#
# apps: An enumerable of rack applications.
# cascade_for: The statuses to use cascading for. If a response is received
# from an app, the next app is tried.
def initialize(apps, cascade_for = [404, 405])
@apps = []
apps.each { |app| add app }
@cascade_for = {}
[*cascade_for].each { |status| @cascade_for[status] = true }
end
# Call each app in order. If the responses uses a status that requires
# cascading, try the next app. If all responses require cascading,
# return the response from the last app.
def call(env)
return [404, { CONTENT_TYPE => "text/plain" }, []] if @apps.empty?
result = nil
last_body = nil
@apps.each do |app|
# The SPEC says that the body must be closed after it has been iterated
# by the server, or if it is replaced by a middleware action. Cascade
# replaces the body each time a cascade happens. It is assumed that nil
# does not respond to close, otherwise the previous application body
# will be closed. The final application body will not be closed, as it
# will be passed to the server as a result.
last_body.close if last_body.respond_to? :close
result = app.call(env)
return result unless @cascade_for.include?(result[0].to_i)
last_body = result[2]
end
result
end
# Append an app to the list of apps to cascade. This app will
# be tried last.
def add(app)
@apps << app
end
# Whether the given app is one of the apps to cascade to.
def include?(app)
@apps.include?(app)
end
alias_method :<<, :add
end
end

View File

@ -1,120 +0,0 @@
# frozen_string_literal: true
require_relative 'constants'
require_relative 'utils'
module Rack
warn "Rack::Chunked is deprecated and will be removed in Rack 3.1", uplevel: 1
# Middleware that applies chunked transfer encoding to response bodies
# when the response does not include a content-length header.
#
# This supports the trailer response header to allow the use of trailing
# headers in the chunked encoding. However, using this requires you manually
# specify a response body that supports a +trailers+ method. Example:
#
# [200, { 'trailer' => 'expires'}, ["Hello", "World"]]
# # error raised
#
# body = ["Hello", "World"]
# def body.trailers
# { 'expires' => Time.now.to_s }
# end
# [200, { 'trailer' => 'expires'}, body]
# # No exception raised
class Chunked
include Rack::Utils
# A body wrapper that emits chunked responses.
class Body
TERM = "\r\n"
TAIL = "0#{TERM}"
# Store the response body to be chunked.
def initialize(body)
@body = body
end
# For each element yielded by the response body, yield
# the element in chunked encoding.
def each(&block)
term = TERM
@body.each do |chunk|
size = chunk.bytesize
next if size == 0
yield [size.to_s(16), term, chunk.b, term].join
end
yield TAIL
yield_trailers(&block)
yield term
end
# Close the response body if the response body supports it.
def close
@body.close if @body.respond_to?(:close)
end
private
# Do nothing as this class does not support trailer headers.
def yield_trailers
end
end
# A body wrapper that emits chunked responses and also supports
# sending Trailer headers. Note that the response body provided to
# initialize must have a +trailers+ method that returns a hash
# of trailer headers, and the rack response itself should have a
# Trailer header listing the headers that the +trailers+ method
# will return.
class TrailerBody < Body
private
# Yield strings for each trailer header.
def yield_trailers
@body.trailers.each_pair do |k, v|
yield "#{k}: #{v}\r\n"
end
end
end
def initialize(app)
@app = app
end
# Whether the HTTP version supports chunked encoding (HTTP 1.1 does).
def chunkable_version?(ver)
case ver
# pre-HTTP/1.0 (informally "HTTP/0.9") HTTP requests did not have
# a version (nor response headers)
when 'HTTP/1.0', nil, 'HTTP/0.9'
false
else
true
end
end
# If the rack app returns a response that should have a body,
# but does not have content-length or transfer-encoding headers,
# modify the response to use chunked transfer-encoding.
def call(env)
status, headers, body = response = @app.call(env)
if chunkable_version?(env[SERVER_PROTOCOL]) &&
!STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) &&
!headers[CONTENT_LENGTH] &&
!headers[TRANSFER_ENCODING]
headers[TRANSFER_ENCODING] = 'chunked'
if headers['trailer']
response[2] = TrailerBody.new(body)
else
response[2] = Body.new(body)
end
end
response
end
end
end

View File

@ -1,88 +0,0 @@
# frozen_string_literal: true
require_relative 'constants'
require_relative 'utils'
require_relative 'body_proxy'
require_relative 'request'
module Rack
# Rack::CommonLogger forwards every request to the given +app+, and
# logs a line in the
# {Apache common log format}[http://httpd.apache.org/docs/1.3/logs.html#common]
# to the configured logger.
class CommonLogger
# Common Log Format: http://httpd.apache.org/docs/1.3/logs.html#common
#
# lilith.local - - [07/Aug/2006 23:58:02 -0400] "GET / HTTP/1.1" 500 -
#
# %{%s - %s [%s] "%s %s%s %s" %d %s\n} %
#
# The actual format is slightly different than the above due to the
# separation of SCRIPT_NAME and PATH_INFO, and because the elapsed
# time in seconds is included at the end.
FORMAT = %{%s - %s [%s] "%s %s%s%s %s" %d %s %0.4f\n}
# +logger+ can be any object that supports the +write+ or +<<+ methods,
# which includes the standard library Logger. These methods are called
# with a single string argument, the log message.
# If +logger+ is nil, CommonLogger will fall back <tt>env['rack.errors']</tt>.
def initialize(app, logger = nil)
@app = app
@logger = logger
end
# Log all requests in common_log format after a response has been
# returned. Note that if the app raises an exception, the request
# will not be logged, so if exception handling middleware are used,
# they should be loaded after this middleware. Additionally, because
# the logging happens after the request body has been fully sent, any
# exceptions raised during the sending of the response body will
# cause the request not to be logged.
def call(env)
began_at = Utils.clock_time
status, headers, body = response = @app.call(env)
response[2] = BodyProxy.new(body) { log(env, status, headers, began_at) }
response
end
private
# Log the request to the configured logger.
def log(env, status, response_headers, began_at)
request = Rack::Request.new(env)
length = extract_content_length(response_headers)
msg = sprintf(FORMAT,
request.ip || "-",
request.get_header("REMOTE_USER") || "-",
Time.now.strftime("%d/%b/%Y:%H:%M:%S %z"),
request.request_method,
request.script_name,
request.path_info,
request.query_string.empty? ? "" : "?#{request.query_string}",
request.get_header(SERVER_PROTOCOL),
status.to_s[0..3],
length,
Utils.clock_time - began_at)
msg.gsub!(/[^[:print:]\n]/) { |c| sprintf("\\x%x", c.ord) }
logger = @logger || request.get_header(RACK_ERRORS)
# Standard library logger doesn't support write but it supports << which actually
# calls to write on the log device without formatting
if logger.respond_to?(:write)
logger.write(msg)
else
logger << msg
end
end
# Attempt to determine the content length for the response to
# include it in the logged data.
def extract_content_length(headers)
value = headers[CONTENT_LENGTH]
!value || value.to_s == '0' ? '-' : value
end
end
end

View File

@ -1,86 +0,0 @@
# frozen_string_literal: true
require_relative 'constants'
require_relative 'utils'
require_relative 'body_proxy'
module Rack
# Middleware that enables conditional GET using if-none-match and
# if-modified-since. The application should set either or both of the
# last-modified or etag response headers according to RFC 2616. When
# either of the conditions is met, the response body is set to be zero
# length and the response status is set to 304 Not Modified.
#
# Applications that defer response body generation until the body's each
# message is received will avoid response body generation completely when
# a conditional GET matches.
#
# Adapted from Michael Klishin's Merb implementation:
# https://github.com/wycats/merb/blob/master/merb-core/lib/merb-core/rack/middleware/conditional_get.rb
class ConditionalGet
def initialize(app)
@app = app
end
# Return empty 304 response if the response has not been
# modified since the last request.
def call(env)
case env[REQUEST_METHOD]
when "GET", "HEAD"
status, headers, body = response = @app.call(env)
if status == 200 && fresh?(env, headers)
response[0] = 304
headers.delete(CONTENT_TYPE)
headers.delete(CONTENT_LENGTH)
response[2] = Rack::BodyProxy.new([]) do
body.close if body.respond_to?(:close)
end
end
response
else
@app.call(env)
end
end
private
# Return whether the response has not been modified since the
# last request.
def fresh?(env, headers)
# if-none-match has priority over if-modified-since per RFC 7232
if none_match = env['HTTP_IF_NONE_MATCH']
etag_matches?(none_match, headers)
elsif (modified_since = env['HTTP_IF_MODIFIED_SINCE']) && (modified_since = to_rfc2822(modified_since))
modified_since?(modified_since, headers)
end
end
# Whether the etag response header matches the if-none-match request header.
# If so, the request has not been modified.
def etag_matches?(none_match, headers)
headers[ETAG] == none_match
end
# Whether the last-modified response header matches the if-modified-since
# request header. If so, the request has not been modified.
def modified_since?(modified_since, headers)
last_modified = to_rfc2822(headers['last-modified']) and
modified_since >= last_modified
end
# Return a Time object for the given string (which should be in RFC2822
# format), or nil if the string cannot be parsed.
def to_rfc2822(since)
# shortest possible valid date is the obsolete: 1 Nov 97 09:55 A
# anything shorter is invalid, this avoids exceptions for common cases
# most common being the empty string
if since && since.length >= 16
# NOTE: there is no trivial way to write this in a non exception way
# _rfc2822 returns a hash but is not that usable
Time.rfc2822(since) rescue nil
end
end
end
end

View File

@ -1,22 +0,0 @@
# frozen_string_literal: true
module Rack
# Rack::Config modifies the environment using the block given during
# initialization.
#
# Example:
# use Rack::Config do |env|
# env['my-key'] = 'some-value'
# end
class Config
def initialize(app, &block)
@app = app
@block = block
end
def call(env)
@block.call(env)
@app.call(env)
end
end
end

View File

@ -1,64 +0,0 @@
# frozen_string_literal: true
module Rack
# Request env keys
HTTP_HOST = 'HTTP_HOST'
HTTP_PORT = 'HTTP_PORT'
HTTPS = 'HTTPS'
PATH_INFO = 'PATH_INFO'
REQUEST_METHOD = 'REQUEST_METHOD'
REQUEST_PATH = 'REQUEST_PATH'
SCRIPT_NAME = 'SCRIPT_NAME'
QUERY_STRING = 'QUERY_STRING'
SERVER_PROTOCOL = 'SERVER_PROTOCOL'
SERVER_NAME = 'SERVER_NAME'
SERVER_PORT = 'SERVER_PORT'
HTTP_COOKIE = 'HTTP_COOKIE'
# Response Header Keys
CACHE_CONTROL = 'cache-control'
CONTENT_LENGTH = 'content-length'
CONTENT_TYPE = 'content-type'
ETAG = 'etag'
EXPIRES = 'expires'
SET_COOKIE = 'set-cookie'
TRANSFER_ENCODING = 'transfer-encoding'
# HTTP method verbs
GET = 'GET'
POST = 'POST'
PUT = 'PUT'
PATCH = 'PATCH'
DELETE = 'DELETE'
HEAD = 'HEAD'
OPTIONS = 'OPTIONS'
LINK = 'LINK'
UNLINK = 'UNLINK'
TRACE = 'TRACE'
# Rack environment variables
RACK_VERSION = 'rack.version'
RACK_TEMPFILES = 'rack.tempfiles'
RACK_ERRORS = 'rack.errors'
RACK_LOGGER = 'rack.logger'
RACK_INPUT = 'rack.input'
RACK_SESSION = 'rack.session'
RACK_SESSION_OPTIONS = 'rack.session.options'
RACK_SHOWSTATUS_DETAIL = 'rack.showstatus.detail'
RACK_URL_SCHEME = 'rack.url_scheme'
RACK_HIJACK = 'rack.hijack'
RACK_IS_HIJACK = 'rack.hijack?'
RACK_RECURSIVE_INCLUDE = 'rack.recursive.include'
RACK_MULTIPART_BUFFER_SIZE = 'rack.multipart.buffer_size'
RACK_MULTIPART_TEMPFILE_FACTORY = 'rack.multipart.tempfile_factory'
RACK_RESPONSE_FINISHED = 'rack.response_finished'
RACK_REQUEST_FORM_INPUT = 'rack.request.form_input'
RACK_REQUEST_FORM_HASH = 'rack.request.form_hash'
RACK_REQUEST_FORM_VARS = 'rack.request.form_vars'
RACK_REQUEST_FORM_ERROR = 'rack.request.form_error'
RACK_REQUEST_COOKIE_HASH = 'rack.request.cookie_hash'
RACK_REQUEST_COOKIE_STRING = 'rack.request.cookie_string'
RACK_REQUEST_QUERY_HASH = 'rack.request.query_hash'
RACK_REQUEST_QUERY_STRING = 'rack.request.query_string'
RACK_METHODOVERRIDE_ORIGINAL_METHOD = 'rack.methodoverride.original_method'
end

View File

@ -1,34 +0,0 @@
# frozen_string_literal: true
require_relative 'constants'
require_relative 'utils'
module Rack
# Sets the content-length header on responses that do not specify
# a content-length or transfer-encoding header. Note that this
# does not fix responses that have an invalid content-length
# header specified.
class ContentLength
include Rack::Utils
def initialize(app)
@app = app
end
def call(env)
status, headers, body = response = @app.call(env)
if !STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) &&
!headers[CONTENT_LENGTH] &&
!headers[TRANSFER_ENCODING] &&
body.respond_to?(:to_ary)
response[2] = body = body.to_ary
headers[CONTENT_LENGTH] = body.sum(&:bytesize).to_s
end
response
end
end
end

View File

@ -1,33 +0,0 @@
# frozen_string_literal: true
require_relative 'constants'
require_relative 'utils'
module Rack
# Sets the content-type header on responses which don't have one.
#
# Builder Usage:
# use Rack::ContentType, "text/plain"
#
# When no content type argument is provided, "text/html" is the
# default.
class ContentType
include Rack::Utils
def initialize(app, content_type = "text/html")
@app = app
@content_type = content_type
end
def call(env)
status, headers, _ = response = @app.call(env)
unless STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i)
headers[CONTENT_TYPE] ||= @content_type
end
response
end
end
end

View File

@ -1,158 +0,0 @@
# frozen_string_literal: true
require "zlib"
require "time" # for Time.httpdate
require_relative 'constants'
require_relative 'utils'
require_relative 'request'
require_relative 'body_proxy'
module Rack
# This middleware enables content encoding of http responses,
# usually for purposes of compression.
#
# Currently supported encodings:
#
# * gzip
# * identity (no transformation)
#
# This middleware automatically detects when encoding is supported
# and allowed. For example no encoding is made when a cache
# directive of 'no-transform' is present, when the response status
# code is one that doesn't allow an entity body, or when the body
# is empty.
#
# Note that despite the name, Deflater does not support the +deflate+
# encoding.
class Deflater
# Creates Rack::Deflater middleware. Options:
#
# :if :: a lambda enabling / disabling deflation based on returned boolean value
# (e.g <tt>use Rack::Deflater, :if => lambda { |*, body| sum=0; body.each { |i| sum += i.length }; sum > 512 }</tt>).
# However, be aware that calling `body.each` inside the block will break cases where `body.each` is not idempotent,
# such as when it is an +IO+ instance.
# :include :: a list of content types that should be compressed. By default, all content types are compressed.
# :sync :: determines if the stream is going to be flushed after every chunk. Flushing after every chunk reduces
# latency for time-sensitive streaming applications, but hurts compression and throughput.
# Defaults to +true+.
def initialize(app, options = {})
@app = app
@condition = options[:if]
@compressible_types = options[:include]
@sync = options.fetch(:sync, true)
end
def call(env)
status, headers, body = response = @app.call(env)
unless should_deflate?(env, status, headers, body)
return response
end
request = Request.new(env)
encoding = Utils.select_best_encoding(%w(gzip identity),
request.accept_encoding)
# Set the Vary HTTP header.
vary = headers["vary"].to_s.split(",").map(&:strip)
unless vary.include?("*") || vary.any?{|v| v.downcase == 'accept-encoding'}
headers["vary"] = vary.push("Accept-Encoding").join(",")
end
case encoding
when "gzip"
headers['content-encoding'] = "gzip"
headers.delete(CONTENT_LENGTH)
mtime = headers["last-modified"]
mtime = Time.httpdate(mtime).to_i if mtime
response[2] = GzipStream.new(body, mtime, @sync)
response
when "identity"
response
else # when nil
# Only possible encoding values here are 'gzip', 'identity', and nil
message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
bp = Rack::BodyProxy.new([message]) { body.close if body.respond_to?(:close) }
[406, { CONTENT_TYPE => "text/plain", CONTENT_LENGTH => message.length.to_s }, bp]
end
end
# Body class used for gzip encoded responses.
class GzipStream
BUFFER_LENGTH = 128 * 1_024
# Initialize the gzip stream. Arguments:
# body :: Response body to compress with gzip
# mtime :: The modification time of the body, used to set the
# modification time in the gzip header.
# sync :: Whether to flush each gzip chunk as soon as it is ready.
def initialize(body, mtime, sync)
@body = body
@mtime = mtime
@sync = sync
end
# Yield gzip compressed strings to the given block.
def each(&block)
@writer = block
gzip = ::Zlib::GzipWriter.new(self)
gzip.mtime = @mtime if @mtime
# @body.each is equivalent to @body.gets (slow)
if @body.is_a? ::File # XXX: Should probably be ::IO
while part = @body.read(BUFFER_LENGTH)
gzip.write(part)
gzip.flush if @sync
end
else
@body.each { |part|
# Skip empty strings, as they would result in no output,
# and flushing empty parts would raise Zlib::BufError.
next if part.empty?
gzip.write(part)
gzip.flush if @sync
}
end
ensure
gzip.finish
end
# Call the block passed to #each with the gzipped data.
def write(data)
@writer.call(data)
end
# Close the original body if possible.
def close
@body.close if @body.respond_to?(:close)
end
end
private
# Whether the body should be compressed.
def should_deflate?(env, status, headers, body)
# Skip compressing empty entity body responses and responses with
# no-transform set.
if Utils::STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) ||
/\bno-transform\b/.match?(headers[CACHE_CONTROL].to_s) ||
headers['content-encoding']&.!~(/\bidentity\b/)
return false
end
# Skip if @compressible_types are given and does not include request's content type
return false if @compressible_types && !(headers.has_key?(CONTENT_TYPE) && @compressible_types.include?(headers[CONTENT_TYPE][/[^;]*/]))
# Skip if @condition lambda is given and evaluates to false
return false if @condition && !@condition.call(env, status, headers, body)
# No point in compressing empty body, also handles usage with
# Rack::Sendfile.
return false if headers[CONTENT_LENGTH] == '0'
true
end
end
end

View File

@ -1,205 +0,0 @@
# frozen_string_literal: true
require 'time'
require_relative 'constants'
require_relative 'utils'
require_relative 'head'
require_relative 'mime'
require_relative 'files'
module Rack
# Rack::Directory serves entries below the +root+ given, according to the
# path info of the Rack request. If a directory is found, the file's contents
# will be presented in an html based index. If a file is found, the env will
# be passed to the specified +app+.
#
# If +app+ is not specified, a Rack::Files of the same +root+ will be used.
class Directory
DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>\n"
DIR_PAGE_HEADER = <<-PAGE
<html><head>
<title>%s</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<style type='text/css'>
table { width:100%%; }
.name { text-align:left; }
.size, .mtime { text-align:right; }
.type { width:11em; }
.mtime { width:15em; }
</style>
</head><body>
<h1>%s</h1>
<hr />
<table>
<tr>
<th class='name'>Name</th>
<th class='size'>Size</th>
<th class='type'>Type</th>
<th class='mtime'>Last Modified</th>
</tr>
PAGE
DIR_PAGE_FOOTER = <<-PAGE
</table>
<hr />
</body></html>
PAGE
# Body class for directory entries, showing an index page with links
# to each file.
class DirectoryBody < Struct.new(:root, :path, :files)
# Yield strings for each part of the directory entry
def each
show_path = Utils.escape_html(path.sub(/^#{root}/, ''))
yield(DIR_PAGE_HEADER % [ show_path, show_path ])
unless path.chomp('/') == root
yield(DIR_FILE % DIR_FILE_escape(files.call('..')))
end
Dir.foreach(path) do |basename|
next if basename.start_with?('.')
next unless f = files.call(basename)
yield(DIR_FILE % DIR_FILE_escape(f))
end
yield(DIR_PAGE_FOOTER)
end
private
# Escape each element in the array of html strings.
def DIR_FILE_escape(htmls)
htmls.map { |e| Utils.escape_html(e) }
end
end
# The root of the directory hierarchy. Only requests for files and
# directories inside of the root directory are supported.
attr_reader :root
# Set the root directory and application for serving files.
def initialize(root, app = nil)
@root = ::File.expand_path(root)
@app = app || Files.new(@root)
@head = Head.new(method(:get))
end
def call(env)
# strip body if this is a HEAD call
@head.call env
end
# Internals of request handling. Similar to call but does
# not remove body for HEAD requests.
def get(env)
script_name = env[SCRIPT_NAME]
path_info = Utils.unescape_path(env[PATH_INFO])
if client_error_response = check_bad_request(path_info) || check_forbidden(path_info)
client_error_response
else
path = ::File.join(@root, path_info)
list_path(env, path, path_info, script_name)
end
end
# Rack response to use for requests with invalid paths, or nil if path is valid.
def check_bad_request(path_info)
return if Utils.valid_path?(path_info)
body = "Bad Request\n"
[400, { CONTENT_TYPE => "text/plain",
CONTENT_LENGTH => body.bytesize.to_s,
"x-cascade" => "pass" }, [body]]
end
# Rack response to use for requests with paths outside the root, or nil if path is inside the root.
def check_forbidden(path_info)
return unless path_info.include? ".."
return if ::File.expand_path(::File.join(@root, path_info)).start_with?(@root)
body = "Forbidden\n"
[403, { CONTENT_TYPE => "text/plain",
CONTENT_LENGTH => body.bytesize.to_s,
"x-cascade" => "pass" }, [body]]
end
# Rack response to use for directories under the root.
def list_directory(path_info, path, script_name)
url_head = (script_name.split('/') + path_info.split('/')).map do |part|
Utils.escape_path part
end
# Globbing not safe as path could contain glob metacharacters
body = DirectoryBody.new(@root, path, ->(basename) do
stat = stat(::File.join(path, basename))
next unless stat
url = ::File.join(*url_head + [Utils.escape_path(basename)])
mtime = stat.mtime.httpdate
if stat.directory?
type = 'directory'
size = '-'
url << '/'
if basename == '..'
basename = 'Parent Directory'
else
basename << '/'
end
else
type = Mime.mime_type(::File.extname(basename))
size = filesize_format(stat.size)
end
[ url, basename, size, type, mtime ]
end)
[ 200, { CONTENT_TYPE => 'text/html; charset=utf-8' }, body ]
end
# File::Stat for the given path, but return nil for missing/bad entries.
def stat(path)
::File.stat(path)
rescue Errno::ENOENT, Errno::ELOOP
return nil
end
# Rack response to use for files and directories under the root.
# Unreadable and non-file, non-directory entries will get a 404 response.
def list_path(env, path, path_info, script_name)
if (stat = stat(path)) && stat.readable?
return @app.call(env) if stat.file?
return list_directory(path_info, path, script_name) if stat.directory?
end
entity_not_found(path_info)
end
# Rack response to use for unreadable and non-file, non-directory entries.
def entity_not_found(path_info)
body = "Entity not found: #{path_info}\n"
[404, { CONTENT_TYPE => "text/plain",
CONTENT_LENGTH => body.bytesize.to_s,
"x-cascade" => "pass" }, [body]]
end
# Stolen from Ramaze
FILESIZE_FORMAT = [
['%.1fT', 1 << 40],
['%.1fG', 1 << 30],
['%.1fM', 1 << 20],
['%.1fK', 1 << 10],
]
# Provide human readable file sizes
def filesize_format(int)
FILESIZE_FORMAT.each do |format, size|
return format % (int.to_f / size) if int >= size
end
"#{int}B"
end
end
end

View File

@ -1,68 +0,0 @@
# frozen_string_literal: true
require 'digest/sha2'
require_relative 'constants'
require_relative 'utils'
module Rack
# Automatically sets the etag header on all String bodies.
#
# The etag header is skipped if etag or last-modified headers are sent or if
# a sendfile body (body.responds_to :to_path) is given (since such cases
# should be handled by apache/nginx).
#
# On initialization, you can pass two parameters: a cache-control directive
# used when etag is absent and a directive when it is present. The first
# defaults to nil, while the second defaults to "max-age=0, private, must-revalidate"
class ETag
ETAG_STRING = Rack::ETAG
DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate"
def initialize(app, no_cache_control = nil, cache_control = DEFAULT_CACHE_CONTROL)
@app = app
@cache_control = cache_control
@no_cache_control = no_cache_control
end
def call(env)
status, headers, body = response = @app.call(env)
if etag_status?(status) && body.respond_to?(:to_ary) && !skip_caching?(headers)
body = body.to_ary
digest = digest_body(body)
headers[ETAG_STRING] = %(W/"#{digest}") if digest
end
unless headers[CACHE_CONTROL]
if digest
headers[CACHE_CONTROL] = @cache_control if @cache_control
else
headers[CACHE_CONTROL] = @no_cache_control if @no_cache_control
end
end
response
end
private
def etag_status?(status)
status == 200 || status == 201
end
def skip_caching?(headers)
headers.key?(ETAG_STRING) || headers.key?('last-modified')
end
def digest_body(body)
digest = nil
body.each do |part|
(digest ||= Digest::SHA256.new) << part unless part.empty?
end
digest && digest.hexdigest.byteslice(0,32)
end
end
end

View File

@ -1,157 +0,0 @@
# frozen_string_literal: true
require_relative 'body_proxy'
require_relative 'request'
require_relative 'response'
module Rack
### This middleware provides hooks to certain places in the request /
# response lifecycle. This is so that middleware that don't need to filter
# the response data can safely leave it alone and not have to send messages
# down the traditional "rack stack".
#
# The events are:
#
# * on_start(request, response)
#
# This event is sent at the start of the request, before the next
# middleware in the chain is called. This method is called with a request
# object, and a response object. Right now, the response object is always
# nil, but in the future it may actually be a real response object.
#
# * on_commit(request, response)
#
# The response has been committed. The application has returned, but the
# response has not been sent to the webserver yet. This method is always
# called with a request object and the response object. The response
# object is constructed from the rack triple that the application returned.
# Changes may still be made to the response object at this point.
#
# * on_send(request, response)
#
# The webserver has started iterating over the response body and presumably
# has started sending data over the wire. This method is always called with
# a request object and the response object. The response object is
# constructed from the rack triple that the application returned. Changes
# SHOULD NOT be made to the response object as the webserver has already
# started sending data. Any mutations will likely result in an exception.
#
# * on_finish(request, response)
#
# The webserver has closed the response, and all data has been written to
# the response socket. The request and response object should both be
# read-only at this point. The body MAY NOT be available on the response
# object as it may have been flushed to the socket.
#
# * on_error(request, response, error)
#
# An exception has occurred in the application or an `on_commit` event.
# This method will get the request, the response (if available) and the
# exception that was raised.
#
# ## Order
#
# `on_start` is called on the handlers in the order that they were passed to
# the constructor. `on_commit`, on_send`, `on_finish`, and `on_error` are
# called in the reverse order. `on_finish` handlers are called inside an
# `ensure` block, so they are guaranteed to be called even if something
# raises an exception. If something raises an exception in a `on_finish`
# method, then nothing is guaranteed.
class Events
module Abstract
def on_start(req, res)
end
def on_commit(req, res)
end
def on_send(req, res)
end
def on_finish(req, res)
end
def on_error(req, res, e)
end
end
class EventedBodyProxy < Rack::BodyProxy # :nodoc:
attr_reader :request, :response
def initialize(body, request, response, handlers, &block)
super(body, &block)
@request = request
@response = response
@handlers = handlers
end
def each
@handlers.reverse_each { |handler| handler.on_send request, response }
super
end
end
class BufferedResponse < Rack::Response::Raw # :nodoc:
attr_reader :body
def initialize(status, headers, body)
super(status, headers)
@body = body
end
def to_a; [status, headers, body]; end
end
def initialize(app, handlers)
@app = app
@handlers = handlers
end
def call(env)
request = make_request env
on_start request, nil
begin
status, headers, body = @app.call request.env
response = make_response status, headers, body
on_commit request, response
rescue StandardError => e
on_error request, response, e
on_finish request, response
raise
end
body = EventedBodyProxy.new(body, request, response, @handlers) do
on_finish request, response
end
[response.status, response.headers, body]
end
private
def on_error(request, response, e)
@handlers.reverse_each { |handler| handler.on_error request, response, e }
end
def on_commit(request, response)
@handlers.reverse_each { |handler| handler.on_commit request, response }
end
def on_start(request, response)
@handlers.each { |handler| handler.on_start request, nil }
end
def on_finish(request, response)
@handlers.reverse_each { |handler| handler.on_finish request, response }
end
def make_request(env)
Rack::Request.new env
end
def make_response(status, headers, body)
BufferedResponse.new status, headers, body
end
end
end

View File

@ -1,9 +0,0 @@
# frozen_string_literal: true
require_relative 'files'
module Rack
warn "Rack::File is deprecated and will be removed in Rack 3.1", uplevel: 1
File = Files
end

View File

@ -1,216 +0,0 @@
# frozen_string_literal: true
require 'time'
require_relative 'constants'
require_relative 'head'
require_relative 'utils'
require_relative 'request'
require_relative 'mime'
module Rack
# Rack::Files serves files below the +root+ directory given, according to the
# path info of the Rack request.
# e.g. when Rack::Files.new("/etc") is used, you can access 'passwd' file
# as http://localhost:9292/passwd
#
# Handlers can detect if bodies are a Rack::Files, and use mechanisms
# like sendfile on the +path+.
class Files
ALLOWED_VERBS = %w[GET HEAD OPTIONS]
ALLOW_HEADER = ALLOWED_VERBS.join(', ')
MULTIPART_BOUNDARY = 'AaB03x'
attr_reader :root
def initialize(root, headers = {}, default_mime = 'text/plain')
@root = (::File.expand_path(root) if root)
@headers = headers
@default_mime = default_mime
@head = Rack::Head.new(lambda { |env| get env })
end
def call(env)
# HEAD requests drop the response body, including 4xx error messages.
@head.call env
end
def get(env)
request = Rack::Request.new env
unless ALLOWED_VERBS.include? request.request_method
return fail(405, "Method Not Allowed", { 'allow' => ALLOW_HEADER })
end
path_info = Utils.unescape_path request.path_info
return fail(400, "Bad Request") unless Utils.valid_path?(path_info)
clean_path_info = Utils.clean_path_info(path_info)
path = ::File.join(@root, clean_path_info)
available = begin
::File.file?(path) && ::File.readable?(path)
rescue SystemCallError
# Not sure in what conditions this exception can occur, but this
# is a safe way to handle such an error.
# :nocov:
false
# :nocov:
end
if available
serving(request, path)
else
fail(404, "File not found: #{path_info}")
end
end
def serving(request, path)
if request.options?
return [200, { 'allow' => ALLOW_HEADER, CONTENT_LENGTH => '0' }, []]
end
last_modified = ::File.mtime(path).httpdate
return [304, {}, []] if request.get_header('HTTP_IF_MODIFIED_SINCE') == last_modified
headers = { "last-modified" => last_modified }
mime_type = mime_type path, @default_mime
headers[CONTENT_TYPE] = mime_type if mime_type
# Set custom headers
headers.merge!(@headers) if @headers
status = 200
size = filesize path
ranges = Rack::Utils.get_byte_ranges(request.get_header('HTTP_RANGE'), size)
if ranges.nil?
# No ranges:
ranges = [0..size - 1]
elsif ranges.empty?
# Unsatisfiable. Return error, and file size:
response = fail(416, "Byte range unsatisfiable")
response[1]["content-range"] = "bytes */#{size}"
return response
else
# Partial content
partial_content = true
if ranges.size == 1
range = ranges[0]
headers["content-range"] = "bytes #{range.begin}-#{range.end}/#{size}"
else
headers[CONTENT_TYPE] = "multipart/byteranges; boundary=#{MULTIPART_BOUNDARY}"
end
status = 206
body = BaseIterator.new(path, ranges, mime_type: mime_type, size: size)
size = body.bytesize
end
headers[CONTENT_LENGTH] = size.to_s
if request.head?
body = []
elsif !partial_content
body = Iterator.new(path, ranges, mime_type: mime_type, size: size)
end
[status, headers, body]
end
class BaseIterator
attr_reader :path, :ranges, :options
def initialize(path, ranges, options)
@path = path
@ranges = ranges
@options = options
end
def each
::File.open(path, "rb") do |file|
ranges.each do |range|
yield multipart_heading(range) if multipart?
each_range_part(file, range) do |part|
yield part
end
end
yield "\r\n--#{MULTIPART_BOUNDARY}--\r\n" if multipart?
end
end
def bytesize
size = ranges.inject(0) do |sum, range|
sum += multipart_heading(range).bytesize if multipart?
sum += range.size
end
size += "\r\n--#{MULTIPART_BOUNDARY}--\r\n".bytesize if multipart?
size
end
def close; end
private
def multipart?
ranges.size > 1
end
def multipart_heading(range)
<<-EOF
\r
--#{MULTIPART_BOUNDARY}\r
content-type: #{options[:mime_type]}\r
content-range: bytes #{range.begin}-#{range.end}/#{options[:size]}\r
\r
EOF
end
def each_range_part(file, range)
file.seek(range.begin)
remaining_len = range.end - range.begin + 1
while remaining_len > 0
part = file.read([8192, remaining_len].min)
break unless part
remaining_len -= part.length
yield part
end
end
end
class Iterator < BaseIterator
alias :to_path :path
end
private
def fail(status, body, headers = {})
body += "\n"
[
status,
{
CONTENT_TYPE => "text/plain",
CONTENT_LENGTH => body.size.to_s,
"x-cascade" => "pass"
}.merge!(headers),
[body]
]
end
# The MIME type for the contents of the file located at @path
def mime_type(path, default_mime)
Mime.mime_type(::File.extname(path), default_mime)
end
def filesize(path)
# We check via File::size? whether this file provides size info
# via stat (e.g. /proc files often don't), otherwise we have to
# figure it out by reading the whole file into memory.
::File.size?(path) || ::File.read(path).bytesize
end
end
end

View File

@ -1,26 +0,0 @@
# frozen_string_literal: true
require_relative 'constants'
require_relative 'body_proxy'
module Rack
# Rack::Head returns an empty body for all HEAD requests. It leaves
# all other requests unchanged.
class Head
def initialize(app)
@app = app
end
def call(env)
_, _, body = response = @app.call(env)
if env[REQUEST_METHOD] == HEAD
response[2] = Rack::BodyProxy.new([]) do
body.close if body.respond_to? :close
end
end
response
end
end
end

View File

@ -1,154 +0,0 @@
module Rack
# Rack::Headers is a Hash subclass that downcases all keys. It's designed
# to be used by rack applications that don't implement the Rack 3 SPEC
# (by using non-lowercase response header keys), automatically handling
# the downcasing of keys.
class Headers < Hash
def self.[](*items)
if items.length % 2 != 0
if items.length == 1 && items.first.is_a?(Hash)
new.merge!(items.first)
else
raise ArgumentError, "odd number of arguments for Rack::Headers"
end
else
hash = new
loop do
break if items.length == 0
key = items.shift
value = items.shift
hash[key] = value
end
hash
end
end
def [](key)
super(downcase_key(key))
end
def []=(key, value)
super(key.downcase.freeze, value)
end
alias store []=
def assoc(key)
super(downcase_key(key))
end
def compare_by_identity
raise TypeError, "Rack::Headers cannot compare by identity, use regular Hash"
end
def delete(key)
super(downcase_key(key))
end
def dig(key, *a)
super(downcase_key(key), *a)
end
def fetch(key, *default, &block)
key = downcase_key(key)
super
end
def fetch_values(*a)
super(*a.map!{|key| downcase_key(key)})
end
def has_key?(key)
super(downcase_key(key))
end
alias include? has_key?
alias key? has_key?
alias member? has_key?
def invert
hash = self.class.new
each{|key, value| hash[value] = key}
hash
end
def merge(hash, &block)
dup.merge!(hash, &block)
end
def reject(&block)
hash = dup
hash.reject!(&block)
hash
end
def replace(hash)
clear
update(hash)
end
def select(&block)
hash = dup
hash.select!(&block)
hash
end
def to_proc
lambda{|x| self[x]}
end
def transform_values(&block)
dup.transform_values!(&block)
end
def update(hash, &block)
hash.each do |key, value|
self[key] = if block_given? && include?(key)
block.call(key, self[key], value)
else
value
end
end
self
end
alias merge! update
def values_at(*keys)
keys.map{|key| self[key]}
end
# :nocov:
if RUBY_VERSION >= '2.5'
# :nocov:
def slice(*a)
h = self.class.new
a.each{|k| h[k] = self[k] if has_key?(k)}
h
end
def transform_keys(&block)
dup.transform_keys!(&block)
end
def transform_keys!
hash = self.class.new
each do |k, v|
hash[yield k] = v
end
replace(hash)
end
end
# :nocov:
if RUBY_VERSION >= '3.0'
# :nocov:
def except(*a)
super(*a.map!{|key| downcase_key(key)})
end
end
private
def downcase_key(key)
key.is_a?(String) ? key.downcase : key
end
end
end

View File

@ -1,907 +0,0 @@
# frozen_string_literal: true
require 'forwardable'
require_relative 'constants'
require_relative 'utils'
module Rack
# Rack::Lint validates your application and the requests and
# responses according to the Rack spec.
class Lint
def initialize(app)
@app = app
end
# :stopdoc:
class LintError < RuntimeError; end
# AUTHORS: n.b. The trailing whitespace between paragraphs is important and
# should not be removed. The whitespace creates paragraphs in the RDoc
# output.
#
## This specification aims to formalize the Rack protocol. You
## can (and should) use Rack::Lint to enforce it.
##
## When you develop middleware, be sure to add a Lint before and
## after to catch all mistakes.
##
## = Rack applications
##
## A Rack application is a Ruby object (not a class) that
## responds to +call+.
def call(env = nil)
Wrapper.new(@app, env).response
end
class Wrapper
def initialize(app, env)
@app = app
@env = env
@response = nil
@head_request = false
@status = nil
@headers = nil
@body = nil
@invoked = nil
@content_length = nil
@closed = false
@size = 0
end
def response
## It takes exactly one argument, the *environment*
raise LintError, "No env given" unless @env
check_environment(@env)
@env[RACK_INPUT] = InputWrapper.new(@env[RACK_INPUT])
@env[RACK_ERRORS] = ErrorWrapper.new(@env[RACK_ERRORS])
## and returns a non-frozen Array of exactly three values:
@response = @app.call(@env)
raise LintError, "response is not an Array, but #{@response.class}" unless @response.kind_of? Array
raise LintError, "response is frozen" if @response.frozen?
raise LintError, "response array has #{@response.size} elements instead of 3" unless @response.size == 3
@status, @headers, @body = @response
## The *status*,
check_status(@status)
## the *headers*,
check_headers(@headers)
hijack_proc = check_hijack_response(@headers, @env)
if hijack_proc
@headers[RACK_HIJACK] = hijack_proc
end
## and the *body*.
check_content_type(@status, @headers)
check_content_length(@status, @headers)
@head_request = @env[REQUEST_METHOD] == HEAD
@lint = (@env['rack.lint'] ||= []) << self
if (@env['rack.lint.body_iteration'] ||= 0) > 0
raise LintError, "Middleware must not call #each directly"
end
return [@status, @headers, self]
end
##
## == The Environment
##
def check_environment(env)
## The environment must be an unfrozen instance of Hash that includes
## CGI-like headers. The Rack application is free to modify the
## environment.
raise LintError, "env #{env.inspect} is not a Hash, but #{env.class}" unless env.kind_of? Hash
raise LintError, "env should not be frozen, but is" if env.frozen?
##
## The environment is required to include these variables
## (adopted from {PEP 333}[https://peps.python.org/pep-0333/]), except when they'd be empty, but see
## below.
## <tt>REQUEST_METHOD</tt>:: The HTTP request method, such as
## "GET" or "POST". This cannot ever
## be an empty string, and so is
## always required.
## <tt>SCRIPT_NAME</tt>:: The initial portion of the request
## URL's "path" that corresponds to the
## application object, so that the
## application knows its virtual
## "location". This may be an empty
## string, if the application corresponds
## to the "root" of the server.
## <tt>PATH_INFO</tt>:: The remainder of the request URL's
## "path", designating the virtual
## "location" of the request's target
## within the application. This may be an
## empty string, if the request URL targets
## the application root and does not have a
## trailing slash. This value may be
## percent-encoded when originating from
## a URL.
## <tt>QUERY_STRING</tt>:: The portion of the request URL that
## follows the <tt>?</tt>, if any. May be
## empty, but is always required!
## <tt>SERVER_NAME</tt>:: When combined with <tt>SCRIPT_NAME</tt> and
## <tt>PATH_INFO</tt>, these variables can be
## used to complete the URL. Note, however,
## that <tt>HTTP_HOST</tt>, if present,
## should be used in preference to
## <tt>SERVER_NAME</tt> for reconstructing
## the request URL.
## <tt>SERVER_NAME</tt> can never be an empty
## string, and so is always required.
## <tt>SERVER_PORT</tt>:: An optional +Integer+ which is the port the
## server is running on. Should be specified if
## the server is running on a non-standard port.
## <tt>SERVER_PROTOCOL</tt>:: A string representing the HTTP version used
## for the request.
## <tt>HTTP_</tt> Variables:: Variables corresponding to the
## client-supplied HTTP request
## headers (i.e., variables whose
## names begin with <tt>HTTP_</tt>). The
## presence or absence of these
## variables should correspond with
## the presence or absence of the
## appropriate HTTP header in the
## request. See
## {RFC3875 section 4.1.18}[https://tools.ietf.org/html/rfc3875#section-4.1.18]
## for specific behavior.
## In addition to this, the Rack environment must include these
## Rack-specific variables:
## <tt>rack.url_scheme</tt>:: +http+ or +https+, depending on the
## request URL.
## <tt>rack.input</tt>:: See below, the input stream.
## <tt>rack.errors</tt>:: See below, the error stream.
## <tt>rack.hijack?</tt>:: See below, if present and true, indicates
## that the server supports partial hijacking.
## <tt>rack.hijack</tt>:: See below, if present, an object responding
## to +call+ that is used to perform a full
## hijack.
## Additional environment specifications have approved to
## standardized middleware APIs. None of these are required to
## be implemented by the server.
## <tt>rack.session</tt>:: A hash-like interface for storing
## request session data.
## The store must implement:
if session = env[RACK_SESSION]
## store(key, value) (aliased as []=);
unless session.respond_to?(:store) && session.respond_to?(:[]=)
raise LintError, "session #{session.inspect} must respond to store and []="
end
## fetch(key, default = nil) (aliased as []);
unless session.respond_to?(:fetch) && session.respond_to?(:[])
raise LintError, "session #{session.inspect} must respond to fetch and []"
end
## delete(key);
unless session.respond_to?(:delete)
raise LintError, "session #{session.inspect} must respond to delete"
end
## clear;
unless session.respond_to?(:clear)
raise LintError, "session #{session.inspect} must respond to clear"
end
## to_hash (returning unfrozen Hash instance);
unless session.respond_to?(:to_hash) && session.to_hash.kind_of?(Hash) && !session.to_hash.frozen?
raise LintError, "session #{session.inspect} must respond to to_hash and return unfrozen Hash instance"
end
end
## <tt>rack.logger</tt>:: A common object interface for logging messages.
## The object must implement:
if logger = env[RACK_LOGGER]
## info(message, &block)
unless logger.respond_to?(:info)
raise LintError, "logger #{logger.inspect} must respond to info"
end
## debug(message, &block)
unless logger.respond_to?(:debug)
raise LintError, "logger #{logger.inspect} must respond to debug"
end
## warn(message, &block)
unless logger.respond_to?(:warn)
raise LintError, "logger #{logger.inspect} must respond to warn"
end
## error(message, &block)
unless logger.respond_to?(:error)
raise LintError, "logger #{logger.inspect} must respond to error"
end
## fatal(message, &block)
unless logger.respond_to?(:fatal)
raise LintError, "logger #{logger.inspect} must respond to fatal"
end
end
## <tt>rack.multipart.buffer_size</tt>:: An Integer hint to the multipart parser as to what chunk size to use for reads and writes.
if bufsize = env[RACK_MULTIPART_BUFFER_SIZE]
unless bufsize.is_a?(Integer) && bufsize > 0
raise LintError, "rack.multipart.buffer_size must be an Integer > 0 if specified"
end
end
## <tt>rack.multipart.tempfile_factory</tt>:: An object responding to #call with two arguments, the filename and content_type given for the multipart form field, and returning an IO-like object that responds to #<< and optionally #rewind. This factory will be used to instantiate the tempfile for each multipart form file upload field, rather than the default class of Tempfile.
if tempfile_factory = env[RACK_MULTIPART_TEMPFILE_FACTORY]
raise LintError, "rack.multipart.tempfile_factory must respond to #call" unless tempfile_factory.respond_to?(:call)
env[RACK_MULTIPART_TEMPFILE_FACTORY] = lambda do |filename, content_type|
io = tempfile_factory.call(filename, content_type)
raise LintError, "rack.multipart.tempfile_factory return value must respond to #<<" unless io.respond_to?(:<<)
io
end
end
## The server or the application can store their own data in the
## environment, too. The keys must contain at least one dot,
## and should be prefixed uniquely. The prefix <tt>rack.</tt>
## is reserved for use with the Rack core distribution and other
## accepted specifications and must not be used otherwise.
##
%w[REQUEST_METHOD SERVER_NAME QUERY_STRING SERVER_PROTOCOL
rack.input rack.errors].each { |header|
raise LintError, "env missing required key #{header}" unless env.include? header
}
## The <tt>SERVER_PORT</tt> must be an Integer if set.
server_port = env["SERVER_PORT"]
unless server_port.nil? || (Integer(server_port) rescue false)
raise LintError, "env[SERVER_PORT] is not an Integer"
end
## The <tt>SERVER_NAME</tt> must be a valid authority as defined by RFC7540.
unless (URI.parse("http://#{env[SERVER_NAME]}/") rescue false)
raise LintError, "#{env[SERVER_NAME]} must be a valid authority"
end
## The <tt>HTTP_HOST</tt> must be a valid authority as defined by RFC7540.
unless (URI.parse("http://#{env[HTTP_HOST]}/") rescue false)
raise LintError, "#{env[HTTP_HOST]} must be a valid authority"
end
## The <tt>SERVER_PROTOCOL</tt> must match the regexp <tt>HTTP/\d(\.\d)?</tt>.
server_protocol = env['SERVER_PROTOCOL']
unless %r{HTTP/\d(\.\d)?}.match?(server_protocol)
raise LintError, "env[SERVER_PROTOCOL] does not match HTTP/\\d(\\.\\d)?"
end
## If the <tt>HTTP_VERSION</tt> is present, it must equal the <tt>SERVER_PROTOCOL</tt>.
if env['HTTP_VERSION'] && env['HTTP_VERSION'] != server_protocol
raise LintError, "env[HTTP_VERSION] does not equal env[SERVER_PROTOCOL]"
end
## The environment must not contain the keys
## <tt>HTTP_CONTENT_TYPE</tt> or <tt>HTTP_CONTENT_LENGTH</tt>
## (use the versions without <tt>HTTP_</tt>).
%w[HTTP_CONTENT_TYPE HTTP_CONTENT_LENGTH].each { |header|
if env.include? header
raise LintError, "env contains #{header}, must use #{header[5..-1]}"
end
}
## The CGI keys (named without a period) must have String values.
## If the string values for CGI keys contain non-ASCII characters,
## they should use ASCII-8BIT encoding.
env.each { |key, value|
next if key.include? "." # Skip extensions
unless value.kind_of? String
raise LintError, "env variable #{key} has non-string value #{value.inspect}"
end
next if value.encoding == Encoding::ASCII_8BIT
unless value.b !~ /[\x80-\xff]/n
raise LintError, "env variable #{key} has value containing non-ASCII characters and has non-ASCII-8BIT encoding #{value.inspect} encoding: #{value.encoding}"
end
}
## There are the following restrictions:
## * <tt>rack.url_scheme</tt> must either be +http+ or +https+.
unless %w[http https].include?(env[RACK_URL_SCHEME])
raise LintError, "rack.url_scheme unknown: #{env[RACK_URL_SCHEME].inspect}"
end
## * There must be a valid input stream in <tt>rack.input</tt>.
check_input env[RACK_INPUT]
## * There must be a valid error stream in <tt>rack.errors</tt>.
check_error env[RACK_ERRORS]
## * There may be a valid hijack callback in <tt>rack.hijack</tt>
check_hijack env
## * The <tt>REQUEST_METHOD</tt> must be a valid token.
unless env[REQUEST_METHOD] =~ /\A[0-9A-Za-z!\#$%&'*+.^_`|~-]+\z/
raise LintError, "REQUEST_METHOD unknown: #{env[REQUEST_METHOD].dump}"
end
## * The <tt>SCRIPT_NAME</tt>, if non-empty, must start with <tt>/</tt>
if env.include?(SCRIPT_NAME) && env[SCRIPT_NAME] != "" && env[SCRIPT_NAME] !~ /\A\//
raise LintError, "SCRIPT_NAME must start with /"
end
## * The <tt>PATH_INFO</tt>, if non-empty, must start with <tt>/</tt>
if env.include?(PATH_INFO) && env[PATH_INFO] != "" && env[PATH_INFO] !~ /\A\//
raise LintError, "PATH_INFO must start with /"
end
## * The <tt>CONTENT_LENGTH</tt>, if given, must consist of digits only.
if env.include?("CONTENT_LENGTH") && env["CONTENT_LENGTH"] !~ /\A\d+\z/
raise LintError, "Invalid CONTENT_LENGTH: #{env["CONTENT_LENGTH"]}"
end
## * One of <tt>SCRIPT_NAME</tt> or <tt>PATH_INFO</tt> must be
## set. <tt>PATH_INFO</tt> should be <tt>/</tt> if
## <tt>SCRIPT_NAME</tt> is empty.
unless env[SCRIPT_NAME] || env[PATH_INFO]
raise LintError, "One of SCRIPT_NAME or PATH_INFO must be set (make PATH_INFO '/' if SCRIPT_NAME is empty)"
end
## <tt>SCRIPT_NAME</tt> never should be <tt>/</tt>, but instead be empty.
unless env[SCRIPT_NAME] != "/"
raise LintError, "SCRIPT_NAME cannot be '/', make it '' and PATH_INFO '/'"
end
## <tt>rack.response_finished</tt>:: An array of callables run by the server after the response has been
## processed. This would typically be invoked after sending the response to the client, but it could also be
## invoked if an error occurs while generating the response or sending the response; in that case, the error
## argument will be a subclass of +Exception+.
## The callables are invoked with +env, status, headers, error+ arguments and should not raise any
## exceptions. They should be invoked in reverse order of registration.
if callables = env[RACK_RESPONSE_FINISHED]
raise LintError, "rack.response_finished must be an array of callable objects" unless callables.is_a?(Array)
callables.each do |callable|
raise LintError, "rack.response_finished values must respond to call(env, status, headers, error)" unless callable.respond_to?(:call)
end
end
end
##
## === The Input Stream
##
## The input stream is an IO-like object which contains the raw HTTP
## POST data.
def check_input(input)
## When applicable, its external encoding must be "ASCII-8BIT" and it
## must be opened in binary mode, for Ruby 1.9 compatibility.
if input.respond_to?(:external_encoding) && input.external_encoding != Encoding::ASCII_8BIT
raise LintError, "rack.input #{input} does not have ASCII-8BIT as its external encoding"
end
if input.respond_to?(:binmode?) && !input.binmode?
raise LintError, "rack.input #{input} is not opened in binary mode"
end
## The input stream must respond to +gets+, +each+, and +read+.
[:gets, :each, :read].each { |method|
unless input.respond_to? method
raise LintError, "rack.input #{input} does not respond to ##{method}"
end
}
end
class InputWrapper
def initialize(input)
@input = input
end
## * +gets+ must be called without arguments and return a string,
## or +nil+ on EOF.
def gets(*args)
raise LintError, "rack.input#gets called with arguments" unless args.size == 0
v = @input.gets
unless v.nil? or v.kind_of? String
raise LintError, "rack.input#gets didn't return a String"
end
v
end
## * +read+ behaves like IO#read.
## Its signature is <tt>read([length, [buffer]])</tt>.
##
## If given, +length+ must be a non-negative Integer (>= 0) or +nil+,
## and +buffer+ must be a String and may not be nil.
##
## If +length+ is given and not nil, then this method reads at most
## +length+ bytes from the input stream.
##
## If +length+ is not given or nil, then this method reads
## all data until EOF.
##
## When EOF is reached, this method returns nil if +length+ is given
## and not nil, or "" if +length+ is not given or is nil.
##
## If +buffer+ is given, then the read data will be placed
## into +buffer+ instead of a newly created String object.
def read(*args)
unless args.size <= 2
raise LintError, "rack.input#read called with too many arguments"
end
if args.size >= 1
unless args.first.kind_of?(Integer) || args.first.nil?
raise LintError, "rack.input#read called with non-integer and non-nil length"
end
unless args.first.nil? || args.first >= 0
raise LintError, "rack.input#read called with a negative length"
end
end
if args.size >= 2
unless args[1].kind_of?(String)
raise LintError, "rack.input#read called with non-String buffer"
end
end
v = @input.read(*args)
unless v.nil? or v.kind_of? String
raise LintError, "rack.input#read didn't return nil or a String"
end
if args[0].nil?
unless !v.nil?
raise LintError, "rack.input#read(nil) returned nil on EOF"
end
end
v
end
## * +each+ must be called without arguments and only yield Strings.
def each(*args)
raise LintError, "rack.input#each called with arguments" unless args.size == 0
@input.each { |line|
unless line.kind_of? String
raise LintError, "rack.input#each didn't yield a String"
end
yield line
}
end
## * +close+ can be called on the input stream to indicate that the
## any remaining input is not needed.
def close(*args)
@input.close(*args)
end
end
##
## === The Error Stream
##
def check_error(error)
## The error stream must respond to +puts+, +write+ and +flush+.
[:puts, :write, :flush].each { |method|
unless error.respond_to? method
raise LintError, "rack.error #{error} does not respond to ##{method}"
end
}
end
class ErrorWrapper
def initialize(error)
@error = error
end
## * +puts+ must be called with a single argument that responds to +to_s+.
def puts(str)
@error.puts str
end
## * +write+ must be called with a single argument that is a String.
def write(str)
raise LintError, "rack.errors#write not called with a String" unless str.kind_of? String
@error.write str
end
## * +flush+ must be called without arguments and must be called
## in order to make the error appear for sure.
def flush
@error.flush
end
## * +close+ must never be called on the error stream.
def close(*args)
raise LintError, "rack.errors#close must not be called"
end
end
##
## === Hijacking
##
## The hijacking interfaces provides a means for an application to take
## control of the HTTP connection. There are two distinct hijack
## interfaces: full hijacking where the application takes over the raw
## connection, and partial hijacking where the application takes over
## just the response body stream. In both cases, the application is
## responsible for closing the hijacked stream.
##
## Full hijacking only works with HTTP/1. Partial hijacking is functionally
## equivalent to streaming bodies, and is still optionally supported for
## backwards compatibility with older Rack versions.
##
## ==== Full Hijack
##
## Full hijack is used to completely take over an HTTP/1 connection. It
## occurs before any headers are written and causes the request to
## ignores any response generated by the application.
##
## It is intended to be used when applications need access to raw HTTP/1
## connection.
##
def check_hijack(env)
## If +rack.hijack+ is present in +env+, it must respond to +call+
if original_hijack = env[RACK_HIJACK]
raise LintError, "rack.hijack must respond to call" unless original_hijack.respond_to?(:call)
env[RACK_HIJACK] = proc do
io = original_hijack.call
## and return an +IO+ instance which can be used to read and write
## to the underlying connection using HTTP/1 semantics and
## formatting.
raise LintError, "rack.hijack must return an IO instance" unless io.is_a?(IO)
io
end
end
end
##
## ==== Partial Hijack
##
## Partial hijack is used for bi-directional streaming of the request and
## response body. It occurs after the status and headers are written by
## the server and causes the server to ignore the Body of the response.
##
## It is intended to be used when applications need bi-directional
## streaming.
##
def check_hijack_response(headers, env)
## If +rack.hijack?+ is present in +env+ and truthy,
if env[RACK_IS_HIJACK]
## an application may set the special response header +rack.hijack+
if original_hijack = headers[RACK_HIJACK]
## to an object that responds to +call+,
unless original_hijack.respond_to?(:call)
raise LintError, 'rack.hijack header must respond to #call'
end
## accepting a +stream+ argument.
return proc do |io|
original_hijack.call StreamWrapper.new(io)
end
end
##
## After the response status and headers have been sent, this hijack
## callback will be invoked with a +stream+ argument which follows the
## same interface as outlined in "Streaming Body". Servers must
## ignore the +body+ part of the response tuple when the
## +rack.hijack+ response header is present. Using an empty +Array+
## instance is recommended.
else
##
## The special response header +rack.hijack+ must only be set
## if the request +env+ has a truthy +rack.hijack?+.
if headers.key?(RACK_HIJACK)
raise LintError, 'rack.hijack header must not be present if server does not support hijacking'
end
end
nil
end
## == The Response
##
## === The Status
##
def check_status(status)
## This is an HTTP status. It must be an Integer greater than or equal to
## 100.
unless status.is_a?(Integer) && status >= 100
raise LintError, "Status must be an Integer >=100"
end
end
##
## === The Headers
##
def check_headers(headers)
## The headers must be a unfrozen Hash.
unless headers.kind_of?(Hash)
raise LintError, "headers object should be a hash, but isn't (got #{headers.class} as headers)"
end
if headers.frozen?
raise LintError, "headers object should not be frozen, but is"
end
headers.each do |key, value|
## The header keys must be Strings.
unless key.kind_of? String
raise LintError, "header key must be a string, was #{key.class}"
end
## Special headers starting "rack." are for communicating with the
## server, and must not be sent back to the client.
next if key.start_with?("rack.")
## The header must not contain a +Status+ key.
raise LintError, "header must not contain status" if key == "status"
## Header keys must conform to RFC7230 token specification, i.e. cannot
## contain non-printable ASCII, DQUOTE or "(),/:;<=>?@[\]{}".
raise LintError, "invalid header name: #{key}" if key =~ /[\(\),\/:;<=>\?@\[\\\]{}[:cntrl:]]/
## Header keys must not contain uppercase ASCII characters (A-Z).
raise LintError, "uppercase character in header name: #{key}" if key =~ /[A-Z]/
## Header values must be either a String instance,
if value.kind_of?(String)
check_header_value(key, value)
elsif value.kind_of?(Array)
## or an Array of String instances,
value.each{|value| check_header_value(key, value)}
else
raise LintError, "a header value must be a String or Array of Strings, but the value of '#{key}' is a #{value.class}"
end
end
end
def check_header_value(key, value)
## such that each String instance must not contain characters below 037.
if value =~ /[\000-\037]/
raise LintError, "invalid header value #{key}: #{value.inspect}"
end
end
##
## === The content-type
##
def check_content_type(status, headers)
headers.each { |key, value|
## There must not be a <tt>content-type</tt> header key when the +Status+ is 1xx,
## 204, or 304.
if key == "content-type"
if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i
raise LintError, "content-type header found in #{status} response, not allowed"
end
return
end
}
end
##
## === The content-length
##
def check_content_length(status, headers)
headers.each { |key, value|
if key == 'content-length'
## There must not be a <tt>content-length</tt> header key when the
## +Status+ is 1xx, 204, or 304.
if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i
raise LintError, "content-length header found in #{status} response, not allowed"
end
@content_length = value
end
}
end
def verify_content_length(size)
if @head_request
unless size == 0
raise LintError, "Response body was given for HEAD request, but should be empty"
end
elsif @content_length
unless @content_length == size.to_s
raise LintError, "content-length header was #{@content_length}, but should be #{size}"
end
end
end
##
## === The Body
##
## The Body is typically an +Array+ of +String+ instances, an enumerable
## that yields +String+ instances, a +Proc+ instance, or a File-like
## object.
##
## The Body must respond to +each+ or +call+. It may optionally respond
## to +to_path+ or +to_ary+. A Body that responds to +each+ is considered
## to be an Enumerable Body. A Body that responds to +call+ is considered
## to be a Streaming Body.
##
## A Body that responds to both +each+ and +call+ must be treated as an
## Enumerable Body, not a Streaming Body. If it responds to +each+, you
## must call +each+ and not +call+. If the Body doesn't respond to
## +each+, then you can assume it responds to +call+.
##
## The Body must either be consumed or returned. The Body is consumed by
## optionally calling either +each+ or +call+.
## Then, if the Body responds to +close+, it must be called to release
## any resources associated with the generation of the body.
## In other words, +close+ must always be called at least once; typically
## after the web server has sent the response to the client, but also in
## cases where the Rack application makes internal/virtual requests and
## discards the response.
##
def close
##
## After calling +close+, the Body is considered closed and should not
## be consumed again.
@closed = true
## If the original Body is replaced by a new Body, the new Body must
## also consume the original Body by calling +close+ if possible.
@body.close if @body.respond_to?(:close)
index = @lint.index(self)
unless @env['rack.lint'][0..index].all? {|lint| lint.instance_variable_get(:@closed)}
raise LintError, "Body has not been closed"
end
end
def verify_to_path
##
## If the Body responds to +to_path+, it must return a +String+
## path for the local file system whose contents are identical
## to that produced by calling +each+; this may be used by the
## server as an alternative, possibly more efficient way to
## transport the response. The +to_path+ method does not consume
## the body.
if @body.respond_to?(:to_path)
unless ::File.exist? @body.to_path
raise LintError, "The file identified by body.to_path does not exist"
end
end
end
##
## ==== Enumerable Body
##
def each
## The Enumerable Body must respond to +each+.
raise LintError, "Enumerable Body must respond to each" unless @body.respond_to?(:each)
## It must only be called once.
raise LintError, "Response body must only be invoked once (#{@invoked})" unless @invoked.nil?
## It must not be called after being closed.
raise LintError, "Response body is already closed" if @closed
@invoked = :each
@body.each do |chunk|
## and must only yield String values.
unless chunk.kind_of? String
raise LintError, "Body yielded non-string value #{chunk.inspect}"
end
##
## The Body itself should not be an instance of String, as this will
## break in Ruby 1.9.
##
## Middleware must not call +each+ directly on the Body.
## Instead, middleware can return a new Body that calls +each+ on the
## original Body, yielding at least once per iteration.
if @lint[0] == self
@env['rack.lint.body_iteration'] += 1
else
if (@env['rack.lint.body_iteration'] -= 1) > 0
raise LintError, "New body must yield at least once per iteration of old body"
end
end
@size += chunk.bytesize
yield chunk
end
verify_content_length(@size)
verify_to_path
end
BODY_METHODS = {to_ary: true, each: true, call: true, to_path: true}
def to_path
@body.to_path
end
def respond_to?(name, *)
if BODY_METHODS.key?(name)
@body.respond_to?(name)
else
super
end
end
##
## If the Body responds to +to_ary+, it must return an +Array+ whose
## contents are identical to that produced by calling +each+.
## Middleware may call +to_ary+ directly on the Body and return a new
## Body in its place. In other words, middleware can only process the
## Body directly if it responds to +to_ary+. If the Body responds to both
## +to_ary+ and +close+, its implementation of +to_ary+ must call
## +close+.
def to_ary
@body.to_ary.tap do |content|
unless content == @body.enum_for.to_a
raise LintError, "#to_ary not identical to contents produced by calling #each"
end
end
ensure
close
end
##
## ==== Streaming Body
##
def call(stream)
## The Streaming Body must respond to +call+.
raise LintError, "Streaming Body must respond to call" unless @body.respond_to?(:call)
## It must only be called once.
raise LintError, "Response body must only be invoked once (#{@invoked})" unless @invoked.nil?
## It must not be called after being closed.
raise LintError, "Response body is already closed" if @closed
@invoked = :call
## It takes a +stream+ argument.
##
## The +stream+ argument must implement:
## <tt>read, write, <<, flush, close, close_read, close_write, closed?</tt>
##
@body.call(StreamWrapper.new(stream))
end
class StreamWrapper
extend Forwardable
## The semantics of these IO methods must be a best effort match to
## those of a normal Ruby IO or Socket object, using standard arguments
## and raising standard exceptions. Servers are encouraged to simply
## pass on real IO objects, although it is recognized that this approach
## is not directly compatible with HTTP/2.
REQUIRED_METHODS = [
:read, :write, :<<, :flush, :close,
:close_read, :close_write, :closed?
]
def_delegators :@stream, *REQUIRED_METHODS
def initialize(stream)
@stream = stream
REQUIRED_METHODS.each do |method_name|
raise LintError, "Stream must respond to #{method_name}" unless stream.respond_to?(method_name)
end
end
end
# :startdoc:
end
end
end
##
## == Thanks
## Some parts of this specification are adopted from {PEP 333 Python Web Server Gateway Interface v1.0}[https://peps.python.org/pep-0333/]
## I'd like to thank everyone involved in that effort.

View File

@ -1,29 +0,0 @@
# frozen_string_literal: true
require_relative 'body_proxy'
module Rack
# Rack::Lock locks every request inside a mutex, so that every request
# will effectively be executed synchronously.
class Lock
def initialize(app, mutex = Mutex.new)
@app, @mutex = app, mutex
end
def call(env)
@mutex.lock
begin
response = @app.call(env)
returned = response << BodyProxy.new(response.pop) { unlock }
ensure
unlock unless returned
end
end
private
def unlock
@mutex.unlock
end
end
end

View File

@ -1,22 +0,0 @@
# frozen_string_literal: true
require 'logger'
require_relative 'constants'
module Rack
# Sets up rack.logger to write to rack.errors stream
class Logger
def initialize(app, level = ::Logger::INFO)
@app, @level = app, level
end
def call(env)
logger = ::Logger.new(env[RACK_ERRORS])
logger.level = @level
env[RACK_LOGGER] = logger
@app.call(env)
end
end
end

View File

@ -1,43 +0,0 @@
# frozen_string_literal: true
module Rack
# Rack::MediaType parse media type and parameters out of content_type string
class MediaType
SPLIT_PATTERN = %r{\s*[;,]\s*}
class << self
# The media type (type/subtype) portion of the CONTENT_TYPE header
# without any media type parameters. e.g., when CONTENT_TYPE is
# "text/plain;charset=utf-8", the media-type is "text/plain".
#
# For more information on the use of media types in HTTP, see:
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
def type(content_type)
return nil unless content_type
content_type.split(SPLIT_PATTERN, 2).first.tap(&:downcase!)
end
# The media type parameters provided in CONTENT_TYPE as a Hash, or
# an empty Hash if no CONTENT_TYPE or media-type parameters were
# provided. e.g., when the CONTENT_TYPE is "text/plain;charset=utf-8",
# this method responds with the following Hash:
# { 'charset' => 'utf-8' }
def params(content_type)
return {} if content_type.nil?
content_type.split(SPLIT_PATTERN)[1..-1].each_with_object({}) do |s, hsh|
k, v = s.split('=', 2)
hsh[k.tap(&:downcase!)] = strip_doublequotes(v)
end
end
private
def strip_doublequotes(str)
(str.start_with?('"') && str.end_with?('"')) ? str[1..-2] : str
end
end
end
end

View File

@ -1,56 +0,0 @@
# frozen_string_literal: true
require_relative 'constants'
require_relative 'request'
require_relative 'utils'
module Rack
class MethodOverride
HTTP_METHODS = %w[GET HEAD PUT POST DELETE OPTIONS PATCH LINK UNLINK]
METHOD_OVERRIDE_PARAM_KEY = "_method"
HTTP_METHOD_OVERRIDE_HEADER = "HTTP_X_HTTP_METHOD_OVERRIDE"
ALLOWED_METHODS = %w[POST]
def initialize(app)
@app = app
end
def call(env)
if allowed_methods.include?(env[REQUEST_METHOD])
method = method_override(env)
if HTTP_METHODS.include?(method)
env[RACK_METHODOVERRIDE_ORIGINAL_METHOD] = env[REQUEST_METHOD]
env[REQUEST_METHOD] = method
end
end
@app.call(env)
end
def method_override(env)
req = Request.new(env)
method = method_override_param(req) ||
env[HTTP_METHOD_OVERRIDE_HEADER]
begin
method.to_s.upcase
rescue ArgumentError
env[RACK_ERRORS].puts "Invalid string for method"
end
end
private
def allowed_methods
ALLOWED_METHODS
end
def method_override_param(req)
req.POST[METHOD_OVERRIDE_PARAM_KEY] if req.form_data? || req.parseable_data?
rescue Utils::InvalidParameterError, Utils::ParameterTypeError, QueryParser::ParamsTooDeepError
req.get_header(RACK_ERRORS).puts "Invalid or incomplete POST params"
rescue EOFError
req.get_header(RACK_ERRORS).puts "Bad request content body"
end
end
end

View File

@ -1,693 +0,0 @@
# frozen_string_literal: true
module Rack
module Mime
# Returns String with mime type if found, otherwise use +fallback+.
# +ext+ should be filename extension in the '.ext' format that
# File.extname(file) returns.
# +fallback+ may be any object
#
# Also see the documentation for MIME_TYPES
#
# Usage:
# Rack::Mime.mime_type('.foo')
#
# This is a shortcut for:
# Rack::Mime::MIME_TYPES.fetch('.foo', 'application/octet-stream')
def mime_type(ext, fallback = 'application/octet-stream')
MIME_TYPES.fetch(ext.to_s.downcase, fallback)
end
module_function :mime_type
# Returns true if the given value is a mime match for the given mime match
# specification, false otherwise.
#
# Rack::Mime.match?('text/html', 'text/*') => true
# Rack::Mime.match?('text/plain', '*') => true
# Rack::Mime.match?('text/html', 'application/json') => false
def match?(value, matcher)
v1, v2 = value.split('/', 2)
m1, m2 = matcher.split('/', 2)
(m1 == '*' || v1 == m1) && (m2.nil? || m2 == '*' || m2 == v2)
end
module_function :match?
# List of most common mime-types, selected various sources
# according to their usefulness in a webserving scope for Ruby
# users.
#
# To amend this list with your local mime.types list you can use:
#
# require 'webrick/httputils'
# list = WEBrick::HTTPUtils.load_mime_types('/etc/mime.types')
# Rack::Mime::MIME_TYPES.merge!(list)
#
# N.B. On Ubuntu the mime.types file does not include the leading period, so
# users may need to modify the data before merging into the hash.
MIME_TYPES = {
".123" => "application/vnd.lotus-1-2-3",
".3dml" => "text/vnd.in3d.3dml",
".3g2" => "video/3gpp2",
".3gp" => "video/3gpp",
".a" => "application/octet-stream",
".acc" => "application/vnd.americandynamics.acc",
".ace" => "application/x-ace-compressed",
".acu" => "application/vnd.acucobol",
".aep" => "application/vnd.audiograph",
".afp" => "application/vnd.ibm.modcap",
".ai" => "application/postscript",
".aif" => "audio/x-aiff",
".aiff" => "audio/x-aiff",
".ami" => "application/vnd.amiga.ami",
".apng" => "image/apng",
".appcache" => "text/cache-manifest",
".apr" => "application/vnd.lotus-approach",
".asc" => "application/pgp-signature",
".asf" => "video/x-ms-asf",
".asm" => "text/x-asm",
".aso" => "application/vnd.accpac.simply.aso",
".asx" => "video/x-ms-asf",
".atc" => "application/vnd.acucorp",
".atom" => "application/atom+xml",
".atomcat" => "application/atomcat+xml",
".atomsvc" => "application/atomsvc+xml",
".atx" => "application/vnd.antix.game-component",
".au" => "audio/basic",
".avi" => "video/x-msvideo",
".avif" => "image/avif",
".bat" => "application/x-msdownload",
".bcpio" => "application/x-bcpio",
".bdm" => "application/vnd.syncml.dm+wbxml",
".bh2" => "application/vnd.fujitsu.oasysprs",
".bin" => "application/octet-stream",
".bmi" => "application/vnd.bmi",
".bmp" => "image/bmp",
".box" => "application/vnd.previewsystems.box",
".btif" => "image/prs.btif",
".bz" => "application/x-bzip",
".bz2" => "application/x-bzip2",
".c" => "text/x-c",
".c4g" => "application/vnd.clonk.c4group",
".cab" => "application/vnd.ms-cab-compressed",
".cc" => "text/x-c",
".ccxml" => "application/ccxml+xml",
".cdbcmsg" => "application/vnd.contact.cmsg",
".cdkey" => "application/vnd.mediastation.cdkey",
".cdx" => "chemical/x-cdx",
".cdxml" => "application/vnd.chemdraw+xml",
".cdy" => "application/vnd.cinderella",
".cer" => "application/pkix-cert",
".cgm" => "image/cgm",
".chat" => "application/x-chat",
".chm" => "application/vnd.ms-htmlhelp",
".chrt" => "application/vnd.kde.kchart",
".cif" => "chemical/x-cif",
".cii" => "application/vnd.anser-web-certificate-issue-initiation",
".cil" => "application/vnd.ms-artgalry",
".cla" => "application/vnd.claymore",
".class" => "application/octet-stream",
".clkk" => "application/vnd.crick.clicker.keyboard",
".clkp" => "application/vnd.crick.clicker.palette",
".clkt" => "application/vnd.crick.clicker.template",
".clkw" => "application/vnd.crick.clicker.wordbank",
".clkx" => "application/vnd.crick.clicker",
".clp" => "application/x-msclip",
".cmc" => "application/vnd.cosmocaller",
".cmdf" => "chemical/x-cmdf",
".cml" => "chemical/x-cml",
".cmp" => "application/vnd.yellowriver-custom-menu",
".cmx" => "image/x-cmx",
".com" => "application/x-msdownload",
".conf" => "text/plain",
".cpio" => "application/x-cpio",
".cpp" => "text/x-c",
".cpt" => "application/mac-compactpro",
".crd" => "application/x-mscardfile",
".crl" => "application/pkix-crl",
".crt" => "application/x-x509-ca-cert",
".csh" => "application/x-csh",
".csml" => "chemical/x-csml",
".csp" => "application/vnd.commonspace",
".css" => "text/css",
".csv" => "text/csv",
".curl" => "application/vnd.curl",
".cww" => "application/prs.cww",
".cxx" => "text/x-c",
".daf" => "application/vnd.mobius.daf",
".davmount" => "application/davmount+xml",
".dcr" => "application/x-director",
".dd2" => "application/vnd.oma.dd2+xml",
".ddd" => "application/vnd.fujixerox.ddd",
".deb" => "application/x-debian-package",
".der" => "application/x-x509-ca-cert",
".dfac" => "application/vnd.dreamfactory",
".diff" => "text/x-diff",
".dis" => "application/vnd.mobius.dis",
".djv" => "image/vnd.djvu",
".djvu" => "image/vnd.djvu",
".dll" => "application/x-msdownload",
".dmg" => "application/octet-stream",
".dna" => "application/vnd.dna",
".doc" => "application/msword",
".docm" => "application/vnd.ms-word.document.macroEnabled.12",
".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".dot" => "application/msword",
".dotm" => "application/vnd.ms-word.template.macroEnabled.12",
".dotx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
".dp" => "application/vnd.osgi.dp",
".dpg" => "application/vnd.dpgraph",
".dsc" => "text/prs.lines.tag",
".dtd" => "application/xml-dtd",
".dts" => "audio/vnd.dts",
".dtshd" => "audio/vnd.dts.hd",
".dv" => "video/x-dv",
".dvi" => "application/x-dvi",
".dwf" => "model/vnd.dwf",
".dwg" => "image/vnd.dwg",
".dxf" => "image/vnd.dxf",
".dxp" => "application/vnd.spotfire.dxp",
".ear" => "application/java-archive",
".ecelp4800" => "audio/vnd.nuera.ecelp4800",
".ecelp7470" => "audio/vnd.nuera.ecelp7470",
".ecelp9600" => "audio/vnd.nuera.ecelp9600",
".ecma" => "application/ecmascript",
".edm" => "application/vnd.novadigm.edm",
".edx" => "application/vnd.novadigm.edx",
".efif" => "application/vnd.picsel",
".ei6" => "application/vnd.pg.osasli",
".eml" => "message/rfc822",
".eol" => "audio/vnd.digital-winds",
".eot" => "application/vnd.ms-fontobject",
".eps" => "application/postscript",
".es3" => "application/vnd.eszigno3+xml",
".esf" => "application/vnd.epson.esf",
".etx" => "text/x-setext",
".exe" => "application/x-msdownload",
".ext" => "application/vnd.novadigm.ext",
".ez" => "application/andrew-inset",
".ez2" => "application/vnd.ezpix-album",
".ez3" => "application/vnd.ezpix-package",
".f" => "text/x-fortran",
".f77" => "text/x-fortran",
".f90" => "text/x-fortran",
".fbs" => "image/vnd.fastbidsheet",
".fdf" => "application/vnd.fdf",
".fe_launch" => "application/vnd.denovo.fcselayout-link",
".fg5" => "application/vnd.fujitsu.oasysgp",
".fli" => "video/x-fli",
".flif" => "image/flif",
".flo" => "application/vnd.micrografx.flo",
".flv" => "video/x-flv",
".flw" => "application/vnd.kde.kivio",
".flx" => "text/vnd.fmi.flexstor",
".fly" => "text/vnd.fly",
".fm" => "application/vnd.framemaker",
".fnc" => "application/vnd.frogans.fnc",
".for" => "text/x-fortran",
".fpx" => "image/vnd.fpx",
".fsc" => "application/vnd.fsc.weblaunch",
".fst" => "image/vnd.fst",
".ftc" => "application/vnd.fluxtime.clip",
".fti" => "application/vnd.anser-web-funds-transfer-initiation",
".fvt" => "video/vnd.fvt",
".fzs" => "application/vnd.fuzzysheet",
".g3" => "image/g3fax",
".gac" => "application/vnd.groove-account",
".gdl" => "model/vnd.gdl",
".gem" => "application/octet-stream",
".gemspec" => "text/x-script.ruby",
".ghf" => "application/vnd.groove-help",
".gif" => "image/gif",
".gim" => "application/vnd.groove-identity-message",
".gmx" => "application/vnd.gmx",
".gph" => "application/vnd.flographit",
".gqf" => "application/vnd.grafeq",
".gram" => "application/srgs",
".grv" => "application/vnd.groove-injector",
".grxml" => "application/srgs+xml",
".gtar" => "application/x-gtar",
".gtm" => "application/vnd.groove-tool-message",
".gtw" => "model/vnd.gtw",
".gv" => "text/vnd.graphviz",
".gz" => "application/x-gzip",
".h" => "text/x-c",
".h261" => "video/h261",
".h263" => "video/h263",
".h264" => "video/h264",
".hbci" => "application/vnd.hbci",
".hdf" => "application/x-hdf",
".heic" => "image/heic",
".heics" => "image/heic-sequence",
".heif" => "image/heif",
".heifs" => "image/heif-sequence",
".hh" => "text/x-c",
".hlp" => "application/winhlp",
".hpgl" => "application/vnd.hp-hpgl",
".hpid" => "application/vnd.hp-hpid",
".hps" => "application/vnd.hp-hps",
".hqx" => "application/mac-binhex40",
".htc" => "text/x-component",
".htke" => "application/vnd.kenameaapp",
".htm" => "text/html",
".html" => "text/html",
".hvd" => "application/vnd.yamaha.hv-dic",
".hvp" => "application/vnd.yamaha.hv-voice",
".hvs" => "application/vnd.yamaha.hv-script",
".icc" => "application/vnd.iccprofile",
".ice" => "x-conference/x-cooltalk",
".ico" => "image/vnd.microsoft.icon",
".ics" => "text/calendar",
".ief" => "image/ief",
".ifb" => "text/calendar",
".ifm" => "application/vnd.shana.informed.formdata",
".igl" => "application/vnd.igloader",
".igs" => "model/iges",
".igx" => "application/vnd.micrografx.igx",
".iif" => "application/vnd.shana.informed.interchange",
".imp" => "application/vnd.accpac.simply.imp",
".ims" => "application/vnd.ms-ims",
".ipk" => "application/vnd.shana.informed.package",
".irm" => "application/vnd.ibm.rights-management",
".irp" => "application/vnd.irepository.package+xml",
".iso" => "application/octet-stream",
".itp" => "application/vnd.shana.informed.formtemplate",
".ivp" => "application/vnd.immervision-ivp",
".ivu" => "application/vnd.immervision-ivu",
".jad" => "text/vnd.sun.j2me.app-descriptor",
".jam" => "application/vnd.jam",
".jar" => "application/java-archive",
".java" => "text/x-java-source",
".jisp" => "application/vnd.jisp",
".jlt" => "application/vnd.hp-jlyt",
".jnlp" => "application/x-java-jnlp-file",
".joda" => "application/vnd.joost.joda-archive",
".jp2" => "image/jp2",
".jpeg" => "image/jpeg",
".jpg" => "image/jpeg",
".jpgv" => "video/jpeg",
".jpm" => "video/jpm",
".js" => "application/javascript",
".json" => "application/json",
".karbon" => "application/vnd.kde.karbon",
".kfo" => "application/vnd.kde.kformula",
".kia" => "application/vnd.kidspiration",
".kml" => "application/vnd.google-earth.kml+xml",
".kmz" => "application/vnd.google-earth.kmz",
".kne" => "application/vnd.kinar",
".kon" => "application/vnd.kde.kontour",
".kpr" => "application/vnd.kde.kpresenter",
".ksp" => "application/vnd.kde.kspread",
".ktz" => "application/vnd.kahootz",
".kwd" => "application/vnd.kde.kword",
".latex" => "application/x-latex",
".lbd" => "application/vnd.llamagraphics.life-balance.desktop",
".lbe" => "application/vnd.llamagraphics.life-balance.exchange+xml",
".les" => "application/vnd.hhe.lesson-player",
".link66" => "application/vnd.route66.link66+xml",
".log" => "text/plain",
".lostxml" => "application/lost+xml",
".lrm" => "application/vnd.ms-lrm",
".ltf" => "application/vnd.frogans.ltf",
".lvp" => "audio/vnd.lucent.voice",
".lwp" => "application/vnd.lotus-wordpro",
".m3u" => "audio/x-mpegurl",
".m3u8" => "application/x-mpegurl",
".m4a" => "audio/mp4a-latm",
".m4v" => "video/mp4",
".ma" => "application/mathematica",
".mag" => "application/vnd.ecowin.chart",
".man" => "text/troff",
".manifest" => "text/cache-manifest",
".mathml" => "application/mathml+xml",
".mbk" => "application/vnd.mobius.mbk",
".mbox" => "application/mbox",
".mc1" => "application/vnd.medcalcdata",
".mcd" => "application/vnd.mcd",
".mdb" => "application/x-msaccess",
".mdi" => "image/vnd.ms-modi",
".mdoc" => "text/troff",
".me" => "text/troff",
".mfm" => "application/vnd.mfmp",
".mgz" => "application/vnd.proteus.magazine",
".mid" => "audio/midi",
".midi" => "audio/midi",
".mif" => "application/vnd.mif",
".mime" => "message/rfc822",
".mj2" => "video/mj2",
".mlp" => "application/vnd.dolby.mlp",
".mmd" => "application/vnd.chipnuts.karaoke-mmd",
".mmf" => "application/vnd.smaf",
".mml" => "application/mathml+xml",
".mmr" => "image/vnd.fujixerox.edmics-mmr",
".mng" => "video/x-mng",
".mny" => "application/x-msmoney",
".mov" => "video/quicktime",
".movie" => "video/x-sgi-movie",
".mp3" => "audio/mpeg",
".mp4" => "video/mp4",
".mp4a" => "audio/mp4",
".mp4s" => "application/mp4",
".mp4v" => "video/mp4",
".mpc" => "application/vnd.mophun.certificate",
".mpd" => "application/dash+xml",
".mpeg" => "video/mpeg",
".mpg" => "video/mpeg",
".mpga" => "audio/mpeg",
".mpkg" => "application/vnd.apple.installer+xml",
".mpm" => "application/vnd.blueice.multipass",
".mpn" => "application/vnd.mophun.application",
".mpp" => "application/vnd.ms-project",
".mpy" => "application/vnd.ibm.minipay",
".mqy" => "application/vnd.mobius.mqy",
".mrc" => "application/marc",
".ms" => "text/troff",
".mscml" => "application/mediaservercontrol+xml",
".mseq" => "application/vnd.mseq",
".msf" => "application/vnd.epson.msf",
".msh" => "model/mesh",
".msi" => "application/x-msdownload",
".msl" => "application/vnd.mobius.msl",
".msty" => "application/vnd.muvee.style",
".mts" => "model/vnd.mts",
".mus" => "application/vnd.musician",
".mvb" => "application/x-msmediaview",
".mwf" => "application/vnd.mfer",
".mxf" => "application/mxf",
".mxl" => "application/vnd.recordare.musicxml",
".mxml" => "application/xv+xml",
".mxs" => "application/vnd.triscape.mxs",
".mxu" => "video/vnd.mpegurl",
".n" => "application/vnd.nokia.n-gage.symbian.install",
".nc" => "application/x-netcdf",
".ngdat" => "application/vnd.nokia.n-gage.data",
".nlu" => "application/vnd.neurolanguage.nlu",
".nml" => "application/vnd.enliven",
".nnd" => "application/vnd.noblenet-directory",
".nns" => "application/vnd.noblenet-sealer",
".nnw" => "application/vnd.noblenet-web",
".npx" => "image/vnd.net-fpx",
".nsf" => "application/vnd.lotus-notes",
".oa2" => "application/vnd.fujitsu.oasys2",
".oa3" => "application/vnd.fujitsu.oasys3",
".oas" => "application/vnd.fujitsu.oasys",
".obd" => "application/x-msbinder",
".oda" => "application/oda",
".odc" => "application/vnd.oasis.opendocument.chart",
".odf" => "application/vnd.oasis.opendocument.formula",
".odg" => "application/vnd.oasis.opendocument.graphics",
".odi" => "application/vnd.oasis.opendocument.image",
".odp" => "application/vnd.oasis.opendocument.presentation",
".ods" => "application/vnd.oasis.opendocument.spreadsheet",
".odt" => "application/vnd.oasis.opendocument.text",
".oga" => "audio/ogg",
".ogg" => "application/ogg",
".ogv" => "video/ogg",
".ogx" => "application/ogg",
".org" => "application/vnd.lotus-organizer",
".otc" => "application/vnd.oasis.opendocument.chart-template",
".otf" => "application/vnd.oasis.opendocument.formula-template",
".otg" => "application/vnd.oasis.opendocument.graphics-template",
".oth" => "application/vnd.oasis.opendocument.text-web",
".oti" => "application/vnd.oasis.opendocument.image-template",
".otm" => "application/vnd.oasis.opendocument.text-master",
".ots" => "application/vnd.oasis.opendocument.spreadsheet-template",
".ott" => "application/vnd.oasis.opendocument.text-template",
".oxt" => "application/vnd.openofficeorg.extension",
".p" => "text/x-pascal",
".p10" => "application/pkcs10",
".p12" => "application/x-pkcs12",
".p7b" => "application/x-pkcs7-certificates",
".p7m" => "application/pkcs7-mime",
".p7r" => "application/x-pkcs7-certreqresp",
".p7s" => "application/pkcs7-signature",
".pas" => "text/x-pascal",
".pbd" => "application/vnd.powerbuilder6",
".pbm" => "image/x-portable-bitmap",
".pcl" => "application/vnd.hp-pcl",
".pclxl" => "application/vnd.hp-pclxl",
".pcx" => "image/x-pcx",
".pdb" => "chemical/x-pdb",
".pdf" => "application/pdf",
".pem" => "application/x-x509-ca-cert",
".pfr" => "application/font-tdpfr",
".pgm" => "image/x-portable-graymap",
".pgn" => "application/x-chess-pgn",
".pgp" => "application/pgp-encrypted",
".pic" => "image/x-pict",
".pict" => "image/pict",
".pkg" => "application/octet-stream",
".pki" => "application/pkixcmp",
".pkipath" => "application/pkix-pkipath",
".pl" => "text/x-script.perl",
".plb" => "application/vnd.3gpp.pic-bw-large",
".plc" => "application/vnd.mobius.plc",
".plf" => "application/vnd.pocketlearn",
".pls" => "application/pls+xml",
".pm" => "text/x-script.perl-module",
".pml" => "application/vnd.ctc-posml",
".png" => "image/png",
".pnm" => "image/x-portable-anymap",
".pntg" => "image/x-macpaint",
".portpkg" => "application/vnd.macports.portpkg",
".pot" => "application/vnd.ms-powerpoint",
".potm" => "application/vnd.ms-powerpoint.template.macroEnabled.12",
".potx" => "application/vnd.openxmlformats-officedocument.presentationml.template",
".ppa" => "application/vnd.ms-powerpoint",
".ppam" => "application/vnd.ms-powerpoint.addin.macroEnabled.12",
".ppd" => "application/vnd.cups-ppd",
".ppm" => "image/x-portable-pixmap",
".pps" => "application/vnd.ms-powerpoint",
".ppsm" => "application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
".ppsx" => "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
".ppt" => "application/vnd.ms-powerpoint",
".pptm" => "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
".pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
".prc" => "application/vnd.palm",
".pre" => "application/vnd.lotus-freelance",
".prf" => "application/pics-rules",
".ps" => "application/postscript",
".psb" => "application/vnd.3gpp.pic-bw-small",
".psd" => "image/vnd.adobe.photoshop",
".ptid" => "application/vnd.pvi.ptid1",
".pub" => "application/x-mspublisher",
".pvb" => "application/vnd.3gpp.pic-bw-var",
".pwn" => "application/vnd.3m.post-it-notes",
".py" => "text/x-script.python",
".pya" => "audio/vnd.ms-playready.media.pya",
".pyv" => "video/vnd.ms-playready.media.pyv",
".qam" => "application/vnd.epson.quickanime",
".qbo" => "application/vnd.intu.qbo",
".qfx" => "application/vnd.intu.qfx",
".qps" => "application/vnd.publishare-delta-tree",
".qt" => "video/quicktime",
".qtif" => "image/x-quicktime",
".qxd" => "application/vnd.quark.quarkxpress",
".ra" => "audio/x-pn-realaudio",
".rake" => "text/x-script.ruby",
".ram" => "audio/x-pn-realaudio",
".rar" => "application/x-rar-compressed",
".ras" => "image/x-cmu-raster",
".rb" => "text/x-script.ruby",
".rcprofile" => "application/vnd.ipunplugged.rcprofile",
".rdf" => "application/rdf+xml",
".rdz" => "application/vnd.data-vision.rdz",
".rep" => "application/vnd.businessobjects",
".rgb" => "image/x-rgb",
".rif" => "application/reginfo+xml",
".rl" => "application/resource-lists+xml",
".rlc" => "image/vnd.fujixerox.edmics-rlc",
".rld" => "application/resource-lists-diff+xml",
".rm" => "application/vnd.rn-realmedia",
".rmp" => "audio/x-pn-realaudio-plugin",
".rms" => "application/vnd.jcp.javame.midlet-rms",
".rnc" => "application/relax-ng-compact-syntax",
".roff" => "text/troff",
".rpm" => "application/x-redhat-package-manager",
".rpss" => "application/vnd.nokia.radio-presets",
".rpst" => "application/vnd.nokia.radio-preset",
".rq" => "application/sparql-query",
".rs" => "application/rls-services+xml",
".rsd" => "application/rsd+xml",
".rss" => "application/rss+xml",
".rtf" => "application/rtf",
".rtx" => "text/richtext",
".ru" => "text/x-script.ruby",
".s" => "text/x-asm",
".saf" => "application/vnd.yamaha.smaf-audio",
".sbml" => "application/sbml+xml",
".sc" => "application/vnd.ibm.secure-container",
".scd" => "application/x-msschedule",
".scm" => "application/vnd.lotus-screencam",
".scq" => "application/scvp-cv-request",
".scs" => "application/scvp-cv-response",
".sdkm" => "application/vnd.solent.sdkm+xml",
".sdp" => "application/sdp",
".see" => "application/vnd.seemail",
".sema" => "application/vnd.sema",
".semd" => "application/vnd.semd",
".semf" => "application/vnd.semf",
".setpay" => "application/set-payment-initiation",
".setreg" => "application/set-registration-initiation",
".sfd" => "application/vnd.hydrostatix.sof-data",
".sfs" => "application/vnd.spotfire.sfs",
".sgm" => "text/sgml",
".sgml" => "text/sgml",
".sh" => "application/x-sh",
".shar" => "application/x-shar",
".shf" => "application/shf+xml",
".sig" => "application/pgp-signature",
".sit" => "application/x-stuffit",
".sitx" => "application/x-stuffitx",
".skp" => "application/vnd.koan",
".slt" => "application/vnd.epson.salt",
".smi" => "application/smil+xml",
".snd" => "audio/basic",
".so" => "application/octet-stream",
".spf" => "application/vnd.yamaha.smaf-phrase",
".spl" => "application/x-futuresplash",
".spot" => "text/vnd.in3d.spot",
".spp" => "application/scvp-vp-response",
".spq" => "application/scvp-vp-request",
".src" => "application/x-wais-source",
".srt" => "text/srt",
".srx" => "application/sparql-results+xml",
".sse" => "application/vnd.kodak-descriptor",
".ssf" => "application/vnd.epson.ssf",
".ssml" => "application/ssml+xml",
".stf" => "application/vnd.wt.stf",
".stk" => "application/hyperstudio",
".str" => "application/vnd.pg.format",
".sus" => "application/vnd.sus-calendar",
".sv4cpio" => "application/x-sv4cpio",
".sv4crc" => "application/x-sv4crc",
".svd" => "application/vnd.svd",
".svg" => "image/svg+xml",
".svgz" => "image/svg+xml",
".swf" => "application/x-shockwave-flash",
".swi" => "application/vnd.arastra.swi",
".t" => "text/troff",
".tao" => "application/vnd.tao.intent-module-archive",
".tar" => "application/x-tar",
".tbz" => "application/x-bzip-compressed-tar",
".tcap" => "application/vnd.3gpp2.tcap",
".tcl" => "application/x-tcl",
".tex" => "application/x-tex",
".texi" => "application/x-texinfo",
".texinfo" => "application/x-texinfo",
".text" => "text/plain",
".tif" => "image/tiff",
".tiff" => "image/tiff",
".tmo" => "application/vnd.tmobile-livetv",
".torrent" => "application/x-bittorrent",
".tpl" => "application/vnd.groove-tool-template",
".tpt" => "application/vnd.trid.tpt",
".tr" => "text/troff",
".tra" => "application/vnd.trueapp",
".trm" => "application/x-msterminal",
".ts" => "video/mp2t",
".tsv" => "text/tab-separated-values",
".ttf" => "application/octet-stream",
".twd" => "application/vnd.simtech-mindmapper",
".txd" => "application/vnd.genomatix.tuxedo",
".txf" => "application/vnd.mobius.txf",
".txt" => "text/plain",
".ufd" => "application/vnd.ufdl",
".umj" => "application/vnd.umajin",
".unityweb" => "application/vnd.unity",
".uoml" => "application/vnd.uoml+xml",
".uri" => "text/uri-list",
".ustar" => "application/x-ustar",
".utz" => "application/vnd.uiq.theme",
".uu" => "text/x-uuencode",
".vcd" => "application/x-cdlink",
".vcf" => "text/x-vcard",
".vcg" => "application/vnd.groove-vcard",
".vcs" => "text/x-vcalendar",
".vcx" => "application/vnd.vcx",
".vis" => "application/vnd.visionary",
".viv" => "video/vnd.vivo",
".vrml" => "model/vrml",
".vsd" => "application/vnd.visio",
".vsf" => "application/vnd.vsf",
".vtt" => "text/vtt",
".vtu" => "model/vnd.vtu",
".vxml" => "application/voicexml+xml",
".war" => "application/java-archive",
".wasm" => "application/wasm",
".wav" => "audio/x-wav",
".wax" => "audio/x-ms-wax",
".wbmp" => "image/vnd.wap.wbmp",
".wbs" => "application/vnd.criticaltools.wbs+xml",
".wbxml" => "application/vnd.wap.wbxml",
".webm" => "video/webm",
".webp" => "image/webp",
".wm" => "video/x-ms-wm",
".wma" => "audio/x-ms-wma",
".wmd" => "application/x-ms-wmd",
".wmf" => "application/x-msmetafile",
".wml" => "text/vnd.wap.wml",
".wmlc" => "application/vnd.wap.wmlc",
".wmls" => "text/vnd.wap.wmlscript",
".wmlsc" => "application/vnd.wap.wmlscriptc",
".wmv" => "video/x-ms-wmv",
".wmx" => "video/x-ms-wmx",
".wmz" => "application/x-ms-wmz",
".woff" => "application/font-woff",
".woff2" => "application/font-woff2",
".wpd" => "application/vnd.wordperfect",
".wpl" => "application/vnd.ms-wpl",
".wps" => "application/vnd.ms-works",
".wqd" => "application/vnd.wqd",
".wri" => "application/x-mswrite",
".wrl" => "model/vrml",
".wsdl" => "application/wsdl+xml",
".wspolicy" => "application/wspolicy+xml",
".wtb" => "application/vnd.webturbo",
".wvx" => "video/x-ms-wvx",
".x3d" => "application/vnd.hzn-3d-crossword",
".xar" => "application/vnd.xara",
".xbd" => "application/vnd.fujixerox.docuworks.binder",
".xbm" => "image/x-xbitmap",
".xdm" => "application/vnd.syncml.dm+xml",
".xdp" => "application/vnd.adobe.xdp+xml",
".xdw" => "application/vnd.fujixerox.docuworks",
".xenc" => "application/xenc+xml",
".xer" => "application/patch-ops-error+xml",
".xfdf" => "application/vnd.adobe.xfdf",
".xfdl" => "application/vnd.xfdl",
".xhtml" => "application/xhtml+xml",
".xif" => "image/vnd.xiff",
".xla" => "application/vnd.ms-excel",
".xlam" => "application/vnd.ms-excel.addin.macroEnabled.12",
".xls" => "application/vnd.ms-excel",
".xlsb" => "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
".xlsm" => "application/vnd.ms-excel.sheet.macroEnabled.12",
".xlt" => "application/vnd.ms-excel",
".xltx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
".xml" => "application/xml",
".xo" => "application/vnd.olpc-sugar",
".xop" => "application/xop+xml",
".xpm" => "image/x-xpixmap",
".xpr" => "application/vnd.is-xpr",
".xps" => "application/vnd.ms-xpsdocument",
".xpw" => "application/vnd.intercon.formnet",
".xsl" => "application/xml",
".xslt" => "application/xslt+xml",
".xsm" => "application/vnd.syncml+xml",
".xspf" => "application/xspf+xml",
".xul" => "application/vnd.mozilla.xul+xml",
".xwd" => "image/x-xwindowdump",
".xyz" => "chemical/x-xyz",
".yaml" => "text/yaml",
".yml" => "text/yaml",
".zaz" => "application/vnd.zzazz.deck+xml",
".zip" => "application/zip",
".zmm" => "application/vnd.handheld-entertainment+xml",
}
end
end

View File

@ -1,3 +0,0 @@
# frozen_string_literal: true
require_relative 'mock_request'

View File

@ -1,166 +0,0 @@
# frozen_string_literal: true
require 'uri'
require 'stringio'
require_relative 'constants'
require_relative 'mock_response'
module Rack
# Rack::MockRequest helps testing your Rack application without
# actually using HTTP.
#
# After performing a request on a URL with get/post/put/patch/delete, it
# returns a MockResponse with useful helper methods for effective
# testing.
#
# You can pass a hash with additional configuration to the
# get/post/put/patch/delete.
# <tt>:input</tt>:: A String or IO-like to be used as rack.input.
# <tt>:fatal</tt>:: Raise a FatalWarning if the app writes to rack.errors.
# <tt>:lint</tt>:: If true, wrap the application in a Rack::Lint.
class MockRequest
class FatalWarning < RuntimeError
end
class FatalWarner
def puts(warning)
raise FatalWarning, warning
end
def write(warning)
raise FatalWarning, warning
end
def flush
end
def string
""
end
end
DEFAULT_ENV = {
RACK_INPUT => StringIO.new,
RACK_ERRORS => StringIO.new,
}.freeze
def initialize(app)
@app = app
end
# Make a GET request and return a MockResponse. See #request.
def get(uri, opts = {}) request(GET, uri, opts) end
# Make a POST request and return a MockResponse. See #request.
def post(uri, opts = {}) request(POST, uri, opts) end
# Make a PUT request and return a MockResponse. See #request.
def put(uri, opts = {}) request(PUT, uri, opts) end
# Make a PATCH request and return a MockResponse. See #request.
def patch(uri, opts = {}) request(PATCH, uri, opts) end
# Make a DELETE request and return a MockResponse. See #request.
def delete(uri, opts = {}) request(DELETE, uri, opts) end
# Make a HEAD request and return a MockResponse. See #request.
def head(uri, opts = {}) request(HEAD, uri, opts) end
# Make an OPTIONS request and return a MockResponse. See #request.
def options(uri, opts = {}) request(OPTIONS, uri, opts) end
# Make a request using the given request method for the given
# uri to the rack application and return a MockResponse.
# Options given are passed to MockRequest.env_for.
def request(method = GET, uri = "", opts = {})
env = self.class.env_for(uri, opts.merge(method: method))
if opts[:lint]
app = Rack::Lint.new(@app)
else
app = @app
end
errors = env[RACK_ERRORS]
status, headers, body = app.call(env)
MockResponse.new(status, headers, body, errors)
ensure
body.close if body.respond_to?(:close)
end
# For historical reasons, we're pinning to RFC 2396.
# URI::Parser = URI::RFC2396_Parser
def self.parse_uri_rfc2396(uri)
@parser ||= URI::Parser.new
@parser.parse(uri)
end
# Return the Rack environment used for a request to +uri+.
# All options that are strings are added to the returned environment.
# Options:
# :fatal :: Whether to raise an exception if request outputs to rack.errors
# :input :: The rack.input to set
# :http_version :: The SERVER_PROTOCOL to set
# :method :: The HTTP request method to use
# :params :: The params to use
# :script_name :: The SCRIPT_NAME to set
def self.env_for(uri = "", opts = {})
uri = parse_uri_rfc2396(uri)
uri.path = "/#{uri.path}" unless uri.path[0] == ?/
env = DEFAULT_ENV.dup
env[REQUEST_METHOD] = (opts[:method] ? opts[:method].to_s.upcase : GET).b
env[SERVER_NAME] = (uri.host || "example.org").b
env[SERVER_PORT] = (uri.port ? uri.port.to_s : "80").b
env[SERVER_PROTOCOL] = opts[:http_version] || 'HTTP/1.1'
env[QUERY_STRING] = (uri.query.to_s).b
env[PATH_INFO] = (uri.path).b
env[RACK_URL_SCHEME] = (uri.scheme || "http").b
env[HTTPS] = (env[RACK_URL_SCHEME] == "https" ? "on" : "off").b
env[SCRIPT_NAME] = opts[:script_name] || ""
if opts[:fatal]
env[RACK_ERRORS] = FatalWarner.new
else
env[RACK_ERRORS] = StringIO.new
end
if params = opts[:params]
if env[REQUEST_METHOD] == GET
params = Utils.parse_nested_query(params) if params.is_a?(String)
params.update(Utils.parse_nested_query(env[QUERY_STRING]))
env[QUERY_STRING] = Utils.build_nested_query(params)
elsif !opts.has_key?(:input)
opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
if params.is_a?(Hash)
if data = Rack::Multipart.build_multipart(params)
opts[:input] = data
opts["CONTENT_LENGTH"] ||= data.length.to_s
opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Rack::Multipart::MULTIPART_BOUNDARY}"
else
opts[:input] = Utils.build_nested_query(params)
end
else
opts[:input] = params
end
end
end
opts[:input] ||= String.new
if String === opts[:input]
rack_input = StringIO.new(opts[:input])
else
rack_input = opts[:input]
end
rack_input.set_encoding(Encoding::BINARY)
env[RACK_INPUT] = rack_input
env["CONTENT_LENGTH"] ||= env[RACK_INPUT].size.to_s if env[RACK_INPUT].respond_to?(:size)
opts.each { |field, value|
env[field] = value if String === field
}
env
end
end
end

View File

@ -1,126 +0,0 @@
# frozen_string_literal: true
require 'cgi/cookie'
require 'time'
require_relative 'response'
module Rack
# Rack::MockResponse provides useful helpers for testing your apps.
# Usually, you don't create the MockResponse on your own, but use
# MockRequest.
class MockResponse < Rack::Response
class << self
alias [] new
end
# Headers
attr_reader :original_headers, :cookies
# Errors
attr_accessor :errors
def initialize(status, headers, body, errors = nil)
@original_headers = headers
if errors
@errors = errors.string if errors.respond_to?(:string)
else
@errors = ""
end
super(body, status, headers)
@cookies = parse_cookies_from_header
buffered_body!
end
def =~(other)
body =~ other
end
def match(other)
body.match other
end
def body
return @buffered_body if defined?(@buffered_body)
# FIXME: apparently users of MockResponse expect the return value of
# MockResponse#body to be a string. However, the real response object
# returns the body as a list.
#
# See spec_showstatus.rb:
#
# should "not replace existing messages" do
# ...
# res.body.should == "foo!"
# end
buffer = @buffered_body = String.new
@body.each do |chunk|
buffer << chunk
end
return buffer
end
def empty?
[201, 204, 304].include? status
end
def cookie(name)
cookies.fetch(name, nil)
end
private
def parse_cookies_from_header
cookies = Hash.new
if headers.has_key? 'set-cookie'
set_cookie_header = headers.fetch('set-cookie')
Array(set_cookie_header).each do |header_value|
header_value.split("\n").each do |cookie|
cookie_name, cookie_filling = cookie.split('=', 2)
cookie_attributes = identify_cookie_attributes cookie_filling
parsed_cookie = CGI::Cookie.new(
'name' => cookie_name.strip,
'value' => cookie_attributes.fetch('value'),
'path' => cookie_attributes.fetch('path', nil),
'domain' => cookie_attributes.fetch('domain', nil),
'expires' => cookie_attributes.fetch('expires', nil),
'secure' => cookie_attributes.fetch('secure', false)
)
cookies.store(cookie_name, parsed_cookie)
end
end
end
cookies
end
def identify_cookie_attributes(cookie_filling)
cookie_bits = cookie_filling.split(';')
cookie_attributes = Hash.new
cookie_attributes.store('value', cookie_bits[0].strip)
cookie_bits.drop(1).each do |bit|
if bit.include? '='
cookie_attribute, attribute_value = bit.split('=', 2)
cookie_attributes.store(cookie_attribute.strip.downcase, attribute_value.strip)
end
if bit.include? 'secure'
cookie_attributes.store('secure', true)
end
end
if cookie_attributes.key? 'max-age'
cookie_attributes.store('expires', Time.now + cookie_attributes['max-age'].to_i)
elsif cookie_attributes.key? 'expires'
cookie_attributes.store('expires', Time.httpdate(cookie_attributes['expires']))
end
cookie_attributes
end
end
end

View File

@ -1,44 +0,0 @@
# frozen_string_literal: true
require_relative 'constants'
require_relative 'utils'
require_relative 'multipart/parser'
require_relative 'multipart/generator'
module Rack
# A multipart form data parser, adapted from IOWA.
#
# Usually, Rack::Request#POST takes care of calling this.
module Multipart
MULTIPART_BOUNDARY = "AaB03x"
class << self
def parse_multipart(env, params = Rack::Utils.default_query_parser)
io = env[RACK_INPUT]
if content_length = env['CONTENT_LENGTH']
content_length = content_length.to_i
end
content_type = env['CONTENT_TYPE']
tempfile = env[RACK_MULTIPART_TEMPFILE_FACTORY] || Parser::TEMPFILE_FACTORY
bufsize = env[RACK_MULTIPART_BUFFER_SIZE] || Parser::BUFSIZE
info = Parser.parse(io, content_length, content_type, tempfile, bufsize, params)
env[RACK_TEMPFILES] = info.tmp_files
return info.params
end
def extract_multipart(request, params = Rack::Utils.default_query_parser)
parse_multipart(request.env)
end
def build_multipart(params, first = true)
Generator.new(params, first).dump
end
end
end
end

View File

@ -1,99 +0,0 @@
# frozen_string_literal: true
require_relative 'uploaded_file'
module Rack
module Multipart
class Generator
def initialize(params, first = true)
@params, @first = params, first
if @first && !@params.is_a?(Hash)
raise ArgumentError, "value must be a Hash"
end
end
def dump
return nil if @first && !multipart?
return flattened_params unless @first
flattened_params.map do |name, file|
if file.respond_to?(:original_filename)
if file.path
::File.open(file.path, 'rb') do |f|
f.set_encoding(Encoding::BINARY)
content_for_tempfile(f, file, name)
end
else
content_for_tempfile(file, file, name)
end
else
content_for_other(file, name)
end
end.join << "--#{MULTIPART_BOUNDARY}--\r"
end
private
def multipart?
query = lambda { |value|
case value
when Array
value.any?(&query)
when Hash
value.values.any?(&query)
when Rack::Multipart::UploadedFile
true
end
}
@params.values.any?(&query)
end
def flattened_params
@flattened_params ||= begin
h = Hash.new
@params.each do |key, value|
k = @first ? key.to_s : "[#{key}]"
case value
when Array
value.map { |v|
Multipart.build_multipart(v, false).each { |subkey, subvalue|
h["#{k}[]#{subkey}"] = subvalue
}
}
when Hash
Multipart.build_multipart(value, false).each { |subkey, subvalue|
h[k + subkey] = subvalue
}
else
h[k] = value
end
end
h
end
end
def content_for_tempfile(io, file, name)
length = ::File.stat(file.path).size if file.path
filename = "; filename=\"#{Utils.escape_path(file.original_filename)}\""
<<-EOF
--#{MULTIPART_BOUNDARY}\r
content-disposition: form-data; name="#{name}"#{filename}\r
content-type: #{file.content_type}\r
#{"content-length: #{length}\r\n" if length}\r
#{io.read}\r
EOF
end
def content_for_other(file, name)
<<-EOF
--#{MULTIPART_BOUNDARY}\r
content-disposition: form-data; name="#{name}"\r
\r
#{file}\r
EOF
end
end
end
end

View File

@ -1,434 +0,0 @@
# frozen_string_literal: true
require 'strscan'
require_relative '../utils'
module Rack
module Multipart
class MultipartPartLimitError < Errno::EMFILE; end
class MultipartTotalPartLimitError < StandardError; end
# Use specific error class when parsing multipart request
# that ends early.
class EmptyContentError < ::EOFError; end
# Base class for multipart exceptions that do not subclass from
# other exception classes for backwards compatibility.
class Error < StandardError; end
EOL = "\r\n"
MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i
MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:[^:]*;\s*name=(#{VALUE})/ni
MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
# Updated definitions from RFC 2231
ATTRIBUTE_CHAR = %r{[^ \x00-\x1f\x7f)(><@,;:\\"/\[\]?='*%]}
ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/
SECTION = /\*[0-9]+/
REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/
REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/
EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/
EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/
EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/
EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/
EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/
EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/
EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/
DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/
RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
class Parser
BUFSIZE = 1_048_576
TEXT_PLAIN = "text/plain"
TEMPFILE_FACTORY = lambda { |filename, content_type|
Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0", '%00'))])
}
class BoundedIO # :nodoc:
def initialize(io, content_length)
@io = io
@content_length = content_length
@cursor = 0
end
def read(size, outbuf = nil)
return if @cursor >= @content_length
left = @content_length - @cursor
str = if left < size
@io.read left, outbuf
else
@io.read size, outbuf
end
if str
@cursor += str.bytesize
else
# Raise an error for mismatching content-length and actual contents
raise EOFError, "bad content body"
end
str
end
end
MultipartInfo = Struct.new :params, :tmp_files
EMPTY = MultipartInfo.new(nil, [])
def self.parse_boundary(content_type)
return unless content_type
data = content_type.match(MULTIPART)
return unless data
data[1]
end
def self.parse(io, content_length, content_type, tmpfile, bufsize, qp)
return EMPTY if 0 == content_length
boundary = parse_boundary content_type
return EMPTY unless boundary
if boundary.length > 70
# RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
# Most clients use no more than 55 characters.
raise Error, "multipart boundary size too large (#{boundary.length} characters)"
end
io = BoundedIO.new(io, content_length) if content_length
parser = new(boundary, tmpfile, bufsize, qp)
parser.parse(io)
parser.result
end
class Collector
class MimePart < Struct.new(:body, :head, :filename, :content_type, :name)
def get_data
data = body
if filename == ""
# filename is blank which means no file has been selected
return
elsif filename
body.rewind if body.respond_to?(:rewind)
# Take the basename of the upload's original filename.
# This handles the full Windows paths given by Internet Explorer
# (and perhaps other broken user agents) without affecting
# those which give the lone filename.
fn = filename.split(/[\/\\]/).last
data = { filename: fn, type: content_type,
name: name, tempfile: body, head: head }
end
yield data
end
end
class BufferPart < MimePart
def file?; false; end
def close; end
end
class TempfilePart < MimePart
def file?; true; end
def close; body.close; end
end
include Enumerable
def initialize(tempfile)
@tempfile = tempfile
@mime_parts = []
@open_files = 0
end
def each
@mime_parts.each { |part| yield part }
end
def on_mime_head(mime_index, head, filename, content_type, name)
if filename
body = @tempfile.call(filename, content_type)
body.binmode if body.respond_to?(:binmode)
klass = TempfilePart
@open_files += 1
else
body = String.new
klass = BufferPart
end
@mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
check_part_limits
end
def on_mime_body(mime_index, content)
@mime_parts[mime_index].body << content
end
def on_mime_finish(mime_index)
end
private
def check_part_limits
file_limit = Utils.multipart_file_limit
part_limit = Utils.multipart_total_part_limit
if file_limit && file_limit > 0
if @open_files >= file_limit
@mime_parts.each(&:close)
raise MultipartPartLimitError, 'Maximum file multiparts in content reached'
end
end
if part_limit && part_limit > 0
if @mime_parts.size >= part_limit
@mime_parts.each(&:close)
raise MultipartTotalPartLimitError, 'Maximum total multiparts in content reached'
end
end
end
end
attr_reader :state
def initialize(boundary, tempfile, bufsize, query_parser)
@query_parser = query_parser
@params = query_parser.make_params
@bufsize = bufsize
@state = :FAST_FORWARD
@mime_index = 0
@collector = Collector.new tempfile
@sbuf = StringScanner.new("".dup)
@body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
@rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
@head_regex = /(.*?#{EOL})#{EOL}/m
end
def parse(io)
outbuf = String.new
read_data(io, outbuf)
loop do
status =
case @state
when :FAST_FORWARD
handle_fast_forward
when :CONSUME_TOKEN
handle_consume_token
when :MIME_HEAD
handle_mime_head
when :MIME_BODY
handle_mime_body
else # when :DONE
return
end
read_data(io, outbuf) if status == :want_read
end
end
def result
@collector.each do |part|
part.get_data do |data|
tag_multipart_encoding(part.filename, part.content_type, part.name, data)
@query_parser.normalize_params(@params, part.name, data)
end
end
MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
end
private
def dequote(str) # From WEBrick::HTTPUtils
ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
ret.gsub!(/\\(.)/, "\\1")
ret
end
def read_data(io, outbuf)
content = io.read(@bufsize, outbuf)
handle_empty_content!(content)
@sbuf.concat(content)
end
# This handles the initial parser state. We read until we find the starting
# boundary, then we can transition to the next state. If we find the ending
# boundary, this is an invalid multipart upload, but keep scanning for opening
# boundary in that case. If no boundary found, we need to keep reading data
# and retry. It's highly unlikely the initial read will not consume the
# boundary. The client would have to deliberately craft a response
# with the opening boundary beyond the buffer size for that to happen.
def handle_fast_forward
while true
case consume_boundary
when :BOUNDARY
# found opening boundary, transition to next state
@state = :MIME_HEAD
return
when :END_BOUNDARY
# invalid multipart upload, but retry for opening boundary
else
# no boundary found, keep reading data
return :want_read
end
end
end
def handle_consume_token
tok = consume_boundary
# break if we're at the end of a buffer, but not if it is the end of a field
@state = if tok == :END_BOUNDARY || (@sbuf.eos? && tok != :BOUNDARY)
:DONE
else
:MIME_HEAD
end
end
def handle_mime_head
if @sbuf.scan_until(@head_regex)
head = @sbuf[1]
content_type = head[MULTIPART_CONTENT_TYPE, 1]
if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
name = dequote(name)
else
name = head[MULTIPART_CONTENT_ID, 1]
end
filename = get_filename(head)
if name.nil? || name.empty?
name = filename || "#{content_type || TEXT_PLAIN}[]".dup
end
@collector.on_mime_head @mime_index, head, filename, content_type, name
@state = :MIME_BODY
else
:want_read
end
end
def handle_mime_body
if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string
@collector.on_mime_body @mime_index, body
@sbuf.pos += body.length + 2 # skip \r\n after the content
@state = :CONSUME_TOKEN
@mime_index += 1
else
# Save what we have so far
if @rx_max_size < @sbuf.rest_size
delta = @sbuf.rest_size - @rx_max_size
@collector.on_mime_body @mime_index, @sbuf.peek(delta)
@sbuf.pos += delta
@sbuf.string = @sbuf.rest
end
:want_read
end
end
# Scan until the we find the start or end of the boundary.
# If we find it, return the appropriate symbol for the start or
# end of the boundary. If we don't find the start or end of the
# boundary, clear the buffer and return nil.
def consume_boundary
if read_buffer = @sbuf.scan_until(@body_regex)
read_buffer.end_with?(EOL) ? :BOUNDARY : :END_BOUNDARY
else
@sbuf.terminate
nil
end
end
def get_filename(head)
filename = nil
case head
when RFC2183
params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
if filename = params['filename*']
encoding, _, filename = filename.split("'", 3)
elsif filename = params['filename']
filename = $1 if filename =~ /^"(.*)"$/
end
when BROKEN
filename = $1
filename = $1 if filename =~ /^"(.*)"$/
end
return unless filename
if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
filename = Utils.unescape_path(filename)
end
filename.scrub!
if filename !~ /\\[^\\"]/
filename = filename.gsub(/\\(.)/, '\1')
end
if encoding
filename.force_encoding ::Encoding.find(encoding)
end
filename
end
CHARSET = "charset"
deprecate_constant :CHARSET
def tag_multipart_encoding(filename, content_type, name, body)
name = name.to_s
encoding = Encoding::UTF_8
name.force_encoding(encoding)
return if filename
if content_type
list = content_type.split(';')
type_subtype = list.first
type_subtype.strip!
if TEXT_PLAIN == type_subtype
rest = list.drop 1
rest.each do |param|
k, v = param.split('=', 2)
k.strip!
v.strip!
v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
if k == "charset"
encoding = begin
Encoding.find v
rescue ArgumentError
Encoding::BINARY
end
end
end
end
end
name.force_encoding(encoding)
body.force_encoding(encoding)
end
def handle_empty_content!(content)
if content.nil? || content.empty?
raise EmptyContentError
end
end
end
end
end

View File

@ -1,45 +0,0 @@
# frozen_string_literal: true
require 'tempfile'
require 'fileutils'
module Rack
module Multipart
class UploadedFile
# The filename, *not* including the path, of the "uploaded" file
attr_reader :original_filename
# The content type of the "uploaded" file
attr_accessor :content_type
def initialize(filepath = nil, ct = "text/plain", bin = false,
path: filepath, content_type: ct, binary: bin, filename: nil, io: nil)
if io
@tempfile = io
@original_filename = filename
else
raise "#{path} file does not exist" unless ::File.exist?(path)
@original_filename = filename || ::File.basename(path)
@tempfile = Tempfile.new([@original_filename, ::File.extname(path)], encoding: Encoding::BINARY)
@tempfile.binmode if binary
FileUtils.copy_file(path, @tempfile.path)
end
@content_type = content_type
end
def path
@tempfile.path if @tempfile.respond_to?(:path)
end
alias_method :local_path, :path
def respond_to?(*args)
super or @tempfile.respond_to?(*args)
end
def method_missing(method_name, *args, &block) #:nodoc:
@tempfile.__send__(method_name, *args, &block)
end
end
end
end

View File

@ -1,48 +0,0 @@
# frozen_string_literal: true
require_relative 'constants'
module Rack
class NullLogger
def initialize(app)
@app = app
end
def call(env)
env[RACK_LOGGER] = self
@app.call(env)
end
def info(progname = nil, &block); end
def debug(progname = nil, &block); end
def warn(progname = nil, &block); end
def error(progname = nil, &block); end
def fatal(progname = nil, &block); end
def unknown(progname = nil, &block); end
def info? ; end
def debug? ; end
def warn? ; end
def error? ; end
def fatal? ; end
def debug! ; end
def error! ; end
def fatal! ; end
def info! ; end
def warn! ; end
def level ; end
def progname ; end
def datetime_format ; end
def formatter ; end
def sev_threshold ; end
def level=(level); end
def progname=(progname); end
def datetime_format=(datetime_format); end
def formatter=(formatter); end
def sev_threshold=(sev_threshold); end
def close ; end
def add(severity, message = nil, progname = nil, &block); end
def log(severity, message = nil, progname = nil, &block); end
def <<(msg); end
def reopen(logdev = nil); end
end
end

View File

@ -1,253 +0,0 @@
# frozen_string_literal: true
require 'uri'
module Rack
class QueryParser
DEFAULT_SEP = /[&] */n
COMMON_SEP = { ";" => /[;] */n, ";," => /[;,] */n, "&" => /[&] */n }
# ParameterTypeError is the error that is raised when incoming structural
# parameters (parsed by parse_nested_query) contain conflicting types.
class ParameterTypeError < TypeError; end
# InvalidParameterError is the error that is raised when incoming structural
# parameters (parsed by parse_nested_query) contain invalid format or byte
# sequence.
class InvalidParameterError < ArgumentError; end
# ParamsTooDeepError is the error that is raised when params are recursively
# nested over the specified limit.
class ParamsTooDeepError < RangeError; end
def self.make_default(_key_space_limit=(not_deprecated = true; nil), param_depth_limit)
unless not_deprecated
warn("`first argument `key_space limit` is deprecated and no longer has an effect. Please call with only one argument, which will be required in a future version of Rack", uplevel: 1)
end
new Params, param_depth_limit
end
attr_reader :param_depth_limit
def initialize(params_class, _key_space_limit=(not_deprecated = true; nil), param_depth_limit)
unless not_deprecated
warn("`second argument `key_space limit` is deprecated and no longer has an effect. Please call with only two arguments, which will be required in a future version of Rack", uplevel: 1)
end
@params_class = params_class
@param_depth_limit = param_depth_limit
end
# Stolen from Mongrel, with some small modifications:
# Parses a query string by breaking it up at the '&'. You can also use this
# to parse cookies by changing the characters used in the second parameter
# (which defaults to '&').
def parse_query(qs, separator = nil, &unescaper)
unescaper ||= method(:unescape)
params = make_params
(qs || '').split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
next if p.empty?
k, v = p.split('=', 2).map!(&unescaper)
if cur = params[k]
if cur.class == Array
params[k] << v
else
params[k] = [cur, v]
end
else
params[k] = v
end
end
return params.to_h
end
# parse_nested_query expands a query string into structural types. Supported
# types are Arrays, Hashes and basic value types. It is possible to supply
# query strings with parameters of conflicting types, in this case a
# ParameterTypeError is raised. Users are encouraged to return a 400 in this
# case.
def parse_nested_query(qs, separator = nil)
params = make_params
unless qs.nil? || qs.empty?
(qs || '').split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
k, v = p.split('=', 2).map! { |s| unescape(s) }
_normalize_params(params, k, v, 0)
end
end
return params.to_h
rescue ArgumentError => e
raise InvalidParameterError, e.message, e.backtrace
end
# normalize_params recursively expands parameters into structural types. If
# the structural types represented by two different parameter names are in
# conflict, a ParameterTypeError is raised. The depth argument is deprecated
# and should no longer be used, it is kept for backwards compatibility with
# earlier versions of rack.
def normalize_params(params, name, v, _depth=nil)
_normalize_params(params, name, v, 0)
end
private def _normalize_params(params, name, v, depth)
raise ParamsTooDeepError if depth >= param_depth_limit
if !name
# nil name, treat same as empty string (required by tests)
k = after = ''
elsif depth == 0
# Start of parsing, don't treat [] or [ at start of string specially
if start = name.index('[', 1)
# Start of parameter nesting, use part before brackets as key
k = name[0, start]
after = name[start, name.length]
else
# Plain parameter with no nesting
k = name
after = ''
end
elsif name.start_with?('[]')
# Array nesting
k = '[]'
after = name[2, name.length]
elsif name.start_with?('[') && (start = name.index(']', 1))
# Hash nesting, use the part inside brackets as the key
k = name[1, start-1]
after = name[start+1, name.length]
else
# Probably malformed input, nested but not starting with [
# treat full name as key for backwards compatibility.
k = name
after = ''
end
return if k.empty?
if after == ''
if k == '[]' && depth != 0
return [v]
else
params[k] = v
end
elsif after == "["
params[name] = v
elsif after == "[]"
params[k] ||= []
raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
params[k] << v
elsif after.start_with?('[]')
# Recognize x[][y] (hash inside array) parameters
unless after[2] == '[' && after.end_with?(']') && (child_key = after[3, after.length-4]) && !child_key.empty? && !child_key.index('[') && !child_key.index(']')
# Handle other nested array parameters
child_key = after[2, after.length]
end
params[k] ||= []
raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
if params_hash_type?(params[k].last) && !params_hash_has_key?(params[k].last, child_key)
_normalize_params(params[k].last, child_key, v, depth + 1)
else
params[k] << _normalize_params(make_params, child_key, v, depth + 1)
end
else
params[k] ||= make_params
raise ParameterTypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params_hash_type?(params[k])
params[k] = _normalize_params(params[k], after, v, depth + 1)
end
params
end
def make_params
@params_class.new
end
def new_depth_limit(param_depth_limit)
self.class.new @params_class, param_depth_limit
end
private
def params_hash_type?(obj)
obj.kind_of?(@params_class)
end
def params_hash_has_key?(hash, key)
return false if /\[\]/.match?(key)
key.split(/[\[\]]+/).inject(hash) do |h, part|
next h if part == ''
return false unless params_hash_type?(h) && h.key?(part)
h[part]
end
true
end
def unescape(string, encoding = Encoding::UTF_8)
URI.decode_www_form_component(string, encoding)
end
class Params
def initialize
@size = 0
@params = {}
end
def [](key)
@params[key]
end
def []=(key, value)
@params[key] = value
end
def key?(key)
@params.key?(key)
end
# Recursively unwraps nested `Params` objects and constructs an object
# of the same shape, but using the objects' internal representations
# (Ruby hashes) in place of the objects. The result is a hash consisting
# purely of Ruby primitives.
#
# Mutation warning!
#
# 1. This method mutates the internal representation of the `Params`
# objects in order to save object allocations.
#
# 2. The value you get back is a reference to the internal hash
# representation, not a copy.
#
# 3. Because the `Params` object's internal representation is mutable
# through the `#[]=` method, it is not thread safe. The result of
# getting the hash representation while another thread is adding a
# key to it is non-deterministic.
#
def to_h
@params.each do |key, value|
case value
when self
# Handle circular references gracefully.
@params[key] = @params
when Params
@params[key] = value.to_h
when Array
value.map! { |v| v.kind_of?(Params) ? v.to_h : v }
else
# Ignore anything that is not a `Params` object or
# a collection that can contain one.
end
end
@params
end
alias_method :to_params_hash, :to_h
end
end
end

View File

@ -1,66 +0,0 @@
# frozen_string_literal: true
require 'uri'
require_relative 'constants'
module Rack
# Rack::ForwardRequest gets caught by Rack::Recursive and redirects
# the current request to the app at +url+.
#
# raise ForwardRequest.new("/not-found")
#
class ForwardRequest < Exception
attr_reader :url, :env
def initialize(url, env = {})
@url = URI(url)
@env = env
@env[PATH_INFO] = @url.path
@env[QUERY_STRING] = @url.query if @url.query
@env[HTTP_HOST] = @url.host if @url.host
@env[HTTP_PORT] = @url.port if @url.port
@env[RACK_URL_SCHEME] = @url.scheme if @url.scheme
super "forwarding to #{url}"
end
end
# Rack::Recursive allows applications called down the chain to
# include data from other applications (by using
# <tt>rack['rack.recursive.include'][...]</tt> or raise a
# ForwardRequest to redirect internally.
class Recursive
def initialize(app)
@app = app
end
def call(env)
dup._call(env)
end
def _call(env)
@script_name = env[SCRIPT_NAME]
@app.call(env.merge(RACK_RECURSIVE_INCLUDE => method(:include)))
rescue ForwardRequest => req
call(env.merge(req.env))
end
def include(env, path)
unless path.index(@script_name) == 0 && (path[@script_name.size] == ?/ ||
path[@script_name.size].nil?)
raise ArgumentError, "can only include below #{@script_name}, not #{path}"
end
env = env.merge(PATH_INFO => path,
SCRIPT_NAME => @script_name,
REQUEST_METHOD => GET,
"CONTENT_LENGTH" => "0", "CONTENT_TYPE" => "",
RACK_INPUT => StringIO.new(""))
@app.call(env)
end
end
end

View File

@ -1,112 +0,0 @@
# frozen_string_literal: true
# Copyright (C) 2009-2018 Michael Fellinger <m.fellinger@gmail.com>
# Rack::Reloader is subject to the terms of an MIT-style license.
# See MIT-LICENSE or https://opensource.org/licenses/MIT.
require 'pathname'
module Rack
# High performant source reloader
#
# This class acts as Rack middleware.
#
# What makes it especially suited for use in a production environment is that
# any file will only be checked once and there will only be made one system
# call stat(2).
#
# Please note that this will not reload files in the background, it does so
# only when actively called.
#
# It is performing a check/reload cycle at the start of every request, but
# also respects a cool down time, during which nothing will be done.
class Reloader
def initialize(app, cooldown = 10, backend = Stat)
@app = app
@cooldown = cooldown
@last = (Time.now - cooldown)
@cache = {}
@mtimes = {}
@reload_mutex = Mutex.new
extend backend
end
def call(env)
if @cooldown and Time.now > @last + @cooldown
if Thread.list.size > 1
@reload_mutex.synchronize{ reload! }
else
reload!
end
@last = Time.now
end
@app.call(env)
end
def reload!(stderr = $stderr)
rotation do |file, mtime|
previous_mtime = @mtimes[file] ||= mtime
safe_load(file, mtime, stderr) if mtime > previous_mtime
end
end
# A safe Kernel::load, issuing the hooks depending on the results
def safe_load(file, mtime, stderr = $stderr)
load(file)
stderr.puts "#{self.class}: reloaded `#{file}'"
file
rescue LoadError, SyntaxError => ex
stderr.puts ex
ensure
@mtimes[file] = mtime
end
module Stat
def rotation
files = [$0, *$LOADED_FEATURES].uniq
paths = ['./', *$LOAD_PATH].uniq
files.map{|file|
next if /\.(so|bundle)$/.match?(file) # cannot reload compiled files
found, stat = figure_path(file, paths)
next unless found && stat && mtime = stat.mtime
@cache[file] = found
yield(found, mtime)
}.compact
end
# Takes a relative or absolute +file+ name, a couple possible +paths+ that
# the +file+ might reside in. Returns the full path and File::Stat for the
# path.
def figure_path(file, paths)
found = @cache[file]
found = file if !found and Pathname.new(file).absolute?
found, stat = safe_stat(found)
return found, stat if found
paths.find do |possible_path|
path = ::File.join(possible_path, file)
found, stat = safe_stat(path)
return ::File.expand_path(found), stat if found
end
return false, false
end
def safe_stat(file)
return unless file
stat = ::File.stat(file)
return file, stat if stat.file?
rescue Errno::ENOENT, Errno::ENOTDIR, Errno::ESRCH
@cache.delete(file) and false
end
end
end
end

View File

@ -1,777 +0,0 @@
# frozen_string_literal: true
require_relative 'constants'
require_relative 'utils'
require_relative 'media_type'
module Rack
# Rack::Request provides a convenient interface to a Rack
# environment. It is stateless, the environment +env+ passed to the
# constructor will be directly modified.
#
# req = Rack::Request.new(env)
# req.post?
# req.params["data"]
class Request
class << self
attr_accessor :ip_filter
# The priority when checking forwarded headers. The default
# is <tt>[:forwarded, :x_forwarded]</tt>, which means, check the
# +Forwarded+ header first, followed by the appropriate
# <tt>X-Forwarded-*</tt> header. You can revert the priority by
# reversing the priority, or remove checking of either
# or both headers by removing elements from the array.
#
# This should be set as appropriate in your environment
# based on what reverse proxies are in use. If you are not
# using reverse proxies, you should probably use an empty
# array.
attr_accessor :forwarded_priority
# The priority when checking either the <tt>X-Forwarded-Proto</tt>
# or <tt>X-Forwarded-Scheme</tt> header for the forwarded protocol.
# The default is <tt>[:proto, :scheme]</tt>, to try the
# <tt>X-Forwarded-Proto</tt> header before the
# <tt>X-Forwarded-Scheme</tt> header. Rack 2 had behavior
# similar to <tt>[:scheme, :proto]</tt>. You can remove either or
# both of the entries in array to ignore that respective header.
attr_accessor :x_forwarded_proto_priority
end
@forwarded_priority = [:forwarded, :x_forwarded]
@x_forwarded_proto_priority = [:proto, :scheme]
valid_ipv4_octet = /\.(25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])/
trusted_proxies = Regexp.union(
/\A127#{valid_ipv4_octet}{3}\z/, # localhost IPv4 range 127.x.x.x, per RFC-3330
/\A::1\z/, # localhost IPv6 ::1
/\Af[cd][0-9a-f]{2}(?::[0-9a-f]{0,4}){0,7}\z/i, # private IPv6 range fc00 .. fdff
/\A10#{valid_ipv4_octet}{3}\z/, # private IPv4 range 10.x.x.x
/\A172\.(1[6-9]|2[0-9]|3[01])#{valid_ipv4_octet}{2}\z/, # private IPv4 range 172.16.0.0 .. 172.31.255.255
/\A192\.168#{valid_ipv4_octet}{2}\z/, # private IPv4 range 192.168.x.x
/\Alocalhost\z|\Aunix(\z|:)/i, # localhost hostname, and unix domain sockets
)
self.ip_filter = lambda { |ip| trusted_proxies.match?(ip) }
ALLOWED_SCHEMES = %w(https http wss ws).freeze
def initialize(env)
@env = env
@params = nil
end
def params
@params ||= super
end
def update_param(k, v)
super
@params = nil
end
def delete_param(k)
v = super
@params = nil
v
end
module Env
# The environment of the request.
attr_reader :env
def initialize(env)
@env = env
# This module is included at least in `ActionDispatch::Request`
# The call to `super()` allows additional mixed-in initializers are called
super()
end
# Predicate method to test to see if `name` has been set as request
# specific data
def has_header?(name)
@env.key? name
end
# Get a request specific value for `name`.
def get_header(name)
@env[name]
end
# If a block is given, it yields to the block if the value hasn't been set
# on the request.
def fetch_header(name, &block)
@env.fetch(name, &block)
end
# Loops through each key / value pair in the request specific data.
def each_header(&block)
@env.each(&block)
end
# Set a request specific value for `name` to `v`
def set_header(name, v)
@env[name] = v
end
# Add a header that may have multiple values.
#
# Example:
# request.add_header 'Accept', 'image/png'
# request.add_header 'Accept', '*/*'
#
# assert_equal 'image/png,*/*', request.get_header('Accept')
#
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
def add_header(key, v)
if v.nil?
get_header key
elsif has_header? key
set_header key, "#{get_header key},#{v}"
else
set_header key, v
end
end
# Delete a request specific value for `name`.
def delete_header(name)
@env.delete name
end
def initialize_copy(other)
@env = other.env.dup
end
end
module Helpers
# The set of form-data media-types. Requests that do not indicate
# one of the media types present in this list will not be eligible
# for form-data / param parsing.
FORM_DATA_MEDIA_TYPES = [
'application/x-www-form-urlencoded',
'multipart/form-data'
]
# The set of media-types. Requests that do not indicate
# one of the media types present in this list will not be eligible
# for param parsing like soap attachments or generic multiparts
PARSEABLE_DATA_MEDIA_TYPES = [
'multipart/related',
'multipart/mixed'
]
# Default ports depending on scheme. Used to decide whether or not
# to include the port in a generated URI.
DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 }
# The address of the client which connected to the proxy.
HTTP_X_FORWARDED_FOR = 'HTTP_X_FORWARDED_FOR'
# The contents of the host/:authority header sent to the proxy.
HTTP_X_FORWARDED_HOST = 'HTTP_X_FORWARDED_HOST'
HTTP_FORWARDED = 'HTTP_FORWARDED'
# The value of the scheme sent to the proxy.
HTTP_X_FORWARDED_SCHEME = 'HTTP_X_FORWARDED_SCHEME'
# The protocol used to connect to the proxy.
HTTP_X_FORWARDED_PROTO = 'HTTP_X_FORWARDED_PROTO'
# The port used to connect to the proxy.
HTTP_X_FORWARDED_PORT = 'HTTP_X_FORWARDED_PORT'
# Another way for specifying https scheme was used.
HTTP_X_FORWARDED_SSL = 'HTTP_X_FORWARDED_SSL'
def body; get_header(RACK_INPUT) end
def script_name; get_header(SCRIPT_NAME).to_s end
def script_name=(s); set_header(SCRIPT_NAME, s.to_s) end
def path_info; get_header(PATH_INFO).to_s end
def path_info=(s); set_header(PATH_INFO, s.to_s) end
def request_method; get_header(REQUEST_METHOD) end
def query_string; get_header(QUERY_STRING).to_s end
def content_length; get_header('CONTENT_LENGTH') end
def logger; get_header(RACK_LOGGER) end
def user_agent; get_header('HTTP_USER_AGENT') end
# the referer of the client
def referer; get_header('HTTP_REFERER') end
alias referrer referer
def session
fetch_header(RACK_SESSION) do |k|
set_header RACK_SESSION, default_session
end
end
def session_options
fetch_header(RACK_SESSION_OPTIONS) do |k|
set_header RACK_SESSION_OPTIONS, {}
end
end
# Checks the HTTP request method (or verb) to see if it was of type DELETE
def delete?; request_method == DELETE end
# Checks the HTTP request method (or verb) to see if it was of type GET
def get?; request_method == GET end
# Checks the HTTP request method (or verb) to see if it was of type HEAD
def head?; request_method == HEAD end
# Checks the HTTP request method (or verb) to see if it was of type OPTIONS
def options?; request_method == OPTIONS end
# Checks the HTTP request method (or verb) to see if it was of type LINK
def link?; request_method == LINK end
# Checks the HTTP request method (or verb) to see if it was of type PATCH
def patch?; request_method == PATCH end
# Checks the HTTP request method (or verb) to see if it was of type POST
def post?; request_method == POST end
# Checks the HTTP request method (or verb) to see if it was of type PUT
def put?; request_method == PUT end
# Checks the HTTP request method (or verb) to see if it was of type TRACE
def trace?; request_method == TRACE end
# Checks the HTTP request method (or verb) to see if it was of type UNLINK
def unlink?; request_method == UNLINK end
def scheme
if get_header(HTTPS) == 'on'
'https'
elsif get_header(HTTP_X_FORWARDED_SSL) == 'on'
'https'
elsif forwarded_scheme
forwarded_scheme
else
get_header(RACK_URL_SCHEME)
end
end
# The authority of the incoming request as defined by RFC3976.
# https://tools.ietf.org/html/rfc3986#section-3.2
#
# In HTTP/1, this is the `host` header.
# In HTTP/2, this is the `:authority` pseudo-header.
def authority
forwarded_authority || host_authority || server_authority
end
# The authority as defined by the `SERVER_NAME` and `SERVER_PORT`
# variables.
def server_authority
host = self.server_name
port = self.server_port
if host
if port
"#{host}:#{port}"
else
host
end
end
end
def server_name
get_header(SERVER_NAME)
end
def server_port
get_header(SERVER_PORT)
end
def cookies
hash = fetch_header(RACK_REQUEST_COOKIE_HASH) do |key|
set_header(key, {})
end
string = get_header(HTTP_COOKIE)
unless string == get_header(RACK_REQUEST_COOKIE_STRING)
hash.replace Utils.parse_cookies_header(string)
set_header(RACK_REQUEST_COOKIE_STRING, string)
end
hash
end
def content_type
content_type = get_header('CONTENT_TYPE')
content_type.nil? || content_type.empty? ? nil : content_type
end
def xhr?
get_header("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest"
end
# The `HTTP_HOST` header.
def host_authority
get_header(HTTP_HOST)
end
def host_with_port(authority = self.authority)
host, _, port = split_authority(authority)
if port == DEFAULT_PORTS[self.scheme]
host
else
authority
end
end
# Returns a formatted host, suitable for being used in a URI.
def host
split_authority(self.authority)[0]
end
# Returns an address suitable for being to resolve to an address.
# In the case of a domain name or IPv4 address, the result is the same
# as +host+. In the case of IPv6 or future address formats, the square
# brackets are removed.
def hostname
split_authority(self.authority)[1]
end
def port
if authority = self.authority
_, _, port = split_authority(authority)
end
port || forwarded_port&.last || DEFAULT_PORTS[scheme] || server_port
end
def forwarded_for
forwarded_priority.each do |type|
case type
when :forwarded
if forwarded_for = get_http_forwarded(:for)
return(forwarded_for.map! do |authority|
split_authority(authority)[1]
end)
end
when :x_forwarded
if value = get_header(HTTP_X_FORWARDED_FOR)
return(split_header(value).map do |authority|
split_authority(wrap_ipv6(authority))[1]
end)
end
end
end
nil
end
def forwarded_port
forwarded_priority.each do |type|
case type
when :forwarded
if forwarded = get_http_forwarded(:for)
return(forwarded.map do |authority|
split_authority(authority)[2]
end.compact)
end
when :x_forwarded
if value = get_header(HTTP_X_FORWARDED_PORT)
return split_header(value).map(&:to_i)
end
end
end
nil
end
def forwarded_authority
forwarded_priority.each do |type|
case type
when :forwarded
if forwarded = get_http_forwarded(:host)
return forwarded.last
end
when :x_forwarded
if value = get_header(HTTP_X_FORWARDED_HOST)
return wrap_ipv6(split_header(value).last)
end
end
end
nil
end
def ssl?
scheme == 'https' || scheme == 'wss'
end
def ip
remote_addresses = split_header(get_header('REMOTE_ADDR'))
external_addresses = reject_trusted_ip_addresses(remote_addresses)
unless external_addresses.empty?
return external_addresses.last
end
if (forwarded_for = self.forwarded_for) && !forwarded_for.empty?
# The forwarded for addresses are ordered: client, proxy1, proxy2.
# So we reject all the trusted addresses (proxy*) and return the
# last client. Or if we trust everyone, we just return the first
# address.
return reject_trusted_ip_addresses(forwarded_for).last || forwarded_for.first
end
# If all the addresses are trusted, and we aren't forwarded, just return
# the first remote address, which represents the source of the request.
remote_addresses.first
end
# The media type (type/subtype) portion of the CONTENT_TYPE header
# without any media type parameters. e.g., when CONTENT_TYPE is
# "text/plain;charset=utf-8", the media-type is "text/plain".
#
# For more information on the use of media types in HTTP, see:
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
def media_type
MediaType.type(content_type)
end
# The media type parameters provided in CONTENT_TYPE as a Hash, or
# an empty Hash if no CONTENT_TYPE or media-type parameters were
# provided. e.g., when the CONTENT_TYPE is "text/plain;charset=utf-8",
# this method responds with the following Hash:
# { 'charset' => 'utf-8' }
def media_type_params
MediaType.params(content_type)
end
# The character set of the request body if a "charset" media type
# parameter was given, or nil if no "charset" was specified. Note
# that, per RFC2616, text/* media types that specify no explicit
# charset are to be considered ISO-8859-1.
def content_charset
media_type_params['charset']
end
# Determine whether the request body contains form-data by checking
# the request content-type for one of the media-types:
# "application/x-www-form-urlencoded" or "multipart/form-data". The
# list of form-data media types can be modified through the
# +FORM_DATA_MEDIA_TYPES+ array.
#
# A request body is also assumed to contain form-data when no
# content-type header is provided and the request_method is POST.
def form_data?
type = media_type
meth = get_header(RACK_METHODOVERRIDE_ORIGINAL_METHOD) || get_header(REQUEST_METHOD)
(meth == POST && type.nil?) || FORM_DATA_MEDIA_TYPES.include?(type)
end
# Determine whether the request body contains data by checking
# the request media_type against registered parse-data media-types
def parseable_data?
PARSEABLE_DATA_MEDIA_TYPES.include?(media_type)
end
# Returns the data received in the query string.
def GET
if get_header(RACK_REQUEST_QUERY_STRING) == query_string
get_header(RACK_REQUEST_QUERY_HASH)
else
query_hash = parse_query(query_string, '&')
set_header(RACK_REQUEST_QUERY_STRING, query_string)
set_header(RACK_REQUEST_QUERY_HASH, query_hash)
end
end
# Returns the data received in the request body.
#
# This method support both application/x-www-form-urlencoded and
# multipart/form-data.
def POST
if error = get_header(RACK_REQUEST_FORM_ERROR)
raise error.class, error.message, cause: error.cause
end
begin
rack_input = get_header(RACK_INPUT)
# If the form hash was already memoized:
if form_hash = get_header(RACK_REQUEST_FORM_HASH)
# And it was memoized from the same input:
if get_header(RACK_REQUEST_FORM_INPUT).equal?(rack_input)
return form_hash
end
end
# Otherwise, figure out how to parse the input:
if rack_input.nil?
set_header RACK_REQUEST_FORM_INPUT, nil
set_header(RACK_REQUEST_FORM_HASH, {})
elsif form_data? || parseable_data?
unless set_header(RACK_REQUEST_FORM_HASH, parse_multipart)
form_vars = get_header(RACK_INPUT).read
# Fix for Safari Ajax postings that always append \0
# form_vars.sub!(/\0\z/, '') # performance replacement:
form_vars.slice!(-1) if form_vars.end_with?("\0")
set_header RACK_REQUEST_FORM_VARS, form_vars
set_header RACK_REQUEST_FORM_HASH, parse_query(form_vars, '&')
end
set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
get_header RACK_REQUEST_FORM_HASH
else
set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
set_header(RACK_REQUEST_FORM_HASH, {})
end
rescue => error
set_header(RACK_REQUEST_FORM_ERROR, error)
raise
end
end
# The union of GET and POST data.
#
# Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
def params
self.GET.merge(self.POST)
end
# Destructively update a parameter, whether it's in GET and/or POST. Returns nil.
#
# The parameter is updated wherever it was previous defined, so GET, POST, or both. If it wasn't previously defined, it's inserted into GET.
#
# <tt>env['rack.input']</tt> is not touched.
def update_param(k, v)
found = false
if self.GET.has_key?(k)
found = true
self.GET[k] = v
end
if self.POST.has_key?(k)
found = true
self.POST[k] = v
end
unless found
self.GET[k] = v
end
end
# Destructively delete a parameter, whether it's in GET or POST. Returns the value of the deleted parameter.
#
# If the parameter is in both GET and POST, the POST value takes precedence since that's how #params works.
#
# <tt>env['rack.input']</tt> is not touched.
def delete_param(k)
post_value, get_value = self.POST.delete(k), self.GET.delete(k)
post_value || get_value
end
def base_url
"#{scheme}://#{host_with_port}"
end
# Tries to return a remake of the original request URL as a string.
def url
base_url + fullpath
end
def path
script_name + path_info
end
def fullpath
query_string.empty? ? path : "#{path}?#{query_string}"
end
def accept_encoding
parse_http_accept_header(get_header("HTTP_ACCEPT_ENCODING"))
end
def accept_language
parse_http_accept_header(get_header("HTTP_ACCEPT_LANGUAGE"))
end
def trusted_proxy?(ip)
Rack::Request.ip_filter.call(ip)
end
# shortcut for <tt>request.params[key]</tt>
def [](key)
warn("Request#[] is deprecated and will be removed in a future version of Rack. Please use request.params[] instead", uplevel: 1)
params[key.to_s]
end
# shortcut for <tt>request.params[key] = value</tt>
#
# Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
def []=(key, value)
warn("Request#[]= is deprecated and will be removed in a future version of Rack. Please use request.params[]= instead", uplevel: 1)
params[key.to_s] = value
end
# like Hash#values_at
def values_at(*keys)
keys.map { |key| params[key] }
end
private
def default_session; {}; end
# Assist with compatibility when processing `X-Forwarded-For`.
def wrap_ipv6(host)
# Even thought IPv6 addresses should be wrapped in square brackets,
# sometimes this is not done in various legacy/underspecified headers.
# So we try to fix this situation for compatibility reasons.
# Try to detect IPv6 addresses which aren't escaped yet:
if !host.start_with?('[') && host.count(':') > 1
"[#{host}]"
else
host
end
end
def parse_http_accept_header(header)
header.to_s.split(",").each(&:strip!).map do |part|
attribute, parameters = part.split(";", 2).each(&:strip!)
quality = 1.0
if parameters and /\Aq=([\d.]+)/ =~ parameters
quality = $1.to_f
end
[attribute, quality]
end
end
# Get an array of values set in the RFC 7239 `Forwarded` request header.
def get_http_forwarded(token)
Utils.forwarded_values(get_header(HTTP_FORWARDED))&.[](token)
end
def query_parser
Utils.default_query_parser
end
def parse_query(qs, d = '&')
query_parser.parse_nested_query(qs, d)
end
def parse_multipart
Rack::Multipart.extract_multipart(self, query_parser)
end
def split_header(value)
value ? value.strip.split(/[,\s]+/) : []
end
# ipv6 extracted from resolv stdlib, simplified
# to remove numbered match group creation.
ipv6 = Regexp.union(
/(?:[0-9A-Fa-f]{1,4}:){7}
[0-9A-Fa-f]{1,4}/x,
/(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)? ::
(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?/x,
/(?:[0-9A-Fa-f]{1,4}:){6,6}
\d+\.\d+\.\d+\.\d+/x,
/(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)? ::
(?:[0-9A-Fa-f]{1,4}:)*
\d+\.\d+\.\d+\.\d+/x,
/[Ff][Ee]80
(?::[0-9A-Fa-f]{1,4}){7}
%[-0-9A-Za-z._~]+/x,
/[Ff][Ee]80:
(?:
(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)? ::
(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?
|
:(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?
)?
:[0-9A-Fa-f]{1,4}%[-0-9A-Za-z._~]+/x)
AUTHORITY = /
\A
(?<host>
# Match IPv6 as a string of hex digits and colons in square brackets
\[(?<address>#{ipv6})\]
|
# Match any other printable string (except square brackets) as a hostname
(?<address>[[[:graph:]&&[^\[\]]]]*?)
)
(:(?<port>\d+))?
\z
/x
private_constant :AUTHORITY
def split_authority(authority)
return [] if authority.nil?
return [] unless match = AUTHORITY.match(authority)
return match[:host], match[:address], match[:port]&.to_i
end
def reject_trusted_ip_addresses(ip_addresses)
ip_addresses.reject { |ip| trusted_proxy?(ip) }
end
FORWARDED_SCHEME_HEADERS = {
proto: HTTP_X_FORWARDED_PROTO,
scheme: HTTP_X_FORWARDED_SCHEME
}.freeze
private_constant :FORWARDED_SCHEME_HEADERS
def forwarded_scheme
forwarded_priority.each do |type|
case type
when :forwarded
if (forwarded_proto = get_http_forwarded(:proto)) &&
(scheme = allowed_scheme(forwarded_proto.last))
return scheme
end
when :x_forwarded
x_forwarded_proto_priority.each do |x_type|
if header = FORWARDED_SCHEME_HEADERS[x_type]
split_header(get_header(header)).reverse_each do |scheme|
if allowed_scheme(scheme)
return scheme
end
end
end
end
end
end
nil
end
def allowed_scheme(header)
header if ALLOWED_SCHEMES.include?(header)
end
def forwarded_priority
Request.forwarded_priority
end
def x_forwarded_proto_priority
Request.x_forwarded_proto_priority
end
end
include Env
include Helpers
end
end
# :nocov:
require_relative 'multipart' unless defined?(Rack::Multipart)
# :nocov:

View File

@ -1,393 +0,0 @@
# frozen_string_literal: true
require 'time'
require_relative 'constants'
require_relative 'utils'
require_relative 'media_type'
require_relative 'headers'
module Rack
# Rack::Response provides a convenient interface to create a Rack
# response.
#
# It allows setting of headers and cookies, and provides useful
# defaults (an OK response with empty headers and body).
#
# You can use Response#write to iteratively generate your response,
# but note that this is buffered by Rack::Response until you call
# +finish+. +finish+ however can take a block inside which calls to
# +write+ are synchronous with the Rack response.
#
# Your application's +call+ should end returning Response#finish.
class Response
def self.[](status, headers, body)
self.new(body, status, headers)
end
CHUNKED = 'chunked'
STATUS_WITH_NO_ENTITY_BODY = Utils::STATUS_WITH_NO_ENTITY_BODY
attr_accessor :length, :status, :body
attr_reader :headers
# Deprecated, use headers instead.
def header
warn 'Rack::Response#header is deprecated and will be removed in Rack 3.1', uplevel: 1
headers
end
# Initialize the response object with the specified +body+, +status+
# and +headers+.
#
# If the +body+ is +nil+, construct an empty response object with internal
# buffering.
#
# If the +body+ responds to +to_str+, assume it's a string-like object and
# construct a buffered response object containing using that string as the
# initial contents of the buffer.
#
# Otherwise it is expected +body+ conforms to the normal requirements of a
# Rack response body, typically implementing one of +each+ (enumerable
# body) or +call+ (streaming body).
#
# The +status+ defaults to +200+ which is the "OK" HTTP status code. You
# can provide any other valid status code.
#
# The +headers+ must be a +Hash+ of key-value header pairs which conform to
# the Rack specification for response headers. The key must be a +String+
# instance and the value can be either a +String+ or +Array+ instance.
def initialize(body = nil, status = 200, headers = {})
@status = status.to_i
unless headers.is_a?(Hash)
warn "Providing non-hash headers to Rack::Response is deprecated and will be removed in Rack 3.1", uplevel: 1
end
@headers = Headers.new
# Convert headers input to a plain hash with lowercase keys.
headers.each do |k, v|
@headers[k] = v
end
@writer = self.method(:append)
@block = nil
# Keep track of whether we have expanded the user supplied body.
if body.nil?
@body = []
@buffered = true
@length = 0
elsif body.respond_to?(:to_str)
@body = [body]
@buffered = true
@length = body.to_str.bytesize
else
@body = body
@buffered = nil # undetermined as of yet.
@length = 0
end
yield self if block_given?
end
def redirect(target, status = 302)
self.status = status
self.location = target
end
def chunked?
CHUNKED == get_header(TRANSFER_ENCODING)
end
def no_entity_body?
# The response body is an enumerable body and it is not allowed to have an entity body.
@body.respond_to?(:each) && STATUS_WITH_NO_ENTITY_BODY[@status]
end
# Generate a response array consistent with the requirements of the SPEC.
# @return [Array] a 3-tuple suitable of `[status, headers, body]`
# which is suitable to be returned from the middleware `#call(env)` method.
def finish(&block)
if no_entity_body?
delete_header CONTENT_TYPE
delete_header CONTENT_LENGTH
close
return [@status, @headers, []]
else
if block_given?
@block = block
return [@status, @headers, self]
else
return [@status, @headers, @body]
end
end
end
alias to_a finish # For *response
def each(&callback)
@body.each(&callback)
@buffered = true
if @block
@writer = callback
@block.call(self)
end
end
# Append to body and update content-length.
#
# NOTE: Do not mix #write and direct #body access!
#
def write(chunk)
buffered_body!
@writer.call(chunk.to_s)
end
def close
@body.close if @body.respond_to?(:close)
end
def empty?
@block == nil && @body.empty?
end
def has_header?(key)
raise ArgumentError unless key.is_a?(String)
@headers.key?(key)
end
def get_header(key)
raise ArgumentError unless key.is_a?(String)
@headers[key]
end
def set_header(key, value)
raise ArgumentError unless key.is_a?(String)
@headers[key] = value
end
def delete_header(key)
raise ArgumentError unless key.is_a?(String)
@headers.delete key
end
alias :[] :get_header
alias :[]= :set_header
module Helpers
def invalid?; status < 100 || status >= 600; end
def informational?; status >= 100 && status < 200; end
def successful?; status >= 200 && status < 300; end
def redirection?; status >= 300 && status < 400; end
def client_error?; status >= 400 && status < 500; end
def server_error?; status >= 500 && status < 600; end
def ok?; status == 200; end
def created?; status == 201; end
def accepted?; status == 202; end
def no_content?; status == 204; end
def moved_permanently?; status == 301; end
def bad_request?; status == 400; end
def unauthorized?; status == 401; end
def forbidden?; status == 403; end
def not_found?; status == 404; end
def method_not_allowed?; status == 405; end
def not_acceptable?; status == 406; end
def request_timeout?; status == 408; end
def precondition_failed?; status == 412; end
def unprocessable?; status == 422; end
def redirect?; [301, 302, 303, 307, 308].include? status; end
def include?(header)
has_header?(header)
end
# Add a header that may have multiple values.
#
# Example:
# response.add_header 'vary', 'accept-encoding'
# response.add_header 'vary', 'cookie'
#
# assert_equal 'accept-encoding,cookie', response.get_header('vary')
#
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
def add_header(key, value)
raise ArgumentError unless key.is_a?(String)
if value.nil?
return get_header(key)
end
value = value.to_s
if header = get_header(key)
if header.is_a?(Array)
header << value
else
set_header(key, [header, value])
end
else
set_header(key, value)
end
end
# Get the content type of the response.
def content_type
get_header CONTENT_TYPE
end
# Set the content type of the response.
def content_type=(content_type)
set_header CONTENT_TYPE, content_type
end
def media_type
MediaType.type(content_type)
end
def media_type_params
MediaType.params(content_type)
end
def content_length
cl = get_header CONTENT_LENGTH
cl ? cl.to_i : cl
end
def location
get_header "location"
end
def location=(location)
set_header "location", location
end
def set_cookie(key, value)
add_header SET_COOKIE, Utils.set_cookie_header(key, value)
end
def delete_cookie(key, value = {})
set_header(SET_COOKIE,
Utils.delete_set_cookie_header!(
get_header(SET_COOKIE), key, value
)
)
end
def set_cookie_header
get_header SET_COOKIE
end
def set_cookie_header=(value)
set_header SET_COOKIE, value
end
def cache_control
get_header CACHE_CONTROL
end
def cache_control=(value)
set_header CACHE_CONTROL, value
end
# Specifies that the content shouldn't be cached. Overrides `cache!` if already called.
def do_not_cache!
set_header CACHE_CONTROL, "no-cache, must-revalidate"
set_header EXPIRES, Time.now.httpdate
end
# Specify that the content should be cached.
# @param duration [Integer] The number of seconds until the cache expires.
# @option directive [String] The cache control directive, one of "public", "private", "no-cache" or "no-store".
def cache!(duration = 3600, directive: "public")
unless headers[CACHE_CONTROL] =~ /no-cache/
set_header CACHE_CONTROL, "#{directive}, max-age=#{duration}"
set_header EXPIRES, (Time.now + duration).httpdate
end
end
def etag
get_header ETAG
end
def etag=(value)
set_header ETAG, value
end
protected
def buffered_body!
if @buffered.nil?
if @body.is_a?(Array)
# The user supplied body was an array:
@body = @body.compact
@body.each do |part|
@length += part.to_s.bytesize
end
elsif @body.respond_to?(:each)
# Turn the user supplied body into a buffered array:
body = @body
@body = Array.new
body.each do |part|
@writer.call(part.to_s)
end
body.close if body.respond_to?(:close)
@buffered = true
else
@buffered = false
end
end
return @buffered
end
def append(chunk)
@body << chunk
unless chunked?
@length += chunk.bytesize
set_header(CONTENT_LENGTH, @length.to_s)
end
return chunk
end
end
include Helpers
class Raw
include Helpers
attr_reader :headers
attr_accessor :status
def initialize(status, headers)
@status = status
@headers = headers
end
def has_header?(key)
headers.key?(key)
end
def get_header(key)
headers[key]
end
def set_header(key, value)
headers[key] = value
end
def delete_header(key)
headers.delete(key)
end
end
end
end

View File

@ -1,113 +0,0 @@
# -*- encoding: binary -*-
# frozen_string_literal: true
require 'tempfile'
require_relative 'constants'
module Rack
# Class which can make any IO object rewindable, including non-rewindable ones. It does
# this by buffering the data into a tempfile, which is rewindable.
#
# Don't forget to call #close when you're done. This frees up temporary resources that
# RewindableInput uses, though it does *not* close the original IO object.
class RewindableInput
# Makes rack.input rewindable, for compatibility with applications and middleware
# designed for earlier versions of Rack (where rack.input was required to be
# rewindable).
class Middleware
def initialize(app)
@app = app
end
def call(env)
env[RACK_INPUT] = RewindableInput.new(env[RACK_INPUT])
@app.call(env)
end
end
def initialize(io)
@io = io
@rewindable_io = nil
@unlinked = false
end
def gets
make_rewindable unless @rewindable_io
@rewindable_io.gets
end
def read(*args)
make_rewindable unless @rewindable_io
@rewindable_io.read(*args)
end
def each(&block)
make_rewindable unless @rewindable_io
@rewindable_io.each(&block)
end
def rewind
make_rewindable unless @rewindable_io
@rewindable_io.rewind
end
def size
make_rewindable unless @rewindable_io
@rewindable_io.size
end
# Closes this RewindableInput object without closing the originally
# wrapped IO object. Cleans up any temporary resources that this RewindableInput
# has created.
#
# This method may be called multiple times. It does nothing on subsequent calls.
def close
if @rewindable_io
if @unlinked
@rewindable_io.close
else
@rewindable_io.close!
end
@rewindable_io = nil
end
end
private
def make_rewindable
# Buffer all data into a tempfile. Since this tempfile is private to this
# RewindableInput object, we chmod it so that nobody else can read or write
# it. On POSIX filesystems we also unlink the file so that it doesn't
# even have a file entry on the filesystem anymore, though we can still
# access it because we have the file handle open.
@rewindable_io = Tempfile.new('RackRewindableInput')
@rewindable_io.chmod(0000)
@rewindable_io.set_encoding(Encoding::BINARY)
@rewindable_io.binmode
# :nocov:
if filesystem_has_posix_semantics?
raise 'Unlink failed. IO closed.' if @rewindable_io.closed?
@unlinked = true
end
# :nocov:
buffer = "".dup
while @io.read(1024 * 4, buffer)
entire_buffer_written_out = false
while !entire_buffer_written_out
written = @rewindable_io.write(buffer)
entire_buffer_written_out = written == buffer.bytesize
if !entire_buffer_written_out
buffer.slice!(0 .. written - 1)
end
end
end
@rewindable_io.rewind
end
def filesystem_has_posix_semantics?
RUBY_PLATFORM !~ /(mswin|mingw|cygwin|java)/
end
end
end

View File

@ -1,35 +0,0 @@
# frozen_string_literal: true
require_relative 'utils'
module Rack
# Sets an "x-runtime" response header, indicating the response
# time of the request, in seconds
#
# You can put it right before the application to see the processing
# time, or before all the other middlewares to include time for them,
# too.
class Runtime
FORMAT_STRING = "%0.6f" # :nodoc:
HEADER_NAME = "x-runtime" # :nodoc:
def initialize(app, name = nil)
@app = app
@header_name = HEADER_NAME
@header_name += "-#{name.to_s.downcase}" if name
end
def call(env)
start_time = Utils.clock_time
_, headers, _ = response = @app.call(env)
request_time = Utils.clock_time - start_time
unless headers.key?(@header_name)
headers[@header_name] = FORMAT_STRING % request_time
end
response
end
end
end

View File

@ -1,167 +0,0 @@
# frozen_string_literal: true
require_relative 'constants'
require_relative 'utils'
require_relative 'body_proxy'
module Rack
# = Sendfile
#
# The Sendfile middleware intercepts responses whose body is being
# served from a file and replaces it with a server specific x-sendfile
# header. The web server is then responsible for writing the file contents
# to the client. This can dramatically reduce the amount of work required
# by the Ruby backend and takes advantage of the web server's optimized file
# delivery code.
#
# In order to take advantage of this middleware, the response body must
# respond to +to_path+ and the request must include an x-sendfile-type
# header. Rack::Files and other components implement +to_path+ so there's
# rarely anything you need to do in your application. The x-sendfile-type
# header is typically set in your web servers configuration. The following
# sections attempt to document
#
# === Nginx
#
# Nginx supports the x-accel-redirect header. This is similar to x-sendfile
# but requires parts of the filesystem to be mapped into a private URL
# hierarchy.
#
# The following example shows the Nginx configuration required to create
# a private "/files/" area, enable x-accel-redirect, and pass the special
# x-sendfile-type and x-accel-mapping headers to the backend:
#
# location ~ /files/(.*) {
# internal;
# alias /var/www/$1;
# }
#
# location / {
# proxy_redirect off;
#
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
#
# proxy_set_header x-sendfile-type x-accel-redirect;
# proxy_set_header x-accel-mapping /var/www/=/files/;
#
# proxy_pass http://127.0.0.1:8080/;
# }
#
# Note that the x-sendfile-type header must be set exactly as shown above.
# The x-accel-mapping header should specify the location on the file system,
# followed by an equals sign (=), followed name of the private URL pattern
# that it maps to. The middleware performs a simple substitution on the
# resulting path.
#
# See Also: https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile
#
# === lighttpd
#
# Lighttpd has supported some variation of the x-sendfile header for some
# time, although only recent version support x-sendfile in a reverse proxy
# configuration.
#
# $HTTP["host"] == "example.com" {
# proxy-core.protocol = "http"
# proxy-core.balancer = "round-robin"
# proxy-core.backends = (
# "127.0.0.1:8000",
# "127.0.0.1:8001",
# ...
# )
#
# proxy-core.allow-x-sendfile = "enable"
# proxy-core.rewrite-request = (
# "x-sendfile-type" => (".*" => "x-sendfile")
# )
# }
#
# See Also: http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModProxyCore
#
# === Apache
#
# x-sendfile is supported under Apache 2.x using a separate module:
#
# https://tn123.org/mod_xsendfile/
#
# Once the module is compiled and installed, you can enable it using
# XSendFile config directive:
#
# RequestHeader Set x-sendfile-type x-sendfile
# ProxyPassReverse / http://localhost:8001/
# XSendFile on
#
# === Mapping parameter
#
# The third parameter allows for an overriding extension of the
# x-accel-mapping header. Mappings should be provided in tuples of internal to
# external. The internal values may contain regular expression syntax, they
# will be matched with case indifference.
class Sendfile
def initialize(app, variation = nil, mappings = [])
@app = app
@variation = variation
@mappings = mappings.map do |internal, external|
[/^#{internal}/i, external]
end
end
def call(env)
_, headers, body = response = @app.call(env)
if body.respond_to?(:to_path)
case type = variation(env)
when /x-accel-redirect/i
path = ::File.expand_path(body.to_path)
if url = map_accel_path(env, path)
headers[CONTENT_LENGTH] = '0'
# '?' must be percent-encoded because it is not query string but a part of path
headers[type.downcase] = ::Rack::Utils.escape_path(url).gsub('?', '%3F')
obody = body
response[2] = Rack::BodyProxy.new([]) do
obody.close if obody.respond_to?(:close)
end
else
env[RACK_ERRORS].puts "x-accel-mapping header missing"
end
when /x-sendfile|x-lighttpd-send-file/i
path = ::File.expand_path(body.to_path)
headers[CONTENT_LENGTH] = '0'
headers[type.downcase] = path
obody = body
response[2] = Rack::BodyProxy.new([]) do
obody.close if obody.respond_to?(:close)
end
when '', nil
else
env[RACK_ERRORS].puts "Unknown x-sendfile variation: '#{type}'.\n"
end
end
response
end
private
def variation(env)
@variation ||
env['sendfile.type'] ||
env['HTTP_X_SENDFILE_TYPE']
end
def map_accel_path(env, path)
if mapping = @mappings.find { |internal, _| internal =~ path }
path.sub(*mapping)
elsif mapping = env['HTTP_X_ACCEL_MAPPING']
mapping.split(',').map(&:strip).each do |m|
internal, external = m.split('=', 2).map(&:strip)
new_path = path.sub(/^#{internal}/i, external)
return new_path unless path == new_path
end
path
end
end
end
end

View File

@ -1,403 +0,0 @@
# frozen_string_literal: true
require 'ostruct'
require 'erb'
require_relative 'constants'
require_relative 'utils'
require_relative 'request'
module Rack
# Rack::ShowExceptions catches all exceptions raised from the app it
# wraps. It shows a useful backtrace with the sourcefile and
# clickable context, the whole Rack environment and the request
# data.
#
# Be careful when you use this on public-facing sites as it could
# reveal information helpful to attackers.
class ShowExceptions
CONTEXT = 7
def initialize(app)
@app = app
end
def call(env)
@app.call(env)
rescue StandardError, LoadError, SyntaxError => e
exception_string = dump_exception(e)
env[RACK_ERRORS].puts(exception_string)
env[RACK_ERRORS].flush
if accepts_html?(env)
content_type = "text/html"
body = pretty(env, e)
else
content_type = "text/plain"
body = exception_string
end
[
500,
{
CONTENT_TYPE => content_type,
CONTENT_LENGTH => body.bytesize.to_s,
},
[body],
]
end
def prefers_plaintext?(env)
!accepts_html?(env)
end
def accepts_html?(env)
Rack::Utils.best_q_match(env["HTTP_ACCEPT"], %w[text/html])
end
private :accepts_html?
def dump_exception(exception)
if exception.respond_to?(:detailed_message)
message = exception.detailed_message(highlight: false)
else
message = exception.message
end
string = "#{exception.class}: #{message}\n".dup
string << exception.backtrace.map { |l| "\t#{l}" }.join("\n")
string
end
def pretty(env, exception)
req = Rack::Request.new(env)
# This double assignment is to prevent an "unused variable" warning.
# Yes, it is dumb, but I don't like Ruby yelling at me.
path = path = (req.script_name + req.path_info).squeeze("/")
# This double assignment is to prevent an "unused variable" warning.
# Yes, it is dumb, but I don't like Ruby yelling at me.
frames = frames = exception.backtrace.map { |line|
frame = OpenStruct.new
if line =~ /(.*?):(\d+)(:in `(.*)')?/
frame.filename = $1
frame.lineno = $2.to_i
frame.function = $4
begin
lineno = frame.lineno - 1
lines = ::File.readlines(frame.filename)
frame.pre_context_lineno = [lineno - CONTEXT, 0].max
frame.pre_context = lines[frame.pre_context_lineno...lineno]
frame.context_line = lines[lineno].chomp
frame.post_context_lineno = [lineno + CONTEXT, lines.size].min
frame.post_context = lines[lineno + 1..frame.post_context_lineno]
rescue
end
frame
else
nil
end
}.compact
template.result(binding)
end
def template
TEMPLATE
end
def h(obj) # :nodoc:
case obj
when String
Utils.escape_html(obj)
else
Utils.escape_html(obj.inspect)
end
end
# :stopdoc:
# adapted from Django <www.djangoproject.com>
# Copyright (c) Django Software Foundation and individual contributors.
# Used under the modified BSD license:
# http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
TEMPLATE = ERB.new(<<-'HTML'.gsub(/^ /, ''))
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta name="robots" content="NONE,NOARCHIVE" />
<title><%=h exception.class %> at <%=h path %></title>
<style type="text/css">
html * { padding:0; margin:0; }
body * { padding:10px 20px; }
body * * { padding:0; }
body { font:small sans-serif; }
body>div { border-bottom:1px solid #ddd; }
h1 { font-weight:normal; }
h2 { margin-bottom:.8em; }
h2 span { font-size:80%; color:#666; font-weight:normal; }
h3 { margin:1em 0 .5em 0; }
h4 { margin:0 0 .5em 0; font-weight: normal; }
table {
border:1px solid #ccc; border-collapse: collapse; background:white; }
tbody td, tbody th { vertical-align:top; padding:2px 3px; }
thead th {
padding:1px 6px 1px 3px; background:#fefefe; text-align:left;
font-weight:normal; font-size:11px; border:1px solid #ddd; }
tbody th { text-align:right; color:#666; padding-right:.5em; }
table.vars { margin:5px 0 2px 40px; }
table.vars td, table.req td { font-family:monospace; }
table td.code { width:100%;}
table td.code div { overflow:hidden; }
table.source th { color:#666; }
table.source td {
font-family:monospace; white-space:pre; border-bottom:1px solid #eee; }
ul.traceback { list-style-type:none; }
ul.traceback li.frame { margin-bottom:1em; }
div.context { margin: 10px 0; }
div.context ol {
padding-left:30px; margin:0 10px; list-style-position: inside; }
div.context ol li {
font-family:monospace; white-space:pre; color:#666; cursor:pointer; }
div.context ol.context-line li { color:black; background-color:#ccc; }
div.context ol.context-line li span { float: right; }
div.commands { margin-left: 40px; }
div.commands a { color:black; text-decoration:none; }
#summary { background: #ffc; }
#summary h2 { font-family: monospace; font-weight: normal; color: #666; white-space: pre-wrap; }
#summary ul#quicklinks { list-style-type: none; margin-bottom: 2em; }
#summary ul#quicklinks li { float: left; padding: 0 1em; }
#summary ul#quicklinks>li+li { border-left: 1px #666 solid; }
#explanation { background:#eee; }
#template, #template-not-exist { background:#f6f6f6; }
#template-not-exist ul { margin: 0 0 0 20px; }
#traceback { background:#eee; }
#requestinfo { background:#f6f6f6; padding-left:120px; }
#summary table { border:none; background:transparent; }
#requestinfo h2, #requestinfo h3 { position:relative; margin-left:-100px; }
#requestinfo h3 { margin-bottom:-1em; }
.error { background: #ffc; }
.specific { color:#cc3300; font-weight:bold; }
</style>
<script type="text/javascript">
//<!--
function getElementsByClassName(oElm, strTagName, strClassName){
// Written by Jonathan Snook, http://www.snook.ca/jon;
// Add-ons by Robert Nyman, http://www.robertnyman.com
var arrElements = (strTagName == "*" && document.all)? document.all :
oElm.getElementsByTagName(strTagName);
var arrReturnElements = new Array();
strClassName = strClassName.replace(/\-/g, "\\-");
var oRegExp = new RegExp("(^|\\s)" + strClassName + "(\\s|$$)");
var oElement;
for(var i=0; i<arrElements.length; i++){
oElement = arrElements[i];
if(oRegExp.test(oElement.className)){
arrReturnElements.push(oElement);
}
}
return (arrReturnElements)
}
function hideAll(elems) {
for (var e = 0; e < elems.length; e++) {
elems[e].style.display = 'none';
}
}
window.onload = function() {
hideAll(getElementsByClassName(document, 'table', 'vars'));
hideAll(getElementsByClassName(document, 'ol', 'pre-context'));
hideAll(getElementsByClassName(document, 'ol', 'post-context'));
}
function toggle() {
for (var i = 0; i < arguments.length; i++) {
var e = document.getElementById(arguments[i]);
if (e) {
e.style.display = e.style.display == 'none' ? 'block' : 'none';
}
}
return false;
}
function varToggle(link, id) {
toggle('v' + id);
var s = link.getElementsByTagName('span')[0];
var uarr = String.fromCharCode(0x25b6);
var darr = String.fromCharCode(0x25bc);
s.innerHTML = s.innerHTML == uarr ? darr : uarr;
return false;
}
//-->
</script>
</head>
<body>
<div id="summary">
<h1><%=h exception.class %> at <%=h path %></h1>
<% if exception.respond_to?(:detailed_message) %>
<h2><%=h exception.detailed_message(highlight: false) %></h2>
<% else %>
<h2><%=h exception.message %></h2>
<% end %>
<table><tr>
<th>Ruby</th>
<td>
<% if first = frames.first %>
<code><%=h first.filename %></code>: in <code><%=h first.function %></code>, line <%=h frames.first.lineno %>
<% else %>
unknown location
<% end %>
</td>
</tr><tr>
<th>Web</th>
<td><code><%=h req.request_method %> <%=h(req.host + path)%></code></td>
</tr></table>
<h3>Jump to:</h3>
<ul id="quicklinks">
<li><a href="#get-info">GET</a></li>
<li><a href="#post-info">POST</a></li>
<li><a href="#cookie-info">Cookies</a></li>
<li><a href="#env-info">ENV</a></li>
</ul>
</div>
<div id="traceback">
<h2>Traceback <span>(innermost first)</span></h2>
<ul class="traceback">
<% frames.each { |frame| %>
<li class="frame">
<code><%=h frame.filename %></code>: in <code><%=h frame.function %></code>
<% if frame.context_line %>
<div class="context" id="c<%=h frame.object_id %>">
<% if frame.pre_context %>
<ol start="<%=h frame.pre_context_lineno+1 %>" class="pre-context" id="pre<%=h frame.object_id %>">
<% frame.pre_context.each { |line| %>
<li onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>')"><%=h line %></li>
<% } %>
</ol>
<% end %>
<ol start="<%=h frame.lineno %>" class="context-line">
<li onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>')"><%=h frame.context_line %><span>...</span></li></ol>
<% if frame.post_context %>
<ol start='<%=h frame.lineno+1 %>' class="post-context" id="post<%=h frame.object_id %>">
<% frame.post_context.each { |line| %>
<li onclick="toggle('pre<%=h frame.object_id %>', 'post<%=h frame.object_id %>')"><%=h line %></li>
<% } %>
</ol>
<% end %>
</div>
<% end %>
</li>
<% } %>
</ul>
</div>
<div id="requestinfo">
<h2>Request information</h2>
<h3 id="get-info">GET</h3>
<% if req.GET and not req.GET.empty? %>
<table class="req">
<thead>
<tr>
<th>Variable</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<% req.GET.sort_by { |k, v| k.to_s }.each { |key, val| %>
<tr>
<td><%=h key %></td>
<td class="code"><div><%=h val.inspect %></div></td>
</tr>
<% } %>
</tbody>
</table>
<% else %>
<p>No GET data.</p>
<% end %>
<h3 id="post-info">POST</h3>
<% if ((req.POST and not req.POST.empty?) rescue (no_post_data = "Invalid POST data"; nil)) %>
<table class="req">
<thead>
<tr>
<th>Variable</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<% req.POST.sort_by { |k, v| k.to_s }.each { |key, val| %>
<tr>
<td><%=h key %></td>
<td class="code"><div><%=h val.inspect %></div></td>
</tr>
<% } %>
</tbody>
</table>
<% else %>
<p><%= no_post_data || "No POST data" %>.</p>
<% end %>
<h3 id="cookie-info">COOKIES</h3>
<% unless req.cookies.empty? %>
<table class="req">
<thead>
<tr>
<th>Variable</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<% req.cookies.each { |key, val| %>
<tr>
<td><%=h key %></td>
<td class="code"><div><%=h val.inspect %></div></td>
</tr>
<% } %>
</tbody>
</table>
<% else %>
<p>No cookie data.</p>
<% end %>
<h3 id="env-info">Rack ENV</h3>
<table class="req">
<thead>
<tr>
<th>Variable</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<% env.sort_by { |k, v| k.to_s }.each { |key, val| %>
<tr>
<td><%=h key %></td>
<td class="code"><div><%=h val.inspect %></div></td>
</tr>
<% } %>
</tbody>
</table>
</div>
<div id="explanation">
<p>
You're seeing this error because you use <code>Rack::ShowExceptions</code>.
</p>
</div>
</body>
</html>
HTML
# :startdoc:
end
end

View File

@ -1,123 +0,0 @@
# frozen_string_literal: true
require 'erb'
require_relative 'constants'
require_relative 'utils'
require_relative 'request'
require_relative 'body_proxy'
module Rack
# Rack::ShowStatus catches all empty responses and replaces them
# with a site explaining the error.
#
# Additional details can be put into <tt>rack.showstatus.detail</tt>
# and will be shown as HTML. If such details exist, the error page
# is always rendered, even if the reply was not empty.
class ShowStatus
def initialize(app)
@app = app
@template = ERB.new(TEMPLATE)
end
def call(env)
status, headers, body = response = @app.call(env)
empty = headers[CONTENT_LENGTH].to_i <= 0
# client or server error, or explicit message
if (status.to_i >= 400 && empty) || env[RACK_SHOWSTATUS_DETAIL]
# This double assignment is to prevent an "unused variable" warning.
# Yes, it is dumb, but I don't like Ruby yelling at me.
req = req = Rack::Request.new(env)
message = Rack::Utils::HTTP_STATUS_CODES[status.to_i] || status.to_s
# This double assignment is to prevent an "unused variable" warning.
# Yes, it is dumb, but I don't like Ruby yelling at me.
detail = detail = env[RACK_SHOWSTATUS_DETAIL] || message
html = @template.result(binding)
size = html.bytesize
response[2] = Rack::BodyProxy.new([html]) do
body.close if body.respond_to?(:close)
end
headers[CONTENT_TYPE] = "text/html"
headers[CONTENT_LENGTH] = size.to_s
end
response
end
def h(obj) # :nodoc:
case obj
when String
Utils.escape_html(obj)
else
Utils.escape_html(obj.inspect)
end
end
# :stopdoc:
# adapted from Django <www.djangoproject.com>
# Copyright (c) Django Software Foundation and individual contributors.
# Used under the modified BSD license:
# http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
TEMPLATE = <<'HTML'
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title><%=h message %> at <%=h req.script_name + req.path_info %></title>
<meta name="robots" content="NONE,NOARCHIVE" />
<style type="text/css">
html * { padding:0; margin:0; }
body * { padding:10px 20px; }
body * * { padding:0; }
body { font:small sans-serif; background:#eee; }
body>div { border-bottom:1px solid #ddd; }
h1 { font-weight:normal; margin-bottom:.4em; }
h1 span { font-size:60%; color:#666; font-weight:normal; }
table { border:none; border-collapse: collapse; width:100%; }
td, th { vertical-align:top; padding:2px 3px; }
th { width:12em; text-align:right; color:#666; padding-right:.5em; }
#info { background:#f6f6f6; }
#info ol { margin: 0.5em 4em; }
#info ol li { font-family: monospace; }
#summary { background: #ffc; }
#explanation { background:#eee; border-bottom: 0px none; }
</style>
</head>
<body>
<div id="summary">
<h1><%=h message %> <span>(<%= status.to_i %>)</span></h1>
<table class="meta">
<tr>
<th>Request Method:</th>
<td><%=h req.request_method %></td>
</tr>
<tr>
<th>Request URL:</th>
<td><%=h req.url %></td>
</tr>
</table>
</div>
<div id="info">
<p><%=h detail %></p>
</div>
<div id="explanation">
<p>
You're seeing this error because you use <code>Rack::ShowStatus</code>.
</p>
</div>
</body>
</html>
HTML
# :startdoc:
end
end

View File

@ -1,187 +0,0 @@
# frozen_string_literal: true
require_relative 'constants'
require_relative 'files'
require_relative 'mime'
module Rack
# The Rack::Static middleware intercepts requests for static files
# (javascript files, images, stylesheets, etc) based on the url prefixes or
# route mappings passed in the options, and serves them using a Rack::Files
# object. This allows a Rack stack to serve both static and dynamic content.
#
# Examples:
#
# Serve all requests beginning with /media from the "media" folder located
# in the current directory (ie media/*):
#
# use Rack::Static, :urls => ["/media"]
#
# Same as previous, but instead of returning 404 for missing files under
# /media, call the next middleware:
#
# use Rack::Static, :urls => ["/media"], :cascade => true
#
# Serve all requests beginning with /css or /images from the folder "public"
# in the current directory (ie public/css/* and public/images/*):
#
# use Rack::Static, :urls => ["/css", "/images"], :root => "public"
#
# Serve all requests to / with "index.html" from the folder "public" in the
# current directory (ie public/index.html):
#
# use Rack::Static, :urls => {"/" => 'index.html'}, :root => 'public'
#
# Serve all requests normally from the folder "public" in the current
# directory but uses index.html as default route for "/"
#
# use Rack::Static, :urls => [""], :root => 'public', :index =>
# 'index.html'
#
# Set custom HTTP Headers for based on rules:
#
# use Rack::Static, :root => 'public',
# :header_rules => [
# [rule, {header_field => content, header_field => content}],
# [rule, {header_field => content}]
# ]
#
# Rules for selecting files:
#
# 1) All files
# Provide the :all symbol
# :all => Matches every file
#
# 2) Folders
# Provide the folder path as a string
# '/folder' or '/folder/subfolder' => Matches files in a certain folder
#
# 3) File Extensions
# Provide the file extensions as an array
# ['css', 'js'] or %w(css js) => Matches files ending in .css or .js
#
# 4) Regular Expressions / Regexp
# Provide a regular expression
# %r{\.(?:css|js)\z} => Matches files ending in .css or .js
# /\.(?:eot|ttf|otf|woff2|woff|svg)\z/ => Matches files ending in
# the most common web font formats (.eot, .ttf, .otf, .woff2, .woff, .svg)
# Note: This Regexp is available as a shortcut, using the :fonts rule
#
# 5) Font Shortcut
# Provide the :fonts symbol
# :fonts => Uses the Regexp rule stated right above to match all common web font endings
#
# Rule Ordering:
# Rules are applied in the order that they are provided.
# List rather general rules above special ones.
#
# Complete example use case including HTTP header rules:
#
# use Rack::Static, :root => 'public',
# :header_rules => [
# # Cache all static files in public caches (e.g. Rack::Cache)
# # as well as in the browser
# [:all, {'cache-control' => 'public, max-age=31536000'}],
#
# # Provide web fonts with cross-origin access-control-headers
# # Firefox requires this when serving assets using a Content Delivery Network
# [:fonts, {'access-control-allow-origin' => '*'}]
# ]
#
class Static
def initialize(app, options = {})
@app = app
@urls = options[:urls] || ["/favicon.ico"]
@index = options[:index]
@gzip = options[:gzip]
@cascade = options[:cascade]
root = options[:root] || Dir.pwd
# HTTP Headers
@header_rules = options[:header_rules] || []
# Allow for legacy :cache_control option while prioritizing global header_rules setting
@header_rules.unshift([:all, { CACHE_CONTROL => options[:cache_control] }]) if options[:cache_control]
@file_server = Rack::Files.new(root)
end
def add_index_root?(path)
@index && route_file(path) && path.end_with?('/')
end
def overwrite_file_path(path)
@urls.kind_of?(Hash) && @urls.key?(path) || add_index_root?(path)
end
def route_file(path)
@urls.kind_of?(Array) && @urls.any? { |url| path.index(url) == 0 }
end
def can_serve(path)
route_file(path) || overwrite_file_path(path)
end
def call(env)
path = env[PATH_INFO]
if can_serve(path)
if overwrite_file_path(path)
env[PATH_INFO] = (add_index_root?(path) ? path + @index : @urls[path])
elsif @gzip && env['HTTP_ACCEPT_ENCODING'] && /\bgzip\b/.match?(env['HTTP_ACCEPT_ENCODING'])
path = env[PATH_INFO]
env[PATH_INFO] += '.gz'
response = @file_server.call(env)
env[PATH_INFO] = path
if response[0] == 404
response = nil
elsif response[0] == 304
# Do nothing, leave headers as is
else
response[1][CONTENT_TYPE] = Mime.mime_type(::File.extname(path), 'text/plain')
response[1]['content-encoding'] = 'gzip'
end
end
path = env[PATH_INFO]
response ||= @file_server.call(env)
if @cascade && response[0] == 404
return @app.call(env)
end
headers = response[1]
applicable_rules(path).each do |rule, new_headers|
new_headers.each { |field, content| headers[field] = content }
end
response
else
@app.call(env)
end
end
# Convert HTTP header rules to HTTP headers
def applicable_rules(path)
@header_rules.find_all do |rule, new_headers|
case rule
when :all
true
when :fonts
/\.(?:ttf|otf|eot|woff2|woff|svg)\z/.match?(path)
when String
path = ::Rack::Utils.unescape(path)
path.start_with?(rule) || path.start_with?('/' + rule)
when Array
/\.(#{rule.join('|')})\z/.match?(path)
when Regexp
rule.match?(path)
else
false
end
end
end
end
end

View File

@ -1,33 +0,0 @@
# frozen_string_literal: true
require_relative 'constants'
require_relative 'body_proxy'
module Rack
# Middleware tracks and cleans Tempfiles created throughout a request (i.e. Rack::Multipart)
# Ideas/strategy based on posts by Eric Wong and Charles Oliver Nutter
# https://groups.google.com/forum/#!searchin/rack-devel/temp/rack-devel/brK8eh-MByw/sw61oJJCGRMJ
class TempfileReaper
def initialize(app)
@app = app
end
def call(env)
env[RACK_TEMPFILES] ||= []
begin
_, _, body = response = @app.call(env)
rescue Exception
env[RACK_TEMPFILES]&.each(&:close!)
raise
end
response[2] = BodyProxy.new(body) do
env[RACK_TEMPFILES]&.each(&:close!)
end
response
end
end
end

View File

@ -1,99 +0,0 @@
# frozen_string_literal: true
require 'set'
require_relative 'constants'
module Rack
# Rack::URLMap takes a hash mapping urls or paths to apps, and
# dispatches accordingly. Support for HTTP/1.1 host names exists if
# the URLs start with <tt>http://</tt> or <tt>https://</tt>.
#
# URLMap modifies the SCRIPT_NAME and PATH_INFO such that the part
# relevant for dispatch is in the SCRIPT_NAME, and the rest in the
# PATH_INFO. This should be taken care of when you need to
# reconstruct the URL in order to create links.
#
# URLMap dispatches in such a way that the longest paths are tried
# first, since they are most specific.
class URLMap
def initialize(map = {})
remap(map)
end
def remap(map)
@known_hosts = Set[]
@mapping = map.map { |location, app|
if location =~ %r{\Ahttps?://(.*?)(/.*)}
host, location = $1, $2
@known_hosts << host
else
host = nil
end
unless location[0] == ?/
raise ArgumentError, "paths need to start with /"
end
location = location.chomp('/')
match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", Regexp::NOENCODING)
[host, location, match, app]
}.sort_by do |(host, location, _, _)|
[host ? -host.size : Float::INFINITY, -location.size]
end
end
def call(env)
path = env[PATH_INFO]
script_name = env[SCRIPT_NAME]
http_host = env[HTTP_HOST]
server_name = env[SERVER_NAME]
server_port = env[SERVER_PORT]
is_same_server = casecmp?(http_host, server_name) ||
casecmp?(http_host, "#{server_name}:#{server_port}")
is_host_known = @known_hosts.include? http_host
@mapping.each do |host, location, match, app|
unless casecmp?(http_host, host) \
|| casecmp?(server_name, host) \
|| (!host && is_same_server) \
|| (!host && !is_host_known) # If we don't have a matching host, default to the first without a specified host
next
end
next unless m = match.match(path.to_s)
rest = m[1]
next unless !rest || rest.empty? || rest[0] == ?/
env[SCRIPT_NAME] = (script_name + location)
env[PATH_INFO] = rest
return app.call(env)
end
[404, { CONTENT_TYPE => "text/plain", "x-cascade" => "pass" }, ["Not Found: #{path}"]]
ensure
env[PATH_INFO] = path
env[SCRIPT_NAME] = script_name
end
private
def casecmp?(v1, v2)
# if both nil, or they're the same string
return true if v1 == v2
# if either are nil... (but they're not the same)
return false if v1.nil?
return false if v2.nil?
# otherwise check they're not case-insensitive the same
v1.casecmp(v2).zero?
end
end
end

View File

@ -1,654 +0,0 @@
# -*- encoding: binary -*-
# frozen_string_literal: true
require 'uri'
require 'fileutils'
require 'set'
require 'tempfile'
require 'time'
require_relative 'query_parser'
require_relative 'mime'
require_relative 'headers'
require_relative 'constants'
module Rack
# Rack::Utils contains a grab-bag of useful methods for writing web
# applications adopted from all kinds of Ruby libraries.
module Utils
ParameterTypeError = QueryParser::ParameterTypeError
InvalidParameterError = QueryParser::InvalidParameterError
ParamsTooDeepError = QueryParser::ParamsTooDeepError
DEFAULT_SEP = QueryParser::DEFAULT_SEP
COMMON_SEP = QueryParser::COMMON_SEP
KeySpaceConstrainedParams = QueryParser::Params
class << self
attr_accessor :default_query_parser
end
# The default amount of nesting to allowed by hash parameters.
# This helps prevent a rogue client from triggering a possible stack overflow
# when parsing parameters.
self.default_query_parser = QueryParser.make_default(32)
module_function
# URI escapes. (CGI style space to +)
def escape(s)
URI.encode_www_form_component(s)
end
# Like URI escaping, but with %20 instead of +. Strictly speaking this is
# true URI escaping.
def escape_path(s)
::URI::DEFAULT_PARSER.escape s
end
# Unescapes the **path** component of a URI. See Rack::Utils.unescape for
# unescaping query parameters or form components.
def unescape_path(s)
::URI::DEFAULT_PARSER.unescape s
end
# Unescapes a URI escaped string with +encoding+. +encoding+ will be the
# target encoding of the string returned, and it defaults to UTF-8
def unescape(s, encoding = Encoding::UTF_8)
URI.decode_www_form_component(s, encoding)
end
class << self
attr_accessor :multipart_total_part_limit
attr_accessor :multipart_file_limit
# multipart_part_limit is the original name of multipart_file_limit, but
# the limit only counts parts with filenames.
alias multipart_part_limit multipart_file_limit
alias multipart_part_limit= multipart_file_limit=
end
# The maximum number of file parts a request can contain. Accepting too
# many parts can lead to the server running out of file handles.
# Set to `0` for no limit.
self.multipart_file_limit = (ENV['RACK_MULTIPART_PART_LIMIT'] || ENV['RACK_MULTIPART_FILE_LIMIT'] || 128).to_i
# The maximum total number of parts a request can contain. Accepting too
# many can lead to excessive memory use and parsing time.
self.multipart_total_part_limit = (ENV['RACK_MULTIPART_TOTAL_PART_LIMIT'] || 4096).to_i
def self.param_depth_limit
default_query_parser.param_depth_limit
end
def self.param_depth_limit=(v)
self.default_query_parser = self.default_query_parser.new_depth_limit(v)
end
def self.key_space_limit
warn("`Rack::Utils.key_space_limit` is deprecated as this value no longer has an effect. It will be removed in Rack 3.1", uplevel: 1)
65536
end
def self.key_space_limit=(v)
warn("`Rack::Utils.key_space_limit=` is deprecated and no longer has an effect. It will be removed in Rack 3.1", uplevel: 1)
end
if defined?(Process::CLOCK_MONOTONIC)
def clock_time
Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
else
# :nocov:
def clock_time
Time.now.to_f
end
# :nocov:
end
def parse_query(qs, d = nil, &unescaper)
Rack::Utils.default_query_parser.parse_query(qs, d, &unescaper)
end
def parse_nested_query(qs, d = nil)
Rack::Utils.default_query_parser.parse_nested_query(qs, d)
end
def build_query(params)
params.map { |k, v|
if v.class == Array
build_query(v.map { |x| [k, x] })
else
v.nil? ? escape(k) : "#{escape(k)}=#{escape(v)}"
end
}.join("&")
end
def build_nested_query(value, prefix = nil)
case value
when Array
value.map { |v|
build_nested_query(v, "#{prefix}[]")
}.join("&")
when Hash
value.map { |k, v|
build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
}.delete_if(&:empty?).join('&')
when nil
escape(prefix)
else
raise ArgumentError, "value must be a Hash" if prefix.nil?
"#{escape(prefix)}=#{escape(value)}"
end
end
def q_values(q_value_header)
q_value_header.to_s.split(/\s*,\s*/).map do |part|
value, parameters = part.split(/\s*;\s*/, 2)
quality = 1.0
if parameters && (md = /\Aq=([\d.]+)/.match(parameters))
quality = md[1].to_f
end
[value, quality]
end
end
def forwarded_values(forwarded_header)
return nil unless forwarded_header
forwarded_header = forwarded_header.to_s.gsub("\n", ";")
forwarded_header.split(/\s*;\s*/).each_with_object({}) do |field, values|
field.split(/\s*,\s*/).each do |pair|
return nil unless pair =~ /\A\s*(by|for|host|proto)\s*=\s*"?([^"]+)"?\s*\Z/i
(values[$1.downcase.to_sym] ||= []) << $2
end
end
end
module_function :forwarded_values
# Return best accept value to use, based on the algorithm
# in RFC 2616 Section 14. If there are multiple best
# matches (same specificity and quality), the value returned
# is arbitrary.
def best_q_match(q_value_header, available_mimes)
values = q_values(q_value_header)
matches = values.map do |req_mime, quality|
match = available_mimes.find { |am| Rack::Mime.match?(am, req_mime) }
next unless match
[match, quality]
end.compact.sort_by do |match, quality|
(match.split('/', 2).count('*') * -10) + quality
end.last
matches&.first
end
ESCAPE_HTML = {
"&" => "&amp;",
"<" => "&lt;",
">" => "&gt;",
"'" => "&#x27;",
'"' => "&quot;",
"/" => "&#x2F;"
}
ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
# Escape ampersands, brackets and quotes to their HTML/XML entities.
def escape_html(string)
string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
end
def select_best_encoding(available_encodings, accept_encoding)
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
expanded_accept_encoding = []
accept_encoding.each do |m, q|
preference = available_encodings.index(m) || available_encodings.size
if m == "*"
(available_encodings - accept_encoding.map(&:first)).each do |m2|
expanded_accept_encoding << [m2, q, preference]
end
else
expanded_accept_encoding << [m, q, preference]
end
end
encoding_candidates = expanded_accept_encoding
.sort_by { |_, q, p| [-q, p] }
.map!(&:first)
unless encoding_candidates.include?("identity")
encoding_candidates.push("identity")
end
expanded_accept_encoding.each do |m, q|
encoding_candidates.delete(m) if q == 0.0
end
(encoding_candidates & available_encodings)[0]
end
# :call-seq:
# parse_cookies_header(value) -> hash
#
# Parse cookies from the provided header +value+ according to RFC6265. The
# syntax for cookie headers only supports semicolons. Returns a map of
# cookie +key+ to cookie +value+.
#
# parse_cookies_header('myname=myvalue; max-age=0')
# # => {"myname"=>"myvalue", "max-age"=>"0"}
#
def parse_cookies_header(value)
return {} unless value
value.split(/; */n).each_with_object({}) do |cookie, cookies|
next if cookie.empty?
key, value = cookie.split('=', 2)
cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
end
end
def add_cookie_to_header(header, key, value)
warn("add_cookie_to_header is deprecated and will be removed in Rack 3.1", uplevel: 1)
case header
when nil, ''
return set_cookie_header(key, value)
when String
[header, set_cookie_header(key, value)]
when Array
header + [set_cookie_header(key, value)]
else
raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
end
end
# :call-seq:
# parse_cookies(env) -> hash
#
# Parse cookies from the provided request environment using
# parse_cookies_header. Returns a map of cookie +key+ to cookie +value+.
#
# parse_cookies({'HTTP_COOKIE' => 'myname=myvalue'})
# # => {'myname' => 'myvalue'}
#
def parse_cookies(env)
parse_cookies_header env[HTTP_COOKIE]
end
# :call-seq:
# set_cookie_header(key, value) -> encoded string
#
# Generate an encoded string using the provided +key+ and +value+ suitable
# for the +set-cookie+ header according to RFC6265. The +value+ may be an
# instance of either +String+ or +Hash+.
#
# If the cookie +value+ is an instance of +Hash+, it considers the following
# cookie attribute keys: +domain+, +max_age+, +expires+ (must be instance
# of +Time+), +secure+, +http_only+, +same_site+ and +value+. For more
# details about the interpretation of these fields, consult
# [RFC6265 Section 5.2](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2).
#
# An extra cookie attribute +escape_key+ can be provided to control whether
# or not the cookie key is URL encoded. If explicitly set to +false+, the
# cookie key name will not be url encoded (escaped). The default is +true+.
#
# set_cookie_header("myname", "myvalue")
# # => "myname=myvalue"
#
# set_cookie_header("myname", {value: "myvalue", max_age: 10})
# # => "myname=myvalue; max-age=10"
#
def set_cookie_header(key, value)
case value
when Hash
key = escape(key) unless value[:escape_key] == false
domain = "; domain=#{value[:domain]}" if value[:domain]
path = "; path=#{value[:path]}" if value[:path]
max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
secure = "; secure" if value[:secure]
httponly = "; httponly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
same_site =
case value[:same_site]
when false, nil
nil
when :none, 'None', :None
'; SameSite=None'
when :lax, 'Lax', :Lax
'; SameSite=Lax'
when true, :strict, 'Strict', :Strict
'; SameSite=Strict'
else
raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
end
value = value[:value]
else
key = escape(key)
end
value = [value] unless Array === value
return "#{key}=#{value.map { |v| escape v }.join('&')}#{domain}" \
"#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
end
# :call-seq:
# set_cookie_header!(headers, key, value) -> header value
#
# Append a cookie in the specified headers with the given cookie +key+ and
# +value+ using set_cookie_header.
#
# If the headers already contains a +set-cookie+ key, it will be converted
# to an +Array+ if not already, and appended to.
def set_cookie_header!(headers, key, value)
if header = headers[SET_COOKIE]
if header.is_a?(Array)
header << set_cookie_header(key, value)
else
headers[SET_COOKIE] = [header, set_cookie_header(key, value)]
end
else
headers[SET_COOKIE] = set_cookie_header(key, value)
end
end
# :call-seq:
# delete_set_cookie_header(key, value = {}) -> encoded string
#
# Generate an encoded string based on the given +key+ and +value+ using
# set_cookie_header for the purpose of causing the specified cookie to be
# deleted. The +value+ may be an instance of +Hash+ and can include
# attributes as outlined by set_cookie_header. The encoded cookie will have
# a +max_age+ of 0 seconds, an +expires+ date in the past and an empty
# +value+. When used with the +set-cookie+ header, it will cause the client
# to *remove* any matching cookie.
#
# delete_set_cookie_header("myname")
# # => "myname=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
#
def delete_set_cookie_header(key, value = {})
set_cookie_header(key, value.merge(max_age: '0', expires: Time.at(0), value: ''))
end
def make_delete_cookie_header(header, key, value)
warn("make_delete_cookie_header is deprecated and will be removed in Rack 3.1, use delete_set_cookie_header! instead", uplevel: 1)
delete_set_cookie_header!(header, key, value)
end
def delete_cookie_header!(headers, key, value = {})
headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value)
return nil
end
def add_remove_cookie_to_header(header, key, value = {})
warn("add_remove_cookie_to_header is deprecated and will be removed in Rack 3.1, use delete_set_cookie_header! instead", uplevel: 1)
delete_set_cookie_header!(header, key, value)
end
# :call-seq:
# delete_set_cookie_header!(header, key, value = {}) -> header value
#
# Set an expired cookie in the specified headers with the given cookie
# +key+ and +value+ using delete_set_cookie_header. This causes
# the client to immediately delete the specified cookie.
#
# delete_set_cookie_header!(nil, "mycookie")
# # => "mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
#
# If the header is non-nil, it will be modified in place.
#
# header = []
# delete_set_cookie_header!(header, "mycookie")
# # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
# header
# # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
#
def delete_set_cookie_header!(header, key, value = {})
if header
header = Array(header)
header << delete_set_cookie_header(key, value)
else
header = delete_set_cookie_header(key, value)
end
return header
end
def rfc2822(time)
time.rfc2822
end
# Parses the "Range:" header, if present, into an array of Range objects.
# Returns nil if the header is missing or syntactically invalid.
# Returns an empty array if none of the ranges are satisfiable.
def byte_ranges(env, size)
get_byte_ranges env['HTTP_RANGE'], size
end
def get_byte_ranges(http_range, size)
# See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
return nil unless http_range && http_range =~ /bytes=([^;]+)/
ranges = []
$1.split(/,\s*/).each do |range_spec|
return nil unless range_spec.include?('-')
range = range_spec.split('-')
r0, r1 = range[0], range[1]
if r0.nil? || r0.empty?
return nil if r1.nil?
# suffix-byte-range-spec, represents trailing suffix of file
r0 = size - r1.to_i
r0 = 0 if r0 < 0
r1 = size - 1
else
r0 = r0.to_i
if r1.nil?
r1 = size - 1
else
r1 = r1.to_i
return nil if r1 < r0 # backwards range is syntactically invalid
r1 = size - 1 if r1 >= size
end
end
ranges << (r0..r1) if r0 <= r1
end
ranges
end
# :nocov:
if defined?(OpenSSL.fixed_length_secure_compare)
# Constant time string comparison.
#
# NOTE: the values compared should be of fixed length, such as strings
# that have already been processed by HMAC. This should not be used
# on variable length plaintext strings because it could leak length info
# via timing attacks.
def secure_compare(a, b)
return false unless a.bytesize == b.bytesize
OpenSSL.fixed_length_secure_compare(a, b)
end
# :nocov:
else
def secure_compare(a, b)
return false unless a.bytesize == b.bytesize
l = a.unpack("C*")
r, i = 0, -1
b.each_byte { |v| r |= v ^ l[i += 1] }
r == 0
end
end
# Context allows the use of a compatible middleware at different points
# in a request handling stack. A compatible middleware must define
# #context which should take the arguments env and app. The first of which
# would be the request environment. The second of which would be the rack
# application that the request would be forwarded to.
class Context
attr_reader :for, :app
def initialize(app_f, app_r)
raise 'running context does not respond to #context' unless app_f.respond_to? :context
@for, @app = app_f, app_r
end
def call(env)
@for.context(env, @app)
end
def recontext(app)
self.class.new(@for, app)
end
def context(env, app = @app)
recontext(app).call(env)
end
end
# A wrapper around Headers
# header when set.
#
# @api private
class HeaderHash < Hash # :nodoc:
def self.[](headers)
warn "Rack::Utils::HeaderHash is deprecated and will be removed in Rack 3.1, switch to Rack::Headers", uplevel: 1
if headers.is_a?(Headers) && !headers.frozen?
return headers
end
new_headers = Headers.new
headers.each{|k,v| new_headers[k] = v}
new_headers
end
def self.new(hash = {})
warn "Rack::Utils::HeaderHash is deprecated and will be removed in Rack 3.1, switch to Rack::Headers", uplevel: 1
headers = Headers.new
hash.each{|k,v| headers[k] = v}
headers
end
def self.allocate
raise TypeError, "cannot allocate HeaderHash"
end
end
# Every standard HTTP code mapped to the appropriate message.
# Generated with:
# curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \
# ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \
# puts "#{m[1]} => \x27#{m[2].strip}\x27,"'
HTTP_STATUS_CODES = {
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing',
103 => 'Early Hints',
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
207 => 'Multi-Status',
208 => 'Already Reported',
226 => 'IM Used',
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
306 => '(Unused)',
307 => 'Temporary Redirect',
308 => 'Permanent Redirect',
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Timeout',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Payload Too Large',
414 => 'URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Range Not Satisfiable',
417 => 'Expectation Failed',
421 => 'Misdirected Request',
422 => 'Unprocessable Entity',
423 => 'Locked',
424 => 'Failed Dependency',
425 => 'Too Early',
426 => 'Upgrade Required',
428 => 'Precondition Required',
429 => 'Too Many Requests',
431 => 'Request Header Fields Too Large',
451 => 'Unavailable for Legal Reasons',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported',
506 => 'Variant Also Negotiates',
507 => 'Insufficient Storage',
508 => 'Loop Detected',
509 => 'Bandwidth Limit Exceeded',
510 => 'Not Extended',
511 => 'Network Authentication Required'
}
# Responses with HTTP status codes that should not have an entity body
STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
[message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
}.flatten]
def status_code(status)
if status.is_a?(Symbol)
SYMBOL_TO_STATUS_CODE.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
else
status.to_i
end
end
PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
def clean_path_info(path_info)
parts = path_info.split PATH_SEPS
clean = []
parts.each do |part|
next if part.empty? || part == '.'
part == '..' ? clean.pop : clean << part
end
clean_path = clean.join(::File::SEPARATOR)
clean_path.prepend("/") if parts.empty? || parts.first.empty?
clean_path
end
NULL_BYTE = "\0"
def valid_path?(path)
path.valid_encoding? && !path.include?(NULL_BYTE)
end
end
end

View File

@ -1,34 +0,0 @@
# frozen_string_literal: true
# Copyright (C) 2007-2019 Leah Neukirchen <http://leahneukirchen.org/infopage.html>
#
# Rack is freely distributable under the terms of an MIT-style license.
# See MIT-LICENSE or https://opensource.org/licenses/MIT.
# The Rack main module, serving as a namespace for all core Rack
# modules and classes.
#
# All modules meant for use in your application are <tt>autoload</tt>ed here,
# so it should be enough just to <tt>require 'rack'</tt> in your code.
module Rack
# The Rack protocol version number implemented.
VERSION = [1, 3].freeze
deprecate_constant :VERSION
VERSION_STRING = "1.3".freeze
deprecate_constant :VERSION_STRING
# The Rack protocol version number implemented.
def self.version
warn "Rack.version is deprecated and will be removed in Rack 3.1!", uplevel: 1
VERSION
end
RELEASE = "3.0.8"
# Return the Rack release as a dotted string.
def self.release
RELEASE
end
end

View File

@ -1,24 +0,0 @@
# frozen_string_literal: true
require 'pathname'
require 'yaml'
require 'rubocop'
require_relative 'rubocop/cop/capybara/mixin/capybara_help'
require_relative 'rubocop/cop/capybara/mixin/css_selector'
require_relative 'rubocop/cop/capybara_cops'
project_root = File.join(__dir__, '..')
RuboCop::ConfigLoader.inject_defaults!(project_root)
obsoletion = File.join(project_root, 'config', 'obsoletion.yml')
RuboCop::ConfigObsoletion.files << obsoletion if File.exist?(obsoletion)
RuboCop::Cop::Style::TrailingCommaInArguments.singleton_class.prepend(
Module.new do
def autocorrect_incompatible_with
super.push(RuboCop::Cop::Capybara::CurrentPathExpectation)
end
end
)

View File

@ -1,56 +0,0 @@
# frozen_string_literal: true
require 'yaml'
module RuboCop
module Capybara
# Builds a YAML config file from two config hashes
class ConfigFormatter
EXTENSION_ROOT_DEPARTMENT = %r{^(Capybara/)}.freeze
SUBDEPARTMENTS = [].freeze
AMENDMENTS = [].freeze
COP_DOC_BASE_URL = 'https://www.rubydoc.info/gems/rubocop-capybara/RuboCop/Cop/'
def initialize(config, descriptions)
@config = config
@descriptions = descriptions
end
def dump
YAML.dump(unified_config)
.gsub(EXTENSION_ROOT_DEPARTMENT, "\n\\1")
.gsub(/^(\s+)- /, '\1 - ')
.gsub('"~"', '~')
# .gsub(*AMENDMENTS, "\n\\0")
end
private
def unified_config
cops.each_with_object(config.dup) do |cop, unified|
next if SUBDEPARTMENTS.include?(cop) || AMENDMENTS.include?(cop)
replace_nil(unified[cop])
unified[cop].merge!(descriptions.fetch(cop))
unified[cop]['Reference'] = reference(cop)
end
end
def cops
(descriptions.keys | config.keys).grep(EXTENSION_ROOT_DEPARTMENT)
end
def replace_nil(config)
config.each do |key, value|
config[key] = '~' if value.nil?
end
end
def reference(cop)
COP_DOC_BASE_URL + cop
end
attr_reader :config, :descriptions
end
end
end

View File

@ -1,70 +0,0 @@
# frozen_string_literal: true
module RuboCop
module Capybara
# Extracts cop descriptions from YARD docstrings
class DescriptionExtractor
def initialize(yardocs)
@code_objects = yardocs.map(&CodeObject.public_method(:new))
end
def to_h
code_objects
.select(&:cop?)
.map(&:configuration)
.reduce(:merge)
end
private
attr_reader :code_objects
# Decorator of a YARD code object for working with documented cops
class CodeObject
RUBOCOP_COP_CLASS_NAME = 'RuboCop::Cop::Base'
def initialize(yardoc)
@yardoc = yardoc
end
# Test if the YARD code object documents a concrete cop class
#
# @return [Boolean]
def cop?
cop_subclass? && !abstract?
end
# Configuration for the documented cop that would live in default.yml
#
# @return [Hash]
def configuration
{ cop_name => { 'Description' => description } }
end
private
def cop_name
Object.const_get(documented_constant).cop_name
end
def description
yardoc.docstring.split("\n\n").first.to_s
end
def documented_constant
yardoc.to_s
end
def cop_subclass?
yardoc.superclass.path == RUBOCOP_COP_CLASS_NAME
end
def abstract?
yardoc.tags.any? { |tag| tag.tag_name.eql?('abstract') }
end
attr_reader :yardoc
end
end
end
end

View File

@ -1,10 +0,0 @@
# frozen_string_literal: true
module RuboCop
module Capybara
# Version information for the Capybara RuboCop plugin.
module Version
STRING = '2.18.0'
end
end
end

View File

@ -1,148 +0,0 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Capybara
# Checks that no expectations are set on Capybara's `current_path`.
#
# The
# https://www.rubydoc.info/github/teamcapybara/capybara/master/Capybara/RSpecMatchers#have_current_path-instance_method[`have_current_path` matcher]
# should be used on `page` to set expectations on Capybara's
# current path, since it uses
# https://github.com/teamcapybara/capybara/blob/master/README.md#asynchronous-javascript-ajax-and-friends[Capybara's waiting functionality]
# which ensures that preceding actions (like `click_link`) have
# completed.
#
# This cop does not support autocorrection in some cases.
#
# @example
# # bad
# expect(current_path).to eq('/callback')
#
# # good
# expect(page).to have_current_path('/callback')
#
# # bad (does not support autocorrection)
# expect(page.current_path).to match(variable)
#
# # good
# expect(page).to have_current_path('/callback')
#
class CurrentPathExpectation < ::RuboCop::Cop::Base
extend AutoCorrector
include RangeHelp
MSG = 'Do not set an RSpec expectation on `current_path` in ' \
'Capybara feature specs - instead, use the ' \
'`have_current_path` matcher on `page`'
RESTRICT_ON_SEND = %i[expect].freeze
# @!method expectation_set_on_current_path(node)
def_node_matcher :expectation_set_on_current_path, <<-PATTERN
(send nil? :expect (send {(send nil? :page) nil?} :current_path))
PATTERN
# Supported matchers: eq(...) / match(/regexp/) / match('regexp')
# @!method as_is_matcher(node)
def_node_matcher :as_is_matcher, <<-PATTERN
(send
#expectation_set_on_current_path ${:to :to_not :not_to}
${(send nil? :eq ...) (send nil? :match (regexp ...))})
PATTERN
# @!method regexp_node_matcher(node)
def_node_matcher :regexp_node_matcher, <<-PATTERN
(send
#expectation_set_on_current_path ${:to :to_not :not_to}
$(send nil? :match ${str dstr xstr}))
PATTERN
def self.autocorrect_incompatible_with
[Style::TrailingCommaInArguments]
end
def on_send(node)
expectation_set_on_current_path(node) do
add_offense(node.loc.selector) do |corrector|
next unless node.chained?
autocorrect(corrector, node)
end
end
end
private
def autocorrect(corrector, node)
as_is_matcher(node.parent) do |to_sym, matcher_node|
rewrite_expectation(corrector, node, to_sym, matcher_node)
end
regexp_node_matcher(node.parent) do |to_sym, matcher_node, regexp|
rewrite_expectation(corrector, node, to_sym, matcher_node)
convert_regexp_node_to_literal(corrector, matcher_node, regexp)
end
end
def rewrite_expectation(corrector, node, to_symbol, matcher_node)
corrector.replace(node.first_argument, 'page')
corrector.replace(node.parent.loc.selector, 'to')
matcher_method = if to_symbol == :to
'have_current_path'
else
'have_no_current_path'
end
corrector.replace(matcher_node.loc.selector, matcher_method)
add_argument_parentheses(corrector, matcher_node.first_argument)
add_ignore_query_options(corrector, node)
end
def convert_regexp_node_to_literal(corrector, matcher_node, regexp_node)
str_node = matcher_node.first_argument
regexp_expr = regexp_node_to_regexp_expr(regexp_node)
corrector.replace(str_node, regexp_expr)
end
def regexp_node_to_regexp_expr(regexp_node)
if regexp_node.xstr_type?
"/\#{`#{regexp_node.value.value}`}/"
else
Regexp.new(regexp_node.value).inspect
end
end
def add_argument_parentheses(corrector, arg_node)
return unless method_call_with_no_parentheses?(arg_node)
first_argument_range = range_with_surrounding_space(
arg_node.first_argument.source_range, side: :left
)
corrector.insert_before(first_argument_range, '(')
corrector.insert_after(arg_node.last_argument, ')')
end
def method_call_with_no_parentheses?(arg_node)
arg_node.send_type? && arg_node.arguments? && !arg_node.parenthesized?
end
# `have_current_path` with no options will include the querystring
# while `page.current_path` does not.
# This ensures the option `ignore_query: true` is added
# except when the expectation is a regexp or string
def add_ignore_query_options(corrector, node)
expectation_node = node.parent.last_argument
expectation_last_child = expectation_node.children.last
return if %i[
regexp str dstr xstr
].include?(expectation_last_child.type)
corrector.insert_after(
expectation_last_child,
', ignore_query: true'
)
end
end
end
end
end

View File

@ -1,58 +0,0 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Capybara
# Checks for usage of deprecated style methods.
#
# @example when using `assert_style`
# # bad
# page.find(:css, '#first').assert_style(display: 'block')
#
# # good
# page.find(:css, '#first').assert_matches_style(display: 'block')
#
# @example when using `has_style?`
# # bad
# expect(page.find(:css, 'first')
# .has_style?(display: 'block')).to be true
#
# # good
# expect(page.find(:css, 'first')
# .matches_style?(display: 'block')).to be true
#
# @example when using `have_style`
# # bad
# expect(page).to have_style(display: 'block')
#
# # good
# expect(page).to match_style(display: 'block')
#
class MatchStyle < ::RuboCop::Cop::Base
extend AutoCorrector
MSG = 'Use `%<good>s` instead of `%<bad>s`.'
RESTRICT_ON_SEND = %i[assert_style has_style? have_style].freeze
PREFERRED_METHOD = {
'assert_style' => 'assert_matches_style',
'has_style?' => 'matches_style?',
'have_style' => 'match_style'
}.freeze
def on_send(node)
method_node = node.loc.selector
add_offense(method_node) do |corrector|
corrector.replace(method_node,
PREFERRED_METHOD[method_node.source])
end
end
private
def message(node)
format(MSG, good: PREFERRED_METHOD[node.source], bad: node.source)
end
end
end
end
end

View File

@ -1,131 +0,0 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Capybara
# Help methods for capybara.
# @api private
module CapybaraHelp
COMMON_OPTIONS = %w[
id class style
].freeze
SPECIFIC_OPTIONS = {
'button' => (
COMMON_OPTIONS + %w[disabled name value title type]
).freeze,
'link' => (
COMMON_OPTIONS + %w[href alt title download]
).freeze,
'table' => (
COMMON_OPTIONS + %w[cols rows]
).freeze,
'select' => (
COMMON_OPTIONS + %w[
disabled name placeholder
selected multiple
]
).freeze,
'field' => (
COMMON_OPTIONS + %w[
checked disabled name placeholder
readonly type multiple
]
).freeze
}.freeze
SPECIFIC_PSEUDO_CLASSES = %w[
not() disabled enabled checked unchecked
].freeze
module_function
# @param node [RuboCop::AST::SendNode]
# @param locator [String]
# @param element [String]
# @return [Boolean]
def replaceable_option?(node, locator, element)
attrs = CssSelector.attributes(locator).keys
return false unless replaceable_element?(node, element, attrs)
attrs.all? do |attr|
SPECIFIC_OPTIONS.fetch(element, []).include?(attr)
end
end
# @param selector [String]
# @return [Boolean]
# @example
# common_attributes?('a[focused]') # => true
# common_attributes?('button[focused][visible]') # => true
# common_attributes?('table[id=some-id]') # => true
# common_attributes?('h1[invalid]') # => false
def common_attributes?(selector)
CssSelector.attributes(selector).keys.difference(COMMON_OPTIONS).none?
end
# @param attrs [Array<String>]
# @return [Boolean]
# @example
# replaceable_attributes?('table[id=some-id]') # => true
# replaceable_attributes?('a[focused]') # => false
def replaceable_attributes?(attrs)
attrs.values.none?(&:nil?)
end
# @param locator [String]
# @return [Boolean]
def replaceable_pseudo_classes?(locator)
CssSelector.pseudo_classes(locator).all? do |pseudo_class|
replaceable_pseudo_class?(pseudo_class, locator)
end
end
# @param pseudo_class [String]
# @param locator [String]
# @return [Boolean]
def replaceable_pseudo_class?(pseudo_class, locator)
return false unless SPECIFIC_PSEUDO_CLASSES.include?(pseudo_class)
case pseudo_class
when 'not()' then replaceable_pseudo_class_not?(locator)
else true
end
end
# @param locator [String]
# @return [Boolean]
def replaceable_pseudo_class_not?(locator)
locator.scan(/not\(.*?\)/).all? do |negation|
CssSelector.attributes(negation).values.all? do |v|
v.is_a?(TrueClass) || v.is_a?(FalseClass)
end
end
end
# @param node [RuboCop::AST::SendNode]
# @param element [String]
# @param attrs [Array<String>]
# @return [Boolean]
def replaceable_element?(node, element, attrs)
case element
when 'link' then replaceable_to_link?(node, attrs)
else true
end
end
# @param node [RuboCop::AST::SendNode]
# @param attrs [Array<String>]
# @return [Boolean]
def replaceable_to_link?(node, attrs)
include_option?(node, :href) || attrs.include?('href')
end
# @param node [RuboCop::AST::SendNode]
# @param option [Symbol]
# @return [Boolean]
def include_option?(node, option)
node.each_descendant(:sym).find { |opt| opt.value == option }
end
end
end
end
end

Some files were not shown because too many files have changed in this diff Show More