Merge pull request #15954 from Bo98/vendor-cleanup
vendor/bundle/ruby: cleanup unneeded files
This commit is contained in:
commit
e5018531ae
54
.github/workflows/vendor-gems.yml
vendored
54
.github/workflows/vendor-gems.yml
vendored
@ -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
221
.gitignore
vendored
@ -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-*/
|
||||
|
||||
@ -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: "#",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
20
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/activesupport-6.1.7.6/MIT-LICENSE
vendored
Normal file
20
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/activesupport-6.1.7.6/MIT-LICENSE
vendored
Normal 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.
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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: '£', separator: ',', delimiter: '')
|
||||
# # => "£1234567890,50"
|
||||
# 1234567890.50.to_s(:currency, unit: '£', separator: ',', delimiter: '', format: '%n %u')
|
||||
# # => "1234567890,50 £"
|
||||
#
|
||||
# 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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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__)
|
||||
@ -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
|
||||
@ -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"
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
202
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/addressable-2.8.5/LICENSE.txt
vendored
Normal file
202
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/addressable-2.8.5/LICENSE.txt
vendored
Normal 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.
|
||||
25
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/bindata-2.4.15/LICENSE
vendored
Normal file
25
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/bindata-2.4.15/LICENSE
vendored
Normal 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.
|
||||
21
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/concurrent-ruby-1.2.2/LICENSE.txt
vendored
Normal file
21
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/concurrent-ruby-1.2.2/LICENSE.txt
vendored
Normal 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.
|
||||
22
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/did_you_mean-1.6.3/LICENSE.txt
vendored
Normal file
22
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/did_you_mean-1.6.3/LICENSE.txt
vendored
Normal 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.
|
||||
20
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/i18n-1.14.1/MIT-LICENSE
vendored
Normal file
20
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/i18n-1.14.1/MIT-LICENSE
vendored
Normal 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.
|
||||
20
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/plist-3.7.0/LICENSE.txt
vendored
Normal file
20
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/plist-3.7.0/LICENSE.txt
vendored
Normal 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.
|
||||
22
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/public_suffix-5.0.3/LICENSE.txt
vendored
Normal file
22
Library/Homebrew/vendor/bundle/ruby/2.6.0/gems/public_suffix-5.0.3/LICENSE.txt
vendored
Normal 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.
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -1 +0,0 @@
|
||||
require_relative '../digest'
|
||||
@ -1 +0,0 @@
|
||||
require_relative '../digest'
|
||||
@ -1 +0,0 @@
|
||||
require_relative '../digest'
|
||||
@ -1 +0,0 @@
|
||||
require_relative '../digest'
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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.
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -1,3 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'mock_request'
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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:
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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 = {
|
||||
"&" => "&",
|
||||
"<" => "<",
|
||||
">" => ">",
|
||||
"'" => "'",
|
||||
'"' => """,
|
||||
"/" => "/"
|
||||
}
|
||||
|
||||
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
|
||||
@ -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
|
||||
@ -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
|
||||
)
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user