pax_global_header00006660000000000000000000000064143471756140014526gustar00rootroot0000000000000052 comment=186106d153d08c77e4add922e93b6c4a9d9315f6 sinatra-3.0.5/000077500000000000000000000000001434717561400131745ustar00rootroot00000000000000sinatra-3.0.5/.github/000077500000000000000000000000001434717561400145345ustar00rootroot00000000000000sinatra-3.0.5/.github/workflows/000077500000000000000000000000001434717561400165715ustar00rootroot00000000000000sinatra-3.0.5/.github/workflows/test.yml000066400000000000000000000056051434717561400203010ustar00rootroot00000000000000name: Testing on: push: branches: - '**' pull_request: permissions: contents: read # to fetch code (actions/checkout) jobs: test: permissions: contents: read # to fetch code (actions/checkout) actions: read # to list jobs for workflow run (8398a7/action-slack) name: ${{ matrix.ruby }} (Rack ${{ matrix.rack }}, Puma ${{ matrix.puma }}) runs-on: ubuntu-latest timeout-minutes: 15 strategy: fail-fast: false matrix: puma: - '~> 5' # Due to https://github.com/puma/puma/issues/3000 rack: - stable # Due to https://github.com/actions/runner/issues/849, we have to use quotes for '3.0' ruby: [2.6, 2.7, '3.0', 3.1, 'jruby-9.3', truffleruby, truffleruby-head] include: - { ruby: 3.1, rack: stable, puma: latest, allow-failure: true } - { ruby: 3.1, rack: latest, allow-failure: true } - { ruby: jruby-head, rack: stable, allow-failure: true } env: rack: ${{ matrix.rack }} puma: ${{ matrix.puma }} steps: - name: Install dependencies run: | sudo apt-get install --yes \ pandoc \ nodejs \ pkg-config \ libxml2-dev \ libxslt-dev \ libyaml-dev - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 continue-on-error: ${{ matrix.allow-failure || false }} id: setup-ruby with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - name: Run sinatra tests continue-on-error: ${{ matrix.allow-failure || false }} id: tests run: bundle exec rake - name: Run sinatra-contrib tests continue-on-error: ${{ matrix.allow-failure || false }} id: contrib-tests working-directory: sinatra-contrib run: | bundle install --jobs=3 --retry=3 bundle exec rake - name: Run rack-protection tests continue-on-error: ${{ matrix.allow-failure || false }} id: protection-tests working-directory: rack-protection run: | bundle install --jobs=3 --retry=3 bundle exec rake - uses: 8398a7/action-slack@v3 with: status: ${{ job.status }} fields: repo,message,commit,author,ref,job,took # selectable (default: repo,message) env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }} # required MATRIX_CONTEXT: ${{ toJson(matrix) }} # required if: failure() && env.SLACK_WEBHOOK_URL # because continue-on-error marks the steps as pass even if they fail - name: "setup-ruby (bundle install) outcome: ${{ steps.setup-ruby.outcome }}" run: "" - name: "sinatra tests outcome: ${{ steps.tests.outcome }}" run: "" - name: "sinatra-contrib tests outcome: ${{ steps.contrib-tests.outcome }}" run: "" - name: "rack-protection tests outcome: ${{ steps.protection-tests.outcome }}" run: "" sinatra-3.0.5/.gitignore000066400000000000000000000002521434717561400151630ustar00rootroot00000000000000# please add general patterns to your global ignore list # see https://github.com/github/gitignore#readme .DS_STORE *.swp *.rbc /pkg /Gemfile.lock /coverage .yardoc /doc sinatra-3.0.5/.rubocop.yml000066400000000000000000000051421434717561400154500ustar00rootroot00000000000000# The behavior of RuboCop can be controlled via the .rubocop.yml # configuration file. It makes it possible to enable/disable # certain cops (checks) and to alter their behavior if they accept # any parameters. The file can be placed either in your home # directory or in some project directory. # # RuboCop will start looking for the configuration file in the directory # where the inspected file is and continue its way up to the root directory. # # See https://github.com/rubocop-hq/rubocop/blob/master/manual/configuration.md AllCops: TargetRubyVersion: 2.6 SuggestExtensions: false NewCops: enable Exclude: - 'test/**/*' - 'rack-protection/**/*' - 'sinatra-contrib/**/*' - vendor/bundle/**/* Layout/ExtraSpacing: AllowForAlignment: true AllowBeforeTrailingComments: true # Temporary disable cops because warnings are fixed Style/SingleLineMethods: Enabled: false Style/MutableConstant: Enabled: false Lint/AmbiguousBlockAssociation: Enabled: false Style/CaseEquality: Enabled: false Style/PerlBackrefs: Enabled: false Style/Documentation: Enabled: false Lint/IneffectiveAccessModifier: Enabled: false Lint/RescueException: Enabled: false Style/SpecialGlobalVars: Enabled: false Bundler/DuplicatedGem: Enabled: false Layout/HeredocIndentation: Enabled: false Style/FormatStringToken: Enabled: false Lint/UselessAccessModifier: Enabled: false Style/ClassVars: Enabled: false Lint/UselessAssignment: Enabled: false Style/EmptyLiteral: Enabled: false Layout/LineLength: Enabled: false Metrics/MethodLength: Enabled: false Metrics/AbcSize: Enabled: false Metrics/CyclomaticComplexity: Enabled: false Metrics/PerceivedComplexity: Enabled: false Lint/SuppressedException: Enabled: false Metrics/ClassLength: Enabled: false Metrics/BlockLength: Enabled: false Metrics/ModuleLength: Enabled: false Lint/AmbiguousRegexpLiteral: Enabled: false Style/AccessModifierDeclarations: Enabled: false Style/ClassAndModuleChildren: Enabled: false Style/EvalWithLocation: Enabled: false Lint/MissingSuper: Enabled: false Style/MissingRespondToMissing: Enabled: false Style/MixinUsage: Enabled: false Style/MultilineTernaryOperator: Enabled: false Style/StructInheritance: Enabled: false Style/SymbolProc: Enabled: false Style/IfUnlessModifier: Enabled: false Style/OptionalBooleanParameter: Enabled: false Style/DocumentDynamicEvalDefinition: Enabled: false Lint/ToEnumArguments: Enabled: false Naming/MethodParameterName: Enabled: false Naming/AccessorMethodName: Enabled: false Style/SlicingWithRange: Enabled: false sinatra-3.0.5/.yardopts000066400000000000000000000001601434717561400150370ustar00rootroot00000000000000--readme README.md --title 'Sinatra API Documentation' --charset utf-8 --markup markdown 'lib/**/*.rb' - '*.md' sinatra-3.0.5/AUTHORS.md000066400000000000000000000070051434717561400146450ustar00rootroot00000000000000Sinatra was designed and developed by Blake Mizerany in California. ### Current Team * **Konstantin Haase** (maintainer) * **Zachary Scott** * **Kashyap Kondamudi** * **Ashley Williams** * **Trevor Bramble** * **Kunpei Sakai** ### Alumni * **Blake Mizerany** (creator) * **Ryan Tomayko** * **Simon Rozet** * **Katrina Owen** ### Thanks Sinatra would not have been possible without strong company backing. In the past, financial and emotional support have been provided mainly by [Heroku](http://heroku.com), [GitHub](https://github.com) and [Engine Yard](http://www.engineyard.com/), and is now taken care of by [Travis CI](http://travis-ci.com/). Special thanks to the following extraordinary individuals, without whom Sinatra would not be possible: * [Ryan Tomayko](http://tomayko.com/) (rtomayko) for constantly fixing whitespace errors __60d5006__ * [Ezra Zygmuntowicz](http://brainspl.at/) (ezmobius) for initial help and letting Blake steal some of merbs internal code. * [Chris Schneider](http://gittr.com) (cschneid) for The Book, the blog, [irclogger.com](http://irclogger.com/sinatra/), and a bunch of useful patches. * [Markus Prinz](http://nuclearsquid.com/) (cypher) for patches over the years, caring about the README, and hanging in there when times were rough. * [Erik Kastner](http://metaatem.net/) (kastner) for fixing `MIME_TYPES` under Rack 0.5. * [Ben Bleything](http://blog.bleything.net/) (bleything) for caring about HTTP status codes and doc fixes. * [Igal Koshevoy](http://twitter.com/igalko) (igal) for root path detection under Thin/Passenger. * [Jon Crosby](http://joncrosby.me/) (jcrosby) for coffee breaks, doc fixes, and just because, man. * [Karel Minarik](https://github.com/karmi) (karmi) for screaming until the website came back up. * [Jeremy Evans](http://code.jeremyevans.net/) (jeremyevans) for unbreaking optional path params (twice!) * [The GitHub guys](https://github.com/) for stealing Blake's table. * [Nickolas Means](http://nmeans.org/) (nmeans) for Sass template support. * [Victor Hugo Borja](https://github.com/vic) (vic) for splat'n routes specs and doco. * [Avdi Grimm](http://avdi.org/) (avdi) for basic RSpec support. * [Jack Danger Canty](http://jåck.com/) for a more accurate root directory and for making me watch [this](http://www.youtube.com/watch?v=ueaHLHgskkw) just now. * Mathew Walker for making escaped paths work with static files. * Millions of Us for having the problem that led to Sinatra's conception. * [Songbird](http://getsongbird.com/) for the problems that helped Sinatra's future become realized. * [Rick Olson](http://techno-weenie.net/) (technoweenie) for the killer plug at RailsConf '08. * Steven Garcia for the amazing custom artwork you see on 404's and 500's * [Pat Nakajima](http://patnakajima.com/) (nakajima) for fixing non-nested params in nested params Hash's. * Gabriel Andretta for having people wonder whether our documentation is actually in English or in Spanish. * Vasily Polovnyov, Nickolay Schwarz, Luciano Sousa, Wu Jiang, Mickael Riga, Bernhard Essl, Janos Hardi, Kouhei Yanagita and "burningTyger" for willingly translating whatever ends up in the README. * [Wordy](https://wordy.com/) for proofreading our README. **73e137d** * cactus for digging through code and specs, multiple times. * Nicolás Sanguinetti (foca) for strong demand of karma and shaping helpers/register. And last but not least: * [Frank Sinatra](http://www.sinatra.com/) (chairman of the board) for having so much class he deserves a web-framework named after him. sinatra-3.0.5/CHANGELOG.md000066400000000000000000002253521434717561400150160ustar00rootroot00000000000000## Unreleased * _Your new feature here._ ## 3.0.5 / 2022-12-16 * Fix: Add Zeitwerk compatibility. [#1831](https://github.com/sinatra/sinatra/pull/1831) by Dawid Janczak * Fix: Allow CALLERS_TO_IGNORE to be overridden ## 3.0.4 / 2022-11-25 * Fix: Escape filename in the Content-Disposition header. [#1841](https://github.com/sinatra/sinatra/pull/1841) by Kunpei Sakai ## 3.0.3 / 2022-11-11 * Fix: fixed ReDoS for Rack::Protection::IPSpoofing. [#1823](https://github.com/sinatra/sinatra/pull/1823) by @ooooooo-q ## 3.0.2 / 2022-10-01 * New: Add Haml 6 support. [#1820](https://github.com/sinatra/sinatra/pull/1820) by Jordan Owens ## 3.0.1 / 2022-09-26 * Fix: Revert removal of rack-protection.rb. [#1814](https://github.com/sinatra/sinatra/pull/1814) by Olle Jonsson * Fix: Revert change to server start and stop messaging by using Kernel#warn. Renamed internal warn method warn_for_deprecation. [#1818](https://github.com/sinatra/sinatra/pull/1818) by Jordan Owens ## 3.0.0 / 2022-09-26 * New: Add Falcon support. [#1794](https://github.com/sinatra/sinatra/pull/1794) by Samuel Williams and @horaciob * New: Add AES GCM encryption support for session cookies. [#1324] (https://github.com/sinatra/sinatra/pull/1324) by Michael Coyne * Deprecated: Sinatra Reloader will be removed in the next major release. * Fix: Internal Sinatra errors now extend `Sinatra::Error`. This fixes [#1204](https://github.com/sinatra/sinatra/issues/1204) and [#1518](https://github.com/sinatra/sinatra/issues/1518). [bda8c29d](https://github.com/sinatra/sinatra/commit/bda8c29d70619d53f5b1c181140638d340695514) by Jordan Owens * Fix: Preserve query param value if named route param nil. [#1676](https://github.com/sinatra/sinatra/pull/1676) by Jordan Owens * Require Ruby 2.6 as minimum Ruby version. [#1699](https://github.com/sinatra/sinatra/pull/1699) by Eloy Pérez * Breaking change: Remove support for the Stylus template engine. [#1697](https://github.com/sinatra/sinatra/pull/1697) by Eloy Pérez * Breaking change: Remove support for the erubis template engine. [#1761](https://github.com/sinatra/sinatra/pull/1761) by Eloy Pérez * Breaking change: Remove support for the textile template engine. [#1766](https://github.com/sinatra/sinatra/pull/1766) by Eloy Pérez * Breaking change: Remove support for SASS as a template engine. [#1768](https://github.com/sinatra/sinatra/pull/1768) by Eloy Pérez * Breaking change: Remove support for Wlang as a template engine. [#1780](https://github.com/sinatra/sinatra/pull/1780) by Eloy Pérez * Breaking change: Remove support for CoffeeScript as a template engine. [#1790](https://github.com/sinatra/sinatra/pull/1790) by Eloy Pérez * Breaking change: Remove support for Mediawiki as a template engine. [#1791](https://github.com/sinatra/sinatra/pull/1791) by Eloy Pérez * Breaking change: Remove support for Creole as a template engine. [#1792](https://github.com/sinatra/sinatra/pull/1792) by Eloy Pérez * Breaking change: Remove support for Radius as a template engine. [#1793](https://github.com/sinatra/sinatra/pull/1793) by Eloy Pérez * Breaking change: Remove support for the defunct Less templating library. See [#1716](https://github.com/sinatra/sinatra/issues/1716), [#1715](https://github.com/sinatra/sinatra/issues/1715) for more discussion and background. [d1af2f1e](https://github.com/sinatra/sinatra/commit/d1af2f1e6c8710419dfe3102a660f7a32f0e67e3) by Olle Jonsson * Breaking change: Remove Reel integration. [54597502](https://github.com/sinatra/sinatra/commit/545975025927a27a1daca790598620038979f1c5) by Olle Jonsson * CI: Start testing on Ruby 3.1. [60e221940](https://github.com/sinatra/sinatra/commit/60e2219407e6ae067bf3e53eb060ee4860c60c8d) and [b0fa4bef](https://github.com/sinatra/sinatra/commit/b0fa4beffaa3b10bf02947d0a35e137403296c6b) by Johannes Würbach * Use `Kernel#caller_locations`. [#1491](https://github.com/sinatra/sinatra/pull/1491) by Julik Tarkhanov * Docs: Japanese documentation: Add notes about the `default_content_type` setting. [#1650](https://github.com/sinatra/sinatra/pull/1650) by Akifumi Tominaga * Docs: Polish documentation: Add section about Multithreaded modes and Routes. [#1708](https://github.com/sinatra/sinatra/pull/1708) by Patrick Gramatowski * Docs: Japanese documentation: Make Session section reflect changes done to README.md. [#1731](https://github.com/sinatra/sinatra/pull/1731) by @shu-i-chi ## 2.2.3 / 2022-11-25 * Fix: Escape filename in the Content-Disposition header. [#1841](https://github.com/sinatra/sinatra/pull/1841) by Kunpei Sakai * Fix: fixed ReDoS for Rack::Protection::IPSpoofing. [#1823](https://github.com/sinatra/sinatra/pull/1823) by @ooooooo-q ## 2.2.2 / 2022-07-23 * Update mustermann dependency to version 2. ## 2.2.1 / 2022-07-15 * Fix JRuby regression by using ruby2_keywords for delegation. #1750 by Patrik Ragnarsson * Add JRuby to CI. #1755 by Karol Bucek ## 2.2.0 / 2022-02-15 * Breaking change: Add `#select`, `#reject` and `#compact` methods to `Sinatra::IndifferentHash`. If hash keys need to be converted to symbols, call `#to_h` to get a `Hash` instance first. [#1711](https://github.com/sinatra/sinatra/pull/1711) by Olivier Bellone * Handle EOFError raised by Rack and return Bad Request 400 status. [#1743](https://github.com/sinatra/sinatra/pull/1743) by tamazon * Minor refactors in `base.rb`. [#1640](https://github.com/sinatra/sinatra/pull/1640) by ceclinux * Add escaping to the static 404 page. [#1645](https://github.com/sinatra/sinatra/pull/1645) by Chris Gavin * Remove `detect_rack_handler` method. [#1652](https://github.com/sinatra/sinatra/pull/1652) by ceclinux * Respect content type set in superclass before filter. Fixes [#1647](https://github.com/sinatra/sinatra/issues/1647) [#1649](https://github.com/sinatra/sinatra/pull/1649) by Jordan Owens * *Revert "Use prepend instead of include for helpers.* [#1662](https://github.com/sinatra/sinatra/pull/1662) by namusyaka * Fix usage of inherited `Sinatra::Base` classes keyword arguments. Fixes [#1669](https://github.com/sinatra/sinatra/issues/1669) [#1670](https://github.com/sinatra/sinatra/pull/1670) by Cadu Ribeiro * Reduce RDoc generation time by not including every README. Fixes [#1578](https://github.com/sinatra/sinatra/issues/1578) [#1671](https://github.com/sinatra/sinatra/pull/1671) by Eloy Pérez * Add support for per form csrf tokens. Fixes [#1616](https://github.com/sinatra/sinatra/issues/1616) [#1653](https://github.com/sinatra/sinatra/pull/1653) by Jordan Owens * Update MAINTENANCE.md with the `stable` branch status. [#1681](https://github.com/sinatra/sinatra/pull/1681) by Fredrik Rubensson * Validate expanded path matches `public_dir` when serving static files. [#1683](https://github.com/sinatra/sinatra/pull/1683) by cji-stripe * Fix Delegator to pass keyword arguments for Ruby 3.0. [#1684](https://github.com/sinatra/sinatra/pull/1684) by andrewtblake * Fix use with keyword arguments for Ruby 3.0. [#1701](https://github.com/sinatra/sinatra/pull/1701) by Robin Wallin * Fix memory leaks for proc template. Fixes [#1704](https://github.com/sinatra/sinatra/issues/1714) [#1719](https://github.com/sinatra/sinatra/pull/1719) by Slevin * Remove unnecessary `test_files` from the gemspec. [#1712](https://github.com/sinatra/sinatra/pull/1712) by Masataka Pocke Kuwabara * Docs: Spanish documentation: Update README.es.md with removal of Thin. [#1630](https://github.com/sinatra/sinatra/pull/1630) by Espartaco Palma * Docs: German documentation: Fixed typos in German README.md. [#1648](https://github.com/sinatra/sinatra/pull/1648) by Juri * Docs: Japanese documentation: Update README.ja.md with removal of Thin. [#1629](https://github.com/sinatra/sinatra/pull/1629) by Ryuichi KAWAMATA * Docs: English documentation: Various minor fixes to README.md. [#1663](https://github.com/sinatra/sinatra/pull/1663) by Yanis Zafirópulos * Docs: English documentation: Document when `dump_errors` is enabled. Fixes [#1664](https://github.com/sinatra/sinatra/issues/1664) [#1665](https://github.com/sinatra/sinatra/pull/1665) by Patrik Ragnarsson * Docs: Brazilian Portuguese documentation: Update README.pt-br.md with translation fixes. [#1668](https://github.com/sinatra/sinatra/pull/1668) by Vitor Oliveira ### CI * Use latest JRuby 9.2.16.0 on CI. [#1682](https://github.com/sinatra/sinatra/pull/1682) by Olle Jonsson * Switch CI from travis to GitHub Actions. [#1691](https://github.com/sinatra/sinatra/pull/1691) by namusyaka * Skip the Slack action if `secrets.SLACK_WEBHOOK` is not set. [#1705](https://github.com/sinatra/sinatra/pull/1705) by Robin Wallin * Small CI improvements. [#1703](https://github.com/sinatra/sinatra/pull/1703) by Robin Wallin * Drop auto-generated boilerplate comments from CI configuration file. [#1728](https://github.com/sinatra/sinatra/pull/1728) by Olle Jonsson ### sinatra-contrib * Do not raise when key is an enumerable. [#1619](https://github.com/sinatra/sinatra/pull/1619) by Ulysse Buonomo ### Rack protection * Fix broken `origin_whitelist` option. Fixes [#1641](https://github.com/sinatra/sinatra/issues/1641) [#1642](https://github.com/sinatra/sinatra/pull/1642) by Takeshi YASHIRO ## 2.1.0 / 2020-09-05 * Fix additional Ruby 2.7 keyword warnings [#1586](https://github.com/sinatra/sinatra/pull/1586) by Stefan Sundin * Drop Ruby 2.2 support [#1455](https://github.com/sinatra/sinatra/pull/1455) by Eloy Pérez * Add Rack::Protection::ReferrerPolicy [#1291](https://github.com/sinatra/sinatra/pull/1291) by Stefan Sundin * Add `default_content_type` setting. Fixes [#1238](https://github.com/sinatra/sinatra/pull/1238) [#1239](https://github.com/sinatra/sinatra/pull/1239) by Mike Pastore * Allow `set :` in sinatra-namespace [#1255](https://github.com/sinatra/sinatra/pull/1255) by Christian Höppner * Use prepend instead of include for helpers. Fixes [#1213](https://github.com/sinatra/sinatra/pull/1213) [#1214](https://github.com/sinatra/sinatra/pull/1214) by Mike Pastore * Fix issue with passed routes and provides Fixes [#1095](https://github.com/sinatra/sinatra/pull/1095) [#1606](https://github.com/sinatra/sinatra/pull/1606) by Mike Pastore, Jordan Owens * Add QuietLogger that excludes pathes from Rack::CommonLogger [1250](https://github.com/sinatra/sinatra/pull/1250) by Christoph Wagner * Sinatra::Contrib dependency updates. Fixes [#1207](https://github.com/sinatra/sinatra/pull/1207) [#1411](https://github.com/sinatra/sinatra/pull/1411) by Mike Pastore * Allow CSP to fallback to default-src. Fixes [#1484](https://github.com/sinatra/sinatra/pull/1484) [#1490](https://github.com/sinatra/sinatra/pull/1490) by Jordan Owens * Replace `origin_whitelist` with `permitted_origins`. Closes [#1620](https://github.com/sinatra/sinatra/issues/1620) [#1625](https://github.com/sinatra/sinatra/pull/1625) by rhymes * Use Rainbows instead of thin for async/stream features. Closes [#1624](https://github.com/sinatra/sinatra/issues/1624) [#1627](https://github.com/sinatra/sinatra/pull/1627) by Ryuichi KAWAMATA * Enable EscapedParams if passed via settings. Closes [#1615](https://github.com/sinatra/sinatra/issues/1615) [#1632](https://github.com/sinatra/sinatra/issues/1632) by Anders Bälter * Support for parameters in mime types. Fixes [#1141](https://github.com/sinatra/sinatra/issues/1141) by John Hope * Handle null byte when serving static files [#1574](https://github.com/sinatra/sinatra/issues/1574) by Kush Fanikiso * Improve development support and documentation and source code by Olle Jonsson, Pierre-Adrien Buisson, Shota Iguchi ## 2.0.8.1 / 2020-01-02 * Allow multiple hashes to be passed in `merge` and `merge!` for `Sinatra::IndifferentHash` [#1572](https://github.com/sinatra/sinatra/pull/1572) by Shota Iguchi ## 2.0.8 / 2020-01-01 * Lookup Tilt class for template engine without loading files [#1558](https://github.com/sinatra/sinatra/pull/1558). Fixes [#1172](https://github.com/sinatra/sinatra/issues/1172) by Jordan Owens * Add request info in NotFound exception [#1566](https://github.com/sinatra/sinatra/pull/1566) by Stefan Sundin * Add `.yaml` support in `Sinatra::Contrib::ConfigFile` [#1564](https://github.com/sinatra/sinatra/issues/1564). Fixes [#1563](https://github.com/sinatra/sinatra/issues/1563) by Emerson Manabu Araki * Remove only routing parameters from @params hash [#1569](https://github.com/sinatra/sinatra/pull/1569). Fixes [#1567](https://github.com/sinatra/sinatra/issues/1567) by Jordan Owens, Horacio * Support `capture` and `content_for` with Hamlit [#1580](https://github.com/sinatra/sinatra/pull/1580) by Takashi Kokubun * Eliminate warnings of keyword parameter for Ruby 2.7.0 [#1581](https://github.com/sinatra/sinatra/pull/1581) by Osamtimizer ## 2.0.7 / 2019-08-22 * Fix a regression [#1560](https://github.com/sinatra/sinatra/pull/1560) by Kunpei Sakai ## 2.0.6 / 2019-08-21 * Fix an issue setting environment from command line option [#1547](https://github.com/sinatra/sinatra/pull/1547), [#1554](https://github.com/sinatra/sinatra/pull/1554) by Jordan Owens, Kunpei Sakai * Support pandoc as a new markdown renderer [#1533](https://github.com/sinatra/sinatra/pull/1533) by Vasiliy * Remove outdated code for tilt 1.x [#1532](https://github.com/sinatra/sinatra/pull/1532) by Vasiliy * Remove an extra logic for `force_encoding` [#1527](https://github.com/sinatra/sinatra/pull/1527) by Jordan Owens * Avoid multiple errors even if `params` contains special values [#1526](https://github.com/sinatra/sinatra/pull/1527) by Kunpei Sakai * Support `bundler/inline` with `require 'sinatra'` integration [#1520](https://github.com/sinatra/sinatra/pull/1520) by Kunpei Sakai * Avoid `TypeError` when params contain a key without a value on Ruby < 2.4 [#1516](https://github.com/sinatra/sinatra/pull/1516) by Samuel Giddins * Improve development support and documentation and source code by Olle Jonsson, Basavanagowda Kanur, Yuki MINAMIYA ## 2.0.5 / 2018-12-22 * Avoid FrozenError when params contains frozen value [#1506](https://github.com/sinatra/sinatra/pull/1506) by Kunpei Sakai * Add support for Erubi [#1494](https://github.com/sinatra/sinatra/pull/1494) by @tkmru * `IndifferentHash` monkeypatch warning improvements [#1477](https://github.com/sinatra/sinatra/pull/1477) by Mike Pastore * Improve development support and documentation and source code by Anusree Prakash, Jordan Owens, @ceclinux and @krororo. ### sinatra-contrib * Add `flush` option to `content_for` [#1225](https://github.com/sinatra/sinatra/pull/1225) by Shota Iguchi * Drop activesupport dependency from sinatra-contrib [#1448](https://github.com/sinatra/sinatra/pull/1448) * Update `yield_content` to append default to ERB template buffer [#1500](https://github.com/sinatra/sinatra/pull/1500) by Jordan Owens ### rack-protection * Don't track the Accept-Language header by default [#1504](https://github.com/sinatra/sinatra/pull/1504) by Artem Chistyakov ## 2.0.4 / 2018-09-15 * Don't blow up when passing frozen string to `send_file` disposition [#1137](https://github.com/sinatra/sinatra/pull/1137) by Andrew Selder * Fix ubygems LoadError [#1436](https://github.com/sinatra/sinatra/pull/1436) by Pavel Rosický * Unescape regex captures [#1446](https://github.com/sinatra/sinatra/pull/1446) by Jordan Owens * Slight performance improvements for IndifferentHash [#1427](https://github.com/sinatra/sinatra/pull/1427) by Mike Pastore * Improve development support and documentation and source code by Will Yang, Jake Craige, Grey Baker and Guilherme Goettems Schneider ## 2.0.3 / 2018-06-09 * Fix the backports gem regression [#1442](https://github.com/sinatra/sinatra/issues/1442) by Marc-André Lafortune ## 2.0.2 / 2018-06-05 * Escape invalid query parameters [#1432](https://github.com/sinatra/sinatra/issues/1432) by Kunpei Sakai * The patch fixes [CVE-2018-11627](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-11627). * Fix undefined method error for `Sinatra::RequiredParams` with hash key [#1431](https://github.com/sinatra/sinatra/issues/1431) by Arpit Chauhan * Add xml content-types to valid html_types for Rack::Protection [#1413](https://github.com/sinatra/sinatra/issues/1413) by Reenan Arbitrario * Encode route parameters using :default_encoding setting [#1412](https://github.com/sinatra/sinatra/issues/1412) by Brian m. Carlson * Fix unpredictable behaviour from Sinatra::ConfigFile [#1244](https://github.com/sinatra/sinatra/issues/1244) by John Hope * Add Sinatra::IndifferentHash#slice [#1405](https://github.com/sinatra/sinatra/issues/1405) by Shota Iguchi * Remove status code 205 from drop body response [#1398](https://github.com/sinatra/sinatra/issues/1398) by Shota Iguchi * Ignore empty captures from params [#1390](https://github.com/sinatra/sinatra/issues/1390) by Shota Iguchi * Improve development support and documentation and source code by Zp Yuan, Andreas Finger, Olle Jonsson, Shota Iguchi, Nikita Bulai and Joshua O'Brien ## 2.0.1 / 2018-02-17 * Repair nested namespaces, by avoiding prefix duplication [#1322](https://github.com/sinatra/sinatra/issues/1322). Fixes [#1310](https://github.com/sinatra/sinatra/issues/1310) by Kunpei Sakai * Add pattern matches to values for Mustermann::Concat [#1333](https://github.com/sinatra/sinatra/issues/1333). Fixes [#1332](https://github.com/sinatra/sinatra/issues/1332) by Dawa Ometto * Ship the VERSION file with the gem, to allow local unpacking [#1338](https://github.com/sinatra/sinatra/issues/1338) by Olle Jonsson * Fix issue with custom error handler on bad request [#1351](https://github.com/sinatra/sinatra/issues/1351). Fixes [#1350](https://github.com/sinatra/sinatra/issues/1350) by Jordan Owens * Override Rack::ShowExceptions#pretty to set custom template [#1377](https://github.com/sinatra/sinatra/issues/1377). Fixes [#1376](https://github.com/sinatra/sinatra/issues/1376) by Jordan Owens * Enhanced path validation in Windows [#1379](https://github.com/sinatra/sinatra/issues/1379) by Orange Tsai from DEVCORE * The patch fixes [CVE-2018-7212](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-7212) * Improve development support and documentation by Faheel Ahmad, Shota Iguchi, Olle Jonsson, Manabu Niseki, John Hope, Horacio, Ice-Storm, GraniteRock, Raman Skaskevich, Carlos Azuaje, 284km, Dan Rice and Zachary Scott ## 2.0.0 / 2017-04-10 * Use Mustermann for patterns [#1086](https://github.com/sinatra/sinatra/issues/1086) by Konstantin Haase * Server now provides `-q` flag for quiet mode, which disables start/stop messages [#1153](https://github.com/sinatra/sinatra/issues/1153) by Vasiliy. * Session middleware can now be specified with `:session_store` setting [#1161](https://github.com/sinatra/sinatra/issues/1161) by Jordan Owens. * `APP_ENV` is now preferred and recommended over `RACK_ENV` for setting environment [#984](https://github.com/sinatra/sinatra/issues/984) by Damien Mathieu. * Add Reel support [#793](https://github.com/sinatra/sinatra/issues/793) by Patricio Mac Adden. * Make route params available during error handling [#895](https://github.com/sinatra/sinatra/issues/895) by Jeremy Evans. * Unify `not_found` and `error` 404 behavior [#896](https://github.com/sinatra/sinatra/issues/896) by Jeremy Evans. * Enable Ruby 2.3 `frozen_string_literal` feature [#1076](https://github.com/sinatra/sinatra/issues/1076) by Vladimir Kochnev. * Add Sinatra::ShowExceptions::TEMPLATE and patched Rack::ShowExceptions to prefer Sinatra template by Zachary Scott. * Sinatra::Runner is used internally for integration tests [#840](https://github.com/sinatra/sinatra/issues/840) by Nick Sutterer. * Fix case-sensitivity issue in `uri` method [#889](https://github.com/sinatra/sinatra/issues/889) by rennex. * Use `Rack::Utils.status_code` to allow `status` helper to use symbol as well as numeric codes [#968](https://github.com/sinatra/sinatra/issues/968) by Tobias H. Michaelsen. * Improved error handling for invalid params through Rack [#1070](https://github.com/sinatra/sinatra/issues/1070) by Jordan Owens. * Ensure template is cached only once [#1021](https://github.com/sinatra/sinatra/issues/1021) by Patrik Rak. * Rack middleware is initialized at server runtime rather than after receiving first request [#1205](https://github.com/sinatra/sinatra/issues/1205) by Itamar Turner-Trauring. * Improve Session Secret documentation to encourage better security practices [#1218](https://github.com/sinatra/sinatra/issues/1218) by Glenn Rempe * Exposed global and per-route options for Mustermann route parsing [#1233](https://github.com/sinatra/sinatra/issues/1233) by Mike Pastore * Use same `session_secret` for classic and modular apps in development [#1245](https://github.com/sinatra/sinatra/issues/1245) by Marcus Stollsteimer * Make authenticity token length a fixed value of 32 [#1181](https://github.com/sinatra/sinatra/issues/1181) by Jordan Owens * Modernize Rack::Protection::ContentSecurityPolicy with CSP Level 2 and 3 Directives [#1202](https://github.com/sinatra/sinatra/issues/1202) by Glenn Rempe * Adds preload option to Rack:Protection:StrictTransport [#1209](https://github.com/sinatra/sinatra/issues/1209) by Ed Robinson * Improve BadRequest logic. Raise and handle exceptions if status is 400 [#1212](https://github.com/sinatra/sinatra/issues/1212) by Mike Pastore * Make Rack::Test a development dependency [#1232](https://github.com/sinatra/sinatra/issues/1232) by Mike Pastore * Capture exception messages of raised NotFound and BadRequest [#1210](https://github.com/sinatra/sinatra/issues/1210) by Mike Pastore * Add explicit set method to contrib/cookies to override cookie settings [#1240](https://github.com/sinatra/sinatra/issues/1240) by Andrew Allen * Avoid executing filters even if prefix matches with other namespace [#1253](https://github.com/sinatra/sinatra/issues/1253) by namusyaka * Make `#has_key?` also indifferent in access, can accept String or Symbol [#1262](https://github.com/sinatra/sinatra/issues/1262) by Stephen Paul Weber * Add `allow_if` option to bypass json csrf protection [#1265](https://github.com/sinatra/sinatra/issues/1265) by Jordan Owens * rack-protection: Bundle StrictTransport, CookieTossing, and CSP [#1267](https://github.com/sinatra/sinatra/issues/1267) by Mike Pastore * Add `:strict_paths` option for managing trailing slashes [#1273](https://github.com/sinatra/sinatra/issues/1273) by namusyaka * Add full IndifferentHash implementation to params [#1279](https://github.com/sinatra/sinatra/issues/1279) by Mike Pastore ## 1.4.8 / 2017-01-30 * Fix the deprecation warning from Ruby about Fixnum. [#1235](https://github.com/sinatra/sinatra/issues/1235) by Akira Matsuda ## 1.4.7 / 2016-01-24 * Add Ashley Williams, Trevor Bramble, and Kashyap Kondamudi to team Sinatra. * Correctly handle encoded colons in routes. (Jeremy Evans) * Rename CHANGES to CHANGELOG.md and update Rakefile. [#1043](https://github.com/sinatra/sinatra/issues/1043) (Eliza Sorensen) * Improve documentation. [#941](https://github.com/sinatra/sinatra/issues/941), [#1069](https://github.com/sinatra/sinatra/issues/1069), [#1075](https://github.com/sinatra/sinatra/issues/1075), [#1025](https://github.com/sinatra/sinatra/issues/1025), [#1052](https://github.com/sinatra/sinatra/issues/1052) (Many great folks) * Introduce `Sinatra::Ext` to workaround Rack 1.6 bug to fix Ruby 1.8.7 support. [#1080](https://github.com/sinatra/sinatra/issues/1080) (Zachary Scott) * Add CONTRIBUTING guide. [#987](https://github.com/sinatra/sinatra/issues/987) (Katrina Owen) ## 1.4.6 / 2015-03-23 * Improve tests and documentation. (Darío Hereñú, Seiichi Yonezawa, kyoendo, John Voloski, Ferenc-, Renaud Martinet, Christian Haase, marocchino, huoxito, Damir Svrtan, Amaury Medeiros, Jeremy Evans, Kashyap, shenqihui, Ausmarton Fernandes, kami, Vipul A M, Lei Wu, 7stud, Taylor Shuler, namusyaka, burningTyger, Cornelius Bock, detomastah, hakeda, John Hope, Ruben Gonzalez, Andrey Deryabin, attilaolah, Anton Davydov, Nikita Penzin, Dyego Costa) * Remove duplicate require of sinatra/base. (Alexey Muranov) * Escape HTML in 404 error page. (Andy Brody) * Refactor to method call in `Stream#close` and `#callback`. (Damir Svrtan) * Depend on latest version of Slim. (Damir Svrtan) * Fix compatibility with Tilt version 2. (Yegor Timoschenko) * Fix compatibility issue with Rack `pretty` method from ShowExceptions. (Kashyap) * Show date in local time in exception messages. (tayler1) * Fix logo on error pages when using Ruby 1.8. (Jeremy Evans) * Upgrade test suite to Minitest version 5 and fix Ruby 2.2 compatibility. (Vipul A M) ## 1.4.5 / 2014-04-08 * Improve tests and documentation. (Seiichi Yonezawa, Mike Gehard, Andrew Deitrick, Matthew Nicholas Bradley, GoGo tanaka, Carlos Lazo, Shim Tw, kyoendo, Roman Kuznietsov, Stanislav Chistenko, Ryunosuke SATO, Ben Lewis, wuleicanada, Patricio Mac Adden, Thais Camilo) * Fix Ruby warnings. (Vipul A M, Piotr Szotkowski) * Fix template cache memory leak. (Scott Holden) * Work around UTF-8 bug in JRuby. (namusyaka) * Don't set charset for JSON mime-type (Sebastian Borrazas) * Fix bug in request.accept? that might trigger a NoMethodError. (sbonami) ## 1.4.4 / 2013-10-21 * Allow setting layout to false specifically for a single rendering engine. (Matt Wildig) * Allow using wildcard in argument passed to `request.accept?`. (wilkie) * Treat missing Accept header like wild card. (Patricio Mac Adden) * Improve tests and documentation. (Darío Javier Cravero, Armen P., michelc, Patricio Mac Adden, Matt Wildig, Vipul A M, utenmiki, George Timoschenko, Diogo Scudelletti) * Fix Ruby warnings. (Vipul A M, Patricio Mac Adden) * Improve self-hosted server started by `run!` method or in classic mode. (Tobias Bühlmann) * Reduce objects allocated per request. (Vipul A M) * Drop unused, undocumented options hash from Sinatra.new. (George Timoschenko) * Keep Content-Length header when response is a `Rack::File` or when streaming. (Patricio Mac Adden, George Timoschenko) * Use reel if it's the only server available besides webrick. (Tobias Bühlmann) * Add `disable :traps` so setting up signal traps for self hosted server can be skipped. (George Timoschenko) * The `status` option passed to `send_file` may now be a string. (George Timoschenko) * Reduce file size of dev mode images for 404 and 500 pages. (Francis Go) ## 1.4.3 / 2013-06-07 * Running a Sinatra file directly or via `run!` it will now ignore an empty $PORT env variable. (noxqsgit) * Improve documentation. (burningTyger, Patricio Mac Adden, Konstantin Haase, Diogo Scudelletti, Dominic Imhof) * Expose matched pattern as env["sinatra.route"]. (Aman Gupta) * Fix warning on Ruby 2.0. (Craig Little) * Improve running subset of tests in isolation. (Viliam Pucik) * Reorder private/public methods. (Patricio Mac Adden) * Loosen version dependency for rack, so it runs with Rails 3.2. (Konstantin Haase) * Request#accept? now returns true instead of a truthy value. (Alan Harris) ## 1.4.2 / 2013-03-21 * Fix parsing error for case where both the pattern and the captured part contain a dot. (Florian Hanke, Konstantin Haase) * Missing Accept header is treated like */*. (Greg Denton) * Improve documentation. (Patricio Mac Adden, Joe Bottigliero) ## 1.4.1 / 2013-03-15 * Make delegated methods available in config.ru (Konstantin Haase) ## 1.4.0 / 2013-03-15 * Add support for LINK and UNLINK requests. (Konstantin Haase) * Add support for Yajl templates. (Jamie Hodge) * Add support for Rabl templates. (Jesse Cooke) * Add support for Wlang templates. (Bernard Lambeau) * Add support for Stylus templates. (Juan David Pastas, Konstantin Haase) * You can now pass a block to ERb, Haml, Slim, Liquid and Wlang templates, which will be used when calling `yield` in the template. (Alexey Muranov) * When running in classic mode, no longer include Sinatra::Delegator in Object, instead extend the main object only. (Konstantin Haase) * Improved route parsing: "/:name.?:format?" with "/foo.png" now matches to {name: "foo", format: "png"} instead of {name: "foo.png"}. (Florian Hanke) * Add :status option support to send_file. (Konstantin Haase) * The `provides` condition now respects an earlier set content type. (Konstantin Haase) * Exception#code is only used when :use_code is enabled. Moreover, it will be ignored if the value is not between 400 and 599. You should use Exception#http_status instead. (Konstantin Haase) * Status, headers and body will be set correctly in an after filter when using halt in a before filter or route. (Konstantin Haase) * Sinatra::Base.new now returns a Sinatra::Wrapper instance, exposing `#settings` and `#helpers`, yet going through the middleware stack on `#call`. It also implements a nice `#inspect`, so it plays nice with Rails' `rake routes`. (Konstantin Haase) * In addition to WebRick, Thin and Mongrel, Sinatra will now automatically pick up Puma, Trinidad, ControlTower or Net::HTTP::Server when installed. The logic for picking the server has been improved and now depends on the Ruby implementation used. (Mark Rada, Konstantin Haase, Patricio Mac Adden) * "Sinatra doesn't know this ditty" pages now show the app class when running a modular application. This helps detecting where the response came from when combining multiple modular apps. (Konstantin Haase) * When port is not set explicitly, use $PORT env variable if set and only default to 4567 if not. Plays nice with foreman. (Konstantin Haase) * Allow setting layout on a per engine basis. (Zachary Scott, Konstantin Haase) * You can now use `register` directly in a classic app. (Konstantin Haase) * `redirect` now accepts URI or Addressable::URI instances. (Nicolas Sanguinetti) * Have Content-Disposition header also include file name for `inline`, not just for `attachment`. (Konstantin Haase) * Better compatibility to Rack 1.5. (James Tucker, Konstantin Haase) * Make route parsing regex more robust. (Zoltan Dezso, Konstantin Haase) * Improve Accept header parsing, expose parameters. (Pieter van de Bruggen, Konstantin Haase) * Add `layout_options` render option. Allows you, amongst other things, to render a layout from a different folder. (Konstantin Haase) * Explicitly setting `layout` to `nil` is treated like setting it to `false`. (richo) * Properly escape attributes in Content-Type header. (Pieter van de Bruggen) * Default to only serving localhost in development mode. (Postmodern) * Setting status code to 404 in error handler no longer triggers not_found handler. (Konstantin Haase) * The `protection` option now takes a `session` key for force disabling/enabling session based protections. (Konstantin Haase) * Add `x_cascade` option to disable `X-Cascade` header on missing route. (Konstantin Haase) * Improve documentation. (Kashyap, Stanislav Chistenko, Zachary Scott, Anthony Accomazzo, Peter Suschlik, Rachel Mehl, ymmtmsys, Anurag Priyam, burningTyger, Tony Miller, akicho8, Vasily Polovnyov, Markus Prinz, Alexey Muranov, Erik Johnson, Vipul A M, Konstantin Haase) * Convert documentation to Markdown. (Kashyap, Robin Dupret, burningTyger, Vasily Polovnyov, Iain Barnett, Giuseppe Capizzi, Neil West) * Don't set not_found content type to HTML in development mode with custom not_found handler. (Konstantin Haase) * Fix mixed indentation for private methods. (Robin Dupret) * Recalculate Content-Length even if hard coded if body is reset. Relevant mostly for error handlers. (Nathan Esquenazi, Konstantin Haase) * Plus sign is once again kept as such when used for URL matches. (Konstantin Haase) * Take views option into account for template caching. (Konstantin Haase) * Consistent use of `headers` instead of `header` internally. (Patricio Mac Adden) * Fix compatibility to RDoc 4. (Bohuslav Kabrda) * Make chat example work with latest jQuery. (loveky, Tony Miller) * Make tests run without warnings. (Patricio Mac Adden) * Make sure value returned by `mime_type` is a String or nil, even when a different object is passed in, like an AcceptEntry. (Konstantin Haase) * Exceptions in `after` filter are now handled like any other exception. (Nathan Esquenazi) ## 1.3.6 (backport release) / 2013-03-15 Backported from 1.4.0: * Take views option into account for template caching. (Konstantin Haase) * Improve documentation (Konstantin Haase) * No longer override `define_singleton_method`. (Konstantin Haase) ## 1.3.5 / 2013-02-25 * Fix for RubyGems 2.0 (Uchio KONDO) * Improve documentation (Konstantin Haase) * No longer override `define_singleton_method`. (Konstantin Haase) ## 1.3.4 / 2013-01-26 * Improve documentation. (Kashyap, Stanislav Chistenko, Konstantin Haase, ymmtmsys, Anurag Priyam) * Adjustments to template system to work with Tilt edge. (Konstantin Haase) * Fix streaming with latest Rack release. (Konstantin Haase) * Fix default content type for Sinatra::Response with latest Rack release. (Konstantin Haase) * Fix regression where + was no longer treated like space. (Ross Boucher) * Status, headers and body will be set correctly in an after filter when using halt in a before filter or route. (Konstantin Haase) ## 1.3.3 / 2012-08-19 * Improved documentation. (burningTyger, Konstantin Haase, Gabriel Andretta, Anurag Priyam, michelc) * No longer modify the load path. (Konstantin Haase) * When keeping a stream open, set up callback/errback correctly to deal with clients closing the connection. (Konstantin Haase) * Fix bug where having a query param and a URL param by the same name would concatenate the two values. (Konstantin Haase) * Prevent duplicated log output when application is already wrapped in a `Rack::CommonLogger`. (Konstantin Haase) * Fix issue where `Rack::Link` and Rails were preventing indefinite streaming. (Konstantin Haase) * No longer cause warnings when running Ruby with `-w`. (Konstantin Haase) * HEAD requests on static files no longer report a Content-Length of 0, but instead the proper length. (Konstantin Haase) * When protecting against CSRF attacks, drop the session instead of refusing the request. (Konstantin Haase) ## 1.3.2 / 2011-12-30 * Don't automatically add `Rack::CommonLogger` if `Rack::Server` is adding it, too. (Konstantin Haase) * Setting `logging` to `nil` will avoid setting up `Rack::NullLogger`. (Konstantin Haase) * Route specific params are now available in the block passed to #stream. (Konstantin Haase) * Fix bug where rendering a second template in the same request, after the first one raised an exception, skipped the default layout. (Nathan Baum) * Fix bug where parameter escaping got enabled when disabling a different protection. (Konstantin Haase) * Fix regression: Filters without a pattern may now again manipulate the params hash. (Konstantin Haase) * Added examples directory. (Konstantin Haase) * Improved documentation. (Gabriel Andretta, Markus Prinz, Erick Zetta, Just Lest, Adam Vaughan, Aleksander Dąbrowski) * Improved MagLev support. (Tim Felgentreff) ## 1.3.1 / 2011-10-05 * Support adding more than one callback to the stream object. (Konstantin Haase) * Fix for infinite loop when streaming on 1.9.2 with Thin from a modular application (Konstantin Haase) ## 1.3.0 / 2011-09-30 * Added `stream` helper method for easily creating streaming APIs, Server Sent Events or even WebSockets. See README for more on that topic. (Konstantin Haase) * If a HTTP 1.1 client is redirected from a different verb than GET, use 303 instead of 302 by default. You may still pass 302 explicitly. Fixes AJAX redirects in Internet Explorer 9 (to be fair, everyone else is doing it wrong and IE is behaving correct). (Konstantin Haase) * Added support for HTTP PATCH requests. (Konstantin Haase) * Use rack-protection to defend against common opportunistic attacks. (Josh Lane, Jacob Burkhart, Konstantin Haase) * Support for Creole templates, Creole is a standardized wiki markup, supported by many wiki implementations. (Konstanin Haase) * The `erubis` method has been deprecated. If Erubis is available, Sinatra will automatically use it for rendering ERB templates. `require 'erb'` explicitly to prevent that behavior. (Magnus Holm, Ryan Tomayko, Konstantin Haase) * Patterns now match against the escaped URLs rather than the unescaped version. This makes Sinatra confirm with RFC 2396 section 2.2 and RFC 2616 section 3.2.3 (escaped reserved characters should not be treated like the unescaped version), meaning that "/:name" will also match `/foo%2Fbar`, but not `/foo/bar`. To avoid incompatibility, pattern matching has been adjusted. Moreover, since we do no longer need to keep an unescaped version of path_info around, we handle all changes to `env['PATH_INFO']` correctly. (Konstantin Haase) * `settings.app_file` now defaults to the file subclassing `Sinatra::Base` in modular applications. (Konstantin Haase) * Set up `Rack::Logger` or `Rack::NullLogger` depending on whether logging was enabled or not. Also, expose that logger with the `logger` helper method. (Konstantin Haase) * The sessions setting may be an options hash now. (Konstantin Haase) * Important: Ruby 1.8.6 support has been dropped. This version also depends on at least Rack 1.3.0. This means that it is incompatible with Rails prior to 3.1.0. Please use 1.2.x if you require an earlier version of Ruby or Rack, which we will continue to supply with bug fixes. (Konstantin Haase) * Renamed `:public` to `:public_folder` to avoid overriding Ruby's built-in `public` method/keyword. `set(:public, ...)` is still possible but shows a warning. (Konstantin Haase) * It is now possible to use a different target class for the top level DSL (aka classic style) than `Sinatra::Application` by setting `Delegator.target`. This was mainly introduced to ease testing. (Konstantin Haase) * Error handlers defined for an error class will now also handle subclasses of that class, unless more specific error handlers exist. (Konstantin Haase) * Error handling respects Exception#code, again. (Konstantin Haase) * Changing a setting will merge hashes: `set(:x, :a => 1); set(:x :b => 2)` will result in `{:a => 1, :b => 2}`. Use `set(:x, {:a => 1}, true)` to avoid this behavior. (Konstantin Haase) * Added `request.accept?` and `request.preferred_type` to ease dealing with `Accept` headers. (Konstantin Haase) * Added `:static_cache_control` setting to automatically set cache control headers to static files. (Kenichi Nakamura) * Added `informal?`, `success?`, `redirect?`, `client_error?`, `server_error?` and `not_found?` helper methods to ease dealing with status codes. (Konstantin Haase) * Uses SecureRandom to generate default session secret. (Konstantin Haase) * The `attachment` helper will set Content-Type (if it hasn't been set yet) depending on the supplied file name. (Vasiliy Ermolovich) * Conditional requests on `etag` helper now work properly for unsafe HTTP methods. (Matthew Schinckel, Konstantin Haase) * The `last_modified` helper does not stop execution and change the status code if the status code is something different than 200. (Konstantin Haase) * Added support for If-Unmodified-Since header. (Konstantin Haase) * `Sinatra::Base.run!` now prints to stderr rather than stdout. (Andrew Armenia) * `Sinatra::Base.run!` takes a block allowing access to the Rack handler. (David Waite) * Automatic `app_file` detection now works in directories containing brackets (Konstantin Haase) * Exception objects are now passed to error handlers. (Konstantin Haase) * Improved documentation. (Emanuele Vicentini, Peter Higgins, Takanori Ishikawa, Konstantin Haase) * Also specify charset in Content-Type header for JSON. (Konstantin Haase) * Rack handler names will not be converted to lower case internally, this allows you to run Sinatra with custom Rack handlers, like Kirk or Mongrel2. Example: `ruby app.rb -s Mongrel2` (Konstantin Haase) * Ignore `to_ary` on response bodies. Fixes compatibility to Rails 3.1. (Konstantin Haase) * Middleware setup is now distributed across multiple methods, allowing Sinatra extensions to easily hook into the setup process. (Konstantin Haase) * Internal refactoring and minor performance improvements. (Konstantin Haase) * Move Sinatra::VERSION to separate file, so it can be checked without loading Sinatra. (Konstantin Haase) * Command line options now complain if value passed to `-p` is not a valid integer. (Konstantin Haase) * Fix handling of broken query params when displaying exceptions. (Luke Jahnke) ## 1.2.9 (backports release) / 2013-03-15 IMPORTANT: THIS IS THE LAST 1.2.x RELEASE, PLEASE UPGRADE. * Display EOL warning when loading Sinatra. (Konstantin Haase) * Improve documentation. (Anurag Priyam, Konstantin Haase) * Do not modify the load path. (Konstantin Haase) * Display deprecation warning if RUBY_IGNORE_CALLERS is used. (Konstantin Haase) * Add backports library so we can still run on Ruby 1.8.6. (Konstantin Haase) ## 1.2.8 (backports release) / 2011-12-30 Backported from 1.3.2: * Fix bug where rendering a second template in the same request after the first one raised an exception skipped the default layout (Nathan Baum) ## 1.2.7 (backports release) / 2011-09-30 Custom changes: * Fix Ruby 1.8.6 issue with Accept header parsing. (Konstantin Haase) Backported from 1.3.0: * Ignore `to_ary` on response bodies. Fixes compatibility to Rails 3.1. (Konstantin Haase) * `Sinatra.run!` now prints to stderr rather than stdout. (Andrew Armenia) * Automatic `app_file` detection now works in directories containing brackets (Konstantin Haase) * Improved documentation. (Emanuele Vicentini, Peter Higgins, Takanori Ishikawa, Konstantin Haase) * Also specify charset in Content-Type header for JSON. (Konstantin Haase) * Rack handler names will not be converted to lower case internally, this allows you to run Sinatra with custom Rack handlers, like Kirk or Mongrel2. Example: `ruby app.rb -s Mongrel2` (Konstantin Haase) * Fix uninitialized instance variable warning. (David Kellum) * Command line options now complain if value passed to `-p` is not a valid integer. (Konstantin Haase) * Fix handling of broken query params when displaying exceptions. (Luke Jahnke) ## 1.2.6 / 2011-05-01 * Fix broken delegation, backport delegation tests from Sinatra 1.3. (Konstantin Haase) ## 1.2.5 / 2011-04-30 * Restore compatibility with Ruby 1.8.6. (Konstantin Haase) ## 1.2.4 / 2011-04-30 * Sinatra::Application (classic style) does not use a session secret in development mode, so sessions are not invalidated after every request when using Shotgun. (Konstantin Haase) * The request object was shared between multiple Sinatra instances in the same middleware chain. This caused issues if any non-sinatra routing happened in-between two of those instances, or running a request twice against an application (described in the README). The caching was reverted. See GH[#239](https://github.com/sinatra/sinatra/issues/239) and GH[#256](https://github.com/sinatra/sinatra/issues/256) for more infos. (Konstantin Haase) * Fixes issues where the top level DSL was interfering with method_missing proxies. This issue surfaced when Rails 3 was used with older Sass versions and Sinatra >= 1.2.0. (Konstantin Haase) * Sinatra::Delegator.delegate is now able to delegate any method names, even those containing special characters. This allows better integration into other programming languages on Rubinius (probably on the JVM, too), like Fancy. (Konstantin Haase) * Remove HEAD request logic and let Rack::Head handle it instead. (Paolo "Nusco" Perrotta) ## 1.2.3 / 2011-04-13 * This release is compatible with Tilt 1.3, it will still work with Tilt 1.2.2, however, if you want to use a newer Tilt version, you have to upgrade to at least this version of Sinatra. (Konstantin Haase) * Helpers dealing with time, like `expires`, handle objects that pretend to be numbers, like `ActiveSupport::Duration`, better. (Konstantin Haase) ## 1.2.2 / 2011-04-08 * The `:provides => :js` condition now matches both `application/javascript` and `text/javascript`. The `:provides => :xml` condition now matches both `application/xml` and `text/xml`. The `Content-Type` header is set accordingly. If the client accepts both, the `application/*` version is preferred, since the `text/*` versions are deprecated. (Konstantin Haase) * The `provides` condition now handles wildcards in `Accept` headers correctly. Thus `:provides => :html` matches `text/html`, `text/*` and `*/*`. (Konstantin Haase) * When parsing `Accept` headers, `Content-Type` preferences are honored according to RFC 2616 section 14.1. (Konstantin Haase) * URIs passed to the `url` helper or `redirect` may now use any schema to be identified as absolute URIs, not only `http` or `https`. (Konstantin Haase) * Handles `Content-Type` strings that already contain parameters correctly in `content_type` (example: `content_type "text/plain; charset=utf-16"`). (Konstantin Haase) * If a route with an empty pattern is defined (`get("") { ... }`) requests with an empty path info match this route instead of "/". (Konstantin Haase) * In development environment, when running under a nested path, the image URIs on the error pages are set properly. (Konstantin Haase) ## 1.2.1 / 2011-03-17 * Use a generated session secret when using `enable :sessions`. (Konstantin Haase) * Fixed a bug where the wrong content type was used if no content type was set and a template engine was used with a different engine for the layout with different default content types, say Less embedded in Slim. (Konstantin Haase) * README translations improved (Gabriel Andretta, burningTyger, Sylvain Desvé, Gregor Schmidt) ## 1.2.0 / 2011-03-03 * Added `slim` rendering method for rendering Slim templates. (Steve Hodgkiss) * The `markaby` rendering method now allows passing a block, making inline usage possible. Requires Tilt 1.2 or newer. (Konstantin Haase) * All render methods now take a `:layout_engine` option, allowing to use a layout in a different template language. Even more useful than using this directly (`erb :index, :layout_engine => :haml`) is setting this globally for a template engine that otherwise does not support layouts, like Markdown or Textile (`set :markdown, :layout_engine => :erb`). (Konstantin Haase) * Before and after filters now support conditions, both with and without patterns (`before '/api/*', :agent => /Songbird/`). (Konstantin Haase) * Added a `url` helper method which constructs absolute URLs. Copes with reverse proxies and Rack handlers correctly. Aliased to `to`, so you can write `redirect to('/foo')`. (Konstantin Haase) * If running on 1.9, patterns for routes and filters now support named captures: `get(%r{/hi/(?[^/?#]+)}) { "Hi #{params['name']}" }`. (Steve Price) * All rendering methods now take a `:scope` option, which renders them in another context. Note that helpers and instance variables will be unavailable if you use this feature. (Paul Walker) * The behavior of `redirect` can now be configured with `absolute_redirects` and `prefixed_redirects`. (Konstantin Haase) * `send_file` now allows overriding the Last-Modified header, which defaults to the file's mtime, by passing a `:last_modified` option. (Konstantin Haase) * You can use your own template lookup method by defining `find_template`. This allows, among other things, using more than one views folder. (Konstantin Haase) * Largely improved documentation. (burningTyger, Vasily Polovnyov, Gabriel Andretta, Konstantin Haase) * Improved error handling. (cactus, Konstantin Haase) * Skip missing template engines in tests correctly. (cactus) * Sinatra now ships with a Gemfile for development dependencies, since it eases supporting different platforms, like JRuby. (Konstantin Haase) ## 1.1.4 (backports release) / 2011-04-13 * Compatible with Tilt 1.3. (Konstantin Haase) ## 1.1.3 / 2011-02-20 * Fixed issues with `user_agent` condition if the user agent header is missing. (Konstantin Haase) * Fix some routing tests that have been skipped by accident (Ross A. Baker) * Fix rendering issues with Builder and Nokogiri (Konstantin Haase) * Replace last_modified helper with better implementation. (cactus, Konstantin Haase) * Fix issue with charset not being set when using `provides` condition. (Konstantin Haase) * Fix issue with `render` not picking up all alternative file extensions for a rendering engine - it was not possible to register ".html.erb" without tricks. (Konstantin Haase) ## 1.1.2 / 2010-10-25 Like 1.1.1, but with proper CHANGES file. ## 1.1.1 / 2010-10-25 * README has been translated to Russian (Nickolay Schwarz, Vasily Polovnyov) and Portuguese (Luciano Sousa). * Nested templates without a `:layout` option can now be used from the layout template without causing an infinite loop. (Konstantin Haase) * Inline templates are now encoding aware and can therefore be used with unicode characters on Ruby 1.9. Magic comments at the beginning of the file will be honored. (Konstantin Haase) * Default `app_file` is set correctly when running with bundler. Using bundler caused Sinatra not to find the `app_file` and therefore not to find the `views` folder on it's own. (Konstantin Haase) * Better handling of Content-Type when using `send_file`: If file extension is unknown, fall back to `application/octet-stream` and do not override content type if it has already been set, except if `:type` is passed explicitly (Konstantin Haase) * Path is no longer cached if changed between handlers that do pattern matching. This means you can change `request.path_info` in a pattern matching before filter. (Konstantin Haase) * Headers set by cache_control now always set max_age as an Integer, making sure it is compatible with RFC2616. (Konstantin Haase) * Further improved handling of string encodings on Ruby 1.9, templates now honor default_encoding and URLs support unicode characters. (Konstantin Haase) ## 1.1.0 / 2010-10-24 * Before and after filters now support pattern matching, including the ability to use captures: "before('/user/:name') { |name| ... }". This avoids manual path checking. No performance loss if patterns are avoided. (Konstantin Haase) * It is now possible to render SCSS files with the `scss` method, which behaves exactly like `sass` except for the different file extension and assuming the SCSS syntax. (Pedro Menezes, Konstantin Haase) * Added `liquid`, `markdown`, `nokogiri`, `textile`, `rdoc`, `radius`, `markaby`, and `coffee` rendering methods for rendering Liquid, Markdown, Nokogiri, Textile, RDoc, Radius, Markaby and CoffeeScript templates. (Konstantin Haase) * Now supports byte-range requests (the HTTP_RANGE header) for static files. Multi-range requests are not supported, however. (Jens Alfke) * You can now use #settings method from class and top level for convenience. (Konstantin Haase) * Setting multiple values now no longer relies on #to_hash and therefore accepts any Enumerable as parameter. (Simon Rozet) * Nested templates default the `layout` option to `false` rather than `true`. This eases the use of partials. If you wanted to render one haml template embedded in another, you had to call `haml :partial, {}, :layout => false`. As you almost never want the partial to be wrapped in the standard layout in this situation, you now only have to call `haml :partial`. Passing in `layout` explicitly is still possible. (Konstantin Haase) * If a the return value of one of the render functions is used as a response body and the content type has not been set explicitly, Sinatra chooses a content type corresponding to the rendering engine rather than just using "text/html". (Konstantin Haase) * README is now available in Chinese (Wu Jiang), French (Mickael Riga), German (Bernhard Essl, Konstantin Haase, burningTyger), Hungarian (Janos Hardi) and Spanish (Gabriel Andretta). The extremely outdated Japanese README has been updated (Kouhei Yanagita). * It is now possible to access Sinatra's template_cache from the outside. (Nick Sutterer) * The `last_modified` method now also accepts DateTime instances and makes sure the header will always be set to a string. (Konstantin Haase) * 599 now is a legal status code. (Steve Shreeve) * This release is compatible with Ruby 1.9.2. Sinatra was trying to read non existent files Ruby added to the call stack. (Shota Fukumori, Konstantin Haase) * Prevents a memory leak on 1.8.6 in production mode. Note, however, that this is due to a bug in 1.8.6 and request will have the additional overhead of parsing templates again on that version. It is recommended to use at least Ruby 1.8.7. (Konstantin Haase) * Compares last modified date correctly. `last_modified` was halting only when the 'If-Modified-Since' header date was equal to the time specified. Now, it halts when is equal or later than the time specified (Gabriel Andretta). * Sinatra is now usable in combination with Rails 3. When mounting a Sinatra application under a subpath in Rails 3, the PATH_INFO is not prefixed with a slash and no routes did match. (José Valim) * Better handling of encodings in 1.9, defaults params encoding to UTF-8. (Konstantin Haase) * `show_exceptions` handling is now triggered after custom error handlers, if it is set to `:after_handlers`, thus not disabling those handler in development mode. (pangel, Konstantin Haase) * Added ability to handle weighted HTTP_ACCEPT headers. (Davide D'Agostino) * `send_file` now always respects the `:type` option if set. Previously it was discarded if no matching mime type was found, which made it impossible to directly pass a mime type. (Konstantin Haase) * `redirect` always redirects to an absolute URI, even if a relative URI was passed. Ensures compatibility with RFC 2616 section 14.30. (Jean-Philippe Garcia Ballester, Anthony Williams) * Broken examples for using Erubis, Haml and Test::Unit in README have been fixed. (Nick Sutterer, Doug Ireton, Jason Stewart, Eric Marden) * Sinatra now handles SIGTERM correctly. (Patrick Collison) * Fixes an issue with inline templates in modular applications that manually call `run!`. (Konstantin Haase) * Spaces after inline template names are now ignored (Konstantin Haase) * It's now possible to use Sinatra with different package management systems defining a custom require. (Konstantin Haase) * Lighthouse has been dropped in favor of GitHub issues. * Tilt is now a dependency and therefore no longer ships bundled with Sinatra. (Ryan Tomayko, Konstantin Haase) * Sinatra now depends on Rack 1.1 or higher. Rack 1.0 is no longer supported. (Konstantin Haase) ## 1.0 / 2010-03-23 * It's now possible to register blocks to run after each request using after filters. After filters run at the end of each request, after routes and error handlers. (Jimmy Schementi) * Sinatra now uses Tilt for rendering templates. This adds support for template caching, consistent template backtraces, and support for new template engines, like mustache and liquid. (Ryan Tomayko) * ERB, Erubis, and Haml templates are now compiled the first time they're rendered instead of being string eval'd on each invocation. Benchmarks show a 5x-10x improvement in render time. This also reduces the number of objects created, decreasing pressure on Ruby's GC. (Ryan Tomayko) * New 'settings' method gives access to options in both class and request scopes. This replaces the 'options' method. (Chris Wanstrath) * New boolean 'reload_templates' setting controls whether template files are reread from disk and recompiled on each request. Template read/compile is cached by default in all environments except development. (Ryan Tomayko) * New 'erubis' helper method for rendering ERB template with Erubis. The erubis gem is required. (Dylan Egan) * New 'cache_control' helper method provides a convenient way of setting the Cache-Control response header. Takes a variable number of boolean directives followed by a hash of value directives, like this: cache_control :public, :must_revalidate, :max_age => 60 (Ryan Tomayko) * New 'expires' helper method is like cache_control but takes an integer number of seconds or Time object: expires 300, :public, :must_revalidate (Ryan Tomayko) * New request.secure? method for checking for an SSL connection. (Adam Wiggins) * Sinatra apps can now be run with a `-o ` argument to specify the address to bind to. (Ryan Tomayko) * Rack::Session::Cookie is now added to the middleware pipeline when running in test environments if the :sessions option is set. (Simon Rozet) * Route handlers, before filters, templates, error mappings, and middleware are now resolved dynamically up the inheritance hierarchy when needed instead of duplicating the superclass's version when a new Sinatra::Base subclass is created. This should fix a variety of issues with extensions that need to add any of these things to the base class. (Ryan Tomayko) * Exception error handlers always override the raise_errors option now. Previously, all exceptions would be raised outside of the application when the raise_errors option was enabled, even if an error handler was defined for that exception. The raise_errors option now controls whether unhandled exceptions are raised (enabled) or if a generic 500 error is returned (disabled). (Ryan Tomayko) * The X-Cascade response header is set to 'pass' when no matching route is found or all routes pass. (Josh Peek) * Filters do not run when serving static files anymore. (Ryan Tomayko) * pass takes an optional block to be used as the route handler if no subsequent route matches the request. (Blake Mizerany) The following Sinatra features have been obsoleted (removed entirely) in the 1.0 release: * The `sinatra/test` library is obsolete. This includes the `Sinatra::Test` module, the `Sinatra::TestHarness` class, and the `get_it`, `post_it`, `put_it`, `delete_it`, and `head_it` helper methods. The [`Rack::Test` library](http://gitrdoc.com/brynary/rack-test) should be used instead. * Test framework specific libraries (`sinatra/test/spec`, `sinatra/test/bacon`,`sinatra/test/rspec`, etc.) are obsolete. See http://www.sinatrarb.com/testing.html for instructions on setting up a testing environment under each of these frameworks. * `Sinatra::Default` is obsolete; use `Sinatra::Base` instead. `Sinatra::Base` acts more like `Sinatra::Default` in development mode. For example, static file serving and sexy development error pages are enabled by default. * Auto-requiring template libraries in the `erb`, `builder`, `haml`, and `sass` methods is obsolete due to thread-safety issues. You must require the template libraries explicitly in your app. * The `:views_directory` option to rendering methods is obsolete; use `:views` instead. * The `:haml` and `:sass` options to rendering methods are obsolete. Template engine options should be passed in the second Hash argument instead. * The `use_in_file_templates` method is obsolete. Use `enable :inline_templates` or `set :inline_templates, 'path/to/file'` * The 'media_type' helper method is obsolete. Use 'mime_type' instead. * The 'mime' main and class method is obsolete. Use 'mime_type' instead. * The request-level `send_data` method is no longer supported. * The `Sinatra::Event` and `Sinatra::EventContext` classes are no longer supported. This may effect extensions written for versions prior to 0.9.2. See [Writing Sinatra Extensions](http://www.sinatrarb.com/extensions.html) for the officially supported extensions API. * The `set_option` and `set_options` methods are obsolete; use `set` instead. * The `:env` setting (`settings.env`) is obsolete; use `:environment` instead. * The request level `stop` method is obsolete; use `halt` instead. * The request level `entity_tag` method is obsolete; use `etag` instead. * The request level `headers` method (HTTP response headers) is obsolete; use `response['Header-Name']` instead. * `Sinatra.application` is obsolete; use `Sinatra::Application` instead. * Using `Sinatra.application = nil` to reset an application is obsolete. This should no longer be necessary. * Using `Sinatra.default_options` to set base configuration items is obsolete; use `Sinatra::Base.set(key, value)` instead. * The `Sinatra::ServerError` exception is obsolete. All exceptions raised within a request are now treated as internal server errors and result in a 500 response status. * The `:methodoverride' option to enable/disable the POST _method hack is obsolete; use `:method_override` instead. ## 0.9.2 / 2009-05-18 * This version is compatible with Rack 1.0. [Rein Henrichs] * The development-mode unhandled exception / error page has been greatly enhanced, functionally and aesthetically. The error page is used when the :show_exceptions option is enabled and an exception propagates outside of a route handler or before filter. [Simon Rozet / Matte Noble / Ryan Tomayko] * Backtraces that move through templates now include filenames and line numbers where possible. [#51 / S. Brent Faulkner] * All templates now have an app-level option for setting default template options (:haml, :sass, :erb, :builder). The app-level option value must be a Hash if set and is merged with the template options specified to the render method (Base#haml, Base#erb, Base#builder). [S. Brent Faulkner, Ryan Tomayko] * The method signature for all template rendering methods has been unified: "def engine(template, options={}, locals={})". The options Hash now takes the generic :views, :layout, and :locals options but also any template-specific options. The generic options are removed before calling the template specific render method. Locals may be specified using either the :locals key in the options hash or a second Hash option to the rendering method. [#191 / Ryan Tomayko] * The receiver is now passed to "configure" blocks. This allows for the following idiom in top-level apps: configure { |app| set :foo, app.root + '/foo' } [TJ Holowaychuck / Ryan Tomayko] * The "sinatra/test" lib is deprecated and will be removed in Sinatra 1.0. This includes the Sinatra::Test module and Sinatra::TestHarness class in addition to all the framework test helpers that were deprecated in 0.9.1. The Rack::Test lib should be used instead: http://gitrdoc.com/brynary/rack-test [#176 / Simon Rozet] * Development mode source file reloading has been removed. The "shotgun" (http://rtomayko.github.com/shotgun/) program can be used to achieve the same basic functionality in most situations. Passenger users should use the "tmp/always_restart.txt" file (http://tinyurl.com/c67o4h). [#166 / Ryan Tomayko] * Auto-requiring template libs in the erb, builder, haml, and sass methods is deprecated due to thread-safety issues. You must require the template libs explicitly in your app file. [Simon Rozet] * A new Sinatra::Base#route_missing method was added. route_missing is sent when no route matches the request or all route handlers pass. The default implementation forwards the request to the downstream app when running as middleware (i.e., "@app" is non-nil), or raises a NotFound exception when no downstream app is defined. Subclasses can override this method to perform custom route miss logic. [Jon Crosby] * A new Sinatra::Base#route_eval method was added. The method yields to the block and throws :halt with the result. Subclasses can override this method to tap into the route execution logic. [TJ Holowaychuck] * Fix the "-x" (enable request mutex / locking) command line argument. Passing -x now properly sets the :lock option. [S. Brent Faulkner, Ryan Tomayko] * Fix writer ("foo=") and predicate ("foo?") methods in extension modules not being added to the registering class. [#172 / Pat Nakajima] * Fix in-file templates when running alongside activesupport and fatal errors when requiring activesupport before sinatra [#178 / Brian Candler] * Fix various issues running on Google AppEngine. [Samuel Goebert, Simon Rozet] * Fix in-file templates __END__ detection when __END__ exists with other stuff on a line [Yoji Shidara] ## 0.9.1.1 / 2009-03-09 * Fix directory traversal vulnerability in default static files route. See [#177] for more info. ## 0.9.1 / 2009-03-01 * Sinatra now runs under Ruby 1.9.1 [#61] * Route patterns (splats, :named, or Regexp captures) are now passed as arguments to the block. [#140] * The "helpers" method now takes a variable number of modules along with the normal block syntax. [#133] * New request-level #forward method for middleware components: passes the env to the downstream app and merges the response status, headers, and body into the current context. [#126] * Requests are now automatically forwarded to the downstream app when running as middleware and no matching route is found or all routes pass. * New simple API for extensions/plugins to add DSL-level and request-level methods. Use Sinatra.register(mixin) to extend the DSL with all public methods defined in the mixin module; use Sinatra.helpers(mixin) to make all public methods defined in the mixin module available at the request level. [#138] See http://www.sinatrarb.com/extensions.html for details. * Named parameters in routes now capture the "." character. This makes routes like "/:path/:filename" match against requests like "/foo/bar.txt"; in this case, "params[:filename]" is "bar.txt". Previously, the route would not match at all. * Added request-level "redirect back" to redirect to the referring URL. * Added a new "clean_trace" option that causes backtraces dumped to rack.errors and displayed on the development error page to omit framework and core library backtrace lines. The option is enabled by default. [#77] * The ERB output buffer is now available to helpers via the @_out_buf instance variable. * It's now much easier to test sessions in unit tests by passing a ":session" option to any of the mock request methods. e.g., get '/', {}, :session => { 'foo' => 'bar' } * The testing framework specific files ('sinatra/test/spec', 'sinatra/test/bacon', 'sinatra/test/rspec', etc.) have been deprecated. See http://sinatrarb.com/testing.html for instructions on setting up a testing environment with these frameworks. * The request-level #send_data method from Sinatra 0.3.3 has been added for compatibility but is deprecated. * Fix :provides causing crash on any request when request has no Accept header [#139] * Fix that ERB templates were evaluated twice per "erb" call. * Fix app-level middleware not being run when the Sinatra application is run as middleware. * Fixed some issues with running under Rack's CGI handler caused by writing informational stuff to stdout. * Fixed that reloading was sometimes enabled when starting from a rackup file [#110] * Fixed that "." in route patterns erroneously matched any character instead of a literal ".". [#124] ## 0.9.0.4 / 2009-01-25 * Using halt with more than 1 args causes ArgumentError [#131] * using halt in a before filter doesn't modify response [#127] * Add deprecated Sinatra::EventContext to unbreak plugins [#130] * Give access to GET/POST params in filters [#129] * Preserve non-nested params in nested params hash [#117] * Fix backtrace dump with Rack::Lint [#116] ## 0.9.0.3 / 2009-01-21 * Fall back on mongrel then webrick when thin not found. [#75] * Use :environment instead of :env in test helpers to fix deprecation warnings coming from framework. * Make sinatra/test/rspec work again [#113] * Fix app_file detection on windows [#118] * Fix static files with Rack::Lint in pipeline [#121] ## 0.9.0.2 / 2009-01-18 * Halting a before block should stop processing of routes [#85] * Fix redirect/halt in before filters [#85] ## 0.9.0 / 2009-01-18 * Works with and requires Rack >= 0.9.1 * Multiple Sinatra applications can now co-exist peacefully within a single process. The new "Sinatra::Base" class can be subclassed to establish a blank-slate Rack application or middleware component. Documentation on using these features is forth-coming; the following provides the basic gist: http://gist.github.com/38605 * Parameters with subscripts are now parsed into a nested/recursive Hash structure. e.g., "post[title]=Hello&post[body]=World" yields params: {'post' => {'title' => 'Hello', 'body' => 'World'}}. * Regular expressions may now be used in route pattens; captures are available at "params[:captures]". * New ":provides" route condition takes an array of mime types and matches only when an Accept request header is present with a corresponding type. [cypher] * New request-level "pass" method; immediately exits the current block and passes control to the next matching route. * The request-level "body" method now takes a block; evaluation is deferred until an attempt is made to read the body. The block must return a String or Array. * New "route conditions" system for attaching rules for when a route matches. The :agent and :host route options now use this system. * New "dump_errors" option controls whether the backtrace is dumped to rack.errors when an exception is raised from a route. The option is enabled by default for top-level apps. * Better default "app_file", "root", "public", and "views" location detection; changes to "root" and "app_file" automatically cascade to other options that depend on them. * Error mappings are now split into two distinct layers: exception mappings and custom error pages. Exception mappings are registered with "error(Exception)" and are run only when the app raises an exception. Custom error pages are registered with "error(status_code)", where "status_code" is an integer, and are run any time the response has the status code specified. It's also possible to register an error page for a range of status codes: "error(500..599)". * In-file templates are now automatically imported from the file that requires 'sinatra'. The use_in_file_templates! method is still available for loading templates from other files. * Sinatra's testing support is no longer dependent on Test::Unit. Requiring 'sinatra/test' adds the Sinatra::Test module and Sinatra::TestHarness class, which can be used with any test framework. The 'sinatra/test/unit', 'sinatra/test/spec', 'sinatra/test/rspec', or 'sinatra/test/bacon' files can be required to setup a framework-specific testing environment. See the README for more information. * Added support for Bacon (test framework). The 'sinatra/test/bacon' file can be required to setup Sinatra test helpers on Bacon::Context. * Deprecated "set_option" and "set_options"; use "set" instead. * Deprecated the "env" option ("options.env"); use "environment" instead. * Deprecated the request level "stop" method; use "halt" instead. * Deprecated the request level "entity_tag" method; use "etag" instead. Both "entity_tag" and "etag" were previously supported. * Deprecated the request level "headers" method (HTTP response headers); use "response['Header-Name']" instead. * Deprecated "Sinatra.application"; use "Sinatra::Application" instead. * Deprecated setting Sinatra.application = nil to reset an application. This should no longer be necessary. * Deprecated "Sinatra.default_options"; use "Sinatra::Default.set(key, value)" instead. * Deprecated the "ServerError" exception. All Exceptions are now treated as internal server errors and result in a 500 response status. * Deprecated the "get_it", "post_it", "put_it", "delete_it", and "head_it" test helper methods. Use "get", "post", "put", "delete", and "head", respectively, instead. * Removed Event and EventContext classes. Applications are defined in a subclass of Sinatra::Base; each request is processed within an instance. ## 0.3.3 / 2009-01-06 * Pin to Rack 0.4.0 (this is the last release on Rack 0.4) * Log unhandled exception backtraces to rack.errors. * Use RACK_ENV environment variable to establish Sinatra environment when given. Thin sets this when started with the -e argument. * BUG: raising Sinatra::NotFound resulted in a 500 response code instead of 404. * BUG: use_in_file_templates! fails with CR/LF [#45] * BUG: Sinatra detects the app file and root path when run under thin/passenger. ## 0.3.2 * BUG: Static and send_file read entire file into String before sending. Updated to stream with 8K chunks instead. * Rake tasks and assets for building basic documentation website. See http://sinatra.rubyforge.org * Various minor doc fixes. ## 0.3.1 * Unbreak optional path parameters [jeremyevans] ## 0.3.0 * Add sinatra.gemspec w/ support for github gem builds. Forks can now enable the build gem option in github to get free username-sinatra.gem builds: gem install username-sinatra.gem --source=http://gems.github.com/ * Require rack-0.4 gem; removes frozen rack dir. * Basic RSpec support; require 'sinatra/test/rspec' instead of 'sinatra/test/spec' to use. [avdi] * before filters can modify request environment vars used for routing (e.g., PATH_INFO, REQUEST_METHOD, etc.) for URL rewriting type functionality. * In-file templates now uses @@ instead of ## as template separator. * Top-level environment test predicates: development?, test?, production? * Top-level "set", "enable", and "disable" methods for tweaking app options. [rtomayko] * Top-level "use" method for building Rack middleware pipelines leading to app. See README for usage. [rtomayko] * New "reload" option - set false to disable reloading in development. * New "host" option - host/ip to bind to [cschneid] * New "app_file" option - override the file to reload in development mode [cschneid] * Development error/not_found page cleanup [sr, adamwiggins] * Remove a bunch of core extensions (String#to_param, String#from_param, Hash#from_params, Hash#to_params, Hash#symbolize_keys, Hash#pass) * Various grammar and formatting fixes to README; additions on community and contributing [cypher] * Build RDoc using Hanna template: http://sinatrarb.rubyforge.org/api * Specs, documentation and fixes for splat'n routes [vic] * Fix whitespace errors across all source files. [rtomayko] * Fix streaming issues with Mongrel (body not closed). [bmizerany] * Fix various issues with environment not being set properly (configure blocks not running, error pages not registering, etc.) [cypher] * Fix to allow locals to be passed to ERB templates [cschneid] * Fix locking issues causing random errors during reload in development. * Fix for escaped paths not resolving static files [Matthew Walker] ## 0.2.1 * File upload fix and minor tweaks. ## 0.2.0 * Initial gem release of 0.2 codebase. sinatra-3.0.5/CONTRIBUTING.md000066400000000000000000000116001434717561400154230ustar00rootroot00000000000000# Contribute Want to show Sinatra some love? Help out by contributing! ## Found a bug? Log it in our [issue tracker][ghi] or send a note to the [mailing list][ml]. Be sure to include all relevant information, like the versions of Sinatra and Ruby you're using. A [gist](http://gist.github.com/) of the code that caused the issue as well as any error messages are also very helpful. ## Need help? The [Sinatra mailing list][ml] has over 900 subscribers, many of which are happy to help out newbies or talk about potential feature additions. You can also drop by the [#sinatra](irc://chat.freenode.net/#sinatra) channel on [irc.freenode.net](http://freenode.net). ## Have a patch? Bugs and feature requests that include patches are much more likely to get attention. Here are some guidelines that will help ensure your patch can be applied as quickly as possible: 1. **Use [Git](http://git-scm.com) and [GitHub](http://github.com):** The easiest way to get setup is to fork the [sinatra/sinatra repo](http://github.com/sinatra/sinatra/). Or, the [sinatra.github.com repo](http://github.com/sinatra/sinatra.github.com/), if the patch is doc related. 2. **Write unit tests:** If you add or modify functionality, it must include unit tests. If you don't write tests, we have to, and this can hold up acceptance of the patch. 3. **Mind the `README`:** If the patch adds or modifies a major feature, modify the `README.md` file to reflect that. Again, if you don't update the `README`, we have to, and this holds up acceptance. 4. **Push it:** Once you're ready, push your changes to a topic branch and add a note to the ticket with the URL to your branch. Or, say something like, "you can find the patch on johndoe/foobranch". We also gladly accept GitHub [pull requests](http://help.github.com/pull-requests/). __NOTE:__ _We will take whatever we can get._ If you prefer to attach diffs in emails to the mailing list, that's fine; but do know that _someone_ will need to take the diff through the process described above and this can hold things up considerably. ## Want to write docs? The process for contributing to Sinatra's website, documentation or the book is the same as contributing code. We use Git for versions control and GitHub to track patch requests. * [The sinatra.github.com repo](http://github.com/sinatra/sinatra.github.com/) is where the website sources are managed. There are almost always people in `#sinatra` that are happy to discuss, apply, and publish website patches. * [The Book](http://sinatra-org-book.herokuapp.com/) has its own [Git repository](http://github.com/sinatra/sinatra-book/) and build process but is managed the same as the website and project codebase. * [Sinatra Recipes](http://recipes.sinatrarb.com/) is a community project where anyone is free to contribute ideas, recipes and tutorials. Which also has its own [Git repository](http://github.com/sinatra/sinatra-recipes). * [The Introduction](http://www.sinatrarb.com/intro.html) is generated from Sinatra's [README file](http://github.com/sinatra/sinatra/blob/master/README.md). * If you want to help translating the documentation, the README is already available in [Japanese](http://github.com/sinatra/sinatra/blob/master/README.ja.md), [German](http://github.com/sinatra/sinatra/blob/master/README.de.md), [Chinese](https://github.com/sinatra/sinatra/blob/master/README.zh.md), [Russian](https://github.com/sinatra/sinatra/blob/master/README.ru.md), [European](https://github.com/sinatra/sinatra/blob/master/README.pt-pt.md) and [Brazilian](https://github.com/sinatra/sinatra/blob/master/README.pt-br.md) Portuguese, [French](https://github.com/sinatra/sinatra/blob/master/README.fr.md), [Spanish](https://github.com/sinatra/sinatra/blob/master/README.es.md), [Korean](https://github.com/sinatra/sinatra/blob/master/README.ko.md), and [Hungarian](https://github.com/sinatra/sinatra/blob/master/README.hu.md). The translations tend to fall behind the English version. Translations into other languages would also be appreciated. ## Looking for something to do? If you'd like to help out but aren't sure how, pick something that looks interesting from the [issues][ghi] list and hack on. Make sure to leave a comment on the ticket noting that you're investigating (a simple "Taking…" is fine). [ghi]: http://github.com/sinatra/sinatra/issues [ml]: http://groups.google.com/group/sinatrarb/topics "Sinatra Mailing List" * ["Help Wanted"](https://github.com/sinatra/sinatra/labels/help%20wanted): Anyone willing to pitch in is open to contribute to this ticket as they see fit (will try to add context / summarize or ask for requirements) * ["Good First Issue"](https://github.com/sinatra/sinatra/labels/good%20first%20issue): Potential first time contributors should start here * ["Wishlist"](https://github.com/sinatra/sinatra/labels/Wishlist): All the things I wish we had but have no time for sinatra-3.0.5/Gemfile000066400000000000000000000033131434717561400144670ustar00rootroot00000000000000# frozen_string_literal: true # Why use bundler? # Well, not all development dependencies install on all rubies. Moreover, `gem # install sinatra --development` doesn't work, as it will also try to install # development dependencies of our dependencies, and those are not conflict free. # So, here we are, `bundle install`. # # If you have issues with a gem: `bundle install --without-coffee-script`. source 'https://rubygems.org' gemspec gem 'rake' rack_version = ENV['rack'].to_s rack_version = nil if rack_version.empty? || (rack_version == 'stable') rack_version = { github: 'rack/rack' } if rack_version == 'latest' gem 'rack', rack_version puma_version = ENV['puma'].to_s puma_version = nil if puma_version.empty? || (puma_version == 'stable') puma_version = { github: 'puma/puma' } if puma_version == 'latest' gem 'puma', puma_version gem 'minitest', '~> 5.0' gem 'rack-test', github: 'rack/rack-test' gem 'rubocop', '~> 1.32.0', require: false gem 'yard' gem 'rack-protection', path: 'rack-protection' gem 'sinatra-contrib', path: 'sinatra-contrib' gem 'activesupport', '~> 6.1' gem 'asciidoctor' gem 'builder' gem 'commonmarker', '~> 0.23.4', platforms: [:ruby] gem 'erubi' gem 'eventmachine' gem 'falcon', '~> 0.40', platforms: [:ruby] gem 'haml', '~> 5' gem 'kramdown' gem 'liquid' gem 'markaby' gem 'nokogiri', '> 1.5.0' gem 'pandoc-ruby', '~> 2.0.2' gem 'rabl' gem 'rainbows', platforms: [:mri] # uses #fork gem 'rdiscount', platforms: [:ruby] gem 'rdoc' gem 'redcarpet', platforms: [:ruby] gem 'simplecov', require: false gem 'slim', '~> 4' gem 'yajl-ruby', platforms: [:ruby] gem 'json', platforms: %i[jruby mri] gem 'jar-dependencies', '= 0.4.1', platforms: [:jruby] # Gem::LoadError with jar-dependencies 0.4.2 sinatra-3.0.5/LICENSE000066400000000000000000000022241434717561400142010ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2007, 2008, 2009 Blake Mizerany Copyright (c) 2010-2017 Konstantin Haase Copyright (c) 2015-2017 Zachary Scott 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. sinatra-3.0.5/MAINTENANCE.md000066400000000000000000000023741434717561400152460ustar00rootroot00000000000000# Sinatra maintenance ## Versions ### Releases The next major version of Sinatra will be released from the master branch. Each version will be tagged so it will be possible to branch of should there be a need for bug fixes and other updates. ## Issues ### New features New features will only be added to the master branch and will not be made available in point releases. ### Bug fixes Only the latest release series will receive bug fixes. When enough bugs are fixed and its deemed worthy to release a new gem, this is the branch it happens from. ### Security issues The current release series will receive patches and new versions in case of a security issue. ### Severe security issues For severe security issues we will provide new versions as above, and also the last major release series will receive patches and new versions. The classification of the security issue is judged by the core team. ### Unsupported Release Series When a release series is no longer supported, it’s your own responsibility to deal with bugs and security issues. We may provide back-ports of the fixes and publish them to git, however there will be no new versions released. If you are not comfortable maintaining your own versions, you should upgrade to a supported version. sinatra-3.0.5/README.md000066400000000000000000002147351434717561400144670ustar00rootroot00000000000000# Sinatra [![Gem Version](https://badge.fury.io/rb/sinatra.svg)](https://badge.fury.io/rb/sinatra) [![Testing](https://github.com/sinatra/sinatra/actions/workflows/test.yml/badge.svg)](https://github.com/sinatra/sinatra/actions/workflows/test.yml) Sinatra is a [DSL](https://en.wikipedia.org/wiki/Domain-specific_language) for quickly creating web applications in Ruby with minimal effort: ```ruby # myapp.rb require 'sinatra' get '/' do 'Hello world!' end ``` Install the gem: ```shell gem install sinatra gem install puma # or any other server ``` And run with: ```shell ruby myapp.rb ``` View at: [http://localhost:4567](http://localhost:4567) The code you changed will not take effect until you restart the server. Please restart the server every time you change or use a code reloader like [rerun](https://github.com/alexch/rerun) or [rack-unreloader](https://github.com/jeremyevans/rack-unreloader). It is recommended to also run `gem install puma`, which Sinatra will pick up if available. ## Table of Contents - [Sinatra](#sinatra) - [Table of Contents](#table-of-contents) - [Routes](#routes) - [Conditions](#conditions) - [Return Values](#return-values) - [Custom Route Matchers](#custom-route-matchers) - [Static Files](#static-files) - [Views / Templates](#views--templates) - [Literal Templates](#literal-templates) - [Available Template Languages](#available-template-languages) - [Haml Templates](#haml-templates) - [Erb Templates](#erb-templates) - [Builder Templates](#builder-templates) - [Nokogiri Templates](#nokogiri-templates) - [Liquid Templates](#liquid-templates) - [Markdown Templates](#markdown-templates) - [RDoc Templates](#rdoc-templates) - [AsciiDoc Templates](#asciidoc-templates) - [Markaby Templates](#markaby-templates) - [RABL Templates](#rabl-templates) - [Slim Templates](#slim-templates) - [Yajl Templates](#yajl-templates) - [Accessing Variables in Templates](#accessing-variables-in-templates) - [Templates with `yield` and nested layouts](#templates-with-yield-and-nested-layouts) - [Inline Templates](#inline-templates) - [Named Templates](#named-templates) - [Associating File Extensions](#associating-file-extensions) - [Adding Your Own Template Engine](#adding-your-own-template-engine) - [Using Custom Logic for Template Lookup](#using-custom-logic-for-template-lookup) - [Filters](#filters) - [Helpers](#helpers) - [Using Sessions](#using-sessions) - [Session Secret Security](#session-secret-security) - [Session Config](#session-config) - [Choosing Your Own Session Middleware](#choosing-your-own-session-middleware) - [Halting](#halting) - [Passing](#passing) - [Triggering Another Route](#triggering-another-route) - [Setting Body, Status Code, and Headers](#setting-body-status-code-and-headers) - [Streaming Responses](#streaming-responses) - [Logging](#logging) - [Mime Types](#mime-types) - [Generating URLs](#generating-urls) - [Browser Redirect](#browser-redirect) - [Cache Control](#cache-control) - [Sending Files](#sending-files) - [Accessing the Request Object](#accessing-the-request-object) - [Attachments](#attachments) - [Dealing with Date and Time](#dealing-with-date-and-time) - [Looking Up Template Files](#looking-up-template-files) - [Configuration](#configuration) - [Configuring attack protection](#configuring-attack-protection) - [Available Settings](#available-settings) - [Environments](#environments) - [Error Handling](#error-handling) - [Not Found](#not-found) - [Error](#error) - [Rack Middleware](#rack-middleware) - [Testing](#testing) - [Sinatra::Base - Middleware, Libraries, and Modular Apps](#sinatrabase---middleware-libraries-and-modular-apps) - [Modular vs. Classic Style](#modular-vs-classic-style) - [Serving a Modular Application](#serving-a-modular-application) - [Using a Classic Style Application with a config.ru](#using-a-classic-style-application-with-a-configru) - [When to use a config.ru?](#when-to-use-a-configru) - [Using Sinatra as Middleware](#using-sinatra-as-middleware) - [Dynamic Application Creation](#dynamic-application-creation) - [Scopes and Binding](#scopes-and-binding) - [Application/Class Scope](#applicationclass-scope) - [Request/Instance Scope](#requestinstance-scope) - [Delegation Scope](#delegation-scope) - [Command Line](#command-line) - [Multi-threading](#multi-threading) - [Requirement](#requirement) - [The Bleeding Edge](#the-bleeding-edge) - [With Bundler](#with-bundler) - [Versioning](#versioning) - [Further Reading](#further-reading) ## Routes In Sinatra, a route is an HTTP method paired with a URL-matching pattern. Each route is associated with a block: ```ruby get '/' do .. show something .. end post '/' do .. create something .. end put '/' do .. replace something .. end patch '/' do .. modify something .. end delete '/' do .. annihilate something .. end options '/' do .. appease something .. end link '/' do .. affiliate something .. end unlink '/' do .. separate something .. end ``` Routes are matched in the order they are defined. The first route that matches the request is invoked. Routes with trailing slashes are different from the ones without: ```ruby get '/foo' do # Does not match "GET /foo/" end ``` Route patterns may include named parameters, accessible via the `params` hash: ```ruby get '/hello/:name' do # matches "GET /hello/foo" and "GET /hello/bar" # params['name'] is 'foo' or 'bar' "Hello #{params['name']}!" end ``` You can also access named parameters via block parameters: ```ruby get '/hello/:name' do |n| # matches "GET /hello/foo" and "GET /hello/bar" # params['name'] is 'foo' or 'bar' # n stores params['name'] "Hello #{n}!" end ``` Route patterns may also include splat (or wildcard) parameters, accessible via the `params['splat']` array: ```ruby get '/say/*/to/*' do # matches /say/hello/to/world params['splat'] # => ["hello", "world"] end get '/download/*.*' do # matches /download/path/to/file.xml params['splat'] # => ["path/to/file", "xml"] end ``` Or with block parameters: ```ruby get '/download/*.*' do |path, ext| [path, ext] # => ["path/to/file", "xml"] end ``` Route matching with Regular Expressions: ```ruby get /\/hello\/([\w]+)/ do "Hello, #{params['captures'].first}!" end ``` Or with a block parameter: ```ruby get %r{/hello/([\w]+)} do |c| # Matches "GET /meta/hello/world", "GET /hello/world/1234" etc. "Hello, #{c}!" end ``` Route patterns may have optional parameters: ```ruby get '/posts/:format?' do # matches "GET /posts/" and any extension "GET /posts/json", "GET /posts/xml" etc end ``` Routes may also utilize query parameters: ```ruby get '/posts' do # matches "GET /posts?title=foo&author=bar" title = params['title'] author = params['author'] # uses title and author variables; query is optional to the /posts route end ``` By the way, unless you disable the path traversal attack protection (see [below](#configuring-attack-protection)), the request path might be modified before matching against your routes. You may customize the [Mustermann](https://github.com/sinatra/mustermann#readme) options used for a given route by passing in a `:mustermann_opts` hash: ```ruby get '\A/posts\z', :mustermann_opts => { :type => :regexp, :check_anchors => false } do # matches /posts exactly, with explicit anchoring "If you match an anchored pattern clap your hands!" end ``` It looks like a [condition](#conditions), but it isn't one! These options will be merged into the global `:mustermann_opts` hash described [below](#available-settings). ## Conditions Routes may include a variety of matching conditions, such as the user agent: ```ruby get '/foo', :agent => /Songbird (\d\.\d)[\d\/]*?/ do "You're using Songbird version #{params['agent'][0]}" end get '/foo' do # Matches non-songbird browsers end ``` Other available conditions are `host_name` and `provides`: ```ruby get '/', :host_name => /^admin\./ do "Admin Area, Access denied!" end get '/', :provides => 'html' do haml :index end get '/', :provides => ['rss', 'atom', 'xml'] do builder :feed end ``` `provides` searches the request's Accept header. You can easily define your own conditions: ```ruby set(:probability) { |value| condition { rand <= value } } get '/win_a_car', :probability => 0.1 do "You won!" end get '/win_a_car' do "Sorry, you lost." end ``` For a condition that takes multiple values use a splat: ```ruby set(:auth) do |*roles| # <- notice the splat here condition do unless logged_in? && roles.any? {|role| current_user.in_role? role } redirect "/login/", 303 end end end get "/my/account/", :auth => [:user, :admin] do "Your Account Details" end get "/only/admin/", :auth => :admin do "Only admins are allowed here!" end ``` ## Return Values The return value of a route block determines at least the response body passed on to the HTTP client or at least the next middleware in the Rack stack. Most commonly, this is a string, as in the above examples. But other values are also accepted. You can return an object that would either be a valid Rack response, Rack body object or HTTP status code: * An Array with three elements: `[status (Integer), headers (Hash), response body (responds to #each)]` * An Array with two elements: `[status (Integer), response body (responds to #each)]` * An object that responds to `#each` and passes nothing but strings to the given block * A Integer representing the status code That way we can, for instance, easily implement a streaming example: ```ruby class Stream def each 100.times { |i| yield "#{i}\n" } end end get('/') { Stream.new } ``` You can also use the `stream` helper method ([described below](#streaming-responses)) to reduce boilerplate and embed the streaming logic in the route. ## Custom Route Matchers As shown above, Sinatra ships with built-in support for using String patterns and regular expressions as route matches. However, it does not stop there. You can easily define your own matchers: ```ruby class AllButPattern def initialize(except) @except = except end def to_pattern(options) return self end def params(route) return {} unless @except === route end end def all_but(pattern) AllButPattern.new(pattern) end get all_but("/index") do # ... end ``` Note that the above example might be over-engineered, as it can also be expressed as: ```ruby get /.*/ do pass if request.path_info == "/index" # ... end ``` ## Static Files Static files are served from the `./public` directory. You can specify a different location by setting the `:public_folder` option: ```ruby set :public_folder, __dir__ + '/static' ``` Note that the public directory name is not included in the URL. A file `./public/css/style.css` is made available as `http://example.com/css/style.css`. Use the `:static_cache_control` setting (see [below](#cache-control)) to add `Cache-Control` header info. ## Views / Templates Each template language is exposed via its own rendering method. These methods simply return a string: ```ruby get '/' do erb :index end ``` This renders `views/index.erb`. Instead of a template name, you can also just pass in the template content directly: ```ruby get '/' do code = "<%= Time.now %>" erb code end ``` Templates take a second argument, the options hash: ```ruby get '/' do erb :index, :layout => :post end ``` This will render `views/index.erb` embedded in the `views/post.erb` (default is `views/layout.erb`, if it exists). Any options not understood by Sinatra will be passed on to the template engine: ```ruby get '/' do haml :index, :format => :html5 end ``` You can also set options per template language in general: ```ruby set :haml, :format => :html5 get '/' do haml :index end ``` Options passed to the render method override options set via `set`. Available Options:
locals
List of locals passed to the document. Handy with partials. Example: erb "<%= foo %>", :locals => {:foo => "bar"}
default_encoding
String encoding to use if uncertain. Defaults to settings.default_encoding.
views
Views folder to load templates from. Defaults to settings.views.
layout
Whether to use a layout (true or false). If it's a Symbol, specifies what template to use. Example: erb :index, :layout => !request.xhr?
content_type
Content-Type the template produces. Default depends on template language.
scope
Scope to render template under. Defaults to the application instance. If you change this, instance variables and helper methods will not be available.
layout_engine
Template engine to use for rendering the layout. Useful for languages that do not support layouts otherwise. Defaults to the engine used for the template. Example: set :rdoc, :layout_engine => :erb
layout_options
Special options only used for rendering the layout. Example: set :rdoc, :layout_options => { :views => 'views/layouts' }
Templates are assumed to be located directly under the `./views` directory. To use a different views directory: ```ruby set :views, settings.root + '/templates' ``` One important thing to remember is that you always have to reference templates with symbols, even if they're in a subdirectory (in this case, use: `:'subdir/template'` or `'subdir/template'.to_sym`). You must use a symbol because otherwise rendering methods will render any strings passed to them directly. ### Literal Templates ```ruby get '/' do haml '%div.title Hello World' end ``` Renders the template string. You can optionally specify `:path` and `:line` for a clearer backtrace if there is a filesystem path or line associated with that string: ```ruby get '/' do haml '%div.title Hello World', :path => 'examples/file.haml', :line => 3 end ``` ### Available Template Languages Some languages have multiple implementations. To specify what implementation to use (and to be thread-safe), you should simply require it first: ```ruby require 'rdiscount' get('/') { markdown :index } ``` #### Haml Templates
Dependency haml
File Extension .haml
Example haml :index, :format => :html5
#### Erb Templates
Dependency erubi or erb (included in Ruby)
File Extensions .erb, .rhtml or .erubi (Erubi only)
Example erb :index
#### Builder Templates
Dependency builder
File Extension .builder
Example builder { |xml| xml.em "hi" }
It also takes a block for inline templates (see [example](#inline-templates)). #### Nokogiri Templates
Dependency nokogiri
File Extension .nokogiri
Example nokogiri { |xml| xml.em "hi" }
It also takes a block for inline templates (see [example](#inline-templates)). #### Liquid Templates
Dependency liquid
File Extension .liquid
Example liquid :index, :locals => { :key => 'value' }
Since you cannot call Ruby methods (except for `yield`) from a Liquid template, you almost always want to pass locals to it. #### Markdown Templates
Dependency Anyone of: RDiscount, RedCarpet, kramdown, commonmarker pandoc
File Extensions .markdown, .mkd and .md
Example markdown :index, :layout_engine => :erb
It is not possible to call methods from Markdown, nor to pass locals to it. You therefore will usually use it in combination with another rendering engine: ```ruby erb :overview, :locals => { :text => markdown(:introduction) } ``` Note that you may also call the `markdown` method from within other templates: ```ruby %h1 Hello From Haml! %p= markdown(:greetings) ``` Since you cannot call Ruby from Markdown, you cannot use layouts written in Markdown. However, it is possible to use another rendering engine for the template than for the layout by passing the `:layout_engine` option. #### RDoc Templates
Dependency RDoc
File Extension .rdoc
Example rdoc :README, :layout_engine => :erb
It is not possible to call methods from RDoc, nor to pass locals to it. You therefore will usually use it in combination with another rendering engine: ```ruby erb :overview, :locals => { :text => rdoc(:introduction) } ``` Note that you may also call the `rdoc` method from within other templates: ```ruby %h1 Hello From Haml! %p= rdoc(:greetings) ``` Since you cannot call Ruby from RDoc, you cannot use layouts written in RDoc. However, it is possible to use another rendering engine for the template than for the layout by passing the `:layout_engine` option. #### AsciiDoc Templates
Dependency Asciidoctor
File Extension .asciidoc, .adoc and .ad
Example asciidoc :README, :layout_engine => :erb
Since you cannot call Ruby methods directly from an AsciiDoc template, you almost always want to pass locals to it. #### Markaby Templates
Dependency Markaby
File Extension .mab
Example markaby { h1 "Welcome!" }
It also takes a block for inline templates (see [example](#inline-templates)). #### RABL Templates
Dependency Rabl
File Extension .rabl
Example rabl :index
#### Slim Templates
Dependency Slim Lang
File Extension .slim
Example slim :index
#### Yajl Templates
Dependency yajl-ruby
File Extension .yajl
Example yajl :index, :locals => { :key => 'qux' }, :callback => 'present', :variable => 'resource'
The template source is evaluated as a Ruby string, and the resulting json variable is converted using `#to_json`: ```ruby json = { :foo => 'bar' } json[:baz] = key ``` The `:callback` and `:variable` options can be used to decorate the rendered object: ```javascript var resource = {"foo":"bar","baz":"qux"}; present(resource); ``` ### Accessing Variables in Templates Templates are evaluated within the same context as route handlers. Instance variables set in route handlers are directly accessible by templates: ```ruby get '/:id' do @foo = Foo.find(params['id']) haml '%h1= @foo.name' end ``` Or, specify an explicit Hash of local variables: ```ruby get '/:id' do foo = Foo.find(params['id']) haml '%h1= bar.name', :locals => { :bar => foo } end ``` This is typically used when rendering templates as partials from within other templates. ### Templates with `yield` and nested layouts A layout is usually just a template that calls `yield`. Such a template can be used either through the `:template` option as described above, or it can be rendered with a block as follows: ```ruby erb :post, :layout => false do erb :index end ``` This code is mostly equivalent to `erb :index, :layout => :post`. Passing blocks to rendering methods is most useful for creating nested layouts: ```ruby erb :main_layout, :layout => false do erb :admin_layout do erb :user end end ``` This can also be done in fewer lines of code with: ```ruby erb :admin_layout, :layout => :main_layout do erb :user end ``` Currently, the following rendering methods accept a block: `erb`, `haml`, `liquid`, `slim `. Also, the general `render` method accepts a block. ### Inline Templates Templates may be defined at the end of the source file: ```ruby require 'sinatra' get '/' do haml :index end __END__ @@ layout %html != yield @@ index %div.title Hello world. ``` NOTE: Inline templates defined in the source file that requires Sinatra are automatically loaded. Call `enable :inline_templates` explicitly if you have inline templates in other source files. ### Named Templates Templates may also be defined using the top-level `template` method: ```ruby template :layout do "%html\n =yield\n" end template :index do '%div.title Hello World!' end get '/' do haml :index end ``` If a template named "layout" exists, it will be used each time a template is rendered. You can individually disable layouts by passing `:layout => false` or disable them by default via `set :haml, :layout => false`: ```ruby get '/' do haml :index, :layout => !request.xhr? end ``` ### Associating File Extensions To associate a file extension with a template engine, use `Tilt.register`. For instance, if you like to use the file extension `tt` for Haml templates, you can do the following: ```ruby Tilt.register Tilt[:haml], :tt ``` ### Adding Your Own Template Engine First, register your engine with Tilt, then create a rendering method: ```ruby Tilt.register MyAwesomeTemplateEngine, :myat helpers do def myat(*args) render(:myat, *args) end end get '/' do myat :index end ``` Renders `./views/index.myat`. Learn more about [Tilt](https://github.com/rtomayko/tilt#readme). ### Using Custom Logic for Template Lookup To implement your own template lookup mechanism you can write your own `#find_template` method: ```ruby configure do set :views, [ './views/a', './views/b' ] end def find_template(views, name, engine, &block) Array(views).each do |v| super(v, name, engine, &block) end end ``` ## Filters Before filters are evaluated before each request within the same context as the routes will be and can modify the request and response. Instance variables set in filters are accessible by routes and templates: ```ruby before do @note = 'Hi!' request.path_info = '/foo/bar/baz' end get '/foo/*' do @note #=> 'Hi!' params['splat'] #=> 'bar/baz' end ``` After filters are evaluated after each request within the same context as the routes will be and can also modify the request and response. Instance variables set in before filters and routes are accessible by after filters: ```ruby after do puts response.status end ``` Note: Unless you use the `body` method rather than just returning a String from the routes, the body will not yet be available in the after filter, since it is generated later on. Filters optionally take a pattern, causing them to be evaluated only if the request path matches that pattern: ```ruby before '/protected/*' do authenticate! end after '/create/:slug' do |slug| session[:last_slug] = slug end ``` Like routes, filters also take conditions: ```ruby before :agent => /Songbird/ do # ... end after '/blog/*', :host_name => 'example.com' do # ... end ``` ## Helpers Use the top-level `helpers` method to define helper methods for use in route handlers and templates: ```ruby helpers do def bar(name) "#{name}bar" end end get '/:name' do bar(params['name']) end ``` Alternatively, helper methods can be separately defined in a module: ```ruby module FooUtils def foo(name) "#{name}foo" end end module BarUtils def bar(name) "#{name}bar" end end helpers FooUtils, BarUtils ``` The effect is the same as including the modules in the application class. ### Using Sessions A session is used to keep state during requests. If activated, you have one session hash per user session: ```ruby enable :sessions get '/' do "value = " << session[:value].inspect end get '/:value' do session['value'] = params['value'] end ``` #### Session Secret Security To improve security, the session data in the cookie is signed with a session secret using `HMAC-SHA1`. This session secret should optimally be a cryptographically secure random value of an appropriate length which for `HMAC-SHA1` is greater than or equal to 64 bytes (512 bits, 128 hex characters). You would be advised not to use a secret that is less than 32 bytes of randomness (256 bits, 64 hex characters). It is therefore **very important** that you don't just make the secret up, but instead use a secure random number generator to create it. Humans are extremely bad at generating random values. By default, a 32 byte secure random session secret is generated for you by Sinatra, but it will change with every restart of your application. If you have multiple instances of your application, and you let Sinatra generate the key, each instance would then have a different session key which is probably not what you want. For better security and usability it's [recommended](https://12factor.net/config) that you generate a secure random secret and store it in an environment variable on each host running your application so that all of your application instances will share the same secret. You should periodically rotate this session secret to a new value. Here are some examples of how you might create a 64-byte secret and set it: **Session Secret Generation** ```text $ ruby -e "require 'securerandom'; puts SecureRandom.hex(64)" 99ae8af...snip...ec0f262ac ``` **Session Secret Generation (Bonus Points)** Use the [sysrandom gem](https://github.com/cryptosphere/sysrandom#readme) to use the system RNG facilities to generate random values instead of userspace `OpenSSL` which MRI Ruby currently defaults to: ```text $ gem install sysrandom Building native extensions. This could take a while... Successfully installed sysrandom-1.x 1 gem installed $ ruby -e "require 'sysrandom/securerandom'; puts SecureRandom.hex(64)" 99ae8af...snip...ec0f262ac ``` **Session Secret Environment Variable** Set a `SESSION_SECRET` environment variable for Sinatra to the value you generated. Make this value persistent across reboots of your host. Since the method for doing this will vary across systems this is for illustrative purposes only: ```bash # echo "export SESSION_SECRET=99ae8af...snip...ec0f262ac" >> ~/.bashrc ``` **Session Secret App Config** Set up your app config to fail-safe to a secure random secret if the `SESSION_SECRET` environment variable is not available. For bonus points use the [sysrandom gem](https://github.com/cryptosphere/sysrandom#readme) here as well: ```ruby require 'securerandom' # -or- require 'sysrandom/securerandom' set :session_secret, ENV.fetch('SESSION_SECRET') { SecureRandom.hex(64) } ``` #### Session Config If you want to configure it further, you may also store a hash with options in the `sessions` setting: ```ruby set :sessions, :domain => 'foo.com' ``` To share your session across other apps on subdomains of foo.com, prefix the domain with a *.* like this instead: ```ruby set :sessions, :domain => '.foo.com' ``` #### Choosing Your Own Session Middleware Note that `enable :sessions` actually stores all data in a cookie. This might not always be what you want (storing lots of data will increase your traffic, for instance). You can use any Rack session middleware in order to do so, one of the following methods can be used: ```ruby enable :sessions set :session_store, Rack::Session::Pool ``` Or to set up sessions with a hash of options: ```ruby set :sessions, :expire_after => 2592000 set :session_store, Rack::Session::Pool ``` Another option is to **not** call `enable :sessions`, but instead pull in your middleware of choice as you would any other middleware. It is important to note that when using this method, session based protection **will not be enabled by default**. The Rack middleware to do that will also need to be added: ```ruby use Rack::Session::Pool, :expire_after => 2592000 use Rack::Protection::RemoteToken use Rack::Protection::SessionHijacking ``` See '[Configuring attack protection](#configuring-attack-protection)' for more information. ### Halting To immediately stop a request within a filter or route use: ```ruby halt ``` You can also specify the status when halting: ```ruby halt 410 ``` Or the body: ```ruby halt 'this will be the body' ``` Or both: ```ruby halt 401, 'go away!' ``` With headers: ```ruby halt 402, {'Content-Type' => 'text/plain'}, 'revenge' ``` It is of course possible to combine a template with `halt`: ```ruby halt erb(:error) ``` ### Passing A route can punt processing to the next matching route using `pass`: ```ruby get '/guess/:who' do pass unless params['who'] == 'Frank' 'You got me!' end get '/guess/*' do 'You missed!' end ``` The route block is immediately exited and control continues with the next matching route. If no matching route is found, a 404 is returned. ### Triggering Another Route Sometimes `pass` is not what you want, instead, you would like to get the result of calling another route. Simply use `call` to achieve this: ```ruby get '/foo' do status, headers, body = call env.merge("PATH_INFO" => '/bar') [status, headers, body.map(&:upcase)] end get '/bar' do "bar" end ``` Note that in the example above, you would ease testing and increase performance by simply moving `"bar"` into a helper used by both `/foo` and `/bar`. If you want the request to be sent to the same application instance rather than a duplicate, use `call!` instead of `call`. Check out the Rack specification if you want to learn more about `call`. ### Setting Body, Status Code, and Headers It is possible and recommended to set the status code and response body with the return value of the route block. However, in some scenarios, you might want to set the body at an arbitrary point in the execution flow. You can do so with the `body` helper method. If you do so, you can use that method from thereon to access the body: ```ruby get '/foo' do body "bar" end after do puts body end ``` It is also possible to pass a block to `body`, which will be executed by the Rack handler (this can be used to implement streaming, [see "Return Values"](#return-values)). Similar to the body, you can also set the status code and headers: ```ruby get '/foo' do status 418 headers \ "Allow" => "BREW, POST, GET, PROPFIND, WHEN", "Refresh" => "Refresh: 20; https://ietf.org/rfc/rfc2324.txt" body "I'm a teapot!" end ``` Like `body`, `headers` and `status` with no arguments can be used to access their current values. ### Streaming Responses Sometimes you want to start sending out data while still generating parts of the response body. In extreme examples, you want to keep sending data until the client closes the connection. You can use the `stream` helper to avoid creating your own wrapper: ```ruby get '/' do stream do |out| out << "It's gonna be legen -\n" sleep 0.5 out << " (wait for it) \n" sleep 1 out << "- dary!\n" end end ``` This allows you to implement streaming APIs, [Server Sent Events](https://w3c.github.io/eventsource/), and can be used as the basis for [WebSockets](https://en.wikipedia.org/wiki/WebSocket). It can also be used to increase throughput if some but not all content depends on a slow resource. Note that the streaming behavior, especially the number of concurrent requests, highly depends on the webserver used to serve the application. Some servers might not even support streaming at all. If the server does not support streaming, the body will be sent all at once after the block passed to `stream` finishes executing. Streaming does not work at all with Shotgun. If the optional parameter is set to `keep_open`, it will not call `close` on the stream object, allowing you to close it at any later point in the execution flow. This only works on evented servers, like Rainbows. Other servers will still close the stream: ```ruby # config.ru require 'sinatra/base' class App < Sinatra::Base connections = [] get '/subscribe', provides: 'text/event-stream' do # register a client's interest in server events stream(:keep_open) do |out| connections << out # purge dead connections connections.reject!(&:closed?) end end post '/' do connections.each do |out| # notify client that a new message has arrived out << "data: #{params[:msg]}\n\n" # indicate client to connect again out.close end 204 # response without entity body end end run App ``` ```ruby # rainbows.conf Rainbows! do use :EventMachine end ```` Run: ```shell rainbows -c rainbows.conf ``` It's also possible for the client to close the connection when trying to write to the socket. Because of this, it's recommended to check `out.closed?` before trying to write. ### Logging In the request scope, the `logger` helper exposes a `Logger` instance: ```ruby get '/' do logger.info "loading data" # ... end ``` This logger will automatically take your Rack handler's logging settings into account. If logging is disabled, this method will return a dummy object, so you do not have to worry about it in your routes and filters. Note that logging is only enabled for `Sinatra::Application` by default, so if you inherit from `Sinatra::Base`, you probably want to enable it yourself: ```ruby class MyApp < Sinatra::Base configure :production, :development do enable :logging end end ``` To avoid any logging middleware to be set up, set the `logging` option to `nil`. However, keep in mind that `logger` will in that case return `nil`. A common use case is when you want to set your own logger. Sinatra will use whatever it will find in `env['rack.logger']`. ### Mime Types When using `send_file` or static files you may have mime types Sinatra doesn't understand. Use `mime_type` to register them by file extension: ```ruby configure do mime_type :foo, 'text/foo' end ``` You can also use it with the `content_type` helper: ```ruby get '/' do content_type :foo "foo foo foo" end ``` ### Generating URLs For generating URLs you should use the `url` helper method, for instance, in Haml: ```ruby %a{:href => url('/foo')} foo ``` It takes reverse proxies and Rack routers into account - if present. This method is also aliased to `to` (see [below](#browser-redirect) for an example). ### Browser Redirect You can trigger a browser redirect with the `redirect` helper method: ```ruby get '/foo' do redirect to('/bar') end ``` Any additional parameters are handled like arguments passed to `halt`: ```ruby redirect to('/bar'), 303 redirect 'http://www.google.com/', 'wrong place, buddy' ``` You can also easily redirect back to the page the user came from with `redirect back`: ```ruby get '/foo' do "do something" end get '/bar' do do_something redirect back end ``` To pass arguments with a redirect, either add them to the query: ```ruby redirect to('/bar?sum=42') ``` Or use a session: ```ruby enable :sessions get '/foo' do session[:secret] = 'foo' redirect to('/bar') end get '/bar' do session[:secret] end ``` ### Cache Control Setting your headers correctly is the foundation for proper HTTP caching. You can easily set the Cache-Control header like this: ```ruby get '/' do cache_control :public "cache it!" end ``` Pro tip: Set up caching in a before filter: ```ruby before do cache_control :public, :must_revalidate, :max_age => 60 end ``` If you are using the `expires` helper to set the corresponding header, `Cache-Control` will be set automatically for you: ```ruby before do expires 500, :public, :must_revalidate end ``` To properly use caches, you should consider using `etag` or `last_modified`. It is recommended to call those helpers *before* doing any heavy lifting, as they will immediately flush a response if the client already has the current version in its cache: ```ruby get "/article/:id" do @article = Article.find params['id'] last_modified @article.updated_at etag @article.sha1 erb :article end ``` It is also possible to use a [weak ETag](https://en.wikipedia.org/wiki/HTTP_ETag#Strong_and_weak_validation): ```ruby etag @article.sha1, :weak ``` These helpers will not do any caching for you, but rather feed the necessary information to your cache. If you are looking for a quick reverse-proxy caching solution, try [rack-cache](https://github.com/rtomayko/rack-cache#readme): ```ruby require "rack/cache" require "sinatra" use Rack::Cache get '/' do cache_control :public, :max_age => 36000 sleep 5 "hello" end ``` Use the `:static_cache_control` setting (see [below](#cache-control)) to add `Cache-Control` header info to static files. According to RFC 2616, your application should behave differently if the If-Match or If-None-Match header is set to `*`, depending on whether the resource requested is already in existence. Sinatra assumes resources for safe (like get) and idempotent (like put) requests are already in existence, whereas other resources (for instance post requests) are treated as new resources. You can change this behavior by passing in a `:new_resource` option: ```ruby get '/create' do etag '', :new_resource => true Article.create erb :new_article end ``` If you still want to use a weak ETag, pass in a `:kind` option: ```ruby etag '', :new_resource => true, :kind => :weak ``` ### Sending Files To return the contents of a file as the response, you can use the `send_file` helper method: ```ruby get '/' do send_file 'foo.png' end ``` It also takes options: ```ruby send_file 'foo.png', :type => :jpg ``` The options are:
filename
File name to be used in the response, defaults to the real file name.
last_modified
Value for Last-Modified header, defaults to the file's mtime.
type
Value for Content-Type header, guessed from the file extension if missing.
disposition
Value for Content-Disposition header, possible values: nil (default), :attachment and :inline
length
Value for Content-Length header, defaults to file size.
status
Status code to be sent. Useful when sending a static file as an error page. If supported by the Rack handler, other means than streaming from the Ruby process will be used. If you use this helper method, Sinatra will automatically handle range requests.
### Accessing the Request Object The incoming request object can be accessed from request level (filter, routes, error handlers) through the `request` method: ```ruby # app running on http://example.com/example get '/foo' do t = %w[text/css text/html application/javascript] request.accept # ['text/html', '*/*'] request.accept? 'text/xml' # true request.preferred_type(t) # 'text/html' request.body # request body sent by the client (see below) request.scheme # "http" request.script_name # "/example" request.path_info # "/foo" request.port # 80 request.request_method # "GET" request.query_string # "" request.content_length # length of request.body request.media_type # media type of request.body request.host # "example.com" request.get? # true (similar methods for other verbs) request.form_data? # false request["some_param"] # value of some_param parameter. [] is a shortcut to the params hash. request.referrer # the referrer of the client or '/' request.user_agent # user agent (used by :agent condition) request.cookies # hash of browser cookies request.xhr? # is this an ajax request? request.url # "http://example.com/example/foo" request.path # "/example/foo" request.ip # client IP address request.secure? # false (would be true over ssl) request.forwarded? # true (if running behind a reverse proxy) request.env # raw env hash handed in by Rack end ``` Some options, like `script_name` or `path_info`, can also be written: ```ruby before { request.path_info = "/" } get "/" do "all requests end up here" end ``` The `request.body` is an IO or StringIO object: ```ruby post "/api" do request.body.rewind # in case someone already read it data = JSON.parse request.body.read "Hello #{data['name']}!" end ``` ### Attachments You can use the `attachment` helper to tell the browser the response should be stored on disk rather than displayed in the browser: ```ruby get '/' do attachment "store it!" end ``` You can also pass it a file name: ```ruby get '/' do attachment "info.txt" "store it!" end ``` ### Dealing with Date and Time Sinatra offers a `time_for` helper method that generates a Time object from the given value. It is also able to convert `DateTime`, `Date` and similar classes: ```ruby get '/' do pass if Time.now > time_for('Dec 23, 2016') "still time" end ``` This method is used internally by `expires`, `last_modified` and akin. You can therefore easily extend the behavior of those methods by overriding `time_for` in your application: ```ruby helpers do def time_for(value) case value when :yesterday then Time.now - 24*60*60 when :tomorrow then Time.now + 24*60*60 else super end end end get '/' do last_modified :yesterday expires :tomorrow "hello" end ``` ### Looking Up Template Files The `find_template` helper is used to find template files for rendering: ```ruby find_template settings.views, 'foo', Tilt[:haml] do |file| puts "could be #{file}" end ``` This is not really useful. But it is useful that you can actually override this method to hook in your own lookup mechanism. For instance, if you want to be able to use more than one view directory: ```ruby set :views, ['views', 'templates'] helpers do def find_template(views, name, engine, &block) Array(views).each { |v| super(v, name, engine, &block) } end end ``` Another example would be using different directories for different engines: ```ruby set :views, :haml => 'templates', :default => 'views' helpers do def find_template(views, name, engine, &block) _, folder = views.detect { |k,v| engine == Tilt[k] } folder ||= views[:default] super(folder, name, engine, &block) end end ``` You can also easily wrap this up in an extension and share it with others! Note that `find_template` does not check if the file really exists but rather calls the given block for all possible paths. This is not a performance issue, since `render` will use `break` as soon as a file is found. Also, template locations (and content) will be cached if you are not running in development mode. You should keep that in mind if you write a really crazy method. ## Configuration Run once, at startup, in any environment: ```ruby configure do # setting one option set :option, 'value' # setting multiple options set :a => 1, :b => 2 # same as `set :option, true` enable :option # same as `set :option, false` disable :option # you can also have dynamic settings with blocks set(:css_dir) { File.join(views, 'css') } end ``` Run only when the environment (`APP_ENV` environment variable) is set to `:production`: ```ruby configure :production do ... end ``` Run when the environment is set to either `:production` or `:test`: ```ruby configure :production, :test do ... end ``` You can access those options via `settings`: ```ruby configure do set :foo, 'bar' end get '/' do settings.foo? # => true settings.foo # => 'bar' ... end ``` ### Configuring attack protection Sinatra is using [Rack::Protection](https://github.com/sinatra/sinatra/tree/master/rack-protection#readme) to defend your application against common, opportunistic attacks. You can easily disable this behavior (which will open up your application to tons of common vulnerabilities): ```ruby disable :protection ``` To skip a single defense layer, set `protection` to an options hash: ```ruby set :protection, :except => :path_traversal ``` You can also hand in an array in order to disable a list of protections: ```ruby set :protection, :except => [:path_traversal, :session_hijacking] ``` By default, Sinatra will only set up session based protection if `:sessions` have been enabled. See '[Using Sessions](#using-sessions)'. Sometimes you may want to set up sessions "outside" of the Sinatra app, such as in the config.ru or with a separate `Rack::Builder` instance. In that case, you can still set up session based protection by passing the `:session` option: ```ruby set :protection, :session => true ``` ### Available Settings
absolute_redirects
If disabled, Sinatra will allow relative redirects, however, Sinatra will no longer conform with RFC 2616 (HTTP 1.1), which only allows absolute redirects.
Enable if your app is running behind a reverse proxy that has not been set up properly. Note that the url helper will still produce absolute URLs, unless you pass in false as the second parameter.
Disabled by default.
add_charset
Mime types the content_type helper will automatically add the charset info to. You should add to it rather than overriding this option: settings.add_charset << "application/foobar"
app_file
Path to the main application file, used to detect project root, views and public folder and inline templates.
bind
IP address to bind to (default: 0.0.0.0 or localhost if your `environment` is set to development). Only used for built-in server.
default_content_type
Content-Type to assume if unknown (defaults to "text/html"). Set to nil to not set a default Content-Type on every response; when configured so, you must set the Content-Type manually when emitting content or the user-agent will have to sniff it (or, if nosniff is enabled in Rack::Protection::XSSHeader, assume application/octet-stream).
default_encoding
Encoding to assume if unknown (defaults to "utf-8").
dump_errors
Display errors in the log. Enabled by default unless environment is "test".
environment
Current environment. Defaults to ENV['APP_ENV'], or "development" if not available.
logging
Use the logger.
lock
Places a lock around every request, only running processing on request per Ruby process concurrently.
Enabled if your app is not thread-safe. Disabled by default.
method_override
Use _method magic to allow put/delete forms in browsers that don't support it.
mustermann_opts
A default hash of options to pass to Mustermann.new when compiling routing paths.
port
Port to listen on. Only used for built-in server.
prefixed_redirects
Whether or not to insert request.script_name into redirects if no absolute path is given. That way redirect '/foo' would behave like redirect to('/foo'). Disabled by default.
protection
Whether or not to enable web attack protections. See protection section above.
public_dir
Alias for public_folder. See below.
public_folder
Path to the folder public files are served from. Only used if static file serving is enabled (see static setting below). Inferred from app_file setting if not set.
quiet
Disables logs generated by Sinatra's start and stop commands. false by default.
reload_templates
Whether or not to reload templates between requests. Enabled in development mode.
root
Path to project root folder. Inferred from app_file setting if not set.
raise_errors
Raise exceptions (will stop application). Enabled by default when environment is set to "test", disabled otherwise.
run
If enabled, Sinatra will handle starting the web server. Do not enable if using rackup or other means.
running
Is the built-in server running now? Do not change this setting!
server
Server or list of servers to use for built-in server. Order indicates priority, default depends on Ruby implementation.
server_settings
If you are using a WEBrick web server, presumably for your development environment, you can pass a hash of options to server_settings, such as SSLEnable or SSLVerifyClient. However, web servers such as Puma do not support this, so you can set server_settings by defining it as a method when you call configure.
sessions
Enable cookie-based sessions support using Rack::Session::Cookie. See 'Using Sessions' section for more information.
session_store
The Rack session middleware used. Defaults to Rack::Session::Cookie. See 'Using Sessions' section for more information.
show_exceptions
Show a stack trace in the browser when an exception happens. Enabled by default when environment is set to "development", disabled otherwise.
Can also be set to :after_handler to trigger app-specified error handling before showing a stack trace in the browser.
static
Whether Sinatra should handle serving static files.
Disable when using a server able to do this on its own.
Disabling will boost performance.
Enabled by default in classic style, disabled for modular apps.
static_cache_control
When Sinatra is serving static files, set this to add Cache-Control headers to the responses. Uses the cache_control helper. Disabled by default.
Use an explicit array when setting multiple values: set :static_cache_control, [:public, :max_age => 300]
threaded
If set to true, will tell server to use EventMachine.defer for processing the request.
traps
Whether Sinatra should handle system signals.
views
Path to the views folder. Inferred from app_file setting if not set.
x_cascade
Whether or not to set the X-Cascade header if no route matches. Defaults to true.
## Environments There are three predefined `environments`: `"development"`, `"production"` and `"test"`. Environments can be set through the `APP_ENV` environment variable. The default value is `"development"`. In the `"development"` environment all templates are reloaded between requests, and special `not_found` and `error` handlers display stack traces in your browser. In the `"production"` and `"test"` environments, templates are cached by default. To run different environments, set the `APP_ENV` environment variable: ```shell APP_ENV=production ruby my_app.rb ``` You can use predefined methods: `development?`, `test?` and `production?` to check the current environment setting: ```ruby get '/' do if settings.development? "development!" else "not development!" end end ``` ## Error Handling Error handlers run within the same context as routes and before filters, which means you get all the goodies it has to offer, like `haml`, `erb`, `halt`, etc. ### Not Found When a `Sinatra::NotFound` exception is raised, or the response's status code is 404, the `not_found` handler is invoked: ```ruby not_found do 'This is nowhere to be found.' end ``` ### Error The `error` handler is invoked any time an exception is raised from a route block or a filter. But note in development it will only run if you set the show exceptions option to `:after_handler`: ```ruby set :show_exceptions, :after_handler ``` The exception object can be obtained from the `sinatra.error` Rack variable: ```ruby error do 'Sorry there was a nasty error - ' + env['sinatra.error'].message end ``` Custom errors: ```ruby error MyCustomError do 'So what happened was...' + env['sinatra.error'].message end ``` Then, if this happens: ```ruby get '/' do raise MyCustomError, 'something bad' end ``` You get this: ``` So what happened was... something bad ``` Alternatively, you can install an error handler for a status code: ```ruby error 403 do 'Access forbidden' end get '/secret' do 403 end ``` Or a range: ```ruby error 400..510 do 'Boom' end ``` Sinatra installs special `not_found` and `error` handlers when running under the development environment to display nice stack traces and additional debugging information in your browser. ## Rack Middleware Sinatra rides on [Rack](https://rack.github.io/), a minimal standard interface for Ruby web frameworks. One of Rack's most interesting capabilities for application developers is support for "middleware" -- components that sit between the server and your application monitoring and/or manipulating the HTTP request/response to provide various types of common functionality. Sinatra makes building Rack middleware pipelines a cinch via a top-level `use` method: ```ruby require 'sinatra' require 'my_custom_middleware' use Rack::Lint use MyCustomMiddleware get '/hello' do 'Hello World' end ``` The semantics of `use` are identical to those defined for the [Rack::Builder](http://www.rubydoc.info/github/rack/rack/master/Rack/Builder) DSL (most frequently used from rackup files). For example, the `use` method accepts multiple/variable args as well as blocks: ```ruby use Rack::Auth::Basic do |username, password| username == 'admin' && password == 'secret' end ``` Rack is distributed with a variety of standard middleware for logging, debugging, URL routing, authentication, and session handling. Sinatra uses many of these components automatically based on configuration so you typically don't have to `use` them explicitly. You can find useful middleware in [rack](https://github.com/rack/rack/tree/master/lib/rack), [rack-contrib](https://github.com/rack/rack-contrib#readme), or in the [Rack wiki](https://github.com/rack/rack/wiki/List-of-Middleware). ## Testing Sinatra tests can be written using any Rack-based testing library or framework. [Rack::Test](http://www.rubydoc.info/github/brynary/rack-test/master/frames) is recommended: ```ruby require 'my_sinatra_app' require 'minitest/autorun' require 'rack/test' class MyAppTest < Minitest::Test include Rack::Test::Methods def app Sinatra::Application end def test_my_default get '/' assert_equal 'Hello World!', last_response.body end def test_with_params get '/meet', :name => 'Frank' assert_equal 'Hello Frank!', last_response.body end def test_with_user_agent get '/', {}, 'HTTP_USER_AGENT' => 'Songbird' assert_equal "You're using Songbird!", last_response.body end end ``` Note: If you are using Sinatra in the modular style, replace `Sinatra::Application` above with the class name of your app. ## Sinatra::Base - Middleware, Libraries, and Modular Apps Defining your app at the top-level works well for micro-apps but has considerable drawbacks when building reusable components such as Rack middleware, Rails metal, simple libraries with a server component, or even Sinatra extensions. The top-level assumes a micro-app style configuration (e.g., a single application file, `./public` and `./views` directories, logging, exception detail page, etc.). That's where `Sinatra::Base` comes into play: ```ruby require 'sinatra/base' class MyApp < Sinatra::Base set :sessions, true set :foo, 'bar' get '/' do 'Hello world!' end end ``` The methods available to `Sinatra::Base` subclasses are exactly the same as those available via the top-level DSL. Most top-level apps can be converted to `Sinatra::Base` components with two modifications: * Your file should require `sinatra/base` instead of `sinatra`; otherwise, all of Sinatra's DSL methods are imported into the main namespace. * Put your app's routes, error handlers, filters, and options in a subclass of `Sinatra::Base`. `Sinatra::Base` is a blank slate. Most options are disabled by default, including the built-in server. See [Configuring Settings](http://www.sinatrarb.com/configuration.html) for details on available options and their behavior. If you want behavior more similar to when you define your app at the top level (also known as Classic style), you can subclass `Sinatra::Application`: ```ruby require 'sinatra/base' class MyApp < Sinatra::Application get '/' do 'Hello world!' end end ``` ### Modular vs. Classic Style Contrary to common belief, there is nothing wrong with the classic style. If it suits your application, you do not have to switch to a modular application. The main disadvantage of using the classic style rather than the modular style is that you will only have one Sinatra application per Ruby process. If you plan to use more than one, switch to the modular style. There is no reason you cannot mix the modular and classic styles. If switching from one style to the other, you should be aware of slightly different default settings:
Setting Classic Modular Modular
app_file file loading sinatra file subclassing Sinatra::Base file subclassing Sinatra::Application
run $0 == app_file false false
logging true false true
method_override true false true
inline_templates true false true
static true File.exist?(public_folder) true
### Serving a Modular Application There are two common options for starting a modular app, actively starting with `run!`: ```ruby # my_app.rb require 'sinatra/base' class MyApp < Sinatra::Base # ... app code here ... # start the server if ruby file executed directly run! if app_file == $0 end ``` Start with: ```shell ruby my_app.rb ``` Or with a `config.ru` file, which allows using any Rack handler: ```ruby # config.ru (run with rackup) require './my_app' run MyApp ``` Run: ```shell rackup -p 4567 ``` ### Using a Classic Style Application with a config.ru Write your app file: ```ruby # app.rb require 'sinatra' get '/' do 'Hello world!' end ``` And a corresponding `config.ru`: ```ruby require './app' run Sinatra::Application ``` ### When to use a config.ru? A `config.ru` file is recommended if: * You want to deploy with a different Rack handler (Passenger, Unicorn, Heroku, ...). * You want to use more than one subclass of `Sinatra::Base`. * You want to use Sinatra only for middleware, and not as an endpoint. **There is no need to switch to a `config.ru` simply because you switched to the modular style, and you don't have to use the modular style for running with a `config.ru`.** ### Using Sinatra as Middleware Not only is Sinatra able to use other Rack middleware, any Sinatra application can, in turn, be added in front of any Rack endpoint as middleware itself. This endpoint could be another Sinatra application, or any other Rack-based application (Rails/Hanami/Roda/...): ```ruby require 'sinatra/base' class LoginScreen < Sinatra::Base enable :sessions get('/login') { haml :login } post('/login') do if params['name'] == 'admin' && params['password'] == 'admin' session['user_name'] = params['name'] else redirect '/login' end end end class MyApp < Sinatra::Base # middleware will run before filters use LoginScreen before do unless session['user_name'] halt "Access denied, please login." end end get('/') { "Hello #{session['user_name']}." } end ``` ### Dynamic Application Creation Sometimes you want to create new applications at runtime without having to assign them to a constant. You can do this with `Sinatra.new`: ```ruby require 'sinatra/base' my_app = Sinatra.new { get('/') { "hi" } } my_app.run! ``` It takes the application to inherit from as an optional argument: ```ruby # config.ru (run with rackup) require 'sinatra/base' controller = Sinatra.new do enable :logging helpers MyHelpers end map('/a') do run Sinatra.new(controller) { get('/') { 'a' } } end map('/b') do run Sinatra.new(controller) { get('/') { 'b' } } end ``` This is especially useful for testing Sinatra extensions or using Sinatra in your own library. This also makes using Sinatra as middleware extremely easy: ```ruby require 'sinatra/base' use Sinatra do get('/') { ... } end run RailsProject::Application ``` ## Scopes and Binding The scope you are currently in determines what methods and variables are available. ### Application/Class Scope Every Sinatra application corresponds to a subclass of `Sinatra::Base`. If you are using the top-level DSL (`require 'sinatra'`), then this class is `Sinatra::Application`, otherwise it is the subclass you created explicitly. At the class level, you have methods like `get` or `before`, but you cannot access the `request` or `session` objects, as there is only a single application class for all requests. Options created via `set` are methods at class level: ```ruby class MyApp < Sinatra::Base # Hey, I'm in the application scope! set :foo, 42 foo # => 42 get '/foo' do # Hey, I'm no longer in the application scope! end end ``` You have the application scope binding inside: * Your application class body * Methods defined by extensions * The block passed to `helpers` * Procs/blocks used as a value for `set` * The block passed to `Sinatra.new` You can reach the scope object (the class) like this: * Via the object passed to configure blocks (`configure { |c| ... }`) * `settings` from within the request scope ### Request/Instance Scope For every incoming request, a new instance of your application class is created, and all handler blocks run in that scope. From within this scope you can access the `request` and `session` objects or call rendering methods like `erb` or `haml`. You can access the application scope from within the request scope via the `settings` helper: ```ruby class MyApp < Sinatra::Base # Hey, I'm in the application scope! get '/define_route/:name' do # Request scope for '/define_route/:name' @value = 42 settings.get("/#{params['name']}") do # Request scope for "/#{params['name']}" @value # => nil (not the same request) end "Route defined!" end end ``` You have the request scope binding inside: * get, head, post, put, delete, options, patch, link and unlink blocks * before and after filters * helper methods * templates/views ### Delegation Scope The delegation scope just forwards methods to the class scope. However, it does not behave exactly like the class scope, as you do not have the class binding. Only methods explicitly marked for delegation are available, and you do not share variables/state with the class scope (read: you have a different `self`). You can explicitly add method delegations by calling `Sinatra::Delegator.delegate :method_name`. You have the delegate scope binding inside: * The top-level binding, if you did `require "sinatra"` * An object extended with the `Sinatra::Delegator` mixin Have a look at the code for yourself: here's the [Sinatra::Delegator mixin](https://github.com/sinatra/sinatra/blob/ca06364/lib/sinatra/base.rb#L1609-1633) being [extending the main object](https://github.com/sinatra/sinatra/blob/ca06364/lib/sinatra/main.rb#L28-30). ## Command Line Sinatra applications can be run directly: ```shell ruby myapp.rb [-h] [-x] [-q] [-e ENVIRONMENT] [-p PORT] [-o HOST] [-s HANDLER] ``` Options are: ``` -h # help -p # set the port (default is 4567) -o # set the host (default is 0.0.0.0) -e # set the environment (default is development) -s # specify rack server/handler (default is puma) -q # turn on quiet mode for server (default is off) -x # turn on the mutex lock (default is off) ``` ### Multi-threading _Paraphrasing from [this StackOverflow answer](https://stackoverflow.com/a/6282999/5245129) by Konstantin_ Sinatra doesn't impose any concurrency model but leaves that to the underlying Rack handler (server) like Puma or WEBrick. Sinatra itself is thread-safe, so there won't be any problem if the Rack handler uses a threaded model of concurrency. This would mean that when starting the server, you'd have to specify the correct invocation method for the specific Rack handler. The following example is a demonstration of how to start a multi-threaded Rainbows server: ```ruby # config.ru require 'sinatra/base' class App < Sinatra::Base get '/' do "Hello, World" end end run App ``` ```ruby # rainbows.conf # Rainbows configurator is based on Unicorn. Rainbows! do use :ThreadSpawn end ``` To start the server, the command would be: ```shell rainbows -c rainbows.conf ``` ## Requirement The following Ruby versions are officially supported:
Ruby 2.6
2.6 is fully supported and recommended. There are currently no plans to drop official support for it.
Rubinius
Rubinius is officially supported (Rubinius >= 2.x). It is recommended to gem install puma.
JRuby
The latest stable release of JRuby is officially supported. It is not recommended to use C extensions with JRuby. It is recommended to gem install trinidad.
Versions of Ruby before 2.6 are no longer supported as of Sinatra 3.0.0. We also keep an eye on upcoming Ruby versions. Expect upcoming 3.x releases to be fully supported. Sinatra should work on any operating system supported by the chosen Ruby implementation. Running Sinatra on a not officially supported Ruby flavor means that if things only break there we assume it's not our issue but theirs. ## The Bleeding Edge If you would like to use Sinatra's latest bleeding-edge code, feel free to run your application against the master branch, it should be rather stable. We also push out prerelease gems from time to time, so you can do a ```shell gem install sinatra --pre ``` to get some of the latest features. ### With Bundler If you want to run your application with the latest Sinatra, using [Bundler](https://bundler.io) is the recommended way. First, install bundler, if you haven't: ```shell gem install bundler ``` Then, in your project directory, create a `Gemfile`: ```ruby source 'https://rubygems.org' gem 'sinatra', :github => 'sinatra/sinatra' # other dependencies gem 'haml' # for instance, if you use haml ``` Note that you will have to list all your application's dependencies in the `Gemfile`. Sinatra's direct dependencies (Rack and Tilt) will, however, be automatically fetched and added by Bundler. Now you can run your app like this: ```shell bundle exec ruby myapp.rb ``` ## Versioning Sinatra follows [Semantic Versioning](https://semver.org/), both SemVer and SemVerTag. ## Further Reading * [Project Website](http://www.sinatrarb.com/) - Additional documentation, news, and links to other resources. * [Contributing](http://www.sinatrarb.com/contributing) - Find a bug? Need help? Have a patch? * [Issue tracker](https://github.com/sinatra/sinatra/issues) * [Twitter](https://twitter.com/sinatra) * [Mailing List](https://groups.google.com/forum/#!forum/sinatrarb) * IRC: [#sinatra](irc://chat.freenode.net/#sinatra) on [Freenode](https://freenode.net) * [Sinatra & Friends](https://sinatrarb.slack.com) on Slack ([get an invite](https://sinatra-slack.herokuapp.com/)) * [Sinatra Book](https://github.com/sinatra/sinatra-book) - Cookbook Tutorial * [Sinatra Recipes](http://recipes.sinatrarb.com/) - Community contributed recipes * API documentation for the [latest release](http://www.rubydoc.info/gems/sinatra) or the [current HEAD](http://www.rubydoc.info/github/sinatra/sinatra) on [RubyDoc](http://www.rubydoc.info/) * [CI Actions](https://github.com/sinatra/sinatra/actions) sinatra-3.0.5/RELEASING.md000066400000000000000000000036351434717561400150360ustar00rootroot00000000000000# Releasing Sinatra 🥂 This document explains releasing process for all Sinatra gems. Since everything is bundled in same repo (except `Mustermann`), we now have one rake task to cut a release. (Please refer to [Mustermann](https://github.com/sinatra/mustermann) if that also needs a release.) ### Releasing For releasing new version of [`sinatra`, `sinatra-contrib`, `rack-protection`], this is the procedure: 1. Update `VERSION` file with target version 2. Run `bundle exec rake release:all` 3. ??? 4. Profit!!! Thats it! This rake task can be broken down as: * Pick up latest version string from `VERSION` file * Run all tests to ensure gems are not broken * Update `version.rb` file in all gems with latest `VERSION` * Create a new commit with new `VERSION` and `version.rb` files * Tag the commit with same version * Push the commit and tags to github * Package all the gems, ie create all `.gem` files * Ensure that all the gems can be installed locally * If no issues, push all gems to upstream. In addition to above rake task, there are other rake tasks which can help with development. ### Packaging These rake tasks will generate `.gem` and `.tar.gz` files. For each gem, there is one dedicated rake task. ```sh # Build sinatra-contrib package $ bundle exec rake package:sinatra-contrib # Build rack-protection package $ bundle exec rake package:rack-protection # Build sinatra package $ bundle exec rake package:sinatra # Build all packages $ bundle exec rake package:all ``` ### Packaging and installing locally These rake tasks will package all the gems, and install them locally ```sh # Build and install sinatra-contrib gem locally $ bundle exec rake install:sinatra-contrib # Build and install rack-protection gem locally $ bundle exec rake install:rack-protection # Build and install sinatra gem locally $ bundle exec rake install:sinatra # Build and install all gems locally $ bundle exec rake install:all ``` sinatra-3.0.5/Rakefile000066400000000000000000000154301434717561400146440ustar00rootroot00000000000000# frozen_string_literal: true require 'rake/clean' require 'rake/testtask' require 'fileutils' require 'date' task default: :test task spec: :test CLEAN.include '**/*.rbc' def source_version @source_version ||= File.read(File.expand_path('VERSION', __dir__)).strip end def prev_feature source_version.gsub(/^(\d\.)(\d+)\..*$/) { $1 + ($2.to_i - 1).to_s } end def prev_version return "#{prev_feature}.0" if source_version.end_with? '.0' source_version.gsub(/\d+$/) { |s| s.to_i - 1 } end # SPECS =============================================================== Rake::TestTask.new(:test) do |t| t.test_files = FileList['test/*_test.rb'] t.ruby_opts = ['-r rubygems'] if defined? Gem t.warning = true end Rake::TestTask.new(:'test:core') do |t| core_tests = %w[ base delegator encoding extensions filter helpers mapped_error middleware rdoc readme request response result route_added_hook routing server settings sinatra static templates ] t.test_files = core_tests.map { |n| "test/#{n}_test.rb" } t.ruby_opts = ['-r rubygems'] if defined? Gem t.warning = true end # Rcov ================================================================ namespace :test do desc 'Measures test coverage' task :coverage do rm_f 'coverage' ENV['COVERAGE'] = '1' Rake::Task['test'].invoke end end # Website ============================================================= desc 'Generate RDoc under doc/api' task 'doc' => ['doc:api'] task('doc:api') { sh 'yardoc -o doc/api' } CLEAN.include 'doc/api' # README =============================================================== task :add_template, [:name] do |_t, args| Dir.glob('README.*') do |file| code = File.read(file) if code =~ /^===.*#{args.name.capitalize}/ puts "Already covered in #{file}" else template = code[%r{===[^\n]*Liquid.*index\.liquid[^\n]*}m] if template puts "Adding section to #{file}" template = template.gsub(/Liquid/, args.name.capitalize).gsub(/liquid/, args.name.downcase) code.gsub! /^(\s*===.*CoffeeScript)/, "\n" << template << "\n\\1" File.open(file, 'w') { |f| f << code } else puts "Liquid not found in #{file}" end end end end # Thanks in announcement =============================================== team = ['Ryan Tomayko', 'Blake Mizerany', 'Simon Rozet', 'Konstantin Haase', 'Zachary Scott'] desc 'list of contributors' task :thanks, ['release:all', :backports] do |_t, a| a.with_defaults release: "#{prev_version}..HEAD", backports: "#{prev_feature}.0..#{prev_feature}.x" included = `git log --format=format:"%aN\t%s" #{a.release}`.lines.map { |l| l.force_encoding('binary') } excluded = `git log --format=format:"%aN\t%s" #{a.backports}`.lines.map { |l| l.force_encoding('binary') } commits = (included - excluded).group_by { |c| c[/^[^\t]+/] } authors = commits.keys.sort_by { |n| - commits[n].size } - team puts authors[0..-2].join(', ') << ' and ' << authors.last, "(based on commits included in #{a.release}, but not in #{a.backports})" end desc 'list of authors' task :authors, [:commit_range, :format, :sep] do |_t, a| a.with_defaults format: '%s (%d)', sep: ', ', commit_range: '--all' authors = Hash.new(0) blake = 'Blake Mizerany' overall = 0 mapping = { 'blake.mizerany@gmail.com' => blake, 'bmizerany' => blake, 'a_user@mac.com' => blake, 'ichverstehe' => 'Harry Vangberg', 'Wu Jiang (nouse)' => 'Wu Jiang' } `git shortlog -s #{a.commit_range}`.lines.map do |line| line = line.force_encoding 'binary' if line.respond_to? :force_encoding num, name = line.split("\t", 2).map(&:strip) authors[mapping[name] || name] += num.to_i overall += num.to_i end puts "#{overall} commits by #{authors.count} authors:" puts authors.sort_by { |_n, c| -c }.map { |e| a.format % e }.join(a.sep) end desc 'generates TOC' task :toc, [:readme] do |_t, a| a.with_defaults readme: 'README.md' def self.link(title) title.downcase.gsub(/(?!-)\W /, '-').gsub(' ', '-').gsub(/(?!-)\W/, '') end puts '* [Sinatra](#sinatra)' title = Regexp.new('(?<=\* )(.*)') # so Ruby 1.8 doesn't complain File.binread(a.readme).scan(/^##.*/) do |line| puts line.gsub(/#(?=#)/, ' ').gsub('#', '*').gsub(title) { "[#{$1}](##{link($1)})" } end end # PACKAGING ============================================================ if defined?(Gem) GEMS_AND_ROOT_DIRECTORIES = { 'sinatra' => '.', 'sinatra-contrib' => './sinatra-contrib', 'rack-protection' => './rack-protection' }.freeze def package(gem, ext = '') "pkg/#{gem}-#{source_version}" + ext end directory 'pkg/' CLOBBER.include('pkg') GEMS_AND_ROOT_DIRECTORIES.each do |gem, directory| file package(gem, '.gem') => ['pkg/', "#{"#{directory}/#{gem}"}.gemspec"] do |f| sh "cd #{directory} && gem build #{gem}.gemspec" mv "#{directory}/#{File.basename(f.name)}", f.name end file package(gem, '.tar.gz') => ['pkg/'] do |f| sh <<-SH git archive \ --prefix=#{gem}-#{source_version}/ \ --format=tar \ HEAD -- #{directory} | gzip > #{f.name} SH end end namespace :package do GEMS_AND_ROOT_DIRECTORIES.each do |gem, _directory| desc "Build #{gem} packages" task gem => %w[.gem .tar.gz].map { |e| package(gem, e) } end desc 'Build all packages' task all: GEMS_AND_ROOT_DIRECTORIES.keys end namespace :install do GEMS_AND_ROOT_DIRECTORIES.each do |gem, _directory| desc "Build and install #{gem} as local gem" task gem => package(gem, '.gem') do sh "gem install #{package(gem, '.gem')}" end end desc 'Build and install all of the gems as local gems' task all: GEMS_AND_ROOT_DIRECTORIES.keys end namespace :release do GEMS_AND_ROOT_DIRECTORIES.each do |gem, _directory| desc "Release #{gem} as a package" task gem => "package:#{gem}" do sh <<-SH gem install #{package(gem, '.gem')} --local && gem push #{package(gem, '.gem')} SH end end desc 'Commits the version to github repository' task :commit_version do %w[ lib/sinatra sinatra-contrib/lib/sinatra/contrib rack-protection/lib/rack/protection ].each do |path| path = File.join(path, 'version.rb') File.write(path, File.read(path).sub(/VERSION = '(.+?)'/, "VERSION = '#{source_version}'")) end sh <<-SH git commit --allow-empty -a -m '#{source_version} release' && git tag -s v#{source_version} -m '#{source_version} release' && git push && (git push origin || true) && git push --tags && (git push origin --tags || true) SH end desc 'Release all gems as packages' task all: %i[test commit_version] + GEMS_AND_ROOT_DIRECTORIES.keys end end sinatra-3.0.5/SECURITY.md000066400000000000000000000055041434717561400147710ustar00rootroot00000000000000# Reporting a security bug All security bugs in Sinatra should be reported to the core team through our private mailing list [sinatra-security@googlegroups.com](https://groups.google.com/group/sinatra-security). Your report will be acknowledged within 24 hours, and you’ll receive a more detailed response to your email within 48 hours indicating the next steps in handling your report. After the initial reply to your report the security team will endeavor to keep you informed of the progress being made towards a fix and full announcement. These updates will be sent at least every five days, in reality this is more likely to be every 24-48 hours. If you have not received a reply to your email within 48 hours, or have not heard from the security team for the past five days there are a few steps you can take: * Contact the current security coordinator [Zachary Scott](mailto:zzak@ruby-lang.org) directly ## Disclosure Policy Sinatra has a 5 step disclosure policy, that is upheld to the best of our ability. 1. Security report received and is assigned a primary handler. This person will coordinate the fix and release process. 2. Problem is confirmed and, a list of all affected versions is determined. Code is audited to find any potential similar problems. 3. Fixes are prepared for all releases which are still supported. These fixes are not committed to the public repository but rather held locally pending the announcement. 4. A suggested embargo date for this vulnerability is chosen and distros@openwall is notified. This notification will include patches for all versions still under support and a contact address for packagers who need advice back-porting patches to older versions. 5. On the embargo date, the [mailing list][mailing-list] and [security list][security-list] are sent a copy of the announcement. The changes are pushed to the public repository and new gems released to rubygems. Typically the embargo date will be set 72 hours from the time vendor-sec is first notified, however this may vary depending on the severity of the bug or difficulty in applying a fix. This process can take some time, especially when coordination is required with maintainers of other projects. Every effort will be made to handle the bug in as timely a manner as possible, however it’s important that we follow the release process above to ensure that the disclosure is handled in a consistent manner. ## Security Updates Security updates will be posted on the [mailing list][mailing-list] and [security list][security-list]. ## Comments on this Policy If you have any suggestions to improve this policy, please send an email the core team at [sinatrarb@googlegroups.com](https://groups.google.com/group/sinatrarb). [mailing-list]: http://groups.google.com/group/sinatrarb/topics [security-list]: http://groups.google.com/group/sinatra-security/topics sinatra-3.0.5/VERSION000066400000000000000000000000061434717561400142400ustar00rootroot000000000000003.0.5 sinatra-3.0.5/examples/000077500000000000000000000000001434717561400150125ustar00rootroot00000000000000sinatra-3.0.5/examples/chat.rb000077500000000000000000000025251434717561400162650ustar00rootroot00000000000000#!/usr/bin/env ruby -I ../lib -I lib # frozen_string_literal: true require_relative 'rainbows' require 'sinatra' set :server, :rainbows connections = [] get '/' do halt erb(:login) unless params[:user] erb :chat, locals: { user: params[:user].gsub(/\W/, '') } end get '/stream', provides: 'text/event-stream' do stream :keep_open do |out| connections << out out.callback { connections.delete(out) } end end post '/' do connections.each { |out| out << "data: #{params[:msg]}\n\n" } 204 # response without entity body end __END__ @@ layout Super Simple Chat with Sinatra <%= yield %> @@ login
@@ chat

sinatra-3.0.5/examples/rainbows.conf000066400000000000000000000000451434717561400175040ustar00rootroot00000000000000Rainbows! do use :EventMachine end sinatra-3.0.5/examples/rainbows.rb000066400000000000000000000007721434717561400171710ustar00rootroot00000000000000# frozen_string_literal: true require 'rainbows' module Rack module Handler class Rainbows def self.run(app, **options) rainbows_options = { listeners: ["#{options[:Host]}:#{options[:Port]}"], worker_processes: 1, timeout: 30, config_file: ::File.expand_path('rainbows.conf', __dir__) } ::Rainbows::HttpServer.new(app, rainbows_options).start.join end end register :rainbows, ::Rack::Handler::Rainbows end end sinatra-3.0.5/examples/simple.rb000077500000000000000000000001721434717561400166330ustar00rootroot00000000000000#!/usr/bin/env ruby -I ../lib -I lib # frozen_string_literal: true require 'sinatra' get('/') { 'this is a simple app' } sinatra-3.0.5/examples/stream.ru000066400000000000000000000011741434717561400166600ustar00rootroot00000000000000# frozen_string_literal: true # this example does *not* work properly with WEBrick # # run *one* of these: # # rackup -s mongrel stream.ru # gem install mongrel # unicorn stream.ru # gem install unicorn # puma stream.ru # gem install puma # rainbows -c rainbows.conf stream.ru # gem install rainbows eventmachine require 'sinatra/base' class Stream < Sinatra::Base get '/' do content_type :txt stream do |out| out << "It's gonna be legen -\n" sleep 0.5 out << " (wait for it) \n" sleep 1 out << "- dary!\n" end end end run Stream sinatra-3.0.5/lib/000077500000000000000000000000001434717561400137425ustar00rootroot00000000000000sinatra-3.0.5/lib/sinatra.rb000066400000000000000000000001201434717561400157210ustar00rootroot00000000000000# frozen_string_literal: true require 'sinatra/main' enable :inline_templates sinatra-3.0.5/lib/sinatra/000077500000000000000000000000001434717561400154035ustar00rootroot00000000000000sinatra-3.0.5/lib/sinatra/base.rb000066400000000000000000001760131434717561400166520ustar00rootroot00000000000000# frozen_string_literal: true # external dependencies require 'rack' require 'tilt' require 'rack/protection' require 'mustermann' require 'mustermann/sinatra' require 'mustermann/regular' # stdlib dependencies require 'time' require 'uri' # other files we need require 'sinatra/indifferent_hash' require 'sinatra/show_exceptions' require 'sinatra/version' module Sinatra # The request object. See Rack::Request for more info: # http://rubydoc.info/github/rack/rack/master/Rack/Request class Request < Rack::Request HEADER_PARAM = /\s*[\w.]+=(?:[\w.]+|"(?:[^"\\]|\\.)*")?\s*/.freeze HEADER_VALUE_WITH_PARAMS = %r{(?:(?:\w+|\*)/(?:\w+(?:\.|-|\+)?|\*)*)\s*(?:;#{HEADER_PARAM})*}.freeze # Returns an array of acceptable media types for the response def accept @env['sinatra.accept'] ||= if @env.include?('HTTP_ACCEPT') && (@env['HTTP_ACCEPT'].to_s != '') @env['HTTP_ACCEPT'] .to_s .scan(HEADER_VALUE_WITH_PARAMS) .map! { |e| AcceptEntry.new(e) } .sort else [AcceptEntry.new('*/*')] end end def accept?(type) preferred_type(type).to_s.include?(type) end def preferred_type(*types) return accept.first if types.empty? types.flatten! return types.first if accept.empty? accept.detect do |accept_header| type = types.detect { |t| MimeTypeEntry.new(t).accepts?(accept_header) } return type if type end end alias secure? ssl? def forwarded? @env.include? 'HTTP_X_FORWARDED_HOST' end def safe? get? || head? || options? || trace? end def idempotent? safe? || put? || delete? || link? || unlink? end def link? request_method == 'LINK' end def unlink? request_method == 'UNLINK' end def params super rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e raise BadRequest, "Invalid query parameters: #{Rack::Utils.escape_html(e.message)}" rescue EOFError => e raise BadRequest, "Invalid multipart/form-data: #{Rack::Utils.escape_html(e.message)}" end class AcceptEntry attr_accessor :params attr_reader :entry def initialize(entry) params = entry.scan(HEADER_PARAM).map! do |s| key, value = s.strip.split('=', 2) value = value[1..-2].gsub(/\\(.)/, '\1') if value.start_with?('"') [key, value] end @entry = entry @type = entry[/[^;]+/].delete(' ') @params = params.to_h @q = @params.delete('q') { 1.0 }.to_f end def <=>(other) other.priority <=> priority end def priority # We sort in descending order; better matches should be higher. [@q, -@type.count('*'), @params.size] end def to_str @type end def to_s(full = false) full ? entry : to_str end def respond_to?(*args) super || to_str.respond_to?(*args) end def method_missing(*args, &block) to_str.send(*args, &block) end end class MimeTypeEntry attr_reader :params def initialize(entry) params = entry.scan(HEADER_PARAM).map! do |s| key, value = s.strip.split('=', 2) value = value[1..-2].gsub(/\\(.)/, '\1') if value.start_with?('"') [key, value] end @type = entry[/[^;]+/].delete(' ') @params = params.to_h end def accepts?(entry) File.fnmatch(entry, self) && matches_params?(entry.params) end def to_str @type end def matches_params?(params) return true if @params.empty? params.all? { |k, v| !@params.key?(k) || @params[k] == v } end end end # The response object. See Rack::Response and Rack::Response::Helpers for # more info: # http://rubydoc.info/github/rack/rack/master/Rack/Response # http://rubydoc.info/github/rack/rack/master/Rack/Response/Helpers class Response < Rack::Response DROP_BODY_RESPONSES = [204, 304].freeze def body=(value) value = value.body while Rack::Response === value @body = String === value ? [value.to_str] : value end def each block_given? ? super : enum_for(:each) end def finish result = body if drop_content_info? headers.delete 'Content-Length' headers.delete 'Content-Type' end if drop_body? close result = [] end if calculate_content_length? # if some other code has already set Content-Length, don't muck with it # currently, this would be the static file-handler headers['Content-Length'] = body.map(&:bytesize).reduce(0, :+).to_s end [status, headers, result] end private def calculate_content_length? headers['Content-Type'] && !headers['Content-Length'] && (Array === body) end def drop_content_info? informational? || drop_body? end def drop_body? DROP_BODY_RESPONSES.include?(status) end end # Some Rack handlers (Rainbows!) implement an extended body object protocol, however, # some middleware (namely Rack::Lint) will break it by not mirroring the methods in question. # This middleware will detect an extended body object and will make sure it reaches the # handler directly. We do this here, so our middleware and middleware set up by the app will # still be able to run. class ExtendedRack < Struct.new(:app) def call(env) result = app.call(env) callback = env['async.callback'] return result unless callback && async?(*result) after_response { callback.call result } setup_close(env, *result) throw :async end private def setup_close(env, _status, _headers, body) return unless body.respond_to?(:close) && env.include?('async.close') env['async.close'].callback { body.close } env['async.close'].errback { body.close } end def after_response(&block) raise NotImplementedError, 'only supports EventMachine at the moment' unless defined? EventMachine EventMachine.next_tick(&block) end def async?(status, _headers, body) return true if status == -1 body.respond_to?(:callback) && body.respond_to?(:errback) end end # Behaves exactly like Rack::CommonLogger with the notable exception that it does nothing, # if another CommonLogger is already in the middleware chain. class CommonLogger < Rack::CommonLogger def call(env) env['sinatra.commonlogger'] ? @app.call(env) : super end superclass.class_eval do alias_method :call_without_check, :call unless method_defined? :call_without_check def call(env) env['sinatra.commonlogger'] = true call_without_check(env) end end end class Error < StandardError # :nodoc: end class BadRequest < Error # :nodoc: def http_status; 400 end end class NotFound < Error # :nodoc: def http_status; 404 end end # Methods available to routes, before/after filters, and views. module Helpers # Set or retrieve the response status code. def status(value = nil) response.status = Rack::Utils.status_code(value) if value response.status end # Set or retrieve the response body. When a block is given, # evaluation is deferred until the body is read with #each. def body(value = nil, &block) if block_given? def block.each; yield(call) end response.body = block elsif value # Rack 2.0 returns a Rack::File::Iterator here instead of # Rack::File as it was in the previous API. unless request.head? || value.is_a?(Rack::File::Iterator) || value.is_a?(Stream) headers.delete 'Content-Length' end response.body = value else response.body end end # Halt processing and redirect to the URI provided. def redirect(uri, *args) if (env['HTTP_VERSION'] == 'HTTP/1.1') && (env['REQUEST_METHOD'] != 'GET') status 303 else status 302 end # According to RFC 2616 section 14.30, "the field value consists of a # single absolute URI" response['Location'] = uri(uri.to_s, settings.absolute_redirects?, settings.prefixed_redirects?) halt(*args) end # Generates the absolute URI for a given path in the app. # Takes Rack routers and reverse proxies into account. def uri(addr = nil, absolute = true, add_script_name = true) return addr if addr =~ /\A[a-z][a-z0-9+.\-]*:/i uri = [host = String.new] if absolute host << "http#{'s' if request.secure?}://" host << if request.forwarded? || (request.port != (request.secure? ? 443 : 80)) request.host_with_port else request.host end end uri << request.script_name.to_s if add_script_name uri << (addr || request.path_info).to_s File.join uri end alias url uri alias to uri # Halt processing and return the error status provided. def error(code, body = nil) if code.respond_to? :to_str body = code.to_str code = 500 end response.body = body unless body.nil? halt code end # Halt processing and return a 404 Not Found. def not_found(body = nil) error 404, body end # Set multiple response headers with Hash. def headers(hash = nil) response.headers.merge! hash if hash response.headers end # Access the underlying Rack session. def session request.session end # Access shared logger object. def logger request.logger end # Look up a media type by file extension in Rack's mime registry. def mime_type(type) Base.mime_type(type) end # Set the Content-Type of the response body given a media type or file # extension. def content_type(type = nil, params = {}) return response['Content-Type'] unless type default = params.delete :default mime_type = mime_type(type) || default raise format('Unknown media type: %p', type) if mime_type.nil? mime_type = mime_type.dup unless params.include?(:charset) || settings.add_charset.all? { |p| !(p === mime_type) } params[:charset] = params.delete('charset') || settings.default_encoding end params.delete :charset if mime_type.include? 'charset' unless params.empty? mime_type << (mime_type.include?(';') ? ', ' : ';') mime_type << params.map do |key, val| val = val.inspect if val =~ /[";,]/ "#{key}=#{val}" end.join(', ') end response['Content-Type'] = mime_type end # https://html.spec.whatwg.org/#multipart-form-data MULTIPART_FORM_DATA_REPLACEMENT_TABLE = { '"' => '%22', "\r" => '%0D', "\n" => '%0A' }.freeze # Set the Content-Disposition to "attachment" with the specified filename, # instructing the user agents to prompt to save. def attachment(filename = nil, disposition = :attachment) response['Content-Disposition'] = disposition.to_s.dup return unless filename params = format('; filename="%s"', File.basename(filename).gsub(/["\r\n]/, MULTIPART_FORM_DATA_REPLACEMENT_TABLE)) response['Content-Disposition'] << params ext = File.extname(filename) content_type(ext) unless response['Content-Type'] || ext.empty? end # Use the contents of the file at +path+ as the response body. def send_file(path, opts = {}) if opts[:type] || !response['Content-Type'] content_type opts[:type] || File.extname(path), default: 'application/octet-stream' end disposition = opts[:disposition] filename = opts[:filename] disposition = :attachment if disposition.nil? && filename filename = path if filename.nil? attachment(filename, disposition) if disposition last_modified opts[:last_modified] if opts[:last_modified] file = Rack::File.new(File.dirname(settings.app_file)) result = file.serving(request, path) result[1].each { |k, v| headers[k] ||= v } headers['Content-Length'] = result[1]['Content-Length'] opts[:status] &&= Integer(opts[:status]) halt (opts[:status] || result[0]), result[2] rescue Errno::ENOENT not_found end # Class of the response body in case you use #stream. # # Three things really matter: The front and back block (back being the # block generating content, front the one sending it to the client) and # the scheduler, integrating with whatever concurrency feature the Rack # handler is using. # # Scheduler has to respond to defer and schedule. class Stream def self.schedule(*) yield end def self.defer(*) yield end def initialize(scheduler = self.class, keep_open = false, &back) @back = back.to_proc @scheduler = scheduler @keep_open = keep_open @callbacks = [] @closed = false end def close return if closed? @closed = true @scheduler.schedule { @callbacks.each { |c| c.call } } end def each(&front) @front = front @scheduler.defer do begin @back.call(self) rescue Exception => e @scheduler.schedule { raise e } end close unless @keep_open end end def <<(data) @scheduler.schedule { @front.call(data.to_s) } self end def callback(&block) return yield if closed? @callbacks << block end alias errback callback def closed? @closed end end # Allows to start sending data to the client even though later parts of # the response body have not yet been generated. # # The close parameter specifies whether Stream#close should be called # after the block has been executed. This is only relevant for evented # servers like Rainbows. def stream(keep_open = false) scheduler = env['async.callback'] ? EventMachine : Stream current = @params.dup body Stream.new(scheduler, keep_open) { |out| with_params(current) { yield(out) } } end # Specify response freshness policy for HTTP caches (Cache-Control header). # Any number of non-value directives (:public, :private, :no_cache, # :no_store, :must_revalidate, :proxy_revalidate) may be passed along with # a Hash of value directives (:max_age, :s_maxage). # # cache_control :public, :must_revalidate, :max_age => 60 # => Cache-Control: public, must-revalidate, max-age=60 # # See RFC 2616 / 14.9 for more on standard cache control directives: # http://tools.ietf.org/html/rfc2616#section-14.9.1 def cache_control(*values) if values.last.is_a?(Hash) hash = values.pop hash.reject! { |_k, v| v == false } hash.reject! { |k, v| values << k if v == true } else hash = {} end values.map! { |value| value.to_s.tr('_', '-') } hash.each do |key, value| key = key.to_s.tr('_', '-') value = value.to_i if %w[max-age s-maxage].include? key values << "#{key}=#{value}" end response['Cache-Control'] = values.join(', ') if values.any? end # Set the Expires header and Cache-Control/max-age directive. Amount # can be an integer number of seconds in the future or a Time object # indicating when the response should be considered "stale". The remaining # "values" arguments are passed to the #cache_control helper: # # expires 500, :public, :must_revalidate # => Cache-Control: public, must-revalidate, max-age=500 # => Expires: Mon, 08 Jun 2009 08:50:17 GMT # def expires(amount, *values) values << {} unless values.last.is_a?(Hash) if amount.is_a? Integer time = Time.now + amount.to_i max_age = amount else time = time_for amount max_age = time - Time.now end values.last.merge!(max_age: max_age) { |_key, v1, v2| v1 || v2 } cache_control(*values) response['Expires'] = time.httpdate end # Set the last modified time of the resource (HTTP 'Last-Modified' header) # and halt if conditional GET matches. The +time+ argument is a Time, # DateTime, or other object that responds to +to_time+. # # When the current request includes an 'If-Modified-Since' header that is # equal or later than the time specified, execution is immediately halted # with a '304 Not Modified' response. def last_modified(time) return unless time time = time_for time response['Last-Modified'] = time.httpdate return if env['HTTP_IF_NONE_MATCH'] if (status == 200) && env['HTTP_IF_MODIFIED_SINCE'] # compare based on seconds since epoch since = Time.httpdate(env['HTTP_IF_MODIFIED_SINCE']).to_i halt 304 if since >= time.to_i end if (success? || (status == 412)) && env['HTTP_IF_UNMODIFIED_SINCE'] # compare based on seconds since epoch since = Time.httpdate(env['HTTP_IF_UNMODIFIED_SINCE']).to_i halt 412 if since < time.to_i end rescue ArgumentError end ETAG_KINDS = %i[strong weak].freeze # Set the response entity tag (HTTP 'ETag' header) and halt if conditional # GET matches. The +value+ argument is an identifier that uniquely # identifies the current version of the resource. The +kind+ argument # indicates whether the etag should be used as a :strong (default) or :weak # cache validator. # # When the current request includes an 'If-None-Match' header with a # matching etag, execution is immediately halted. If the request method is # GET or HEAD, a '304 Not Modified' response is sent. def etag(value, options = {}) # Before touching this code, please double check RFC 2616 14.24 and 14.26. options = { kind: options } unless Hash === options kind = options[:kind] || :strong new_resource = options.fetch(:new_resource) { request.post? } unless ETAG_KINDS.include?(kind) raise ArgumentError, ':strong or :weak expected' end value = format('"%s"', value) value = "W/#{value}" if kind == :weak response['ETag'] = value return unless success? || status == 304 if etag_matches?(env['HTTP_IF_NONE_MATCH'], new_resource) halt(request.safe? ? 304 : 412) end if env['HTTP_IF_MATCH'] return if etag_matches?(env['HTTP_IF_MATCH'], new_resource) halt 412 end nil end # Sugar for redirect (example: redirect back) def back request.referer end # whether or not the status is set to 1xx def informational? status.between? 100, 199 end # whether or not the status is set to 2xx def success? status.between? 200, 299 end # whether or not the status is set to 3xx def redirect? status.between? 300, 399 end # whether or not the status is set to 4xx def client_error? status.between? 400, 499 end # whether or not the status is set to 5xx def server_error? status.between? 500, 599 end # whether or not the status is set to 404 def not_found? status == 404 end # whether or not the status is set to 400 def bad_request? status == 400 end # Generates a Time object from the given value. # Used by #expires and #last_modified. def time_for(value) if value.is_a? Numeric Time.at value elsif value.respond_to? :to_s Time.parse value.to_s else value.to_time end rescue ArgumentError => e raise e rescue Exception raise ArgumentError, "unable to convert #{value.inspect} to a Time object" end private # Helper method checking if a ETag value list includes the current ETag. def etag_matches?(list, new_resource = request.post?) return !new_resource if list == '*' list.to_s.split(/\s*,\s*/).include? response['ETag'] end def with_params(temp_params) original = @params @params = temp_params yield ensure @params = original if original end end # Template rendering methods. Each method takes the name of a template # to render as a Symbol and returns a String with the rendered output, # as well as an optional hash with additional options. # # `template` is either the name or path of the template as symbol # (Use `:'subdir/myview'` for views in subdirectories), or a string # that will be rendered. # # Possible options are: # :content_type The content type to use, same arguments as content_type. # :layout If set to something falsy, no layout is rendered, otherwise # the specified layout is used # :layout_engine Engine to use for rendering the layout. # :locals A hash with local variables that should be available # in the template # :scope If set, template is evaluate with the binding of the given # object rather than the application instance. # :views Views directory to use. module Templates module ContentTyped attr_accessor :content_type end def initialize super @default_layout = :layout @preferred_extension = nil end def erb(template, options = {}, locals = {}, &block) render(:erb, template, options, locals, &block) end def haml(template, options = {}, locals = {}, &block) render(:haml, template, options, locals, &block) end def builder(template = nil, options = {}, locals = {}, &block) options[:default_content_type] = :xml render_ruby(:builder, template, options, locals, &block) end def liquid(template, options = {}, locals = {}, &block) render(:liquid, template, options, locals, &block) end def markdown(template, options = {}, locals = {}) options[:exclude_outvar] = true render :markdown, template, options, locals end def rdoc(template, options = {}, locals = {}) render :rdoc, template, options, locals end def asciidoc(template, options = {}, locals = {}) render :asciidoc, template, options, locals end def markaby(template = nil, options = {}, locals = {}, &block) render_ruby(:mab, template, options, locals, &block) end def nokogiri(template = nil, options = {}, locals = {}, &block) options[:default_content_type] = :xml render_ruby(:nokogiri, template, options, locals, &block) end def slim(template, options = {}, locals = {}, &block) render(:slim, template, options, locals, &block) end def yajl(template, options = {}, locals = {}) options[:default_content_type] = :json render :yajl, template, options, locals end def rabl(template, options = {}, locals = {}) Rabl.register! render :rabl, template, options, locals end # Calls the given block for every possible template file in views, # named name.ext, where ext is registered on engine. def find_template(views, name, engine) yield ::File.join(views, "#{name}.#{@preferred_extension}") Tilt.default_mapping.extensions_for(engine).each do |ext| yield ::File.join(views, "#{name}.#{ext}") unless ext == @preferred_extension end end private # logic shared between builder and nokogiri def render_ruby(engine, template, options = {}, locals = {}, &block) if template.is_a?(Hash) options = template template = nil end template = proc { block } if template.nil? render engine, template, options, locals end def render(engine, data, options = {}, locals = {}, &block) # merge app-level options engine_options = settings.respond_to?(engine) ? settings.send(engine) : {} options.merge!(engine_options) { |_key, v1, _v2| v1 } # extract generic options locals = options.delete(:locals) || locals || {} views = options.delete(:views) || settings.views || './views' layout = options[:layout] layout = false if layout.nil? && options.include?(:layout) eat_errors = layout.nil? layout = engine_options[:layout] if layout.nil? || (layout == true && engine_options[:layout] != false) layout = @default_layout if layout.nil? || (layout == true) layout_options = options.delete(:layout_options) || {} content_type = options.delete(:default_content_type) content_type = options.delete(:content_type) || content_type layout_engine = options.delete(:layout_engine) || engine scope = options.delete(:scope) || self exclude_outvar = options.delete(:exclude_outvar) options.delete(:layout) # set some defaults options[:outvar] ||= '@_out_buf' unless exclude_outvar options[:default_encoding] ||= settings.default_encoding # compile and render template begin layout_was = @default_layout @default_layout = false template = compile_template(engine, data, options, views) output = template.render(scope, locals, &block) ensure @default_layout = layout_was end # render layout if layout extra_options = { views: views, layout: false, eat_errors: eat_errors, scope: scope } options = options.merge(extra_options).merge!(layout_options) catch(:layout_missing) { return render(layout_engine, layout, options, locals) { output } } end output.extend(ContentTyped).content_type = content_type if content_type output end def compile_template(engine, data, options, views) eat_errors = options.delete :eat_errors template = Tilt[engine] raise "Template engine not found: #{engine}" if template.nil? case data when Symbol template_cache.fetch engine, data, options, views do body, path, line = settings.templates[data] if body body = body.call if body.respond_to?(:call) template.new(path, line.to_i, options) { body } else found = false @preferred_extension = engine.to_s find_template(views, data, template) do |file| path ||= file # keep the initial path rather than the last one found = File.exist?(file) if found path = file break end end throw :layout_missing if eat_errors && !found template.new(path, 1, options) end end when Proc compile_block_template(template, options, &data) when String template_cache.fetch engine, data, options, views do compile_block_template(template, options) { data } end else raise ArgumentError, "Sorry, don't know how to render #{data.inspect}." end end def compile_block_template(template, options, &body) first_location = caller_locations.first path = first_location.path line = first_location.lineno path = options[:path] || path line = options[:line] || line template.new(path, line.to_i, options, &body) end end # Base class for all Sinatra applications and middleware. class Base include Rack::Utils include Helpers include Templates URI_INSTANCE = URI::Parser.new attr_accessor :app, :env, :request, :response, :params attr_reader :template_cache def initialize(app = nil, **_kwargs) super() @app = app @template_cache = Tilt::Cache.new @pinned_response = nil # whether a before! filter pinned the content-type yield self if block_given? end # Rack call interface. def call(env) dup.call!(env) end def call!(env) # :nodoc: @env = env @params = IndifferentHash.new @request = Request.new(env) @response = Response.new @pinned_response = nil template_cache.clear if settings.reload_templates invoke { dispatch! } invoke { error_block!(response.status) } unless @env['sinatra.error'] unless @response['Content-Type'] if Array === body && body[0].respond_to?(:content_type) content_type body[0].content_type elsif (default = settings.default_content_type) content_type default end end @response.finish end # Access settings defined with Base.set. def self.settings self end # Access settings defined with Base.set. def settings self.class.settings end # Exit the current block, halts any further processing # of the request, and returns the specified response. def halt(*response) response = response.first if response.length == 1 throw :halt, response end # Pass control to the next matching route. # If there are no more matching routes, Sinatra will # return a 404 response. def pass(&block) throw :pass, block end # Forward the request to the downstream app -- middleware only. def forward raise 'downstream app not set' unless @app.respond_to? :call status, headers, body = @app.call env @response.status = status @response.body = body @response.headers.merge! headers nil end private # Run filters defined on the class and all superclasses. # Accepts an optional block to call after each filter is applied. def filter!(type, base = settings, &block) filter!(type, base.superclass, &block) if base.superclass.respond_to?(:filters) base.filters[type].each do |args| result = process_route(*args) block.call(result) if block_given? end end # Run routes defined on the class and all superclasses. def route!(base = settings, pass_block = nil) routes = base.routes[@request.request_method] routes&.each do |pattern, conditions, block| response.delete_header('Content-Type') unless @pinned_response returned_pass_block = process_route(pattern, conditions) do |*args| env['sinatra.route'] = "#{@request.request_method} #{pattern}" route_eval { block[*args] } end # don't wipe out pass_block in superclass pass_block = returned_pass_block if returned_pass_block end # Run routes defined in superclass. if base.superclass.respond_to?(:routes) return route!(base.superclass, pass_block) end route_eval(&pass_block) if pass_block route_missing end # Run a route block and throw :halt with the result. def route_eval throw :halt, yield end # If the current request matches pattern and conditions, fill params # with keys and call the given block. # Revert params afterwards. # # Returns pass block. def process_route(pattern, conditions, block = nil, values = []) route = @request.path_info route = '/' if route.empty? && !settings.empty_path_info? route = route[0..-2] if !settings.strict_paths? && route != '/' && route.end_with?('/') params = pattern.params(route) return unless params params.delete('ignore') # TODO: better params handling, maybe turn it into "smart" object or detect changes force_encoding(params) @params = @params.merge(params) { |_k, v1, v2| v2 || v1 } if params.any? regexp_exists = pattern.is_a?(Mustermann::Regular) || (pattern.respond_to?(:patterns) && pattern.patterns.any? { |subpattern| subpattern.is_a?(Mustermann::Regular) }) if regexp_exists captures = pattern.match(route).captures.map { |c| URI_INSTANCE.unescape(c) if c } values += captures @params[:captures] = force_encoding(captures) unless captures.nil? || captures.empty? else values += params.values.flatten end catch(:pass) do conditions.each { |c| throw :pass if c.bind(self).call == false } block ? block[self, values] : yield(self, values) end rescue StandardError @env['sinatra.error.params'] = @params raise ensure params ||= {} params.each { |k, _| @params.delete(k) } unless @env['sinatra.error.params'] end # No matching route was found or all routes passed. The default # implementation is to forward the request downstream when running # as middleware (@app is non-nil); when no downstream app is set, raise # a NotFound exception. Subclasses can override this method to perform # custom route miss logic. def route_missing raise NotFound unless @app forward end # Attempt to serve static files from public directory. Throws :halt when # a matching file is found, returns nil otherwise. def static!(options = {}) return if (public_dir = settings.public_folder).nil? path = "#{public_dir}#{URI_INSTANCE.unescape(request.path_info)}" return unless valid_path?(path) path = File.expand_path(path) return unless path.start_with?("#{File.expand_path(public_dir)}/") return unless File.file?(path) env['sinatra.static_file'] = path cache_control(*settings.static_cache_control) if settings.static_cache_control? send_file path, options.merge(disposition: nil) end # Run the block with 'throw :halt' support and apply result to the response. def invoke(&block) res = catch(:halt, &block) res = [res] if (Integer === res) || (String === res) if (Array === res) && (Integer === res.first) res = res.dup status(res.shift) body(res.pop) headers(*res) elsif res.respond_to? :each body res end nil # avoid double setting the same response tuple twice end # Dispatch a request with error handling. def dispatch! # Avoid passing frozen string in force_encoding @params.merge!(@request.params).each do |key, val| next unless val.respond_to?(:force_encoding) val = val.dup if val.frozen? @params[key] = force_encoding(val) end invoke do static! if settings.static? && (request.get? || request.head?) filter! :before do @pinned_response = !response['Content-Type'].nil? end route! end rescue ::Exception => e invoke { handle_exception!(e) } ensure begin filter! :after unless env['sinatra.static_file'] rescue ::Exception => e invoke { handle_exception!(e) } unless @env['sinatra.error'] end end # Error handling during requests. def handle_exception!(boom) error_params = @env['sinatra.error.params'] @params = @params.merge(error_params) if error_params @env['sinatra.error'] = boom http_status = if boom.is_a? Sinatra::Error if boom.respond_to? :http_status boom.http_status elsif settings.use_code? && boom.respond_to?(:code) boom.code end end http_status = 500 unless http_status&.between?(400, 599) status(http_status) if server_error? dump_errors! boom if settings.dump_errors? raise boom if settings.show_exceptions? && (settings.show_exceptions != :after_handler) elsif not_found? headers['X-Cascade'] = 'pass' if settings.x_cascade? end if (res = error_block!(boom.class, boom) || error_block!(status, boom)) return res end if not_found? || bad_request? if boom.message && boom.message != boom.class.name body Rack::Utils.escape_html(boom.message) else content_type 'text/html' body "

#{not_found? ? 'Not Found' : 'Bad Request'}

" end end return unless server_error? raise boom if settings.raise_errors? || settings.show_exceptions? error_block! Exception, boom end # Find an custom error block for the key(s) specified. def error_block!(key, *block_params) base = settings while base.respond_to?(:errors) args_array = base.errors[key] next base = base.superclass unless args_array args_array.reverse_each do |args| first = args == args_array.first args += [block_params] resp = process_route(*args) return resp unless resp.nil? && !first end end return false unless key.respond_to?(:superclass) && (key.superclass < Exception) error_block!(key.superclass, *block_params) end def dump_errors!(boom) msg = ["#{Time.now.strftime('%Y-%m-%d %H:%M:%S')} - #{boom.class} - #{boom.message}:", *boom.backtrace].join("\n\t") @env['rack.errors'].puts(msg) end class << self CALLERS_TO_IGNORE = [ # :nodoc: %r{/sinatra(/(base|main|show_exceptions))?\.rb$}, # all sinatra code %r{lib/tilt.*\.rb$}, # all tilt code /^\(.*\)$/, # generated code %r{rubygems/(custom|core_ext/kernel)_require\.rb$}, # rubygems require hacks /active_support/, # active_support require hacks %r{bundler(/(?:runtime|inline))?\.rb}, # bundler require hacks /= 1.9.2 %r{zeitwerk/kernel\.rb} # Zeitwerk kernel#require decorator ].freeze attr_reader :routes, :filters, :templates, :errors def callers_to_ignore CALLERS_TO_IGNORE end # Removes all routes, filters, middleware and extension hooks from the # current class (not routes/filters/... defined by its superclass). def reset! @conditions = [] @routes = {} @filters = { before: [], after: [] } @errors = {} @middleware = [] @prototype = nil @extensions = [] @templates = if superclass.respond_to?(:templates) Hash.new { |_hash, key| superclass.templates[key] } else {} end end # Extension modules registered on this class and all superclasses. def extensions if superclass.respond_to?(:extensions) (@extensions + superclass.extensions).uniq else @extensions end end # Middleware used in this class and all superclasses. def middleware if superclass.respond_to?(:middleware) superclass.middleware + @middleware else @middleware end end # Sets an option to the given value. If the value is a proc, # the proc will be called every time the option is accessed. def set(option, value = (not_set = true), ignore_setter = false, &block) raise ArgumentError if block && !not_set if block value = block not_set = false end if not_set raise ArgumentError unless option.respond_to?(:each) option.each { |k, v| set(k, v) } return self end if respond_to?("#{option}=") && !ignore_setter return __send__("#{option}=", value) end setter = proc { |val| set option, val, true } getter = proc { value } case value when Proc getter = value when Symbol, Integer, FalseClass, TrueClass, NilClass getter = value.inspect when Hash setter = proc do |val| val = value.merge val if Hash === val set option, val, true end end define_singleton("#{option}=", setter) define_singleton(option, getter) define_singleton("#{option}?", "!!#{option}") unless method_defined? "#{option}?" self end # Same as calling `set :option, true` for each of the given options. def enable(*opts) opts.each { |key| set(key, true) } end # Same as calling `set :option, false` for each of the given options. def disable(*opts) opts.each { |key| set(key, false) } end # Define a custom error handler. Optionally takes either an Exception # class, or an HTTP status code to specify which errors should be # handled. def error(*codes, &block) args = compile! 'ERROR', /.*/, block codes = codes.flat_map(&method(:Array)) codes << Exception if codes.empty? codes << Sinatra::NotFound if codes.include?(404) codes.each { |c| (@errors[c] ||= []) << args } end # Sugar for `error(404) { ... }` def not_found(&block) error(404, &block) end # Define a named template. The block must return the template source. def template(name, &block) filename, line = caller_locations.first templates[name] = [block, filename, line.to_i] end # Define the layout template. The block must return the template source. def layout(name = :layout, &block) template name, &block end # Load embedded templates from the file; uses the caller's __FILE__ # when no file is specified. def inline_templates=(file = nil) file = (caller_files.first || File.expand_path($0)) if file.nil? || file == true begin io = ::IO.respond_to?(:binread) ? ::IO.binread(file) : ::IO.read(file) app, data = io.gsub("\r\n", "\n").split(/^__END__$/, 2) rescue Errno::ENOENT app, data = nil end return unless data encoding = if app && app =~ /([^\n]*\n)?#[^\n]*coding: *(\S+)/m $2 else settings.default_encoding end lines = app.count("\n") + 1 template = nil force_encoding data, encoding data.each_line do |line| lines += 1 if line =~ /^@@\s*(.*\S)\s*$/ template = force_encoding(String.new, encoding) templates[$1.to_sym] = [template, file, lines] elsif template template << line end end end # Lookup or register a mime type in Rack's mime registry. def mime_type(type, value = nil) return type if type.nil? return type.to_s if type.to_s.include?('/') type = ".#{type}" unless type.to_s[0] == '.' return Rack::Mime.mime_type(type, nil) unless value Rack::Mime::MIME_TYPES[type] = value end # provides all mime types matching type, including deprecated types: # mime_types :html # => ['text/html'] # mime_types :js # => ['application/javascript', 'text/javascript'] def mime_types(type) type = mime_type type type =~ %r{^application/(xml|javascript)$} ? [type, "text/#{$1}"] : [type] end # Define a before filter; runs before all requests within the same # context as route handlers and may access/modify the request and # response. def before(path = /.*/, **options, &block) add_filter(:before, path, **options, &block) end # Define an after filter; runs after all requests within the same # context as route handlers and may access/modify the request and # response. def after(path = /.*/, **options, &block) add_filter(:after, path, **options, &block) end # add a filter def add_filter(type, path = /.*/, **options, &block) filters[type] << compile!(type, path, block, **options) end # Add a route condition. The route is considered non-matching when the # block returns false. def condition(name = "#{caller.first[/`.*'/]} condition", &block) @conditions << generate_method(name, &block) end def public=(value) warn_for_deprecation ':public is no longer used to avoid overloading Module#public, use :public_folder or :public_dir instead' set(:public_folder, value) end def public_dir=(value) self.public_folder = value end def public_dir public_folder end # Defining a `GET` handler also automatically defines # a `HEAD` handler. def get(path, opts = {}, &block) conditions = @conditions.dup route('GET', path, opts, &block) @conditions = conditions route('HEAD', path, opts, &block) end def put(path, opts = {}, &block) route 'PUT', path, opts, &block end def post(path, opts = {}, &block) route 'POST', path, opts, &block end def delete(path, opts = {}, &block) route 'DELETE', path, opts, &block end def head(path, opts = {}, &block) route 'HEAD', path, opts, &block end def options(path, opts = {}, &block) route 'OPTIONS', path, opts, &block end def patch(path, opts = {}, &block) route 'PATCH', path, opts, &block end def link(path, opts = {}, &block) route 'LINK', path, opts, &block end def unlink(path, opts = {}, &block) route 'UNLINK', path, opts, &block end # Makes the methods defined in the block and in the Modules given # in `extensions` available to the handlers and templates def helpers(*extensions, &block) class_eval(&block) if block_given? include(*extensions) if extensions.any? end # Register an extension. Alternatively take a block from which an # extension will be created and registered on the fly. def register(*extensions, &block) extensions << Module.new(&block) if block_given? @extensions += extensions extensions.each do |extension| extend extension extension.registered(self) if extension.respond_to?(:registered) end end def development?; environment == :development end def production?; environment == :production end def test?; environment == :test end # Set configuration options for Sinatra and/or the app. # Allows scoping of settings for certain environments. def configure(*envs) yield self if envs.empty? || envs.include?(environment.to_sym) end # Use the specified Rack middleware def use(middleware, *args, &block) @prototype = nil @middleware << [middleware, args, block] end ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true) # Stop the self-hosted server if running. def quit! return unless running? # Use Thin's hard #stop! if available, otherwise just #stop. running_server.respond_to?(:stop!) ? running_server.stop! : running_server.stop warn '== Sinatra has ended his set (crowd applauds)' unless suppress_messages? set :running_server, nil set :handler_name, nil end alias stop! quit! # Run the Sinatra app as a self-hosted server using # Puma, Falcon, Mongrel, or WEBrick (in that order). If given a block, will call # with the constructed handler once we have taken the stage. def run!(options = {}, &block) return if running? set options handler = Rack::Handler.pick(server) handler_name = handler.name.gsub(/.*::/, '') server_settings = settings.respond_to?(:server_settings) ? settings.server_settings : {} server_settings.merge!(Port: port, Host: bind) begin start_server(handler, server_settings, handler_name, &block) rescue Errno::EADDRINUSE warn "== Someone is already performing on port #{port}!" raise ensure quit! end end alias start! run! # Check whether the self-hosted server is running or not. def running? running_server? end # The prototype instance used to process requests. def prototype @prototype ||= new end # Create a new instance without middleware in front of it. alias new! new unless method_defined? :new! # Create a new instance of the class fronted by its middleware # pipeline. The object is guaranteed to respond to #call but may not be # an instance of the class new was called on. def new(*args, &block) instance = new!(*args, &block) Wrapper.new(build(instance).to_app, instance) end ruby2_keywords :new if respond_to?(:ruby2_keywords, true) # Creates a Rack::Builder instance with all the middleware set up and # the given +app+ as end point. def build(app) builder = Rack::Builder.new setup_default_middleware builder setup_middleware builder builder.run app builder end def call(env) synchronize { prototype.call(env) } end # Like Kernel#caller but excluding certain magic entries and without # line / method information; the resulting array contains filenames only. def caller_files cleaned_caller(1).flatten end private # Starts the server by running the Rack Handler. def start_server(handler, server_settings, handler_name) # Ensure we initialize middleware before startup, to match standard Rack # behavior, by ensuring an instance exists: prototype # Run the instance we created: handler.run(self, **server_settings) do |server| unless suppress_messages? warn "== Sinatra (v#{Sinatra::VERSION}) has taken the stage on #{port} for #{environment} with backup from #{handler_name}" end setup_traps set :running_server, server set :handler_name, handler_name server.threaded = settings.threaded if server.respond_to? :threaded= yield server if block_given? end end def suppress_messages? handler_name =~ /cgi/i || quiet end def setup_traps return unless traps? at_exit { quit! } %i[INT TERM].each do |signal| old_handler = trap(signal) do quit! old_handler.call if old_handler.respond_to?(:call) end end set :traps, false end # Dynamically defines a method on settings. def define_singleton(name, content = Proc.new) singleton_class.class_eval do undef_method(name) if method_defined? name String === content ? class_eval("def #{name}() #{content}; end") : define_method(name, &content) end end # Condition for matching host name. Parameter might be String or Regexp. def host_name(pattern) condition { pattern === request.host } end # Condition for matching user agent. Parameter should be Regexp. # Will set params[:agent]. def user_agent(pattern) condition do if request.user_agent.to_s =~ pattern @params[:agent] = $~[1..-1] true else false end end end alias agent user_agent # Condition for matching mimetypes. Accepts file extensions. def provides(*types) types.map! { |t| mime_types(t) } types.flatten! condition do response_content_type = response['Content-Type'] preferred_type = request.preferred_type(types) if response_content_type types.include?(response_content_type) || types.include?(response_content_type[/^[^;]+/]) elsif preferred_type params = (preferred_type.respond_to?(:params) ? preferred_type.params : {}) content_type(preferred_type, params) true else false end end end def route(verb, path, options = {}, &block) enable :empty_path_info if path == '' && empty_path_info.nil? signature = compile!(verb, path, block, **options) (@routes[verb] ||= []) << signature invoke_hook(:route_added, verb, path, block) signature end def invoke_hook(name, *args) extensions.each { |e| e.send(name, *args) if e.respond_to?(name) } end def generate_method(method_name, &block) define_method(method_name, &block) method = instance_method method_name remove_method method_name method end def compile!(verb, path, block, **options) # Because of self.options.host host_name(options.delete(:host)) if options.key?(:host) # Pass Mustermann opts to compile() route_mustermann_opts = options.key?(:mustermann_opts) ? options.delete(:mustermann_opts) : {}.freeze options.each_pair { |option, args| send(option, *args) } pattern = compile(path, route_mustermann_opts) method_name = "#{verb} #{path}" unbound_method = generate_method(method_name, &block) conditions = @conditions @conditions = [] wrapper = block.arity.zero? ? proc { |a, _p| unbound_method.bind(a).call } : proc { |a, p| unbound_method.bind(a).call(*p) } [pattern, conditions, wrapper] end def compile(path, route_mustermann_opts = {}) Mustermann.new(path, **mustermann_opts.merge(route_mustermann_opts)) end def setup_default_middleware(builder) builder.use ExtendedRack builder.use ShowExceptions if show_exceptions? builder.use Rack::MethodOverride if method_override? builder.use Rack::Head setup_logging builder setup_sessions builder setup_protection builder end def setup_middleware(builder) middleware.each { |c, a, b| builder.use(c, *a, &b) } end def setup_logging(builder) if logging? setup_common_logger(builder) setup_custom_logger(builder) elsif logging == false setup_null_logger(builder) end end def setup_null_logger(builder) builder.use Rack::NullLogger end def setup_common_logger(builder) builder.use Sinatra::CommonLogger end def setup_custom_logger(builder) if logging.respond_to? :to_int builder.use Rack::Logger, logging else builder.use Rack::Logger end end def setup_protection(builder) return unless protection? options = Hash === protection ? protection.dup : {} options = { img_src: "'self' data:", font_src: "'self'" }.merge options protect_session = options.fetch(:session) { sessions? } options[:without_session] = !protect_session options[:reaction] ||= :drop_session builder.use Rack::Protection, options end def setup_sessions(builder) return unless sessions? options = {} options[:secret] = session_secret if session_secret? options.merge! sessions.to_hash if sessions.respond_to? :to_hash builder.use session_store, options end def inherited(subclass) subclass.reset! subclass.set :app_file, caller_files.first unless subclass.app_file? super end @@mutex = Mutex.new def synchronize(&block) if lock? @@mutex.synchronize(&block) else yield end end # used for deprecation warnings def warn_for_deprecation(message) warn message + "\n\tfrom #{cleaned_caller.first.join(':')}" end # Like Kernel#caller but excluding certain magic entries def cleaned_caller(keep = 3) caller(1) .map! { |line| line.split(/:(?=\d|in )/, 3)[0, keep] } .reject { |file, *_| callers_to_ignore.any? { |pattern| file =~ pattern } } end end # Force data to specified encoding. It defaults to settings.default_encoding # which is UTF-8 by default def self.force_encoding(data, encoding = default_encoding) return if data == settings || data.is_a?(Tempfile) if data.respond_to? :force_encoding data.force_encoding(encoding).encode! elsif data.respond_to? :each_value data.each_value { |v| force_encoding(v, encoding) } elsif data.respond_to? :each data.each { |v| force_encoding(v, encoding) } end data end def force_encoding(*args) settings.force_encoding(*args) end reset! set :environment, (ENV['APP_ENV'] || ENV['RACK_ENV'] || :development).to_sym set :raise_errors, proc { test? } set :dump_errors, proc { !test? } set :show_exceptions, proc { development? } set :sessions, false set :session_store, Rack::Protection::EncryptedCookie set :logging, false set :protection, true set :method_override, false set :use_code, false set :default_encoding, 'utf-8' set :x_cascade, true set :add_charset, %w[javascript xml xhtml+xml].map { |t| "application/#{t}" } settings.add_charset << %r{^text/} set :mustermann_opts, {} set :default_content_type, 'text/html' # explicitly generating a session secret eagerly to play nice with preforking begin require 'securerandom' set :session_secret, SecureRandom.hex(64) rescue LoadError, NotImplementedError # SecureRandom raises a NotImplementedError if no random device is available set :session_secret, format('%064x', Kernel.rand((2**256) - 1)) end class << self alias methodoverride? method_override? alias methodoverride= method_override= end set :run, false # start server via at-exit hook? set :running_server, nil set :handler_name, nil set :traps, true set :server, %w[HTTP webrick] set :bind, proc { development? ? 'localhost' : '0.0.0.0' } set :port, Integer(ENV['PORT'] && !ENV['PORT'].empty? ? ENV['PORT'] : 4567) set :quiet, false ruby_engine = defined?(RUBY_ENGINE) && RUBY_ENGINE server.unshift 'puma' server.unshift 'falcon' if ruby_engine != 'jruby' server.unshift 'mongrel' if ruby_engine.nil? server.unshift 'thin' if ruby_engine != 'jruby' server.unshift 'trinidad' if ruby_engine == 'jruby' set :absolute_redirects, true set :prefixed_redirects, false set :empty_path_info, nil set :strict_paths, true set :app_file, nil set :root, proc { app_file && File.expand_path(File.dirname(app_file)) } set :views, proc { root && File.join(root, 'views') } set :reload_templates, proc { development? } set :lock, false set :threaded, true set :public_folder, proc { root && File.join(root, 'public') } set :static, proc { public_folder && File.exist?(public_folder) } set :static_cache_control, false error ::Exception do response.status = 500 content_type 'text/html' '

Internal Server Error

' end configure :development do get '/__sinatra__/:image.png' do filename = __dir__ + "/images/#{params[:image].to_i}.png" content_type :png send_file filename end error NotFound do content_type 'text/html' if instance_of?(Sinatra::Application) code = <<-RUBY.gsub(/^ {12}/, '') #{request.request_method.downcase} '#{request.path_info}' do "Hello World" end RUBY else code = <<-RUBY.gsub(/^ {12}/, '') class #{self.class} #{request.request_method.downcase} '#{request.path_info}' do "Hello World" end end RUBY file = settings.app_file.to_s.sub(settings.root.to_s, '').sub(%r{^/}, '') code = "# in #{file}\n#{code}" unless file.empty? end <<-HTML.gsub(/^ {10}/, '')

Sinatra doesn’t know this ditty.

Try this:
#{Rack::Utils.escape_html(code)}
HTML end end end # Execution context for classic style (top-level) applications. All # DSL methods executed on main are delegated to this class. # # The Application class should not be subclassed, unless you want to # inherit all settings, routes, handlers, and error pages from the # top-level. Subclassing Sinatra::Base is highly recommended for # modular applications. class Application < Base set :logging, proc { !test? } set :method_override, true set :run, proc { !test? } set :app_file, nil def self.register(*extensions, &block) # :nodoc: added_methods = extensions.flat_map(&:public_instance_methods) Delegator.delegate(*added_methods) super(*extensions, &block) end end # Sinatra delegation mixin. Mixing this module into an object causes all # methods to be delegated to the Sinatra::Application class. Used primarily # at the top-level. module Delegator # :nodoc: def self.delegate(*methods) methods.each do |method_name| define_method(method_name) do |*args, &block| return super(*args, &block) if respond_to? method_name Delegator.target.send(method_name, *args, &block) end # ensure keyword argument passing is compatible with ruby >= 2.7 ruby2_keywords(method_name) if respond_to?(:ruby2_keywords, true) private method_name end end delegate :get, :patch, :put, :post, :delete, :head, :options, :link, :unlink, :template, :layout, :before, :after, :error, :not_found, :configure, :set, :mime_type, :enable, :disable, :use, :development?, :test?, :production?, :helpers, :settings, :register class << self attr_accessor :target end self.target = Application end class Wrapper def initialize(stack, instance) @stack = stack @instance = instance end def settings @instance.settings end def helpers @instance end def call(env) @stack.call(env) end def inspect "#<#{@instance.class} app_file=#{settings.app_file.inspect}>" end end # Create a new Sinatra application; the block is evaluated in the class scope. def self.new(base = Base, &block) base = Class.new(base) base.class_eval(&block) if block_given? base end # Extend the top-level DSL with the modules provided. def self.register(*extensions, &block) Delegator.target.register(*extensions, &block) end # Include the helper modules provided in Sinatra's request context. def self.helpers(*extensions, &block) Delegator.target.helpers(*extensions, &block) end # Use the middleware for classic applications. def self.use(*args, &block) Delegator.target.use(*args, &block) end end sinatra-3.0.5/lib/sinatra/images/000077500000000000000000000000001434717561400166505ustar00rootroot00000000000000sinatra-3.0.5/lib/sinatra/images/404.png000066400000000000000000000447151434717561400177000ustar00rootroot00000000000000PNG  IHDR,)҈IIDATxA0p@ L妀 !HH?  AB@B>vΰGqLѭBd0WS>T]ή%vu W?+%T(=Cn] !3FO`Irfm`o/QܱA'tDvK1x;dOB%Lh {eƛ;'>Y]'sïF NsIݐ/ @-Y](B}dpL2Ah_V ~nF]'YKMDf\~S0!ڔeQy+%_1 kLft9g9W` !>aDH`$<#aXY^|jnqV7T z ˆLf{a⒟r%L8ILB.¸p3q8YQڒMtw@hz|ԫ$1;@32M# >Y,vçg0P\ۧQm,㰾aJ!T#x>u ~:}wcPZyR{S4!)G2w̱26˪ Ci\,a,ϛ!!&=9qF Hsea(\4W@[u60@ii+'_/Pt ܛ|L*OmJNKB&u~l6]C5 V2{(,8 Ui@n/W 36~tH,]kNV'=xӃh0SŧF\zhB ӊ~)9 [HB\;9Rlc?v$ 顥qwͲ@ȪA S_}H` D1S;9ן^!п{B9%Xs= !1L}H,zh{38!?y*dM@HO޵FuQ`/{ t( qtA҈U\AIi(I U*]J,p$(6I 3<`ό_#YWΌ@p?!z+]fᄟJG VrA.l/^흘( e4bbNB!@zNok7 #_"(bq6B" ET$OL'(09sO~v/~]Ǻ{>çO~`V ] !.Nw-—Kwp@W+׃Ep(:`{gff~wɩB4xuݿ~}O?|ܽBC u\vk@Xцk3[_-nIϿxwK(Zֶ퇛7oذ޽۷odz{Ǐ|BJ*R%M6Yf@b)$ΆJwn> k׮mƍoBtzX*;5%ĉwޅrDb$TtJd~suzLbsCdlgW! ]n*i!&"x\2xs2ћ7]?8x'OcwBNkG dh`:G7t^xLO&=^2JQ`wm̡+ϏLO#gg6%` O`l67::z/[PNIX A&iAzL}E<[Stp5Ro.[e|a[@ DKKK48?11>6O& 5Cxv,ݽeuuH$ b|~4"?13jhEan97ڹND\KAҌ3RP劋0R5)`N>d2U=C4754$\$AfX)In>SM\ ̐ 3G<@&bC ZSꉳJٹ\>!eOě|_.Ɋ9X9-U*Ŀ3x}; C F}-6éTk"|.1-.x.偺Bf$0>b.LVZ%~'pUu TB~EZ<?"6' "Bl:NDիWq]*'H#?3¡%%(AR $t5i6a@„d%PR$!G O-d:=p +pHUq (_5 D(F$ +T"Mvtlڴ h/HTMNM"/Y$O Ūw` +CJK^Mitޓۢ,_=\iU w%Oy~7o^n;?޶m+V@kE28-gl8^+u*[2$&bNt9ru&Sbb-+VΝ;n rxIN~ғBDL_:::`nUbP$oU~aDɟK#c-p\8LXFsw;mۏ2#w{{_ƍl&Gqhh諯Nwi;v ו9+N$: 5_9x2Ҁ딨$ąNMi*Nt֭~tԩ#G%n}_ӟ?v7ǎݽ{7_>|}uIwA\[`}xLHEb(5LN|'W^]~F)(jՁPioowzΕ5:t$F-|b+I`2D}g-(5]Ȓ8%PsrEn~H!EO7Uܓ'{Ē[x ԏFEmj<,n'Tt 4H :mٲe͚5𦭚h΄bZi/bBS"XN#LONb<@2p/vvvoCsX$[]Qgy肹{{A et Q NSZH-p(Wٳk.zK|tJh_p2|]@ȷX VxJv0-QШ"ȉIJ%8yk&y4m˃n?lkz(B5("HF@q#K a%|xDqJZ셒}QnJ1 e%tbW p88vu5X('mU! hdIOAbsK \Ξ.FPU]mMN5>jѝyBr~m󪁐0)nZ0;z4*~'h FJEL! w&Bxu~^+ʄ\SPr1fPF3O?XNV#,O2eY%yEvG;o< bnXf*Ą_~񇧧.^HK{&] dw~X!fB<'mz=2ev_]\h#~}i3KXo:aE# GZ۝kz9޿Y~*wʽr?E-{EqF@(琠{5}r\?"8#5heumuR.O] W MR0Auݭ}NGuTrC_.(h /$jk! 3I qCAC} 2Q̑i  uE/PMUTNs yV,kPz4ʀb㶫 #>zBr5^ݟ/A05P 18QC㮚Wi]42$ZܶI~!`xfU(]P<]8/*86<~0't"?|%nj!c]l?k$B)؇v_܏;3B_IρLkm;ad1N}kdTJ5N]ؒSs` Tj_Ns7B+9ni^з~̵Ԧ1kC{8q_.]b 1A}/}n߲ +TC<*>=ai=o.:k{AHNx^L·*B{4XMa^G8hRφ%/Dt&lOP .E4~x(X>ޙnOqT];d`LGB[(}v6 *bo7 p^XWˇm&^6Ey0)R2縷[μsĜg!#ߩ g9p#^XVH]4͗/WNEE.YGw {GE2AM,W%tԨ{dHtrt6-w r#?}{{w;lZLqǾ LM3G&VX@`'& `g!nƢ[#O\O8B{,iPY6 zBxTv|B/*\;gY]~7ǝ,QMeWPxx}1|{!lq p%n ݭ\/7 gc0Eខ;Y}R]ŷfB,v+9Y~y$( t7S7ĊXRn堔uB5;4cmZ-Ū4y!Ne 2`X? ]?:d"59/ z.9~.u~Rp w]:m¤J ʌQ[ ˚=B|eUf8w}VBǕ%ZU`'N__Jh|&ںVns4-ik2;=Σvkh˸>&ljh6΄LԶV԰i!ɉY4sCl8[Cn*qaB嫎 9o~OOC%gw;QLkә9/H{dyZW܎i=atwzLB{RC9뒲} u(>K@֬%'^Qg;6 XR/s!Gd!٦ 2ǜRܝ+o!pm2qǺjMU$ICn]цcx':DON|U"?t:,{DTsh޷9=br 6G|Bh|$!m(> x\ߨ*~I?i#RTwͻEJ+-Nzޣ`0bN7&CN`Yugnhh5뫜$N{'axn;A9 sbC,0 :>`{$DXBEtwѝz_?_g~GoA~4$AE{}%!Su]g1t\O ÿmڟ6hܖp7XwQywqWOT(Q`f|O Ӗ /s#Nu`C먏` ̹}|u?W'm=?'wgQ'LUB. L}k"W?[o="F}} 5pcd@~ș~8cC6].ǷA'6i*-zl`ňǎ~yBwb E{A}L7w>:<1ݰq򅶖8y&>bBy-(q5e2 cEa&3kM{y[A1ZdݎU ?^<~y2RVwdL79?ă}5՗<Ѻ7unnސRUxV򳭣/a61J,ͭkş>wd"5JB{Z7JyB<=׎{ƣǒ+ba.#ƽD(x(N=z֏qu1F+S&T?!?fp@oUbg)r::~}M]]cHw1L(?hC/.2c]TYw]fkmFz0X\ NRd ETE:O'4Ȝ:+1 ,pb·pʻh0[;ՆN.׫O KIWtF-rEYw"fD,Zoo ݋ծ4 ӎGX's/%&6U!$v s{*a+EJ.c{/i<~qmW[8^luE4^Хpxߧz<[?@?,CIQɓ'췆Tj/9!t߁A3=S.u>+%[hYvGm!]X׿>̏ ,cq/٧3N_} bm[n=W[곘0tH˓{{jK{rKQz<eRZ8nxFؚO<[.1#+cEQ&f;ܫ.]O+"=mݵH+ُ_NBNv%Ӧt_y/2Ď.QdWVvדٕ6śDýwEn +:sfF'qt. zl%[8t3g֒'g-"nYCѶtW9Gɇc7D+*B UP)^}wuޫyl`]9ED?<͙S@ OVW=!w{̴Xk-rҋ^~UsA1W"zwv"'^t 6:WztoLFEʄ 02 돋$qL u(!'Ov}`&MXT5 ٥ ӃH643${^_Xzȿ>v3隝UzN2ӭ59Kҽ=]gP MgckF]s+ޭߪKeT`O0LC+E}M raLB}%%*׮o+}&27?f ou(څJVE%]bx"׭(s,1$? S_o|>m59IPjHת㕯ftjHdΟ~Nv\=׶?.R**Uu^Ĺ3z;*+fҧꄴ#2銘Jd2O wy>avQ\J{ûr4V\{܃Pcv>؅gl]t])H' wtq(=p]9qqy>ug:H+^'XO(Y!cr|b CC7,/z>ehq'wQשKs%**e^݅\"k/̣isvҟl~xy$8S(n7*~~d\c1y&? +#OpIiqC[,#FMŞuw52 CI& v\^'ħ Tx MJcS{{#wGcKf?S0mz֣K$^S 3헂\Z_h#G+,8D׎{0nk4x{{O)?v4ٻ-rc9(8l0'OPb0_ ЛIΥ=x̹y6b|F8oM1wa.ܾic d7 a,ܽå|({ΕHlV4e<͂oxҹ>g;f(!Fa9Fm׃P1:IUR0dK@m $h`q q!q@<9׋AȝfK ^>;wP=*NN8O(I1v4֛>~I;ۈd3!w΋C|]եW|19ڻAw]ץtY-ת8;6^ BCq\W0/7TR:b1dGR\f {tOZlh}GtAގ<:?`⶜%=>i;l.6xV2ׅ -m |dPsF,.{Ò&2!0T腕|BT#@!m #Ey t2+·n2PfBPt89wTHoBjeddgL}g{֩DO|z+z;z'sp%ERzUG@ၐRbX^K~?kzOYp.;Zi{i}KdõPL{PKWТug#1<TP+i=B}y)$N$ ɇI \׭O_qK_w9n iKSm* 2pԏKrd@_QZ'!qR?4ud>L.!̰r@#D`h| 㭎gp/3gpn< %?ǸojlƪBQ?u6L?ḷ  b |nˑu^u(|mPwؕn#H:>OT)D(&?pZJ:߾,8%iɨ3R* $`pShN-ޗf6ր_uGmU%RoQSdff̙$vAž. aezT7f:OGl3.r4/NzX%^up՟ܾh~OL<ik0_S8Uʟj5,ߡ)~܁l6M1s`zMAvRSUeV|a~SF>>wɨpzw$eZͣ1*)J|`TKHY*>.bP %Q}o6w|K< -e"cߣ CkyFg-X ;+̇q쭁`\ @W? k4ӊzb< SVpV@ &i^{rS47޿7 E.X]e9*p8+XVX"%JMA =pf/cEe LlЩ.b@3Q2;N.#br>3]p.bsluݲq SiԿN7$*fRa"nKjӯP)Ir+{b#l]-iV|Y,3Lat[<:tu~##~DФ(\.8·Z.TFL>dMHdeJݨ~wE0 .3Y߬T ؔLYRCSeF ¨l 8pzI0pB|Ġ w"sePwje@݈oʋ*ApsB@¿\&h󪗮]ޑ5uZ<'YPa-M+%ɰ?WnAtgX 'BFGGFrƥ2~65O]8],B7ˊ24#U+ z*\pM2ǯ{u8Oh3#C;i!\\&ɕU"=a^%6}"n#@bIeM~uMO#v}28"X;J<~|[}E]M5~7-GaðWl&A9du1\&w@dpH wxNG:s@Kq}orcw 7)eT >j687 LݦGB1?9vDe! xhl WJЇ_->KMlfX@ȐMy]690F ɐLlD8ăoZQKMw!,cFq]ᨘc# `b(G.j*e@4O A^xG5侀`- j 5 @P94jhC3Voӫ9{>bc|}Ι9ܛf HR?d2Pt &嵣L$M^x̎<RP[}$ty\/'Sd|S LS/ALx@D r^yіc9rs!9!N>|Ǐܹsn ى7#lWP_~lf:--%!cʀ2!0P ER y3<Үp8r=}?_lw+..11\p>U]{lEyw^sSy&UQAL~fWju#:fB `|& I((mK%qI7%cc  h _b(U )a@'N45k*jީ]z͂ =6ݻx{_ %%o%o%j^+}O _ )LEQIciJ"Ε3 1 U'@ T,[q2 \ -4\ Y/PjlJ)V-ji{KD $LBHN͠$$"kM¶enUZk,>Y>:vR: ~0z$0::zۉsff5SCt\/rFp|?La,p[<&ٟj7LLB=+Mѓf$pp?Aw3BҒFw鳟}dIq6sSL&p86CoUŚ v1 4&]!}5Ku|wppZ#T4<qL< P--g *Fg$9P3)eOesη:,`b*kF٧r!dNL1'ĢPV#} 2B)8?٤ @őDV9h03y@BD1 VKòJՑ^e_N9D&&!!B.ƕ6^Rv.xMJo'>ذ!iu} yb+q2o,Jl&C{ t&^7ׯ_x1oRXQ$3#$`{,'K|e ƍKPlI9 Tf3A2j~xՊA6`B=$ Τ8Eݻ5㺒ԙ&' LL8{AKnܸT8L`$*qoLW&,l1 'Ao^N&XmeJ텘޺uvgΜp8y$ڦ&=7o\[[%F;H/'y$0-{Fn h O͓P*TpDBʄsƁ%*P$mēF-7Nh>+$d BAh#7 #fJIW/D g:Bȁ-D"MW y*L7t}QC GYλLNw EK ZiiNHoXz1wqrE^s 6"|A{%>%,+g o* n[Sj;(ȌϘyrߝifӘ sQ /8ks}>*O[ia>wc] enR"t*}Xpbc9[9q>ԶBUY  F:mrZ !vTT9qZ'K?ݖ`>KPVtJu@SB97|cQBQ0%%QBQB%%QBQB%˽ѣ'T?IENDB`sinatra-3.0.5/lib/sinatra/images/500.png000066400000000000000000000574721434717561400177010ustar00rootroot00000000000000PNG  IHDR93_<_IDATxXUGR6MnLlM5QQM,Ecu`EƂT,ذҥ (;ؕ"5o29{.pVE<QcLt˖DG!oXOw&NꪅK_4n˵/wz0qҤf #-դ&W` WLƏݼiöXb>`@"%KZԩKnƌ;gAXXhL6mjR,:R\ڵ+)&Mar y*.~m@8::bU&ݓwDGEj>!ݻU3䉫2Y6lءi-W>&U3䉫2Y XwߟҴؽk&hfWee5X!r߂ŊI7A;\5CZϡ+:yԬU[nݻnbA&W͐'ҁڵoCPrW/^t=ƛ |><\PNE Bz=o ҵx>V=udK.A!:&ƒ'"n0|Ĉp~ƛZUar޸I; :m`&lڼU!fM ߍкMw-½^Mo2fש[[ //Wy9:\P!c["y2:}6A€e+fΞ%Bԝ23O:|ҸIק;9߹}xy@*s#O4(bm;5`]tV|@?K@۷o7o@W5|UBzȑ5oa DhgZQj၃TRK}E/Q+&Wu dq0ːJJ.E(۵uߎO\{ZzEP.Mue ՒwMIMcRwMŅ*bÆQ%⠏UnZv4k 44j+WRy]QPC (*#)4ˮIwܱ;IIH(UP\%c Yfȧ\Ё*Ni#:^~`QC_{]>!b0\7o;u4>PUc*b8\0r*/A y顸0jNYMָz~'۵;~YAC]ժH,Z٫w)t? z%<59u4 N26wud=Ȧ,BW9*eS`3NYfw\ogQs/H6}'=KEZJ*6< DDdTa *,k}|g{ DZ+M5(\6}+We$ΐ06W6.^D7~byd.S]z|M]-'N^vEg(%( )6\ @D}!၃lhcB]8Z5Xd{LOjmUdP0W*aID"%b. b=.̉7BW?eg2&6IlO8100QԂTt nKPcx(Z֨YiӶ=CؙDr"e5'+E#8#Wj"/ ԁmsO2q*Wa >\9 BY*Ӳet7߂'yG`11"sUY351L@N坏6.層36ڍ8y(qxlCvGq&Z\%J]Y'Oڜ`K)Wvaҭcp2'Mj.C~(BkUY-.< %[X9KUn})k*srUVntteQPEWu*=^m(#͜lEzDՊ2"s 9*?Q`A>4FԢx Of(WD*0K~ <}W9 >sVVnl;!t Wۿ3OשĚnD,!T$Pg)WՔUUlh²b(DmvEZnL'y, \r"^*Ҵ'OlH6KU:("1tEȋaެ]k <D9W ʼ%sxl 7amכ2Z~GXjr5_rW{c} TyUہу-ˁD#F}s7\W,Os|-^.al$@XU,]fI{+*s2LoDI!Uk OUW4vZC!u :Kؒ"Rq#"PM[jY+|9ne'__~K7i>qkM7mع!᪤,\ ))igϝz*Jh ܈+n!9gd *\Yv`ZjDEeāl5= :W6&6No3[CW&lBWTJF\p_Ԇwgm:fqL֪4x(кM[(V圫) VMXF}4`r L6AWٚ&=&Wq@nٶ#5( "QNֹ+}^f(դYK\nqB޽4:yB"l$J ;\]q`bR*)Ŀl.'zB,DN;+_7,ya^]_ $"quM,6`Í8[(m݊ /X}#~{^ùח(y Uٲug(7…ٖ:@ M T){xF'P] ֶE{4d[n'BacMz4-Z1SU W/U,OsuGa!g\1$rv֭=^z70G&%f||g٧lںQ 3GÄqkB amƒdjZZmZ;˛+\Wsz'˹2Uj ?.>*8U"oJ.^)*s"s@g ṑx@{١U Aaڵ^G Ia*\њ7ns.۵RuP3n{Ŋk䏊uxW1GcU-_1wQy.2%n29'q=gQl#CT겯۶DG.]M._iv8 d_,5i֜Kjn:rԝw *wsU[zyZ9HxD>D=Ʋ{N=K$ۯw6/"}SWuBD%S WŋhiG3kq ttW&ۇ]? o-]2<,X pcovW'k_.\ ׁ*zN:EY۾gru;qf-~وӡͷK撫#F}![QjWnFSa&8t5 D;pdՅKCZ<,;vf,hͷyt:z WYd1rB\32x v֚F C,%q%w %96U[PO계s{|i |ŵ߀w5gN a6T*VZRqG#$Sa,WnD_w8A.Դt>bVS8ԩS2:w^ǨH^̭uyP,E+_5Y+WȤz3svJ4G~h\".ض};?=6F "j{KDnc[`eʖc,#W ;ܼ 38Wg)E ΝNuÈͽ򵠆U::9,4N)S|dTk!~*[%">f L7uvĦ|tyYQԥ3Œz䌻M5;}9&L,Tt 0e˖mH~YPeSV lMϜ= T܋?F6Fx,\[ݴN`X`kG]\l蕫5m"22]0F_*vGQ?Q">|ϣaPYˮOGO.]4TJ9|MQApUF,!߳wq*(NI#jWXg#[JaD&Kę8aJKbܶ3~ݢҶ(/y,U>ΊA;:-&M̡h'_6j^߸/CP۾DQz8t \K"[G(Blo /d [%^M!r 3k}leO^T8jB͛X_pl^-$`U`do|$0rk-"q&2Wf/X8tMP"i9)5,UT}8f1$E`[π8ΛXULSʔ-KE``h0DŅ ON ta.]uPtԨrڵJD81d p#wA^,Hڳ:D3q r>5XRp'"e yxiTs0Fﴯ5~2AUlC<'NBվt^,ECfG9i"!\im^^#aimV b&O xStrqŋ{"TpWDIg9ĉu㽃TRAƱL2DǬ11cCzʁFRkO^J-_bX-&y{z>uDĖx'*\QJZ:@5/%2(d@jD t)N{H3 G32;{.]1)\=wҔün. Ea,6*B'Nnn/ɽǙwgOCg_\s5W"Q Ve|*x =_"*4#G9 $@ &7:vQAc^N:PtZT/ /)T9ixҖ;񤓓6^ZIScТeX0M-A90Sc~o K!:zj ;1 d߁7 ǿ5`5`,k(oG٬D\ҸFZEXdJCqk|Io3aR2{lYj{ºGLp?%sA)Om\|ՊUkw/l)я(e ?m쉓?qӖ7˯BЇ#F.4Ïe=)Ѡ J5jt XeyVY0F@٠ R[?PDd6 ɧemI\*|$R 5ܶAyg̚knV3Y,5$PuJfTzISjnfIL7=_EW/"J$;=zӠQ*7ܵǍX\5Xg|D!%BDf3Fy՚/XDb0{Tկً*wߗZQ_9z|>lλ%tKSN9Tʦ[,ҸI7٫ϐa# բUێJaqW~rguv 9) 4lhACϽ zI0\U~|~!Ȅ6~Ҕ!åXP~η}qǗX/mZH!~4'SsxJGѳ:u ^\;+lذI?P}}+{̚iW{L€Us*T,CK8f7l3A>{7R,M@aa|+cO<[(ha,- #OPؿ bh) Kيb7 s|EJATq*lIDPѥnةK (7bB^*g3x4=I& gyWרI ?umۍ_ٵ/; d[N V9;^e3=t]?>zDlіi*~{CGZt( ~`Ҏ]!^.﫮Vw^)(9W&d4a#F .k2\6=naH,zaa #o`?_ڹ52|y h$jD`}ը٫O=0ÎK Fq"6;w*??E nܷ߀IStd,]1z 7lb, 9EKV<kȗ}ߊCPl}bGI<ˆJ`45򼰝Y3g]{A8p0A_ι-8lT;Ie˗9dauO~zNG*WEi -]\A3w'$G D5ЫO?ΙXqݲ` QVt%N5/mX+vEEbӷʕ*+qiztcOڭ;]bwrD P"U);eXաS Ō#ގ?xITp]*[n^=x -:=7HYjKH,VݲeyUu}ԆMoq):v ~M=O~zv*&B2ڵ Wa;蛜+.]qߌf]v3mEML1wmwԭ簑c&NE:U ;1JDR&2DK~w.W)o4 uDO2E&_f ۟~?2͑ -m20;wqZB@zfK` McRgz̓QK=O͝ͷV/ qY)ܵ(c.)eϛ8٦M7IڕF!iwneh۾;s[,؂6eZ{B/ptQgvRpF6wx' hKQBOiOn+-j !Y\x'*pI!&Mbq>O 6O+%緹\(TTs!w(DܖNX'~f \B;(zfA3bڰ8Kewjݖ۪IZFpH7c3UmRгw_qU Nۡc,WɴȁvϮԥO;$iב[Dt0u1U9lݶI ]uK%ܟJ6# <Ԏvy65~~;^I}_@|19WLhDTeBlp9 4@{T.'>(/ ֚.7mz֜,5+Ғu>W\Aϙ?f+_͂Qu{#smΙnF skc*B}Fׂ$dz$-[\7.DF}SC?bΡSŻb#3Scdo*/|& d=z{)WcSD ЩY|ZdvC Ǟ=w3K!:*Ͽ+1 }98;/J*x4%x4%JpNQV'8P . F#f4 ISI dΕĖ["d8xq F[)lpd{v~rT;-[n&-'/i@H(x9o G2|tfA~] WcEsb۴mж]xI*i6dd~ʥet ʝS59ʾp{6+PVY6 )?JPxvV_ddL $Y,%O:p5O#XkA)HK20gqMdCB3֛kǯ d9t!9EQ-y@@u?i/Mpyz<1^KBnՂMBI6|i'[C&ME)нg\g!LBX S|#zA}q&+SQɠ I{ ;l'' Rdrl v*butf>,!`9"?'&`eؠɉl N !*s\e$#M%= b Brsm|v3 \bt nԪm;>˙ On(ɸ!kOZIĎZ?Cb)"V~RwC(' bYOr䨱n>8ٵ{/+`>fVYA-LXt1Y,a$A66>#>Xtf*kB{brp}hm\ՕQD܏Hx-ΔA0DEWx؈Q;POH(9:N^KMj2S)ɳ\e<C>1|G%P$1PlCONp6:VKZhQIjao9PQv:3̈w@c̿PnvSJjB.@"tl?RVA2tJ60Cng<ᖪ,]~b.O/jzbi\r_QC 43 û#TY ؊\<`”Y豿67|?з[F6d۴s-]28nw(27°SH:8C:vTJƃNѵ?Uڡ|kz1`7l SYWU*W'0RMh޲W_󳔒"VXRnLiu"&NoΜͳY}J>6AG3QrWcM+׮suJY*&!K+WC2qJ}„ݥ[BD $3;۱&rUP6_gt$"4(w񲕬M>2z -FM@E}*h#kDa>[ҕ0EN%DK)kӟW{%wp׼] X]\x)8flAW5HzVa5n*Hݐ:cHNRrIQ1$XnMgN j#ק@.\~iZ.=zfwF[A-g+1`4%pҕu_K Y#шj\`ᒉS S:G0ՁG}6:嫣2HILz+PL2$-J*o-˰d>fiIkH@ D̝rdi@a=QWevm|m'iiX`3vG{fԐož$83)ҍ44ymvAt_`Lr֭{Tt=FKKNݎ՜j׭ץ{O//[VȮFSj.,Ze+?(<8QzXxrQ"#dN!C'߉W٥<5,W/*F95ag"Wzm>*U(C3.^!|;vb !{MsΝ9\M28ŸdRE KsTd?F*{ʗѶC磄ҾcgqS+*_9CGkE5w7nu5 fO:jD1SyX1f$pcj-YέgSeS* K5.NU__L\[s-Ҏ)PdA 3":hr12_.jX&d@LO8j}UטhѺ}hԴ׾ 3 .({jML\5-[!BԤY˜\߬b//wO/iܤi$'KSI G#Q'N6l5QƺQ0VƦx#ծSU 7j2U[i1H6SG9,{\HڀX!A!OJo\p) qՅͻU<ɼ@Bf,]C4xu|3":3sΚ=[tL4 y4M=-{x}'\NxV򣇩td8G2 0,4 v૮(H#@ڟ]ݍPs-Z=,ڶHM 8L6p'nzwkg|!@cҵ$dL^!ڇIcb@0aɲU@5Haμǃƹ Qpc9g(ZjJUb-SR4GGy@Gx[`k `W` 9 |QhK EKr$|Cow~W`9_Ո 3ZéhZ*LZq&aAsR.oVCR1SE7i*vܵ'{!v#x݉ #zk|ݹ~jn qoZG)tyƕk3S$ߒ.ƨL^˯˯8 Unׁz[7ئ qq> ,1&q5;⇫MX*Ȗ1t}L]ƼEqFJp0:`H++`Z%cU aZnשslsIr!]5kնLm"1T—ކcxޏ-[5$۷2;~x޵UN.ӍK/Y턹WBxeN9JlfJu٩ z+:/猳PtWr;JzxQ XZ Z|XAoAi^[=Y!taɲIpU%Pc1BsՐuz(z׭+JR6&.JPg{?83!g0ڟYց7hԴY+@2iO;=BR_(u54*0yF)41nsޏNST],'?X[ָjMnF gk%8UD~p5J #?칎3id.7("UuYtR`i !ңb=W zKC-+ڧ׀`ccG[ 0;j.A[^eE6T_o؅N=t~:g@BRLbڕ\ײM YH^E^x%_;+r2^גW\yu mcnV׾λ7RNhҴ 'I[tOآO9'X^WF>'U&ߛv̍xc$r5?;팳R,WM_PcY Οc,At KGC)s؛I]ҏgUFL٧*&Y,?؅GUDWM1[8qsq!-@JgCZB3gt2} 꿫 K6lϒٞ=,WnEv/*H uل| w֫P.iJƟm8poY49T]'0Y6'i͍!9C5漈uFL<:pӭvPŮ0PUf^:̙ r5+ `-a4{Ks=#nR[$RĒ4Wnj߭GrrEZv~Op9{IBBl$;IG (%#W3!jP}f-*8)3KT[p5e\zbuD@#TY|o]'yl8-þZ's"IN'j -gjIP*kH2~6!ܴ%V W0lP5eiu:m&T_ƘܖmYƂϐodN1jRcHu\̬8Mu ^3|[-A{@yK,˜ ] ֡(u=_ ?6 ēR@|W_s]tJZQcč6,? aBd~??b+jN!=hFz it[*2si3fglR|SLaLkj,eIwL4 ñԬ~Ȧ*tAX GCVvݻƛ7|H_i5(~ܰPh D-#G*܌b] 6,-}mH%mnqUSx'X1aCTlhlhw։$9,@ZnðĩrmWFMјRp+G9:DR:xJ/hX iRwXb,۟l ,]מiCy6^ΞGcKſ*UOFo7mN,? )fc|C=-l$&[*J=J=N EbӐS*_ 5ꄓN{4hG?JPohK-'3˸ϐ!C6ߧIfx.ǢP1 X[,(XZe)By\ Un땽i,?4_wDІ>(suQ.-|þM "%;5Roc V 0MS:MV}`GYnԸ~\zDMlƲCo[<:ޖ;a*܉X(rѩ7|;SC!pq2Y' ,ȗ!jYPH[("K\ U[NO[~;bz0Wh^M{weph. gdTSF66-JPpIr-W\b6LS.AP%d ڵaNὥ[4V.= H͞S-U5dB={i!$P-p!q=T_n |uEpr\mѪ+M:m:&j8ݲa 6DGr@dl=i7:qmƱЦ ?od6$EnCXM' $Ĵ g@u'6$piu9ފ8b[躱bmgvbwEDiIw~C&:F~ L@VCIhŵZlA`ruYɓ̛7CrpiF^d ~:_ʈU2Lզ<1U5sQS)P~b]DSоa73B5l9A@5,7d.uY\rzi</-,inT#6,g4w [ Nd?)+yj'Dl!\bGԸrx,\UpR~H yc(WB~-IQ~\Wp8b/z7q4Ԛ* '5!IH@і,Wv`V(%!%dz'fHT"˜s_-9+ٽ~SNUd(PH 7e8D@]{p*sE! DpuեdNC]a!+2 w!s@#q h`ޫI=*eicJY#P%4=x&(={;ɭ$D z?\KdzRhOX 񅒕FJZ'R9C@/ Vr:Up5AaH3Da"0;pz+?im^|eu8)PCU~U J00|:Ts<^ jM%bhQeR HӋ'fcyNn%N2f/nrEJ4*-ܞY'Xs68ilcȻkI'Raz5E? 0 BcB] {" êZ?K^N%PBԘnZ M*(j"%%ٵG 5h`N /Mb:d5A!ql_$r `s4؋%= iV3bfq䖕yXv TP.B\mԡ\en|N0ڸq erEb!=#4WY[wt]pu&6VtŻc|~+@חՑAJFu< W)?rA$Րа,_AJrCڗxkbؼܔߎ]ح -յ*ffw@;B&0_l]q Jv&ċC݉ G|x]oEfCҁ#es? )@K]ZQv.WhI4 f]J9ϐ3 .X1Lqu *(멕Y e4kЎS-Wإyщz™QX q\FG#E:OPGs,=p)MQ_3Ntu~$Y,߬lP%WDB @ 4Fa5WLUJSwee@ui:\1LETAiˏM'AOTC婨oUnлva)7yH#ٲ 4]ijdȑ-:h%+ 6yX+;n2q.e}h&MS,bjA.O {Oh8Rz&q k4G1 , E'9I g#eVh8bLpm48x4#}4髲%8 }A3$ پ0 ϔd(Ja6}caBT ='#åof@cyj{CXZi)lX #t" T~-FP۳+Q?vd &i."U^ ֔mw DBI/ٗYW̩Pf"))]>K嶏mzm*O:0[Uk.Sܫ-'[uway|ꓴ nK1źQ(Dg1̥c._y^0>6 :bOtTȼOKP?;fdҝs ٷM\H!P g:30nXveaٕɺ:evNԻYpI*r_E'$v3i-N:+:FWWp;RrBDa- mQіRc/}iRlcxu)^:Lz!:?[#A2;*\$crRsJ"[PtUsUҜ![p<.7[x@<7iWamr|R&:J3(is%-A>cg%ܛg->I̝mN[\3cS[6uUۑj^Xcpo)$+QwlO@Fn9tӂyd7(̾Gr4H|Cn~*t/SKZzs}ÐLU;c+5)3-ٵD֣jfcζ4CҦK0e} >W#VQM r$ү%O]Koo9_vRX /+\Ra=G̫*afG1pJ%O*h7:d۸Hg~P||puCYm5'X\=.d)EhDeaapvb IENDB`sinatra-3.0.5/lib/sinatra/indifferent_hash.rb000066400000000000000000000105421434717561400212320ustar00rootroot00000000000000# frozen_string_literal: true module Sinatra # A poor man's ActiveSupport::HashWithIndifferentAccess, with all the Rails-y # stuff removed. # # Implements a hash where keys :foo and "foo" are # considered to be the same. # # rgb = Sinatra::IndifferentHash.new # # rgb[:black] = '#000000' # symbol assignment # rgb[:black] # => '#000000' # symbol retrieval # rgb['black'] # => '#000000' # string retrieval # # rgb['white'] = '#FFFFFF' # string assignment # rgb[:white] # => '#FFFFFF' # symbol retrieval # rgb['white'] # => '#FFFFFF' # string retrieval # # Internally, symbols are mapped to strings when used as keys in the entire # writing interface (calling e.g. []=, merge). This mapping # belongs to the public interface. For example, given: # # hash = Sinatra::IndifferentHash.new(:a=>1) # # You are guaranteed that the key is returned as a string: # # hash.keys # => ["a"] # # Technically other types of keys are accepted: # # hash = Sinatra::IndifferentHash.new(:a=>1) # hash[0] = 0 # hash # => { "a"=>1, 0=>0 } # # But this class is intended for use cases where strings or symbols are the # expected keys and it is convenient to understand both as the same. For # example the +params+ hash in Sinatra. class IndifferentHash < Hash def self.[](*args) new.merge!(Hash[*args]) end def initialize(*args) args.map!(&method(:convert_value)) super(*args) end def default(*args) args.map!(&method(:convert_key)) super(*args) end def default=(value) super(convert_value(value)) end def assoc(key) super(convert_key(key)) end def rassoc(value) super(convert_value(value)) end def fetch(key, *args) args.map!(&method(:convert_value)) super(convert_key(key), *args) end def [](key) super(convert_key(key)) end def []=(key, value) super(convert_key(key), convert_value(value)) end alias store []= def key(value) super(convert_value(value)) end def key?(key) super(convert_key(key)) end alias has_key? key? alias include? key? alias member? key? def value?(value) super(convert_value(value)) end alias has_value? value? def delete(key) super(convert_key(key)) end # Added in Ruby 2.3 def dig(key, *other_keys) super(convert_key(key), *other_keys) end def fetch_values(*keys) keys.map!(&method(:convert_key)) super(*keys) end def slice(*keys) keys.map!(&method(:convert_key)) self.class[super(*keys)] end def values_at(*keys) keys.map!(&method(:convert_key)) super(*keys) end def merge!(*other_hashes) other_hashes.each do |other_hash| if other_hash.is_a?(self.class) super(other_hash) else other_hash.each_pair do |key, value| key = convert_key(key) value = yield(key, self[key], value) if block_given? && key?(key) self[key] = convert_value(value) end end end self end alias update merge! def merge(*other_hashes, &block) dup.merge!(*other_hashes, &block) end def replace(other_hash) super(other_hash.is_a?(self.class) ? other_hash : self.class[other_hash]) end def transform_values(&block) dup.transform_values!(&block) end def transform_values! super super(&method(:convert_value)) end def transform_keys(&block) dup.transform_keys!(&block) end def transform_keys! super super(&method(:convert_key)) end def select(*args, &block) return to_enum(:select) unless block_given? dup.tap { |hash| hash.select!(*args, &block) } end def reject(*args, &block) return to_enum(:reject) unless block_given? dup.tap { |hash| hash.reject!(*args, &block) } end def compact dup.tap(&:compact!) end private def convert_key(key) key.is_a?(Symbol) ? key.to_s : key end def convert_value(value) case value when Hash value.is_a?(self.class) ? value : self.class[value] when Array value.map(&method(:convert_value)) else value end end end end sinatra-3.0.5/lib/sinatra/main.rb000066400000000000000000000034411434717561400166560ustar00rootroot00000000000000# frozen_string_literal: true module Sinatra PARAMS_CONFIG = {} if ARGV.any? require 'optparse' parser = OptionParser.new do |op| op.on('-p port', 'set the port (default is 4567)') { |val| PARAMS_CONFIG[:port] = Integer(val) } op.on('-s server', 'specify rack server/handler') { |val| PARAMS_CONFIG[:server] = val } op.on('-q', 'turn on quiet mode (default is off)') { PARAMS_CONFIG[:quiet] = true } op.on('-x', 'turn on the mutex lock (default is off)') { PARAMS_CONFIG[:lock] = true } op.on('-e env', 'set the environment (default is development)') do |val| ENV['RACK_ENV'] = val PARAMS_CONFIG[:environment] = val.to_sym end op.on('-o addr', "set the host (default is (env == 'development' ? 'localhost' : '0.0.0.0'))") do |val| PARAMS_CONFIG[:bind] = val end end begin parser.parse!(ARGV.dup) rescue StandardError => e PARAMS_CONFIG[:optparse_error] = e end end require 'sinatra/base' class Application < Base # we assume that the first file that requires 'sinatra' is the # app_file. all other path related options are calculated based # on this path by default. set :app_file, caller_files.first || $0 set :run, proc { File.expand_path($0) == File.expand_path(app_file) } if run? && ARGV.any? error = PARAMS_CONFIG.delete(:optparse_error) raise error if error PARAMS_CONFIG.each { |k, v| set k, v } end end remove_const(:PARAMS_CONFIG) at_exit { Application.run! if $!.nil? && Application.run? } end # include would include the module in Object # extend only extends the `main` object extend Sinatra::Delegator class Rack::Builder include Sinatra::Delegator end sinatra-3.0.5/lib/sinatra/show_exceptions.rb000066400000000000000000000276111434717561400211600ustar00rootroot00000000000000# frozen_string_literal: true require 'rack/show_exceptions' module Sinatra # Sinatra::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 < Rack::ShowExceptions @@eats_errors = Object.new def @@eats_errors.flush(*) end def @@eats_errors.puts(*) end def initialize(app) @app = app end def call(env) @app.call(env) rescue Exception => e errors = env['rack.errors'] env['rack.errors'] = @@eats_errors if prefers_plain_text?(env) content_type = 'text/plain' body = dump_exception(e) else content_type = 'text/html' body = pretty(env, e) end env['rack.errors'] = errors [ 500, { 'Content-Type' => content_type, 'Content-Length' => body.bytesize.to_s }, [body] ] end def template TEMPLATE end private def bad_request?(exception) Sinatra::BadRequest === exception end def prefers_plain_text?(env) Request.new(env).preferred_type('text/plain', 'text/html') != 'text/html' && [/curl/].index { |item| item =~ env['HTTP_USER_AGENT'] } end def frame_class(frame) if frame.filename =~ %r{lib/sinatra.*\.rb} 'framework' elsif (defined?(Gem) && frame.filename.include?(Gem.dir)) || frame.filename =~ %r{/bin/(\w+)\z} 'system' else 'app' end end TEMPLATE = ERB.new <<-HTML # :nodoc: <%=h exception.class %> at <%=h path %>

BACKTRACE

(expand)

    <% id = 1 %> <% frames.each do |frame| %> <% if frame.context_line && frame.context_line != "#" %>
  • <%=h frame.filename %> in <%=h frame.function %>
  • <% if frame.pre_context %>
      <% frame.pre_context.each do |line| %>
    1. <%=h line %>
    2. <% end %>
    <% end %>
    1. <%= h frame.context_line %>
    <% if frame.post_context %>
      <% frame.post_context.each do |line| %>
    1. <%=h line %>
    2. <% end %>
    <% end %>
  • <% end %> <% id += 1 %> <% end %>
<% unless bad_request?(exception) %>

GET

<% if req.GET and not req.GET.empty? %> <% req.GET.sort_by { |k, v| k.to_s }.each { |key, val| %> <% } %>
Variable Value
<%=h key %>
<%=h val.inspect %>
<% else %>

No GET data.

<% end %>

POST

<% if req.POST and not req.POST.empty? %> <% req.POST.sort_by { |k, v| k.to_s }.each { |key, val| %> <% } %>
Variable Value
<%=h key %>
<%=h val.inspect %>
<% else %>

No POST data.

<% end %>
<% end %>
<% unless req.cookies.empty? %> <% req.cookies.each { |key, val| %> <% } %>
Variable Value
<%=h key %>
<%=h val.inspect %>
<% else %>

No cookie data.

<% end %>

Rack ENV

<% env.sort_by { |k, v| k.to_s }.each { |key, val| %> <% } %>
Variable Value
<%=h key %>
<%=h val %>

You're seeing this error because you have enabled the show_exceptions setting.

HTML end end sinatra-3.0.5/lib/sinatra/version.rb000066400000000000000000000001061434717561400174120ustar00rootroot00000000000000# frozen_string_literal: true module Sinatra VERSION = '3.0.5' end sinatra-3.0.5/rack-protection/000077500000000000000000000000001434717561400163005ustar00rootroot00000000000000sinatra-3.0.5/rack-protection/.gitignore000066400000000000000000000001741434717561400202720ustar00rootroot00000000000000# please add general patterns to your global ignore list # see https://github.com/github/gitignore#readme Gemfile.lock doc/ sinatra-3.0.5/rack-protection/.rspec000066400000000000000000000000511434717561400174110ustar00rootroot00000000000000--color --warnings --require spec_helper sinatra-3.0.5/rack-protection/Gemfile000066400000000000000000000005551434717561400176000ustar00rootroot00000000000000# frozen_string_literal: true source 'https://rubygems.org' # encoding: utf-8 gem 'rake' rack_version = ENV['rack'].to_s rack_version = nil if rack_version.empty? || (rack_version == 'stable') rack_version = { github: 'rack/rack' } if rack_version == 'main' gem 'rack', rack_version gem 'sinatra', path: '..' gemspec gem 'rack-test', github: 'rack/rack-test' sinatra-3.0.5/rack-protection/License000066400000000000000000000021461434717561400176100ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2011-2017 Konstantin Haase Copyright (c) 2015-2017 Zachary Scott 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. sinatra-3.0.5/rack-protection/README.md000066400000000000000000000065641434717561400175720ustar00rootroot00000000000000# Rack::Protection This gem protects against typical web attacks. Should work for all Rack apps, including Rails. # Usage Use all protections you probably want to use: ``` ruby # config.ru require 'rack/protection' use Rack::Protection run MyApp ``` Skip a single protection middleware: ``` ruby # config.ru require 'rack/protection' use Rack::Protection, :except => :path_traversal run MyApp ``` Use a single protection middleware: ``` ruby # config.ru require 'rack/protection' use Rack::Protection::AuthenticityToken run MyApp ``` # Prevented Attacks ## Cross Site Request Forgery Prevented by: * [`Rack::Protection::AuthenticityToken`][authenticity-token] (not included by `use Rack::Protection`) * [`Rack::Protection::FormToken`][form-token] (not included by `use Rack::Protection`) * [`Rack::Protection::JsonCsrf`][json-csrf] * [`Rack::Protection::RemoteReferrer`][remote-referrer] (not included by `use Rack::Protection`) * [`Rack::Protection::RemoteToken`][remote-token] * [`Rack::Protection::HttpOrigin`][http-origin] ## Cross Site Scripting Prevented by: * [`Rack::Protection::EscapedParams`][escaped-params] (not included by `use Rack::Protection`) * [`Rack::Protection::XSSHeader`][xss-header] (Internet Explorer and Chrome only) * [`Rack::Protection::ContentSecurityPolicy`][content-security-policy] ## Clickjacking Prevented by: * [`Rack::Protection::FrameOptions`][frame-options] ## Directory Traversal Prevented by: * [`Rack::Protection::PathTraversal`][path-traversal] ## Session Hijacking Prevented by: * [`Rack::Protection::SessionHijacking`][session-hijacking] ## Cookie Tossing Prevented by: * [`Rack::Protection::CookieTossing`][cookie-tossing] (not included by `use Rack::Protection`) ## IP Spoofing Prevented by: * [`Rack::Protection::IPSpoofing`][ip-spoofing] ## Helps to protect against protocol downgrade attacks and cookie hijacking Prevented by: * [`Rack::Protection::StrictTransport`][strict-transport] (not included by `use Rack::Protection`) # Installation gem install rack-protection # Instrumentation Instrumentation is enabled by passing in an instrumenter as an option. ``` use Rack::Protection, instrumenter: ActiveSupport::Notifications ``` The instrumenter is passed a namespace (String) and environment (Hash). The namespace is 'rack.protection' and the attack type can be obtained from the environment key 'rack.protection.attack'. [authenticity-token]: http://www.sinatrarb.com/protection/authenticity_token [content-security-policy]: http://www.sinatrarb.com/protection/content_security_policy [cookie-tossing]: http://www.sinatrarb.com/protection/cookie_tossing [escaped-params]: http://www.sinatrarb.com/protection/escaped_params [form-token]: http://www.sinatrarb.com/protection/form_token [frame-options]: http://www.sinatrarb.com/protection/frame_options [http-origin]: http://www.sinatrarb.com/protection/http_origin [ip-spoofing]: http://www.sinatrarb.com/protection/ip_spoofing [json-csrf]: http://www.sinatrarb.com/protection/json_csrf [path-traversal]: http://www.sinatrarb.com/protection/path_traversal [remote-referrer]: http://www.sinatrarb.com/protection/remote_referrer [remote-token]: http://www.sinatrarb.com/protection/remote_token [session-hijacking]: http://www.sinatrarb.com/protection/session_hijacking [strict-transport]: http://www.sinatrarb.com/protection/strict_transport [xss-header]: http://www.sinatrarb.com/protection/xss_header sinatra-3.0.5/rack-protection/Rakefile000066400000000000000000000037771434717561400177630ustar00rootroot00000000000000# frozen_string_literal: true $LOAD_PATH.unshift File.expand_path('lib', __dir__) begin require 'bundler' Bundler::GemHelper.install_tasks rescue LoadError => e warn e end desc 'run specs' task(:spec) { ruby '-S rspec' } namespace :doc do task :readmes do Dir.glob 'lib/rack/protection/*.rb' do |file| excluded_files = %w[lib/rack/protection/base.rb lib/rack/protection/version.rb] next if excluded_files.include?(file) doc = File.read(file)[/^ module Protection(\n)+( #[^\n]*\n)*/m].scan(/^ *#(?!#) ?(.*)\n/).join("\n") file = "doc/#{file[4..-4].tr('/_', '-')}.rdoc" Dir.mkdir 'doc' unless File.directory? 'doc' puts "writing #{file}" File.open(file, 'w') { |f| f << doc } end end task :index do doc = File.read('README.md') file = 'doc/rack-protection-readme.md' Dir.mkdir 'doc' unless File.directory? 'doc' puts "writing #{file}" File.open(file, 'w') { |f| f << doc } end task all: %i[readmes index] end desc 'generate documentation' task doc: 'doc:all' desc 'generate gemspec' task 'rack-protection.gemspec' do require 'rack/protection/version' content = File.binread 'rack-protection.gemspec' # fetch data fields = { authors: `git shortlog -sn`.force_encoding('utf-8').scan(/[^\d\s].*/), email: ['mail@zzak.io', 'konstantin.haase@gmail.com'], files: %w[License README.md Rakefile Gemfile rack-protection.gemspec] + Dir['lib/**/*'] } # insert data fields.each do |field, values| updated = " s.#{field} = [" updated << values.map { |v| "\n %p" % v }.join(',') updated << "\n ]" content.sub!(/ s\.#{field} = \[\n( .*\n)* \]/, updated) end # set version content.sub! /(s\.version.*=\s+).*/, "\\1\"#{Rack::Protection::VERSION}\"" # escape unicode content.gsub!(/./) { |c| c.bytesize > 1 ? "\\u{#{c.codepoints.first.to_s(16)}}" : c } File.open('rack-protection.gemspec', 'w') { |f| f << content } end task gemspec: 'rack-protection.gemspec' task default: :spec task test: :spec sinatra-3.0.5/rack-protection/lib/000077500000000000000000000000001434717561400170465ustar00rootroot00000000000000sinatra-3.0.5/rack-protection/lib/rack-protection.rb000066400000000000000000000000321434717561400224720ustar00rootroot00000000000000require 'rack/protection' sinatra-3.0.5/rack-protection/lib/rack/000077500000000000000000000000001434717561400177665ustar00rootroot00000000000000sinatra-3.0.5/rack-protection/lib/rack/protection.rb000066400000000000000000000066641434717561400225150ustar00rootroot00000000000000# frozen_string_literal: true require 'rack/protection/version' require 'rack' module Rack module Protection autoload :AuthenticityToken, 'rack/protection/authenticity_token' autoload :Base, 'rack/protection/base' autoload :CookieTossing, 'rack/protection/cookie_tossing' autoload :ContentSecurityPolicy, 'rack/protection/content_security_policy' autoload :Encryptor, 'rack/protection/encryptor' autoload :EncryptedCookie, 'rack/protection/encrypted_cookie' autoload :EscapedParams, 'rack/protection/escaped_params' autoload :FormToken, 'rack/protection/form_token' autoload :FrameOptions, 'rack/protection/frame_options' autoload :HttpOrigin, 'rack/protection/http_origin' autoload :IPSpoofing, 'rack/protection/ip_spoofing' autoload :JsonCsrf, 'rack/protection/json_csrf' autoload :PathTraversal, 'rack/protection/path_traversal' autoload :ReferrerPolicy, 'rack/protection/referrer_policy' autoload :RemoteReferrer, 'rack/protection/remote_referrer' autoload :RemoteToken, 'rack/protection/remote_token' autoload :SessionHijacking, 'rack/protection/session_hijacking' autoload :StrictTransport, 'rack/protection/strict_transport' autoload :XSSHeader, 'rack/protection/xss_header' def self.new(app, options = {}) # does not include: RemoteReferrer, AuthenticityToken and FormToken except = Array options[:except] use_these = Array options[:use] if options.fetch(:without_session, false) except += %i[session_hijacking remote_token] end Rack::Builder.new do # Off by default, unless added use ::Rack::Protection::AuthenticityToken, options if use_these.include? :authenticity_token use ::Rack::Protection::ContentSecurityPolicy, options if use_these.include? :content_security_policy use ::Rack::Protection::CookieTossing, options if use_these.include? :cookie_tossing use ::Rack::Protection::EscapedParams, options if use_these.include? :escaped_params use ::Rack::Protection::FormToken, options if use_these.include? :form_token use ::Rack::Protection::ReferrerPolicy, options if use_these.include? :referrer_policy use ::Rack::Protection::RemoteReferrer, options if use_these.include? :remote_referrer use ::Rack::Protection::StrictTransport, options if use_these.include? :strict_transport # On by default, unless skipped use ::Rack::Protection::FrameOptions, options unless except.include? :frame_options use ::Rack::Protection::HttpOrigin, options unless except.include? :http_origin use ::Rack::Protection::IPSpoofing, options unless except.include? :ip_spoofing use ::Rack::Protection::JsonCsrf, options unless except.include? :json_csrf use ::Rack::Protection::PathTraversal, options unless except.include? :path_traversal use ::Rack::Protection::RemoteToken, options unless except.include? :remote_token use ::Rack::Protection::SessionHijacking, options unless except.include? :session_hijacking use ::Rack::Protection::XSSHeader, options unless except.include? :xss_header run app end.to_app end end end sinatra-3.0.5/rack-protection/lib/rack/protection/000077500000000000000000000000001434717561400221545ustar00rootroot00000000000000sinatra-3.0.5/rack-protection/lib/rack/protection/authenticity_token.rb000066400000000000000000000176751434717561400264330ustar00rootroot00000000000000# frozen_string_literal: true require 'rack/protection' require 'securerandom' require 'openssl' require 'base64' module Rack module Protection ## # Prevented attack:: CSRF # Supported browsers:: all # More infos:: http://en.wikipedia.org/wiki/Cross-site_request_forgery # # This middleware only accepts requests other than GET, # HEAD, OPTIONS, TRACE if their given access # token matches the token included in the session. # # It checks the X-CSRF-Token header and the POST form # data. # # It is not OOTB-compatible with the {rack-csrf}[https://rubygems.org/gems/rack_csrf] gem. # For that, the following patch needs to be applied: # # Rack::Protection::AuthenticityToken.default_options(key: "csrf.token", authenticity_param: "_csrf") # # == Options # # [:authenticity_param] the name of the param that should contain # the token on a request. Default value: # "authenticity_token" # # [:key] the name of the param that should contain # the token in the session. Default value: # :csrf # # [:allow_if] a proc for custom allow/deny logic. Default value: # nil # # == Example: Forms application # # To show what the AuthenticityToken does, this section includes a sample # program which shows two forms. One with, and one without a CSRF token # The one without CSRF token field will get a 403 Forbidden response. # # Install the gem, then run the program: # # gem install 'rack-protection' # ruby server.rb # # Here is server.rb: # # require 'rack/protection' # # app = Rack::Builder.app do # use Rack::Session::Cookie, secret: 'secret' # use Rack::Protection::AuthenticityToken # # run -> (env) do # [200, {}, [ # <<~EOS # # # # # rack-protection minimal example # # #

Without Authenticity Token

#

This takes you to Forbidden

#
# # #
# #

With Authenticity Token

#

This successfully takes you to back to this form.

#
# # # #
# # # EOS # ]] # end # end # # Rack::Handler::WEBrick.run app # # == Example: Customize which POST parameter holds the token # # To customize the authenticity parameter for form data, use the # :authenticity_param option: # use Rack::Protection::AuthenticityToken, authenticity_param: 'your_token_param_name' class AuthenticityToken < Base TOKEN_LENGTH = 32 default_options authenticity_param: 'authenticity_token', key: :csrf, allow_if: nil def self.token(session, path: nil, method: :post) new(nil).mask_authenticity_token(session, path: path, method: method) end def self.random_token SecureRandom.urlsafe_base64(TOKEN_LENGTH, padding: false) end def accepts?(env) session = session(env) set_token(session) safe?(env) || valid_token?(env, env['HTTP_X_CSRF_TOKEN']) || valid_token?(env, Request.new(env).params[options[:authenticity_param]]) || options[:allow_if]&.call(env) rescue StandardError false end def mask_authenticity_token(session, path: nil, method: :post) set_token(session) token = if path && method per_form_token(session, path, method) else global_token(session) end mask_token(token) end GLOBAL_TOKEN_IDENTIFIER = '!real_csrf_token' private_constant :GLOBAL_TOKEN_IDENTIFIER private def set_token(session) session[options[:key]] ||= self.class.random_token end # Checks the client's masked token to see if it matches the # session token. def valid_token?(env, token) return false if token.nil? || !token.is_a?(String) || token.empty? session = session(env) begin token = decode_token(token) rescue ArgumentError # encoded_masked_token is invalid Base64 return false end # See if it's actually a masked token or not. We should be able # to handle any unmasked tokens that we've issued without error. if unmasked_token?(token) compare_with_real_token(token, session) elsif masked_token?(token) token = unmask_token(token) compare_with_global_token(token, session) || compare_with_real_token(token, session) || compare_with_per_form_token(token, session, Request.new(env)) else false # Token is malformed end end # Creates a masked version of the authenticity token that varies # on each request. The masking is used to mitigate SSL attacks # like BREACH. def mask_token(token) one_time_pad = SecureRandom.random_bytes(token.length) encrypted_token = xor_byte_strings(one_time_pad, token) masked_token = one_time_pad + encrypted_token encode_token(masked_token) end # Essentially the inverse of +mask_token+. def unmask_token(masked_token) # Split the token into the one-time pad and the encrypted # value and decrypt it token_length = masked_token.length / 2 one_time_pad = masked_token[0...token_length] encrypted_token = masked_token[token_length..] xor_byte_strings(one_time_pad, encrypted_token) end def unmasked_token?(token) token.length == TOKEN_LENGTH end def masked_token?(token) token.length == TOKEN_LENGTH * 2 end def compare_with_real_token(token, session) secure_compare(token, real_token(session)) end def compare_with_global_token(token, session) secure_compare(token, global_token(session)) end def compare_with_per_form_token(token, session, request) secure_compare(token, per_form_token(session, request.path.chomp('/'), request.request_method)) end def real_token(session) decode_token(session[options[:key]]) end def global_token(session) token_hmac(session, GLOBAL_TOKEN_IDENTIFIER) end def per_form_token(session, path, method) token_hmac(session, "#{path}##{method.downcase}") end def encode_token(token) Base64.urlsafe_encode64(token) end def decode_token(token) Base64.urlsafe_decode64(token) end def token_hmac(session, identifier) OpenSSL::HMAC.digest( OpenSSL::Digest.new('SHA256'), real_token(session), identifier ) end def xor_byte_strings(s1, s2) s2 = s2.dup size = s1.bytesize i = 0 while i < size s2.setbyte(i, s1.getbyte(i) ^ s2.getbyte(i)) i += 1 end s2 end end end end sinatra-3.0.5/rack-protection/lib/rack/protection/base.rb000066400000000000000000000065431434717561400234230ustar00rootroot00000000000000# frozen_string_literal: true require 'rack/protection' require 'rack/utils' require 'digest' require 'logger' require 'uri' module Rack module Protection class Base DEFAULT_OPTIONS = { reaction: :default_reaction, logging: true, message: 'Forbidden', encryptor: Digest::SHA1, session_key: 'rack.session', status: 403, allow_empty_referrer: true, report_key: 'protection.failed', html_types: %w[text/html application/xhtml text/xml application/xml] } attr_reader :app, :options def self.default_options(options) define_method(:default_options) { super().merge(options) } end def self.default_reaction(reaction) alias_method(:default_reaction, reaction) end def default_options DEFAULT_OPTIONS end def initialize(app, options = {}) @app = app @options = default_options.merge(options) end def safe?(env) %w[GET HEAD OPTIONS TRACE].include? env['REQUEST_METHOD'] end def accepts?(env) raise NotImplementedError, "#{self.class} implementation pending" end def call(env) unless accepts? env instrument env result = react env end result or app.call(env) end def react(env) result = send(options[:reaction], env) result if (Array === result) && (result.size == 3) end def warn(env, message) return unless options[:logging] l = options[:logger] || env['rack.logger'] || ::Logger.new(env['rack.errors']) l.warn(message) end def instrument(env) return unless (i = options[:instrumenter]) env['rack.protection.attack'] = self.class.name.split('::').last.downcase i.instrument('rack.protection', env) end def deny(env) warn env, "attack prevented by #{self.class}" [options[:status], { 'Content-Type' => 'text/plain' }, [options[:message]]] end def report(env) warn env, "attack reported by #{self.class}" env[options[:report_key]] = true end def session?(env) env.include? options[:session_key] end def session(env) return env[options[:session_key]] if session? env raise "you need to set up a session middleware *before* #{self.class}" end def drop_session(env) session(env).clear if session? env end def referrer(env) ref = env['HTTP_REFERER'].to_s return if !options[:allow_empty_referrer] && ref.empty? URI.parse(ref).host || Request.new(env).host rescue URI::InvalidURIError end def origin(env) env['HTTP_ORIGIN'] || env['HTTP_X_ORIGIN'] end def random_string(secure = defined? SecureRandom) secure ? SecureRandom.hex(16) : '%032x' % rand((2**128) - 1) rescue NotImplementedError random_string false end def encrypt(value) options[:encryptor].hexdigest value.to_s end def secure_compare(a, b) Rack::Utils.secure_compare(a.to_s, b.to_s) end alias default_reaction deny def html?(headers) return false unless (header = headers.detect { |k, _v| k.downcase == 'content-type' }) options[:html_types].include? header.last[%r{^\w+/\w+}] end end end end sinatra-3.0.5/rack-protection/lib/rack/protection/content_security_policy.rb000066400000000000000000000064771434717561400274770ustar00rootroot00000000000000# frozen_string_literal: true require 'rack/protection' module Rack module Protection ## # Prevented attack:: XSS and others # Supported browsers:: Firefox 23+, Safari 7+, Chrome 25+, Opera 15+ # # Description:: Content Security Policy, a mechanism web applications # can use to mitigate a broad class of content injection # vulnerabilities, such as cross-site scripting (XSS). # Content Security Policy is a declarative policy that lets # the authors (or server administrators) of a web application # inform the client about the sources from which the # application expects to load resources. # # More info:: W3C CSP Level 1 : https://www.w3.org/TR/CSP1/ (deprecated) # W3C CSP Level 2 : https://www.w3.org/TR/CSP2/ (current) # W3C CSP Level 3 : https://www.w3.org/TR/CSP3/ (draft) # https://developer.mozilla.org/en-US/docs/Web/Security/CSP # http://caniuse.com/#search=ContentSecurityPolicy # http://content-security-policy.com/ # https://securityheaders.io # https://scotthelme.co.uk/csp-cheat-sheet/ # http://www.html5rocks.com/en/tutorials/security/content-security-policy/ # # Sets the 'Content-Security-Policy[-Report-Only]' header. # # Options: ContentSecurityPolicy configuration is a complex topic with # several levels of support that has evolved over time. # See the W3C documentation and the links in the more info # section for CSP usage examples and best practices. The # CSP3 directives in the 'NO_ARG_DIRECTIVES' constant need to be # presented in the options hash with a boolean 'true' in order # to be used in a policy. # class ContentSecurityPolicy < Base default_options default_src: "'self'", report_only: false DIRECTIVES = %i[base_uri child_src connect_src default_src font_src form_action frame_ancestors frame_src img_src manifest_src media_src object_src plugin_types referrer reflected_xss report_to report_uri require_sri_for sandbox script_src style_src worker_src webrtc_src navigate_to prefetch_src].freeze NO_ARG_DIRECTIVES = %i[block_all_mixed_content disown_opener upgrade_insecure_requests].freeze def csp_policy directives = [] DIRECTIVES.each do |d| if options.key?(d) directives << "#{d.to_s.sub(/_/, '-')} #{options[d]}" end end # Set these key values to boolean 'true' to include in policy NO_ARG_DIRECTIVES.each do |d| if options.key?(d) && options[d].is_a?(TrueClass) directives << d.to_s.tr('_', '-') end end directives.compact.sort.join('; ') end def call(env) status, headers, body = @app.call(env) header = options[:report_only] ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy' headers[header] ||= csp_policy if html? headers [status, headers, body] end end end end sinatra-3.0.5/rack-protection/lib/rack/protection/cookie_tossing.rb000066400000000000000000000041721434717561400255240ustar00rootroot00000000000000# frozen_string_literal: true require 'rack/protection' require 'pathname' module Rack module Protection ## # Prevented attack:: Cookie Tossing # Supported browsers:: all # More infos:: https://github.com/blog/1466-yummy-cookies-across-domains # # Does not accept HTTP requests if the HTTP_COOKIE header contains more than one # session cookie. This does not protect against a cookie overflow attack. # # Options: # # session_key:: The name of the session cookie (default: 'rack.session') class CookieTossing < Base default_reaction :deny def call(env) status, headers, body = super response = Rack::Response.new(body, status, headers) request = Rack::Request.new(env) remove_bad_cookies(request, response) response.finish end def accepts?(env) cookie_header = env['HTTP_COOKIE'] cookies = Rack::Utils.parse_query(cookie_header, ';,') { |s| s } cookies.each do |k, v| if (k == session_key && Array(v).size > 1) || (k != session_key && Rack::Utils.unescape(k) == session_key) bad_cookies << k end end bad_cookies.empty? end def remove_bad_cookies(request, response) return if bad_cookies.empty? paths = cookie_paths(request.path) bad_cookies.each do |name| paths.each { |path| response.set_cookie name, empty_cookie(request.host, path) } end end def redirect(env) request = Request.new(env) warn env, "attack prevented by #{self.class}" [302, { 'Content-Type' => 'text/html', 'Location' => request.path }, []] end def bad_cookies @bad_cookies ||= [] end def cookie_paths(path) path = '/' if path.to_s.empty? paths = [] Pathname.new(path).descend { |p| paths << p.to_s } paths end def empty_cookie(host, path) { value: '', domain: host, path: path, expires: Time.at(0) } end def session_key @session_key ||= options[:session_key] end end end end sinatra-3.0.5/rack-protection/lib/rack/protection/encrypted_cookie.rb000066400000000000000000000215001434717561400260250ustar00rootroot00000000000000# frozen_string_literal: true require 'openssl' require 'zlib' require 'json' require 'rack/request' require 'rack/response' require 'rack/session/abstract/id' module Rack module Protection # Rack::Protection::EncryptedCookie provides simple cookie based session management. # By default, the session is a Ruby Hash stored as base64 encoded marshalled # data set to :key (default: rack.session). The object that encodes the # session data is configurable and must respond to +encode+ and +decode+. # Both methods must take a string and return a string. # # When the secret key is set, cookie data is checked for data integrity. # The old_secret key is also accepted and allows graceful secret rotation. # A legacy_hmac_secret is also accepted and is used to upgrade existing # sessions to the new encryption scheme. # # There is also a legacy_hmac_coder option which can be set if a non-default # coder was used for legacy session cookies. # # Example: # # use Rack::Protection::EncryptedCookie, # :key => 'rack.session', # :domain => 'foo.com', # :path => '/', # :expire_after => 2592000, # :secret => 'change_me', # :old_secret => 'old_secret' # # All parameters are optional. # # Example using legacy HMAC options # # Rack::Protection:EncryptedCookie.new(application, { # # The secret used for legacy HMAC cookies # legacy_hmac_secret: 'legacy secret', # # legacy_hmac_coder will default to Rack::Protection::EncryptedCookie::Base64::Marshal # legacy_hmac_coder: Rack::Protection::EncryptedCookie::Identity.new, # # legacy_hmac will default to OpenSSL::Digest::SHA1 # legacy_hmac: OpenSSL::Digest::SHA256 # }) # # Example of a cookie with no encoding: # # Rack::Protection::EncryptedCookie.new(application, { # :coder => Rack::Protection::EncryptedCookie::Identity.new # }) # # Example of a cookie with custom encoding: # # Rack::Protection::EncryptedCookie.new(application, { # :coder => Class.new { # def encode(str); str.reverse; end # def decode(str); str.reverse; end # }.new # }) # class EncryptedCookie < Rack::Session::Abstract::Persisted # Encode session cookies as Base64 class Base64 def encode(str) [str].pack('m0') end def decode(str) str.unpack1('m') end # Encode session cookies as Marshaled Base64 data class Marshal < Base64 def encode(str) super(::Marshal.dump(str)) end def decode(str) return unless str begin ::Marshal.load(super(str)) rescue StandardError nil end end end # N.B. Unlike other encoding methods, the contained objects must be a # valid JSON composite type, either a Hash or an Array. class JSON < Base64 def encode(obj) super(::JSON.dump(obj)) end def decode(str) return unless str begin ::JSON.parse(super(str)) rescue StandardError nil end end end class ZipJSON < Base64 def encode(obj) super(Zlib::Deflate.deflate(::JSON.dump(obj))) end def decode(str) return unless str ::JSON.parse(Zlib::Inflate.inflate(super(str))) rescue StandardError nil end end end # Use no encoding for session cookies class Identity def encode(str); str; end def decode(str); str; end end class Marshal def encode(str) ::Marshal.dump(str) end def decode(str) ::Marshal.load(str) if str end end attr_reader :coder def initialize(app, options = {}) # Assume keys are hex strings and convert them to raw byte strings for # actual key material @secrets = options.values_at(:secret, :old_secret).compact.map do |secret| [secret].pack('H*') end warn <<-MSG unless secure?(options) SECURITY WARNING: No secret option provided to Rack::Protection::EncryptedCookie. This poses a security threat. It is strongly recommended that you provide a secret to prevent exploits that may be possible from crafted cookies. This will not be supported in future versions of Rack, and future versions will even invalidate your existing user cookies. Called from: #{caller[0]}. MSG warn <<-MSG if @secrets.first && @secrets.first.length < 32 SECURITY WARNING: Your secret is not long enough. It must be at least 32 bytes long and securely random. To generate such a key for use you can run the following command: ruby -rsecurerandom -e 'p SecureRandom.hex(32)' Called from: #{caller[0]}. MSG if options.key?(:legacy_hmac_secret) @legacy_hmac = options.fetch(:legacy_hmac, OpenSSL::Digest::SHA1) # Multiply the :digest_length: by 2 because this value is the length of # the digest in bytes but session digest strings are encoded as hex # strings @legacy_hmac_length = @legacy_hmac.new.digest_length * 2 @legacy_hmac_secret = options[:legacy_hmac_secret] @legacy_hmac_coder = (options[:legacy_hmac_coder] ||= Base64::Marshal.new) else @legacy_hmac = false end # If encryption is used we can just use a default Marshal encoder # without Base64 encoding the results. # # If no encryption is used, rely on the previous default (Base64::Marshal) @coder = (options[:coder] ||= (@secrets.any? ? Marshal.new : Base64::Marshal.new)) super(app, options.merge!(cookie_only: true)) end private def find_session(req, _sid) data = unpacked_cookie_data(req) data = persistent_session_id!(data) [data['session_id'], data] end def extract_session_id(request) unpacked_cookie_data(request)['session_id'] end def unpacked_cookie_data(request) request.fetch_header(RACK_SESSION_UNPACKED_COOKIE_DATA) do |k| session_data = cookie_data = request.cookies[@key] # Try to decrypt with the first secret, if that returns nil, try # with old_secret unless @secrets.empty? session_data = Rack::Protection::Encryptor.decrypt_message(cookie_data, @secrets.first) session_data ||= Rack::Protection::Encryptor.decrypt_message(cookie_data, @secrets[1]) if @secrets.size > 1 end # If session_data is still nil, are there is a legacy HMAC # configured, try verify and parse the cookie that way if !session_data && @legacy_hmac digest = cookie_data.slice!(-@legacy_hmac_length..-1) cookie_data.slice!(-2..-1) # remove double dash session_data = cookie_data if digest_match?(cookie_data, digest) # Decode using legacy HMAC decoder request.set_header(k, @legacy_hmac_coder.decode(session_data) || {}) else request.set_header(k, coder.decode(session_data) || {}) end end end def persistent_session_id!(data, sid = nil) data ||= {} data['session_id'] ||= sid || generate_sid data end def write_session(req, session_id, session, _options) session = session.merge('session_id' => session_id) session_data = coder.encode(session) unless @secrets.empty? session_data = Rack::Protection::Encryptor.encrypt_message(session_data, @secrets.first) end if session_data.size > (4096 - @key.size) req.get_header(RACK_ERRORS).puts('Warning! Rack::Protection::EncryptedCookie data size exceeds 4K.') nil else session_data end end def delete_session(_req, _session_id, options) # Nothing to do here, data is in the client generate_sid unless options[:drop] end def digest_match?(data, digest) return false unless data && digest Rack::Utils.secure_compare(digest, generate_hmac(data)) end def generate_hmac(data) OpenSSL::HMAC.hexdigest(@legacy_hmac.new, @legacy_hmac_secret, data) end def secure?(options) @secrets.size >= 1 || (options[:coder] && options[:let_coder_handle_secure_encoding]) end end end end sinatra-3.0.5/rack-protection/lib/rack/protection/encryptor.rb000066400000000000000000000032701434717561400245300ustar00rootroot00000000000000# frozen_string_literal: true require 'openssl' module Rack module Protection module Encryptor CIPHER = 'aes-256-gcm' DELIMITER = '--' def self.base64_encode(str) [str].pack('m0') end def self.base64_decode(str) str.unpack1('m0') end def self.encrypt_message(data, secret, auth_data = '') raise ArgumentError, 'data cannot be nil' if data.nil? cipher = OpenSSL::Cipher.new(CIPHER) cipher.encrypt cipher.key = secret[0, cipher.key_len] # Rely on OpenSSL for the initialization vector iv = cipher.random_iv # This must be set to properly use AES GCM for the OpenSSL module cipher.auth_data = auth_data cipher_text = cipher.update(data) cipher_text << cipher.final "#{base64_encode cipher_text}#{DELIMITER}#{base64_encode iv}#{DELIMITER}#{base64_encode cipher.auth_tag}" end def self.decrypt_message(data, secret) return unless data cipher = OpenSSL::Cipher.new(CIPHER) cipher_text, iv, auth_tag = data.split(DELIMITER, 3).map! { |v| base64_decode(v) } # This check is from ActiveSupport::MessageEncryptor # see: https://github.com/ruby/openssl/issues/63 return if auth_tag.nil? || auth_tag.bytes.length != 16 cipher.decrypt cipher.key = secret[0, cipher.key_len] cipher.iv = iv cipher.auth_tag = auth_tag cipher.auth_data = '' decrypted_data = cipher.update(cipher_text) decrypted_data << cipher.final decrypted_data rescue OpenSSL::Cipher::CipherError, TypeError, ArgumentError nil end end end end sinatra-3.0.5/rack-protection/lib/rack/protection/escaped_params.rb000066400000000000000000000044711434717561400254560ustar00rootroot00000000000000# frozen_string_literal: true require 'rack/protection' require 'rack/utils' require 'tempfile' begin require 'escape_utils' rescue LoadError end module Rack module Protection ## # Prevented attack:: XSS # Supported browsers:: all # More infos:: http://en.wikipedia.org/wiki/Cross-site_scripting # # Automatically escapes Rack::Request#params so they can be embedded in HTML # or JavaScript without any further issues. # # Options: # escape:: What escaping modes to use, should be Symbol or Array of Symbols. # Available: :html (default), :javascript, :url class EscapedParams < Base extend Rack::Utils class << self alias escape_url escape public :escape_html end default_options escape: :html, escaper: defined?(EscapeUtils) ? EscapeUtils : self def initialize(*) super modes = Array options[:escape] @escaper = options[:escaper] @html = modes.include? :html @javascript = modes.include? :javascript @url = modes.include? :url return unless @javascript && (!@escaper.respond_to? :escape_javascript) raise('Use EscapeUtils for JavaScript escaping.') end def call(env) request = Request.new(env) get_was = handle(request.GET) post_was = begin handle(request.POST) rescue StandardError nil end app.call env ensure request.GET.replace get_was if get_was request.POST.replace post_was if post_was end def handle(hash) was = hash.dup hash.replace escape(hash) was end def escape(object) case object when Hash then escape_hash(object) when Array then object.map { |o| escape(o) } when String then escape_string(object) when Tempfile then object end end def escape_hash(hash) hash = hash.dup hash.each { |k, v| hash[k] = escape(v) } hash end def escape_string(str) str = @escaper.escape_url(str) if @url str = @escaper.escape_html(str) if @html str = @escaper.escape_javascript(str) if @javascript str end end end end sinatra-3.0.5/rack-protection/lib/rack/protection/form_token.rb000066400000000000000000000013661434717561400246520ustar00rootroot00000000000000# frozen_string_literal: true require 'rack/protection' module Rack module Protection ## # Prevented attack:: CSRF # Supported browsers:: all # More infos:: http://en.wikipedia.org/wiki/Cross-site_request_forgery # # Only accepts submitted forms if a given access token matches the token # included in the session. Does not expect such a token from Ajax request. # # This middleware is not used when using the Rack::Protection collection, # since it might be a security issue, depending on your application # # Compatible with rack-csrf. class FormToken < AuthenticityToken def accepts?(env) env['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest' or super end end end end sinatra-3.0.5/rack-protection/lib/rack/protection/frame_options.rb000066400000000000000000000024001434717561400253420ustar00rootroot00000000000000# frozen_string_literal: true require 'rack/protection' module Rack module Protection ## # Prevented attack:: Clickjacking # Supported browsers:: Internet Explorer 8, Firefox 3.6.9, Opera 10.50, # Safari 4.0, Chrome 4.1.249.1042 and later # More infos:: https://developer.mozilla.org/en/The_X-FRAME-OPTIONS_response_header # # Sets X-Frame-Options header to tell the browser avoid embedding the page # in a frame. # # Options: # # frame_options:: Defines who should be allowed to embed the page in a # frame. Use :deny to forbid any embedding, :sameorigin # to allow embedding from the same origin (default). class FrameOptions < Base default_options frame_options: :sameorigin def frame_options @frame_options ||= begin frame_options = options[:frame_options] frame_options = options[:frame_options].to_s.upcase unless frame_options.respond_to? :to_str frame_options.to_str end end def call(env) status, headers, body = @app.call(env) headers['X-Frame-Options'] ||= frame_options if html? headers [status, headers, body] end end end end sinatra-3.0.5/rack-protection/lib/rack/protection/http_origin.rb000066400000000000000000000027611434717561400250350ustar00rootroot00000000000000# frozen_string_literal: true require 'rack/protection' module Rack module Protection ## # Prevented attack:: CSRF # Supported browsers:: Google Chrome 2, Safari 4 and later # More infos:: http://en.wikipedia.org/wiki/Cross-site_request_forgery # http://tools.ietf.org/html/draft-abarth-origin # # Does not accept unsafe HTTP requests when value of Origin HTTP request header # does not match default or permitted URIs. # # If you want to permit a specific domain, you can pass in as the `:permitted_origins` option: # # use Rack::Protection, permitted_origins: ["http://localhost:3000", "http://127.0.01:3000"] # # The `:allow_if` option can also be set to a proc to use custom allow/deny logic. class HttpOrigin < Base DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 } default_reaction :deny default_options allow_if: nil def base_url(env) request = Rack::Request.new(env) port = ":#{request.port}" unless request.port == DEFAULT_PORTS[request.scheme] "#{request.scheme}://#{request.host}#{port}" end def accepts?(env) return true if safe? env return true unless (origin = env['HTTP_ORIGIN']) return true if base_url(env) == origin return true if options[:allow_if]&.call(env) permitted_origins = options[:permitted_origins] Array(permitted_origins).include? origin end end end end sinatra-3.0.5/rack-protection/lib/rack/protection/ip_spoofing.rb000066400000000000000000000013471434717561400250220ustar00rootroot00000000000000# frozen_string_literal: true require 'rack/protection' module Rack module Protection ## # Prevented attack:: IP spoofing # Supported browsers:: all # More infos:: http://blog.c22.cc/2011/04/22/surveymonkey-ip-spoofing/ # # Detect (some) IP spoofing attacks. class IPSpoofing < Base default_reaction :deny def accepts?(env) return true unless env.include? 'HTTP_X_FORWARDED_FOR' ips = env['HTTP_X_FORWARDED_FOR'].split(',').map(&:strip) return false if env.include?('HTTP_CLIENT_IP') && (!ips.include? env['HTTP_CLIENT_IP']) return false if env.include?('HTTP_X_REAL_IP') && (!ips.include? env['HTTP_X_REAL_IP']) true end end end end sinatra-3.0.5/rack-protection/lib/rack/protection/json_csrf.rb000066400000000000000000000034401434717561400244700ustar00rootroot00000000000000# frozen_string_literal: true require 'rack/protection' module Rack module Protection ## # Prevented attack:: CSRF # Supported browsers:: all # More infos:: http://flask.pocoo.org/docs/0.10/security/#json-security # http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx # # JSON GET APIs are vulnerable to being embedded as JavaScript when the # Array prototype has been patched to track data. Checks the referrer # even on GET requests if the content type is JSON. # # If request includes Origin HTTP header, defers to HttpOrigin to determine # if the request is safe. Please refer to the documentation for more info. # # The `:allow_if` option can be set to a proc to use custom allow/deny logic. class JsonCsrf < Base default_options allow_if: nil alias react deny def call(env) request = Request.new(env) status, headers, body = app.call(env) if has_vector?(request, headers) warn env, "attack prevented by #{self.class}" react_and_close(env, body) or [status, headers, body] else [status, headers, body] end end def has_vector?(request, headers) return false if request.xhr? return false if options[:allow_if]&.call(request.env) return false unless headers['Content-Type'].to_s.split(';', 2).first =~ %r{^\s*application/json\s*$} origin(request.env).nil? and referrer(request.env) != request.host end def react_and_close(env, body) reaction = react(env) close_body(body) if reaction reaction end def close_body(body) body.close if body.respond_to?(:close) end end end end sinatra-3.0.5/rack-protection/lib/rack/protection/path_traversal.rb000066400000000000000000000024031434717561400255170ustar00rootroot00000000000000# frozen_string_literal: true require 'rack/protection' module Rack module Protection ## # Prevented attack:: Directory traversal # Supported browsers:: all # More infos:: http://en.wikipedia.org/wiki/Directory_traversal # # Unescapes '/' and '.', expands +path_info+. # Thus GET /foo/%2e%2e%2fbar becomes GET /bar. class PathTraversal < Base def call(env) path_was = env['PATH_INFO'] env['PATH_INFO'] = cleanup path_was if path_was && !path_was.empty? app.call env ensure env['PATH_INFO'] = path_was end def cleanup(path) encoding = path.encoding dot = '.'.encode(encoding) slash = '/'.encode(encoding) backslash = '\\'.encode(encoding) parts = [] unescaped = path.gsub(/%2e/i, dot).gsub(/%2f/i, slash).gsub(/%5c/i, backslash) unescaped = unescaped.gsub(backslash, slash) unescaped.split(slash).each do |part| next if part.empty? || (part == dot) part == '..' ? parts.pop : parts << part end cleaned = slash + parts.join(slash) cleaned << slash if parts.any? && unescaped =~ (%r{/\.{0,2}$}) cleaned end end end end sinatra-3.0.5/rack-protection/lib/rack/protection/referrer_policy.rb000066400000000000000000000015021434717561400256720ustar00rootroot00000000000000# frozen_string_literal: true require 'rack/protection' module Rack module Protection ## # Prevented attack:: Secret leakage, third party tracking # Supported browsers:: mixed support # More infos:: https://www.w3.org/TR/referrer-policy/ # https://caniuse.com/#search=referrer-policy # # Sets Referrer-Policy header to tell the browser to limit the Referer header. # # Options: # referrer_policy:: The policy to use (default: 'strict-origin-when-cross-origin') class ReferrerPolicy < Base default_options referrer_policy: 'strict-origin-when-cross-origin' def call(env) status, headers, body = @app.call(env) headers['Referrer-Policy'] ||= options[:referrer_policy] [status, headers, body] end end end end sinatra-3.0.5/rack-protection/lib/rack/protection/remote_referrer.rb000066400000000000000000000010211434717561400256620ustar00rootroot00000000000000# frozen_string_literal: true require 'rack/protection' module Rack module Protection ## # Prevented attack:: CSRF # Supported browsers:: all # More infos:: http://en.wikipedia.org/wiki/Cross-site_request_forgery # # Does not accept unsafe HTTP requests if the Referer [sic] header is set to # a different host. class RemoteReferrer < Base default_reaction :deny def accepts?(env) safe?(env) or referrer(env) == Request.new(env).host end end end end sinatra-3.0.5/rack-protection/lib/rack/protection/remote_token.rb000066400000000000000000000011611434717561400251730ustar00rootroot00000000000000# frozen_string_literal: true require 'rack/protection' module Rack module Protection ## # Prevented attack:: CSRF # Supported browsers:: all # More infos:: http://en.wikipedia.org/wiki/Cross-site_request_forgery # # Only accepts unsafe HTTP requests if a given access token matches the token # included in the session *or* the request comes from the same origin. # # Compatible with rack-csrf. class RemoteToken < AuthenticityToken default_reaction :deny def accepts?(env) super or referrer(env) == Request.new(env).host end end end end sinatra-3.0.5/rack-protection/lib/rack/protection/session_hijacking.rb000066400000000000000000000021471434717561400261770ustar00rootroot00000000000000# frozen_string_literal: true require 'rack/protection' module Rack module Protection ## # Prevented attack:: Session Hijacking # Supported browsers:: all # More infos:: http://en.wikipedia.org/wiki/Session_hijacking # # Tracks request properties like the user agent in the session and empties # the session if those properties change. This essentially prevents attacks # from Firesheep. Since all headers taken into consideration can be # spoofed, too, this will not prevent determined hijacking attempts. class SessionHijacking < Base default_reaction :drop_session default_options tracking_key: :tracking, track: %w[HTTP_USER_AGENT] def accepts?(env) session = session env key = options[:tracking_key] if session.include? key session[key].all? { |k, v| v == encode(env[k]) } else session[key] = {} options[:track].each { |k| session[key][k] = encode(env[k]) } end end def encode(value) value.to_s.downcase end end end end sinatra-3.0.5/rack-protection/lib/rack/protection/strict_transport.rb000066400000000000000000000027361434717561400261350ustar00rootroot00000000000000# frozen_string_literal: true require 'rack/protection' module Rack module Protection ## # Prevented attack:: Protects against against protocol downgrade attacks and cookie hijacking. # Supported browsers:: all # More infos:: https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security # # browser will prevent any communications from being sent over HTTP # to the specified domain and will instead send all communications over HTTPS. # It also prevents HTTPS click through prompts on browsers. # # Options: # # max_age:: How long future requests to the domain should go over HTTPS; specified in seconds # include_subdomains:: If all present and future subdomains will be HTTPS # preload:: Allow this domain to be included in browsers HSTS preload list. See https://hstspreload.appspot.com/ class StrictTransport < Base default_options max_age: 31_536_000, include_subdomains: false, preload: false def strict_transport @strict_transport ||= begin strict_transport = "max-age=#{options[:max_age]}" strict_transport += '; includeSubDomains' if options[:include_subdomains] strict_transport += '; preload' if options[:preload] strict_transport.to_str end end def call(env) status, headers, body = @app.call(env) headers['Strict-Transport-Security'] ||= strict_transport [status, headers, body] end end end end sinatra-3.0.5/rack-protection/lib/rack/protection/version.rb000066400000000000000000000001371434717561400241670ustar00rootroot00000000000000# frozen_string_literal: true module Rack module Protection VERSION = '3.0.5' end end sinatra-3.0.5/rack-protection/lib/rack/protection/xss_header.rb000066400000000000000000000015771434717561400246400ustar00rootroot00000000000000# frozen_string_literal: true require 'rack/protection' module Rack module Protection ## # Prevented attack:: Non-permanent XSS # Supported browsers:: Internet Explorer 8+ and Chrome # More infos:: http://blogs.msdn.com/b/ie/archive/2008/07/01/ie8-security-part-iv-the-xss-filter.aspx # # Sets X-XSS-Protection header to tell the browser to block attacks. # # Options: # xss_mode:: How the browser should prevent the attack (default: :block) class XSSHeader < Base default_options xss_mode: :block, nosniff: true def call(env) status, headers, body = @app.call(env) headers['X-XSS-Protection'] ||= "1; mode=#{options[:xss_mode]}" if html? headers headers['X-Content-Type-Options'] ||= 'nosniff' if options[:nosniff] [status, headers, body] end end end end sinatra-3.0.5/rack-protection/lib/rack_protection.rb000066400000000000000000000000711434717561400225570ustar00rootroot00000000000000# frozen_string_literal: true require 'rack/protection' sinatra-3.0.5/rack-protection/rack-protection.gemspec000066400000000000000000000026031434717561400227520ustar00rootroot00000000000000# frozen_string_literal: true version = File.read(File.expand_path('../VERSION', __dir__)).strip Gem::Specification.new do |s| # general infos s.name = 'rack-protection' s.version = version s.description = 'Protect against typical web attacks, works with all Rack apps, including Rails.' s.homepage = 'http://sinatrarb.com/protection/' s.summary = s.description s.license = 'MIT' s.authors = ['https://github.com/sinatra/sinatra/graphs/contributors'] s.email = 'sinatrarb@googlegroups.com' s.files = Dir['lib/**/*.rb'] + [ 'License', 'README.md', 'Rakefile', 'Gemfile', 'rack-protection.gemspec' ] unless s.respond_to?(:metadata) raise <<-WARN RubyGems 2.0 or newer is required to protect against public gem pushes. You can update your rubygems version by running: gem install rubygems-update update_rubygems: gem update --system WARN end s.metadata = { 'source_code_uri' => 'https://github.com/sinatra/sinatra/tree/master/rack-protection', 'homepage_uri' => 'http://sinatrarb.com/protection/', 'documentation_uri' => 'https://www.rubydoc.info/gems/rack-protection', 'rubygems_mfa_required' => 'true' } s.required_ruby_version = '>= 2.6.0' # dependencies s.add_dependency 'rack' s.add_development_dependency 'rack-test', '~> 2' s.add_development_dependency 'rspec', '~> 3' end sinatra-3.0.5/rack-protection/spec/000077500000000000000000000000001434717561400172325ustar00rootroot00000000000000sinatra-3.0.5/rack-protection/spec/lib/000077500000000000000000000000001434717561400200005ustar00rootroot00000000000000sinatra-3.0.5/rack-protection/spec/lib/rack/000077500000000000000000000000001434717561400207205ustar00rootroot00000000000000sinatra-3.0.5/rack-protection/spec/lib/rack/protection/000077500000000000000000000000001434717561400231065ustar00rootroot00000000000000sinatra-3.0.5/rack-protection/spec/lib/rack/protection/authenticity_token_spec.rb000066400000000000000000000073321434717561400303640ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Rack::Protection::AuthenticityToken do let(:token) { described_class.random_token } let(:masked_token) { described_class.token(session) } let(:bad_token) { Base64.strict_encode64('badtoken') } let(:session) { { csrf: token } } it_behaves_like 'any rack application' it 'denies post requests without any token' do expect(post('/')).not_to be_ok end it 'accepts post requests with correct X-CSRF-Token header' do post('/', {}, 'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => token) expect(last_response).to be_ok end it 'accepts post requests with masked X-CSRF-Token header' do post('/', {}, 'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => masked_token) expect(last_response).to be_ok end it 'denies post requests with wrong X-CSRF-Token header' do post('/', {}, 'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => bad_token) expect(last_response).not_to be_ok end it 'accepts post form requests with correct authenticity_token field' do post('/', { 'authenticity_token' => token }, 'rack.session' => session) expect(last_response).to be_ok end it 'accepts post form requests with masked authenticity_token field' do post('/', { 'authenticity_token' => masked_token }, 'rack.session' => session) expect(last_response).to be_ok end it 'denies post form requests with wrong authenticity_token field' do post('/', { 'authenticity_token' => bad_token }, 'rack.session' => session) expect(last_response).not_to be_ok end it 'accepts post form requests with a valid per form token' do token = Rack::Protection::AuthenticityToken.token(session, path: '/foo') post('/foo', { 'authenticity_token' => token }, 'rack.session' => session) expect(last_response).to be_ok end it 'denies post form requests with an invalid per form token' do token = Rack::Protection::AuthenticityToken.token(session, path: '/foo') post('/bar', { 'authenticity_token' => token }, 'rack.session' => session) expect(last_response).not_to be_ok end it 'prevents ajax requests without a valid token' do expect(post('/', {}, 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest')).not_to be_ok end it 'allows for a custom authenticity token param' do mock_app do use Rack::Protection::AuthenticityToken, authenticity_param: 'csrf_param' run proc { |_e| [200, { 'Content-Type' => 'text/plain' }, ['hi']] } end post('/', { 'csrf_param' => token }, 'rack.session' => { csrf: token }) expect(last_response).to be_ok end it "sets a new csrf token for the session in env, even after a 'safe' request" do get('/', {}, {}) expect(env['rack.session'][:csrf]).not_to be_nil end it 'allows for a custom token session key' do mock_app do use Rack::Session::Cookie, key: 'rack.session' use Rack::Protection::AuthenticityToken, key: :_csrf run DummyApp end get '/' expect(env['rack.session'][:_csrf]).not_to be_nil end describe '.token' do it 'returns a unique masked version of the authenticity token' do expect(Rack::Protection::AuthenticityToken.token(session)).not_to eq(masked_token) end it 'sets a session authenticity token if one does not exist' do session = {} allow(Rack::Protection::AuthenticityToken).to receive(:random_token).and_return(token) allow_any_instance_of(Rack::Protection::AuthenticityToken).to receive(:mask_token).and_return(masked_token) Rack::Protection::AuthenticityToken.token(session) expect(session[:csrf]).to eq(token) end end describe '.random_token' do it 'generates a base64 encoded 32 character string' do expect(Base64.urlsafe_decode64(token).length).to eq(32) end end end sinatra-3.0.5/rack-protection/spec/lib/rack/protection/base_spec.rb000066400000000000000000000024301434717561400253560ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Rack::Protection::Base do subject { described_class.new(-> {}) } describe '#random_string' do it 'outputs a string of 32 characters' do expect(subject.random_string.length).to eq(32) end end describe '#referrer' do it 'Reads referrer from Referer header' do env = { 'HTTP_HOST' => 'foo.com', 'HTTP_REFERER' => 'http://bar.com/valid' } expect(subject.referrer(env)).to eq('bar.com') end it 'Reads referrer from Host header when Referer header is relative' do env = { 'HTTP_HOST' => 'foo.com', 'HTTP_REFERER' => '/valid' } expect(subject.referrer(env)).to eq('foo.com') end it 'Reads referrer from Host header when Referer header is missing' do env = { 'HTTP_HOST' => 'foo.com' } expect(subject.referrer(env)).to eq('foo.com') end it 'Returns nil when Referer header is missing and allow_empty_referrer is false' do env = { 'HTTP_HOST' => 'foo.com' } subject.options[:allow_empty_referrer] = false expect(subject.referrer(env)).to be_nil end it 'Returns nil when Referer header is invalid' do env = { 'HTTP_HOST' => 'foo.com', 'HTTP_REFERER' => 'http://bar.com/bad|uri' } expect(subject.referrer(env)).to be_nil end end end sinatra-3.0.5/rack-protection/spec/lib/rack/protection/content_security_policy_spec.rb000066400000000000000000000062371434717561400314350ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Rack::Protection::ContentSecurityPolicy do it_behaves_like 'any rack application' it 'should set the Content Security Policy' do expect( get('/', {}, 'wants' => 'text/html').headers['Content-Security-Policy'] ).to eq("default-src 'self'") end it 'should not set the Content Security Policy for other content types' do headers = get('/', {}, 'wants' => 'text/foo').headers expect(headers['Content-Security-Policy']).to be_nil expect(headers['Content-Security-Policy-Report-Only']).to be_nil end it 'should allow changing the protection settings' do mock_app do use Rack::Protection::ContentSecurityPolicy, default_src: 'none', script_src: 'https://cdn.mybank.net', style_src: 'https://cdn.mybank.net', img_src: 'https://cdn.mybank.net', connect_src: 'https://api.mybank.com', frame_src: 'self', font_src: 'https://cdn.mybank.net', object_src: 'https://cdn.mybank.net', media_src: 'https://cdn.mybank.net', report_uri: '/my_amazing_csp_report_parser', sandbox: 'allow-scripts' run DummyApp end headers = get('/', {}, 'wants' => 'text/html').headers expect(headers['Content-Security-Policy']).to eq('connect-src https://api.mybank.com; default-src none; font-src https://cdn.mybank.net; frame-src self; img-src https://cdn.mybank.net; media-src https://cdn.mybank.net; object-src https://cdn.mybank.net; report-uri /my_amazing_csp_report_parser; sandbox allow-scripts; script-src https://cdn.mybank.net; style-src https://cdn.mybank.net') expect(headers['Content-Security-Policy-Report-Only']).to be_nil end it 'should allow setting CSP3 no arg directives' do mock_app do use Rack::Protection::ContentSecurityPolicy, block_all_mixed_content: true, disown_opener: true, upgrade_insecure_requests: true run DummyApp end headers = get('/', {}, 'wants' => 'text/html').headers expect(headers['Content-Security-Policy']).to eq("block-all-mixed-content; default-src 'self'; disown-opener; upgrade-insecure-requests") end it 'should ignore CSP3 no arg directives unless they are set to true' do mock_app do use Rack::Protection::ContentSecurityPolicy, block_all_mixed_content: false, disown_opener: 'false', upgrade_insecure_requests: 'foo' run DummyApp end headers = get('/', {}, 'wants' => 'text/html').headers expect(headers['Content-Security-Policy']).to eq("default-src 'self'") end it 'should allow changing report only' do # I have no clue what other modes are available mock_app do use Rack::Protection::ContentSecurityPolicy, report_uri: '/my_amazing_csp_report_parser', report_only: true run DummyApp end headers = get('/', {}, 'wants' => 'text/html').headers expect(headers['Content-Security-Policy']).to be_nil expect(headers['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; report-uri /my_amazing_csp_report_parser") end it 'should not override the header if already set' do mock_app with_headers('Content-Security-Policy' => 'default-src: none') expect(get('/', {}, 'wants' => 'text/html').headers['Content-Security-Policy']).to eq('default-src: none') end end sinatra-3.0.5/rack-protection/spec/lib/rack/protection/cookie_tossing_spec.rb000066400000000000000000000053731434717561400274740ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Rack::Protection::CookieTossing do it_behaves_like 'any rack application' context 'with default reaction' do before(:each) do mock_app do use Rack::Protection::CookieTossing run DummyApp end end it 'accepts requests with a single session cookie' do get '/', {}, 'HTTP_COOKIE' => 'rack.session=SESSION_TOKEN' expect(last_response).to be_ok end it 'denies requests with duplicate session cookies' do get '/', {}, 'HTTP_COOKIE' => 'rack.session=EVIL_SESSION_TOKEN; rack.session=SESSION_TOKEN' expect(last_response).not_to be_ok end it 'denies requests with sneaky encoded session cookies' do get '/', {}, 'HTTP_COOKIE' => 'rack.session=EVIL_SESSION_TOKEN; rack.%73ession=SESSION_TOKEN' expect(last_response).not_to be_ok end it 'adds the correct Set-Cookie header' do get '/some/path', {}, 'HTTP_COOKIE' => 'rack.%73ession=EVIL_SESSION_TOKEN; rack.session=EVIL_SESSION_TOKEN; rack.session=SESSION_TOKEN' expected_header = <<-END.chomp rack.%2573ession=; domain=example.org; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT rack.%2573ession=; domain=example.org; path=/some; expires=Thu, 01 Jan 1970 00:00:00 GMT rack.%2573ession=; domain=example.org; path=/some/path; expires=Thu, 01 Jan 1970 00:00:00 GMT rack.session=; domain=example.org; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT rack.session=; domain=example.org; path=/some; expires=Thu, 01 Jan 1970 00:00:00 GMT rack.session=; domain=example.org; path=/some/path; expires=Thu, 01 Jan 1970 00:00:00 GMT END expect(last_response.headers['Set-Cookie']).to eq(expected_header) end end context 'with redirect reaction' do before(:each) do mock_app do use Rack::Protection::CookieTossing, reaction: :redirect run DummyApp end end it 'redirects requests with duplicate session cookies' do get '/', {}, 'HTTP_COOKIE' => 'rack.session=EVIL_SESSION_TOKEN; rack.session=SESSION_TOKEN' expect(last_response).to be_redirect expect(last_response.location).to eq('/') end it 'redirects requests with sneaky encoded session cookies' do get '/path', {}, 'HTTP_COOKIE' => 'rack.%73ession=EVIL_SESSION_TOKEN; rack.session=SESSION_TOKEN' expect(last_response).to be_redirect expect(last_response.location).to eq('/path') end end context 'with custom session key' do it 'denies requests with duplicate session cookies' do mock_app do use Rack::Protection::CookieTossing, session_key: '_session' run DummyApp end get '/', {}, 'HTTP_COOKIE' => '_session=EVIL_SESSION_TOKEN; _session=SESSION_TOKEN' expect(last_response).not_to be_ok end end end sinatra-3.0.5/rack-protection/spec/lib/rack/protection/encrypted_cookie_spec.rb000066400000000000000000000436111434717561400300000ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Rack::Protection::EncryptedCookie do let(:incrementor) do lambda do |env| env['rack.session']['counter'] ||= 0 env['rack.session']['counter'] += 1 hash = env['rack.session'].dup hash.delete('session_id') Rack::Response.new(hash.inspect).to_a end end let(:session_id) do lambda do |env| Rack::Response.new(env['rack.session'].to_hash.inspect).to_a end end let(:session_option) do lambda do |opt| lambda do |env| Rack::Response.new(env['rack.session.options'][opt].inspect).to_a end end end let(:nothing) do lambda do |_env| Rack::Response.new('Nothing').to_a end end let(:renewer) do lambda do |env| env['rack.session.options'][:renew] = true Rack::Response.new('Nothing').to_a end end let(:only_session_id) do lambda do |env| Rack::Response.new(env['rack.session']['session_id'].to_s).to_a end end let(:bigcookie) do lambda do |env| env['rack.session']['cookie'] = 'big' * 3000 Rack::Response.new(env['rack.session'].inspect).to_a end end let(:destroy_session) do lambda do |env| env['rack.session'].destroy Rack::Response.new('Nothing').to_a end end def response_for(options = {}) request_options = options.fetch(:request, {}) cookie = if options[:cookie].is_a?(Rack::Response) options[:cookie]['Set-Cookie'] else options[:cookie] end request_options['HTTP_COOKIE'] = cookie || '' app_with_cookie = Rack::Protection::EncryptedCookie.new(*options[:app]) app_with_cookie = Rack::Lint.new(app_with_cookie) Rack::MockRequest.new(app_with_cookie).get('/', request_options) end def random_cipher_secret OpenSSL::Cipher.new('aes-256-gcm').random_key.unpack1('H*') end let(:secret) { random_cipher_secret } let(:warnings) { [] } before do local_warnings = warnings Rack::Protection::EncryptedCookie.class_eval do define_method(:warn) { |m| local_warnings << m } end end after do Rack::Protection::EncryptedCookie.class_eval { remove_method :warn } end describe 'Base64' do it 'uses base64 to encode' do coder = Rack::Protection::EncryptedCookie::Base64.new str = 'fuuuuu' expect(coder.encode(str)).to eq([str].pack('m0')) end it 'uses base64 to decode' do coder = Rack::Protection::EncryptedCookie::Base64.new str = ['fuuuuu'].pack('m0') expect(coder.decode(str)).to eq(str.unpack1('m0')) end it 'handles non-strict base64 encoding' do coder = Rack::Protection::EncryptedCookie::Base64.new str = ['A' * 256].pack('m') expect(coder.decode(str)).to eq('A' * 256) end describe 'Marshal' do it 'marshals and base64 encodes' do coder = Rack::Protection::EncryptedCookie::Base64::Marshal.new str = 'fuuuuu' expect(coder.encode(str)).to eq([::Marshal.dump(str)].pack('m0')) end it 'marshals and base64 decodes' do coder = Rack::Protection::EncryptedCookie::Base64::Marshal.new str = [::Marshal.dump('fuuuuu')].pack('m0') expect(coder.decode(str)).to eq(::Marshal.load(str.unpack1('m0'))) end it 'rescues failures on decode' do coder = Rack::Protection::EncryptedCookie::Base64::Marshal.new expect(coder.decode('lulz')).to be_nil end end describe 'JSON' do it 'JSON and base64 encodes' do coder = Rack::Protection::EncryptedCookie::Base64::JSON.new obj = %w[fuuuuu] expect(coder.encode(obj)).to eq([::JSON.dump(obj)].pack('m0')) end it 'JSON and base64 decodes' do coder = Rack::Protection::EncryptedCookie::Base64::JSON.new str = [::JSON.dump(%w[fuuuuu])].pack('m0') expect(coder.decode(str)).to eq(::JSON.parse(str.unpack1('m0'))) end it 'rescues failures on decode' do coder = Rack::Protection::EncryptedCookie::Base64::JSON.new expect(coder.decode('lulz')).to be_nil end end describe 'ZipJSON' do it 'jsons, deflates, and base64 encodes' do coder = Rack::Protection::EncryptedCookie::Base64::ZipJSON.new obj = %w[fuuuuu] json = JSON.dump(obj) expect(coder.encode(obj)).to eq([Zlib::Deflate.deflate(json)].pack('m0')) end it 'base64 decodes, inflates, and decodes json' do coder = Rack::Protection::EncryptedCookie::Base64::ZipJSON.new obj = %w[fuuuuu] json = JSON.dump(obj) b64 = [Zlib::Deflate.deflate(json)].pack('m0') expect(coder.decode(b64)).to eq(obj) end it 'rescues failures on decode' do coder = Rack::Protection::EncryptedCookie::Base64::ZipJSON.new expect(coder.decode('lulz')).to be_nil end end end it 'warns if no secret is given' do Rack::Protection::EncryptedCookie.new(incrementor) expect(warnings.first).to match(/no secret/i) warnings.clear Rack::Protection::EncryptedCookie.new(incrementor, secret: secret) expect(warnings).to be_empty end it 'warns if secret is to short' do Rack::Protection::EncryptedCookie.new(incrementor, secret: secret[0, 16]) expect(warnings.first).to match(/secret is not long enough/i) warnings.clear Rack::Protection::EncryptedCookie.new(incrementor, secret: secret) expect(warnings).to be_empty end it "doesn't warn if coder is configured to handle encoding" do Rack::Protection::EncryptedCookie.new( incrementor, coder: Object.new, let_coder_handle_secure_encoding: true ) expect(warnings).to be_empty end it 'still warns if coder is not set' do Rack::Protection::EncryptedCookie.new( incrementor, let_coder_handle_secure_encoding: true ) expect(warnings.first).to match(/no secret/i) end it 'uses a coder' do identity = Class.new do attr_reader :calls def initialize @calls = [] end def encode(str) @calls << :encode str end def decode(str) @calls << :decode str end end.new response = response_for(app: [incrementor, { coder: identity }]) expect(response['Set-Cookie']).to include('rack.session=') expect(response.body).to eq('{"counter"=>1}') expect(identity.calls).to eq(%i[decode encode]) end it 'creates a new cookie' do response = response_for(app: incrementor) expect(response['Set-Cookie']).to include('rack.session=') expect(response.body).to eq('{"counter"=>1}') end it 'loads from a cookie' do response = response_for(app: incrementor) response = response_for(app: incrementor, cookie: response) expect(response.body).to eq('{"counter"=>2}') response = response_for(app: incrementor, cookie: response) expect(response.body).to eq('{"counter"=>3}') end it 'renew session id' do response = response_for(app: incrementor) cookie = response['Set-Cookie'] response = response_for(app: only_session_id, cookie: cookie) cookie = response['Set-Cookie'] if response['Set-Cookie'] expect(response.body).to_not eq('') old_session_id = response.body response = response_for(app: renewer, cookie: cookie) cookie = response['Set-Cookie'] if response['Set-Cookie'] response = response_for(app: only_session_id, cookie: cookie) expect(response.body).to_not eq('') expect(response.body).to_not eq(old_session_id) end it 'destroys session' do response = response_for(app: incrementor) response = response_for(app: only_session_id, cookie: response) expect(response.body).to_not eq('') old_session_id = response.body response = response_for(app: destroy_session, cookie: response) response = response_for(app: only_session_id, cookie: response) expect(response.body).to_not eq('') expect(response.body).to_not eq(old_session_id) end it 'survives broken cookies' do response = response_for( app: incrementor, cookie: 'rack.session=blarghfasel' ) expect(response.body).to eq('{"counter"=>1}') response = response_for( app: [incrementor, { secret: secret }], cookie: 'rack.session=' ) expect(response.body).to eq('{"counter"=>1}') end it 'barks on too big cookies' do expect do response_for(app: bigcookie, request: { fatal: true }) end.to raise_error Rack::MockRequest::FatalWarning end it 'loads from a cookie with integrity hash' do app = [incrementor, { secret: secret }] response = response_for(app: app) response = response_for(app: app, cookie: response) expect(response.body).to eq('{"counter"=>2}') response = response_for(app: app, cookie: response) expect(response.body).to eq('{"counter"=>3}') app = [incrementor, { secret: random_cipher_secret }] response = response_for(app: app, cookie: response) expect(response.body).to eq('{"counter"=>1}') end it 'loads from a cookie with accept-only integrity hash for graceful key rotation' do response = response_for(app: [incrementor, { secret: secret }]) new_secret = random_cipher_secret app = [incrementor, { secret: new_secret, old_secret: secret }] response = response_for(app: app, cookie: response) expect(response.body).to eq('{"counter"=>2}') newer_secret = random_cipher_secret app = [incrementor, { secret: newer_secret, old_secret: new_secret }] response = response_for(app: app, cookie: response) expect(response.body).to eq('{"counter"=>3}') end it 'loads from a legacy hmac cookie' do legacy_session = Rack::Protection::EncryptedCookie::Base64::Marshal.new.encode({ 'counter' => 1, 'session_id' => 'abcdef' }) legacy_secret = 'test legacy secret' legacy_digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('SHA1'), legacy_secret, legacy_session) legacy_cookie = "rack.session=#{legacy_session}--#{legacy_digest}; path=/; HttpOnly" app = [incrementor, { secret: secret, legacy_hmac_secret: legacy_secret }] response = response_for(app: app, cookie: legacy_cookie) expect(response.body).to eq('{"counter"=>2}') end it 'ignores tampered with session cookies' do app = [incrementor, { secret: secret }] response = response_for(app: app) expect(response.body).to eq('{"counter"=>1}') response = response_for(app: app, cookie: response) expect(response.body).to eq('{"counter"=>2}') ctxt, iv, auth_tag = response['Set-Cookie'].split('--', 3) tampered_with_cookie = [ctxt, iv, auth_tag.reverse].join('--') response = response_for(app: app, cookie: tampered_with_cookie) expect(response.body).to eq('{"counter"=>1}') end it 'ignores tampered with legacy hmac cookie' do legacy_session = Rack::Protection::EncryptedCookie::Base64::Marshal.new.encode({ 'counter' => 1, 'session_id' => 'abcdef' }) legacy_secret = 'test legacy secret' legacy_digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('SHA1'), legacy_secret, legacy_session).reverse legacy_cookie = "rack.session=#{legacy_session}--#{legacy_digest}; path=/; HttpOnly" app = [incrementor, { secret: secret, legacy_hmac_secret: legacy_secret }] response = response_for(app: app, cookie: legacy_cookie) expect(response.body).to eq('{"counter"=>1}') end it 'supports either of secret or old_secret' do app = [incrementor, { secret: secret }] response = response_for(app: app) expect(response.body).to eq('{"counter"=>1}') response = response_for(app: app, cookie: response) expect(response.body).to eq('{"counter"=>2}') app = [incrementor, { old_secret: secret }] response = response_for(app: app) expect(response.body).to eq('{"counter"=>1}') response = response_for(app: app, cookie: response) expect(response.body).to eq('{"counter"=>2}') end it 'supports custom digest class for legacy hmac cookie' do legacy_hmac = OpenSSL::Digest::SHA256 legacy_session = Rack::Protection::EncryptedCookie::Base64::Marshal.new.encode({ 'counter' => 1, 'session_id' => 'abcdef' }) legacy_secret = 'test legacy secret' legacy_digest = OpenSSL::HMAC.hexdigest(legacy_hmac.new, legacy_secret, legacy_session) legacy_cookie = "rack.session=#{Rack::Utils.escape legacy_session}--#{legacy_digest}; path=/; HttpOnly" app = [incrementor, { secret: secret, legacy_hmac_secret: legacy_secret, legacy_hmac: legacy_hmac }] response = response_for(app: app, cookie: legacy_cookie) expect(response.body).to eq('{"counter"=>2}') response = response_for(app: app, cookie: response) expect(response.body).to eq('{"counter"=>3}') end it 'can handle Rack::Lint middleware' do response = response_for(app: incrementor) lint = Rack::Lint.new(session_id) response = response_for(app: lint, cookie: response) expect(response.body).to_not be_nil end it 'can handle middleware that inspects the env' do class TestEnvInspector def initialize(app) @app = app end def call(env) env.inspect @app.call(env) end end response = response_for(app: incrementor) inspector = TestEnvInspector.new(session_id) response = response_for(app: inspector, cookie: response) expect(response.body).to_not be_nil end it 'returns the session id in the session hash' do response = response_for(app: incrementor) expect(response.body).to eq('{"counter"=>1}') response = response_for(app: session_id, cookie: response) expect(response.body).to match(/"session_id"=>/) expect(response.body).to match(/"counter"=>1/) end it 'does not return a cookie if set to secure but not using ssl' do app = [incrementor, { secure: true }] response = response_for(app: app) expect(response['Set-Cookie']).to be_nil response = response_for(app: app, request: { 'HTTPS' => 'on' }) expect(response['Set-Cookie']).to_not be_nil expect(response['Set-Cookie']).to match(/secure/) end it 'does not return a cookie if cookie was not read/written' do response = response_for(app: nothing) expect(response['Set-Cookie']).to be_nil end it 'does not return a cookie if cookie was not written (only read)' do response = response_for(app: session_id) expect(response['Set-Cookie']).to be_nil end it 'returns even if not read/written if :expire_after is set' do app = [nothing, { expire_after: 3600 }] request = { 'rack.session' => { 'not' => 'empty' } } response = response_for(app: app, request: request) expect(response['Set-Cookie']).to_not be_nil end it 'returns no cookie if no data was written and no session was created previously, even if :expire_after is set' do app = [nothing, { expire_after: 3600 }] response = response_for(app: app) expect(response['Set-Cookie']).to be_nil end it "exposes :secret in env['rack.session.option']" do response = response_for(app: [session_option[:secret], { secret: secret }]) expect(response.body).to eq(secret.inspect) end it "exposes :coder in env['rack.session.option']" do response = response_for(app: session_option[:coder]) expect(response.body).to match(/Base64::Marshal/) end it 'exposes correct :coder when a secret is used' do response = response_for(app: session_option[:coder], secret: secret) expect(response.body).to match(/Marshal/) end it 'allows passing in a hash with session data from middleware in front' do request = { 'rack.session' => { foo: 'bar' } } response = response_for(app: session_id, request: request) expect(response.body).to match(/foo/) end it 'allows modifying session data with session data from middleware in front' do request = { 'rack.session' => { foo: 'bar' } } response = response_for(app: incrementor, request: request) expect(response.body).to match(/counter/) expect(response.body).to match(/foo/) end it "allows more than one '--' in the cookie when calculating legacy digests" do @counter = 0 app = lambda do |env| env['rack.session']['message'] ||= '' env['rack.session']['message'] << "#{@counter += 1}--" hash = env['rack.session'].dup hash.delete('session_id') Rack::Response.new(hash['message']).to_a end # another example of an unsafe coder is Base64.urlsafe_encode64 unsafe_coder = Class.new do def encode(hash); hash.inspect end def decode(str); eval(str) if str; end end.new legacy_session = unsafe_coder.encode('message' => "#{@counter += 1}--#{@counter += 1}--", 'session_id' => 'abcdef') legacy_secret = 'test legacy secret' legacy_digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('SHA1'), legacy_secret, legacy_session) legacy_cookie = "rack.session=#{Rack::Utils.escape legacy_session}--#{legacy_digest}; path=/; HttpOnly" _app = [app, { secret: secret, legacy_hmac_secret: legacy_secret, legacy_hmac_coder: unsafe_coder }] response = response_for(app: _app, cookie: legacy_cookie) expect(response.body).to eq('1--2--3--') end it 'allows for non-strict encoded cookie' do long_session_app = lambda do |env| env['rack.session']['value'] = 'A' * 256 env['rack.session']['counter'] = 1 hash = env['rack.session'].dup hash.delete('session_id') Rack::Response.new(hash.inspect).to_a end non_strict_coder = Class.new do def encode(str) [Marshal.dump(str)].pack('m') end def decode(str) return unless str Marshal.load(str.unpack1('m')) end end.new non_strict_response = response_for(app: [ long_session_app, { coder: non_strict_coder } ]) response = response_for(app: [ incrementor ], cookie: non_strict_response) expect(response.body).to match(%("value"=>"#{'A' * 256}")) expect(response.body).to match('"counter"=>2') expect(response.body).to match(/\A{[^}]+}\z/) end end sinatra-3.0.5/rack-protection/spec/lib/rack/protection/encryptor_spec.rb000066400000000000000000000032471434717561400265000ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Rack::Protection::Encryptor do let(:secret) do OpenSSL::Cipher.new(Rack::Protection::Encryptor::CIPHER).random_key end it 'encrypted message contains ciphertext iv and auth_tag' do msg = Rack::Protection::Encryptor.encrypt_message('hello world', secret) ctxt, iv, auth_tag = msg.split(Rack::Protection::Encryptor::DELIMITER, 3) expect(ctxt).not_to be_empty expect(iv).not_to be_empty expect(auth_tag).not_to be_empty end it 'encrypted message is decryptable' do cmsg = Rack::Protection::Encryptor.encrypt_message('hello world', secret) pmsg = Rack::Protection::Encryptor.decrypt_message(cmsg, secret) expect(pmsg).to eql('hello world') end it 'encryptor and decryptor handles overly long keys' do new_secret = "#{secret}abcdef123456" # These methos should truncate the long key (so OpenSSL raise exceptions) cmsg = Rack::Protection::Encryptor.encrypt_message('hello world', new_secret) pmsg = Rack::Protection::Encryptor.decrypt_message(cmsg, new_secret) expect(pmsg).to eq('hello world') end it 'decrypt returns nil for junk messages' do pmsg = Rack::Protection::Encryptor.decrypt_message('aaa--bbb-ccc', secret) expect(pmsg).to be_nil end it 'decrypt returns nil for tampered messages' do cmsg = Rack::Protection::Encryptor.encrypt_message('hello world', secret) csplit = cmsg.split(Rack::Protection::Encryptor::DELIMITER, 3) csplit[2] = csplit.last.reverse tampered_msg = csplit.join(Rack::Protection::Encryptor::DELIMITER) pmsg = Rack::Protection::Encryptor.decrypt_message(tampered_msg, secret) expect(pmsg).to be_nil end end sinatra-3.0.5/rack-protection/spec/lib/rack/protection/escaped_params_spec.rb000066400000000000000000000034551434717561400274230ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Rack::Protection::EscapedParams do it_behaves_like 'any rack application' context 'escaping' do it 'escapes html entities' do mock_app do |env| request = Rack::Request.new(env) [200, { 'Content-Type' => 'text/plain' }, [request.params['foo']]] end get '/', foo: '' expect(body).to eq('<bar>') end it 'leaves normal params untouched' do mock_app do |env| request = Rack::Request.new(env) [200, { 'Content-Type' => 'text/plain' }, [request.params['foo']]] end get '/', foo: 'bar' expect(body).to eq('bar') end it 'copes with nested arrays' do mock_app do |env| request = Rack::Request.new(env) [200, { 'Content-Type' => 'text/plain' }, [request.params['foo']['bar']]] end get '/', foo: { bar: '' } expect(body).to eq('<bar>') end it 'leaves cache-breaker params untouched' do mock_app do |_env| [200, { 'Content-Type' => 'text/plain' }, ['hi']] end get '/?95df8d9bf5237ad08df3115ee74dcb10' expect(body).to eq('hi') end it 'leaves TempFiles untouched' do mock_app do |env| request = Rack::Request.new(env) [200, { 'Content-Type' => 'text/plain' }, ["#{request.params['file'][:filename]}\n#{request.params['file'][:tempfile].read}\n#{request.params['other']}"]] end temp_file = File.open('_escaped_params_tmp_file', 'w') begin temp_file.write('hello world') temp_file.close post '/', file: Rack::Test::UploadedFile.new(temp_file.path), other: '' expect(body).to eq("_escaped_params_tmp_file\nhello world\n<bar>") ensure File.unlink(temp_file.path) end end end end sinatra-3.0.5/rack-protection/spec/lib/rack/protection/form_token_spec.rb000066400000000000000000000032421434717561400266110ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Rack::Protection::FormToken do let(:token) { described_class.random_token } let(:masked_token) { described_class.token(session) } let(:bad_token) { Base64.strict_encode64('badtoken') } let(:session) { { csrf: token } } it_behaves_like 'any rack application' it 'denies post requests without any token' do expect(post('/')).not_to be_ok end it 'accepts post requests with correct X-CSRF-Token header' do post('/', {}, 'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => token) expect(last_response).to be_ok end it 'accepts post requests with masked X-CSRF-Token header' do post('/', {}, 'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => masked_token) expect(last_response).to be_ok end it 'denies post requests with wrong X-CSRF-Token header' do post('/', {}, 'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => bad_token) expect(last_response).not_to be_ok end it 'accepts post form requests with correct authenticity_token field' do post('/', { 'authenticity_token' => token }, 'rack.session' => session) expect(last_response).to be_ok end it 'accepts post form requests with masked authenticity_token field' do post('/', { 'authenticity_token' => masked_token }, 'rack.session' => session) expect(last_response).to be_ok end it 'denies post form requests with wrong authenticity_token field' do post('/', { 'authenticity_token' => bad_token }, 'rack.session' => session) expect(last_response).not_to be_ok end it 'accepts ajax requests without a valid token' do expect(post('/', {}, 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest')).to be_ok end end sinatra-3.0.5/rack-protection/spec/lib/rack/protection/frame_options_spec.rb000066400000000000000000000024111434717561400273100ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Rack::Protection::FrameOptions do it_behaves_like 'any rack application' it 'should set the X-Frame-Options' do expect(get('/', {}, 'wants' => 'text/html').headers['X-Frame-Options']).to eq('SAMEORIGIN') end it 'should not set the X-Frame-Options for other content types' do expect(get('/', {}, 'wants' => 'text/foo').headers['X-Frame-Options']).to be_nil end it 'should allow changing the protection mode' do # I have no clue what other modes are available mock_app do use Rack::Protection::FrameOptions, frame_options: :deny run DummyApp end expect(get('/', {}, 'wants' => 'text/html').headers['X-Frame-Options']).to eq('DENY') end it 'should allow changing the protection mode to a string' do # I have no clue what other modes are available mock_app do use Rack::Protection::FrameOptions, frame_options: 'ALLOW-FROM foo' run DummyApp end expect(get('/', {}, 'wants' => 'text/html').headers['X-Frame-Options']).to eq('ALLOW-FROM foo') end it 'should not override the header if already set' do mock_app with_headers('X-Frame-Options' => 'allow') expect(get('/', {}, 'wants' => 'text/html').headers['X-Frame-Options']).to eq('allow') end end sinatra-3.0.5/rack-protection/spec/lib/rack/protection/http_origin_spec.rb000066400000000000000000000027111434717561400267740ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Rack::Protection::HttpOrigin do it_behaves_like 'any rack application' before(:each) do mock_app do use Rack::Protection::HttpOrigin run DummyApp end end %w[GET HEAD POST PUT DELETE].each do |method| it "accepts #{method} requests with no Origin" do expect(send(method.downcase, '/')).to be_ok end end %w[GET HEAD].each do |method| it "accepts #{method} requests with non-permitted Origin" do expect(send(method.downcase, '/', {}, 'HTTP_ORIGIN' => 'http://malicious.com')).to be_ok end end %w[GET HEAD POST PUT DELETE].each do |method| it "accepts #{method} requests when allow_if is true" do mock_app do use Rack::Protection::HttpOrigin, allow_if: ->(env) { env.key?('HTTP_ORIGIN') } run DummyApp end expect(send(method.downcase, '/', {}, 'HTTP_ORIGIN' => 'http://any.domain.com')).to be_ok end end %w[POST PUT DELETE].each do |method| it "denies #{method} requests with non-permitted Origin" do expect(send(method.downcase, '/', {}, 'HTTP_ORIGIN' => 'http://malicious.com')).not_to be_ok end it "accepts #{method} requests with permitted Origin" do mock_app do use Rack::Protection::HttpOrigin, permitted_origins: ['http://www.friend.com'] run DummyApp end expect(send(method.downcase, '/', {}, 'HTTP_ORIGIN' => 'http://www.friend.com')).to be_ok end end end sinatra-3.0.5/rack-protection/spec/lib/rack/protection/ip_spoofing_spec.rb000066400000000000000000000023541434717561400267650ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Rack::Protection::IPSpoofing do it_behaves_like 'any rack application' it 'accepts requests without X-Forward-For header' do get('/', {}, 'HTTP_CLIENT_IP' => '1.2.3.4', 'HTTP_X_REAL_IP' => '4.3.2.1') expect(last_response).to be_ok end it 'accepts requests with proper X-Forward-For header' do get('/', {}, 'HTTP_CLIENT_IP' => '1.2.3.4', 'HTTP_X_FORWARDED_FOR' => '192.168.1.20, 1.2.3.4, 127.0.0.1') expect(last_response).to be_ok end it 'denies requests where the client spoofs X-Forward-For but not the IP' do get('/', {}, 'HTTP_CLIENT_IP' => '1.2.3.4', 'HTTP_X_FORWARDED_FOR' => '1.2.3.5') expect(last_response).not_to be_ok end it 'denies requests where the client spoofs the IP but not X-Forward-For' do get('/', {}, 'HTTP_CLIENT_IP' => '1.2.3.5', 'HTTP_X_FORWARDED_FOR' => '192.168.1.20, 1.2.3.4, 127.0.0.1') expect(last_response).not_to be_ok end it 'denies requests where IP and X-Forward-For are spoofed but not X-Real-IP' do get('/', {}, 'HTTP_CLIENT_IP' => '1.2.3.5', 'HTTP_X_FORWARDED_FOR' => '1.2.3.5', 'HTTP_X_REAL_IP' => '1.2.3.4') expect(last_response).not_to be_ok end end sinatra-3.0.5/rack-protection/spec/lib/rack/protection/json_csrf_spec.rb000066400000000000000000000056421434717561400264420ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Rack::Protection::JsonCsrf do it_behaves_like 'any rack application' module DummyAppWithBody module Closeable def close @closed = true end def closed? @closed end end def self.body @body ||= begin body = ['ok'] body.extend(Closeable) body end end def self.call(env) Thread.current[:last_env] = env [200, { 'Content-Type' => 'application/json' }, body] end end describe 'json response' do before do mock_app { |_e| [200, { 'Content-Type' => 'application/json' }, []] } end it 'denies get requests with json responses with a remote referrer' do expect(get('/', {}, 'HTTP_REFERER' => 'http://evil.com')).not_to be_ok end it 'closes the body returned by the app if it denies the get request' do mock_app DummyAppWithBody do |_e| [200, { 'Content-Type' => 'application/json' }, []] end get('/', {}, 'HTTP_REFERER' => 'http://evil.com') expect(DummyAppWithBody.body).to be_closed end it 'accepts requests with json responses with a remote referrer when allow_if is true' do mock_app do use Rack::Protection::JsonCsrf, allow_if: ->(env) { env['HTTP_REFERER'] == 'http://good.com' } run proc { |_e| [200, { 'Content-Type' => 'application/json' }, []] } end expect(get('/', {}, 'HTTP_REFERER' => 'http://good.com')).to be_ok end it "accepts requests with json responses with a remote referrer when there's an origin header set" do expect(get('/', {}, 'HTTP_REFERER' => 'http://good.com', 'HTTP_ORIGIN' => 'http://good.com')).to be_ok end it "accepts requests with json responses with a remote referrer when there's an x-origin header set" do expect(get('/', {}, 'HTTP_REFERER' => 'http://good.com', 'HTTP_X_ORIGIN' => 'http://good.com')).to be_ok end it 'accepts get requests with json responses with a local referrer' do expect(get('/', {}, 'HTTP_REFERER' => '/')).to be_ok end it 'accepts get requests with json responses with no referrer' do expect(get('/', {})).to be_ok end it 'accepts XHR requests' do expect(get('/', {}, 'HTTP_REFERER' => 'http://evil.com', 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest')).to be_ok end end describe 'not json response' do it 'accepts get requests with 304 headers' do mock_app { |_e| [304, {}, []] } expect(get('/', {}).status).to eq(304) end end describe 'with drop_session as default reaction' do it 'still denies' do mock_app do use Rack::Protection, reaction: :drop_session run proc { |_e| [200, { 'Content-Type' => 'application/json' }, []] } end session = { foo: :bar } get('/', {}, 'HTTP_REFERER' => 'http://evil.com', 'rack.session' => session) expect(last_response).not_to be_ok end end end sinatra-3.0.5/rack-protection/spec/lib/rack/protection/path_traversal_spec.rb000066400000000000000000000024541434717561400274710ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Rack::Protection::PathTraversal do it_behaves_like 'any rack application' context 'escaping' do before do mock_app { |e| [200, { 'Content-Type' => 'text/plain' }, [e['PATH_INFO']]] } end %w[/foo/bar /foo/bar/ / /.f /a.x].each do |path| it("does not touch #{path.inspect}") { expect(get(path).body).to eq(path) } end { # yes, this is ugly, feel free to change that '/..' => '/', '/a/../b' => '/b', '/a/../b/' => '/b/', '/a/.' => '/a/', '/%2e.' => '/', '/a/%2E%2e/b' => '/b', '/a%2f%2E%2e%2Fb/' => '/b/', '//' => '/', '/%2fetc%2Fpasswd' => '/etc/passwd' }.each do |a, b| it("replaces #{a.inspect} with #{b.inspect}") { expect(get(a).body).to eq(b) } end it 'should be able to deal with PATH_INFO = nil (fcgi?)' do app = Rack::Protection::PathTraversal.new(proc { 42 }) expect(app.call({})).to eq(42) end end context "PATH_INFO's encoding" do before do @app = Rack::Protection::PathTraversal.new(proc { |e| [200, { 'Content-Type' => 'text/plain' }, [e['PATH_INFO'].encoding.to_s]] }) end it 'should remain unchanged as ASCII-8BIT' do body = @app.call({ 'PATH_INFO' => '/'.encode('ASCII-8BIT') })[2][0] expect(body).to eq('ASCII-8BIT') end end end sinatra-3.0.5/rack-protection/spec/lib/rack/protection/protection_spec.rb000066400000000000000000000106771434717561400266460ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Rack::Protection do it_behaves_like 'any rack application' it 'passes on options' do mock_app do use Rack::Protection, track: ['HTTP_FOO'] run proc { |_e| [200, { 'Content-Type' => 'text/plain' }, ['hi']] } end session = { foo: :bar } get '/', {}, 'rack.session' => session, 'HTTP_ACCEPT_ENCODING' => 'a' get '/', {}, 'rack.session' => session, 'HTTP_ACCEPT_ENCODING' => 'b' expect(session[:foo]).to eq(:bar) get '/', {}, 'rack.session' => session, 'HTTP_FOO' => 'BAR' expect(session).to be_empty end it 'passes errors through if :reaction => :report is used' do mock_app do use Rack::Protection, reaction: :report run proc { |e| [200, { 'Content-Type' => 'text/plain' }, [e['protection.failed'].to_s]] } end session = { foo: :bar } post('/', {}, 'rack.session' => session, 'HTTP_ORIGIN' => 'http://malicious.com') expect(last_response).to be_ok expect(body).to eq('true') end describe '#react' do it 'prevents attacks and warns about it' do io = StringIO.new mock_app do use Rack::Protection, logger: Logger.new(io) run DummyApp end post('/', {}, 'rack.session' => {}, 'HTTP_ORIGIN' => 'http://malicious.com') expect(io.string).to match(/prevented.*Origin/) end it 'reports attacks if reaction is to report' do io = StringIO.new mock_app do use Rack::Protection, reaction: :report, logger: Logger.new(io) run DummyApp end post('/', {}, 'rack.session' => {}, 'HTTP_ORIGIN' => 'http://malicious.com') expect(io.string).to match(/reported.*Origin/) expect(io.string).not_to match(/prevented.*Origin/) end it 'passes errors to reaction method if specified' do io = StringIO.new Rack::Protection::Base.send(:define_method, :special) { |*args| io << args.inspect } mock_app do use Rack::Protection, reaction: :special, logger: Logger.new(io) run DummyApp end post('/', {}, 'rack.session' => {}, 'HTTP_ORIGIN' => 'http://malicious.com') expect(io.string).to match(/HTTP_ORIGIN.*malicious.com/) expect(io.string).not_to match(/reported|prevented/) end end describe '#html?' do context 'given an appropriate content-type header' do subject { Rack::Protection::Base.new(nil).html? 'content-type' => 'text/html' } it { is_expected.to be_truthy } end context 'given an appropriate content-type header of text/xml' do subject { Rack::Protection::Base.new(nil).html? 'content-type' => 'text/xml' } it { is_expected.to be_truthy } end context 'given an appropriate content-type header of application/xml' do subject { Rack::Protection::Base.new(nil).html? 'content-type' => 'application/xml' } it { is_expected.to be_truthy } end context 'given an inappropriate content-type header' do subject { Rack::Protection::Base.new(nil).html? 'content-type' => 'image/gif' } it { is_expected.to be_falsey } end context 'given no content-type header' do subject { Rack::Protection::Base.new(nil).html?({}) } it { is_expected.to be_falsey } end end describe '#instrument' do let(:env) { { 'rack.protection.attack' => 'base' } } let(:instrumenter) { double('Instrumenter') } after do app.instrument(env) end context 'with an instrumenter specified' do let(:app) { Rack::Protection::Base.new(nil, instrumenter: instrumenter) } it { expect(instrumenter).to receive(:instrument).with('rack.protection', env) } end context 'with no instrumenter specified' do let(:app) { Rack::Protection::Base.new(nil) } it { expect(instrumenter).not_to receive(:instrument) } end end describe 'new' do it 'should allow disable session protection' do mock_app do use Rack::Protection, without_session: true run DummyApp end session = { foo: :bar } get '/', {}, 'rack.session' => session, 'HTTP_USER_AGENT' => 'a' get '/', {}, 'rack.session' => session, 'HTTP_USER_AGENT' => 'b' expect(session[:foo]).to eq :bar end it 'should allow disable CSRF protection' do mock_app do use Rack::Protection, without_session: true run DummyApp end post('/', {}, 'HTTP_REFERER' => 'http://example.com/foo', 'HTTP_HOST' => 'example.org') expect(last_response).to be_ok end end end sinatra-3.0.5/rack-protection/spec/lib/rack/protection/remote_referrer_spec.rb000066400000000000000000000017241434717561400276400ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Rack::Protection::RemoteReferrer do it_behaves_like 'any rack application' it 'accepts post requests with no referrer' do expect(post('/')).to be_ok end it 'does not accept post requests with no referrer if allow_empty_referrer is false' do mock_app do use Rack::Protection::RemoteReferrer, allow_empty_referrer: false run DummyApp end expect(post('/')).not_to be_ok end it 'should allow post request with a relative referrer' do expect(post('/', {}, 'HTTP_REFERER' => '/')).to be_ok end it 'accepts post requests with the same host in the referrer' do post('/', {}, 'HTTP_REFERER' => 'http://example.com/foo', 'HTTP_HOST' => 'example.com') expect(last_response).to be_ok end it 'denies post requests with a remote referrer' do post('/', {}, 'HTTP_REFERER' => 'http://example.com/foo', 'HTTP_HOST' => 'example.org') expect(last_response).not_to be_ok end end sinatra-3.0.5/rack-protection/spec/lib/rack/protection/remote_token_spec.rb000066400000000000000000000051531434717561400271440ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Rack::Protection::RemoteToken do let(:token) { described_class.random_token } let(:masked_token) { described_class.token(session) } let(:bad_token) { Base64.strict_encode64('badtoken') } let(:session) { { csrf: token } } it_behaves_like 'any rack application' it 'accepts post requests with no referrer' do expect(post('/')).to be_ok end it 'accepts post requests with a local referrer' do expect(post('/', {}, 'HTTP_REFERER' => '/')).to be_ok end it 'denies post requests with a remote referrer and no token' do post('/', {}, 'HTTP_REFERER' => 'http://example.com/foo', 'HTTP_HOST' => 'example.org') expect(last_response).not_to be_ok end it 'accepts post requests with a remote referrer and correct X-CSRF-Token header' do post('/', {}, 'HTTP_REFERER' => 'http://example.com/foo', 'HTTP_HOST' => 'example.org', 'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => token) expect(last_response).to be_ok end it 'accepts post requests with a remote referrer and masked X-CSRF-Token header' do post('/', {}, 'HTTP_REFERER' => 'http://example.com/foo', 'HTTP_HOST' => 'example.org', 'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => masked_token) expect(last_response).to be_ok end it 'denies post requests with a remote referrer and wrong X-CSRF-Token header' do post('/', {}, 'HTTP_REFERER' => 'http://example.com/foo', 'HTTP_HOST' => 'example.org', 'rack.session' => session, 'HTTP_X_CSRF_TOKEN' => bad_token) expect(last_response).not_to be_ok end it 'accepts post form requests with a remote referrer and correct authenticity_token field' do post('/', { 'authenticity_token' => token }, 'HTTP_REFERER' => 'http://example.com/foo', 'HTTP_HOST' => 'example.org', 'rack.session' => session) expect(last_response).to be_ok end it 'accepts post form requests with a remote referrer and masked authenticity_token field' do post('/', { 'authenticity_token' => masked_token }, 'HTTP_REFERER' => 'http://example.com/foo', 'HTTP_HOST' => 'example.org', 'rack.session' => session) expect(last_response).to be_ok end it 'denies post form requests with a remote referrer and wrong authenticity_token field' do post('/', { 'authenticity_token' => bad_token }, 'HTTP_REFERER' => 'http://example.com/foo', 'HTTP_HOST' => 'example.org', 'rack.session' => session) expect(last_response).not_to be_ok end end sinatra-3.0.5/rack-protection/spec/lib/rack/protection/session_hijacking_spec.rb000066400000000000000000000023321434717561400301370ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Rack::Protection::SessionHijacking do it_behaves_like 'any rack application' it 'accepts a session without changes to tracked parameters' do session = { foo: :bar } get '/', {}, 'rack.session' => session get '/', {}, 'rack.session' => session expect(session[:foo]).to eq(:bar) end it 'denies requests with a changing User-Agent header' do session = { foo: :bar } get '/', {}, 'rack.session' => session, 'HTTP_USER_AGENT' => 'a' get '/', {}, 'rack.session' => session, 'HTTP_USER_AGENT' => 'b' expect(session).to be_empty end it 'accepts requests with a changing Accept-Encoding header' do # this is tested because previously it led to clearing the session session = { foo: :bar } get '/', {}, 'rack.session' => session, 'HTTP_ACCEPT_ENCODING' => 'a' get '/', {}, 'rack.session' => session, 'HTTP_ACCEPT_ENCODING' => 'b' expect(session).not_to be_empty end it 'accepts requests with a changing Version header' do session = { foo: :bar } get '/', {}, 'rack.session' => session, 'HTTP_VERSION' => '1.0' get '/', {}, 'rack.session' => session, 'HTTP_VERSION' => '1.1' expect(session[:foo]).to eq(:bar) end end sinatra-3.0.5/rack-protection/spec/lib/rack/protection/strict_transport_spec.rb000066400000000000000000000027541434717561400301010ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Rack::Protection::StrictTransport do it_behaves_like 'any rack application' it 'should set the Strict-Transport-Security header' do expect(get('/', {}, 'wants' => 'text/html').headers['Strict-Transport-Security']).to eq('max-age=31536000') end it 'should allow changing the max-age option' do mock_app do use Rack::Protection::StrictTransport, max_age: 16_070_400 run DummyApp end expect(get('/', {}, 'wants' => 'text/html').headers['Strict-Transport-Security']).to eq('max-age=16070400') end it 'should allow switching on the include_subdomains option' do mock_app do use Rack::Protection::StrictTransport, include_subdomains: true run DummyApp end expect(get('/', {}, 'wants' => 'text/html').headers['Strict-Transport-Security']).to eq('max-age=31536000; includeSubDomains') end it 'should allow switching on the preload option' do mock_app do use Rack::Protection::StrictTransport, preload: true run DummyApp end expect(get('/', {}, 'wants' => 'text/html').headers['Strict-Transport-Security']).to eq('max-age=31536000; preload') end it 'should allow switching on all the options' do mock_app do use Rack::Protection::StrictTransport, preload: true, include_subdomains: true run DummyApp end expect(get('/', {}, 'wants' => 'text/html').headers['Strict-Transport-Security']).to eq('max-age=31536000; includeSubDomains; preload') end end sinatra-3.0.5/rack-protection/spec/lib/rack/protection/xss_header_spec.rb000066400000000000000000000036411434717561400265760ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe Rack::Protection::XSSHeader do it_behaves_like 'any rack application' it 'should set the X-XSS-Protection' do expect(get('/', {}, 'wants' => 'text/html;charset=utf-8').headers['X-XSS-Protection']).to eq('1; mode=block') end it 'should set the X-XSS-Protection for XHTML' do expect(get('/', {}, 'wants' => 'application/xhtml+xml').headers['X-XSS-Protection']).to eq('1; mode=block') end it 'should not set the X-XSS-Protection for other content types' do expect(get('/', {}, 'wants' => 'application/foo').headers['X-XSS-Protection']).to be_nil end it 'should allow changing the protection mode' do # I have no clue what other modes are available mock_app do use Rack::Protection::XSSHeader, xss_mode: :foo run DummyApp end expect(get('/', {}, 'wants' => 'application/xhtml').headers['X-XSS-Protection']).to eq('1; mode=foo') end it 'should not override the header if already set' do mock_app with_headers('X-XSS-Protection' => '0') expect(get('/', {}, 'wants' => 'text/html').headers['X-XSS-Protection']).to eq('0') end it 'should set the X-Content-Type-Options' do expect(get('/', {}, 'wants' => 'text/html').header['X-Content-Type-Options']).to eq('nosniff') end it 'should set the X-Content-Type-Options for other content types' do expect(get('/', {}, 'wants' => 'application/foo').header['X-Content-Type-Options']).to eq('nosniff') end it 'should allow changing the nosniff-mode off' do mock_app do use Rack::Protection::XSSHeader, nosniff: false run DummyApp end expect(get('/').headers['X-Content-Type-Options']).to be_nil end it 'should not override the header if already set X-Content-Type-Options' do mock_app with_headers('X-Content-Type-Options' => 'sniff') expect(get('/', {}, 'wants' => 'text/html').headers['X-Content-Type-Options']).to eq('sniff') end end sinatra-3.0.5/rack-protection/spec/spec_helper.rb000066400000000000000000000072741434717561400220620ustar00rootroot00000000000000# frozen_string_literal: true require 'rack/protection' require 'rack/test' require 'rack' Dir[File.expand_path('support/**/*.rb', __dir__)].sort.each { |f| require f } # This file was generated by the `rspec --init` command. Conventionally, all # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. # The generated `.rspec` file contains `--require spec_helper` which will cause this # file to always be loaded, without a need to explicitly require it in any files. # # Given that it is always loaded, you are encouraged to keep this file as # light-weight as possible. Requiring heavyweight dependencies from this file # will add to the boot time of your test suite on EVERY test run, even for an # individual file that may not need all of that loaded. Instead, make a # separate helper file that requires this one and then use it only in the specs # that actually need it. # # The `.rspec` file also contains a few flags that are not defaults but that # users commonly want. # # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration RSpec.configure do |config| # The settings below are suggested to provide a good initial experience # with RSpec, but feel free to customize to your heart's content. config.disable_monkey_patching! # These two settings work together to allow you to limit a spec run # to individual examples or groups you care about by tagging them with # `:focus` metadata. When nothing is tagged with `:focus`, all examples # get run. config.filter_run :focus config.run_all_when_everything_filtered = true # Many RSpec users commonly either run the entire suite or an individual # file, and it's useful to allow more verbose output when running an # individual spec file. if config.files_to_run.one? # Use the documentation formatter for detailed output, # unless a formatter has already been configured # (e.g. via a command-line flag). config.default_formatter = 'doc' end # Print the 10 slowest examples and example groups at the # end of the spec run, to help surface which specs are running # particularly slow. config.profile_examples = 5 # Run specs in random order to surface order dependencies. If you find an # order dependency and want to debug it, you can fix the order by providing # the seed, which is printed after each run. # --seed 1234 config.order = :random # Seed global randomization in this process using the `--seed` CLI option. # Setting this allows you to use `--seed` to deterministically reproduce # test failures related to randomization by passing the same `--seed` value # as the one that triggered the failure. Kernel.srand config.seed # rspec-expectations config goes here. You can use an alternate # assertion/expectation library such as wrong or the stdlib/minitest # assertions if you prefer. config.expect_with :rspec do |expectations| # Enable only the newer, non-monkey-patching expect syntax. # For more details, see: # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax expectations.syntax = :expect end # rspec-mocks config goes here. You can use an alternate test double # library (such as bogus or mocha) by changing the `mock_with` option here. config.mock_with :rspec do |mocks| # Enable only the newer, non-monkey-patching expect syntax. # For more details, see: # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ mocks.syntax = :expect # Prevents you from mocking or stubbing a method that does not exist on # a real object. This is generally recommended. mocks.verify_partial_doubles = true end config.include Rack::Test::Methods config.include SpecHelpers end sinatra-3.0.5/rack-protection/spec/support/000077500000000000000000000000001434717561400207465ustar00rootroot00000000000000sinatra-3.0.5/rack-protection/spec/support/dummy_app.rb000066400000000000000000000003611434717561400232660ustar00rootroot00000000000000# frozen_string_literal: true module DummyApp def self.call(env) Thread.current[:last_env] = env body = (env['REQUEST_METHOD'] == 'HEAD' ? '' : 'ok') [200, { 'Content-Type' => env['wants'] || 'text/plain' }, [body]] end end sinatra-3.0.5/rack-protection/spec/support/not_implemented_as_pending.rb000066400000000000000000000011731434717561400266470ustar00rootroot00000000000000# frozen_string_literal: true # see http://blog.101ideas.cz/posts/pending-examples-via-not-implemented-error-in-rspec.html module NotImplementedAsPending def self.included(base) base.class_eval do alias_method :__finish__, :finish remove_method :finish end end def finish(reporter) if @exception.is_a?(NotImplementedError) from = @exception.backtrace[0] message = "#{@exception.message} (from #{from})" @pending_declared_in_example = message metadata[:pending] = true @exception = nil end __finish__(reporter) end RSpec::Core::Example.send :include, self end sinatra-3.0.5/rack-protection/spec/support/rack_monkey_patches.rb000066400000000000000000000013001434717561400252760ustar00rootroot00000000000000# frozen_string_literal: true version = if defined? Gem.loaded_specs&.include?('rack') Gem.loaded_specs['rack'].version.to_s else "#{Rack.release}.0" end if version == '1.3' Rack::Session::Abstract::ID.class_eval do private def prepare_session(env) session_was = env[ENV_SESSION_KEY] env[ENV_SESSION_KEY] = SessionHash.new(self, env) env[ENV_SESSION_OPTIONS_KEY] = OptionsHash.new(self, env, @default_options) env[ENV_SESSION_KEY].merge! session_was if session_was end end end unless Rack::MockResponse.method_defined? :header Rack::MockResponse.send(:alias_method, :header, :headers) end sinatra-3.0.5/rack-protection/spec/support/shared_examples.rb000066400000000000000000000027361434717561400244470ustar00rootroot00000000000000# frozen_string_literal: true RSpec.shared_examples_for 'any rack application' do it 'should not interfere with normal get requests' do expect(get('/')).to be_ok expect(body).to eq('ok') end it 'should not interfere with normal head requests' do expect(head('/')).to be_ok end it 'should not leak changes to env' do klass = described_class detector = Struct.new(:app) do def call(env) was = env.dup res = app.call(env) was.each do |k, v| next if env[k] == v raise "env[#{k.inspect}] changed from #{v.inspect} to #{env[k].inspect}" end res end end mock_app do use Rack::Head use(Rack::Config) { |e| e['rack.session'] ||= {} } use detector use klass run DummyApp end expect(get('/..', foo: '')).to be_ok end it 'allows passing on values in env' do klass = described_class changer = Struct.new(:app) do def call(env) env['foo.bar'] = 42 app.call(env) end end detector = Struct.new(:app) do def call(env) app.call(env) end end expect_any_instance_of(detector).to receive(:call).with( hash_including('foo.bar' => 42) ).and_call_original mock_app do use Rack::Head use(Rack::Config) { |e| e['rack.session'] ||= {} } use changer use klass use detector run DummyApp end expect(get('/')).to be_ok end end sinatra-3.0.5/rack-protection/spec/support/spec_helpers.rb000066400000000000000000000014471434717561400237550ustar00rootroot00000000000000# frozen_string_literal: true require 'forwardable' module SpecHelpers extend Forwardable def_delegators :last_response, :body, :headers, :status, :errors def_delegators :current_session, :env_for attr_writer :app def app @app ||= nil @app || mock_app(DummyApp) end def mock_app(app = nil, &block) app = block if app.nil? && (block.arity == 1) if app klass = described_class mock_app do use Rack::Head use(Rack::Config) { |e| e['rack.session'] ||= {} } use klass run app end else @app = Rack::Lint.new Rack::Builder.new(&block).to_app end end def with_headers(headers) proc { [200, { 'Content-Type' => 'text/plain' }.merge(headers), ['ok']] } end def env Thread.current[:last_env] end end sinatra-3.0.5/sinatra-contrib/000077500000000000000000000000001434717561400162735ustar00rootroot00000000000000sinatra-3.0.5/sinatra-contrib/.gitignore000066400000000000000000000000331434717561400202570ustar00rootroot00000000000000doc/ .bundle/ Gemfile.lock sinatra-3.0.5/sinatra-contrib/.rspec000066400000000000000000000000511434717561400174040ustar00rootroot00000000000000--color --warnings --require spec_helper sinatra-3.0.5/sinatra-contrib/Gemfile000066400000000000000000000017521434717561400175730ustar00rootroot00000000000000# frozen_string_literal: true source 'https://rubygems.org' gemspec gem 'rack-protection', path: '../rack-protection' gem 'sinatra', path: '..' gem 'rack-test', github: 'rack/rack-test' group :development, :test do platform :jruby do gem 'json' gem 'rdoc' gem 'therubyrhino' gem 'jar-dependencies', '= 0.4.1' # Gem::LoadError with jar-dependencies 0.4.2 end platform :jruby, :ruby do gem 'hamlit', '>= 3' gem 'liquid', '~> 2.6.x' gem 'slim' end platform :ruby do gem 'execjs', '2.0.0' gem 'nokogiri', '1.13.6' gem 'redcarpet', '3.5.1' gem 'yajl-ruby' end gem 'multi_json' end # Allows stuff like `tilt=1.2.2 bundle install` or `tilt=master ...`. # Used by the CI. repos = { 'tilt' => 'rtomayko/tilt', 'rack' => 'rack/rack' } %w[tilt rack].each do |lib| dep = (ENV[lib] || 'stable').sub "#{lib}-", '' dep = nil if dep == 'stable' dep = { github: repos[lib], branch: dep } if dep && dep !~ (/(\d+\.)+\d+/) gem lib, dep if dep end sinatra-3.0.5/sinatra-contrib/LICENSE000066400000000000000000000022051434717561400172770ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2008-2017 Nicolas Sanguinetti, entp.com, Konstantin Haase Copyright (c) 2015-2017 Zachary Scott 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. sinatra-3.0.5/sinatra-contrib/README.md000066400000000000000000000131651434717561400175600ustar00rootroot00000000000000# Sinatra::Contrib Collection of common Sinatra extensions, semi-officially supported. ## Goals * For every future Sinatra release, have at least one fully compatible release * High code quality, high test coverage * Include plugins people usually ask for a lot ## Included extensions ### Common Extensions These are common extension which will not add significant overhead or change any behavior of already existing APIs. They do not add any dependencies not already installed with this gem. Currently included: * [`sinatra/capture`][sinatra-capture]: Let's you capture the content of blocks in templates. * [`sinatra/config_file`][sinatra-config-file]: Allows loading configuration from yaml files. * [`sinatra/content_for`][sinatra-content-for]: Adds Rails-style `content_for` helpers to Haml, Erb, Erubi and Slim. * [`sinatra/cookies`][sinatra-cookies]: A `cookies` helper for reading and writing cookies. * [`sinatra/engine_tracking`][sinatra-engine-tracking]: Adds methods like `haml?` that allow helper methods to check whether they are called from within a template. * [`sinatra/json`][sinatra-json]: Adds a `#json` helper method to return JSON documents. * [`sinatra/link_header`][sinatra-link-header]: Helpers for generating `link` HTML tags and corresponding `Link` HTTP headers. Adds `link`, `stylesheet` and `prefetch` helper methods. * [`sinatra/multi_route`][sinatra-multi-route]: Adds ability to define one route block for multiple routes and multiple or custom HTTP verbs. * [`sinatra/namespace`][sinatra-namespace]: Adds namespace support to Sinatra. * [`sinatra/respond_with`][sinatra-respond-with]: Choose action and/or template automatically depending on the incoming request. Adds helpers `respond_to` and `respond_with`. * [`sinatra/custom_logger`][sinatra-custom-logger]: This extension allows you to define your own logger instance using +logger+ setting. That logger then will be available as #logger helper method in your routes and views. * [`sinatra/required_params`][sinatra-required-params]: Ensure if required query parameters exist ### Custom Extensions These extensions may add additional dependencies and enhance the behavior of the existing APIs. Currently included: * [`sinatra/reloader`][sinatra-reloader]: Automatically reloads Ruby files on code changes. **DEPRECATED**: Please consider consider using an alternative like [rerun](https://github.com/alexch/rerun) or [rack-unreloader](https://github.com/jeremyevans/rack-unreloader) instead. ### Other Tools * [`sinatra/extension`][sinatra-extension]: Mixin for writing your own Sinatra extensions. * [`sinatra/test_helpers`][sinatra-test-helpers]: Helper methods to ease testing your Sinatra application. Partly extracted from Sinatra. Testing framework agnostic * `sinatra/quiet_logger`: Extension to exclude specific pathes from access log. It works by patching Rack::CommonLogger ## Installation Add `gem 'sinatra-contrib'` to *Gemfile*, then execute `bundle install`. If you don't use Bundler, install the gem manually by executing `gem install sinatra-contrib` in your command line. ### Git If you want to use the gem from git, for whatever reason, you can do the following: ```ruby github 'sinatra/sinatra' do gem 'sinatra-contrib' end ``` Within this block you can also specify other gems from this git repository. ## Usage ### Classic Style A single extension (example: sinatra-content-for): ``` ruby require 'sinatra' require 'sinatra/content_for' ``` Common extensions: ``` ruby require 'sinatra' require 'sinatra/contrib' ``` All extensions: ``` ruby require 'sinatra' require 'sinatra/contrib/all' ``` ### Modular Style A single extension (example: sinatra-content-for): ``` ruby require 'sinatra/base' require 'sinatra/content_for' require 'sinatra/namespace' class MyApp < Sinatra::Base # Note: Some modules are extensions, some helpers, see the specific # documentation or the source helpers Sinatra::ContentFor register Sinatra::Namespace end ``` Common extensions: ``` ruby require 'sinatra/base' require 'sinatra/contrib' class MyApp < Sinatra::Base register Sinatra::Contrib end ``` All extensions: ``` ruby require 'sinatra/base' require 'sinatra/contrib/all' class MyApp < Sinatra::Base register Sinatra::Contrib end ``` ### Documentation For more info check the [official docs](http://www.sinatrarb.com/contrib/) and [api docs](http://www.rubydoc.info/gems/sinatra-contrib). [sinatra-reloader]: http://www.sinatrarb.com/contrib/reloader [sinatra-namespace]: http://www.sinatrarb.com/contrib/namespace [sinatra-content-for]: http://www.sinatrarb.com/contrib/content_for [sinatra-cookies]: http://www.sinatrarb.com/contrib/cookies [sinatra-streaming]: http://www.sinatrarb.com/contrib/streaming [sinatra-webdav]: http://www.sinatrarb.com/contrib/webdav [sinatra-runner]: http://www.sinatrarb.com/contrib/runner [sinatra-extension]: http://www.sinatrarb.com/contrib/extension [sinatra-test-helpers]: https://github.com/sinatra/sinatra/blob/master/sinatra-contrib/lib/sinatra/test_helpers.rb [sinatra-required-params]: http://www.sinatrarb.com/contrib/required_params [sinatra-custom-logger]: http://www.sinatrarb.com/contrib/custom_logger [sinatra-multi-route]: http://www.sinatrarb.com/contrib/multi_route [sinatra-json]: http://www.sinatrarb.com/contrib/json [sinatra-respond-with]: http://www.sinatrarb.com/contrib/respond_with [sinatra-config-file]: http://www.sinatrarb.com/contrib/config_file [sinatra-link-header]: http://www.sinatrarb.com/contrib/link_header [sinatra-capture]: http://www.sinatrarb.com/contrib/capture [sinatra-engine-tracking]: https://github.com/sinatra/sinatra/blob/master/sinatra-contrib/lib/sinatra/engine_tracking.rb sinatra-3.0.5/sinatra-contrib/Rakefile000066400000000000000000000042071434717561400177430ustar00rootroot00000000000000# frozen_string_literal: true $LOAD_PATH.unshift File.expand_path('lib', __dir__) require 'open-uri' require 'yaml' require 'sinatra/contrib/version' desc 'run specs' task(:spec) { ruby '-S rspec' } task(test: :spec) task(default: :spec) namespace :doc do task :readmes do Dir.glob 'lib/sinatra/*.rb' do |file| puts "Trying file... #{file}" excluded_files = %w[lib/sinatra/contrib.rb lib/sinatra/decompile.rb] next if excluded_files.include?(file) doc = File.read(file)[/^module Sinatra(\n)+( #[^\n]*\n)*/m].scan(/^ *#(?!#) ?(.*)\n/).join("\n") file = "doc/#{file[4..-4].tr('/_', '-')}.rdoc" Dir.mkdir 'doc' unless File.directory? 'doc' puts "writing #{file}" File.open(file, 'w') { |f| f << doc } end end task :index do doc = File.read('README.md') file = 'doc/sinatra-contrib-readme.md' Dir.mkdir 'doc' unless File.directory? 'doc' puts "writing #{file}" File.open(file, 'w') { |f| f << doc } end task all: %i[readmes index] end desc 'generate documentation' task doc: 'doc:all' desc 'generate gemspec' task 'sinatra-contrib.gemspec' do content = File.read 'sinatra-contrib.gemspec' fields = { authors: `git shortlog -sn`.scan(/[^\d\s].*/), email: `git shortlog -sne`.scan(/[^<]+@[^>]+/), files: `git ls-files`.split("\n").grep_v(/^(\.|Gemfile)/) } fields.each do |field, values| updated = " s.#{field} = [" updated << values.map { |v| "\n %p" % v }.join(',') updated << "\n ]" content.sub!(/ s\.#{field} = \[\n( .*\n)* \]/, updated) end File.open('sinatra-contrib.gemspec', 'w') { |f| f << content } end task gemspec: 'sinatra-contrib.gemspec' task release: :gemspec do sh <<-SH rm -Rf sinatra-contrib*.gem && gem build sinatra-contrib.gemspec && gem install sinatra-contrib*.gem --local && gem push sinatra-contrib*.gem && git commit --allow-empty -a -m '#{Sinatra::Contrib::VERSION} release' && git tag -s v#{Sinatra::Contrib::VERSION} -m '#{Sinatra::Contrib::VERSION} release' && git push && (git push origin master || true) && git push --tags && (git push origin --tags || true) SH end sinatra-3.0.5/sinatra-contrib/ideas.md000066400000000000000000000017371434717561400177120ustar00rootroot00000000000000* Extension that does something like this: def build(*) if settings.memcached? use Rack::Cache, :backend => :memcached use Rack::Session::Memcached # ... end super end * `sinatra-smart-cache`: update cache header only if arguments are more restrictive than curent value, set caching headers that way for most helper methods (i.e. `send_file`) * Some verbose logging extension: Log what filters, routes, error handlers, templates, and so on is used. * Form helpers, with forms as first class objects that accepts hashes or something, so the form meta data can also be used to expose a JSON API or similar, possibly defining routes (like "Sinatra's Hat"), strictly using the ActiveModel API. * Extend `sinatra-content-for` to support Liquid, Radius, Markaby, Nokogiri and Builder. At least the first two probably involve patching Tilt. * Rewrite of `sinatra-compass`? * Helpers for HTML escaping and such. sinatra-3.0.5/sinatra-contrib/lib/000077500000000000000000000000001434717561400170415ustar00rootroot00000000000000sinatra-3.0.5/sinatra-contrib/lib/sinatra/000077500000000000000000000000001434717561400205025ustar00rootroot00000000000000sinatra-3.0.5/sinatra-contrib/lib/sinatra/capture.rb000066400000000000000000000047211434717561400224760ustar00rootroot00000000000000require 'sinatra/base' require 'sinatra/engine_tracking' module Sinatra # # = Sinatra::Capture # # Extension that enables blocks inside other extensions. # It currently works for erb, slim and haml. # Enables mixing of different template languages. # # Example: # # # in hello_world.erb # # Say # <% a = capture do %>World<% end %> # Hello <%= a %>! # # # in hello_world.slim # # | Say # - a = capture do # | World # | Hello #{a}! # # # in hello_world.haml # # Say # - a = capture do # World # Hello #{a.strip}! # # # You can also use nested blocks. # # Example # # # in hello_world.erb # # Say # <% a = capture do %> # <% b = capture do %>World<% end %> # <%= b %>! # <% end %> # Hello <%= a.strip %> # # # The main advantage of capture is mixing of different template engines. # # Example # # # in mix_me_up.slim # # - two = capture do # - erb "<%= 1 + 1 %>" # | 1 + 1 = #{two} # # == Usage # # === Classic Application # # In a classic application simply require the helpers, and start using them: # # require "sinatra" # require "sinatra/capture" # # # The rest of your classic application code goes here... # # === Modular Application # # In a modular application you need to require the helpers, and then tell # the application you will use them: # # require "sinatra/base" # require "sinatra/capture" # # class MyApp < Sinatra::Base # helpers Sinatra::Capture # # # The rest of your modular application code goes here... # end # module Capture include Sinatra::EngineTracking def capture(*args, &block) return block[*args] if ruby? if haml? && Tilt[:haml] == Tilt::HamlTemplate && defined?(Haml::Buffer) buffer = Haml::Buffer.new(nil, Haml::Options.new.for_buffer) with_haml_buffer(buffer) { capture_haml(*args, &block) } else buf_was = @_out_buf @_out_buf = '' begin raw = block[*args] captured = block.binding.eval('@_out_buf') captured.empty? ? raw : captured ensure @_out_buf = buf_was end end end def capture_later(&block) engine = current_engine proc { |*a| with_engine(engine) { @capture = capture(*a, &block) } } end end helpers Capture end sinatra-3.0.5/sinatra-contrib/lib/sinatra/config_file.rb000066400000000000000000000122011434717561400232670ustar00rootroot00000000000000require 'sinatra/base' require 'yaml' require 'erb' module Sinatra # = Sinatra::ConfigFile # # Sinatra::ConfigFile is an extension that allows you to load the # application's configuration from YAML files. It automatically detects if # the files contain specific environment settings and it will use those # corresponding to the current one. # # You can access those options through +settings+ within the application. If # you try to get the value for a setting that hasn't been defined in the # config file for the current environment, you will get whatever it was set # to in the application. # # == Usage # # Once you have written your configurations to a YAML file you can tell the # extension to load them. See below for more information about how these # files are interpreted. # # For the examples, lets assume the following config.yml file: # # greeting: Welcome to my file configurable application # # === Classic Application # # require "sinatra" # require "sinatra/config_file" # # config_file 'path/to/config.yml' # # get '/' do # @greeting = settings.greeting # haml :index # end # # # The rest of your classic application code goes here... # # === Modular Application # # require "sinatra/base" # require "sinatra/config_file" # # class MyApp < Sinatra::Base # register Sinatra::ConfigFile # # config_file 'path/to/config.yml' # # get '/' do # @greeting = settings.greeting # haml :index # end # # # The rest of your modular application code goes here... # end # # === Config File Format # # In its most simple form this file is just a key-value list: # # foo: bar # something: 42 # nested: # a: 1 # b: 2 # # But it also can provide specific environment configuration. There are two # ways to do that: at the file level and at the settings level. # # At the settings level (e.g. in 'path/to/config.yml'): # # development: # foo: development # bar: bar # test: # foo: test # bar: bar # production: # foo: production # bar: bar # # Or at the file level: # # foo: # development: development # test: test # production: production # bar: bar # # In either case, settings.foo will return the environment name, and # settings.bar will return "bar". # # If you wish to provide defaults that may be shared among all the # environments, this can be done by using a YAML alias, and then overwriting # values in environments where appropriate: # # default: &common_settings # foo: 'foo' # bar: 'bar' # # production: # <<: *common_settings # bar: 'baz' # override the default value # module ConfigFile # When the extension is registered sets the +environments+ setting to the # traditional environments: development, test and production. def self.registered(base) base.set :environments, %w[test production development] end # Loads the configuration from the YAML files whose +paths+ are passed as # arguments, filtering the settings for the current environment. Note that # these +paths+ can actually be globs. def config_file(*paths) Dir.chdir(root || '.') do paths.each do |pattern| Dir.glob(pattern) do |file| raise UnsupportedConfigType unless ['.yml', '.yaml', '.erb'].include?(File.extname(file)) logger.info "loading config file '#{file}'" if logging? && respond_to?(:logger) document = ERB.new(File.read(file)).result yaml = YAML.respond_to?(:unsafe_load) ? YAML.unsafe_load(document) : YAML.load(document) config = config_for_env(yaml) config.each_pair { |key, value| set(key, value) } end end end end class UnsupportedConfigType < StandardError def message 'Invalid config file type, use .yml, .yaml or .erb' end end private # Given a +hash+ containing application configuration it returns # settings applicable to the current environment. Note: It gives # precedence to environment settings defined at the root-level. def config_for_env(hash) return from_environment_key(hash) if environment_keys?(hash) hash.each_with_object(IndifferentHash[]) do |(k, v), acc| if environment_keys?(v) acc.merge!(k => v[environment.to_s]) if v.key?(environment.to_s) else acc.merge!(k => v) end end end # Given a +hash+ returns the settings corresponding to the current # environment. def from_environment_key(hash) hash[environment.to_s] || hash[environment.to_sym] || {} end # Returns true if supplied with a hash that has any recognized # +environments+ in its root keys. def environment_keys?(hash) hash.is_a?(Hash) && hash.any? { |k, _| environments.include?(k.to_s) } end end register ConfigFile Delegator.delegate :config_file end sinatra-3.0.5/sinatra-contrib/lib/sinatra/content_for.rb000066400000000000000000000135621434717561400233560ustar00rootroot00000000000000# frozen_string_literal: true require 'sinatra/base' require 'sinatra/capture' module Sinatra # = Sinatra::ContentFor # # Sinatra::ContentFor is a set of helpers that allows you to capture # blocks inside views to be rendered later during the request. The most # common use is to populate different parts of your layout from your view. # # The currently supported engines are: Erb, Erubi, Haml and Slim. # # == Usage # # You call +content_for+, generally from a view, to capture a block of markup # giving it an identifier: # # # index.erb # <% content_for :some_key do %> # ... # <% end %> # # Then, you call +yield_content+ with that identifier, generally from a # layout, to render the captured block: # # # layout.erb # <%= yield_content :some_key %> # # If you have provided +yield_content+ with a block and no content for the # specified key is found, it will render the results of the block provided # to yield_content. # # # layout.erb # <% yield_content :some_key_with_no_content do %> # ... # <% end %> # # === Classic Application # # To use the helpers in a classic application all you need to do is require # them: # # require "sinatra" # require "sinatra/content_for" # # # Your classic application code goes here... # # === Modular Application # # To use the helpers in a modular application you need to require them, and # then, tell the application you will use them: # # require "sinatra/base" # require "sinatra/content_for" # # class MyApp < Sinatra::Base # helpers Sinatra::ContentFor # # # The rest of your modular application code goes here... # end # # == And How Is This Useful? # # For example, some of your views might need a few javascript tags and # stylesheets, but you don't want to force this files in all your pages. # Then you can put <%= yield_content :scripts_and_styles %> on your # layout, inside the tag, and each view can call content_for # setting the appropriate set of tags that should be added to the layout. # # == Limitations # # Due to the rendering process limitation using <%= yield_content %> # from within nested templates do not work above the <%= yield %> statement. # For more details https://github.com/sinatra/sinatra-contrib/issues/140#issuecomment-48831668 # # # app.rb # get '/' do # erb :body, :layout => :layout do # erb :foobar # end # end # # # foobar.erb # <% content_for :one do %> # # <% end %> # <% content_for :two do %> # # <% end %> # # Using <%= yield_content %> before <%= yield %> will cause only the second # alert to display: # # # body.erb # # Display only second alert # <%= yield_content :one %> # <%= yield %> # <%= yield_content :two %> # # # body.erb # # Display both alerts # <%= yield %> # <%= yield_content :one %> # <%= yield_content :two %> # module ContentFor include Capture # Capture a block of content to be rendered later. For example: # # <% content_for :head do %> # # <% end %> # # You can also pass an immediate value instead of a block: # # <% content_for :title, "foo" %> # # You can call +content_for+ multiple times with the same key # (in the example +:head+), and when you render the blocks for # that key all of them will be rendered, in the same order you # captured them. # # Your blocks can also receive values, which are passed to them # by yield_content def content_for(key, value = nil, options = {}, &block) block ||= proc { |*| value } clear_content_for(key) if options[:flush] content_blocks[key.to_sym] << capture_later(&block) end # Check if a block of content with the given key was defined. For # example: # # <% content_for :head do %> # # <% end %> # # <% if content_for? :head %> # content "head" was defined. # <% end %> def content_for?(key) content_blocks[key.to_sym].any? end # Unset a named block of content. For example: # # <% clear_content_for :head %> def clear_content_for(key) content_blocks.delete(key.to_sym) if content_for?(key) end # Render the captured blocks for a given key. For example: # # # Example # <%= yield_content :head %> # # # Would render everything you declared with content_for # :head before closing the tag. # # You can also pass values to the content blocks by passing them # as arguments after the key: # # <%= yield_content :head, 1, 2 %> # # Would pass 1 and 2 to all the blocks registered # for :head. def yield_content(key, *args, &block) if block_given? && !content_for?(key) haml? && Tilt[:haml] == Tilt::HamlTemplate && respond_to?(:capture_haml) ? capture_haml(*args, &block) : yield(*args) else content = content_blocks[key.to_sym].map { |b| capture(*args, &b) } content.join.tap do |c| if block_given? && (erb? || erubi?) @_out_buf << c end end end end private def content_blocks @content_blocks ||= Hash.new { |h, k| h[k] = [] } end end helpers ContentFor end sinatra-3.0.5/sinatra-contrib/lib/sinatra/contrib.rb000066400000000000000000000023251434717561400224710ustar00rootroot00000000000000# frozen_string_literal: true require 'sinatra/contrib/setup' module Sinatra module Contrib ## # Common middleware that doesn't bring run time overhead if not used # or breaks if external dependencies are missing. Will extend # Sinatra::Application by default. module Common register :ConfigFile, 'sinatra/config_file' register :MultiRoute, 'sinatra/multi_route' register :Namespace, 'sinatra/namespace' register :RespondWith, 'sinatra/respond_with' helpers :Capture, 'sinatra/capture' helpers :ContentFor, 'sinatra/content_for' helpers :Cookies, 'sinatra/cookies' helpers :EngineTracking, 'sinatra/engine_tracking' helpers :JSON, 'sinatra/json' helpers :LinkHeader, 'sinatra/link_header' helpers :Streaming, 'sinatra/streaming' helpers :RequiredParams, 'sinatra/required_params' end ## # Other extensions you don't want to be loaded unless needed. module Custom register :Reloader, 'sinatra/reloader' end ## # Stuff that aren't Sinatra extensions, technically. autoload :Extension, 'sinatra/extension' autoload :TestHelpers, 'sinatra/test_helpers' end register Sinatra::Contrib::Common end sinatra-3.0.5/sinatra-contrib/lib/sinatra/contrib/000077500000000000000000000000001434717561400221425ustar00rootroot00000000000000sinatra-3.0.5/sinatra-contrib/lib/sinatra/contrib/all.rb000066400000000000000000000001401434717561400232320ustar00rootroot00000000000000# frozen_string_literal: true require 'sinatra/contrib' Sinatra.register Sinatra::Contrib::All sinatra-3.0.5/sinatra-contrib/lib/sinatra/contrib/setup.rb000066400000000000000000000020371434717561400236310ustar00rootroot00000000000000# frozen_string_literal: true require 'sinatra/base' require 'sinatra/contrib/version' module Sinatra module Contrib module Loader def extensions @extensions ||= { helpers: [], register: [] } end def register(name, path) autoload name, path, :register end def helpers(name, path) autoload name, path, :helpers end def autoload(name, path, method = nil) extensions[method] << name if method Sinatra.autoload(name, path) end def registered(base) @extensions.each do |method, list| list = list.map { |name| Sinatra.const_get name } base.send(method, *list) unless base == ::Sinatra::Application end end end module Common extend Loader end module Custom extend Loader end module All def self.registered(base) base.register Common, Custom end end extend Loader def self.registered(base) base.register Common, Custom end end end sinatra-3.0.5/sinatra-contrib/lib/sinatra/contrib/version.rb000066400000000000000000000001371434717561400241550ustar00rootroot00000000000000# frozen_string_literal: true module Sinatra module Contrib VERSION = '3.0.5' end end sinatra-3.0.5/sinatra-contrib/lib/sinatra/cookies.rb000066400000000000000000000156571434717561400225010ustar00rootroot00000000000000# frozen_string_literal: true require 'sinatra/base' module Sinatra # = Sinatra::Cookies # # Easy way to deal with cookies # # == Usage # # Allows you to read cookies: # # get '/' do # "value: #{cookies[:something]}" # end # # And of course to write cookies: # # get '/set' do # cookies[:something] = 'foobar' # redirect to('/') # end # # And generally behaves like a hash: # # get '/demo' do # cookies.merge! 'foo' => 'bar', 'bar' => 'baz' # cookies.keep_if { |key, value| key.start_with? 'b' } # foo, bar = cookies.values_at 'foo', 'bar' # "size: #{cookies.length}" # end # # === Classic Application # # In a classic application simply require the helpers, and start using them: # # require "sinatra" # require "sinatra/cookies" # # # The rest of your classic application code goes here... # # === Modular Application # # In a modular application you need to require the helpers, and then tell # the application to use them: # # require "sinatra/base" # require "sinatra/cookies" # # class MyApp < Sinatra::Base # helpers Sinatra::Cookies # # # The rest of your modular application code goes here... # end # module Cookies class Jar include Enumerable attr_reader :options def initialize(app) @response_string = nil @response_hash = {} @response = app.response @request = app.request @deleted = [] @options = { path: @request.script_name.to_s.empty? ? '/' : @request.script_name, domain: @request.host == 'localhost' ? nil : @request.host, secure: @request.secure?, httponly: true } return unless app.settings.respond_to? :cookie_options @options.merge! app.settings.cookie_options end def ==(other) other.respond_to? :to_hash and to_hash == other.to_hash end def [](key) response_cookies[key.to_s] || request_cookies[key.to_s] end def []=(key, value) set(key, value: value) end if Hash.method_defined? :assoc def assoc(key) to_hash.assoc(key.to_s) end end def clear each_key { |k| delete(k) } end def compare_by_identity? false end def default nil end alias default_proc default def delete(key) result = self[key] @response.delete_cookie(key.to_s, @options) result end def delete_if return enum_for(__method__) unless block_given? each { |k, v| delete(k) if yield(k, v) } self end def each(&block) return enum_for(__method__) unless block_given? to_hash.each(&block) end def each_key(&block) return enum_for(__method__) unless block_given? to_hash.each_key(&block) end alias each_pair each def each_value(&block) return enum_for(__method__) unless block_given? to_hash.each_value(&block) end def empty? to_hash.empty? end def fetch(key, &block) response_cookies.fetch(key.to_s) do request_cookies.fetch(key.to_s, &block) end end if Hash.method_defined? :flatten def flatten to_hash.flatten end end def has_key?(key) response_cookies.key? key.to_s or request_cookies.key? key.to_s end def has_value?(value) response_cookies.value? value or request_cookies.value? value end def hash to_hash.hash end alias include? has_key? alias member? has_key? def inspect "<##{self.class}: #{to_hash.inspect[1..-2]}>" end if Hash.method_defined? :invert def invert to_hash.invert end end def keep_if return enum_for(__method__) unless block_given? delete_if { |*a| !yield(*a) } end def key(value) to_hash.key(value) end alias key? has_key? def keys to_hash.keys end def length to_hash.length end def merge(other, &block) to_hash.merge(other, &block) end def merge!(other) other.each_pair do |key, value| self[key] = if block_given? && include?(key) yield(key.to_s, self[key], value) else value end end end def rassoc(value) to_hash.rassoc(value) end def rehash response_cookies.rehash request_cookies.rehash self end def reject(&block) return enum_for(__method__) unless block_given? to_hash.reject(&block) end alias reject! delete_if def replace(other) select! { |k, _v| other.include?(k) or other.include?(k.to_s) } merge! other end def select(&block) return enum_for(__method__) unless block_given? to_hash.select(&block) end alias select! keep_if if Hash.method_defined? :select! def set(key, options = {}) @response.set_cookie key.to_s, @options.merge(options) end def shift key, value = to_hash.shift delete(key) [key, value] end alias size length if Hash.method_defined? :sort def sort(&block) to_hash.sort(&block) end end alias store []= def to_hash request_cookies.merge(response_cookies) end def to_a to_hash.to_a end def to_s to_hash.to_s end alias update merge! alias value? has_value? def values to_hash.values end def values_at(*list) list.map { |k| self[k] } end private def warn(message) super "#{caller.first[/^[^:]:\d+:/]} warning: #{message}" end def deleted parse_response @deleted end def response_cookies parse_response @response_hash end def parse_response string = @response['Set-Cookie'] return if @response_string == string hash = {} string.each_line do |line| key, value = line.split(';', 2).first.to_s.split('=', 2) next if key.nil? key = Rack::Utils.unescape(key) if line =~ /expires=Thu, 01[-\s]Jan[-\s]1970/ @deleted << key else @deleted.delete key hash[key] = value end end @response_hash.replace hash @response_string = string end def request_cookies @request.cookies.reject { |key, _value| deleted.include? key } end end def cookies @cookies ||= Jar.new(self) end end helpers Cookies end sinatra-3.0.5/sinatra-contrib/lib/sinatra/custom_logger.rb000066400000000000000000000027071434717561400237060ustar00rootroot00000000000000# frozen_string_literal: true module Sinatra # = Sinatra::CustomLogger # # CustomLogger extension allows you to define your own logger instance # using +logger+ setting. That logger then will be available # as #logger helper method in your routes and views. # # == Usage # # === Classic Application # # To define your custom logger instance in a classic application: # # require 'sinatra' # require 'sinatra/custom_logger' # require 'logger' # # set :logger, Logger.new(STDOUT) # # get '/' do # logger.info 'Some message' # STDOUT logger is used # # Other code... # end # # === Modular Application # # The same for modular application: # # require 'sinatra/base' # require 'sinatra/custom_logger' # require 'logger' # # class MyApp < Sinatra::Base # helpers Sinatra::CustomLogger # # configure :development, :production do # logger = Logger.new(File.open("#{root}/log/#{environment}.log", 'a')) # logger.level = Logger::DEBUG if development? # set :logger, logger # end # # get '/' do # logger.info 'Some message' # File-based logger is used # # Other code... # end # end # module CustomLogger def logger if settings.respond_to?(:logger) settings.logger else request.logger end end end helpers CustomLogger end sinatra-3.0.5/sinatra-contrib/lib/sinatra/engine_tracking.rb000066400000000000000000000042501434717561400241570ustar00rootroot00000000000000# frozen_string_literal: true require 'sinatra/base' module Sinatra # Adds methods like `haml?` that allow helper methods to check whether they # are called from within a template. module EngineTracking attr_reader :current_engine # @return [Boolean] Returns true if current engine is `:erb`. def erb? @current_engine == :erb end # Returns true if the current engine is `:erubi`, or `Tilt[:erb]` is set # to Tilt::ErubiTemplate. # # @return [Boolean] Returns true if current engine is `:erubi`. def erubi? @current_engine == :erubi or (erb? && Tilt[:erb] == Tilt::ErubiTemplate) end # @return [Boolean] Returns true if current engine is `:haml`. def haml? @current_engine == :haml end # @return [Boolean] Returns true if current engine is `:builder`. def builder? @current_engine == :builder end # @return [Boolean] Returns true if current engine is `:liquid`. def liquid? @current_engine == :liquid end # @return [Boolean] Returns true if current engine is `:markdown`. def markdown? @current_engine == :markdown end # @return [Boolean] Returns true if current engine is `:rdoc`. def rdoc? @current_engine == :rdoc end # @return [Boolean] Returns true if current engine is `:markaby`. def markaby? @current_engine == :markaby end # @return [Boolean] Returns true if current engine is `:nokogiri`. def nokogiri? @current_engine == :nokogiri end # @return [Boolean] Returns true if current engine is `:slim`. def slim? @current_engine == :slim end # @return [Boolean] Returns true if current engine is `:ruby`. def ruby? @current_engine == :ruby end def initialize(*) @current_engine = :ruby super end # @param engine [Symbol, String] Name of Engine to shift to. def with_engine(engine) engine_was = @current_engine @current_engine = engine.to_sym yield ensure @current_engine = engine_was end private def render(engine, *) with_engine(engine) { super } end end helpers EngineTracking end sinatra-3.0.5/sinatra-contrib/lib/sinatra/extension.rb000066400000000000000000000050511434717561400230440ustar00rootroot00000000000000# frozen_string_literal: true require 'sinatra/base' module Sinatra # = Sinatra::Extension # # Sinatra::Extension is a mixin that provides some syntactic sugar # for your extensions. It allows you to call almost any # Sinatra::Base method directly inside your extension # module. This means you can use +get+ to define a route, +before+ # to define a before filter, +set+ to define a setting and so on. # # Is important to be aware that this mixin remembers the method calls you # make, and then, when your extension is registered, replays them on the # Sinatra application that has been extended. In order to do that, it # defines a registered method, so, if your extension defines one # too, remember to call +super+. # # == Usage # # Just require the mixin and extend your extension with it: # # require 'sinatra/extension' # # module MyExtension # extend Sinatra::Extension # # # set some settings for development # configure :development do # set :reload_stuff, true # end # # # define a route # get '/' do # 'Hello World' # end # # # The rest of your extension code goes here... # end # # You can also create an extension with the +new+ method: # # MyExtension = Sinatra::Extension.new do # # Your extension code goes here... # end # # This is useful when you just want to pass a block to # Sinatra::Base.register. module Extension def self.new(&block) ext = Module.new.extend(self) ext.class_eval(&block) ext end def settings self end def configure(*args, &block) record(:configure, *args) { |c| c.instance_exec(c, &block) } end def registered(base = nil, &block) base ? replay(base) : record(:class_eval, &block) end private def record(method, *args, &block) recorded_methods << [method, args, block] end def replay(object) recorded_methods.each { |m, a, b| object.send(m, *a, &b) } end def recorded_methods @recorded_methods ||= [] end def method_missing(method, *args, &block) return super unless Sinatra::Base.respond_to? method record(method, *args, &block) DontCall.new(method) end class DontCall < BasicObject def initialize(method) @method = method end def method_missing(*) raise "not supposed to use result of #{@method}!" end def inspect; "#<#{self.class}: #{@method}>" end end end end sinatra-3.0.5/sinatra-contrib/lib/sinatra/json.rb000066400000000000000000000066001434717561400220020ustar00rootroot00000000000000# frozen_string_literal: true require 'sinatra/base' require 'multi_json' module Sinatra # = Sinatra::JSON # # Sinatra::JSON adds a helper method, called +json+, for (obviously) # json generation. # # == Usage # # === Classic Application # # In a classic application simply require the helper, and start using it: # # require "sinatra" # require "sinatra/json" # # # define a route that uses the helper # get '/' do # json :foo => 'bar' # end # # # The rest of your classic application code goes here... # # === Modular Application # # In a modular application you need to require the helper, and then tell the # application you will use it: # # require "sinatra/base" # require "sinatra/json" # # class MyApp < Sinatra::Base # # # define a route that uses the helper # get '/' do # json :foo => 'bar' # end # # # The rest of your modular application code goes here... # end # # === Encoders # # By default it will try to call +to_json+ on the object, but if it doesn't # respond to that message, it will use its own rather simple encoder. You can # easily change that anyways. To use +JSON+, simply require it: # # require 'json' # # The same goes for Yajl::Encoder: # # require 'yajl' # # For other encoders, besides requiring them, you need to define the # :json_encoder setting. For instance, for the +Whatever+ encoder: # # require 'whatever' # set :json_encoder, Whatever # # To force +json+ to simply call +to_json+ on the object: # # set :json_encoder, :to_json # # Actually, it can call any method: # # set :json_encoder, :my_fancy_json_method # # === Content-Type # # It will automatically set the content type to "application/json". As # usual, you can easily change that, with the :json_content_type # setting: # # set :json_content_type, :js # # === Overriding the Encoder and the Content-Type # # The +json+ helper will also take two options :encoder and # :content_type. The values of this options are the same as the # :json_encoder and :json_content_type settings, # respectively. You can also pass those to the json method: # # get '/' do # json({:foo => 'bar'}, :encoder => :to_json, :content_type => :js) # end # module JSON class << self def encode(object) ::MultiJson.dump(object) end end def json(object, options = {}) content_type resolve_content_type(options) resolve_encoder_action object, resolve_encoder(options) end private def resolve_content_type(options = {}) options[:content_type] || settings.json_content_type end def resolve_encoder(options = {}) options[:json_encoder] || settings.json_encoder end def resolve_encoder_action(object, encoder) %i[encode generate].each do |method| return encoder.send(method, object) if encoder.respond_to? method end raise "#{encoder} does not respond to #generate nor #encode" unless encoder.is_a? Symbol object.__send__(encoder) end end Base.set :json_encoder do ::MultiJson end Base.set :json_content_type, :json # Load the JSON helpers in modular style automatically Base.helpers JSON end sinatra-3.0.5/sinatra-contrib/lib/sinatra/link_header.rb000066400000000000000000000072451434717561400233040ustar00rootroot00000000000000require 'sinatra/base' module Sinatra # = Sinatra::LinkHeader # # Sinatra::LinkHeader adds a set of helper methods to generate link # HTML tags and their corresponding Link HTTP headers. # # == Usage # # Once you had set up the helpers in your application (see below), you will # be able to call the following methods from inside your route handlers, # filters and templates: # # +prefetch+:: # Sets the Link HTTP headers and returns HTML tags to prefetch the given # resources. # # +stylesheet+:: # Sets the Link HTTP headers and returns HTML tags to use the given # stylesheets. # # +link+:: # Sets the Link HTTP headers and returns the corresponding HTML tags # for the given resources. # # +link_headers+:: # Returns the corresponding HTML tags for the current Link HTTP headers. # # === Classic Application # # In a classic application simply require the helpers, and start using them: # # require "sinatra" # require "sinatra/link_header" # # # The rest of your classic application code goes here... # # === Modular Application # # In a modular application you need to require the helpers, and then tell # the application you will use them: # # require "sinatra/base" # require "sinatra/link_header" # # class MyApp < Sinatra::Base # helpers Sinatra::LinkHeader # # # The rest of your modular application code goes here... # end # module LinkHeader ## # Sets Link HTTP header and returns HTML tags for telling the browser to # prefetch given resources (only supported by Opera and Firefox at the # moment). def prefetch(*urls) link(:prefetch, *urls) end ## # Sets Link HTTP header and returns HTML tags for using stylesheets. def stylesheet(*urls) urls << {} unless urls.last.respond_to? :to_hash urls.last[:type] ||= mime_type(:css) link(:stylesheet, *urls) end ## # Sets Link HTTP header and returns corresponding HTML tags. # # Example: # # # Sets header: # # Link: ; rel="next" # # Returns String: # # '' # link '/foo', :rel => :next # # # Multiple URLs # link :stylesheet, '/a.css', '/b.css' def link(*urls) opts = urls.last.respond_to?(:to_hash) ? urls.pop : {} opts[:rel] = urls.shift unless urls.first.respond_to? :to_str options = opts.map { |k, v| " #{k}=#{v.to_s.inspect}" } html_pattern = "" http_pattern = ['<%s>', *options].join ';' link = (response['Link'] ||= '') urls.map do |url| link << ",\n" unless link.empty? link << (http_pattern % url) html_pattern % url end.join "\n" end ## # Takes the current value of th Link header(s) and generates HTML tags # from it. # # Example: # # get '/' do # # You can of course use fancy helpers like #link, #stylesheet # # or #prefetch # response["Link"] = '; rel="next"' # haml :some_page # end # # __END__ # # @@ layout # %head= link_headers # %body= yield def link_headers yield if block_given? return '' unless response.include? 'Link' response['Link'].split(",\n").map do |line| url, *opts = line.split(';').map(&:strip) "" end.join "\n" end def self.registered(_base) puts "WARNING: #{self} is a helpers module, not an extension." end end helpers LinkHeader end sinatra-3.0.5/sinatra-contrib/lib/sinatra/multi_route.rb000066400000000000000000000042251434717561400234020ustar00rootroot00000000000000# frozen_string_literal: true require 'sinatra/base' module Sinatra # = Sinatra::MultiRoute # # Create multiple routes with one statement. # # == Usage # # Use this extension to create a handler for multiple routes: # # get '/foo', '/bar' do # # ... # end # # Or for multiple verbs: # # route :get, :post, '/' do # # ... # end # # Or for multiple verbs and multiple routes: # # route :get, :post, ['/foo', '/bar'] do # # ... # end # # Or even for custom verbs: # # route 'LIST', '/' do # # ... # end # # === Classic Application # # To use the extension in a classic application all you need to do is require # it: # # require "sinatra" # require "sinatra/multi_route" # # # Your classic application code goes here... # # === Modular Application # # To use the extension in a modular application you need to require it, and # then, tell the application you will use it: # # require "sinatra/base" # require "sinatra/multi_route" # # class MyApp < Sinatra::Base # register Sinatra::MultiRoute # # # The rest of your modular application code goes here... # end # module MultiRoute def head(*args, &block) super(*route_args(args), &block) end def delete(*args, &block) super(*route_args(args), &block) end def get(*args, &block) super(*route_args(args), &block) end def options(*args, &block) super(*route_args(args), &block) end def patch(*args, &block) super(*route_args(args), &block) end def post(*args, &block) super(*route_args(args), &block) end def put(*args, &block) super(*route_args(args), &block) end def route(*args, &block) options = Hash === args.last ? args.pop : {} routes = [*args.pop] args.each do |verb| verb = verb.to_s.upcase if Symbol === verb routes.each do |route| super(verb, route, options, &block) end end end private def route_args(args) options = Hash === args.last ? args.pop : {} [args, options] end end register MultiRoute end sinatra-3.0.5/sinatra-contrib/lib/sinatra/namespace.rb000066400000000000000000000244071434717561400227720ustar00rootroot00000000000000# frozen_string_literal: true require 'sinatra/base' require 'mustermann' module Sinatra # = Sinatra::Namespace # # Sinatra::Namespace is an extension that adds namespaces to an # application. This namespaces will allow you to share a path prefix for the # routes within the namespace, and define filters, conditions and error # handlers exclusively for them. Besides that, you can also register helpers # and extensions that will be used only within the namespace. # # == Usage # # Once you have loaded the extension (see below), you can use the +namespace+ # method to define namespaces in your application. # # You can define a namespace by a path prefix: # # namespace '/blog' do # get { haml :blog } # get '/:entry_permalink' do # @entry = Entry.find_by_permalink!(params[:entry_permalink]) # haml :entry # end # # # More blog routes... # end # # by a condition: # # namespace :host_name => 'localhost' do # get('/admin/dashboard') { haml :dashboard } # get('/admin/login') { haml :login } # # # More admin routes... # end # # or both: # # namespace '/admin', :host_name => 'localhost' do # get('/dashboard') { haml :dashboard } # get('/login') { haml :login } # post('/login') { login_user } # # # More admin routes... # end # # Regex is also accepted: # # namespace /\/posts\/([^\/&?]+)\// do # get { haml :blog } # # # More blog routes... # end # # When you define a filter or an error handler, or register an extension or a # set of helpers within a namespace, they only affect the routes defined in # it. For instance, lets define a before filter to prevent the access of # unauthorized users to the admin section of the application: # # namespace '/admin' do # helpers AdminHelpers # before { authenticate unless request.path_info == '/admin/login' } # # get '/dashboard' do # # Only authenticated users can access here... # haml :dashboard # end # # # More admin routes... # end # # get '/' do # # Any user can access here... # haml :index # end # # Well, they actually also affect the nested namespaces: # # namespace '/admin' do # helpers AdminHelpers # before { authenticate unless request.path_info == '/admin/login' } # # namespace '/users' do # get do # # Only authenticated users can access here... # @users = User.all # haml :users # end # # # More user admin routes... # end # # # More admin routes... # end # # Redirecting within the namespace can be done using redirect_to: # # namespace '/admin' do # get '/foo' do # redirect_to '/bar' # Redirects to /admin/bar # end # # get '/foo' do # redirect '/bar' # Redirects to /bar # end # end # # === Classic Application Setup # # To be able to use namespaces in a classic application all you need to do is # require the extension: # # require "sinatra" # require "sinatra/namespace" # # namespace '/users' do # end # # === Modular Application Setup # # To be able to use namespaces in a modular application all you need to do is # require the extension, and then, register it: # # require "sinatra/base" # require "sinatra/namespace" # # class MyApp < Sinatra::Base # register Sinatra::Namespace # # namespace '/users' do # end # end # # === Within an extension # # To be able to use namespaces within an extension, you need to first create # an extension. This includes defining the `registered(app)` method in the # module. # # require 'sinatra/base' # For creating Sinatra extensions # require 'sinatra/namespace' # To create namespaces # # module Zomg # Keep everything under "Zomg" namespace for sanity # module Routes # Define a new "Routes" module # # def self.registered(app) # # First, register the Namespace extension # app.register Sinatra::Namespace # # # This defines an `/api` namespace on the application # app.namespace '/api' do # get '/users' do # # do something with `GET "/api/users"` # end # end # # end # end # # # Lastly, register the extension to use in our app # Sinatra.register Routes # end # # In order to use this extension, is the same as any other Sinatra extension: # # module Zomg # # Define our app class, we use modular app for this example # class App < Sinatra::Base # # this gives us all the namespaces we defined earlier # register Routes # # get '/' do # "Welcome to my application!" # end # end # end # # Zomg::App.run! # Don't forget to start your app ;) # # Phew! That was a mouthful. # # I hope that helps you use `Sinatra::Namespace` in every way imaginable! # module Namespace def self.new(base, pattern, conditions = {}, &block) Module.new do # quelch uninitialized variable warnings, since these get used by compile method. @pattern = nil @conditions = nil extend NamespacedMethods include InstanceMethods @base = base @extensions = [] @errors = {} @pattern, @conditions = compile(pattern, conditions) @templates = Hash.new { |_h, k| @base.templates[k] } namespace = self before { extend(@namespace = namespace) } class_eval(&block) end end module InstanceMethods def settings @namespace end def template_cache super.fetch(:nested, @namespace) { Tilt::Cache.new } end def redirect_to(uri, *args) redirect("#{@namespace.pattern}#{uri}", *args) end end module SharedMethods def namespace(pattern, conditions = {}, &block) Sinatra::Namespace.new(self, pattern, conditions, &block) end end module NamespacedMethods include SharedMethods attr_reader :base, :templates ALLOWED_ENGINES = %i[ erb erubi haml hamlit builder nokogiri liquid markdown rdoc asciidoc markaby rabl slim yajl ] def self.prefixed(*names) names.each { |n| define_method(n) { |*a, &b| prefixed(n, *a, &b) } } end prefixed :before, :after, :delete, :get, :head, :options, :patch, :post, :put def helpers(*extensions, &block) class_eval(&block) if block_given? include(*extensions) if extensions.any? end def register(*extensions, &block) extensions << Module.new(&block) if block_given? @extensions += extensions extensions.each do |extension| extend extension extension.registered(self) if extension.respond_to?(:registered) end end def invoke_hook(name, *args) @extensions.each { |e| e.send(name, *args) if e.respond_to?(name) } end def not_found(&block) error(Sinatra::NotFound, &block) end def errors base.errors.merge(namespace_errors) end def namespace_errors @errors end def error(*codes, &block) args = Sinatra::Base.send(:compile!, 'ERROR', /.*/, block) codes = codes.map { |c| Array(c) }.flatten codes << Exception if codes.empty? codes << Sinatra::NotFound if codes.include?(404) codes.each do |c| errors = @errors[c] ||= [] errors << args end end def respond_to(*args) return @conditions[:provides] || base.respond_to if args.empty? @conditions[:provides] = args end def set(key, value = self, &block) return key.each { |k, v| set(k, v) } if key.respond_to?(:each) && block.nil? && (value == self) raise ArgumentError, "may not set #{key}" unless ([:views] + ALLOWED_ENGINES).include?(key) block ||= proc { value } singleton_class.send(:define_method, key, &block) end def enable(*opts) opts.each { |key| set(key, true) } end def disable(*opts) opts.each { |key| set(key, false) } end def template(name, &block) first_location = caller_locations.first filename = first_location.path line = first_location.lineno templates[name] = [block, filename, line] end def layout(name = :layout, &block) template name, &block end def pattern @pattern end private def app base.respond_to?(:base) ? base.base : base end def compile(pattern, conditions, default_pattern = nil) if pattern.respond_to? :to_hash conditions = conditions.merge pattern.to_hash pattern = nil end base_pattern = @pattern base_conditions = @conditions pattern ||= default_pattern [prefixed_path(base_pattern, pattern), (base_conditions || {}).merge(conditions)] end def prefixed_path(a, b) return a || b || /.*/ unless a && b return Mustermann.new(b) if a == /.*/ Mustermann.new(a) + Mustermann.new(b) end def prefixed(method, pattern = nil, conditions = {}, &block) default = %r{(?:/.*)?} if (method == :before) || (method == :after) pattern, conditions = compile pattern, conditions, default result = base.send(method, pattern, **conditions, &block) invoke_hook :route_added, method.to_s.upcase, pattern, block result end def method_missing(method, *args, &block) base.send(method, *args, &block) end def respond_to?(method, include_private = false) super || base.respond_to?(method, include_private) end end module BaseMethods include SharedMethods end def self.extended(base) base.extend BaseMethods end end register Sinatra::Namespace Delegator.delegate :namespace end sinatra-3.0.5/sinatra-contrib/lib/sinatra/quiet_logger.rb000066400000000000000000000030211434717561400235110ustar00rootroot00000000000000# frozen_string_literal: true module Sinatra # = Sinatra::QuietLogger # # QuietLogger extension allows you to define paths excluded # from logging using the +quiet_logger_prefixes+ setting. # It is inspired from rails quiet_logger, but handles multiple paths. # # == Usage # # === Classic Application # # You have to require the quiet_logger, set the prefixes # and register the extension in your application. # # require 'sinatra' # require 'sinatra/quiet_logger' # # set :quiet_logger_prefixes, %w(css js images fonts) # register Sinatra::QuietLogger # # === Modular Application # # The same for modular application: # # require 'sinatra/base' # require 'sinatra/quiet_logger' # # set :quiet_logger_prefixes, %w(css js images fonts) # # class App < Sinatra::Base # register Sinatra::QuietLogger # end # module QuietLogger def self.registered(app) quiet_logger_prefixes = begin app.settings.quiet_logger_prefixes.join('|') rescue StandardError '' end return warn('You need to specify the paths you wish to exclude from logging via `set :quiet_logger_prefixes, %w(images css fonts)`') if quiet_logger_prefixes.empty? const_set('QUIET_LOGGER_REGEX', %r(\A/{0,2}(?:#{quiet_logger_prefixes}))) ::Rack::CommonLogger.prepend( ::Module.new do def log(env, *) super unless env['PATH_INFO'] =~ QUIET_LOGGER_REGEX end end ) end end end sinatra-3.0.5/sinatra-contrib/lib/sinatra/reloader.rb000066400000000000000000000336771434717561400226440ustar00rootroot00000000000000# frozen_string_literal: true require 'sinatra/base' module Sinatra # = Sinatra::Reloader # # DEPRECATED: Please consider using an alternative like # rerun or rack-unreloader instead. # # Extension to reload modified files. Useful during development, # since it will automatically require files defining routes, filters, # error handlers and inline templates, with every incoming request, # but only if they have been updated. # # == Usage # # === Classic Application # # To enable the reloader in a classic application all you need to do is # require it: # # require "sinatra" # require "sinatra/reloader" if development? # # # Your classic application code goes here... # # === Modular Application # # To enable the reloader in a modular application all you need to do is # require it, and then, register it: # # require "sinatra/base" # require "sinatra/reloader" # # class MyApp < Sinatra::Base # configure :development do # register Sinatra::Reloader # end # # # Your modular application code goes here... # end # # == Using the Reloader in Other Environments # # By default, the reloader is only enabled for the development # environment. Similar to registering the reloader in a modular # application, a classic application requires manually enabling the # extension for it to be available in a non-development environment. # # require "sinatra" # require "sinatra/reloader" # # configure :production do # enable :reloader # end # # == Changing the Reloading Policy # # You can refine the reloading policy with +also_reload+ and # +dont_reload+, to customize which files should, and should not, be # reloaded, respectively. You can also use +after_reload+ to execute a # block after any file being reloaded. # # === Classic Application # # Simply call the methods: # # require "sinatra" # require "sinatra/reloader" if development? # # also_reload '/path/to/some/file' # dont_reload '/path/to/other/file' # after_reload do # puts 'reloaded' # end # # # Your classic application code goes here... # # === Modular Application # # Call the methods inside the +configure+ block: # # require "sinatra/base" # require "sinatra/reloader" # # class MyApp < Sinatra::Base # configure :development do # register Sinatra::Reloader # also_reload '/path/to/some/file' # dont_reload '/path/to/other/file' # after_reload do # puts 'reloaded' # end # end # # # Your modular application code goes here... # end # module Reloader # Watches a file so it can tell when it has been updated, and what # elements does it contain. class Watcher # Represents an element of a Sinatra application that may need to # be reloaded. An element could be: # * a route # * a filter # * an error handler # * a middleware # * inline templates # # Its +representation+ attribute is there to allow to identify the # element within an application, that is, to match it with its # Sinatra's internal representation. class Element < Struct.new(:type, :representation) end # Collection of file +Watcher+ that can be associated with a # Sinatra application. That way, we can know which files belong # to a given application and which files have been modified. It # also provides a mechanism to inform a Watcher of the elements # defined in the file being watched and if its changes should be # ignored. class List @app_list_map = Hash.new { |hash, key| hash[key] = new } # Returns the +List+ for the application +app+. def self.for(app) @app_list_map[app] end # Creates a new +List+ instance. def initialize @path_watcher_map = Hash.new do |hash, key| hash[key] = Watcher.new(key) end end # Lets the +Watcher+ for the file located at +path+ know that the # +element+ is defined there, and adds the +Watcher+ to the +List+, # if it isn't already there. def watch(path, element) watcher_for(path).elements << element end # Tells the +Watcher+ for the file located at +path+ to ignore # the file changes, and adds the +Watcher+ to the +List+, if # it isn't already there. def ignore(path) watcher_for(path).ignore end # Adds a +Watcher+ for the file located at +path+ to the # +List+, if it isn't already there. def watcher_for(path) @path_watcher_map[File.expand_path(path)] end alias watch_file watcher_for # Returns an array with all the watchers in the +List+. def watchers @path_watcher_map.values end # Returns an array with all the watchers in the +List+ that # have been updated. def updated watchers.find_all(&:updated?) end end attr_reader :path, :elements, :mtime # Creates a new +Watcher+ instance for the file located at +path+. def initialize(path) @ignore = nil @path = path @elements = [] update end # Indicates whether or not the file being watched has been modified. def updated? !ignore? && !removed? && mtime != File.mtime(path) end # Updates the mtime of the file being watched. def update @mtime = File.mtime(path) end # Indicates whether or not the file being watched has inline # templates. def inline_templates? elements.any? { |element| element.type == :inline_templates } end # Informs that the modifications to the file being watched # should be ignored. def ignore @ignore = true end # Indicates whether or not the modifications to the file being # watched should be ignored. def ignore? !!@ignore end # Indicates whether or not the file being watched has been removed. def removed? !File.exist?(path) end end MUTEX_FOR_PERFORM = Mutex.new # Allow a block to be executed after any file being reloaded @@after_reload = [] def after_reload(&block) @@after_reload << block end # When the extension is registered it extends the Sinatra application # +klass+ with the modules +BaseMethods+ and +ExtensionMethods+ and # defines a before filter to +perform+ the reload of the modified files. def self.registered(klass) @reloader_loaded_in ||= {} return if @reloader_loaded_in[klass] @reloader_loaded_in[klass] = true klass.extend BaseMethods klass.extend ExtensionMethods klass.set(:reloader) { klass.development? } klass.set(:reload_templates) { klass.reloader? } klass.before do if klass.reloader? MUTEX_FOR_PERFORM.synchronize { Reloader.perform(klass) } end end klass.set(:inline_templates, klass.app_file) if klass == Sinatra::Application end # Reloads the modified files, adding, updating and removing the # needed elements. def self.perform(klass) reloaded_paths = [] Watcher::List.for(klass).updated.each do |watcher| klass.set(:inline_templates, watcher.path) if watcher.inline_templates? watcher.elements.each { |element| klass.deactivate(element) } # Deletes all old elements. watcher.elements.delete_if { true } $LOADED_FEATURES.delete(watcher.path) require watcher.path watcher.update reloaded_paths << watcher.path end return if reloaded_paths.empty? @@after_reload.each do |block| block.arity.zero? ? block.call : block.call(reloaded_paths) end # Prevents after_reload from increasing each time it's reloaded. @@after_reload.delete_if do |blk| path, = blk.source_location path && reloaded_paths.include?(path) end end # Contains the methods defined in Sinatra::Base that are overridden. module BaseMethods # Protects Sinatra::Base.run! from being called more than once. def run!(*args) if settings.reloader? super unless running? else super end end # Does everything Sinatra::Base#route does, but it also tells the # +Watcher::List+ for the Sinatra application to watch the defined # route. # # Note: We are using #compile! so we don't interfere with extensions # changing #route. def compile!(verb, path, block, **options) source_location = block.respond_to?(:source_location) ? block.source_location.first : caller_files[1] signature = super watch_element( source_location, :route, { verb: verb, signature: signature } ) signature end # Does everything Sinatra::Base#inline_templates= does, but it also # tells the +Watcher::List+ for the Sinatra application to watch the # inline templates in +file+ or the file who made the call to this # method. def inline_templates=(file = nil) file = (caller_files[1] || File.expand_path($0)) if file.nil? || file == true watch_element(file, :inline_templates) super end # Does everything Sinatra::Base#use does, but it also tells the # +Watcher::List+ for the Sinatra application to watch the middleware # being used. def use(middleware, *args, &block) path = caller_files[1] || File.expand_path($0) watch_element(path, :middleware, [middleware, args, block]) super end # Does everything Sinatra::Base#add_filter does, but it also tells # the +Watcher::List+ for the Sinatra application to watch the defined # filter. def add_filter(type, path = nil, **options, &block) source_location = block.respond_to?(:source_location) ? block.source_location.first : caller_files[1] result = super watch_element(source_location, :"#{type}_filter", filters[type].last) result end # Does everything Sinatra::Base#error does, but it also tells the # +Watcher::List+ for the Sinatra application to watch the defined # error handler. def error(*codes, &block) path = caller_files[1] || File.expand_path($0) result = super codes.each do |c| watch_element(path, :error, code: c, handler: @errors[c]) end result end # Does everything Sinatra::Base#register does, but it also lets the # reloader know that an extension is being registered, because the # elements defined in its +registered+ method need a special treatment. def register(*extensions, &block) start_registering_extension result = super stop_registering_extension result end # Does everything Sinatra::Base#register does and then registers the # reloader in the +subclass+. def inherited(subclass) result = super subclass.register Sinatra::Reloader result end end # Contains the methods that the extension adds to the Sinatra application. module ExtensionMethods # Removes the +element+ from the Sinatra application. def deactivate(element) case element.type when :route verb = element.representation[:verb] signature = element.representation[:signature] (routes[verb] ||= []).delete(signature) when :middleware @middleware.delete(element.representation) when :before_filter filters[:before].delete(element.representation) when :after_filter filters[:after].delete(element.representation) when :error code = element.representation[:code] handler = element.representation[:handler] @errors.delete(code) if @errors[code] == handler end end # Indicates with a +glob+ which files should be reloaded if they # have been modified. It can be called several times. def also_reload(*glob) Dir[*glob].each { |path| Watcher::List.for(self).watch_file(path) } end # Indicates with a +glob+ which files should not be reloaded even if # they have been modified. It can be called several times. def dont_reload(*glob) Dir[*glob].each { |path| Watcher::List.for(self).ignore(path) } end private # attr_reader :register_path warn on -w (private attribute) def register_path; @register_path ||= nil; end # Indicates an extesion is being registered. def start_registering_extension @register_path = caller_files[2] end # Indicates the extesion has already been registered. def stop_registering_extension @register_path = nil end # Indicates whether or not an extension is being registered. def registering_extension? !register_path.nil? end # Builds a Watcher::Element from +type+ and +representation+ and # tells the Watcher::List for the current application to watch it # in the file located at +path+. # # If an extension is being registered, it also tells the list to # watch it in the file where the extension has been registered. # This prevents the duplication of the elements added by the # extension in its +registered+ method with every reload. def watch_element(path, type, representation = nil) list = Watcher::List.for(self) element = Watcher::Element.new(type, representation) list.watch(path, element) list.watch(register_path, element) if registering_extension? end end end register Reloader Delegator.delegate :also_reload, :dont_reload end sinatra-3.0.5/sinatra-contrib/lib/sinatra/required_params.rb000066400000000000000000000032231434717561400242120ustar00rootroot00000000000000# frozen_string_literal: true require 'sinatra/base' module Sinatra # = Sinatra::RequiredParams # # Ensure required query parameters # # == Usage # # Set required query parameter keys in the argument. # It'll halt with 400 if required keys don't exist. # # get '/simple_keys' do # required_params :p1, :p2 # end # # Complicated pattern is also fine. # # get '/complicated_keys' do # required_params :p1, :p2 => [:p3, :p4] # end # # === Classic Application # # In a classic application simply require the helpers, and start using them: # # require "sinatra" # require "sinatra/required_params" # # # The rest of your classic application code goes here... # # === Modular Application # # In a modular application you need to require the helpers, and then tell # the application to use them: # # require "sinatra/base" # require "sinatra/required_params" # # class MyApp < Sinatra::Base # helpers Sinatra::RequiredParams # # # The rest of your modular application code goes here... # end # module RequiredParams def required_params(*keys) _required_params(params, *keys) end private def _required_params(p, *keys) keys.each do |key| if key.is_a?(Hash) _required_params(p, *key.keys) key.each do |k, v| _required_params(p[k.to_s], v) end elsif key.is_a?(Array) _required_params(p, *key) else halt 400 unless p.respond_to?(:key?) && p&.key?(key.to_s) end end true end end helpers RequiredParams end sinatra-3.0.5/sinatra-contrib/lib/sinatra/respond_with.rb000066400000000000000000000175711434717561400235470ustar00rootroot00000000000000# frozen_string_literal: true require 'sinatra/json' require 'sinatra/base' module Sinatra # # = Sinatra::RespondWith # # These extensions let Sinatra automatically choose what template to render or # action to perform depending on the request's Accept header. # # Example: # # # Without Sinatra::RespondWith # get '/' do # data = { :name => 'example' } # request.accept.each do |type| # case type.to_s # when 'text/html' # halt haml(:index, :locals => data) # when 'text/json' # halt data.to_json # when 'application/atom+xml' # halt nokogiri(:'index.atom', :locals => data) # when 'application/xml', 'text/xml' # halt nokogiri(:'index.xml', :locals => data) # when 'text/plain' # halt 'just an example' # end # end # error 406 # end # # # With Sinatra::RespondWith # get '/' do # respond_with :index, :name => 'example' do |f| # f.txt { 'just an example' } # end # end # # Both helper methods +respond_to+ and +respond_with+ let you define custom # handlers like the one above for +text/plain+. +respond_with+ additionally # takes a template name and/or an object to offer the following default # behavior: # # * If a template name is given, search for a template called # +name.format.engine+ (+index.xml.nokogiri+ in the above example). # * If a template name is given, search for a templated called +name.engine+ # for engines known to result in the requested format (+index.haml+). # * If a file extension associated with the mime type is known to Sinatra, and # the object responds to +to_extension+, call that method and use the result # (+data.to_json+). # # == Security # # Since methods are triggered based on client input, this can lead to security # issues (but not as severe as those might appear in the first place: keep in # mind that only known file extensions are used). You should limit # the possible formats you serve. # # This is possible with the +provides+ condition: # # get '/', :provides => [:html, :json, :xml, :atom] do # respond_with :index, :name => 'example' # end # # However, since you have to set +provides+ for every route, this extension # adds an app global (class method) `respond_to`, that lets you define content # types for all routes: # # respond_to :html, :json, :xml, :atom # get('/a') { respond_with :index, :name => 'a' } # get('/b') { respond_with :index, :name => 'b' } # # == Custom Types # # Use the +on+ method for defining actions for custom types: # # get '/' do # respond_to do |f| # f.xml { nokogiri :index } # f.on('application/custom') { custom_action } # f.on('text/*') { data.to_s } # f.on('*/*') { "matches everything" } # end # end # # Definition order does not matter. module RespondWith class Format def initialize(app) @app = app @map = {} @generic = {} @default = nil end def on(type, &block) @app.settings.mime_types(type).each do |mime| case mime when '*/*' then @default = block when %r{^([^/]+)/\*$} then @generic[$1] = block else @map[mime] = block end end end def finish yield self if block_given? mime_type = @app.content_type || @app.request.preferred_type(@map.keys) || @app.request.preferred_type || 'text/html' type = mime_type.split(/\s*;\s*/, 2).first handlers = [@map[type], @generic[type[%r{^[^/]+}]], @default].compact handlers.each do |block| if (result = block.call(type)) @app.content_type mime_type @app.halt result end end @app.halt 500, 'Unknown template engine' end def method_missing(method, *args, &block) return super if args.any? || block.nil? || !@app.mime_type(method) on(method, &block) end end module Helpers include Sinatra::JSON def respond_with(template, object = nil, &block) unless Symbol === template object = template template = nil end format = Format.new(self) format.on '*/*' do |type| exts = settings.ext_map[type] exts << :xml if type.end_with? '+xml' if template args = template_cache.fetch(type, template) { template_for(template, exts) } if args.any? locals = { object: object } locals.merge! object.to_hash if object.respond_to? :to_hash renderer = args.first options = args[1..] + [{ locals: locals }] halt send(renderer, *options) end end if object exts.each do |ext| halt json(object) if ext == :json next unless object.respond_to? method = "to_#{ext}" halt(*object.send(method)) end end false end format.finish(&block) end def respond_to(&block) Format.new(self).finish(&block) end private def template_for(name, exts) # in production this is cached, so don't worry too much about runtime possible = [] settings.template_engines[:all].each do |engine| exts.each { |ext| possible << [engine, "#{name}.#{ext}"] } end exts.each do |ext| settings.template_engines[ext].each { |e| possible << [e, name] } end possible.each do |engine, template| klass = Tilt.default_mapping.template_map[engine.to_s] || Tilt.lazy_map[engine.to_s].fetch(0, [])[0] find_template(settings.views, template, klass) do |file| next unless File.exist? file return settings.rendering_method(engine) << template.to_sym end end [] # nil or false would not be cached end end def remap_extensions ext_map.clear Rack::Mime::MIME_TYPES.each { |e, t| ext_map[t] << e[1..].to_sym } ext_map['text/javascript'] << 'js' ext_map['text/xml'] << 'xml' end def mime_type(*) result = super remap_extensions result end def respond_to(*formats) @respond_to ||= nil if formats.any? @respond_to ||= [] @respond_to.concat formats elsif @respond_to.nil? && superclass.respond_to?(:respond_to) superclass.respond_to else @respond_to end end def rendering_method(engine) return [engine] if Sinatra::Templates.method_defined? engine return [:mab] if engine.to_sym == :markaby %i[render engine] end private def compile!(verb, path, block, **options) options[:provides] ||= respond_to if respond_to super end def self.jrubyify(engs) not_supported = [:markdown] engs.each_key do |key| engs[key].collect! { |eng| eng == :yajl ? :json_pure : eng } engs[key].delete_if { |eng| not_supported.include?(eng) } end engs end def self.engines engines = { xml: %i[builder nokogiri], html: %i[erb erubi haml hamlit slim liquid mab markdown rdoc], all: (Sinatra::Templates.instance_methods.map(&:to_sym) + [:mab] - %i[find_template markaby]), json: [:yajl] } engines.default = [] defined?(JRUBY_VERSION) ? jrubyify(engines) : engines end def self.registered(base) base.set :ext_map, Hash.new { |h, k| h[k] = [] } base.set :template_engines, engines base.remap_extensions base.helpers Helpers end end register RespondWith Delegator.delegate :respond_to end sinatra-3.0.5/sinatra-contrib/lib/sinatra/runner.rb000066400000000000000000000070251434717561400223440ustar00rootroot00000000000000# frozen_string_literal: true require 'open-uri' require 'net/http' require 'timeout' module Sinatra # NOTE: This feature is experimental, and missing tests! # # Helps you spinning up and shutting down your own sinatra app. This is especially helpful for running # real network tests against a sinatra backend. # # The backend server could look like the following (in test/server.rb). # # require "sinatra" # # get "/" do # "Cheers from test server" # end # # get "/ping" do # "1" # end # # Note that you need to implement a ping action for internal use. # # Next, you need to write your runner. # # require 'sinatra/runner' # # class Runner < Sinatra::Runner # def app_file # File.expand_path("server.rb", __dir__) # end # end # # Override Runner#app_file, #command, #port, #protocol and #ping_path for customization. # # **Don't forget to override #app_file specific to your application!** # # Wherever you need this test backend, here's how you manage it. The following example assumes you # have a test in your app that needs to be run against your test backend. # # runner = ServerRunner.new # runner.run # # # ..tests against localhost:4567 here.. # # runner.kill # # For an example, check https://github.com/apotonick/roar/blob/master/test/integration/runner.rb class Runner def app_file File.expand_path('server.rb', __dir__) end def run @pipe = start @started = Time.now warn "#{server} up and running on port #{port}" if ping end def kill return unless pipe Process.kill('KILL', pipe.pid) rescue NotImplementedError system "kill -9 #{pipe.pid}" rescue Errno::ESRCH end def get(url) Timeout.timeout(1) { get_url("#{protocol}://127.0.0.1:#{port}#{url}") } end def get_stream(url = '/stream', &block) Net::HTTP.start '127.0.0.1', port do |http| request = Net::HTTP::Get.new url http.request request do |response| response.read_body(&block) end end end def get_response(url) Net::HTTP.start '127.0.0.1', port do |http| request = Net::HTTP::Get.new url http.request request do |response| response end end end def log @log ||= '' loop { @log << pipe.read_nonblock(1) } rescue Exception @log end private attr_accessor :pipe def start IO.popen(command) end # to be overwritten def command "bundle exec ruby #{app_file} -p #{port} -e production" end def ping(timeout = 30) loop do return if alive? if Time.now - @started > timeout warn command, log raise 'timeout' else sleep 0.1 end end end def alive? 3.times { get(ping_path) } true rescue EOFError, SystemCallError, OpenURI::HTTPError, Timeout::Error false end # to be overwritten def ping_path '/ping' end # to be overwritten def port 4567 end def protocol 'http' end def get_url(url) uri = URI.parse(url) return uri.read unless protocol == 'https' get_https_url(uri) end def get_https_url(uri) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true http.verify_mode = OpenSSL::SSL::VERIFY_NONE request = Net::HTTP::Get.new(uri.request_uri) http.request(request).body end end end sinatra-3.0.5/sinatra-contrib/lib/sinatra/streaming.rb000066400000000000000000000132501434717561400230210ustar00rootroot00000000000000# frozen_string_literal: true require 'sinatra/base' module Sinatra # = Sinatra::Streaming # # Sinatra 1.3 introduced the +stream+ helper. This addon improves the # streaming API by making the stream object imitate an IO object, turning # it into a real Deferrable and making the body play nicer with middleware # unaware of streaming. # # == IO-like behavior # # This is useful when passing the stream object to a library expecting an # IO or StringIO object. # # get '/' do # stream do |out| # out.puts "Hello World!", "How are you?" # out.write "Written #{out.pos} bytes so far!\n" # out.putc(65) unless out.closed? # out.flush # end # end # # == Better Middleware Handling # # Blocks passed to #map! or #map will actually be applied when streaming # takes place (as you might have suspected, #map! applies modifications # to the current body, while #map creates a new one): # # class StupidMiddleware # def initialize(app) @app = app end # # def call(env) # status, headers, body = @app.call(env) # body.map! { |e| e.upcase } # [status, headers, body] # end # end # # use StupidMiddleware # # get '/' do # stream do |out| # out.puts "still" # sleep 1 # out.puts "streaming" # end # end # # Even works if #each is used to generate an Enumerator: # # def call(env) # status, headers, body = @app.call(env) # body = body.each.map { |s| s.upcase } # [status, headers, body] # end # # Note that both examples violate the Rack specification. # # == Setup # # In a classic application: # # require "sinatra" # require "sinatra/streaming" # # In a modular application: # # require "sinatra/base" # require "sinatra/streaming" # # class MyApp < Sinatra::Base # helpers Sinatra::Streaming # end module Streaming def stream(*) stream = super stream.extend Stream stream.app = self env['async.close'].callback { stream.close } if env.key? 'async.close' stream end module Stream attr_accessor :app, :lineno, :pos, :transformer, :closed alias tell pos alias closed? closed def self.extended(obj) obj.closed = false obj.lineno = 0 obj.pos = 0 obj.callback { obj.closed = true } obj.errback { obj.closed = true } end def <<(data) raise IOError, 'not opened for writing' if closed? @transformer ||= nil data = data.to_s data = @transformer[data] if @transformer @pos += data.bytesize super(data) end def each # that way body.each.map { ... } works return self unless block_given? super end def map(&block) # dup would not copy the mixin clone.map!(&block) end def map!(&block) @transformer ||= nil if @transformer inner = @transformer outer = block block = proc { |value| outer[inner[value]] } end @transformer = block self end def write(data) self << data data.to_s.bytesize end alias syswrite write alias write_nonblock write def print(*args) args.each { |arg| self << arg } nil end def printf(format, *args) print(format.to_s % args) end def putc(c) print c.chr end def puts(*args) args.each { |arg| self << "#{arg}\n" } nil end def close_read raise IOError, 'closing non-duplex IO for reading' end def closed_read? true end def closed_write? closed? end def external_encoding Encoding.find settings.default_encoding rescue NameError settings.default_encoding end def settings app.settings end def rewind @pos = @lineno = 0 end def not_open_for_reading(*) raise IOError, 'not opened for reading' end alias bytes not_open_for_reading alias eof? not_open_for_reading alias eof not_open_for_reading alias getbyte not_open_for_reading alias getc not_open_for_reading alias gets not_open_for_reading alias read not_open_for_reading alias read_nonblock not_open_for_reading alias readbyte not_open_for_reading alias readchar not_open_for_reading alias readline not_open_for_reading alias readlines not_open_for_reading alias readpartial not_open_for_reading alias sysread not_open_for_reading alias ungetbyte not_open_for_reading alias ungetc not_open_for_reading private :not_open_for_reading def enum_not_open_for_reading(*) not_open_for_reading if block_given? enum_for(:not_open_for_reading) end alias chars enum_not_open_for_reading alias each_line enum_not_open_for_reading alias each_byte enum_not_open_for_reading alias each_char enum_not_open_for_reading alias lines enum_not_open_for_reading undef enum_not_open_for_reading def dummy(*) end alias flush dummy alias fsync dummy alias internal_encoding dummy alias pid dummy undef dummy def seek(*) 0 end alias sysseek seek def sync true end def tty? false end alias isatty tty? end end helpers Streaming end sinatra-3.0.5/sinatra-contrib/lib/sinatra/test_helpers.rb000066400000000000000000000131361434717561400235340ustar00rootroot00000000000000# frozen_string_literal: true require 'sinatra/base' require 'rack' begin require 'rack/test' rescue LoadError abort 'Add rack-test to your Gemfile to use this module!' end require 'forwardable' module Sinatra Base.set :environment, :test # Helper methods to ease testing your Sinatra application. Partly extracted # from Sinatra. Testing framework agnostic. module TestHelpers include Rack::Test::Methods extend Forwardable attr_accessor :settings # @!group Instance Methods delegated to last_response # @!method body # # Body of last_response # # @see http://www.rubydoc.info/github/rack/rack/master/Rack/Response#body-instance_method # @return [String] body of the last response # @!method headers # # Headers of last_response # # @return [Hash] hash of the last response # @!method status # # HTTP status of last_response # # @return [Integer] HTTP status of the last response # @!method errors # # Errors of last_response # # @return [Array] errors of the last response def_delegators :last_response, :body, :headers, :status, :errors # @!endgroup # @!group Class Methods delegated to app # @!method configure(*envs) {|_self| ... } # @!scope class # @yieldparam _self [Sinatra::Base] the object that the method was called on # # Set configuration options for Sinatra and/or the app. Allows scoping of # settings for certain environments. # @!method set(option, value = (not_set = true), ignore_setter = false, &block) # @!scope class # Sets an option to the given value. If the value is a proc, the proc will # be called every time the option is accessed. # @raise [ArgumentError] # @!method enable(*opts) # @!scope class # # Same as calling `set :option, true` for each of the given options. # @!method disable(*opts) # @!scope class # # Same as calling `set :option, false` for each of the given options. # @!method use(middleware, *args, &block) # @!scope class # Use the specified Rack middleware # @!method helpers(*extensions, &block) # @!scope class # # Makes the methods defined in the block and in the Modules given in # `extensions` available to the handlers and templates. # @!method register(*extensions, &block) # @!scope class # Register an extension. Alternatively take a block from which an # extension will be created and registered on the fly. def_delegators :app, :configure, :set, :enable, :disable, :use, :helpers, :register # @!endgroup # @!group Instance Methods delegated to current_session # @!method env_for(uri = "", opts = {}) # # Return the Rack environment used for a request to `uri`. # # @return [Hash] def_delegators :current_session, :env_for # @!endgroup # @!group Instance Methods delegated to rack_mock_session # @!method cookie_jar # # Returns a {http://www.rubydoc.info/github/rack-test/rack-test/Rack/Test/CookieJar Rack::Test::CookieJar}. # # @return [Rack::Test::CookieJar] def_delegators :rack_mock_session, :cookie_jar # @!endgroup # Instantiate and configure a mock Sinatra app. # # Takes a `base` app class, or defaults to Sinatra::Base, and instantiates # an app instance. Any given code in `block` is `class_eval`'d on this new # instance before the instance is returned. # # @param base [Sinatra::Base] App base class # # @return [Sinatra] Configured mocked app def mock_app(base = Sinatra::Base, &block) inner = nil @app = Sinatra.new(base) do inner = self class_eval(&block) end @settings = inner app end # Replaces the configured app. # # @param base [Sinatra::Base] a configured app def app=(base) @app = base end alias set_app app= # Returns a Rack::Lint-wrapped Sinatra app. # # If no app has been configured, a new subclass of Sinatra::Base will be # used and stored. # # (Rack::Lint validates your application and the requests and # responses according to the Rack spec.) # # @return [Sinatra::Base] def app @app ||= Class.new Sinatra::Base Rack::Lint.new @app end unless method_defined? :options # Processes an OPTIONS request in the context of the current session. # # @param uri [String] # @param params [Hash] # @param env [Hash] def options(uri, params = {}, env = {}, &block) env = env_for(uri, env.merge(method: 'OPTIONS', params: params)) current_session.send(:process_request, uri, env, &block) end end unless method_defined? :patch # Processes a PATCH request in the context of the current session. # # @param uri [String] # @param params [Hash] # @param env [Hash] def patch(uri, params = {}, env = {}, &block) env = env_for(uri, env.merge(method: 'PATCH', params: params)) current_session.send(:process_request, uri, env, &block) end end # @return [Boolean] def last_request? last_request true rescue Rack::Test::Error false end # @raise [Rack::Test:Error] If sessions are not enabled for app # @return [Hash] Session of last request, or the empty Hash def session return {} unless last_request? raise Rack::Test::Error, 'session not enabled for app' unless last_env['rack.session'] || app.session? last_request.session end # @return The env of the last request def last_env last_request.env end end end sinatra-3.0.5/sinatra-contrib/lib/sinatra/webdav.rb000066400000000000000000000044341434717561400223040ustar00rootroot00000000000000# frozen_string_literal: true require 'sinatra/base' module Sinatra # = Sinatra::WebDAV # # This extensions provides WebDAV verbs, as defined by RFC 4918 # (https://tools.ietf.org/html/rfc4918). To use this in your app, # just +register+ it: # # require 'sinatra/base' # require 'sinatra/webdav' # # class Application < Sinatra::Base # register Sinatra::WebDAV # # # Now you can use any WebDAV verb: # propfind '/2014/january/21' do # 'I have a lunch at 9 PM' # end # end # # You can use it in classic application just by requring the extension: # # require 'sinatra' # require 'sinatra/webdav' # # mkcol '/2015' do # 'You started 2015!' # end # module WebDAV def self.registered(_) Sinatra::Request.include WebDAV::Request end module Request def self.included(base) base.class_eval do alias_method :_safe?, :safe? alias_method :_idempotent?, :idempotent? def safe? _safe? or propfind? end def idempotent? _idempotent? or propfind? or move? or unlock? # or lock? end end end def propfind? request_method == 'PROPFIND' end def proppatch? request_method == 'PROPPATCH' end def mkcol? request_method == 'MKCOL' end def copy? request_method == 'COPY' end def move? request_method == 'MOVE' end # def lock? # request_method == 'LOCK' # end def unlock? request_method == 'UNLOCK' end end def propfind(path, opts = {}, &bk) route 'PROPFIND', path, opts, &bk end def proppatch(path, opts = {}, &bk) route 'PROPPATCH', path, opts, &bk end def mkcol(path, opts = {}, &bk) route 'MKCOL', path, opts, &bk end def copy(path, opts = {}, &bk) route 'COPY', path, opts, &bk end def move(path, opts = {}, &bk) route 'MOVE', path, opts, &bk end # def lock(path, opts = {}, &bk) route 'LOCK', path, opts, &bk end def unlock(path, opts = {}, &bk) route 'UNLOCK', path, opts, &bk end end register WebDAV Delegator.delegate :propfind, :proppatch, :mkcol, :copy, :move, :unlock # :lock end sinatra-3.0.5/sinatra-contrib/sinatra-contrib.gemspec000066400000000000000000000035761434717561400227520ustar00rootroot00000000000000# frozen_string_literal: true version = File.read(File.expand_path('../VERSION', __dir__)).strip Gem::Specification.new do |s| s.name = 'sinatra-contrib' s.version = version s.description = 'Collection of useful Sinatra extensions' s.homepage = 'http://sinatrarb.com/contrib/' s.license = 'MIT' s.summary = s.description s.authors = ['https://github.com/sinatra/sinatra/graphs/contributors'] s.email = 'sinatrarb@googlegroups.com' s.files = Dir['lib/**/*.rb'] + [ 'LICENSE', 'README.md', 'Rakefile', 'ideas.md', 'sinatra-contrib.gemspec' ] unless s.respond_to?(:metadata) raise <<-WARN RubyGems 2.0 or newer is required to protect against public gem pushes. You can update your rubygems version by running: gem install rubygems-update update_rubygems: gem update --system WARN end s.metadata = { 'source_code_uri' => 'https://github.com/sinatra/sinatra/tree/master/sinatra-contrib', 'homepage_uri' => 'http://sinatrarb.com/contrib/', 'documentation_uri' => 'https://www.rubydoc.info/gems/sinatra-contrib', 'rubygems_mfa_required' => 'true' } s.required_ruby_version = '>= 2.6.0' s.add_dependency 'multi_json' s.add_dependency 'mustermann', '~> 3.0' s.add_dependency 'rack-protection', version s.add_dependency 'sinatra', version s.add_dependency 'tilt', '~> 2.0' s.add_development_dependency 'asciidoctor' s.add_development_dependency 'builder' s.add_development_dependency 'erubi' s.add_development_dependency 'haml' s.add_development_dependency 'liquid' s.add_development_dependency 'markaby' s.add_development_dependency 'nokogiri' s.add_development_dependency 'rack-test', '~> 2' s.add_development_dependency 'rake', '>= 12.3.3' s.add_development_dependency 'redcarpet' s.add_development_dependency 'rspec', '~> 3' s.add_development_dependency 'slim' end sinatra-3.0.5/sinatra-contrib/spec/000077500000000000000000000000001434717561400172255ustar00rootroot00000000000000sinatra-3.0.5/sinatra-contrib/spec/capture_spec.rb000066400000000000000000000043101434717561400222250ustar00rootroot00000000000000# -*- coding: utf-8 -*- require 'slim' require 'spec_helper' RSpec.describe Sinatra::Capture do subject do Sinatra.new do enable :inline_templates helpers Sinatra::Capture end.new! end Tilt.prefer Tilt::ERBTemplate extend Forwardable def_delegators :subject, :capture, :capture_later def render(engine, template) subject.send(:render, engine, template.to_sym).strip.gsub(/\s+/, ' ') end shared_examples_for "a template language" do |engine| lang = engine if engine == :erubi lang = :erb end if engine == :hamlit lang = :haml end require "#{engine}" it "captures content" do expect(render(engine, "simple_#{lang}")).to eq("Say Hello World!") end it "allows nested captures" do expect(render(engine, "nested_#{lang}")).to eq("Say Hello World!") end end describe('haml') { it_behaves_like "a template language", :haml } describe('hamlit') { it_behaves_like "a template language", :hamlit } describe('slim') { it_behaves_like "a template language", :slim } describe('erubi') { it_behaves_like "a template language", :erubi } describe 'erb' do it_behaves_like "a template language", :erb it "handles utf-8 encoding" do expect(render(:erb, "utf_8")).to eq("UTF-8 –") end it "handles ISO-8859-1 encoding" do expect(render(:erb, "iso_8859_1")).to eq("ISO-8859-1 -") end end describe 'without templates' do it 'captures empty blocks' do expect(capture {}).to be_nil end end end __END__ @@ simple_erb Say <% a = capture do %>World<% end %> Hello <%= a %>! @@ nested_erb Say <% a = capture do %> <% b = capture do %>World<% end %> <%= b %>! <% end %> Hello <%= a.strip %> @@ simple_slim | Say - a = capture do | World | Hello #{a.strip}! @@ nested_slim | Say - a = capture do - b = capture do | World | #{b.strip}! | Hello #{a.strip} @@ simple_haml Say - a = capture do World Hello #{a.strip}! @@ nested_haml Say - a = capture do - b = capture do World #{b.strip}! Hello #{a.strip} @@ utf_8 <% a = capture do %>–<% end %> UTF-8 <%= a %> @@ iso_8859_1 <% a = capture do %>-<% end %> ISO-8859-1 <%= a.force_encoding("iso-8859-1") %> sinatra-3.0.5/sinatra-contrib/spec/config_file/000077500000000000000000000000001434717561400214715ustar00rootroot00000000000000sinatra-3.0.5/sinatra-contrib/spec/config_file/config.txt000066400000000000000000000000001434717561400234650ustar00rootroot00000000000000sinatra-3.0.5/sinatra-contrib/spec/config_file/key_value.yaml000066400000000000000000000001031434717561400243330ustar00rootroot00000000000000--- foo: bar bar: <%= "bar" %> something: 42 nested: a: 1 b: 2 sinatra-3.0.5/sinatra-contrib/spec/config_file/key_value.yml000066400000000000000000000001031434717561400241720ustar00rootroot00000000000000--- foo: bar bar: <%= "bar" %> something: 42 nested: a: 1 b: 2 sinatra-3.0.5/sinatra-contrib/spec/config_file/key_value.yml.erb000066400000000000000000000001171434717561400247460ustar00rootroot00000000000000--- foo: <%= "bar" %> something: <%= 42 %> nested: a: <%= 1 %> b: <%= 2 %> sinatra-3.0.5/sinatra-contrib/spec/config_file/key_value_override.yml000066400000000000000000000000151434717561400260730ustar00rootroot00000000000000--- foo: foo sinatra-3.0.5/sinatra-contrib/spec/config_file/missing_env.yml000066400000000000000000000000531434717561400245330ustar00rootroot00000000000000--- foo: production: 10 development: 20sinatra-3.0.5/sinatra-contrib/spec/config_file/with_env_defaults.yml000066400000000000000000000002601434717561400257240ustar00rootroot00000000000000--- default: &default foo: default bar: baz development: <<: *default foo: development production: <<: *default foo: production test: <<: *default foo: test sinatra-3.0.5/sinatra-contrib/spec/config_file/with_envs.yml000066400000000000000000000001241434717561400242170ustar00rootroot00000000000000--- development: foo: development production: foo: production test: foo: test sinatra-3.0.5/sinatra-contrib/spec/config_file/with_nested_envs.yml000066400000000000000000000003101434717561400255560ustar00rootroot00000000000000--- database: production: adapter: postgresql database: foo_production development: adapter: sqlite database: db/development.db test: adapter: sqlite database: db/test.dbsinatra-3.0.5/sinatra-contrib/spec/config_file_spec.rb000066400000000000000000000064311434717561400230340ustar00rootroot00000000000000require 'spec_helper' RSpec.describe Sinatra::ConfigFile do def config_file(*args, &block) mock_app do register Sinatra::ConfigFile set :root, File.expand_path('config_file', __dir__) instance_eval(&block) if block config_file(*args) end end it 'should set options from a simple config_file' do config_file 'key_value.yml' expect(settings.foo).to eq('bar') expect(settings.something).to eq(42) end it 'should create indifferent hashes' do config_file 'key_value.yml' expect(settings.nested['a']).to eq(1) expect(settings.nested[:a]).to eq(1) end it 'should render options in ERB tags when using .yml files' do config_file 'key_value.yml' expect(settings.bar).to eq "bar" expect(settings.something).to eq 42 expect(settings.nested['a']).to eq 1 expect(settings.nested[:a]).to eq 1 expect(settings.nested['b']).to eq 2 expect(settings.nested[:b]).to eq 2 end it 'should render options in ERB tags when using .yml.erb files' do config_file 'key_value.yml.erb' expect(settings.foo).to eq("bar") expect(settings.something).to eq(42) expect(settings.nested['a']).to eq(1) expect(settings.nested[:a]).to eq(1) expect(settings.nested['b']).to eq(2) expect(settings.nested[:b]).to eq(2) end it 'should render options in ERB tags when using .yaml files' do config_file 'key_value.yaml' expect(settings.foo).to eq("bar") expect(settings.something).to eq(42) expect(settings.nested['a']).to eq(1) expect(settings.nested[:a]).to eq(1) expect(settings.nested['b']).to eq(2) expect(settings.nested[:b]).to eq(2) end it 'should raise error if config file extension is not .yml, .yaml or .erb' do expect{ config_file 'config.txt' }.to raise_error(Sinatra::ConfigFile::UnsupportedConfigType) end it 'should recognize env specific settings per file' do config_file 'with_envs.yml' expect(settings.foo).to eq('test') end it 'should recognize env specific settings per setting' do config_file 'with_nested_envs.yml' expect(settings.database[:adapter]).to eq('sqlite') end it 'should not set present values to nil if the current env is missing' do # first let's check the test is actually working properly config_file('missing_env.yml') { set :foo => 42, :environment => :production } expect(settings.foo).to eq(10) # now test it config_file('missing_env.yml') { set :foo => 42, :environment => :test } expect(settings.foo).to eq(42) end it 'should prioritize settings in latter files' do # first let's check the test is actually working properly config_file 'key_value.yml' expect(settings.foo).to eq('bar') # now test it config_file 'key_value_override.yml' expect(settings.foo).to eq('foo') end context 'when file contains superfluous environments' do before { config_file 'with_env_defaults.yml' } it 'loads settings for the current environment anyway' do expect { settings.foo }.not_to raise_error end end context 'when file contains defaults' do before { config_file 'with_env_defaults.yml' } it 'uses the overridden value' do expect(settings.foo).to eq('test') end it 'uses the default value' do expect(settings.bar).to eq('baz') end end end sinatra-3.0.5/sinatra-contrib/spec/content_for/000077500000000000000000000000001434717561400215455ustar00rootroot00000000000000sinatra-3.0.5/sinatra-contrib/spec/content_for/different_key.erb000066400000000000000000000000461434717561400250550ustar00rootroot00000000000000<% content_for :bar do %>bar<% end %> sinatra-3.0.5/sinatra-contrib/spec/content_for/different_key.erubis000066400000000000000000000000461434717561400255760ustar00rootroot00000000000000<% content_for :bar do %>bar<% end %> sinatra-3.0.5/sinatra-contrib/spec/content_for/different_key.haml000066400000000000000000000000341434717561400252230ustar00rootroot00000000000000- content_for :bar do bar sinatra-3.0.5/sinatra-contrib/spec/content_for/different_key.hamlit000066400000000000000000000000341434717561400255600ustar00rootroot00000000000000- content_for :bar do bar sinatra-3.0.5/sinatra-contrib/spec/content_for/different_key.slim000066400000000000000000000000361434717561400252500ustar00rootroot00000000000000- content_for :bar do | bar sinatra-3.0.5/sinatra-contrib/spec/content_for/footer.erb000066400000000000000000000001001434717561400235240ustar00rootroot00000000000000<% if content_for? :foo %> <%= yield_content :foo %> <% end %>sinatra-3.0.5/sinatra-contrib/spec/content_for/footer.erubis000066400000000000000000000001001434717561400242450ustar00rootroot00000000000000<% if content_for? :foo %> <%= yield_content :foo %> <% end %>sinatra-3.0.5/sinatra-contrib/spec/content_for/footer.haml000066400000000000000000000000551434717561400237060ustar00rootroot00000000000000- if content_for? :foo = yield_content :foosinatra-3.0.5/sinatra-contrib/spec/content_for/footer.hamlit000066400000000000000000000000571434717561400242450ustar00rootroot00000000000000- if content_for? :foo != yield_content :foo sinatra-3.0.5/sinatra-contrib/spec/content_for/footer.slim000066400000000000000000000000551434717561400237310ustar00rootroot00000000000000- if content_for? :foo = yield_content :foosinatra-3.0.5/sinatra-contrib/spec/content_for/layout.erb000066400000000000000000000000321434717561400235470ustar00rootroot00000000000000<%= yield_content :foo %> sinatra-3.0.5/sinatra-contrib/spec/content_for/layout.erubis000066400000000000000000000000321434717561400242700ustar00rootroot00000000000000<%= yield_content :foo %> sinatra-3.0.5/sinatra-contrib/spec/content_for/layout.haml000066400000000000000000000000251434717561400237220ustar00rootroot00000000000000= yield_content :foo sinatra-3.0.5/sinatra-contrib/spec/content_for/layout.hamlit000066400000000000000000000000251434717561400242570ustar00rootroot00000000000000= yield_content :foo sinatra-3.0.5/sinatra-contrib/spec/content_for/layout.slim000066400000000000000000000000251434717561400237450ustar00rootroot00000000000000= yield_content :foo sinatra-3.0.5/sinatra-contrib/spec/content_for/multiple_blocks.erb000066400000000000000000000002441434717561400254270ustar00rootroot00000000000000<% content_for :foo do %>foo<% end %> <% content_for :foo do %>bar<% end %> <% content_for :baz do %>WON'T RENDER ME<% end %> <% content_for :foo do %>baz<% end %> sinatra-3.0.5/sinatra-contrib/spec/content_for/multiple_blocks.erubis000066400000000000000000000002441434717561400261500ustar00rootroot00000000000000<% content_for :foo do %>foo<% end %> <% content_for :foo do %>bar<% end %> <% content_for :baz do %>WON'T RENDER ME<% end %> <% content_for :foo do %>baz<% end %> sinatra-3.0.5/sinatra-contrib/spec/content_for/multiple_blocks.haml000066400000000000000000000001741434717561400256020ustar00rootroot00000000000000- content_for :foo do foo - content_for :foo do bar - content_for :baz do WON'T RENDER ME - content_for :foo do baz sinatra-3.0.5/sinatra-contrib/spec/content_for/multiple_blocks.hamlit000066400000000000000000000001741434717561400261370ustar00rootroot00000000000000- content_for :foo do foo - content_for :foo do bar - content_for :baz do WON'T RENDER ME - content_for :foo do baz sinatra-3.0.5/sinatra-contrib/spec/content_for/multiple_blocks.slim000066400000000000000000000002041434717561400256170ustar00rootroot00000000000000- content_for :foo do | foo - content_for :foo do | bar - content_for :baz do | WON'T RENDER ME - content_for :foo do | baz sinatra-3.0.5/sinatra-contrib/spec/content_for/multiple_yields.erb000066400000000000000000000001161434717561400254410ustar00rootroot00000000000000<%= yield_content :foo %> <%= yield_content :foo %> <%= yield_content :foo %> sinatra-3.0.5/sinatra-contrib/spec/content_for/multiple_yields.erubis000066400000000000000000000001161434717561400261620ustar00rootroot00000000000000<%= yield_content :foo %> <%= yield_content :foo %> <%= yield_content :foo %> sinatra-3.0.5/sinatra-contrib/spec/content_for/multiple_yields.haml000066400000000000000000000000771434717561400256200ustar00rootroot00000000000000= yield_content :foo = yield_content :foo = yield_content :foo sinatra-3.0.5/sinatra-contrib/spec/content_for/multiple_yields.hamlit000066400000000000000000000000771434717561400261550ustar00rootroot00000000000000= yield_content :foo = yield_content :foo = yield_content :foo sinatra-3.0.5/sinatra-contrib/spec/content_for/multiple_yields.slim000066400000000000000000000000771434717561400256430ustar00rootroot00000000000000= yield_content :foo = yield_content :foo = yield_content :foo sinatra-3.0.5/sinatra-contrib/spec/content_for/parameter_value.erb000066400000000000000000000000361434717561400254120ustar00rootroot00000000000000<% content_for :foo, 'foo' %> sinatra-3.0.5/sinatra-contrib/spec/content_for/parameter_value.erubis000066400000000000000000000000361434717561400261330ustar00rootroot00000000000000<% content_for :foo, 'foo' %> sinatra-3.0.5/sinatra-contrib/spec/content_for/parameter_value.haml000066400000000000000000000000321434717561400255570ustar00rootroot00000000000000- content_for :foo, 'foo' sinatra-3.0.5/sinatra-contrib/spec/content_for/parameter_value.hamlit000066400000000000000000000000321434717561400261140ustar00rootroot00000000000000- content_for :foo, 'foo' sinatra-3.0.5/sinatra-contrib/spec/content_for/parameter_value.slim000066400000000000000000000000321434717561400256020ustar00rootroot00000000000000- content_for :foo, 'foo' sinatra-3.0.5/sinatra-contrib/spec/content_for/passes_values.erb000066400000000000000000000000371434717561400251140ustar00rootroot00000000000000<%= yield_content :foo, 1, 2 %>sinatra-3.0.5/sinatra-contrib/spec/content_for/passes_values.erubis000066400000000000000000000000371434717561400256350ustar00rootroot00000000000000<%= yield_content :foo, 1, 2 %>sinatra-3.0.5/sinatra-contrib/spec/content_for/passes_values.haml000066400000000000000000000000341434717561400252620ustar00rootroot00000000000000!= yield_content :foo, 1, 2 sinatra-3.0.5/sinatra-contrib/spec/content_for/passes_values.hamlit000066400000000000000000000000341434717561400256170ustar00rootroot00000000000000!= yield_content :foo, 1, 2 sinatra-3.0.5/sinatra-contrib/spec/content_for/passes_values.slim000066400000000000000000000000341434717561400253050ustar00rootroot00000000000000== yield_content :foo, 1, 2 sinatra-3.0.5/sinatra-contrib/spec/content_for/same_key.erb000066400000000000000000000000461434717561400240340ustar00rootroot00000000000000<% content_for :foo do %>foo<% end %> sinatra-3.0.5/sinatra-contrib/spec/content_for/same_key.erubis000066400000000000000000000000461434717561400245550ustar00rootroot00000000000000<% content_for :foo do %>foo<% end %> sinatra-3.0.5/sinatra-contrib/spec/content_for/same_key.haml000066400000000000000000000000341434717561400242020ustar00rootroot00000000000000- content_for :foo do foo sinatra-3.0.5/sinatra-contrib/spec/content_for/same_key.hamlit000066400000000000000000000000341434717561400245370ustar00rootroot00000000000000- content_for :foo do foo sinatra-3.0.5/sinatra-contrib/spec/content_for/same_key.slim000066400000000000000000000000361434717561400242270ustar00rootroot00000000000000- content_for :foo do | foo sinatra-3.0.5/sinatra-contrib/spec/content_for/takes_values.erb000066400000000000000000000001011434717561400247150ustar00rootroot00000000000000<% content_for :foo do |a, b| %><%= a %> <%= b %><% end %>sinatra-3.0.5/sinatra-contrib/spec/content_for/takes_values.erubis000066400000000000000000000001011434717561400254360ustar00rootroot00000000000000<% content_for :foo do |a, b| %><%= a %> <%= b %><% end %>sinatra-3.0.5/sinatra-contrib/spec/content_for/takes_values.haml000066400000000000000000000000521434717561400250730ustar00rootroot00000000000000- content_for :foo do |a, b| %i= a =b sinatra-3.0.5/sinatra-contrib/spec/content_for/takes_values.hamlit000066400000000000000000000000521434717561400254300ustar00rootroot00000000000000- content_for :foo do |a, b| %i= a =b sinatra-3.0.5/sinatra-contrib/spec/content_for/takes_values.slim000066400000000000000000000000521434717561400251160ustar00rootroot00000000000000- content_for :foo do |a, b| i= a = b sinatra-3.0.5/sinatra-contrib/spec/content_for/yield_block.erb000066400000000000000000000000501434717561400245120ustar00rootroot00000000000000<% yield_content :foo do %>baz<% end %> sinatra-3.0.5/sinatra-contrib/spec/content_for/yield_block.erubis000066400000000000000000000000501434717561400252330ustar00rootroot00000000000000<% yield_content :foo do %>baz<% end %> sinatra-3.0.5/sinatra-contrib/spec/content_for/yield_block.haml000066400000000000000000000000371434717561400246700ustar00rootroot00000000000000= yield_content :foo do baz sinatra-3.0.5/sinatra-contrib/spec/content_for/yield_block.hamlit000066400000000000000000000000401434717561400252170ustar00rootroot00000000000000!= yield_content :foo do baz sinatra-3.0.5/sinatra-contrib/spec/content_for/yield_block.slim000066400000000000000000000000411434717561400247060ustar00rootroot00000000000000= yield_content :foo do | baz sinatra-3.0.5/sinatra-contrib/spec/content_for_spec.rb000066400000000000000000000214651434717561400231140ustar00rootroot00000000000000require 'spec_helper' RSpec.describe Sinatra::ContentFor do subject do Sinatra.new do helpers Sinatra::ContentFor set :views, File.expand_path("content_for", __dir__) end.new! end Tilt.prefer Tilt::ERBTemplate require 'hamlit' Tilt.register Tilt::HamlTemplate, :haml extend Forwardable def_delegators :subject, :content_for, :clear_content_for, :yield_content def render(engine, template) subject.send(:render, engine, template, :layout => false).gsub(/\s/, '') end describe "without templates" do it 'renders blocks declared with the same key you use when rendering' do content_for(:foo) { "foo" } expect(yield_content(:foo)).to eq("foo") end it 'renders blocks more than once' do content_for(:foo) { "foo" } 3.times { expect(yield_content(:foo)).to eq("foo") } end it 'does not render a block with a different key' do content_for(:bar) { "bar" } expect(yield_content(:foo)).to be_empty end it 'renders default content if no block matches the key and a default block is specified' do expect(yield_content(:foo) {}).to be_nil expect(yield_content(:foo) { "foo" }).to eq("foo") end it 'renders multiple blocks with the same key' do content_for(:foo) { "foo" } content_for(:foo) { "bar" } content_for(:bar) { "WON'T RENDER ME" } content_for(:foo) { "baz" } expect(yield_content(:foo)).to eq("foobarbaz") end it 'renders multiple blocks more than once' do content_for(:foo) { "foo" } content_for(:foo) { "bar" } content_for(:bar) { "WON'T RENDER ME" } content_for(:foo) { "baz" } 3.times { expect(yield_content(:foo)).to eq("foobarbaz") } end it 'passes values to the blocks' do content_for(:foo) { |a| a.upcase } expect(yield_content(:foo, 'a')).to eq("A") expect(yield_content(:foo, 'b')).to eq("B") end it 'clears named blocks with the specified key' do content_for(:foo) { "foo" } expect(yield_content(:foo)).to eq("foo") clear_content_for(:foo) expect(yield_content(:foo)).to be_empty end it 'takes an immediate value instead of a block' do content_for(:foo, "foo") expect(yield_content(:foo)).to eq("foo") end context 'when flush option was disabled' do it 'append content' do content_for(:foo, "foo") content_for(:foo, "bar") expect(yield_content(:foo)).to eq("foobar") end end context 'when flush option was enabled' do it 'flush first content' do content_for(:foo, "foo") content_for(:foo, "bar", flush: true) expect(yield_content(:foo)).to eq("bar") end end end # TODO: liquid markaby builder nokogiri engines = %w[erb erubi haml hamlit slim] engines.each do |inner| describe inner.capitalize do before :all do begin require inner rescue LoadError => e skip "Skipping: " << e.message end end describe "with yield_content in Ruby" do it 'renders blocks declared with the same key you use when rendering' do render inner, :same_key expect(yield_content(:foo).strip).to eq("foo") end it 'renders blocks more than once' do render inner, :same_key 3.times { expect(yield_content(:foo).strip).to eq("foo") } end it 'does not render a block with a different key' do render inner, :different_key expect(yield_content(:foo)).to be_empty end it 'renders default content if no block matches the key and a default block is specified' do render inner, :different_key expect(yield_content(:foo) { "foo" }).to eq("foo") end it 'renders multiple blocks with the same key' do render inner, :multiple_blocks expect(yield_content(:foo).gsub(/\s/, '')).to eq("foobarbaz") end it 'renders multiple blocks more than once' do render inner, :multiple_blocks 3.times { expect(yield_content(:foo).gsub(/\s/, '')).to eq("foobarbaz") } end it 'passes values to the blocks' do render inner, :takes_values expect(yield_content(:foo, 1, 2).gsub(/\s/, '')).to eq("12") end end describe "with content_for in Ruby" do it 'renders blocks declared with the same key you use when rendering' do content_for(:foo) { "foo" } expect(render(inner, :layout)).to eq("foo") end it 'renders blocks more than once' do content_for(:foo) { "foo" } expect(render(inner, :multiple_yields)).to eq("foofoofoo") end it 'does not render a block with a different key' do content_for(:bar) { "foo" } expect(render(inner, :layout)).to be_empty end it 'renders multiple blocks with the same key' do content_for(:foo) { "foo" } content_for(:foo) { "bar" } content_for(:bar) { "WON'T RENDER ME" } content_for(:foo) { "baz" } expect(render(inner, :layout)).to eq("foobarbaz") end it 'renders multiple blocks more than once' do content_for(:foo) { "foo" } content_for(:foo) { "bar" } content_for(:bar) { "WON'T RENDER ME" } content_for(:foo) { "baz" } expect(render(inner, :multiple_yields)).to eq("foobarbazfoobarbazfoobarbaz") end it 'passes values to the blocks' do content_for(:foo) { |a,b| "#{a}#{b}" } expect(render(inner, :passes_values)).to eq("12") end it 'clears named blocks with the specified key' do content_for(:foo) { "foo" } expect(render(inner, :layout)).to eq("foo") clear_content_for(:foo) expect(render(inner, :layout)).to be_empty end end describe "with content_for? in Ruby" do it 'renders block if key is set' do content_for(:foo) { "foot" } expect(render(inner, :footer)).to eq("foot") end it 'does not render a block if different key' do content_for(:different_key) { "foot" } expect(render(inner, :footer)).to be_empty end end engines.each do |outer| describe "with yield_content in #{outer.capitalize}" do def body last_response.body.gsub(/\s/, '') end before :all do begin require outer rescue LoadError => e skip "Skipping: " << e.message end end before do mock_app do helpers Sinatra::ContentFor set inner, :layout_engine => outer set :views, File.expand_path("content_for", __dir__) get('/:view') { render(inner, params[:view].to_sym) } get('/:layout/:view') do render inner, params[:view].to_sym, :layout => params[:layout].to_sym end end end describe 'with a default content block' do describe 'when content_for key exists' do it 'ignores default content and renders content' do expect(get('/yield_block/same_key')).to be_ok expect(body).to eq("foo") end end describe 'when content_for key is missing' do it 'renders default content block' do expect(get('/yield_block/different_key')).to be_ok expect(body).to eq("baz") end end end it 'renders content set as parameter' do expect(get('/parameter_value')).to be_ok expect(body).to eq("foo") end it 'renders blocks declared with the same key you use when rendering' do expect(get('/same_key')).to be_ok expect(body).to eq("foo") end it 'renders blocks more than once' do expect(get('/multiple_yields/same_key')).to be_ok expect(body).to eq("foofoofoo") end it 'does not render a block with a different key' do expect(get('/different_key')).to be_ok expect(body).to be_empty end it 'renders multiple blocks with the same key' do expect(get('/multiple_blocks')).to be_ok expect(body).to eq("foobarbaz") end it 'renders multiple blocks more than once' do expect(get('/multiple_yields/multiple_blocks')).to be_ok expect(body).to eq("foobarbazfoobarbazfoobarbaz") end it 'passes values to the blocks' do expect(get('/passes_values/takes_values')).to be_ok expect(body).to eq("12") end end end end end end sinatra-3.0.5/sinatra-contrib/spec/cookies_spec.rb000066400000000000000000000475351434717561400222360ustar00rootroot00000000000000require 'spec_helper' RSpec.describe Sinatra::Cookies do def cookie_route(*cookies, headers: {}, &block) result = nil set_cookie(cookies) @cookie_app.get('/') do result = instance_eval(&block) "ok" end get '/', {}, headers || {} expect(last_response).to be_ok expect(body).to eq("ok") result end def cookies(*set_cookies) cookie_route(*set_cookies) { cookies } end before do app = nil mock_app do helpers Sinatra::Cookies app = self end @cookie_app = app clear_cookies end describe :cookie_route do it 'runs the block' do ran = false cookie_route { ran = true } expect(ran).to be true end it 'returns the block result' do expect(cookie_route { 42 }).to eq(42) end end describe :== do it 'is comparable to hashes' do expect(cookies).to eq({}) end it 'is comparable to anything that responds to to_hash' do other = Struct.new(:to_hash).new({}) expect(cookies).to eq(other) end end describe :[] do it 'allows access to request cookies' do expect(cookies("foo=bar")["foo"]).to eq("bar") end it 'takes symbols as keys' do expect(cookies("foo=bar")[:foo]).to eq("bar") end it 'returns nil for missing keys' do expect(cookies("foo=bar")['bar']).to be_nil end it 'allows access to response cookies' do expect(cookie_route do response.set_cookie 'foo', 'bar' cookies['foo'] end).to eq('bar') end it 'favors response cookies over request cookies' do expect(cookie_route('foo=bar') do response.set_cookie 'foo', 'baz' cookies['foo'] end).to eq('baz') end it 'takes the last value for response cookies' do expect(cookie_route do response.set_cookie 'foo', 'bar' response.set_cookie 'foo', 'baz' cookies['foo'] end).to eq('baz') end end describe :[]= do it 'sets cookies to httponly' do expect(cookie_route do cookies['foo'] = 'bar' response['Set-Cookie'].lines.detect { |l| l.start_with? 'foo=' } end).to include('HttpOnly') end it 'sets domain to nil if localhost' do headers = {'HTTP_HOST' => 'localhost'} expect(cookie_route(headers: headers) do cookies['foo'] = 'bar' response['Set-Cookie'] end).not_to include("domain") end it 'sets the domain' do expect(cookie_route do cookies['foo'] = 'bar' response['Set-Cookie'].lines.detect { |l| l.start_with? 'foo=' } end).to include('domain=example.org') end it 'sets path to / by default' do expect(cookie_route do cookies['foo'] = 'bar' response['Set-Cookie'].lines.detect { |l| l.start_with? 'foo=' } end).to include('path=/') end it 'sets path to the script_name if app is nested' do expect(cookie_route do request.script_name = '/foo' cookies['foo'] = 'bar' response['Set-Cookie'].lines.detect { |l| l.start_with? 'foo=' } end).to include('path=/foo') end it 'sets a cookie' do cookie_route { cookies['foo'] = 'bar' } expect(cookie_jar['foo']).to eq('bar') end it 'adds a value to the cookies hash' do expect(cookie_route do cookies['foo'] = 'bar' cookies['foo'] end).to eq('bar') end end describe :assoc do it 'behaves like Hash#assoc' do cookies('foo=bar').assoc('foo') == ['foo', 'bar'] end end if Hash.method_defined? :assoc describe :clear do it 'removes request cookies from cookies hash' do jar = cookies('foo=bar') expect(jar['foo']).to eq('bar') jar.clear expect(jar['foo']).to be_nil end it 'removes response cookies from cookies hash' do expect(cookie_route do cookies['foo'] = 'bar' cookies.clear cookies['foo'] end).to be_nil end it 'expires existing cookies' do expect(cookie_route("foo=bar") do cookies.clear response['Set-Cookie'] end).to include("foo=;", "expires=", "1970 00:00:00") end end describe :compare_by_identity? do it { expect(cookies).not_to be_compare_by_identity } end describe :default do it { expect(cookies.default).to be_nil } end describe :default_proc do it { expect(cookies.default_proc).to be_nil } end describe :delete do it 'removes request cookies from cookies hash' do jar = cookies('foo=bar') expect(jar['foo']).to eq('bar') jar.delete 'foo' expect(jar['foo']).to be_nil end it 'removes response cookies from cookies hash' do expect(cookie_route do cookies['foo'] = 'bar' cookies.delete 'foo' cookies['foo'] end).to be_nil end it 'expires existing cookies' do expect(cookie_route("foo=bar") do cookies.delete 'foo' response['Set-Cookie'] end).to include("foo=;", "expires=", "1970 00:00:00") end it 'honours the app cookie_options' do @cookie_app.class_eval do set :cookie_options, { :path => '/foo', :domain => 'bar.com', :secure => true, :httponly => true } end cookie_header = cookie_route("foo=bar") do cookies.delete 'foo' response['Set-Cookie'] end expect(cookie_header).to include("path=/foo;", "domain=bar.com;", "secure;", "HttpOnly") end it 'does not touch other cookies' do expect(cookie_route("foo=bar", "bar=baz") do cookies.delete 'foo' cookies['bar'] end).to eq('baz') end it 'returns the previous value for request cookies' do expect(cookie_route("foo=bar") do cookies.delete "foo" end).to eq("bar") end it 'returns the previous value for response cookies' do expect(cookie_route do cookies['foo'] = 'bar' cookies.delete "foo" end).to eq("bar") end it 'returns nil for non-existing cookies' do expect(cookie_route { cookies.delete("foo") }).to be_nil end end describe :delete_if do it 'deletes cookies that match the block' do expect(cookie_route('foo=bar') do cookies['bar'] = 'baz' cookies['baz'] = 'foo' cookies.delete_if { |*a| a.include? 'bar' } cookies.values_at 'foo', 'bar', 'baz' end).to eq([nil, nil, 'foo']) end end describe :each do it 'loops through cookies' do keys = [] foo = nil bar = nil cookie_route('foo=bar', 'bar=baz') do cookies.each do |key, value| foo = value if key == 'foo' bar = value if key == 'bar' keys << key end end expect(keys.sort).to eq(['bar', 'foo']) expect(foo).to eq('bar') expect(bar).to eq('baz') end it 'favors response over request cookies' do seen = false key = nil value = nil cookie_route('foo=bar') do cookies[:foo] = 'baz' cookies.each do |k,v| key = k value = v end end expect(key).to eq('foo') expect(value).to eq('baz') expect(seen).to eq(false) end it 'does not loop through deleted cookies' do cookie_route('foo=bar') do cookies.delete :foo cookies.each { fail } end end it 'returns an enumerator' do keys = [] cookie_route('foo=bar') do enum = cookies.each enum.each { |key, value| keys << key } end keys.each{ |key| expect(key).to eq('foo')} end end describe :each_key do it 'loops through cookies' do keys = [] cookie_route('foo=bar', 'bar=baz') do cookies.each_key do |key| keys << key end end expect(keys.sort).to eq(['bar', 'foo']) end it 'only yields keys once' do seen = false cookie_route('foo=bar') do cookies[:foo] = 'baz' end expect(seen).to eq(false) end it 'does not loop through deleted cookies' do cookie_route('foo=bar') do cookies.delete :foo cookies.each_key { fail } end end it 'returns an enumerator' do keys = [] cookie_route('foo=bar') do enum = cookies.each_key enum.each { |key| keys << key } end keys.each{ |key| expect(key).to eq('foo')} end end describe :each_pair do it 'loops through cookies' do keys = [] foo = nil bar = nil cookie_route('foo=bar', 'bar=baz') do cookies.each_pair do |key, value| foo = value if key == 'foo' bar = value if key == 'bar' keys << key end end expect(keys.sort).to eq(['bar', 'foo']) expect(foo).to eq('bar') expect(bar).to eq('baz') end it 'favors response over request cookies' do seen = false key = nil value = nil cookie_route('foo=bar') do cookies[:foo] = 'baz' cookies.each_pair do |k, v| key = k value = v end end expect(key).to eq('foo') expect(value).to eq('baz') expect(seen).to eq(false) end it 'does not loop through deleted cookies' do cookie_route('foo=bar') do cookies.delete :foo cookies.each_pair { fail } end end it 'returns an enumerator' do keys = [] cookie_route('foo=bar') do enum = cookies.each_pair enum.each { |key, value| keys << key } end keys.each{ |key| expect(key).to eq('foo')} end end describe :each_value do it 'loops through cookies' do values = [] cookie_route('foo=bar', 'bar=baz') do cookies.each_value do |value| values << value end end expect(values.sort).to eq(['bar', 'baz']) end it 'favors response over request cookies' do value = nil cookie_route('foo=bar') do cookies[:foo] = 'baz' cookies.each_value do |v| value = v end end expect(value).to eq('baz') end it 'does not loop through deleted cookies' do cookie_route('foo=bar') do cookies.delete :foo cookies.each_value { fail } end end it 'returns an enumerator' do enum = nil cookie_route('foo=bar') do enum = cookies.each_value end enum.each { |value| expect(value).to eq('bar') } end end describe :empty? do it 'returns true if there are no cookies' do expect(cookies).to be_empty end it 'returns false if there are request cookies' do expect(cookies('foo=bar')).not_to be_empty end it 'returns false if there are response cookies' do expect(cookie_route do cookies['foo'] = 'bar' cookies.empty? end).to be false end it 'becomes true if response cookies are removed' do expect(cookie_route do cookies['foo'] = 'bar' cookies.delete :foo cookies.empty? end).to be true end it 'becomes true if request cookies are removed' do expect(cookie_route('foo=bar') do cookies.delete :foo cookies.empty? end).to be_truthy end it 'becomes true after clear' do expect(cookie_route('foo=bar', 'bar=baz') do cookies['foo'] = 'bar' cookies.clear cookies.empty? end).to be_truthy end end describe :fetch do it 'returns values from request cookies' do expect(cookies('foo=bar').fetch('foo')).to eq('bar') end it 'returns values from response cookies' do expect(cookie_route do cookies['foo'] = 'bar' cookies.fetch('foo') end).to eq('bar') end it 'favors response over request cookies' do expect(cookie_route('foo=baz') do cookies['foo'] = 'bar' cookies.fetch('foo') end).to eq('bar') end it 'raises an exception if key does not exist' do error = if defined? JRUBY_VERSION IndexError else KeyError end expect { cookies.fetch('foo') }.to raise_exception(error) end it 'returns the block result if missing' do expect(cookies.fetch('foo') { 'bar' }).to eq('bar') end end describe :flatten do it { expect(cookies('foo=bar').flatten).to eq({'foo' => 'bar'}.flatten) } end if Hash.method_defined? :flatten describe :has_key? do it 'checks request cookies' do expect(cookies('foo=bar')).to have_key('foo') end it 'checks response cookies' do jar = cookies jar['foo'] = 'bar' expect(jar).to have_key(:foo) end it 'does not use deleted cookies' do jar = cookies('foo=bar') jar.delete :foo expect(jar).not_to have_key('foo') end end describe :has_value? do it 'checks request cookies' do expect(cookies('foo=bar')).to have_value('bar') end it 'checks response cookies' do jar = cookies jar[:foo] = 'bar' expect(jar).to have_value('bar') end it 'does not use deleted cookies' do jar = cookies('foo=bar') jar.delete :foo expect(jar).not_to have_value('bar') end end describe :include? do it 'checks request cookies' do expect(cookies('foo=bar')).to include('foo') end it 'checks response cookies' do jar = cookies jar['foo'] = 'bar' expect(jar).to include(:foo) end it 'does not use deleted cookies' do jar = cookies('foo=bar') jar.delete :foo expect(jar).not_to include('foo') end end describe :keep_if do it 'removes entries' do jar = cookies('foo=bar', 'bar=baz') jar.keep_if { |*args| args == ['bar', 'baz'] } expect(jar).to eq({'bar' => 'baz'}) end end describe :key do it 'checks request cookies' do expect(cookies('foo=bar').key('bar')).to eq('foo') end it 'checks response cookies' do jar = cookies jar['foo'] = 'bar' expect(jar.key('bar')).to eq('foo') end it 'returns nil when missing' do expect(cookies('foo=bar').key('baz')).to be_nil end end describe :key? do it 'checks request cookies' do expect(cookies('foo=bar').key?('foo')).to be true end it 'checks response cookies' do jar = cookies jar['foo'] = 'bar' expect(jar.key?(:foo)).to be true end it 'does not use deleted cookies' do jar = cookies('foo=bar') jar.delete :foo expect(jar.key?('foo')).to be false end end describe :keys do it { expect(cookies('foo=bar').keys).to eq(['foo']) } end describe :length do it { expect(cookies.length).to eq(0) } it { expect(cookies('foo=bar').length).to eq(1) } end describe :member? do it 'checks request cookies' do expect(cookies('foo=bar').member?('foo')).to be true end it 'checks response cookies' do jar = cookies jar['foo'] = 'bar' expect(jar.member?(:foo)).to be true end it 'does not use deleted cookies' do jar = cookies('foo=bar') jar.delete :foo expect(jar.member?('foo')).to be false end end describe :merge do it 'is mergable with a hash' do expect(cookies('foo=bar').merge(:bar => :baz)).to eq({"foo" => "bar", :bar => :baz}) end it 'does not create cookies' do jar = cookies('foo=bar') jar.merge(:bar => 'baz') expect(jar).not_to include(:bar) end it 'takes a block for conflict resolution' do update = {'foo' => 'baz', 'bar' => 'baz'} merged = cookies('foo=bar').merge(update) do |key, old, other| expect(key).to eq('foo') expect(old).to eq('bar') expect(other).to eq('baz') 'foo' end expect(merged['foo']).to eq('foo') end end describe :merge! do it 'creates cookies' do jar = cookies('foo=bar') jar.merge! :bar => 'baz' expect(jar).to include('bar') end it 'overrides existing values' do jar = cookies('foo=bar') jar.merge! :foo => "baz" expect(jar["foo"]).to eq("baz") end it 'takes a block for conflict resolution' do update = {'foo' => 'baz', 'bar' => 'baz'} jar = cookies('foo=bar') jar.merge!(update) do |key, old, other| expect(key).to eq('foo') expect(old).to eq('bar') expect(other).to eq('baz') 'foo' end expect(jar['foo']).to eq('foo') end end describe :rassoc do it 'behaves like Hash#assoc' do cookies('foo=bar').rassoc('bar') == ['foo', 'bar'] end end if Hash.method_defined? :rassoc describe :reject do it 'removes entries from new hash' do jar = cookies('foo=bar', 'bar=baz') sub = jar.reject { |*args| args == ['bar', 'baz'] } expect(sub).to eq({'foo' => 'bar'}) expect(jar['bar']).to eq('baz') end end describe :reject! do it 'removes entries' do jar = cookies('foo=bar', 'bar=baz') jar.reject! { |*args| args == ['bar', 'baz'] } expect(jar).to eq({'foo' => 'bar'}) end end describe :replace do it 'replaces entries' do jar = cookies('foo=bar', 'bar=baz') jar.replace 'foo' => 'baz', 'baz' => 'bar' expect(jar).to eq({'foo' => 'baz', 'baz' => 'bar'}) end end describe :set do it 'sets a cookie' do cookie_route { cookies.set('foo', value: 'bar') } expect(cookie_jar['foo']).to eq('bar') end it 'sets a cookie with HttpOnly' do expect(cookie_route do request.script_name = '/foo' cookies.set('foo', value: 'bar', httponly: true) response['Set-Cookie'].lines.detect { |l| l.start_with? 'foo=' } end).to include('HttpOnly') end it 'sets a cookie without HttpOnly' do expect(cookie_route do request.script_name = '/foo' cookies.set('foo', value: 'bar', httponly: false) response['Set-Cookie'].lines.detect { |l| l.start_with? 'foo=' } end).not_to include('HttpOnly') end end describe :select do it 'removes entries from new hash' do jar = cookies('foo=bar', 'bar=baz') sub = jar.select { |*args| args != ['bar', 'baz'] } expect(sub).to eq({'foo' => 'bar'}.select { true }) expect(jar['bar']).to eq('baz') end end describe :select! do it 'removes entries' do jar = cookies('foo=bar', 'bar=baz') jar.select! { |*args| args != ['bar', 'baz'] } expect(jar).to eq({'foo' => 'bar'}) end end if Hash.method_defined? :select! describe :shift do it 'removes from the hash' do jar = cookies('foo=bar') expect(jar.shift).to eq(['foo', 'bar']) expect(jar).not_to include('bar') end end describe :size do it { expect(cookies.size).to eq(0) } it { expect(cookies('foo=bar').size).to eq(1) } end describe :update do it 'creates cookies' do jar = cookies('foo=bar') jar.update :bar => 'baz' expect(jar).to include('bar') end it 'overrides existing values' do jar = cookies('foo=bar') jar.update :foo => "baz" expect(jar["foo"]).to eq("baz") end it 'takes a block for conflict resolution' do merge = {'foo' => 'baz', 'bar' => 'baz'} jar = cookies('foo=bar') jar.update(merge) do |key, old, other| expect(key).to eq('foo') expect(old).to eq('bar') expect(other).to eq('baz') 'foo' end expect(jar['foo']).to eq('foo') end end describe :value? do it 'checks request cookies' do expect(cookies('foo=bar').value?('bar')).to be true end it 'checks response cookies' do jar = cookies jar[:foo] = 'bar' expect(jar.value?('bar')).to be true end it 'does not use deleted cookies' do jar = cookies('foo=bar') jar.delete :foo expect(jar.value?('bar')).to be false end end describe :values do it { expect(cookies('foo=bar', 'bar=baz').values.sort).to eq(['bar', 'baz']) } end describe :values_at do it { expect(cookies('foo=bar', 'bar=baz').values_at('foo')).to eq(['bar']) } end end sinatra-3.0.5/sinatra-contrib/spec/custom_logger_spec.rb000066400000000000000000000016051434717561400234370ustar00rootroot00000000000000require 'spec_helper' require 'sinatra/custom_logger' RSpec.describe Sinatra::CustomLogger do before do rack_logger = @rack_logger = double mock_app do helpers Sinatra::CustomLogger before do env['rack.logger'] = rack_logger end get '/' do logger.info 'Logged message' 'Response' end end end describe '#logger' do it 'falls back to request.logger' do expect(@rack_logger).to receive(:info).with('Logged message') get '/' end context 'logger setting is set' do before do custom_logger = @custom_logger = double @app.class_eval do configure do set :logger, custom_logger end end end it 'calls custom logger' do expect(@custom_logger).to receive(:info).with('Logged message') get '/' end end end end sinatra-3.0.5/sinatra-contrib/spec/extension_spec.rb000066400000000000000000000012441434717561400226010ustar00rootroot00000000000000require 'spec_helper' RSpec.describe Sinatra::Extension do module ExampleExtension extend Sinatra::Extension set :foo, :bar settings.set :bar, :blah configure :test, :production do set :reload_stuff, false end configure :development do set :reload_stuff, true end get '/' do "from extension, yay" end end before { mock_app { register ExampleExtension }} it('allows using set') { expect(settings.foo).to eq(:bar) } it('implements configure') { expect(settings.reload_stuff).to be false } it 'allows defing routes' do expect(get('/')).to be_ok expect(body).to eq("from extension, yay") end end sinatra-3.0.5/sinatra-contrib/spec/json_spec.rb000066400000000000000000000063631434717561400215450ustar00rootroot00000000000000require 'multi_json' require 'spec_helper' require 'okjson' RSpec.shared_examples_for "a json encoder" do |lib, const| before do begin require lib if lib @encoder = eval(const) rescue LoadError skip "unable to load #{lib}" end end it "allows setting :encoder to #{const}" do enc = @encoder mock_app { get('/') { json({'foo' => 'bar'}, :encoder => enc) }} results_in 'foo' => 'bar' end it "allows setting settings.json_encoder to #{const}" do enc = @encoder mock_app do set :json_encoder, enc get('/') { json 'foo' => 'bar' } end results_in 'foo' => 'bar' end end RSpec.describe Sinatra::JSON do def mock_app(&block) super do class_eval(&block) end end def results_in(obj) expect(OkJson.decode(get('/').body)).to eq(obj) end it "encodes objects to json out of the box" do mock_app { get('/') { json :foo => [1, 'bar', nil] } } results_in 'foo' => [1, 'bar', nil] end it "sets the content type to 'application/json'" do mock_app { get('/') { json({}) } } expect(get('/')["Content-Type"]).to include("application/json") end it "allows overriding content type with :content_type" do mock_app { get('/') { json({}, :content_type => "foo/bar") } } expect(get('/')["Content-Type"]).to eq("foo/bar") end it "accepts shorthands for :content_type" do mock_app { get('/') { json({}, :content_type => :js) } } expect(get('/')["Content-Type"]).to eq("application/javascript;charset=utf-8") end it 'calls generate on :encoder if available' do enc = Object.new def enc.generate(obj) obj.inspect end mock_app { get('/') { json(42, :encoder => enc) }} expect(get('/').body).to eq('42') end it 'calls encode on :encoder if available' do enc = Object.new def enc.encode(obj) obj.inspect end mock_app { get('/') { json(42, :encoder => enc) }} expect(get('/').body).to eq('42') end it 'sends :encoder as method call if it is a Symbol' do mock_app { get('/') { json(42, :encoder => :inspect) }} expect(get('/').body).to eq('42') end it 'calls generate on settings.json_encoder if available' do enc = Object.new def enc.generate(obj) obj.inspect end mock_app do set :json_encoder, enc get('/') { json 42 } end expect(get('/').body).to eq('42') end it 'calls encode on settings.json_encode if available' do enc = Object.new def enc.encode(obj) obj.inspect end mock_app do set :json_encoder, enc get('/') { json 42 } end expect(get('/').body).to eq('42') end it 'sends settings.json_encode as method call if it is a Symbol' do mock_app do set :json_encoder, :inspect get('/') { json 42 } end expect(get('/').body).to eq('42') end describe('Yajl') { it_should_behave_like "a json encoder", "yajl", "Yajl::Encoder" } unless defined? JRUBY_VERSION describe('JSON') { it_should_behave_like "a json encoder", "json", "::JSON" } describe('OkJson') { it_should_behave_like "a json encoder", nil, "OkJson" } describe('to_json') { it_should_behave_like "a json encoder", "json", ":to_json" } describe('without') { it_should_behave_like "a json encoder", nil, "Sinatra::JSON" } end sinatra-3.0.5/sinatra-contrib/spec/link_header_spec.rb000066400000000000000000000045051434717561400230350ustar00rootroot00000000000000require 'spec_helper' RSpec.describe Sinatra::LinkHeader do before do mock_app do helpers Sinatra::LinkHeader before('/') { link 'something', :rel => 'from-filter', :foo => :bar } get '/' do link :something, 'booyah' end get '/style' do stylesheet '/style.css' end get '/prefetch' do prefetch '/foo' end get '/link_headers' do response['Link'] = " ;bar=\"baz\"" stylesheet '/style.css' prefetch '/foo' link_headers end end end describe :link do it "sets link headers" do get '/' expect(headers['Link'].lines).to include('; rel="something"') end it "returns link html tags" do get '/' expect(body).to eq('') end it "takes an options hash" do get '/' elements = ["", "foo=\"bar\"", "rel=\"from-filter\""] expect(headers['Link'].split(",\n").first.strip.split('; ').sort).to eq(elements) end end describe :stylesheet do it 'sets link headers' do get '/style' expect(headers['Link']).to match(%r{^;}) end it 'sets type to text/css' do get '/style' expect(headers['Link']).to include('type="text/css"') end it 'sets rel to stylesheet' do get '/style' expect(headers['Link']).to include('rel="stylesheet"') end it 'returns html tag' do get '/style' expect(body).to match(%r{^;}) end it 'sets rel to prefetch' do get '/prefetch' expect(headers['Link']).to include('rel="prefetch"') end it 'returns html tag' do get '/prefetch' expect(body).to eq('') end end describe :link_headers do it 'generates html for all link headers' do get '/link_headers' expect(body).to include('') expect(body).to include('') end end end sinatra-3.0.5/sinatra-contrib/spec/multi_route_spec.rb000066400000000000000000000023641434717561400231410ustar00rootroot00000000000000require 'spec_helper' RSpec.describe Sinatra::MultiRoute do it 'does not break normal routing' do mock_app do register Sinatra::MultiRoute get('/') { 'normal' } end expect(get('/')).to be_ok expect(body).to eq('normal') end it 'supports multiple routes' do mock_app do register Sinatra::MultiRoute get('/foo', '/bar') { 'paths' } end expect(get('/foo')).to be_ok expect(body).to eq('paths') expect(get('/bar')).to be_ok expect(body).to eq('paths') end it 'triggers conditions' do count = 0 mock_app do register Sinatra::MultiRoute set(:some_condition) { |_| count += 1 } get('/foo', '/bar', :some_condition => true) { 'paths' } end expect(count).to eq(4) end it 'supports multiple verbs' do mock_app do register Sinatra::MultiRoute route('PUT', 'POST', '/') { 'verb' } end expect(post('/')).to be_ok expect(body).to eq('verb') expect(put('/')).to be_ok expect(body).to eq('verb') end it 'takes symbols as verbs' do mock_app do register Sinatra::MultiRoute route(:get, '/baz') { 'symbol as verb' } end expect(get('/baz')).to be_ok expect(body).to eq('symbol as verb') end end sinatra-3.0.5/sinatra-contrib/spec/namespace/000077500000000000000000000000001434717561400211615ustar00rootroot00000000000000sinatra-3.0.5/sinatra-contrib/spec/namespace/foo.erb000066400000000000000000000000031434717561400224270ustar00rootroot00000000000000hi sinatra-3.0.5/sinatra-contrib/spec/namespace/nested/000077500000000000000000000000001434717561400224435ustar00rootroot00000000000000sinatra-3.0.5/sinatra-contrib/spec/namespace/nested/foo.erb000066400000000000000000000000031434717561400237110ustar00rootroot00000000000000ho sinatra-3.0.5/sinatra-contrib/spec/namespace_spec.rb000066400000000000000000000657671434717561400225450ustar00rootroot00000000000000require 'spec_helper' RSpec.describe Sinatra::Namespace do verbs = [:get, :head, :post, :put, :delete, :options, :patch] def mock_app(&block) super do register Sinatra::Namespace class_eval(&block) end end def namespace(*args, &block) mock_app { namespace(*args, &block) } end verbs.each do |verb| describe "HTTP #{verb.to_s.upcase}" do it 'prefixes the path with the namespace' do namespace('/foo') { send(verb, '/bar') { 'baz' }} expect(send(verb, '/foo/bar')).to be_ok expect(body).to eq('baz') unless verb == :head expect(send(verb, '/foo/baz')).not_to be_ok end describe 'redirect_to' do it 'redirect within namespace' do namespace('/foo') { send(verb, '/bar') { redirect_to '/foo_bar' }} expect(send(verb, '/foo/bar')).to be_redirect expect(send(verb, '/foo/bar').location).to include("/foo/foo_bar") end end context 'when namespace is a string' do it 'accepts routes with no path' do namespace('/foo') { send(verb) { 'bar' } } expect(send(verb, '/foo')).to be_ok expect(body).to eq('bar') unless verb == :head end it 'accepts the path as a named parameter' do namespace('/foo') { send(verb, '/:bar') { params[:bar] }} expect(send(verb, '/foo/bar')).to be_ok expect(body).to eq('bar') unless verb == :head expect(send(verb, '/foo/baz')).to be_ok expect(body).to eq('baz') unless verb == :head end it 'accepts the path as a regular expression' do namespace('/foo') { send(verb, /\/\d\d/) { 'bar' }} expect(send(verb, '/foo/12')).to be_ok expect(body).to eq 'bar' unless verb == :head expect(send(verb, '/foo/123')).not_to be_ok end end context 'when namespace is a named parameter' do it 'accepts routes with no path' do namespace('/:foo') { send(verb) { 'bar' } } expect(send(verb, '/foo')).to be_ok expect(body).to eq('bar') unless verb == :head end it 'sets the parameter correctly' do namespace('/:foo') { send(verb, '/bar') { params[:foo] }} expect(send(verb, '/foo/bar')).to be_ok expect(body).to eq('foo') unless verb == :head expect(send(verb, '/fox/bar')).to be_ok expect(body).to eq('fox') unless verb == :head expect(send(verb, '/foo/baz')).not_to be_ok end it 'accepts the path as a named parameter' do namespace('/:foo') { send(verb, '/:bar') { params[:bar] }} expect(send(verb, '/foo/bar')).to be_ok expect(body).to eq('bar') unless verb == :head expect(send(verb, '/foo/baz')).to be_ok expect(body).to eq('baz') unless verb == :head end it 'accepts the path as regular expression' do namespace('/:foo') { send(verb, %r{/bar}) { params[:foo] }} expect(send(verb, '/foo/bar')).to be_ok expect(body).to eq('foo') unless verb == :head expect(send(verb, '/fox/bar')).to be_ok expect(body).to eq('fox') unless verb == :head expect(send(verb, '/foo/baz')).not_to be_ok end end context 'when namespace is a regular expression' do it 'accepts routes with no path' do namespace(%r{/foo}) { send(verb) { 'bar' } } expect(send(verb, '/foo')).to be_ok expect(body).to eq('bar') unless verb == :head end it 'accepts the path as a named parameter' do namespace(%r{/foo}) { send(verb, '/:bar') { params[:bar] }} expect(send(verb, '/foo/bar')).to be_ok expect(body).to eq('bar') unless verb == :head expect(send(verb, '/foo/baz')).to be_ok expect(body).to eq('baz') unless verb == :head end it 'accepts the path as a regular expression' do namespace(/\/\d\d/) { send(verb, /\/\d\d/) { 'foo' }} expect(send(verb, '/23/12')).to be_ok expect(body).to eq('foo') unless verb == :head expect(send(verb, '/123/12')).not_to be_ok end describe "before/after filters" do it 'trigger before filter' do ran = false namespace(/\/foo\/([^\/&?]+)\/bar\/([^\/&?]+)\//) { before { ran = true };} send(verb, '/bar/') expect(ran).to eq(false) send(verb, '/foo/1/bar/1/') expect(ran).to eq(true) end it 'trigger after filter' do ran = false namespace(/\/foo\/([^\/&?]+)\/bar\/([^\/&?]+)\//) { after { ran = true };} send(verb, '/bar/') expect(ran).to eq(false) send(verb, '/foo/1/bar/1/') expect(ran).to eq(true) end end describe 'helpers' do it 'are defined using the helpers method' do namespace(/\/foo\/([^\/&?]+)\/bar\/([^\/&?]+)\//) do helpers do def foo 'foo' end end send verb, '' do foo.to_s end end expect(send(verb, '/foo/1/bar/1/')).to be_ok expect(body).to eq('foo') unless verb == :head end end end context 'when namespace is a splat' do it 'accepts the path as a splat' do namespace('/*') { send(verb, '/*') { params[:splat].join ' - ' }} expect(send(verb, '/foo/bar')).to be_ok expect(body).to eq('foo - bar') unless verb == :head end end describe 'before-filters' do specify 'are triggered' do ran = false namespace('/foo') { before { ran = true }} send(verb, '/foo') expect(ran).to be true end specify 'are not triggered for a different namespace' do ran = false namespace('/foo') { before { ran = true }} send(verb, '/fox') expect(ran).to be false end end describe 'after-filters' do specify 'are triggered' do ran = false namespace('/foo') { after { ran = true }} send(verb, '/foo') expect(ran).to be true end specify 'are not triggered for a different namespace' do ran = false namespace('/foo') { after { ran = true }} send(verb, '/fox') expect(ran).to be false end end describe 'conditions' do context 'when the namespace has no prefix' do specify 'are accepted in the namespace' do mock_app do namespace(:host_name => 'example.com') { send(verb) { 'yes' }} send(verb, '/') { 'no' } end send(verb, '/', {}, 'HTTP_HOST' => 'example.com') expect(last_response).to be_ok expect(body).to eq('yes') unless verb == :head send(verb, '/', {}, 'HTTP_HOST' => 'example.org') expect(last_response).to be_ok expect(body).to eq('no') unless verb == :head end specify 'are accepted in the route definition' do namespace :host_name => 'example.com' do send(verb, '/foo', :provides => :txt) { 'ok' } end expect(send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/plain')).to be_ok expect(send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/html')).not_to be_ok expect(send(verb, '/foo', {}, 'HTTP_HOST' => 'example.org', 'HTTP_ACCEPT' => 'text/plain')).not_to be_ok end specify 'are accepted in the before-filter' do ran = false namespace :provides => :txt do before('/foo', :host_name => 'example.com') { ran = true } send(verb, '/*') { 'ok' } end send(verb, '/foo', {}, 'HTTP_HOST' => 'example.org', 'HTTP_ACCEPT' => 'text/plain') expect(ran).to be false send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/html') expect(ran).to be false send(verb, '/bar', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/plain') expect(ran).to be false send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/plain') expect(ran).to be true end specify 'are accepted in the after-filter' do ran = false namespace :provides => :txt do after('/foo', :host_name => 'example.com') { ran = true } send(verb, '/*') { 'ok' } end send(verb, '/foo', {}, 'HTTP_HOST' => 'example.org', 'HTTP_ACCEPT' => 'text/plain') expect(ran).to be false send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/html') expect(ran).to be false send(verb, '/bar', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/plain') expect(ran).to be false send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/plain') expect(ran).to be true end end context 'when the namespace is a string' do specify 'are accepted in the namespace' do namespace '/foo', :host_name => 'example.com' do send(verb) { 'ok' } end expect(send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com')).to be_ok expect(send(verb, '/foo', {}, 'HTTP_HOST' => 'example.org')).not_to be_ok end specify 'are accepted in the before-filter' do namespace '/foo' do before { @yes = nil } before(:host_name => 'example.com') { @yes = 'yes' } send(verb) { @yes || 'no' } end send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com') expect(last_response).to be_ok expect(body).to eq('yes') unless verb == :head send(verb, '/foo', {}, 'HTTP_HOST' => 'example.org') expect(last_response).to be_ok expect(body).to eq('no') unless verb == :head end specify 'are accepted in the after-filter' do ran = false namespace '/foo' do before(:host_name => 'example.com') { ran = true } send(verb) { 'ok' } end send(verb, '/foo', {}, 'HTTP_HOST' => 'example.org') expect(ran).to be false send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com') expect(ran).to be true end specify 'are accepted in the route definition' do namespace '/foo' do send(verb, :host_name => 'example.com') { 'ok' } end expect(send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com')).to be_ok expect(send(verb, '/foo', {}, 'HTTP_HOST' => 'example.org')).not_to be_ok end context 'when the namespace has a condition' do specify 'are accepted in the before-filter' do ran = false namespace '/', :provides => :txt do before(:host_name => 'example.com') { ran = true } send(verb) { 'ok' } end send(verb, '/', {}, 'HTTP_HOST' => 'example.org', 'HTTP_ACCEPT' => 'text/plain') expect(ran).to be false send(verb, '/', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/html') expect(ran).to be false send(verb, '/', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/plain') expect(ran).to be true end specify 'are accepted in the filters' do ran = false namespace '/f', :provides => :txt do before('oo', :host_name => 'example.com') { ran = true } send(verb, '/*') { 'ok' } end send(verb, '/foo', {}, 'HTTP_HOST' => 'example.org', 'HTTP_ACCEPT' => 'text/plain') expect(ran).to be false send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/html') expect(ran).to be false send(verb, '/far', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/plain') expect(ran).to be false send(verb, '/foo', {}, 'HTTP_HOST' => 'example.com', 'HTTP_ACCEPT' => 'text/plain') expect(ran).to be true end end end end describe 'helpers' do it 'are defined using the helpers method' do namespace '/foo' do helpers do def magic 42 end end send verb, '/bar' do magic.to_s end end expect(send(verb, '/foo/bar')).to be_ok expect(body).to eq('42') unless verb == :head end it 'can be defined as normal methods' do namespace '/foo' do def magic 42 end send verb, '/bar' do magic.to_s end end expect(send(verb, '/foo/bar')).to be_ok expect(body).to eq('42') unless verb == :head end it 'can be defined using module mixins' do mixin = Module.new do def magic 42 end end namespace '/foo' do helpers mixin send verb, '/bar' do magic.to_s end end expect(send(verb, '/foo/bar')).to be_ok expect(body).to eq('42') unless verb == :head end specify 'are unavailable outside the namespace where they are defined' do mock_app do namespace '/foo' do def magic 42 end send verb, '/bar' do magic.to_s end end send verb, '/' do magic.to_s end end expect { send verb, '/' }.to raise_error(NameError) end specify 'are unavailable outside the namespace that they are mixed into' do mixin = Module.new do def magic 42 end end mock_app do namespace '/foo' do helpers mixin send verb, '/bar' do magic.to_s end end send verb, '/' do magic.to_s end end expect { send verb, '/' }.to raise_error(NameError) end specify 'are available to nested namespaces' do mock_app do helpers do def magic 42 end end namespace '/foo' do send verb, '/bar' do magic.to_s end end end expect(send(verb, '/foo/bar')).to be_ok expect(body).to eq('42') unless verb == :head end specify 'can call super from nested definitions' do mock_app do helpers do def magic 42 end end namespace '/foo' do def magic super - 19 end send verb, '/bar' do magic.to_s end end end expect(send(verb, '/foo/bar')).to be_ok expect(body).to eq('23') unless verb == :head end end describe 'nesting' do it 'routes to nested namespaces' do namespace '/foo' do namespace '/bar' do send(verb, '/baz') { 'OKAY!!11!'} end end expect(send(verb, '/foo/bar/baz')).to be_ok expect(body).to eq('OKAY!!11!') unless verb == :head end it 'works correctly if deep nesting' do namespace '/a' do namespace '/b' do namespace '/c' do send(verb, '') { 'hey' } end end end expect(send(verb, '/a/b/c')).to be_ok expect(body).to eq('hey') unless verb == :head end it 'exposes helpers to nested namespaces' do namespace '/foo' do helpers do def magic 42 end end namespace '/bar' do send verb, '/baz' do magic.to_s end end end expect(send(verb, '/foo/bar/baz')).to be_ok expect(body).to eq('42') unless verb == :head end specify 'does not provide access to nested helper methods' do namespace '/foo' do namespace '/bar' do def magic 42 end send verb, '/baz' do magic.to_s end end send verb do magic.to_s end end expect { send verb, '/foo' }.to raise_error(NameError) end it 'accepts a nested namespace as a named parameter' do namespace('/:a') { namespace('/:b') { send(verb) { params[:a] }}} expect(send(verb, '/foo/bar')).to be_ok expect(body).to eq('foo') unless verb == :head end end describe 'error handling' do it 'can be customized using the not_found block' do namespace('/de') do not_found { 'nicht gefunden' } end expect(send(verb, '/foo').status).to eq 404 expect(last_response.body).not_to eq 'nicht gefunden' unless verb == :head expect(get('/en/foo').status).to eq 404 expect(last_response.body).not_to eq 'nicht gefunden' unless verb == :head expect(get('/de/foo').status).to eq 404 expect(last_response.body).to eq 'nicht gefunden' unless verb == :head end it 'can be customized for specific error codes' do namespace('/de') do error(404) { 'nicht gefunden' } end expect(send(verb, '/foo').status).to eq 404 expect(last_response.body).not_to eq 'nicht gefunden' unless verb == :head expect(get('/en/foo').status).to eq 404 expect(last_response.body).not_to eq 'nicht gefunden' unless verb == :head expect(get('/de/foo').status).to eq 404 expect(last_response.body).to eq 'nicht gefunden' unless verb == :head end it 'falls back to the handler defined in the base app' do mock_app do error(404) { 'not found...' } namespace('/en') do end namespace('/de') do error(404) { 'nicht gefunden' } end end expect(send(verb, '/foo').status).to eq 404 expect(last_response.body).to eq 'not found...' unless verb == :head expect(get('/en/foo').status).to eq 404 expect(last_response.body).to eq 'not found...' unless verb == :head expect(get('/de/foo').status).to eq 404 expect(last_response.body).to eq 'nicht gefunden' unless verb == :head end it 'can be customized for specific Exception classes' do mock_app do class AError < StandardError; end class BError < AError; end error(AError) do body('auth failed') 401 end namespace('/en') do get '/foo' do raise BError end end namespace('/de') do error(AError) do body('methode nicht erlaubt') 406 end get '/foo' do raise BError end end end expect(get('/en/foo').status).to eq 401 expect(last_response.body).to eq 'auth failed' unless verb == :head expect(get('/de/foo').status).to eq 406 expect(last_response.body).to eq 'methode nicht erlaubt' unless verb == :head end it "allows custom error handlers when namespace is declared as /en/:id. Issue #119" do mock_app { class CError < StandardError; end error { raise "should not come here" } namespace('/en/:id') do error(CError) { 201 } get '/?' do raise CError end end } expect(get('/en/1').status).to eq(201) end end unless verb == :head describe 'templates' do specify 'default to the base app\'s template' do mock_app do template(:foo) { 'hi' } send(verb, '/') { erb :foo } namespace '/foo' do send(verb) { erb :foo } end end expect(send(verb, '/').body).to eq 'hi' expect(send(verb, '/foo').body).to eq 'hi' end specify 'can be nested' do mock_app do template(:foo) { 'hi' } send(verb, '/') { erb :foo } namespace '/foo' do template(:foo) { 'ho' } send(verb) { erb :foo } end end expect(send(verb, '/').body).to eq 'hi' expect(send(verb, '/foo').body).to eq 'ho' end specify 'can use a custom views directory' do mock_app do set :views, File.expand_path('namespace', __dir__) send(verb, '/') { erb :foo } namespace('/foo') do set :views, File.expand_path('namespace/nested', __dir__) send(verb) { erb :foo } end end expect(send(verb, '/').body).to eq "hi\n" expect(send(verb, '/foo').body).to eq "ho\n" end specify 'default to the base app\'s layout' do mock_app do layout { 'he said: <%= yield %>' } template(:foo) { 'hi' } send(verb, '/') { erb :foo } namespace '/foo' do template(:foo) { 'ho' } send(verb) { erb :foo } end end expect(send(verb, '/').body).to eq 'he said: hi' expect(send(verb, '/foo').body).to eq 'he said: ho' end specify 'can define nested layouts' do mock_app do layout { 'Hello <%= yield %>!' } template(:foo) { 'World' } send(verb, '/') { erb :foo } namespace '/foo' do layout { 'Hi <%= yield %>!' } send(verb) { erb :foo } end end expect(send(verb, '/').body).to eq 'Hello World!' expect(send(verb, '/foo').body).to eq 'Hi World!' end specify 'can render strings' do mock_app do namespace '/foo' do send(verb) { erb 'foo' } end end expect(send(verb, '/foo').body).to eq 'foo' end specify 'can render strings nested' do mock_app do namespace '/foo' do namespace '/bar' do send(verb) { erb 'bar' } end end end expect(send(verb, '/foo/bar').body).to eq 'bar' end end end describe 'extensions' do specify 'provide read access to settings' do value = nil mock_app do set :foo, 42 namespace '/foo' do value = foo end end expect(value).to eq 42 end specify 'can be registered within a namespace' do a = b = nil extension = Module.new { define_method(:views) { 'CUSTOM!!!' } } mock_app do namespace '/' do register extension a = views end b = views end expect(a).to eq 'CUSTOM!!!' expect(b).not_to eq 'CUSTOM!!!' end specify 'trigger the route_added hook' do route = nil extension = Module.new extension.singleton_class.class_eval do define_method(:route_added) { |*r| route = r } end mock_app do namespace '/f' do register extension get('oo') { } end get('/bar') { } end expect(route[1]).to eq(Mustermann.new '/foo') end specify 'prevent app-global settings from being changed' do expect { namespace('/') { set :foo, :bar }}.to raise_error(ArgumentError) end end end end describe 'settings' do it 'provides access to top-level settings' do mock_app do set :foo, 'ok' namespace '/foo' do get '/bar' do settings.foo end end end expect(get('/foo/bar').status).to eq(200) expect(last_response.body).to eq('ok') end it 'sets hashes correctly' do mock_app do namespace '/foo' do set erb: 'o', haml: 'k' get '/bar' do settings.erb + settings.haml end end end expect(get('/foo/bar').status).to eq(200) expect(last_response.body).to eq('ok') end it 'uses some repro' do mock_app do set :foo, 42 namespace '/foo' do get '/bar' do #settings.respond_to?(:foo).to_s settings.foo.to_s end end end expect(get('/foo/bar').status).to eq(200) expect(last_response.body).to eq('42') end it 'allows checking setting existence with respond_to?' do mock_app do set :foo, 42 namespace '/foo' do get '/bar' do settings.respond_to?(:foo).to_s end end end expect(get('/foo/bar').status).to eq(200) expect(last_response.body).to eq('true') end it 'avoids executing filters even if prefix matches with other namespace' do mock_app do helpers do def dump_args(*args) args.inspect end end namespace '/foo' do helpers do def dump_args(*args) super(:foo, *args) end end get('') { dump_args } end namespace '/foo-bar' do helpers do def dump_args(*args) super(:foo_bar, *args) end end get('') { dump_args } end end get '/foo-bar' expect(last_response.body).to eq('[:foo_bar]') end end it 'forbids unknown engine settings' do expect { mock_app do namespace '/foo' do set :unknownsetting end end }.to raise_error(ArgumentError, 'may not set unknownsetting') end end sinatra-3.0.5/sinatra-contrib/spec/okjson.rb000066400000000000000000000326341434717561400210650ustar00rootroot00000000000000# Copyright 2011 Keith Rarick # # 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. # See https://github.com/kr/okjson for updates. require 'stringio' # Some parts adapted from # http://golang.org/src/pkg/json/decode.go and # http://golang.org/src/pkg/utf8/utf8.go module OkJson extend self # Decodes a json document in string s and # returns the corresponding ruby value. # String s must be valid UTF-8. If you have # a string in some other encoding, convert # it first. # # String values in the resulting structure # will be UTF-8. def decode(s) ts = lex(s) v, ts = textparse(ts) if ts.length > 0 raise Error, 'trailing garbage' end v end # Parses a "json text" in the sense of RFC 4627. # Returns the parsed value and any trailing tokens. # Note: this is almost the same as valparse, # except that it does not accept atomic values. def textparse(ts) if ts.length < 0 raise Error, 'empty' end typ, _, val = ts[0] case typ when '{' then objparse(ts) when '[' then arrparse(ts) else raise Error, "unexpected #{val.inspect}" end end # Parses a "value" in the sense of RFC 4627. # Returns the parsed value and any trailing tokens. def valparse(ts) if ts.length < 0 raise Error, 'empty' end typ, _, val = ts[0] case typ when '{' then objparse(ts) when '[' then arrparse(ts) when :val,:str then [val, ts[1..-1]] else raise Error, "unexpected #{val.inspect}" end end # Parses an "object" in the sense of RFC 4627. # Returns the parsed value and any trailing tokens. def objparse(ts) ts = eat('{', ts) obj = {} if ts[0][0] == '}' return obj, ts[1..-1] end k, v, ts = pairparse(ts) obj[k] = v if ts[0][0] == '}' return obj, ts[1..-1] end loop do ts = eat(',', ts) k, v, ts = pairparse(ts) obj[k] = v if ts[0][0] == '}' return obj, ts[1..-1] end end end # Parses a "member" in the sense of RFC 4627. # Returns the parsed values and any trailing tokens. def pairparse(ts) (typ, _, k), ts = ts[0], ts[1..-1] if typ != :str raise Error, "unexpected #{k.inspect}" end ts = eat(':', ts) v, ts = valparse(ts) [k, v, ts] end # Parses an "array" in the sense of RFC 4627. # Returns the parsed value and any trailing tokens. def arrparse(ts) ts = eat('[', ts) arr = [] if ts[0][0] == ']' return arr, ts[1..-1] end v, ts = valparse(ts) arr << v if ts[0][0] == ']' return arr, ts[1..-1] end loop do ts = eat(',', ts) v, ts = valparse(ts) arr << v if ts[0][0] == ']' return arr, ts[1..-1] end end end def eat(typ, ts) if ts[0][0] != typ raise Error, "expected #{typ} (got #{ts[0].inspect})" end ts[1..-1] end # Sans s and returns a list of json tokens, # excluding white space (as defined in RFC 4627). def lex(s) ts = [] while s.length > 0 typ, lexeme, val = tok(s) if typ == nil raise Error, "invalid character at #{s[0,10].inspect}" end if typ != :space ts << [typ, lexeme, val] end s = s[lexeme.length..-1] end ts end # Scans the first token in s and # returns a 3-element list, or nil # if no such token exists. # # The first list element is one of # '{', '}', ':', ',', '[', ']', # :val, :str, and :space. # # The second element is the lexeme. # # The third element is the value of the # token for :val and :str, otherwise # it is the lexeme. def tok(s) case s[0] when ?{ then ['{', s[0,1], s[0,1]] when ?} then ['}', s[0,1], s[0,1]] when ?: then [':', s[0,1], s[0,1]] when ?, then [',', s[0,1], s[0,1]] when ?[ then ['[', s[0,1], s[0,1]] when ?] then [']', s[0,1], s[0,1]] when ?n then nulltok(s) when ?t then truetok(s) when ?f then falsetok(s) when ?" then strtok(s) when Spc then [:space, s[0,1], s[0,1]] when ?\t then [:space, s[0,1], s[0,1]] when ?\n then [:space, s[0,1], s[0,1]] when ?\r then [:space, s[0,1], s[0,1]] else numtok(s) end end def nulltok(s); s[0,4] == 'null' && [:val, 'null', nil] end def truetok(s); s[0,4] == 'true' && [:val, 'true', true] end def falsetok(s); s[0,5] == 'false' && [:val, 'false', false] end def numtok(s) m = /-?([1-9][0-9]+|[0-9])([.][0-9]+)?([eE][+-]?[0-9]+)?/.match(s) if m && m.begin(0) == 0 if m[3] && !m[2] [:val, m[0], Integer(m[1])*(10**Integer(m[3][1..-1]))] elsif m[2] [:val, m[0], Float(m[0])] else [:val, m[0], Integer(m[0])] end end end def strtok(s) m = /"([^"\\]|\\["\/\\bfnrt]|\\u[0-9a-fA-F]{4})*"/.match(s) if ! m raise Error, "invalid string literal at #{abbrev(s)}" end [:str, m[0], unquote(m[0])] end def abbrev(s) t = s[0,10] p = t['`'] t = t[0,p] if p t = t + '...' if t.length < s.length '`' + t + '`' end # Converts a quoted json string literal q into a UTF-8-encoded string. # The rules are different than for Ruby, so we cannot use eval. # Unquote will raise an error if q contains control characters. def unquote(q) q = q[1...-1] a = q.dup # allocate a big enough string r, w = 0, 0 while r < q.length c = q[r] case true when c == ?\\ r += 1 if r >= q.length raise Error, "string literal ends with a \"\\\": \"#{q}\"" end case q[r] when ?",?\\,?/,?' a[w] = q[r] r += 1 w += 1 when ?b,?f,?n,?r,?t a[w] = Unesc[q[r]] r += 1 w += 1 when ?u r += 1 uchar = begin hexdec4(q[r,4]) rescue RuntimeError => e raise Error, "invalid escape sequence \\u#{q[r,4]}: #{e}" end r += 4 if surrogate? uchar if q.length >= r+6 uchar1 = hexdec4(q[r+2,4]) uchar = subst(uchar, uchar1) if uchar != Ucharerr # A valid pair; consume. r += 6 end end end w += ucharenc(a, w, uchar) else raise Error, "invalid escape char #{q[r]} in \"#{q}\"" end when c == ?", c < Spc raise Error, "invalid character in string literal \"#{q}\"" else # Copy anything else byte-for-byte. # Valid UTF-8 will remain valid UTF-8. # Invalid UTF-8 will remain invalid UTF-8. a[w] = c r += 1 w += 1 end end a[0,w] end # Encodes unicode character u as UTF-8 # bytes in string a at position i. # Returns the number of bytes written. def ucharenc(a, i, u) case true when u <= Uchar1max a[i] = (u & 0xff).chr 1 when u <= Uchar2max a[i+0] = (Utag2 | ((u>>6)&0xff)).chr a[i+1] = (Utagx | (u&Umaskx)).chr 2 when u <= Uchar3max a[i+0] = (Utag3 | ((u>>12)&0xff)).chr a[i+1] = (Utagx | ((u>>6)&Umaskx)).chr a[i+2] = (Utagx | (u&Umaskx)).chr 3 else a[i+0] = (Utag4 | ((u>>18)&0xff)).chr a[i+1] = (Utagx | ((u>>12)&Umaskx)).chr a[i+2] = (Utagx | ((u>>6)&Umaskx)).chr a[i+3] = (Utagx | (u&Umaskx)).chr 4 end end def hexdec4(s) if s.length != 4 raise Error, 'short' end (nibble(s[0])<<12) | (nibble(s[1])<<8) | (nibble(s[2])<<4) | nibble(s[3]) end def subst(u1, u2) if Usurr1 <= u1 && u1 < Usurr2 && Usurr2 <= u2 && u2 < Usurr3 return ((u1-Usurr1)<<10) | (u2-Usurr2) + Usurrself end return Ucharerr end def unsubst(u) if u < Usurrself || u > Umax || surrogate?(u) return Ucharerr, Ucharerr end u -= Usurrself [Usurr1 + ((u>>10)&0x3ff), Usurr2 + (u&0x3ff)] end def surrogate?(u) Usurr1 <= u && u < Usurr3 end def nibble(c) case true when ?0 <= c && c <= ?9 then c.ord - ?0.ord when ?a <= c && c <= ?z then c.ord - ?a.ord + 10 when ?A <= c && c <= ?Z then c.ord - ?A.ord + 10 else raise Error, "invalid hex code #{c}" end end # Encodes x into a json text. It may contain only # Array, Hash, String, Numeric, true, false, nil. # (Note, this list excludes Symbol.) # X itself must be an Array or a Hash. # No other value can be encoded, and an error will # be raised if x contains any other value, such as # Nan, Infinity, Symbol, and Proc, or if a Hash key # is not a String. # Strings contained in x must be valid UTF-8. def encode(x) case x when Hash then objenc(x) when Array then arrenc(x) else raise Error, 'root value must be an Array or a Hash' end end def valenc(x) case x when Hash then objenc(x) when Array then arrenc(x) when String then strenc(x) when Numeric then numenc(x) when true then "true" when false then "false" when nil then "null" else raise Error, "cannot encode #{x.class}: #{x.inspect}" end end def objenc(x) '{' + x.map{|k,v| keyenc(k) + ':' + valenc(v)}.join(',') + '}' end def arrenc(a) '[' + a.map{|x| valenc(x)}.join(',') + ']' end def keyenc(k) case k when String then strenc(k) else raise Error, "Hash key is not a string: #{k.inspect}" end end def strenc(s) t = StringIO.new t.putc(?") r = 0 while r < s.length case s[r] when ?" then t.print('\\"') when ?\\ then t.print('\\\\') when ?\b then t.print('\\b') when ?\f then t.print('\\f') when ?\n then t.print('\\n') when ?\r then t.print('\\r') when ?\t then t.print('\\t') else c = s[r] case true when Spc <= c && c <= ?~ t.putc(c) when true u, size = uchardec(s, r) r += size - 1 # we add one more at the bottom of the loop if u < 0x10000 t.print('\\u') hexenc4(t, u) else u1, u2 = unsubst(u) t.print('\\u') hexenc4(t, u1) t.print('\\u') hexenc4(t, u2) end else # invalid byte; skip it end end r += 1 end t.putc(?") t.string end def hexenc4(t, u) t.putc(Hex[(u>>12)&0xf]) t.putc(Hex[(u>>8)&0xf]) t.putc(Hex[(u>>4)&0xf]) t.putc(Hex[u&0xf]) end def numenc(x) if x.nan? || x.infinite? return 'null' end rescue nil "#{x}" end # Decodes unicode character u from UTF-8 # bytes in string s at position i. # Returns u and the number of bytes read. def uchardec(s, i) n = s.length - i return [Ucharerr, 1] if n < 1 c0 = s[i].ord # 1-byte, 7-bit sequence? if c0 < Utagx return [c0, 1] end # unexpected continuation byte? return [Ucharerr, 1] if c0 < Utag2 # need continuation byte return [Ucharerr, 1] if n < 2 c1 = s[i+1].ord return [Ucharerr, 1] if c1 < Utagx || Utag2 <= c1 # 2-byte, 11-bit sequence? if c0 < Utag3 u = (c0&Umask2)<<6 | (c1&Umaskx) return [Ucharerr, 1] if u <= Uchar1max return [u, 2] end # need second continuation byte return [Ucharerr, 1] if n < 3 c2 = s[i+2].ord return [Ucharerr, 1] if c2 < Utagx || Utag2 <= c2 # 3-byte, 16-bit sequence? if c0 < Utag4 u = (c0&Umask3)<<12 | (c1&Umaskx)<<6 | (c2&Umaskx) return [Ucharerr, 1] if u <= Uchar2max return [u, 3] end # need third continuation byte return [Ucharerr, 1] if n < 4 c3 = s[i+3].ord return [Ucharerr, 1] if c3 < Utagx || Utag2 <= c3 # 4-byte, 21-bit sequence? if c0 < Utag5 u = (c0&Umask4)<<18 | (c1&Umaskx)<<12 | (c2&Umaskx)<<6 | (c3&Umaskx) return [Ucharerr, 1] if u <= Uchar3max return [u, 4] end return [Ucharerr, 1] end class Error < ::StandardError end Utagx = 0x80 # 1000 0000 Utag2 = 0xc0 # 1100 0000 Utag3 = 0xe0 # 1110 0000 Utag4 = 0xf0 # 1111 0000 Utag5 = 0xF8 # 1111 1000 Umaskx = 0x3f # 0011 1111 Umask2 = 0x1f # 0001 1111 Umask3 = 0x0f # 0000 1111 Umask4 = 0x07 # 0000 0111 Uchar1max = (1<<7) - 1 Uchar2max = (1<<11) - 1 Uchar3max = (1<<16) - 1 Ucharerr = 0xFFFD # unicode "replacement char" Usurrself = 0x10000 Usurr1 = 0xd800 Usurr2 = 0xdc00 Usurr3 = 0xe000 Umax = 0x10ffff Spc = ' '[0] Unesc = {?b=>?\b, ?f=>?\f, ?n=>?\n, ?r=>?\r, ?t=>?\t} Hex = '0123456789abcdef' end sinatra-3.0.5/sinatra-contrib/spec/quiet_logger_spec.rb000066400000000000000000000015341434717561400232550ustar00rootroot00000000000000require 'spec_helper' require 'sinatra/quiet_logger' require 'logger' RSpec.describe Sinatra::QuietLogger do it 'logs just paths not excluded' do log = StringIO.new logger = Logger.new(log) mock_app do use Rack::CommonLogger, logger set :quiet_logger_prefixes, %w(quiet asset) register Sinatra::QuietLogger get('/log') { 'in log' } get('/quiet') { 'not in log' } end get('/log') get('/quiet') str = log.string expect(str).to include('GET /log') expect(str).to_not include('GET /quiet') end it 'warns about not setting quiet_logger_prefixes' do expect { mock_app do register Sinatra::QuietLogger end }.to output("You need to specify the paths you wish to exclude from logging via `set :quiet_logger_prefixes, %w(images css fonts)`\n").to_stderr end end sinatra-3.0.5/sinatra-contrib/spec/reloader/000077500000000000000000000000001434717561400210225ustar00rootroot00000000000000sinatra-3.0.5/sinatra-contrib/spec/reloader/app.rb.erb000066400000000000000000000012701434717561400226760ustar00rootroot00000000000000class <%= name %> < <%= parent %> <% if enable_reloader %> register Sinatra::Reloader enable :reloader <% end %> <% unless inline_templates.nil? %> enable :inline_templates <% end %> <% extensions.each do |extension| %> register <%= extension %> <% end %> <% middlewares.each do |middleware| %> use <%= middleware %> <% end %> <% filters.each do |filter| %> <%= filter %> <% end %> <% errors.each do |number, code| %> error <%= number %> do <%= code %> end <% end %> <% routes.each do |route| %> <%= route %> <% end %> end <% unless inline_templates.nil? %> __END__ <% inline_templates.each_pair do |name, content| %> @@<%= name %> <%= content %> <% end %> <% end %> sinatra-3.0.5/sinatra-contrib/spec/reloader_spec.rb000066400000000000000000000365501434717561400223720ustar00rootroot00000000000000require 'spec_helper' require 'fileutils' RSpec.describe Sinatra::Reloader do # Returns the temporary directory. def tmp_dir File.expand_path('../tmp', __dir__) end # Returns the path of the Sinatra application file created by # +setup_example_app+. def app_file_path File.join(tmp_dir, "example_app_#{$example_app_counter}.rb") end # Returns the name of the Sinatra application created by # +setup_example_app+: 'ExampleApp1' for the first application, # 'ExampleApp2' fo the second one, and so on... def app_name "ExampleApp#{$example_app_counter}" end # Returns the (constant of the) Sinatra application created by # +setup_example_app+. def app_const Module.const_get(app_name) end # Writes a file with a Sinatra application using the template # located at specs/reloader/app.rb.erb. It expects an # +options+ hash, with an array of strings containing the # application's routes (+:routes+ key), a hash with the inline # template's names as keys and the bodys as values # (+:inline_templates+ key) and an optional application name # (+:name+) otherwise +app_name+ is used. # # It ensures to change the written file's mtime when it already # exists. def write_app_file(options={}) options[:routes] ||= ['get("/foo") { erb :foo }'] options[:inline_templates] ||= nil options[:extensions] ||= [] options[:middlewares] ||= [] options[:filters] ||= [] options[:errors] ||= {} options[:name] ||= app_name options[:enable_reloader] = true unless options[:enable_reloader] === false options[:parent] ||= 'Sinatra::Base' update_file(app_file_path) do |f| template_path = File.expand_path('reloader/app.rb.erb', __dir__) template = Tilt.new(template_path, nil, :trim => '<>') f.write template.render(Object.new, options) end end alias update_app_file write_app_file # It calls File.open(path, 'w', &block) all the times # needed to change the file's mtime. def update_file(path, &block) original_mtime = File.exist?(path) ? File.mtime(path) : Time.at(0) new_time = original_mtime + 1 File.open(path, 'w', &block) File.utime(new_time, new_time, path) end # Writes a Sinatra application to a file, requires the file, sets # the new application as the one being tested and enables the # reloader. def setup_example_app(options={}) $example_app_counter ||= 0 $example_app_counter += 1 FileUtils.mkdir_p(tmp_dir) write_app_file(options) $LOADED_FEATURES.delete app_file_path require app_file_path self.app = app_const app_const.enable :reloader end after(:all) { FileUtils.rm_rf(tmp_dir) } describe "default route reloading mechanism" do before(:each) do setup_example_app(:routes => ['get("/foo") { "foo" }']) end it "doesn't mess up the application" do expect(get('/foo').body).to eq('foo') end it "knows when a route has been modified" do update_app_file(:routes => ['get("/foo") { "bar" }']) expect(get('/foo').body).to eq('bar') end it "knows when a route has been added" do update_app_file( :routes => ['get("/foo") { "foo" }', 'get("/bar") { "bar" }'] ) expect(get('/foo').body).to eq('foo') expect(get('/bar').body).to eq('bar') end it "knows when a route has been removed" do update_app_file(:routes => ['get("/bar") { "bar" }']) expect(get('/foo').status).to eq(404) end it "doesn't try to reload a removed file" do update_app_file(:routes => ['get("/foo") { "i shall not be reloaded" }']) FileUtils.rm app_file_path expect(get('/foo').body.strip).to eq('foo') end end describe "default inline templates reloading mechanism" do before(:each) do setup_example_app( :routes => ['get("/foo") { erb :foo }'], :inline_templates => { :foo => 'foo' } ) end it "doesn't mess up the application" do expect(get('/foo').body.strip).to eq('foo') end it "reloads inline templates in the app file" do update_app_file( :routes => ['get("/foo") { erb :foo }'], :inline_templates => { :foo => 'bar' } ) expect(get('/foo').body.strip).to eq('bar') end it "reloads inline templates in other file" do setup_example_app(:routes => ['get("/foo") { erb :foo }']) template_file_path = File.join(tmp_dir, 'templates.rb') File.open(template_file_path, 'w') do |f| f.write "__END__\n\n@@foo\nfoo" end require template_file_path app_const.inline_templates= template_file_path expect(get('/foo').body.strip).to eq('foo') update_file(template_file_path) do |f| f.write "__END__\n\n@@foo\nbar" end expect(get('/foo').body.strip).to eq('bar') end end describe "default middleware reloading mechanism" do it "knows when a middleware has been added" do setup_example_app(:routes => ['get("/foo") { "foo" }']) update_app_file( :routes => ['get("/foo") { "foo" }'], :middlewares => [Rack::Head] ) get('/foo') # ...to perform the reload expect(app_const.middleware).not_to be_empty end it "knows when a middleware has been removed" do setup_example_app( :routes => ['get("/foo") { "foo" }'], :middlewares => [Rack::Head] ) update_app_file(:routes => ['get("/foo") { "foo" }']) get('/foo') # ...to perform the reload expect(app_const.middleware).to be_empty end end describe "default filter reloading mechanism" do it "knows when a before filter has been added" do setup_example_app(:routes => ['get("/foo") { "foo" }']) expect { update_app_file( :routes => ['get("/foo") { "foo" }'], :filters => ['before { @hi = "hi" }'] ) get('/foo') # ...to perform the reload }.to change { app_const.filters[:before].size }.by(1) end it "knows when an after filter has been added" do setup_example_app(:routes => ['get("/foo") { "foo" }']) expect { update_app_file( :routes => ['get("/foo") { "foo" }'], :filters => ['after { @bye = "bye" }'] ) get('/foo') # ...to perform the reload }.to change { app_const.filters[:after].size }.by(1) end it "knows when a before filter has been removed" do setup_example_app( :routes => ['get("/foo") { "foo" }'], :filters => ['before { @hi = "hi" }'] ) expect { update_app_file(:routes => ['get("/foo") { "foo" }']) get('/foo') # ...to perform the reload }.to change { app_const.filters[:before].size }.by(-1) end it "knows when an after filter has been removed" do setup_example_app( :routes => ['get("/foo") { "foo" }'], :filters => ['after { @bye = "bye" }'] ) expect { update_app_file(:routes => ['get("/foo") { "foo" }']) get('/foo') # ...to perform the reload }.to change { app_const.filters[:after].size }.by(-1) end end describe "error reloading" do before do setup_example_app( :routes => ['get("/secret") { 403 }'], :errors => { 403 => "'Access forbiden'" } ) end it "doesn't mess up the application" do expect(get('/secret')).to be_client_error expect(get('/secret').body.strip).to eq('Access forbiden') end it "knows when a error has been added" do update_app_file(:errors => { 404 => "'Nowhere'" }) expect(get('/nowhere')).to be_not_found expect(get('/nowhere').body).to eq('Nowhere') end it "knows when a error has been removed" do update_app_file(:routes => ['get("/secret") { 403 }']) expect(get('/secret')).to be_client_error expect(get('/secret').body).not_to eq('Access forbiden') end it "knows when a error has been modified" do update_app_file( :routes => ['get("/secret") { 403 }'], :errors => { 403 => "'What are you doing here?'" } ) expect(get('/secret')).to be_client_error expect(get('/secret').body).to eq('What are you doing here?') end end describe "extension reloading" do it "doesn't duplicate routes with every reload" do module ::RouteExtension def self.registered(klass) klass.get('/bar') { 'bar' } end end setup_example_app( :routes => ['get("/foo") { "foo" }'], :extensions => ['RouteExtension'] ) expect { update_app_file( :routes => ['get("/foo") { "foo" }'], :extensions => ['RouteExtension'] ) get('/foo') # ...to perform the reload }.to_not change { app_const.routes['GET'].size } end it "doesn't duplicate middleware with every reload" do module ::MiddlewareExtension def self.registered(klass) klass.use Rack::Head end end setup_example_app( :routes => ['get("/foo") { "foo" }'], :extensions => ['MiddlewareExtension'] ) expect { update_app_file( :routes => ['get("/foo") { "foo" }'], :extensions => ['MiddlewareExtension'] ) get('/foo') # ...to perform the reload }.to_not change { app_const.middleware.size } end it "doesn't duplicate before filters with every reload" do module ::BeforeFilterExtension def self.registered(klass) klass.before { @hi = 'hi' } end end setup_example_app( :routes => ['get("/foo") { "foo" }'], :extensions => ['BeforeFilterExtension'] ) expect { update_app_file( :routes => ['get("/foo") { "foo" }'], :extensions => ['BeforeFilterExtension'] ) get('/foo') # ...to perform the reload }.to_not change { app_const.filters[:before].size } end it "doesn't duplicate after filters with every reload" do module ::AfterFilterExtension def self.registered(klass) klass.after { @bye = 'bye' } end end setup_example_app( :routes => ['get("/foo") { "foo" }'], :extensions => ['AfterFilterExtension'] ) expect { update_app_file( :routes => ['get("/foo") { "foo" }'], :extensions => ['AfterFilterExtension'] ) get('/foo') # ...to perform the reload }.to_not change { app_const.filters[:after].size } end end describe ".dont_reload" do before(:each) do setup_example_app( :routes => ['get("/foo") { erb :foo }'], :inline_templates => { :foo => 'foo' } ) end it "allows to specify a file to stop from being reloaded" do app_const.dont_reload app_file_path update_app_file(:routes => ['get("/foo") { "bar" }']) expect(get('/foo').body.strip).to eq('foo') end it "allows to specify a glob to stop matching files from being reloaded" do app_const.dont_reload '**/*.rb' update_app_file(:routes => ['get("/foo") { "bar" }']) expect(get('/foo').body.strip).to eq('foo') end it "doesn't interfere with other application's reloading policy" do app_const.dont_reload '**/*.rb' setup_example_app(:routes => ['get("/foo") { "foo" }']) update_app_file(:routes => ['get("/foo") { "bar" }']) expect(get('/foo').body.strip).to eq('bar') end end describe ".also_reload" do before(:each) do setup_example_app(:routes => ['get("/foo") { Foo.foo }']) @foo_path = File.join(tmp_dir, 'foo.rb') update_file(@foo_path) do |f| f.write 'class Foo; def self.foo() "foo" end end' end $LOADED_FEATURES.delete @foo_path require @foo_path app_const.also_reload @foo_path end it "allows to specify a file to be reloaded" do expect(get('/foo').body.strip).to eq('foo') update_file(@foo_path) do |f| f.write 'class Foo; def self.foo() "bar" end end' end expect(get('/foo').body.strip).to eq('bar') end it "allows to specify glob to reaload matching files" do expect(get('/foo').body.strip).to eq('foo') update_file(@foo_path) do |f| f.write 'class Foo; def self.foo() "bar" end end' end expect(get('/foo').body.strip).to eq('bar') end it "doesn't try to reload a removed file" do update_file(@foo_path) do |f| f.write 'class Foo; def self.foo() "bar" end end' end FileUtils.rm @foo_path expect(get('/foo').body.strip).to eq('foo') end it "doesn't interfere with other application's reloading policy" do app_const.also_reload '**/*.rb' setup_example_app(:routes => ['get("/foo") { Foo.foo }']) expect(get('/foo').body.strip).to eq('foo') update_file(@foo_path) do |f| f.write 'class Foo; def self.foo() "bar" end end' end expect(get('/foo').body.strip).to eq('foo') end end describe ".after_reload" do before(:each) do $reloaded = nil setup_example_app(:routes => ['get("/foo") { Foo.foo }']) @foo_path = File.join(tmp_dir, 'foo.rb') update_file(@foo_path) do |f| f.write 'class Foo; def self.foo() "foo" end end' end $LOADED_FEATURES.delete @foo_path require @foo_path app_const.also_reload @foo_path end it "allows block execution after reloading files" do app_const.after_reload do |files| $reloaded = files end expect($reloaded).to eq(nil) expect(get('/foo').body.strip).to eq('foo') expect($reloaded).to eq(nil) # after_reload was not called update_file(@foo_path) do |f| f.write 'class Foo; def self.foo() "bar" end end' end expect(get("/foo").body.strip).to eq("bar") # Makes the reload happen expect($reloaded.size).to eq(1) expect(File.basename($reloaded[0])).to eq(File.basename(@foo_path)) end it "does not break block without input param" do app_const.after_reload do $reloaded = "worked without param" end expect($reloaded).to eq(nil) expect(get('/foo').body.strip).to eq('foo') expect($reloaded).to eq(nil) # after_reload was not called update_file(@foo_path) do |f| f.write 'class Foo; def self.foo() "bar" end end' end expect { get("/foo") }.to_not raise_error # Makes the reload happen expect($reloaded).to eq("worked without param") end it "handles lambdas with arity 0" do user_proc = -> { $reloaded = "lambda?=true arity=0" } expect { user_proc.call(1) }.to raise_error(ArgumentError) # What we avoid app_const.after_reload(&user_proc) expect($reloaded).to eq(nil) expect(get('/foo').body.strip).to eq('foo') expect($reloaded).to eq(nil) # after_reload was not called update_file(@foo_path) do |f| f.write 'class Foo; def self.foo() "bar" end end' end expect { get("/foo") }.to_not raise_error # Makes the reload happen expect($reloaded).to eq("lambda?=true arity=0") end end it "automatically registers the reloader in the subclasses" do class ::Parent < Sinatra::Base register Sinatra::Reloader enable :reloader end setup_example_app( :routes => ['get("/foo") { "foo" }'], :enable_reloader => false, :parent => 'Parent' ) update_app_file( :routes => ['get("/foo") { "bar" }'], :enable_reloader => false, :parent => 'Parent' ) expect(get('/foo').body).to eq('bar') end end sinatra-3.0.5/sinatra-contrib/spec/required_params_spec.rb000066400000000000000000000043131434717561400237500ustar00rootroot00000000000000require_relative 'spec_helper' RSpec.describe Sinatra::RequiredParams do context "#required_params" do context "simple keys" do before do mock_app do helpers Sinatra::RequiredParams get('/') { required_params(:p1, :p2) } end end it 'return 400 if required params do not exist' do get('/') expect(last_response.status).to eq(400) end it 'return 400 if required params do not exist partially' do get('/', :p1 => 1) expect(last_response.status).to eq(400) end it 'return 200 if required params exist' do get('/', :p1 => 1, :p2 => 2) expect(last_response.status).to eq(200) end it 'return 200 if required params exist with array' do get('/', :p1 => 1, :p2 => [31, 32, 33]) expect(last_response.status).to eq(200) end end context "hash keys" do before do mock_app do helpers Sinatra::RequiredParams get('/') { required_params(:p1, :p2 => :p21) } end end it 'return 400 if required params do not exist' do get('/') expect(last_response.status).to eq(400) end it 'return 200 if required params exist' do get('/', :p1 => 1, :p2 => {:p21 => 21}) expect(last_response.status).to eq(200) end it 'return 400 if p2 is not a hash' do get('/', :p1 => 1, :p2 => 2) expect(last_response.status).to eq(400) end end context "complex keys" do before do mock_app do helpers Sinatra::RequiredParams get('/') { required_params(:p1 => [:p11, {:p12 => :p121, :p122 => [:p123, {:p124 => :p1241}]}]) } end end it 'return 400 if required params do not exist' do get('/') expect(last_response.status).to eq(400) end it 'return 200 if required params exist' do get('/', :p1 => {:p11 => 11, :p12 => {:p121 => 121}, :p122 => {:p123 => 123, :p124 => {:p1241 => 1241}}}) expect(last_response.status).to eq(200) end end end context "#_required_params" do it "is invisible" do expect { _required_params }.to raise_error(NameError) end end end sinatra-3.0.5/sinatra-contrib/spec/respond_with/000077500000000000000000000000001434717561400217325ustar00rootroot00000000000000sinatra-3.0.5/sinatra-contrib/spec/respond_with/bar.erb000066400000000000000000000000121434717561400231610ustar00rootroot00000000000000guten Tag!sinatra-3.0.5/sinatra-contrib/spec/respond_with/bar.json.erb000066400000000000000000000000051434717561400241330ustar00rootroot00000000000000json!sinatra-3.0.5/sinatra-contrib/spec/respond_with/baz.yajl000066400000000000000000000000171434717561400233650ustar00rootroot00000000000000json = "yajl!" sinatra-3.0.5/sinatra-contrib/spec/respond_with/foo.html.erb000066400000000000000000000000221434717561400241440ustar00rootroot00000000000000Hello <%= name %>!sinatra-3.0.5/sinatra-contrib/spec/respond_with_spec.rb000066400000000000000000000207341434717561400232770ustar00rootroot00000000000000require 'multi_json' require 'spec_helper' require 'okjson' RSpec.describe Sinatra::RespondWith do def respond_app(&block) mock_app do set :app_file, __FILE__ set :views, root + '/respond_with' register Sinatra::RespondWith class_eval(&block) end end def respond_to(*args, &block) respond_app { get('/') { respond_to(*args, &block) } } end def respond_with(*args, &block) respond_app { get('/') { respond_with(*args, &block) } } end def req(*types) path = types.shift if types.first.is_a?(String) && types.first.start_with?('/') accept = types.map { |t| Sinatra::Base.mime_type(t).to_s }.join ',' get (path || '/'), {}, 'HTTP_ACCEPT' => accept end describe "Helpers#respond_to" do it 'allows defining handlers by file extensions' do respond_to do |format| format.html { "html!" } format.json { "json!" } end expect(req(:html).body).to eq("html!") expect(req(:json).body).to eq("json!") end it 'respects quality' do respond_to do |format| format.html { "html!" } format.json { "json!" } end expect(req("text/html;q=0.7, application/json;q=0.3").body).to eq("html!") expect(req("text/html;q=0.3, application/json;q=0.7").body).to eq("json!") end it 'allows using mime types' do respond_to do |format| format.on('text/html') { "html!" } format.json { "json!" } end expect(req(:html).body).to eq("html!") end it 'allows using wildcards in format matchers' do respond_to do |format| format.on('text/*') { "text!" } format.json { "json!" } end expect(req(:html).body).to eq("text!") end it 'allows using catch all wildcards in format matchers' do respond_to do |format| format.on('*/*') { "anything!" } format.json { "json!" } end expect(req(:html).body).to eq("anything!") end it 'prefers concret over generic' do respond_to do |format| format.on('text/*') { "text!" } format.on('*/*') { "anything!" } format.json { "json!" } end expect(req(:json).body).to eq("json!") expect(req(:html).body).to eq("text!") end it 'does not set up default handlers' do respond_to expect(req).not_to be_ok expect(status).to eq(500) expect(body).to eq("Unknown template engine") end end describe "Helpers#respond_with" do describe "matching" do it 'allows defining handlers by file extensions' do respond_with(:ignore) do |format| format.html { "html!" } format.json { "json!" } end expect(req(:html).body).to eq("html!") expect(req(:json).body).to eq("json!") end it 'respects quality' do respond_with(:ignore) do |format| format.html { "html!" } format.json { "json!" } end expect(req("text/html;q=0.7, application/json;q=0.3").body).to eq("html!") expect(req("text/html;q=0.3, application/json;q=0.7").body).to eq("json!") end it 'allows using mime types' do respond_with(:ignore) do |format| format.on('text/html') { "html!" } format.json { "json!" } end expect(req(:html).body).to eq("html!") end it 'allows using wildcards in format matchers' do respond_with(:ignore) do |format| format.on('text/*') { "text!" } format.json { "json!" } end expect(req(:html).body).to eq("text!") end it 'allows using catch all wildcards in format matchers' do respond_with(:ignore) do |format| format.on('*/*') { "anything!" } format.json { "json!" } end expect(req(:html).body).to eq("anything!") end it 'prefers concret over generic' do respond_with(:ignore) do |format| format.on('text/*') { "text!" } format.on('*/*') { "anything!" } format.json { "json!" } end expect(req(:json).body).to eq("json!") expect(req(:html).body).to eq("text!") end end describe "default behavior" do it 'converts objects to json out of the box' do respond_with 'a' => 'b' expect(OkJson.decode(req(:json).body)).to eq({'a' => 'b'}) end it 'handles multiple routes correctly' do respond_app do get('/') { respond_with 'a' => 'b' } get('/:name') { respond_with 'a' => params[:name] } end expect(OkJson.decode(req('/', :json).body)).to eq({'a' => 'b'}) expect(OkJson.decode(req('/b', :json).body)).to eq({'a' => 'b'}) expect(OkJson.decode(req('/c', :json).body)).to eq({'a' => 'c'}) end it "calls to_EXT if available" do respond_with Struct.new(:to_pdf).new("hello") expect(req(:pdf).body).to eq("hello") end it 'results in a 500 if format cannot be produced' do respond_with({}) expect(req(:html)).not_to be_ok expect(status).to eq(500) expect(body).to eq("Unknown template engine") end end describe 'templates' do it 'looks for templates with name.target.engine' do respond_with :foo, :name => 'World' expect(req(:html)).to be_ok expect(body).to eq("Hello World!") end it 'looks for templates with name.engine for specific engines' do respond_with :bar expect(req(:html)).to be_ok expect(body).to eq("guten Tag!") end it 'does not use name.engine for engines producing other formats' do respond_with :not_html expect(req(:html)).not_to be_ok expect(status).to eq(500) expect(body).to eq("Unknown template engine") end it 'falls back to #json if no template is found' do respond_with :foo, :name => 'World' expect(req(:json)).to be_ok expect(OkJson.decode(body)).to eq({'name' => 'World'}) end it 'favors templates over #json' do respond_with :bar, :name => 'World' expect(req(:json)).to be_ok expect(body).to eq('json!') end it 'falls back to to_EXT if no template is found' do object = {:name => 'World'} def object.to_pdf; "hi" end respond_with :foo, object expect(req(:pdf)).to be_ok expect(body).to eq("hi") end unless defined? JRUBY_VERSION it 'uses yajl for json' do respond_with :baz expect(req(:json)).to be_ok expect(body).to eq("\"yajl!\"") end end end describe 'customizing' do it 'allows customizing' do respond_with(:foo, :name => 'World') { |f| f.html { 'html!' }} expect(req(:html)).to be_ok expect(body).to eq("html!") end it 'falls back to default behavior if none matches' do respond_with(:foo, :name => 'World') { |f| f.json { 'json!' }} expect(req(:html)).to be_ok expect(body).to eq("Hello World!") end it 'favors generic rule over default behavior' do respond_with(:foo, :name => 'World') { |f| f.on('*/*') { 'generic!' }} expect(req(:html)).to be_ok expect(body).to eq("generic!") end end describe "inherited" do it "registers RespondWith in an inherited app" do app = Sinatra.new do set :app_file, __FILE__ set :views, root + '/respond_with' register Sinatra::RespondWith get '/a' do respond_with :json end end self.app = Sinatra.new(app) expect(req('/a', :json)).not_to be_ok end end end describe :respond_to do it 'acts as global provides condition' do respond_app do respond_to :json, :html get('/a') { 'ok' } get('/b') { 'ok' } end expect(req('/b', :xml)).not_to be_ok expect(req('/b', :html)).to be_ok end it 'still allows provides' do respond_app do respond_to :json, :html get('/a') { 'ok' } get('/b', :provides => :json) { 'ok' } end expect(req('/b', :html)).not_to be_ok expect(req('/b', :json)).to be_ok end it 'plays well with namespaces' do respond_app do register Sinatra::Namespace namespace '/a' do respond_to :json get { 'json' } end get('/b') { 'anything' } end expect(req('/a', :html)).not_to be_ok expect(req('/b', :html)).to be_ok end end end sinatra-3.0.5/sinatra-contrib/spec/spec_helper.rb000066400000000000000000000070501434717561400220450ustar00rootroot00000000000000ENV['RACK_ENV'] = 'test' require 'sinatra/contrib' # This file was generated by the `rspec --init` command. Conventionally, all # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. # The generated `.rspec` file contains `--require spec_helper` which will cause this # file to always be loaded, without a need to explicitly require it in any files. # # Given that it is always loaded, you are encouraged to keep this file as # light-weight as possible. Requiring heavyweight dependencies from this file # will add to the boot time of your test suite on EVERY test run, even for an # individual file that may not need all of that loaded. Instead, make a # separate helper file that requires this one and then use it only in the specs # that actually need it. # # The `.rspec` file also contains a few flags that are not defaults but that # users commonly want. # # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration RSpec.configure do |config| # The settings below are suggested to provide a good initial experience # with RSpec, but feel free to customize to your heart's content. config.disable_monkey_patching! # These two settings work together to allow you to limit a spec run # to individual examples or groups you care about by tagging them with # `:focus` metadata. When nothing is tagged with `:focus`, all examples # get run. config.filter_run :focus config.run_all_when_everything_filtered = true # Many RSpec users commonly either run the entire suite or an individual # file, and it's useful to allow more verbose output when running an # individual spec file. if config.files_to_run.one? # Use the documentation formatter for detailed output, # unless a formatter has already been configured # (e.g. via a command-line flag). config.default_formatter = 'doc' end # Print the 10 slowest examples and example groups at the # end of the spec run, to help surface which specs are running # particularly slow. config.profile_examples = 5 # Run specs in random order to surface order dependencies. If you find an # order dependency and want to debug it, you can fix the order by providing # the seed, which is printed after each run. # --seed 1234 config.order = :random # Seed global randomization in this process using the `--seed` CLI option. # Setting this allows you to use `--seed` to deterministically reproduce # test failures related to randomization by passing the same `--seed` value # as the one that triggered the failure. Kernel.srand config.seed # rspec-expectations config goes here. You can use an alternate # assertion/expectation library such as wrong or the stdlib/minitest # assertions if you prefer. config.expect_with :rspec do |expectations| # Enable only the newer, non-monkey-patching expect syntax. # For more details, see: # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax expectations.syntax = :expect end # rspec-mocks config goes here. You can use an alternate test double # library (such as bogus or mocha) by changing the `mock_with` option here. config.mock_with :rspec do |mocks| # Enable only the newer, non-monkey-patching expect syntax. # For more details, see: # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ mocks.syntax = :expect # Prevents you from mocking or stubbing a method that does not exist on # a real object. This is generally recommended. mocks.verify_partial_doubles = true end config.include Sinatra::TestHelpers end sinatra-3.0.5/sinatra-contrib/spec/streaming_spec.rb000066400000000000000000000223351434717561400225620ustar00rootroot00000000000000require 'spec_helper' RSpec.describe Sinatra::Streaming do def stream(&block) rack_middleware = @use out = nil mock_app do rack_middleware.each { |args| use(*args) } helpers Sinatra::Streaming get('/') { out = stream(&block) } end get('/') out end def use(*args) @use << args end before do @use = [] end context 'stream test helper' do it 'runs the given block' do ran = false stream { ran = true } expect(ran).to be true end it 'returns the stream object' do out = stream { } expect(out).to be_a(Sinatra::Helpers::Stream) end it 'fires a request against that stream' do stream { |out| out << "Hello World!" } expect(last_response).to be_ok expect(body).to eq("Hello World!") end it 'passes the stream object to the block' do passed = nil returned = stream { |out| passed = out } expect(passed).to eq(returned) end end context Sinatra::Streaming::Stream do it 'should extend the stream object' do out = stream { } expect(out).to be_a(Sinatra::Streaming::Stream) end it 'should not extend stream objects of other apps' do out = nil mock_app { get('/') { out = stream { }}} get('/') expect(out).to be_a(Sinatra::Helpers::Stream) expect(out).not_to be_a(Sinatra::Streaming::Stream) end end context 'app' do it 'is the app instance the stream was created from' do out = stream { } expect(out.app).to be_a(Sinatra::Base) end end context 'lineno' do it 'defaults to 0' do expect(stream { }.lineno).to eq(0) end it 'does not increase on write' do stream do |out| out << "many\nlines\n" expect(out.lineno).to eq(0) end end it 'is writable' do out = stream { } out.lineno = 10 expect(out.lineno).to eq(10) end end context 'pos' do it 'defaults to 0' do expect(stream { }.pos).to eq(0) end it 'increases when writing data' do stream do |out| expect(out.pos).to eq(0) out << 'hi' expect(out.pos).to eq(2) end end it 'is writable' do out = stream { } out.pos = 10 expect(out.pos).to eq(10) end it 'aliased to #tell' do out = stream { } expect(out.tell).to eq(0) out.pos = 10 expect(out.tell).to eq(10) end end context 'closed' do it 'returns false while streaming' do stream { |out| expect(out).not_to be_closed } end it 'returns true after streaming' do expect(stream {}).to be_closed end end context 'map!' do it 'applies transformations later' do stream do |out| out.map! { |s| s.upcase } out << 'ok' end expect(body).to eq("OK") end it 'is chainable' do stream do |out| out.map! { |s| s.upcase } out.map! { |s| s.reverse } out << 'ok' end expect(body).to eq("KO") end it 'works with middleware' do middleware = Class.new do def initialize(app) @app = app end def call(env) status, headers, body = @app.call(env) body.map! { |s| s.upcase } [status, headers, body] end end use middleware stream { |out| out << "ok" } expect(body).to eq("OK") end it 'modifies each value separately' do stream do |out| out.map! { |s| s.reverse } out << "ab" << "cd" end expect(body).to eq("badc") end end context 'map' do it 'works with middleware' do middleware = Class.new do def initialize(app) @app = app end def call(env) status, headers, body = @app.call(env) [status, headers, body.map(&:upcase)] end end use middleware stream { |out| out << "ok" } expect(body).to eq("OK") end it 'is chainable' do middleware = Class.new do def initialize(app) @app = app end def call(env) status, headers, body = @app.call(env) [status, headers, body.map(&:upcase).map(&:reverse)] end end use middleware stream { |out| out << "ok" } expect(body).to eq("KO") end it 'can be written as each.map' do middleware = Class.new do def initialize(app) @app = app end def call(env) status, headers, body = @app.call(env) [status, headers, body.each.map(&:upcase)] end end use middleware stream { |out| out << "ok" } expect(body).to eq("OK") end it 'does not modify the original body' do stream do |out| out.map { |s| s.reverse } out << 'ok' end expect(body).to eq('ok') end end context 'write' do it 'writes to the stream' do stream { |out| out.write 'hi' } expect(body).to eq('hi') end it 'returns the number of bytes' do stream do |out| expect(out.write('hi')).to eq(2) expect(out.write('hello')).to eq(5) end end it 'accepts non-string objects' do stream do |out| expect(out.write(12)).to eq(2) end end it 'should be aliased to syswrite' do stream { |out| expect(out.syswrite('hi')).to eq(2) } expect(body).to eq('hi') end it 'should be aliased to write_nonblock' do stream { |out| expect(out.write_nonblock('hi')).to eq(2) } expect(body).to eq('hi') end end context 'print' do it 'writes to the stream' do stream { |out| out.print('hi') } expect(body).to eq('hi') end it 'accepts multiple arguments' do stream { |out| out.print(1, 2, 3, 4) } expect(body).to eq('1234') end it 'returns nil' do stream { |out| expect(out.print('hi')).to be_nil } end end context 'printf' do it 'writes to the stream' do stream { |out| out.printf('hi') } expect(body).to eq('hi') end it 'interpolates the format string' do stream { |out| out.printf("%s: %d", "answer", 42) } expect(body).to eq('answer: 42') end it 'returns nil' do stream { |out| expect(out.printf('hi')).to be_nil } end end context 'putc' do it 'writes the first character of a string' do stream { |out| out.putc('hi') } expect(body).to eq('h') end it 'writes the character corresponding to an integer' do stream { |out| out.putc(42) } expect(body).to eq('*') end it 'returns nil' do stream { |out| expect(out.putc('hi')).to be_nil } end end context 'puts' do it 'writes to the stream' do stream { |out| out.puts('hi') } expect(body).to eq("hi\n") end it 'accepts multiple arguments' do stream { |out| out.puts(1, 2, 3, 4) } expect(body).to eq("1\n2\n3\n4\n") end it 'returns nil' do stream { |out| expect(out.puts('hi')).to be_nil } end end context 'close' do it 'sets #closed? to true' do stream do |out| out.close expect(out).to be_closed end end it 'sets #closed_write? to true' do stream do |out| expect(out).not_to be_closed_write out.close expect(out).to be_closed_write end end it 'fires callbacks' do stream do |out| fired = false out.callback { fired = true } out.close expect(fired).to be true end end it 'prevents from further writing' do stream do |out| out.close expect { out << 'hi' }.to raise_error(IOError, 'not opened for writing') end end end context 'close_read' do it 'raises the appropriate exception' do expect { stream { |out| out.close_read }}. to raise_error(IOError, "closing non-duplex IO for reading") end end context 'closed_read?' do it('returns true') { stream { |out| expect(out).to be_closed_read }} end context 'rewind' do it 'resets pos' do stream do |out| out << 'hi' out.rewind expect(out.pos).to eq(0) end end it 'resets lineno' do stream do |out| out.lineno = 10 out.rewind expect(out.lineno).to eq(0) end end end raises = %w[ bytes eof? eof getbyte getc gets read read_nonblock readbyte readchar readline readlines readpartial sysread ungetbyte ungetc ] enum = %w[chars each_line each_byte each_char lines] dummies = %w[flush fsync internal_encoding pid] raises.each do |method| context method do it 'raises the appropriate exception' do expect { stream { |out| out.public_send(method) }}. to raise_error(IOError, "not opened for reading") end end end enum.each do |method| context method do it 'creates an Enumerator' do stream { |out| expect(out.public_send(method)).to be_a(Enumerator) } end it 'calling each raises the appropriate exception' do expect { stream { |out| out.public_send(method).each { }}}. to raise_error(IOError, "not opened for reading") end end end dummies.each do |method| context method do it 'returns nil' do stream { |out| expect(out.public_send(method)).to be_nil } end end end end sinatra-3.0.5/sinatra.gemspec000066400000000000000000000035771434717561400162160ustar00rootroot00000000000000# frozen_string_literal: true version = File.read(File.expand_path('VERSION', __dir__)).strip Gem::Specification.new 'sinatra', version do |s| s.description = 'Sinatra is a DSL for quickly creating web applications in Ruby with minimal effort.' s.summary = 'Classy web-development dressed in a DSL' s.authors = ['Blake Mizerany', 'Ryan Tomayko', 'Simon Rozet', 'Konstantin Haase'] s.email = 'sinatrarb@googlegroups.com' s.homepage = 'http://sinatrarb.com/' s.license = 'MIT' s.files = Dir['README*.md', 'lib/**/*', 'examples/*'] + [ '.yardopts', 'AUTHORS.md', 'CHANGELOG.md', 'CONTRIBUTING.md', 'Gemfile', 'LICENSE', 'MAINTENANCE.md', 'Rakefile', 'SECURITY.md', 'sinatra.gemspec', 'VERSION' ] s.extra_rdoc_files = %w[README.md LICENSE] s.rdoc_options = %w[--line-numbers --title Sinatra --main README.rdoc --encoding=UTF-8] unless s.respond_to?(:metadata) raise <<-WARN RubyGems 2.0 or newer is required to protect against public gem pushes. You can update your rubygems version by running: gem install rubygems-update update_rubygems: gem update --system WARN end s.metadata = { 'source_code_uri' => 'https://github.com/sinatra/sinatra', 'changelog_uri' => 'https://github.com/sinatra/sinatra/blob/master/CHANGELOG.md', 'homepage_uri' => 'http://sinatrarb.com/', 'bug_tracker_uri' => 'https://github.com/sinatra/sinatra/issues', 'mailing_list_uri' => 'http://groups.google.com/group/sinatrarb', 'documentation_uri' => 'https://www.rubydoc.info/gems/sinatra' } s.required_ruby_version = '>= 2.6.0' s.add_dependency 'mustermann', '~> 3.0' s.add_dependency 'rack', '~> 2.2', '>= 2.2.4' s.add_dependency 'rack-protection', version s.add_dependency 'tilt', '~> 2.0' s.add_development_dependency 'rack-test', '~> 2' end sinatra-3.0.5/test/000077500000000000000000000000001434717561400141535ustar00rootroot00000000000000sinatra-3.0.5/test/asciidoctor_test.rb000066400000000000000000000036051434717561400200460ustar00rootroot00000000000000require File.expand_path('helper', __dir__) begin require 'asciidoctor' class AsciidoctorTest < Minitest::Test def asciidoc_app(&block) mock_app do set :views, __dir__ + '/views' get('/', &block) end get '/' end it 'renders inline AsciiDoc strings' do asciidoc_app { asciidoc '== Hiya' } assert ok? assert_match %r{Hiya}, body end it 'uses the correct engine' do engine = Tilt::AsciidoctorTemplate assert_equal engine, Tilt[:ad] assert_equal engine, Tilt[:adoc] assert_equal engine, Tilt[:asciidoc] end it 'renders .asciidoc files in views path' do asciidoc_app { asciidoc :hello } assert ok? assert_match %r{Hello from AsciiDoc}, body end it 'raises error if template not found' do mock_app { get('/') { asciidoc :no_such_template } } assert_raises(Errno::ENOENT) { get('/') } end it 'renders with inline layouts' do mock_app do layout { 'THIS. IS. #{yield.upcase}!' } get('/') { asciidoc 'Sparta', :layout_engine => :str } end get '/' assert ok? assert_include body, 'THIS. IS.' assert_include body, '

SPARTA

' end it 'renders with file layouts' do asciidoc_app do asciidoc 'Hello World', :layout => :layout2, :layout_engine => :erb end assert ok? assert_include body, 'ERB Layout!' assert_include body, '

Hello World

' end it 'can be used in a nested fashion for partials and whatnot' do mock_app do template(:inner) { 'hi' } template(:outer) { '<%= asciidoc :inner %>' } get('/') { erb :outer } end get '/' assert ok? assert_match %r{.*hi

.*
}m, body end end rescue LoadError warn "#{$!}: skipping asciidoc tests" end sinatra-3.0.5/test/base_test.rb000066400000000000000000000125061434717561400164550ustar00rootroot00000000000000require File.expand_path('helper', __dir__) class BaseTest < Minitest::Test describe 'Sinatra::Base subclasses' do class TestApp < Sinatra::Base get('/') { 'Hello World' } end class TestKeywordArgumentInitializerApp < Sinatra::Base def initialize(argument:) @argument = argument end get('/') { "Hello World with Keyword Arguments: #{@argument}" } end it 'include Rack::Utils' do assert TestApp.included_modules.include?(Rack::Utils) end it 'processes requests with #call' do assert TestApp.respond_to?(:call) request = Rack::MockRequest.new(TestApp) response = request.get('/') assert response.ok? assert_equal 'Hello World', response.body end class TestApp < Sinatra::Base get '/state' do @foo ||= "new" body = "Foo: #{@foo}" @foo = 'discard' body end end it 'does not maintain state between requests' do request = Rack::MockRequest.new(TestApp) 2.times do response = request.get('/state') assert response.ok? assert_equal 'Foo: new', response.body end end it "passes the subclass to configure blocks" do ref = nil TestApp.configure { |app| ref = app } assert_equal TestApp, ref end it "allows the configure block arg to be omitted and does not change context" do context = nil TestApp.configure { context = self } assert_equal self, context end it "allows constructor to receive keyword arguments" do app = TestKeywordArgumentInitializerApp.new(argument: "some argument") request = Rack::MockRequest.new(app) response = request.get('/') assert response.ok? assert_equal 'Hello World with Keyword Arguments: some argument', response.body end end describe "Sinatra::Base#new" do it 'returns a wrapper' do assert_equal Sinatra::Wrapper, Sinatra::Base.new.class end it 'implements a nice inspect' do assert_equal '#', Sinatra::Base.new.inspect end it 'exposes settings' do assert_equal Sinatra::Base.settings, Sinatra::Base.new.settings end it 'exposes helpers' do assert_equal 'image/jpeg', Sinatra::Base.new.helpers.mime_type(:jpg) end end describe "Sinatra::Base as Rack middleware" do app = lambda { |env| headers = {'X-Downstream' => 'true'} headers['X-Route-Missing'] = env['sinatra.route-missing'] || '' [210, headers, ['Hello from downstream']] } class TestMiddleware < Sinatra::Base end it 'creates a middleware that responds to #call with .new' do middleware = TestMiddleware.new(app) assert middleware.respond_to?(:call) end it 'exposes the downstream app' do middleware = TestMiddleware.new!(app) assert_same app, middleware.app end class TestMiddleware < Sinatra::Base def route_missing env['sinatra.route-missing'] = '1' super end get('/') { 'Hello from middleware' } end middleware = TestMiddleware.new(app) request = Rack::MockRequest.new(middleware) it 'intercepts requests' do response = request.get('/') assert response.ok? assert_equal 'Hello from middleware', response.body end it 'automatically forwards requests downstream when no matching route found' do response = request.get('/missing') assert_equal 210, response.status assert_equal 'Hello from downstream', response.body end it 'calls #route_missing before forwarding downstream' do response = request.get('/missing') assert_equal '1', response['X-Route-Missing'] end class TestMiddleware < Sinatra::Base get('/low-level-forward') { app.call(env) } end it 'can call the downstream app directly and return result' do response = request.get('/low-level-forward') assert_equal 210, response.status assert_equal 'true', response['X-Downstream'] assert_equal 'Hello from downstream', response.body end class TestMiddleware < Sinatra::Base get '/explicit-forward' do response['X-Middleware'] = 'true' res = forward assert_nil res assert_equal 210, response.status assert_equal 'true', response['X-Downstream'] assert_equal ['Hello from downstream'], response.body 'Hello after explicit forward' end end it 'forwards the request downstream and integrates the response into the current context' do response = request.get('/explicit-forward') assert_equal 210, response.status assert_equal 'true', response['X-Downstream'] assert_equal 'Hello after explicit forward', response.body assert_equal '28', response['Content-Length'] end app_content_length = lambda {|env| [200, {'Content-Length' => '16'}, 'From downstream!']} class TestMiddlewareContentLength < Sinatra::Base get '/forward' do 'From after explicit forward!' end end middleware_content_length = TestMiddlewareContentLength.new(app_content_length) request_content_length = Rack::MockRequest.new(middleware_content_length) it "sets content length for last response" do response = request_content_length.get('/forward') assert_equal '28', response['Content-Length'] end end end sinatra-3.0.5/test/builder_test.rb000066400000000000000000000042551434717561400171730ustar00rootroot00000000000000require File.expand_path('helper', __dir__) begin require 'builder' class BuilderTest < Minitest::Test def builder_app(options = {}, &block) mock_app do set :views, __dir__ + '/views' set options get('/', &block) end get '/' end it 'renders inline Builder strings' do builder_app { builder 'xml.instruct!' } assert ok? assert_equal %{\n}, body end it 'defaults content type to xml' do builder_app { builder 'xml.instruct!' } assert ok? assert_equal "application/xml;charset=utf-8", response['Content-Type'] end it 'defaults allows setting content type per route' do builder_app do content_type :html builder 'xml.instruct!' end assert ok? assert_equal "text/html;charset=utf-8", response['Content-Type'] end it 'defaults allows setting content type globally' do builder_app(:builder => { :content_type => 'html' }) do builder 'xml.instruct!' end assert ok? assert_equal "text/html;charset=utf-8", response['Content-Type'] end it 'renders inline blocks' do builder_app do @name = "Frank & Mary" builder { |xml| xml.couple @name } end assert ok? assert_equal "Frank & Mary\n", body end it 'renders .builder files in views path' do builder_app do @name = "Blue" builder :hello end assert ok? assert_equal %(You're my boy, Blue!\n), body end it "renders with inline layouts" do mock_app do layout { %(xml.layout { xml << yield }) } get('/') { builder %(xml.em 'Hello World') } end get '/' assert ok? assert_equal "\nHello World\n\n", body end it "renders with file layouts" do builder_app do builder %(xml.em 'Hello World'), :layout => :layout2 end assert ok? assert_equal "\nHello World\n\n", body end it "raises error if template not found" do mock_app do get('/') { builder :no_such_template } end assert_raises(Errno::ENOENT) { get('/') } end end rescue LoadError warn "#{$!}: skipping builder tests" end sinatra-3.0.5/test/compile_test.rb000066400000000000000000000142551434717561400171760ustar00rootroot00000000000000# I like coding: UTF-8 require File.expand_path('helper', __dir__) class CompileTest < Minitest::Test def self.parses pattern, example, expected_params, mtype = :sinatra, mopts = {} it "parses #{example} with #{pattern} into params #{expected_params}" do compiled = mock_app { set :mustermann_opts, :type => mtype }.send(:compile, pattern, mopts) params = compiled.params(example) fail %Q{"#{example}" does not parse on pattern "#{pattern}".} unless params assert_equal expected_params, params, "Pattern #{pattern} does not match path #{example}." end end def self.fails pattern, example, mtype = :sinatra, mopts = {} it "does not parse #{example} with #{pattern}" do compiled = mock_app { set :mustermann_opts, :type => mtype }.send(:compile, pattern, mopts) match = compiled.match(example) fail %Q{"#{pattern}" does parse "#{example}" but it should fail} if match end end def self.raises pattern, mtype = :sinatra, mopts = {} it "does not compile #{pattern}" do assert_raises(Mustermann::CompileError, %Q{Pattern "#{pattern}" compiles but it should not}) do mock_app { set :mustermann_opts, :type => mtype }.send(:compile, pattern, mopts) end end end parses "/", "/", {} parses "/foo", "/foo", {} parses "/:foo", "/foo", "foo" => "foo" parses "/:foo", "/foo.bar", "foo" => "foo.bar" parses "/:foo", "/foo%2Fbar", "foo" => "foo/bar" parses "/:foo", "/%0Afoo", "foo" => "\nfoo" fails "/:foo", "/foo?" fails "/:foo", "/foo/bar" fails "/:foo", "/" fails "/:foo", "/foo/" parses "/föö", "/f%C3%B6%C3%B6", {} parses "/:foo/:bar", "/foo/bar", "foo" => "foo", "bar" => "bar" parses "/hello/:person", "/hello/Frank", "person" => "Frank" parses "/?:foo?/?:bar?", "/hello/world", "foo" => "hello", "bar" => "world" parses "/?:foo?/?:bar?", "/hello", "foo" => "hello", "bar" => nil parses "/?:foo?/?:bar?", "/", "foo" => nil, "bar" => nil parses "/?:foo?/?:bar?", "", "foo" => nil, "bar" => nil parses "/*", "/", "splat" => [""] parses "/*", "/foo", "splat" => ["foo"] parses "/*", "/foo/bar", "splat" => ["foo/bar"] parses "/:foo/*", "/foo/bar/baz", "foo" => "foo", "splat" => ["bar/baz"] parses "/:foo/:bar", "/user@example.com/name", "foo" => "user@example.com", "bar" => "name" parses "/test$/", "/test$/", {} parses "/te+st/", "/te+st/", {} fails "/te+st/", "/test/" fails "/te+st/", "/teeest/" parses "/test(bar)/", "/testbar/", {} parses "/path with spaces", "/path%20with%20spaces", {} parses "/path with spaces", "/path%2Bwith%2Bspaces", {} parses "/path with spaces", "/path+with+spaces", {} parses "/foo&bar", "/foo&bar", {} parses "/:foo/*", "/hello%20world/how%20are%20you", "foo" => "hello world", "splat" => ["how are you"] parses "/*/foo/*/*", "/bar/foo/bling/baz/boom", "splat" => ["bar", "bling", "baz/boom"] parses "/*/foo/*/*rest", "/bar/foo/bling/baz/boom", "splat" => ["bar", "bling"], "rest" => "baz/boom" fails "/*/foo/*/*", "/bar/foo/baz" parses "/test.bar", "/test.bar", {} fails "/test.bar", "/test0bar" parses "/:file.:ext", "/pony.jpg", "file" => "pony", "ext" => "jpg" parses "/:file.:ext", "/pony%2Ejpg", "file" => "pony", "ext" => "jpg" fails "/:file.:ext", "/.jpg" parses "/:name.?:format?", "/foo", "name" => "foo", "format" => nil parses "/:name.?:format?", "/foo.bar", "name" => "foo", "format" => "bar" parses "/:name.?:format?", "/foo%2Ebar", "name" => "foo", "format" => "bar" parses "/:user@?:host?", "/foo@bar", "user" => "foo", "host" => "bar" parses "/:user@?:host?", "/foo.foo@bar", "user" => "foo.foo", "host" => "bar" parses "/:user@?:host?", "/foo@bar.bar", "user" => "foo", "host" => "bar.bar" # From https://gist.github.com/2154980#gistcomment-169469. # parses "/:name(.:format)?", "/foo", "name" => "foo", "format" => nil parses "/:name(.:format)?", "/foo.bar", "name" => "foo", "format" => "bar" parses "/:name(.:format)?", "/foo.", "name" => "foo.", "format" => nil parses "/:id/test.bar", "/3/test.bar", {"id" => "3"} parses "/:id/test.bar", "/2/test.bar", {"id" => "2"} parses "/:id/test.bar", "/2E/test.bar", {"id" => "2E"} parses "/:id/test.bar", "/2e/test.bar", {"id" => "2e"} parses "/:id/test.bar", "/%2E/test.bar", {"id" => "."} parses "/{id}/test.bar", "/%2E/test.bar", {"id" => "."} parses '/10/:id', '/10/test', "id" => "test" parses '/10/:id', '/10/te.st', "id" => "te.st" parses '/10.1/:id', '/10.1/test', "id" => "test" parses '/10.1/:id', '/10.1/te.st', "id" => "te.st" parses '/:foo/:id', '/10.1/te.st', "foo" => "10.1", "id" => "te.st" parses '/:foo/:id', '/10.1.2/te.st', "foo" => "10.1.2", "id" => "te.st" parses '/:foo.:bar/:id', '/10.1/te.st', "foo" => "10", "bar" => "1", "id" => "te.st" parses '/:a/:b.?:c?', '/a/b', "a" => "a", "b" => "b", "c" => nil parses '/:a/:b.?:c?', '/a/b.c', "a" => "a", "b" => "b", "c" => "c" parses '/:a/:b.?:c?', '/a.b/c', "a" => "a.b", "b" => "c", "c" => nil parses '/:a/:b.?:c?', '/a.b/c.d', "a" => "a.b", "b" => "c", "c" => "d" fails '/:a/:b.?:c?', '/a.b/c.d/e' parses "/:file.:ext", "/pony%2ejpg", "file" => "pony", "ext" => "jpg" parses "/:file.:ext", "/pony%E6%AD%A3%2Ejpg", "file" => "pony正", "ext" => "jpg" parses "/:file.:ext", "/pony%e6%ad%a3%2ejpg", "file" => "pony正", "ext" => "jpg" parses "/:file.:ext", "/pony正%2Ejpg", "file" => "pony正", "ext" => "jpg" parses "/:file.:ext", "/pony正%2ejpg", "file" => "pony正", "ext" => "jpg" parses "/:file.:ext", "/pony正..jpg", "file" => "pony正.", "ext" => "jpg" parses "/:name.:format", "/file.tar.gz", "name" => "file.tar", "format" => "gz" parses "/:name.:format1.:format2", "/file.tar.gz", "name" => "file", "format1" => "tar", "format2" => "gz" parses "/:name.:format1.:format2", "/file.temp.tar.gz", "name" => "file.temp", "format1" => "tar", "format2" => "gz" # From issue #688. # parses "/articles/10.1103/:doi", "/articles/10.1103/PhysRevLett.110.026401", "doi" => "PhysRevLett.110.026401" # Mustermann anchoring fails "/bar", "/foo/bar", :regexp raises "^/foo/bar$", :regexp parses "^/foo/bar$", "/foo/bar", {}, :regexp, :check_anchors => false end sinatra-3.0.5/test/contest.rb000066400000000000000000000062451434717561400161660ustar00rootroot00000000000000# Copyright (c) 2009 Damian Janowski and Michel Martens for Citrusbyte # # 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. require "rubygems" require "minitest/autorun" # Contest adds +teardown+, +test+ and +context+ as class methods, and the # instance methods +setup+ and +teardown+ now iterate on the corresponding # blocks. Note that all setup and teardown blocks must be defined with the # block syntax. Adding setup or teardown instance methods defeats the purpose # of this library. class Minitest::Test def self.setup(&block) setup_blocks << block end def self.teardown(&block) teardown_blocks << block end def self.setup_blocks() @setup_blocks ||= [] end def self.teardown_blocks() @teardown_blocks ||= [] end def setup_blocks(base = self.class) setup_blocks base.superclass if base.superclass.respond_to? :setup_blocks base.setup_blocks.each do |block| instance_eval(&block) end end def teardown_blocks(base = self.class) teardown_blocks base.superclass if base.superclass.respond_to? :teardown_blocks base.teardown_blocks.each do |block| instance_eval(&block) end end alias setup setup_blocks alias teardown teardown_blocks def self.context(*name, &block) subclass = Class.new(self) remove_tests(subclass) subclass.class_eval(&block) if block_given? const_set(context_name(name.join(" ")), subclass) end def self.test(name, &block) define_method(test_name(name), &block) end class << self alias_method :should, :test alias_method :describe, :context end private def self.context_name(name) # "Test#{sanitize_name(name).gsub(/(^| )(\w)/) { $2.upcase }}".to_sym name = "Test#{sanitize_name(name).gsub(/(^| )(\w)/) { $2.upcase }}" name.tr(" ", "_").to_sym end def self.test_name(name) name = "test_#{sanitize_name(name).gsub(/\s+/,'_')}_0" name = name.succ while method_defined? name name.to_sym end def self.sanitize_name(name) # name.gsub(/\W+/, ' ').strip name.gsub(/\W+/, ' ') end def self.remove_tests(subclass) subclass.public_instance_methods.grep(/^test_/).each do |meth| subclass.send(:undef_method, meth.to_sym) end end end sinatra-3.0.5/test/delegator_test.rb000066400000000000000000000077421434717561400175170ustar00rootroot00000000000000require File.expand_path('helper', __dir__) class DelegatorTest < Minitest::Test class Mirror attr_reader :last_call def method_missing(*a, &b) @last_call = [*a.map(&:to_s)] @last_call << b if b end end def self.delegates(name) it "delegates #{name}" do m = mirror { send name } assert_equal [name.to_s], m.last_call end it "delegates #{name} with arguments" do m = mirror { send name, "foo", "bar" } assert_equal [name.to_s, "foo", "bar"], m.last_call end it "delegates #{name} with block" do block = proc { } m = mirror { send(name, &block) } assert_equal [name.to_s, block], m.last_call end end setup do @target_was = Sinatra::Delegator.target end def teardown Sinatra::Delegator.target = @target_was end def delegation_app(&block) mock_app { Sinatra::Delegator.target = self } delegate(&block) end def mirror(&block) mirror = Mirror.new Sinatra::Delegator.target = mirror delegate(&block) end def delegate(&block) assert Sinatra::Delegator.target != Sinatra::Application Object.new.extend(Sinatra::Delegator).instance_eval(&block) if block Sinatra::Delegator.target end def target Sinatra::Delegator.target end it 'defaults to Sinatra::Application as target' do assert_equal Sinatra::Application, Sinatra::Delegator.target end %w[get put post delete options patch link unlink].each do |verb| it "delegates #{verb} correctly" do delegation_app do send(verb, '/hello') { 'Hello World' } end request = Rack::MockRequest.new(@app) response = request.request(verb.upcase, '/hello', {}) assert response.ok? assert_equal 'Hello World', response.body end end it "delegates head correctly" do delegation_app do head '/hello' do response['X-Hello'] = 'World!' 'remove me' end end request = Rack::MockRequest.new(@app) response = request.request('HEAD', '/hello', {}) assert response.ok? assert_equal 'World!', response['X-Hello'] assert_equal '', response.body end it "delegates before with keyword arguments correctly" do delegation_app do set(:foo) do |something| something end before(foo: 'bar') do :nothing end end end it "registers extensions with the delegation target" do app, mixin = mirror, Module.new Sinatra.register mixin assert_equal ["register", mixin.to_s], app.last_call end it "registers helpers with the delegation target" do app, mixin = mirror, Module.new Sinatra.helpers mixin assert_equal ["helpers", mixin.to_s], app.last_call end it "registers middleware with the delegation target" do app, mixin = mirror, Module.new Sinatra.use mixin assert_equal ["use", mixin.to_s], app.last_call end it "should work with method_missing proxies for options" do mixin = Module.new do def respond_to?(method, *) method.to_sym == :options or super end def method_missing(method, *args, &block) return super unless method.to_sym == :options {:some => :option} end end value = nil mirror do extend mixin value = options end assert_equal({:some => :option}, value) end it "delegates crazy method names" do Sinatra::Delegator.delegate "foo:bar:" method = mirror { send "foo:bar:" }.last_call.first assert_equal "foo:bar:", method end delegates 'get' delegates 'patch' delegates 'put' delegates 'post' delegates 'delete' delegates 'head' delegates 'options' delegates 'template' delegates 'layout' delegates 'before' delegates 'after' delegates 'error' delegates 'not_found' delegates 'configure' delegates 'set' delegates 'mime_type' delegates 'enable' delegates 'disable' delegates 'use' delegates 'development?' delegates 'test?' delegates 'production?' delegates 'helpers' delegates 'settings' end sinatra-3.0.5/test/encoding_test.rb000066400000000000000000000011301434717561400173200ustar00rootroot00000000000000# encoding: UTF-8 require File.expand_path('helper', __dir__) require 'erb' class BaseTest < Minitest::Test setup do @base = Sinatra.new(Sinatra::Base) @base.set :views, __dir__ + "/views" end it 'allows unicode strings in ascii templates per default (1.9)' do next unless defined? Encoding @base.new!.erb(File.read(@base.views + "/ascii.erb").encode("ASCII"), {}, :value => "åkej") end it 'allows ascii strings in unicode templates per default (1.9)' do next unless defined? Encoding @base.new!.erb(:utf8, {}, :value => "Some Lyrics".encode("ASCII")) end end sinatra-3.0.5/test/erb_test.rb000066400000000000000000000050301434717561400163050ustar00rootroot00000000000000require File.expand_path('helper', __dir__) class ERBTest < Minitest::Test def engine Tilt::ERBTemplate end def setup Tilt.prefer engine, :erb super end def erb_app(&block) mock_app do set :views, __dir__ + '/views' get('/', &block) end get '/' end it 'uses the correct engine' do assert_equal engine, Tilt[:erb] end it 'renders inline ERB strings' do erb_app { erb '<%= 1 + 1 %>' } assert ok? assert_equal '2', body end it 'renders .erb files in views path' do erb_app { erb :hello } assert ok? assert_equal "Hello World\n", body end it 'takes a :locals option' do erb_app do locals = {:foo => 'Bar'} erb '<%= foo %>', :locals => locals end assert ok? assert_equal 'Bar', body end it "renders with inline layouts" do mock_app do layout { 'THIS. IS. <%= yield.upcase %>!' } get('/') { erb 'Sparta' } end get '/' assert ok? assert_equal 'THIS. IS. SPARTA!', body end it "renders with file layouts" do erb_app { erb 'Hello World', :layout => :layout2 } assert ok? assert_body "ERB Layout!\nHello World" end it "renders erb with blocks" do mock_app do def container @_out_buf << "THIS." yield @_out_buf << "SPARTA!" end def is; "IS." end get('/') { erb '<% container do %> <%= is %> <% end %>' } end get '/' assert ok? assert_equal 'THIS. IS. SPARTA!', body end it "can be used in a nested fashion for partials and whatnot" do mock_app do template(:inner) { "<%= 'hi' %>" } template(:outer) { "<%= erb :inner %>" } get('/') { erb :outer } end get '/' assert ok? assert_equal 'hi', body end it "can render truly nested layouts by accepting a layout and a block with the contents" do mock_app do template(:main_outer_layout) { "

Title

\n<%= yield %>" } template(:an_inner_layout) { "

Subtitle

\n<%= yield %>" } template(:a_page) { "

Contents.

\n" } get('/') do erb :main_outer_layout, :layout => false do erb :an_inner_layout do erb :a_page end end end end get '/' assert ok? assert_body "

Title

\n

Subtitle

\n

Contents.

\n" end end begin require 'erubi' class ErubiTest < ERBTest def engine; Tilt::ErubiTemplate end end rescue LoadError warn "#{$!}: skipping erubi tests" end sinatra-3.0.5/test/extensions_test.rb000066400000000000000000000052761434717561400177500ustar00rootroot00000000000000require File.expand_path('helper', __dir__) class ExtensionsTest < Minitest::Test module FooExtensions def foo end private def im_hiding_in_ur_foos end end module BarExtensions def bar end end module BazExtensions def baz end end module QuuxExtensions def quux end end module PainExtensions def foo=(name); end def bar?(name); end def fizz!(name); end end it 'will add the methods to the DSL for the class in which you register them and its subclasses' do Sinatra::Base.register FooExtensions assert Sinatra::Base.respond_to?(:foo) Sinatra::Application.register BarExtensions assert Sinatra::Application.respond_to?(:bar) assert Sinatra::Application.respond_to?(:foo) assert !Sinatra::Base.respond_to?(:bar) end it 'allows extending by passing a block' do Sinatra::Base.register { def im_in_ur_anonymous_module; end } assert Sinatra::Base.respond_to?(:im_in_ur_anonymous_module) end it 'will make sure any public methods added via Application#register are delegated to Sinatra::Delegator' do Sinatra::Application.register FooExtensions assert Sinatra::Delegator.private_instance_methods. map { |m| m.to_sym }.include?(:foo) assert !Sinatra::Delegator.private_instance_methods. map { |m| m.to_sym }.include?(:im_hiding_in_ur_foos) end it 'will handle special method names' do Sinatra::Application.register PainExtensions assert Sinatra::Delegator.private_instance_methods. map { |m| m.to_sym }.include?(:foo=) assert Sinatra::Delegator.private_instance_methods. map { |m| m.to_sym }.include?(:bar?) assert Sinatra::Delegator.private_instance_methods. map { |m| m.to_sym }.include?(:fizz!) end it 'will not delegate methods on Base#register' do Sinatra::Base.register QuuxExtensions assert !Sinatra::Delegator.private_instance_methods.include?("quux") end it 'will extend the Sinatra::Application application by default' do Sinatra.register BazExtensions assert !Sinatra::Base.respond_to?(:baz) assert Sinatra::Application.respond_to?(:baz) end module BizzleExtension def bizzle bizzle_option end def self.registered(base) fail "base should be BizzleApp" unless base == BizzleApp fail "base should have already extended BizzleExtension" unless base.respond_to?(:bizzle) base.set :bizzle_option, 'bizzle!' end end class BizzleApp < Sinatra::Base end it 'sends .registered to the extension module after extending the class' do BizzleApp.register BizzleExtension assert_equal 'bizzle!', BizzleApp.bizzle_option assert_equal 'bizzle!', BizzleApp.bizzle end end sinatra-3.0.5/test/filter_test.rb000066400000000000000000000270031434717561400170260ustar00rootroot00000000000000require File.expand_path('helper', __dir__) class BeforeFilterTest < Minitest::Test it "executes filters in the order defined" do count = 0 mock_app do get('/') { 'Hello World' } before do assert_equal 0, count count = 1 end before do assert_equal 1, count count = 2 end end get '/' assert ok? assert_equal 2, count assert_equal 'Hello World', body end it "can modify the request" do mock_app do get('/foo') { 'foo' } get('/bar') { 'bar' } before { request.path_info = '/bar' } end get '/foo' assert ok? assert_equal 'bar', body end it "can modify instance variables available to routes" do mock_app do before { @foo = 'bar' } get('/foo') { @foo } end get '/foo' assert ok? assert_equal 'bar', body end it "allows redirects" do mock_app do before { redirect '/bar' } get('/foo') do fail 'before block should have halted processing' 'ORLY?!' end end get '/foo' assert redirect? assert_equal 'http://example.org/bar', response['Location'] assert_equal '', body end it "does not modify the response with its return value" do mock_app do before { 'Hello World!' } get('/foo') do assert_equal [], response.body 'cool' end end get '/foo' assert ok? assert_equal 'cool', body end it "does modify the response with halt" do mock_app do before { halt 302, 'Hi' } get '/foo' do "should not happen" end end get '/foo' assert_equal 302, response.status assert_equal 'Hi', body end it "gives you access to params" do mock_app do before { @foo = params['foo'] } get('/foo') { @foo } end get '/foo?foo=cool' assert ok? assert_equal 'cool', body end it "properly unescapes parameters" do mock_app do before { @foo = params['foo'] } get('/foo') { @foo } end get '/foo?foo=bar%3Abaz%2Fbend' assert ok? assert_equal 'bar:baz/bend', body end it "runs filters defined in superclasses" do base = Class.new(Sinatra::Base) base.before { @foo = 'hello from superclass' } mock_app(base) { get('/foo') { @foo } } get '/foo' assert_equal 'hello from superclass', body end it 'does not run before filter when serving static files' do ran_filter = false mock_app do before { ran_filter = true } set :static, true set :public_folder, __dir__ end get "/#{File.basename(__FILE__)}" assert ok? assert_equal File.read(__FILE__), body assert !ran_filter end it 'takes an optional route pattern' do ran_filter = false mock_app do before("/b*") { ran_filter = true } get('/foo') { } get('/bar') { } end get '/foo' assert !ran_filter get '/bar' assert ran_filter end it 'generates block arguments from route pattern' do subpath = nil mock_app do before("/foo/:sub") { |s| subpath = s } get('/foo/*') { } end get '/foo/bar' assert_equal subpath, 'bar' end it 'can catch exceptions in before filters and handle them properly' do doodle = '' mock_app do before do doodle += 'This begins' raise StandardError, "before" end get "/" do doodle = 'and runs' end error 500 do "Error handled #{env['sinatra.error'].message}" end end doodle = '' get '/' assert_equal 'Error handled before', body assert_equal 'This begins', doodle end end class AfterFilterTest < Minitest::Test it "executes before and after filters in correct order" do invoked = 0 mock_app do before { invoked = 2 } get('/') { invoked += 2; 'hello' } after { invoked *= 2 } end get '/' assert ok? assert_equal 8, invoked end it "executes filters in the order defined" do count = 0 mock_app do get('/') { 'Hello World' } after do assert_equal 0, count count = 1 end after do assert_equal 1, count count = 2 end end get '/' assert ok? assert_equal 2, count assert_equal 'Hello World', body end it "allows redirects" do mock_app do get('/foo') { 'ORLY' } after { redirect '/bar' } end get '/foo' assert redirect? assert_equal 'http://example.org/bar', response['Location'] assert_equal '', body end it "does not modify the response with its return value" do mock_app do get('/foo') { 'cool' } after { 'Hello World!' } end get '/foo' assert ok? assert_equal 'cool', body end it "does modify the response with halt" do mock_app do get '/foo' do "should not be returned" end after { halt 302, 'Hi' } end get '/foo' assert_equal 302, response.status assert_equal 'Hi', body end it "runs filters defined in superclasses" do count = 2 base = Class.new(Sinatra::Base) base.after { count *= 2 } mock_app(base) do get('/foo') do count += 2 "ok" end end get '/foo' assert_equal 8, count end it "respects content type set in superclass filter" do base = Class.new(Sinatra::Base) base.before { content_type :json } mock_app(base) do get('/foo'){ {foo: :bar}.to_json } end get '/foo' assert_equal 'application/json', response.headers['Content-Type'] end it 'does not run after filter when serving static files' do ran_filter = false mock_app do after { ran_filter = true } set :static, true set :public_folder, __dir__ end get "/#{File.basename(__FILE__)}" assert ok? assert_equal File.read(__FILE__), body assert !ran_filter end it 'takes an optional route pattern' do ran_filter = false mock_app do after("/b*") { ran_filter = true } get('/foo') { } get('/bar') { } end get '/foo' assert !ran_filter get '/bar' assert ran_filter end it 'changes to path_info from a pattern matching before filter are respected when routing' do mock_app do before('/foo') { request.path_info = '/bar' } get('/bar') { 'blah' } end get '/foo' assert ok? assert_equal 'blah', body end it 'generates block arguments from route pattern' do subpath = nil mock_app do after("/foo/:sub") { |s| subpath = s } get('/foo/*') { } end get '/foo/bar' assert_equal subpath, 'bar' end it 'is possible to access url params from the route param' do ran = false mock_app do get('/foo/*') { } before('/foo/:sub') do assert_equal params[:sub], 'bar' ran = true end end get '/foo/bar' assert ran end it 'is possible to apply host_name conditions to before filters with no path' do ran = false mock_app do before(:host_name => 'example.com') { ran = true } get('/') { 'welcome' } end get('/', {}, { 'HTTP_HOST' => 'example.org' }) assert !ran get('/', {}, { 'HTTP_HOST' => 'example.com' }) assert ran end it 'is possible to apply host_name conditions to before filters with a path' do ran = false mock_app do before('/foo', :host_name => 'example.com') { ran = true } get('/') { 'welcome' } end get('/', {}, { 'HTTP_HOST' => 'example.com' }) assert !ran get('/foo', {}, { 'HTTP_HOST' => 'example.org' }) assert !ran get('/foo', {}, { 'HTTP_HOST' => 'example.com' }) assert ran end it 'is possible to apply host_name conditions to after filters with no path' do ran = false mock_app do after(:host_name => 'example.com') { ran = true } get('/') { 'welcome' } end get('/', {}, { 'HTTP_HOST' => 'example.org' }) assert !ran get('/', {}, { 'HTTP_HOST' => 'example.com' }) assert ran end it 'is possible to apply host_name conditions to after filters with a path' do ran = false mock_app do after('/foo', :host_name => 'example.com') { ran = true } get('/') { 'welcome' } end get('/', {}, { 'HTTP_HOST' => 'example.com' }) assert !ran get('/foo', {}, { 'HTTP_HOST' => 'example.org' }) assert !ran get('/foo', {}, { 'HTTP_HOST' => 'example.com' }) assert ran end it 'is possible to apply user_agent conditions to before filters with no path' do ran = false mock_app do before(:user_agent => /foo/) { ran = true } get('/') { 'welcome' } end get('/', {}, { 'HTTP_USER_AGENT' => 'bar' }) assert !ran get('/', {}, { 'HTTP_USER_AGENT' => 'foo' }) assert ran end it 'is possible to apply user_agent conditions to before filters with a path' do ran = false mock_app do before('/foo', :user_agent => /foo/) { ran = true } get('/') { 'welcome' } end get('/', {}, { 'HTTP_USER_AGENT' => 'foo' }) assert !ran get('/foo', {}, { 'HTTP_USER_AGENT' => 'bar' }) assert !ran get('/foo', {}, { 'HTTP_USER_AGENT' => 'foo' }) assert ran end it 'can add params' do mock_app do before { params['foo'] = 'bar' } get('/') { params['foo'] } end get '/' assert_body 'bar' end it 'can add params on a single path' do mock_app do before('/hi'){ params['foo'] = 'bar' } get('/hi') { params['foo'] } end get '/hi' assert_body 'bar' end # ref: issue #1567 it 'can add params on named parameters path' do mock_app do before('/:id/hi'){ params['foo'] = 'bar' } get('/:id/hi') { params['foo'] } end get '/:id/hi' assert_body 'bar' end it 'can remove params' do mock_app do before { params.delete('foo') } get('/') { params['foo'].to_s } end get '/?foo=bar' assert_body '' end it 'is possible to apply user_agent conditions to after filters with no path' do ran = false mock_app do after(:user_agent => /foo/) { ran = true } get('/') { 'welcome' } end get('/', {}, { 'HTTP_USER_AGENT' => 'bar' }) assert !ran get('/', {}, { 'HTTP_USER_AGENT' => 'foo' }) assert ran end it 'is possible to apply user_agent conditions to after filters with a path' do ran = false mock_app do after('/foo', :user_agent => /foo/) { ran = true } get('/') { 'welcome' } end get('/', {}, { 'HTTP_USER_AGENT' => 'foo' }) assert !ran get('/foo', {}, { 'HTTP_USER_AGENT' => 'bar' }) assert !ran get('/foo', {}, { 'HTTP_USER_AGENT' => 'foo' }) assert ran end it 'only triggers provides condition if conforms with current Content-Type' do mock_app do before(:provides => :txt) { @type = 'txt' } before(:provides => :html) { @type = 'html' } get('/') { @type } end get('/', {}, { 'HTTP_ACCEPT' => '*/*' }) assert_body 'txt' end it 'can catch exceptions in after filters and handle them properly' do doodle = '' mock_app do after do doodle += ' and after' raise StandardError, "after" end get "/foo" do doodle = 'Been now' raise StandardError, "now" end get "/" do doodle = 'Been now' end error 500 do "Error handled #{env['sinatra.error'].message}" end end get '/foo' assert_equal 'Error handled now', body assert_equal 'Been now and after', doodle doodle = '' get '/' assert_equal 'Error handled after', body assert_equal 'Been now and after', doodle end end sinatra-3.0.5/test/haml_test.rb000066400000000000000000000056141434717561400164660ustar00rootroot00000000000000require File.expand_path('helper', __dir__) begin require 'haml' class HAMLTest < Minitest::Test def haml_app(&block) mock_app do set :views, __dir__ + '/views' get('/', &block) end get '/' end it 'renders inline HAML strings' do haml_app { haml '%h1 Hiya' } assert ok? assert_equal "

Hiya

\n", body end it 'renders .haml files in views path' do haml_app { haml :hello } assert ok? assert_equal "

Hello From Haml

\n", body end it "renders with inline layouts" do mock_app do layout { %q(%h1= 'THIS. IS. ' + yield.upcase) } get('/') { haml '%em Sparta' } end get '/' assert ok? assert_equal "

THIS. IS. SPARTA\n

\n", body end it "renders with file layouts" do haml_app { haml 'Hello World', :layout => :layout2 } assert ok? assert_equal "

HAML Layout!

\n

Hello World\n

\n", body end it "raises error if template not found" do mock_app { get('/') { haml :no_such_template } } assert_raises(Errno::ENOENT) { get('/') } end it "passes HAML options to the Haml engine" do mock_app { get('/') { haml "!!!\n%h1 Hello World", :format => :html5 } } get '/' assert ok? assert_equal "\n

Hello World

\n", body end it "passes default HAML options to the Haml engine" do mock_app do set :haml, {:format => :html5} get('/') { haml "!!!\n%h1 Hello World" } end get '/' assert ok? assert_equal "\n

Hello World

\n", body end it "merges the default HAML options with the overrides and passes them to the Haml engine" do mock_app do set :haml, {:format => :html5, :attr_wrapper => '"'} # default HAML attr are get('/') { haml "!!!\n%h1{:class => :header} Hello World" } get('/html4') { haml "!!!\n%h1{:class => 'header'} Hello World", :format => :html4 } end get '/' assert ok? assert_equal "\n

Hello World

\n", body get '/html4' assert ok? assert_match(/^ { :foo => 'bar' }} assert_equal "bar\n", body end it "can render truly nested layouts by accepting a layout and a block with the contents" do mock_app do template(:main_outer_layout) { "%h1 Title\n= yield" } template(:an_inner_layout) { "%h2 Subtitle\n= yield" } template(:a_page) { "%p Contents." } get('/') do haml :main_outer_layout, :layout => false do haml :an_inner_layout do haml :a_page end end end end get '/' assert ok? assert_body "

Title

\n

Subtitle

\n

Contents.

\n" end end rescue LoadError warn "#{$!}: skipping haml tests" end sinatra-3.0.5/test/helper.rb000066400000000000000000000071741434717561400157700ustar00rootroot00000000000000if ENV['COVERAGE'] require 'simplecov' SimpleCov.start do add_filter '/test/' add_group 'sinatra-contrib', 'sinatra-contrib' add_group 'rack-protection', 'rack-protection' end end ENV['APP_ENV'] = 'test' Encoding.default_external = "UTF-8" if defined? Encoding RUBY_ENGINE = 'ruby' unless defined? RUBY_ENGINE require 'rack' testdir = __dir__ $LOAD_PATH.unshift testdir unless $LOAD_PATH.include?(testdir) libdir = File.dirname(__dir__) + '/lib' $LOAD_PATH.unshift libdir unless $LOAD_PATH.include?(libdir) require 'minitest' require 'contest' require 'rack/test' # Some of ActiveSupport's core extensions to Hash get loaded during internal # testing (e.g. by RABL and our RABL test) that we have no control over, but we # need them to load *before* Sinatra::IndifferentHash (which is itself loaded # by Sinatra::Base) whenever the full test suite is executed, so we'll do it # preemptively here. # # Newer Rubies have these methods built-in, so the extensions are no-ops. require 'active_support/core_ext/hash/conversions' require 'active_support/core_ext/hash/slice' require 'active_support/core_ext/hash/keys' require 'sinatra/base' class Sinatra::Base include Minitest::Assertions # Allow assertions in request context def assertions @assertions ||= 0 end attr_writer :assertions end class Rack::Builder def include?(middleware) @ins.any? { |m| middleware === m } end end Sinatra::Base.set :environment, :test class Minitest::Test include Rack::Test::Methods class << self alias_method :it, :test alias_method :section, :context end def self.example(desc = nil, &block) @example_count = 0 unless instance_variable_defined? :@example_count @example_count += 1 it(desc || "Example #{@example_count}", &block) end alias_method :response, :last_response setup do Sinatra::Base.set :environment, :test end # Sets up a Sinatra::Base subclass defined with the block # given. Used in setup or individual spec methods to establish # the application. def mock_app(base=Sinatra::Base, &block) @app = Sinatra.new(base, &block) end def app Rack::Lint.new(@app) end def body response.body.to_s end def assert_body(value) if value.respond_to? :to_str assert_equal value.lstrip.gsub(/\s*\n\s*/, ""), body.lstrip.gsub(/\s*\n\s*/, "") else assert_match value, body end end def assert_status(expected) assert_equal Integer(expected), Integer(status) end def assert_like(a,b) pattern = /id=['"][^"']*["']|\s+/ assert_equal a.strip.gsub(pattern, ""), b.strip.gsub(pattern, "") end def assert_include(str, substr) assert str.include?(substr), "expected #{str.inspect} to include #{substr.inspect}" end def options(uri, params = {}, env = {}, &block) request(uri, env.merge(:method => "OPTIONS", :params => params), &block) end def patch(uri, params = {}, env = {}, &block) request(uri, env.merge(:method => "PATCH", :params => params), &block) end def link(uri, params = {}, env = {}, &block) request(uri, env.merge(:method => "LINK", :params => params), &block) end def unlink(uri, params = {}, env = {}, &block) request(uri, env.merge(:method => "UNLINK", :params => params), &block) end # Delegate other missing methods to response. def method_missing(name, *args, &block) if response && response.respond_to?(name) response.send(name, *args, &block) else super end rescue Rack::Test::Error super end # Do not output warnings for the duration of the block. def silence_warnings $VERBOSE, v = nil, $VERBOSE yield ensure $VERBOSE = v end end sinatra-3.0.5/test/helpers_test.rb000066400000000000000000001452151434717561400172110ustar00rootroot00000000000000require File.expand_path('helper', __dir__) require 'date' require 'json' class HelpersTest < Minitest::Test def test_default assert true end def status_app(code, &block) code += 2 if [204, 304].include? code block ||= proc { } mock_app do get('/') do status code instance_eval(&block).inspect end end get '/' end describe 'status' do it 'sets the response status code' do status_app 207 assert_equal 207, response.status end end describe 'bad_request?' do it 'is true for status == 400' do status_app(400) { bad_request? } assert_body 'true' end it 'is false for status gt 400' do status_app(401) { bad_request? } assert_body 'false' end it 'is false for status lt 400' do status_app(399) { bad_request? } assert_body 'false' end end describe 'not_found?' do it 'is true for status == 404' do status_app(404) { not_found? } assert_body 'true' end it 'is false for status gt 404' do status_app(405) { not_found? } assert_body 'false' end it 'is false for status lt 404' do status_app(403) { not_found? } assert_body 'false' end end describe 'informational?' do it 'is true for 1xx status' do status_app(100 + rand(100)) { informational? } assert_body 'true' end it 'is false for status > 199' do status_app(200 + rand(400)) { informational? } assert_body 'false' end end describe 'success?' do it 'is true for 2xx status' do status_app(200 + rand(100)) { success? } assert_body 'true' end it 'is false for status < 200' do status_app(100 + rand(100)) { success? } assert_body 'false' end it 'is false for status > 299' do status_app(300 + rand(300)) { success? } assert_body 'false' end end describe 'redirect?' do it 'is true for 3xx status' do status_app(300 + rand(100)) { redirect? } assert_body 'true' end it 'is false for status < 300' do status_app(200 + rand(100)) { redirect? } assert_body 'false' end it 'is false for status > 399' do status_app(400 + rand(200)) { redirect? } assert_body 'false' end end describe 'client_error?' do it 'is true for 4xx status' do status_app(400 + rand(100)) { client_error? } assert_body 'true' end it 'is false for status < 400' do status_app(200 + rand(200)) { client_error? } assert_body 'false' end it 'is false for status > 499' do status_app(500 + rand(100)) { client_error? } assert_body 'false' end end describe 'server_error?' do it 'is true for 5xx status' do status_app(500 + rand(100)) { server_error? } assert_body 'true' end it 'is false for status < 500' do status_app(200 + rand(300)) { server_error? } assert_body 'false' end end describe 'body' do it 'takes a block for deferred body generation' do mock_app do get('/') { body { 'Hello World' } } end get '/' assert_equal 'Hello World', body end it 'takes a String, Array, or other object responding to #each' do mock_app { get('/') { body 'Hello World' } } get '/' assert_equal 'Hello World', body end it 'can be used with other objects' do mock_app do get '/' do body :hello => 'from json' end after do if Hash === response.body body response.body[:hello] end end end get '/' assert_body 'from json' end it 'can be set in after filter' do mock_app do get('/') { body 'route' } after { body 'filter' } end get '/' assert_body 'filter' end end describe 'redirect' do it 'uses a 302 when only a path is given' do mock_app do get('/') do redirect '/foo' fail 'redirect should halt' end end get '/' assert_equal 302, status assert_equal '', body assert_equal 'http://example.org/foo', response['Location'] end it 'uses the code given when specified' do mock_app do get('/') do redirect '/foo', 301 fail 'redirect should halt' end end get '/' assert_equal 301, status assert_equal '', body assert_equal 'http://example.org/foo', response['Location'] end it 'redirects back to request.referer when passed back' do mock_app { get('/try_redirect') { redirect back } } request = Rack::MockRequest.new(@app) response = request.get('/try_redirect', 'HTTP_REFERER' => '/foo') assert_equal 302, response.status assert_equal 'http://example.org/foo', response['Location'] end it 'redirects using a non-standard HTTP port' do mock_app { get('/') { redirect '/foo' } } request = Rack::MockRequest.new(@app) response = request.get('/', 'SERVER_PORT' => '81') assert_equal 'http://example.org:81/foo', response['Location'] end it 'redirects using a non-standard HTTPS port' do mock_app { get('/') { redirect '/foo' } } request = Rack::MockRequest.new(@app) response = request.get('/', 'SERVER_PORT' => '444') assert_equal 'http://example.org:444/foo', response['Location'] end it 'uses 303 for post requests if request is HTTP 1.1' do mock_app { post('/') { redirect '/'} } post('/', {}, 'HTTP_VERSION' => 'HTTP/1.1') assert_equal 303, status assert_equal '', body assert_equal 'http://example.org/', response['Location'] end it 'uses 302 for post requests if request is HTTP 1.0' do mock_app { post('/') { redirect '/'} } post('/', {}, 'HTTP_VERSION' => 'HTTP/1.0') assert_equal 302, status assert_equal '', body assert_equal 'http://example.org/', response['Location'] end it 'works behind a reverse proxy' do mock_app { get('/') { redirect '/foo' } } request = Rack::MockRequest.new(@app) response = request.get('/', 'HTTP_X_FORWARDED_HOST' => 'example.com', 'SERVER_PORT' => '8080') assert_equal 'http://example.com/foo', response['Location'] end it 'accepts absolute URIs' do mock_app do get('/') do redirect 'http://google.com' fail 'redirect should halt' end end get '/' assert_equal 302, status assert_equal '', body assert_equal 'http://google.com', response['Location'] end it 'accepts absolute URIs with a different schema' do mock_app do get('/') do redirect 'mailto:jsmith@example.com' fail 'redirect should halt' end end get '/' assert_equal 302, status assert_equal '', body assert_equal 'mailto:jsmith@example.com', response['Location'] end it 'accepts a URI object instead of a String' do mock_app do get('/') { redirect URI.parse('http://sinatrarb.com') } end get '/' assert_equal 302, status assert_equal '', body assert_equal 'http://sinatrarb.com', response['Location'] end end describe 'error' do it 'sets a status code and halts' do mock_app do get('/') do error 501 fail 'error should halt' end end get '/' assert_equal 501, status assert_equal '', body end it 'takes an optional body' do mock_app do get('/') do error 501, 'FAIL' fail 'error should halt' end end get '/' assert_equal 501, status assert_equal 'FAIL', body end it 'should not invoke error handler when setting status inside an error handler' do mock_app do disable :raise_errors not_found do body "not_found handler" status 404 end error do body "error handler" status 404 end get '/' do raise end end get '/' assert_equal 404, status assert_equal 'error handler', body end it 'should not reset the content-type to html for error handlers' do mock_app do disable :raise_errors before { content_type "application/json" } not_found { JSON.dump("error" => "Not Found") } end get '/' assert_equal 404, status assert_equal 'application/json', response.content_type end it 'should not invoke error handler when halting with 500 inside an error handler' do mock_app do disable :raise_errors not_found do body "not_found handler" halt 404 end error do body "error handler" halt 404 end get '/' do raise end end get '/' assert_equal 404, status assert_equal 'error handler', body end it 'should not invoke not_found handler when halting with 404 inside a not found handler' do mock_app do disable :raise_errors not_found do body "not_found handler" halt 500 end error do body "error handler" halt 500 end end get '/' assert_equal 500, status assert_equal 'not_found handler', body end it 'uses a 500 status code when first argument is a body' do mock_app do get('/') do error 'FAIL' fail 'error should halt' end end get '/' assert_equal 500, status assert_equal 'FAIL', body end end describe 'not_found' do it 'halts with a 404 status' do mock_app do get('/') do not_found fail 'not_found should halt' end end get '/' assert_equal 404, status assert_equal '', body end it 'does not set a X-Cascade header' do mock_app do get('/') do not_found fail 'not_found should halt' end end get '/' assert_equal 404, status assert_nil response.headers['X-Cascade'] end end describe 'headers' do it 'sets headers on the response object when given a Hash' do mock_app do get('/') do headers 'X-Foo' => 'bar', 'X-Baz' => 'bling' 'kthx' end end get '/' assert ok? assert_equal 'bar', response['X-Foo'] assert_equal 'bling', response['X-Baz'] assert_equal 'kthx', body end it 'returns the response headers hash when no hash provided' do mock_app do get('/') do headers['X-Foo'] = 'bar' 'kthx' end end get '/' assert ok? assert_equal 'bar', response['X-Foo'] end end describe 'session' do it 'uses the existing rack.session' do mock_app do get('/') do session[:foo] end end get('/', {}, { 'rack.session' => { :foo => 'bar' } }) assert_equal 'bar', body end it 'creates a new session when none provided' do mock_app do enable :sessions get('/') do assert session[:foo].nil? session[:foo] = 'bar' redirect '/hi' end get('/hi') do "hi #{session[:foo]}" end end get '/' follow_redirect! assert_equal 'hi bar', body end it 'inserts session middleware' do mock_app do enable :sessions get('/') do assert env['rack.session'] assert env['rack.session.options'] 'ok' end end get '/' assert_body 'ok' end it 'sets a default session secret' do mock_app do enable :sessions get('/') do secret = env['rack.session.options'][:secret] assert secret assert_equal secret, settings.session_secret 'ok' end end get '/' assert_body 'ok' end it 'allows disabling session secret' do mock_app do enable :sessions disable :session_secret get('/') do assert !env['rack.session.options'].include?(:session_secret) 'ok' end end # Silence warnings since Rack::Session::Cookie complains about the non-present session secret silence_warnings do get '/' end assert_body 'ok' end it 'accepts an options hash' do mock_app do set :sessions, :foo => :bar get('/') do assert_equal env['rack.session.options'][:foo], :bar 'ok' end end get '/' assert_body 'ok' end end describe 'mime_type' do include Sinatra::Helpers it "looks up mime types in Rack's MIME registry" do Rack::Mime::MIME_TYPES['.foo'] = 'application/foo' assert_equal 'application/foo', mime_type('foo') assert_equal 'application/foo', mime_type('.foo') assert_equal 'application/foo', mime_type(:foo) end it 'returns nil when given nil' do assert mime_type(nil).nil? end it 'returns nil when media type not registered' do assert mime_type(:bizzle).nil? end it 'returns the argument when given a media type string' do assert_equal 'text/plain', mime_type('text/plain') end it 'turns AcceptEntry into String' do type = mime_type(Sinatra::Request::AcceptEntry.new('text/plain')) assert_equal String, type.class assert_equal 'text/plain', type end end test 'Base.mime_type registers mime type' do mock_app do mime_type :foo, 'application/foo' get('/') do "foo is #{mime_type(:foo)}" end end get '/' assert_equal 'foo is application/foo', body end describe 'content_type' do it 'sets the Content-Type header' do mock_app do get('/') do content_type 'text/plain' 'Hello World' end end get '/' assert_equal 'text/plain;charset=utf-8', response['Content-Type'] assert_equal 'Hello World', body end it 'takes media type parameters (like charset=)' do mock_app do get('/') do content_type 'text/html', :charset => 'latin1' "

Hello, World

" end end get '/' assert ok? assert_equal 'text/html;charset=latin1', response['Content-Type'] assert_equal "

Hello, World

", body end it "looks up symbols in Rack's mime types dictionary" do Rack::Mime::MIME_TYPES['.foo'] = 'application/foo' mock_app do get('/foo.xml') do content_type :foo "I AM FOO" end end get '/foo.xml' assert ok? assert_equal 'application/foo', response['Content-Type'] assert_equal 'I AM FOO', body end it 'fails when no mime type is registered for the argument provided' do mock_app do get('/foo.xml') do content_type :bizzle "I AM FOO" end end assert_raises(RuntimeError) { get '/foo.xml' } end it 'only sets default charset for specific mime types' do tests_ran = false mock_app do mime_type :foo, 'text/foo' mime_type :bar, 'application/bar' mime_type :baz, 'application/baz' add_charset << mime_type(:baz) get('/') do assert_equal content_type(:txt), 'text/plain;charset=utf-8' assert_equal content_type(:css), 'text/css;charset=utf-8' assert_equal content_type(:html), 'text/html;charset=utf-8' assert_equal content_type(:foo), 'text/foo;charset=utf-8' assert_equal content_type(:xml), 'application/xml;charset=utf-8' assert_equal content_type(:xhtml), 'application/xhtml+xml;charset=utf-8' assert_equal content_type(:js), 'application/javascript;charset=utf-8' assert_equal content_type(:json), 'application/json' assert_equal content_type(:bar), 'application/bar' assert_equal content_type(:png), 'image/png' assert_equal content_type(:baz), 'application/baz;charset=utf-8' tests_ran = true "done" end end get '/' assert tests_ran end it 'handles already present params' do mock_app do get('/') do content_type 'foo/bar;level=1', :charset => 'utf-8' 'ok' end end get '/' assert_equal 'foo/bar;level=1, charset=utf-8', response['Content-Type'] end it 'does not add charset if present' do mock_app do get('/') do content_type 'text/plain;charset=utf-16' 'ok' end end get '/' assert_equal 'text/plain;charset=utf-16', response['Content-Type'] end it 'properly encodes parameters with delimiter characters' do mock_app do before '/comma' do content_type 'image/png', :comment => 'Hello, world!' end before '/semicolon' do content_type 'image/png', :comment => 'semi;colon' end before '/quote' do content_type 'image/png', :comment => '"Whatever."' end get('*') { 'ok' } end get '/comma' assert_equal 'image/png;comment="Hello, world!"', response['Content-Type'] get '/semicolon' assert_equal 'image/png;comment="semi;colon"', response['Content-Type'] get '/quote' assert_equal 'image/png;comment="\"Whatever.\""', response['Content-Type'] end end describe 'attachment' do def attachment_app(filename=nil) mock_app do get('/attachment') do attachment filename response.write("") end end end it 'sets the Content-Type response header' do attachment_app('test.xml') get '/attachment' assert_equal 'application/xml;charset=utf-8', response['Content-Type'] assert_equal '', body end it 'sets the Content-Type response header without extname' do attachment_app('test') get '/attachment' assert_equal 'text/html;charset=utf-8', response['Content-Type'] assert_equal '', body end it 'sets the Content-Type response header with extname' do mock_app do get('/attachment') do content_type :atom attachment 'test.xml' response.write("") end end get '/attachment' assert_equal 'application/atom+xml', response['Content-Type'] assert_equal '', body end it 'escapes filename in the Content-Disposition header according to the multipart form data spec in WHATWG living standard' do mock_app do get('/attachment') do attachment "test.xml\";\r\next=.txt" response.write("") end end get '/attachment' assert_equal 'attachment; filename="test.xml%22;%0D%0Aext=.txt"', response['Content-Disposition'] assert_equal '', body end end describe 'send_file' do setup do @file = __dir__ + '/file.txt' File.open(@file, 'wb') { |io| io.write('Hello World') } end def teardown File.unlink @file @file = nil end def send_file_app(opts={}) path = @file mock_app { get '/file.txt' do send_file path, opts end } end it "sends the contents of the file" do send_file_app get '/file.txt' assert ok? assert_equal 'Hello World', body end it 'sets the Content-Type response header if a mime-type can be located' do send_file_app get '/file.txt' assert_equal 'text/plain;charset=utf-8', response['Content-Type'] end it 'sets the Content-Type response header if type option is set to a file extension' do send_file_app :type => 'html' get '/file.txt' assert_equal 'text/html;charset=utf-8', response['Content-Type'] end it 'sets the Content-Type response header if type option is set to a mime type' do send_file_app :type => 'application/octet-stream' get '/file.txt' assert_equal 'application/octet-stream', response['Content-Type'] end it 'sets the Content-Length response header' do send_file_app get '/file.txt' assert_equal 'Hello World'.length.to_s, response['Content-Length'] end it 'sets the Last-Modified response header' do send_file_app get '/file.txt' assert_equal File.mtime(@file).httpdate, response['Last-Modified'] end it 'allows passing in a different Last-Modified response header with :last_modified' do time = Time.now send_file_app :last_modified => time get '/file.txt' assert_equal time.httpdate, response['Last-Modified'] end it "returns a 404 when not found" do mock_app { get('/') { send_file 'this-file-does-not-exist.txt' } } get '/' assert not_found? end it "does not set the Content-Disposition header by default" do send_file_app get '/file.txt' assert_nil response['Content-Disposition'] end it "sets the Content-Disposition header when :disposition set to 'attachment'" do send_file_app :disposition => 'attachment' get '/file.txt' assert_equal 'attachment; filename="file.txt"', response['Content-Disposition'] end it "does not set add a file name if filename is false" do send_file_app :disposition => 'inline', :filename => false get '/file.txt' assert_equal 'inline', response['Content-Disposition'] end it "sets the Content-Disposition header when :disposition set to 'inline'" do send_file_app :disposition => 'inline' get '/file.txt' assert_equal 'inline; filename="file.txt"', response['Content-Disposition'] end it "does not raise an error when :disposition set to a frozen string" do send_file_app :disposition => 'inline'.freeze get '/file.txt' assert_equal 'inline; filename="file.txt"', response['Content-Disposition'] end it "sets the Content-Disposition header when :filename provided" do send_file_app :filename => 'foo.txt' get '/file.txt' assert_equal 'attachment; filename="foo.txt"', response['Content-Disposition'] end it 'allows setting a custom status code' do send_file_app :status => 201 get '/file.txt' assert_status 201 end it "is able to send files with unknown mime type" do @file = __dir__ + '/file.foobar' File.open(@file, 'wb') { |io| io.write('Hello World') } send_file_app get '/file.txt' assert_equal 'application/octet-stream', response['Content-Type'] end it "does not override Content-Type if already set and no explicit type is given" do path = @file mock_app do get('/') do content_type :png send_file path end end get '/' assert_equal 'image/png', response['Content-Type'] end it "does override Content-Type even if already set, if explicit type is given" do path = @file mock_app do get('/') do content_type :png send_file path, :type => :gif end end get '/' assert_equal 'image/gif', response['Content-Type'] end it 'can have :status option as a string' do path = @file mock_app do post '/' do send_file path, :status => '422' end end post '/' assert_equal response.status, 422 end end describe 'cache_control' do setup do mock_app do get('/foo') do cache_control :public, :no_cache, :max_age => 60.0 'Hello World' end get('/bar') do cache_control :public, :no_cache 'Hello World' end end end it 'sets the Cache-Control header' do get '/foo' assert_equal ['public', 'no-cache', 'max-age=60'], response['Cache-Control'].split(', ') end it 'last argument does not have to be a hash' do get '/bar' assert_equal ['public', 'no-cache'], response['Cache-Control'].split(', ') end end describe 'expires' do setup do mock_app do get('/foo') do expires 60, :public, :no_cache 'Hello World' end get('/bar') { expires Time.now } get('/baz') { expires Time.at(0) } get('/bah') { expires Time.at(0), :max_age => 20 } get('/blah') do obj = Object.new def obj.method_missing(*a, &b) 60.send(*a, &b) end def obj.is_a?(thing) 60.is_a?(thing) end expires obj, :public, :no_cache 'Hello World' end get('/boom') { expires '9999' } end end it 'sets the Cache-Control header' do get '/foo' assert_equal ['public', 'no-cache', 'max-age=60'], response['Cache-Control'].split(', ') end it 'sets the Expires header' do get '/foo' refute_nil response['Expires'] end it 'allows passing Time.now objects' do get '/bar' refute_nil response['Expires'] end it 'allows passing Time.at objects' do get '/baz' assert_equal 'Thu, 01 Jan 1970 00:00:00 GMT', response['Expires'] end it 'allows max_age to be specified separately' do get '/bah' assert_equal 'Thu, 01 Jan 1970 00:00:00 GMT', response['Expires'] assert_equal ['max-age=20'], response['Cache-Control'].split(', ') end it 'accepts values pretending to be a Numeric (like ActiveSupport::Duration)' do get '/blah' assert_equal ['public', 'no-cache', 'max-age=60'], response['Cache-Control'].split(', ') end it 'fails when Time.parse raises an ArgumentError' do assert_raises(ArgumentError) { get '/boom' } end end describe 'last_modified' do it 'ignores nil' do mock_app { get('/') { last_modified nil; 200; } } get '/' assert ! response['Last-Modified'] end it 'does not change a status other than 200' do mock_app do get('/') do status 299 last_modified Time.at(0) 'ok' end end get('/', {}, 'HTTP_IF_MODIFIED_SINCE' => 'Sun, 26 Sep 2030 23:43:52 GMT') assert_status 299 assert_body 'ok' end [Time.now, DateTime.now, Date.today, Time.now.to_i, Struct.new(:to_time).new(Time.now) ].each do |last_modified_time| describe "with #{last_modified_time.class.name}" do setup do mock_app do get('/') do last_modified last_modified_time 'Boo!' end end wrapper = Object.new.extend Sinatra::Helpers @last_modified_time = wrapper.time_for last_modified_time end # fixes strange missing test error when running complete test suite. it("does not complain about missing tests") { } context "when there's no If-Modified-Since header" do it 'sets the Last-Modified header to a valid RFC 2616 date value' do get '/' assert_equal @last_modified_time.httpdate, response['Last-Modified'] end it 'conditional GET misses and returns a body' do get '/' assert_equal 200, status assert_equal 'Boo!', body end end context "when there's an invalid If-Modified-Since header" do it 'sets the Last-Modified header to a valid RFC 2616 date value' do get('/', {}, { 'HTTP_IF_MODIFIED_SINCE' => 'a really weird date' }) assert_equal @last_modified_time.httpdate, response['Last-Modified'] end it 'conditional GET misses and returns a body' do get('/', {}, { 'HTTP_IF_MODIFIED_SINCE' => 'a really weird date' }) assert_equal 200, status assert_equal 'Boo!', body end end context "when the resource has been modified since the If-Modified-Since header date" do it 'sets the Last-Modified header to a valid RFC 2616 date value' do get('/', {}, { 'HTTP_IF_MODIFIED_SINCE' => (@last_modified_time - 1).httpdate }) assert_equal @last_modified_time.httpdate, response['Last-Modified'] end it 'conditional GET misses and returns a body' do get('/', {}, { 'HTTP_IF_MODIFIED_SINCE' => (@last_modified_time - 1).httpdate }) assert_equal 200, status assert_equal 'Boo!', body end it 'does not rely on string comparison' do mock_app do get('/compare') do last_modified "Mon, 18 Oct 2010 20:57:11 GMT" "foo" end end get('/compare', {}, { 'HTTP_IF_MODIFIED_SINCE' => 'Sun, 26 Sep 2010 23:43:52 GMT' }) assert_equal 200, status assert_equal 'foo', body get('/compare', {}, { 'HTTP_IF_MODIFIED_SINCE' => 'Sun, 26 Sep 2030 23:43:52 GMT' }) assert_equal 304, status assert_equal '', body end end context "when the resource has been modified on the exact If-Modified-Since header date" do it 'sets the Last-Modified header to a valid RFC 2616 date value' do get('/', {}, { 'HTTP_IF_MODIFIED_SINCE' => @last_modified_time.httpdate }) assert_equal @last_modified_time.httpdate, response['Last-Modified'] end it 'conditional GET matches and halts' do get( '/', {}, { 'HTTP_IF_MODIFIED_SINCE' => @last_modified_time.httpdate }) assert_equal 304, status assert_equal '', body end end context "when the resource hasn't been modified since the If-Modified-Since header date" do it 'sets the Last-Modified header to a valid RFC 2616 date value' do get('/', {}, { 'HTTP_IF_MODIFIED_SINCE' => (@last_modified_time + 1).httpdate }) assert_equal @last_modified_time.httpdate, response['Last-Modified'] end it 'conditional GET matches and halts' do get('/', {}, { 'HTTP_IF_MODIFIED_SINCE' => (@last_modified_time + 1).httpdate }) assert_equal 304, status assert_equal '', body end end context "If-Unmodified-Since" do it 'results in 200 if resource has not been modified' do get('/', {}, { 'HTTP_IF_UNMODIFIED_SINCE' => 'Sun, 26 Sep 2030 23:43:52 GMT' }) assert_equal 200, status assert_equal 'Boo!', body end it 'results in 412 if resource has been modified' do get('/', {}, { 'HTTP_IF_UNMODIFIED_SINCE' => Time.at(0).httpdate }) assert_equal 412, status assert_equal '', body end end end end end describe 'etag' do context "safe requests" do it 'returns 200 for normal requests' do mock_app do get('/') do etag 'foo' 'ok' end end get '/' assert_status 200 assert_body 'ok' end context "If-None-Match" do it 'returns 304 when If-None-Match is *' do mock_app do get('/') do etag 'foo' 'ok' end end get('/', {}, 'HTTP_IF_NONE_MATCH' => '*') assert_status 304 assert_body '' end it 'returns 200 when If-None-Match is * for new resources' do mock_app do get('/') do etag 'foo', :new_resource => true 'ok' end end get('/', {}, 'HTTP_IF_NONE_MATCH' => '*') assert_status 200 assert_body 'ok' end it 'returns 304 when If-None-Match is * for existing resources' do mock_app do get('/') do etag 'foo', :new_resource => false 'ok' end end get('/', {}, 'HTTP_IF_NONE_MATCH' => '*') assert_status 304 assert_body '' end it 'returns 304 when If-None-Match is the etag' do mock_app do get('/') do etag 'foo' 'ok' end end get('/', {}, 'HTTP_IF_NONE_MATCH' => '"foo"') assert_status 304 assert_body '' end it 'returns 304 when If-None-Match includes the etag' do mock_app do get('/') do etag 'foo' 'ok' end end get('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar", "foo"') assert_status 304 assert_body '' end it 'returns 200 when If-None-Match does not include the etag' do mock_app do get('/') do etag 'foo' 'ok' end end get('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar"') assert_status 200 assert_body 'ok' end it 'ignores If-Modified-Since if If-None-Match does not match' do mock_app do get('/') do etag 'foo' last_modified Time.at(0) 'ok' end end get('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar"') assert_status 200 assert_body 'ok' end it 'does not change a status code other than 2xx or 304' do mock_app do get('/') do status 499 etag 'foo' 'ok' end end get('/', {}, 'HTTP_IF_NONE_MATCH' => '"foo"') assert_status 499 assert_body 'ok' end it 'does change 2xx status codes' do mock_app do get('/') do status 299 etag 'foo' 'ok' end end get('/', {}, 'HTTP_IF_NONE_MATCH' => '"foo"') assert_status 304 assert_body '' end it 'does not send a body on 304 status codes' do mock_app do get('/') do status 304 etag 'foo' 'ok' end end get('/', {}, 'HTTP_IF_NONE_MATCH' => '"foo"') assert_status 304 assert_body '' end end context "If-Match" do it 'returns 200 when If-Match is the etag' do mock_app do get('/') do etag 'foo' 'ok' end end get('/', {}, 'HTTP_IF_MATCH' => '"foo"') assert_status 200 assert_body 'ok' end it 'returns 200 when If-Match includes the etag' do mock_app do get('/') do etag 'foo' 'ok' end end get('/', {}, 'HTTP_IF_MATCH' => '"foo", "bar"') assert_status 200 assert_body 'ok' end it 'returns 200 when If-Match is *' do mock_app do get('/') do etag 'foo' 'ok' end end get('/', {}, 'HTTP_IF_MATCH' => '*') assert_status 200 assert_body 'ok' end it 'returns 412 when If-Match is * for new resources' do mock_app do get('/') do etag 'foo', :new_resource => true 'ok' end end get('/', {}, 'HTTP_IF_MATCH' => '*') assert_status 412 assert_body '' end it 'returns 200 when If-Match is * for existing resources' do mock_app do get('/') do etag 'foo', :new_resource => false 'ok' end end get('/', {}, 'HTTP_IF_MATCH' => '*') assert_status 200 assert_body 'ok' end it 'returns 412 when If-Match does not include the etag' do mock_app do get('/') do etag 'foo' 'ok' end end get('/', {}, 'HTTP_IF_MATCH' => '"bar"') assert_status 412 assert_body '' end end end context "idempotent requests" do it 'returns 200 for normal requests' do mock_app do put('/') do etag 'foo' 'ok' end end put '/' assert_status 200 assert_body 'ok' end context "If-None-Match" do it 'returns 412 when If-None-Match is *' do mock_app do put('/') do etag 'foo' 'ok' end end put('/', {}, 'HTTP_IF_NONE_MATCH' => '*') assert_status 412 assert_body '' end it 'returns 200 when If-None-Match is * for new resources' do mock_app do put('/') do etag 'foo', :new_resource => true 'ok' end end put('/', {}, 'HTTP_IF_NONE_MATCH' => '*') assert_status 200 assert_body 'ok' end it 'returns 412 when If-None-Match is * for existing resources' do mock_app do put('/') do etag 'foo', :new_resource => false 'ok' end end put('/', {}, 'HTTP_IF_NONE_MATCH' => '*') assert_status 412 assert_body '' end it 'returns 412 when If-None-Match is the etag' do mock_app do put '/' do etag 'foo' 'ok' end end put('/', {}, 'HTTP_IF_NONE_MATCH' => '"foo"') assert_status 412 assert_body '' end it 'returns 412 when If-None-Match includes the etag' do mock_app do put('/') do etag 'foo' 'ok' end end put('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar", "foo"') assert_status 412 assert_body '' end it 'returns 200 when If-None-Match does not include the etag' do mock_app do put('/') do etag 'foo' 'ok' end end put('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar"') assert_status 200 assert_body 'ok' end it 'ignores If-Modified-Since if If-None-Match does not match' do mock_app do put('/') do etag 'foo' last_modified Time.at(0) 'ok' end end put('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar"') assert_status 200 assert_body 'ok' end end context "If-Match" do it 'returns 200 when If-Match is the etag' do mock_app do put('/') do etag 'foo' 'ok' end end put('/', {}, 'HTTP_IF_MATCH' => '"foo"') assert_status 200 assert_body 'ok' end it 'returns 200 when If-Match includes the etag' do mock_app do put('/') do etag 'foo' 'ok' end end put('/', {}, 'HTTP_IF_MATCH' => '"foo", "bar"') assert_status 200 assert_body 'ok' end it 'returns 200 when If-Match is *' do mock_app do put('/') do etag 'foo' 'ok' end end put('/', {}, 'HTTP_IF_MATCH' => '*') assert_status 200 assert_body 'ok' end it 'returns 412 when If-Match is * for new resources' do mock_app do put('/') do etag 'foo', :new_resource => true 'ok' end end put('/', {}, 'HTTP_IF_MATCH' => '*') assert_status 412 assert_body '' end it 'returns 200 when If-Match is * for existing resources' do mock_app do put('/') do etag 'foo', :new_resource => false 'ok' end end put('/', {}, 'HTTP_IF_MATCH' => '*') assert_status 200 assert_body 'ok' end it 'returns 412 when If-Match does not include the etag' do mock_app do put('/') do etag 'foo' 'ok' end end put('/', {}, 'HTTP_IF_MATCH' => '"bar"') assert_status 412 assert_body '' end end end context "post requests" do it 'returns 200 for normal requests' do mock_app do post('/') do etag 'foo' 'ok' end end post('/') assert_status 200 assert_body 'ok' end context "If-None-Match" do it 'returns 200 when If-None-Match is *' do mock_app do post('/') do etag 'foo' 'ok' end end post('/', {}, 'HTTP_IF_NONE_MATCH' => '*') assert_status 200 assert_body 'ok' end it 'returns 200 when If-None-Match is * for new resources' do mock_app do post('/') do etag 'foo', :new_resource => true 'ok' end end post('/', {}, 'HTTP_IF_NONE_MATCH' => '*') assert_status 200 assert_body 'ok' end it 'returns 412 when If-None-Match is * for existing resources' do mock_app do post('/') do etag 'foo', :new_resource => false 'ok' end end post('/', {}, 'HTTP_IF_NONE_MATCH' => '*') assert_status 412 assert_body '' end it 'returns 412 when If-None-Match is the etag' do mock_app do post('/') do etag 'foo' 'ok' end end post('/', {}, 'HTTP_IF_NONE_MATCH' => '"foo"') assert_status 412 assert_body '' end it 'returns 412 when If-None-Match includes the etag' do mock_app do post('/') do etag 'foo' 'ok' end end post('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar", "foo"') assert_status 412 assert_body '' end it 'returns 200 when If-None-Match does not include the etag' do mock_app do post('/') do etag 'foo' 'ok' end end post('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar"') assert_status 200 assert_body 'ok' end it 'ignores If-Modified-Since if If-None-Match does not match' do mock_app do post('/') do etag 'foo' last_modified Time.at(0) 'ok' end end post('/', {}, 'HTTP_IF_NONE_MATCH' => '"bar"') assert_status 200 assert_body 'ok' end end context "If-Match" do it 'returns 200 when If-Match is the etag' do mock_app do post('/') do etag 'foo' 'ok' end end post('/', {}, 'HTTP_IF_MATCH' => '"foo"') assert_status 200 assert_body 'ok' end it 'returns 200 when If-Match includes the etag' do mock_app do post('/') do etag 'foo' 'ok' end end post('/', {}, 'HTTP_IF_MATCH' => '"foo", "bar"') assert_status 200 assert_body 'ok' end it 'returns 412 when If-Match is *' do mock_app do post('/') do etag 'foo' 'ok' end end post('/', {}, 'HTTP_IF_MATCH' => '*') assert_status 412 assert_body '' end it 'returns 412 when If-Match is * for new resources' do mock_app do post('/') do etag 'foo', :new_resource => true 'ok' end end post('/', {}, 'HTTP_IF_MATCH' => '*') assert_status 412 assert_body '' end it 'returns 200 when If-Match is * for existing resources' do mock_app do post('/') do etag 'foo', :new_resource => false 'ok' end end post('/', {}, 'HTTP_IF_MATCH' => '*') assert_status 200 assert_body 'ok' end it 'returns 412 when If-Match does not include the etag' do mock_app do post('/') do etag 'foo' 'ok' end end post('/', {}, 'HTTP_IF_MATCH' => '"bar"') assert_status 412 assert_body '' end end end it 'uses a weak etag with the :weak option' do mock_app do get('/') do etag 'FOO', :weak "that's weak, dude." end end get '/' assert_equal 'W/"FOO"', response['ETag'] end it 'raises an ArgumentError for an invalid strength' do mock_app do get('/') do etag 'FOO', :w00t "that's weak, dude." end end assert_raises(ArgumentError) { get('/') } end end describe 'back' do it "makes redirecting back pretty" do mock_app { get('/foo') { redirect back } } get('/foo', {}, 'HTTP_REFERER' => 'http://github.com') assert redirect? assert_equal "http://github.com", response.location end end describe 'uri' do it 'generates absolute urls' do mock_app { get('/') { uri }} get '/' assert_equal 'http://example.org/', body end it 'includes path_info' do mock_app { get('/:name') { uri }} get '/foo' assert_equal 'http://example.org/foo', body end it 'allows passing an alternative to path_info' do mock_app { get('/:name') { uri '/bar' }} get '/foo' assert_equal 'http://example.org/bar', body end it 'includes script_name' do mock_app { get('/:name') { uri '/bar' }} get '/foo', {}, { "SCRIPT_NAME" => '/foo' } assert_equal 'http://example.org/foo/bar', body end it 'handles absolute URIs' do mock_app { get('/') { uri 'http://google.com' }} get '/' assert_equal 'http://google.com', body end it 'handles different protocols' do mock_app { get('/') { uri 'mailto:jsmith@example.com' }} get '/' assert_equal 'mailto:jsmith@example.com', body end it 'is aliased to #url' do mock_app { get('/') { url }} get '/' assert_equal 'http://example.org/', body end it 'is aliased to #to' do mock_app { get('/') { to }} get '/' assert_equal 'http://example.org/', body end it 'is case-insensitive' do mock_app { get('/:foo') { uri params[:foo] }} assert_equal get('HtTP://google.com').body, get('http://google.com').body end it 'generates relative link for invalid path' do mock_app { get('/') { uri 'htt^p://google.com' }} get '/' assert_equal 'http://example.org/htt^p://google.com', body end end describe 'logger' do it 'logging works when logging is enabled' do mock_app do enable :logging get('/') do logger.info "Program started" logger.warn "Nothing to do!" end end io = StringIO.new get '/', {}, 'rack.errors' => io assert io.string.include?("INFO -- : Program started") assert io.string.include?("WARN -- : Nothing to do") end it 'logging works when logging is disable, but no output is produced' do mock_app do disable :logging get('/') do logger.info "Program started" logger.warn "Nothing to do!" end end io = StringIO.new get '/', {}, 'rack.errors' => io assert !io.string.include?("INFO -- : Program started") assert !io.string.include?("WARN -- : Nothing to do") end it 'does not create a logger when logging is set to nil' do mock_app do set :logging, nil get('/') { logger.inspect } end get '/' assert_body 'nil' end end module ::HelperOne; def one; '1'; end; end module ::HelperTwo; def two; '2'; end; end describe 'Adding new helpers' do it 'takes a list of modules to mix into the app' do mock_app do helpers ::HelperOne, ::HelperTwo get('/one') { one } get('/two') { two } end get '/one' assert_equal '1', body get '/two' assert_equal '2', body end it 'takes a block to mix into the app' do mock_app do helpers do def foo 'foo' end end get('/') { foo } end get '/' assert_equal 'foo', body end it 'evaluates the block in class context so that methods can be aliased' do mock_app do helpers { alias_method :h, :escape_html } get('/') { h('42 < 43') } end get '/' assert ok? assert_equal '42 < 43', body end it 'prepends modules so previously-defined methods can be overridden consistently' do skip <<-EOS This test will be helpful after switching #helpers's code from Module#include to Module#prepend See more details: https://github.com/sinatra/sinatra/pull/1214 EOS mock_app do helpers do def one; nil end def two; nil end end helpers ::HelperOne do def two; '2' end end get('/one') { one } get('/two') { two } end get '/one' assert_equal '1', body get '/two' assert_equal '2', body end module HelpersOverloadingBaseHelper def my_test 'BaseHelper#test' end end class HelpersOverloadingIncludeAndOverride < Sinatra::Base helpers HelpersOverloadingBaseHelper get '/' do my_test end helpers do def my_test 'InlineHelper#test' end end end it 'uses overloaded inline helper' do mock_app(HelpersOverloadingIncludeAndOverride) get '/' assert ok? assert_equal 'InlineHelper#test', body end module HelperWithIncluded def self.included(base) base.extend(ClassMethods) end module ClassMethods def nickname(name) # do something. end end end class ServerApp < Sinatra::Base helpers HelperWithIncluded # `nickname` method should be available. end it 'calls included method of helpers' do assert ServerApp.respond_to?(:nickname) end end end sinatra-3.0.5/test/indifferent_hash_test.rb000066400000000000000000000207311434717561400210420ustar00rootroot00000000000000# frozen_string_literal: true # # We don't need the full test helper for this standalone class. # require 'minitest/autorun' unless defined?(Minitest) require_relative '../lib/sinatra/indifferent_hash' class TestIndifferentHashBasics < Minitest::Test def test_flattened_constructor hash = Sinatra::IndifferentHash[:a, 1, ?b, 2] assert_equal 1, hash[?a] assert_equal 2, hash[?b] end def test_pairs_constructor hash = Sinatra::IndifferentHash[[[:a, 1], [?b, 2]]] assert_equal 1, hash[?a] assert_equal 2, hash[?b] end def test_default_block hash = Sinatra::IndifferentHash.new { |h, k| h[k] = k.upcase } assert_nil hash.default assert_equal ?A, hash.default(:a) end def test_default_object hash = Sinatra::IndifferentHash.new(:a=>1, ?b=>2) assert_equal({ ?a=>1, ?b=>2 }, hash.default) assert_equal({ ?a=>1, ?b=>2 }, hash[:a]) end def test_default_assignment hash = Sinatra::IndifferentHash.new hash.default = { :a=>1, ?b=>2 } assert_equal({ ?a=>1, ?b=>2 }, hash.default) assert_equal({ ?a=>1, ?b=>2 }, hash[:a]) end def test_assignment hash = Sinatra::IndifferentHash.new hash[:a] = :a hash[?b] = :b hash[3] = 3 hash[:simple_nested] = { :a=>:a, ?b=>:b } assert_equal :a, hash[?a] assert_equal :b, hash[?b] assert_equal 3, hash[3] assert_equal({ ?a=>:a, ?b=>:b }, hash['simple_nested']) assert_nil hash[?d] end def test_merge! # merge! is already mostly tested by the different constructors, so we # really just need to test the block form here hash = Sinatra::IndifferentHash[:a=>'a', ?b=>'b', 3=>3] hash.merge!(?a=>'A', :b=>'B', :d=>'D') do |key, oldval, newval| "#{oldval}*#{key}*#{newval}" end assert_equal({ ?a=>'a*a*A', ?b=>'b*b*B', 3=>3, ?d=>'D' }, hash) end end class TestIndifferentHash < Minitest::Test def setup @hash = Sinatra::IndifferentHash[:a=>:a, ?b=>:b, 3=>3, :simple_nested=>{ :a=>:a, ?b=>:b }, :nested=>{ :a=>[{ :a=>:a, ?b=>:b }, :c, 4], ?f=>:f, 7=>7 } ] end def test_hash_constructor assert_equal :a, @hash[?a] assert_equal :b, @hash[?b] assert_equal 3, @hash[3] assert_equal({ ?a=>:a, ?b=>:b }, @hash['nested'][?a][0]) assert_equal :c, @hash['nested'][?a][1] assert_equal 4, @hash['nested'][?a][2] assert_equal :f, @hash['nested'][?f] assert_equal 7, @hash['nested'][7] assert_equal :a, @hash['simple_nested'][?a] assert_equal :b, @hash['simple_nested'][?b] assert_nil @hash[?d] end def test_assoc assert_nil @hash.assoc(:d) assert_equal [?a, :a], @hash.assoc(:a) assert_equal [?b, :b], @hash.assoc(:b) end def test_rassoc assert_nil @hash.rassoc(:d) assert_equal [?a, :a], @hash.rassoc(:a) assert_equal [?b, :b], @hash.rassoc(:b) assert_equal ['simple_nested', { ?a=>:a, ?b=>:b }], @hash.rassoc(:a=>:a, ?b=>:b) end def test_fetch assert_raises(KeyError) { @hash.fetch(:d) } assert_equal 1, @hash.fetch(:d, 1) assert_equal 2, @hash.fetch(:d) { 2 } assert_equal ?d, @hash.fetch(:d) { |k| k } assert_equal :a, @hash.fetch(:a, 1) assert_equal :a, @hash.fetch(:a) { 2 } end def test_symbolic_retrieval assert_equal :a, @hash[:a] assert_equal :b, @hash[:b] assert_equal({ ?a=>:a, ?b=>:b }, @hash[:nested][:a][0]) assert_equal :c, @hash[:nested][:a][1] assert_equal 4, @hash[:nested][:a][2] assert_equal :f, @hash[:nested][:f] assert_equal 7, @hash[:nested][7] assert_equal :a, @hash[:simple_nested][:a] assert_equal :b, @hash[:simple_nested][:b] assert_nil @hash[:d] end def test_key assert_nil @hash.key(:d) assert_equal ?a, @hash.key(:a) assert_equal 'simple_nested', @hash.key(:a=>:a, ?b=>:b) end def test_key? assert_operator @hash, :key?, :a assert_operator @hash, :key?, ?b assert_operator @hash, :key?, 3 refute_operator @hash, :key?, :d end def test_value? assert_operator @hash, :value?, :a assert_operator @hash, :value?, :b assert_operator @hash, :value?, 3 assert_operator @hash, :value?, { :a=>:a, ?b=>:b } refute_operator @hash, :value?, :d end def test_delete @hash.delete(:a) @hash.delete(?b) assert_nil @hash[:a] assert_nil @hash[?b] end def test_dig assert_equal :a, @hash.dig(:a) assert_equal :b, @hash.dig(?b) assert_nil @hash.dig(:d) assert_equal :a, @hash.dig(:simple_nested, :a) assert_equal :b, @hash.dig('simple_nested', ?b) assert_nil @hash.dig('simple_nested', :d) assert_equal :a, @hash.dig(:nested, :a, 0, :a) assert_equal :b, @hash.dig('nested', ?a, 0, ?b) assert_nil @hash.dig('nested', ?a, 0, :d) end def test_slice assert_equal Sinatra::IndifferentHash[a: :a], @hash.slice(:a) assert_equal Sinatra::IndifferentHash[b: :b], @hash.slice(?b) assert_equal Sinatra::IndifferentHash[3 => 3], @hash.slice(3) assert_equal Sinatra::IndifferentHash.new, @hash.slice(:d) assert_equal Sinatra::IndifferentHash[a: :a, b: :b, 3 => 3], @hash.slice(:a, :b, 3) assert_equal Sinatra::IndifferentHash[simple_nested: { a: :a, ?b => :b }], @hash.slice(:simple_nested) assert_equal Sinatra::IndifferentHash[nested: { a: [{ a: :a, ?b => :b }, :c, 4], ?f => :f, 7 => 7 }], @hash.slice(:nested) end def test_fetch_values assert_raises(KeyError) { @hash.fetch_values(3, :d) } assert_equal [:a, :b, 3, ?D], @hash.fetch_values(:a, ?b, 3, :d) { |k| k.upcase } end def test_values_at assert_equal [:a, :b, 3, nil], @hash.values_at(:a, ?b, 3, :d) end def test_merge # merge just calls merge!, which is already thoroughly tested hash2 = @hash.merge(?a=>1, :q=>2) { |key, oldval, newval| "#{oldval}*#{key}*#{newval}" } refute_equal @hash, hash2 assert_equal 'a*a*1', hash2[:a] assert_equal 2, hash2[?q] end def test_merge_with_multiple_argument hash = Sinatra::IndifferentHash.new.merge({a: 1}, {b: 2}, {c: 3}) assert_equal 1, hash[?a] assert_equal 2, hash[?b] assert_equal 3, hash[?c] hash2 = Sinatra::IndifferentHash[d: 4] hash3 = {e: 5} hash.merge!(hash2, hash3) assert_equal 4, hash[?d] assert_equal 5, hash[?e] end def test_replace @hash.replace(?a=>1, :q=>2) assert_equal({ ?a=>1, ?q=>2 }, @hash) end def test_transform_values! @hash.transform_values! { |v| v.is_a?(Hash) ? Hash[v.to_a] : v } assert_instance_of Sinatra::IndifferentHash, @hash[:simple_nested] end def test_transform_values hash2 = @hash.transform_values { |v| v.respond_to?(:upcase) ? v.upcase : v } refute_equal @hash, hash2 assert_equal :A, hash2[:a] assert_equal :A, hash2[?a] end def test_transform_keys! @hash.transform_keys! { |k| k.respond_to?(:to_sym) ? k.to_sym : k } assert_equal :a, @hash[:a] assert_equal :a, @hash[?a] end def test_transform_keys hash2 = @hash.transform_keys { |k| k.respond_to?(:upcase) ? k.upcase : k } refute_equal @hash, hash2 refute_operator hash2, :key?, :a refute_operator hash2, :key?, ?a assert_equal :a, hash2[:A] assert_equal :a, hash2[?A] end def test_select hash = @hash.select { |k, v| v == :a } assert_equal Sinatra::IndifferentHash[a: :a], hash assert_instance_of Sinatra::IndifferentHash, hash hash2 = @hash.select { |k, v| true } assert_equal @hash, hash2 assert_instance_of Sinatra::IndifferentHash, hash2 enum = @hash.select assert_instance_of Enumerator, enum end def test_select! @hash.select! { |k, v| v == :a } assert_equal Sinatra::IndifferentHash[a: :a], @hash end def test_reject hash = @hash.reject { |k, v| v != :a } assert_equal Sinatra::IndifferentHash[a: :a], hash assert_instance_of Sinatra::IndifferentHash, hash hash2 = @hash.reject { |k, v| false } assert_equal @hash, hash2 assert_instance_of Sinatra::IndifferentHash, hash2 enum = @hash.reject assert_instance_of Enumerator, enum end def test_reject! @hash.reject! { |k, v| v != :a } assert_equal Sinatra::IndifferentHash[a: :a], @hash end def test_compact hash_with_nil_values = @hash.merge({?z => nil}) compacted_hash = hash_with_nil_values.compact assert_equal @hash, compacted_hash assert_instance_of Sinatra::IndifferentHash, compacted_hash empty_hash = Sinatra::IndifferentHash.new compacted_hash = empty_hash.compact assert_equal empty_hash, compacted_hash non_empty_hash = Sinatra::IndifferentHash[a: :a] compacted_hash = non_empty_hash.compact assert_equal non_empty_hash, compacted_hash end end sinatra-3.0.5/test/integration/000077500000000000000000000000001434717561400164765ustar00rootroot00000000000000sinatra-3.0.5/test/integration/app.rb000066400000000000000000000024241434717561400176050ustar00rootroot00000000000000$stderr.puts "loading" require 'sinatra' require_relative 'rainbows' if RUBY_ENGINE == 'ruby' configure do set :foo, :bar end get '/app_file' do content_type :txt settings.app_file end get '/ping' do 'pong' end get '/stream' do stream do |out| sleep 0.1 out << "a" sleep 1.2 out << "b" end end get '/mainonly' do object = Object.new begin object.send(:get, '/foo') { } 'false' rescue NameError 'true' end end set :out, nil get '/async' do stream(:keep_open) { |o| (settings.out = o) << "hi!" } end get '/send' do settings.out << params[:msg] if params[:msg] settings.out.close if params[:close] "ok" end get '/send_file' do file = File.expand_path '../views/a/in_a.str', __dir__ send_file file end get '/streaming' do headers['Content-Length'] = '46' stream do |out| out << "It's gonna be legen -\n" sleep 0.5 out << " (wait for it) \n" puts headers sleep 1 out << "- dary!\n" end end class Subclass < Sinatra::Base set :out, nil get '/subclass/async' do stream(:keep_open) { |o| (settings.out = o) << "hi!" } end get '/subclass/send' do settings.out << params[:msg] if params[:msg] settings.out.close if params[:close] "ok" end end use Subclass $stderr.puts "starting" sinatra-3.0.5/test/integration/rainbows.conf000066400000000000000000000000451434717561400211700ustar00rootroot00000000000000Rainbows! do use :EventMachine end sinatra-3.0.5/test/integration/rainbows.rb000066400000000000000000000007341434717561400206530ustar00rootroot00000000000000require 'rainbows' module Rack module Handler class Rainbows def self.run(app, **options) rainbows_options = { listeners: ["#{options[:Host]}:#{options[:Port]}"], worker_processes: 1, timeout: 30, config_file: ::File.expand_path('rainbows.conf', __dir__), } ::Rainbows::HttpServer.new(app, rainbows_options).start.join end end register :rainbows, ::Rack::Handler::Rainbows end end sinatra-3.0.5/test/integration_async_helper.rb000066400000000000000000000006551434717561400215650ustar00rootroot00000000000000require File.expand_path('integration_helper', __dir__) module IntegrationAsyncHelper def it(message, &block) base_port = 5100 + Process.pid % 100 %w(rainbows).each_with_index do |server_name, index| server = IntegrationHelper::BaseServer.new(server_name, base_port + index) next unless server.installed? super("with #{server.name}: #{message}") { server.run_test(self, &block) } end end end sinatra-3.0.5/test/integration_async_test.rb000066400000000000000000000020671434717561400212640ustar00rootroot00000000000000require File.expand_path('helper', __dir__) require File.expand_path('integration_async_helper', __dir__) # These tests are like integration_test, but they test asynchronous streaming. class IntegrationAsyncTest < Minitest::Test extend IntegrationAsyncHelper attr_accessor :server it 'streams async' do Timeout.timeout(3) do chunks = [] server.get_stream '/async' do |chunk| next if chunk.empty? chunks << chunk case chunk when "hi!" then server.get "/send?msg=hello" when "hello" then server.get "/send?close=1" end end assert_equal ['hi!', 'hello'], chunks end end it 'streams async from subclass' do Timeout.timeout(3) do chunks = [] server.get_stream '/subclass/async' do |chunk| next if chunk.empty? chunks << chunk case chunk when "hi!" then server.get "/subclass/send?msg=hello" when "hello" then server.get "/subclass/send?close=1" end end assert_equal ['hi!', 'hello'], chunks end end end sinatra-3.0.5/test/integration_helper.rb000066400000000000000000000056171434717561400203730ustar00rootroot00000000000000require 'sinatra/base' require 'rbconfig' require 'open-uri' require 'sinatra/runner' module IntegrationHelper class BaseServer < Sinatra::Runner extend Enumerable attr_accessor :server, :port alias name server def self.all @all ||= [] end def self.each(&block) all.each(&block) end def self.run(server, port) new(server, port).run end def app_file File.expand_path('integration/app.rb', __dir__) end def environment "development" end def initialize(server, port) @installed, @pipe, @server, @port = nil, nil, server, port Server.all << self end def run return unless installed? kill @log = "" super at_exit { kill } end def installed? return @installed unless @installed.nil? s = server == 'HTTP' ? 'net/http/server' : server require s @installed = true rescue LoadError warn "#{server} is not installed, skipping integration tests" @installed = false end def command @command ||= begin cmd = ["APP_ENV=#{environment}", "exec"] if RbConfig.respond_to? :ruby cmd << RbConfig.ruby.inspect else file, dir = RbConfig::CONFIG.values_at('ruby_install_name', 'bindir') cmd << File.expand_path(file, dir).inspect end cmd << "-w" unless net_http_server? cmd << "-I" << File.expand_path('../lib', __dir__).inspect cmd << app_file.inspect << '-s' << server << '-o' << '127.0.0.1' << '-p' << port cmd << "-e" << environment.to_s << '2>&1' cmd.join " " end end def webrick? name.to_s == "webrick" end def rainbows? name.to_s == "rainbows" end def puma? name.to_s == "puma" end def falcon? name.to_s == "falcon" end def trinidad? name.to_s == "trinidad" end def net_http_server? name.to_s == 'HTTP' end def warnings log.scan(%r[(?:\(eval|lib/sinatra).*warning:.*$]) end def run_test(target, &block) retries ||= 3 target.server = self run unless alive? target.instance_eval(&block) rescue Exception => error retries -= 1 kill retries < 0 ? retry : raise(error) end end Server = BaseServer def it(message, &block) Server.each do |server| next unless server.installed? super("with #{server.name}: #{message}") { server.run_test(self, &block) } end end def self.extend_object(obj) super base_port = 5000 + Process.pid % 100 servers = Sinatra::Base.server.dup # TruffleRuby doesn't support `Fiber.set_scheduler` yet unless Fiber.respond_to?(:set_scheduler) warn "skip falcon server" servers.delete('falcon') end servers.each_with_index do |server, index| Server.run(server, base_port+index) end end end sinatra-3.0.5/test/integration_test.rb000066400000000000000000000041241434717561400200630ustar00rootroot00000000000000require File.expand_path('helper', __dir__) require File.expand_path('integration_helper', __dir__) # These tests start a real server and talk to it over TCP. # Every test runs with every detected server. # # See test/integration/app.rb for the code of the app we test against. class IntegrationTest < Minitest::Test extend IntegrationHelper attr_accessor :server it('sets the app_file') { assert_equal server.app_file, server.get("/app_file") } it('only extends main') { assert_equal "true", server.get("/mainonly") } it 'logs once in development mode' do next if server.puma? or server.falcon? or server.rainbows? or RUBY_ENGINE == 'jruby' random = "%064x" % Kernel.rand(2**256-1) server.get "/ping?x=#{random}" count = server.log.scan("GET /ping?x=#{random}").count if server.net_http_server? assert_equal 0, count elsif server.webrick? assert(count > 0) else assert_equal(1, count) end end it 'streams' do next if server.webrick? or server.trinidad? times, chunks = [Time.now], [] server.get_stream do |chunk| next if chunk.empty? chunks << chunk times << Time.now end assert_equal ["a", "b"], chunks assert times[1] - times[0] < 1 assert times[2] - times[1] > 1 end it 'starts the correct server' do exp = %r{ ==\sSinatra\s\(v#{Sinatra::VERSION}\)\s has\staken\sthe\sstage\son\s\d+\sfor\sdevelopment\s with\sbackup\sfrom\s#{server} }ix # because Net HTTP Server logs to $stderr by default assert_match exp, server.log unless server.net_http_server? || server.rainbows? end it 'does not generate warnings' do assert_raises(OpenURI::HTTPError) { server.get '/' } server.get '/app_file' assert_equal [], server.warnings end it 'sets the Content-Length response header when sending files' do response = server.get_response '/send_file' assert response['Content-Length'] end it "doesn't ignore Content-Length header when streaming" do response = server.get_response '/streaming' assert_equal '46', response['Content-Length'] end end sinatra-3.0.5/test/liquid_test.rb000066400000000000000000000036171434717561400170350ustar00rootroot00000000000000require File.expand_path('helper', __dir__) begin require 'liquid' class LiquidTest < Minitest::Test def liquid_app(&block) mock_app do set :views, __dir__ + '/views' get('/', &block) end get '/' end it 'renders inline liquid strings' do liquid_app { liquid '

Hiya

' } assert ok? assert_equal "

Hiya

", body end it 'renders .liquid files in views path' do liquid_app { liquid :hello } assert ok? assert_equal "

Hello From Liquid

\n", body end it "renders with inline layouts" do mock_app do layout { "

THIS. IS. {{ yield }}

" } get('/') { liquid 'SPARTA' } end get '/' assert ok? assert_equal "

THIS. IS. SPARTA

", body end it "renders with file layouts" do liquid_app { liquid 'Hello World', :layout => :layout2 } assert ok? assert_equal "

Liquid Layout!

\n

Hello World

\n", body end it "raises error if template not found" do mock_app { get('/') { liquid :no_such_template } } assert_raises(Errno::ENOENT) { get('/') } end it "allows passing locals" do liquid_app { liquid '{{ value }}', :locals => { :value => 'foo' } } assert ok? assert_equal 'foo', body end it "can render truly nested layouts by accepting a layout and a block with the contents" do mock_app do template(:main_outer_layout) { "

Title

\n{{ yield }}" } template(:an_inner_layout) { "

Subtitle

\n{{ yield }}" } template(:a_page) { "

Contents.

\n" } get('/') do liquid :main_outer_layout, :layout => false do liquid :an_inner_layout do liquid :a_page end end end end get '/' assert ok? assert_body "

Title

\n

Subtitle

\n

Contents.

\n" end end rescue LoadError warn "#{$!}: skipping liquid tests" end sinatra-3.0.5/test/mapped_error_test.rb000066400000000000000000000161621434717561400202240ustar00rootroot00000000000000require File.expand_path('helper', __dir__) class FooError < RuntimeError end class FooNotFound < Sinatra::NotFound end class FooSpecialError < Sinatra::Error def http_status; 501 end end class FooStatusOutOfRangeError < Sinatra::Error def code; 4000 end end class FooWithCode < Sinatra::Error def code; 419 end end class FirstError < RuntimeError; end class SecondError < RuntimeError; end class MappedErrorTest < Minitest::Test def test_default assert true end describe 'Exception Mappings' do it 'invokes handlers registered with ::error when raised' do mock_app do set :raise_errors, false error(FooError) { 'Foo!' } get('/') { raise FooError } end get '/' assert_equal 500, status assert_equal 'Foo!', body end it 'passes the exception object to the error handler' do mock_app do set :raise_errors, false error(FooError) { |e| assert_equal(FooError, e.class) } get('/') { raise FooError } end get('/') end it 'uses the Exception handler if no matching handler found' do mock_app do set :raise_errors, false error(Exception) { 'Exception!' } get('/') { raise FooError } end get '/' assert_equal 500, status assert_equal 'Exception!', body end it 'walks down inheritance chain for errors' do mock_app do set :raise_errors, false error(RuntimeError) { 'Exception!' } get('/') { raise FooError } end get '/' assert_equal 500, status assert_equal 'Exception!', body end it 'favors subclass handler over superclass handler if available' do mock_app do set :raise_errors, false error(Exception) { 'Exception!' } error(FooError) { 'FooError!' } error(RuntimeError) { 'Exception!' } get('/') { raise FooError } end get '/' assert_equal 500, status assert_equal 'FooError!', body end it "sets env['sinatra.error'] to the rescued exception" do mock_app do set :raise_errors, false error(FooError) do assert env.include?('sinatra.error') assert env['sinatra.error'].kind_of?(FooError) 'looks good' end get('/') { raise FooError } end get '/' assert_equal 'looks good', body end it "raises errors from the app when raise_errors set and no handler defined" do mock_app do set :raise_errors, true get('/') { raise FooError } end assert_raises(FooError) { get '/' } end it "calls error handlers before raising errors even when raise_errors is set" do mock_app do set :raise_errors, true error(FooError) { "she's there." } get('/') { raise FooError } end get '/' assert_equal 500, status end it "never raises Sinatra::NotFound beyond the application" do mock_app(Sinatra::Application) do get('/') { raise Sinatra::NotFound } end get '/' assert_equal 404, status end it "cascades for subclasses of Sinatra::NotFound" do mock_app do set :raise_errors, true error(FooNotFound) { "foo! not found." } get('/') { raise FooNotFound } end get '/' assert_equal 404, status assert_equal 'foo! not found.', body end it 'has a not_found method for backwards compatibility' do mock_app { not_found { "Lost, are we?" } } get '/test' assert_equal 404, status assert_equal "Lost, are we?", body end it 'inherits error mappings from base class' do base = Class.new(Sinatra::Base) base.error(FooError) { 'base class' } mock_app(base) do set :raise_errors, false get('/') { raise FooError } end get '/' assert_equal 'base class', body end it 'overrides error mappings in base class' do base = Class.new(Sinatra::Base) base.error(FooError) { 'base class' } mock_app(base) do set :raise_errors, false error(FooError) { 'subclass' } get('/') { raise FooError } end get '/' assert_equal 'subclass', body end it 'honors Exception#http_status if present' do mock_app do set :raise_errors, false error(501) { 'Foo!' } get('/') { raise FooSpecialError } end get '/' assert_equal 501, status assert_equal 'Foo!', body end it 'does not use Exception#code by default' do mock_app do set :raise_errors, false get('/') { raise FooWithCode } end get '/' assert_equal 500, status end it 'uses Exception#code if use_code is enabled' do mock_app do set :raise_errors, false set :use_code, true get('/') { raise FooWithCode } end get '/' assert_equal 419, status end it 'does not rely on Exception#code for invalid codes' do mock_app do set :raise_errors, false set :use_code, true get('/') { raise FooStatusOutOfRangeError } end get '/' assert_equal 500, status end it "allows a stack of exception_handlers" do mock_app do set :raise_errors, false error(FirstError) { 'First!' } error(SecondError) { 'Second!' } get('/'){ raise SecondError } end get '/' assert_equal 500, status assert_equal 'Second!', body end it "allows an exception handler to pass control to the next exception handler" do mock_app do set :raise_errors, false error(500, FirstError) { 'First!' } error(500, SecondError) { pass } get('/') { raise 500 } end get '/' assert_equal 500, status assert_equal 'First!', body end it "allows an exception handler to handle the exception" do mock_app do set :raise_errors, false error(500, FirstError) { 'First!' } error(500, SecondError) { 'Second!' } get('/') { raise 500 } end get '/' assert_equal 500, status assert_equal 'Second!', body end end describe 'Custom Error Pages' do it 'allows numeric status code mappings to be registered with ::error' do mock_app do set :raise_errors, false error(500) { 'Foo!' } get('/') { [500, {}, 'Internal Foo Error'] } end get '/' assert_equal 500, status assert_equal 'Foo!', body end it 'allows ranges of status code mappings to be registered with :error' do mock_app do set :raise_errors, false error(500..550) { "Error: #{response.status}" } get('/') { [507, {}, 'A very special error'] } end get '/' assert_equal 507, status assert_equal 'Error: 507', body end it 'allows passing more than one range' do mock_app do set :raise_errors, false error(409..411, 503..509) { "Error: #{response.status}" } get('/') { [507, {}, 'A very special error'] } end get '/' assert_equal 507, status assert_equal 'Error: 507', body end end end sinatra-3.0.5/test/markaby_test.rb000066400000000000000000000036771434717561400172020ustar00rootroot00000000000000require File.expand_path('helper', __dir__) begin require 'markaby' class MarkabyTest < Minitest::Test def markaby_app(&block) mock_app do set :views, __dir__ + '/views' get('/', &block) end get '/' end it 'renders inline markaby strings' do markaby_app { markaby 'h1 "Hiya"' } assert ok? assert_equal "

Hiya

", body end it 'renders .markaby files in views path' do markaby_app { markaby :hello } assert ok? assert_equal "

Hello From Markaby

", body end it "renders with inline layouts" do mock_app do layout { 'h1 { text "THIS. IS. "; yield }' } get('/') { markaby 'em "SPARTA"' } end get '/' assert ok? assert_equal "

THIS. IS. SPARTA

", body end it "renders with file layouts" do markaby_app { markaby 'text "Hello World"', :layout => :layout2 } assert ok? assert_equal "

Markaby Layout!

Hello World

", body end it 'renders inline markaby blocks' do markaby_app { markaby { h1 'Hiya' } } assert ok? assert_equal "

Hiya

", body end it 'renders inline markaby blocks with inline layouts' do markaby_app do settings.layout { 'h1 { text "THIS. IS. "; yield }' } markaby { em 'SPARTA' } end assert ok? assert_equal "

THIS. IS. SPARTA

", body end it 'renders inline markaby blocks with file layouts' do markaby_app { markaby(:layout => :layout2) { text "Hello World" } } assert ok? assert_equal "

Markaby Layout!

Hello World

", body end it "raises error if template not found" do mock_app { get('/') { markaby :no_such_template } } assert_raises(Errno::ENOENT) { get('/') } end it "allows passing locals" do markaby_app { markaby 'text value', :locals => { :value => 'foo' } } assert ok? assert_equal 'foo', body end end rescue LoadError warn "#{$!}: skipping markaby tests" end sinatra-3.0.5/test/markdown_test.rb000066400000000000000000000042271434717561400173660ustar00rootroot00000000000000require File.expand_path('helper', __dir__) MarkdownTest = proc do def markdown_app(&block) mock_app do set :views, __dir__ + '/views' get('/', &block) end get '/' end def setup Tilt.prefer engine, 'markdown', 'mkd', 'md' super end it 'uses the correct engine' do assert_equal engine, Tilt[:md] assert_equal engine, Tilt[:mkd] assert_equal engine, Tilt[:markdown] end it 'renders inline markdown strings' do markdown_app { markdown '# Hiya' } assert ok? assert_like "

Hiya

\n", body end it 'renders .markdown files in views path' do markdown_app { markdown :hello } assert ok? assert_like "

Hello From Markdown

", body end it "raises error if template not found" do mock_app { get('/') { markdown :no_such_template } } assert_raises(Errno::ENOENT) { get('/') } end it "renders with inline layouts" do mock_app do layout { 'THIS. IS. #{yield.upcase}!' } get('/') { markdown 'Sparta', :layout_engine => :str } end get '/' assert ok? assert_like 'THIS. IS.

SPARTA

!', body end it "renders with file layouts" do markdown_app { markdown 'Hello World', :layout => :layout2, :layout_engine => :erb } assert ok? assert_body "ERB Layout!\n

Hello World

" end it "can be used in a nested fashion for partials and whatnot" do mock_app do template(:inner) { "hi" } template(:outer) { "<%= markdown :inner %>" } get('/') { erb :outer } end get '/' assert ok? assert_like '

hi

', body end end [ "Tilt::PandocTemplate", "Tilt::CommonMarkerTemplate", "Tilt::KramdownTemplate", "Tilt::RedcarpetTemplate", "Tilt::RDiscountTemplate" ].each do |template_name| begin template = Object.const_get(template_name) klass = Class.new(Minitest::Test) { define_method(:engine) { template } } klass.class_eval(&MarkdownTest) name = template_name.split('::').last.sub(/Template$/, 'Test') Object.const_set name, klass rescue LoadError, NameError warn "#{$!}: skipping markdown tests with #{template_name}" end end sinatra-3.0.5/test/middleware_test.rb000066400000000000000000000051001434717561400176500ustar00rootroot00000000000000require File.expand_path('helper', __dir__) class MiddlewareTest < Minitest::Test setup do @app = mock_app(Sinatra::Application) do get('/*')do response.headers['X-Tests'] = env['test.ran']. map { |n| n.split('::').last }. join(', ') env['PATH_INFO'] end end end class MockMiddleware < Struct.new(:app) def call(env) (env['test.ran'] ||= []) << self.class.to_s app.call(env) end end class UpcaseMiddleware < MockMiddleware def call(env) env['PATH_INFO'] = env['PATH_INFO'].upcase super end end it "is added with Sinatra::Application.use" do @app.use UpcaseMiddleware get '/hello-world' assert ok? assert_equal '/HELLO-WORLD', body end class DowncaseMiddleware < MockMiddleware def call(env) env['PATH_INFO'] = env['PATH_INFO'].downcase super end end it "runs in the order defined" do @app.use UpcaseMiddleware @app.use DowncaseMiddleware get '/Foo' assert_equal "/foo", body assert_equal "UpcaseMiddleware, DowncaseMiddleware", response['X-Tests'] end it "resets the prebuilt pipeline when new middleware is added" do @app.use UpcaseMiddleware get '/Foo' assert_equal "/FOO", body @app.use DowncaseMiddleware get '/Foo' assert_equal '/foo', body assert_equal "UpcaseMiddleware, DowncaseMiddleware", response['X-Tests'] end it "works when app is used as middleware" do @app.use UpcaseMiddleware @app = @app.new get '/Foo' assert_equal "/FOO", body assert_equal "UpcaseMiddleware", response['X-Tests'] end class FreezeMiddleware < MockMiddleware def call(env) req = Rack::Request.new(env) req.update_param('bar', 'baz'.freeze) super end end it "works when middleware adds a frozen param" do @app.use FreezeMiddleware get '/Foo' end class SpecialConstsMiddleware < MockMiddleware def call(env) req = Rack::Request.new(env) req.update_param('s', :s) req.update_param('i', 1) req.update_param('c', 3.to_c) req.update_param('t', true) req.update_param('f', false) req.update_param('n', nil) super end end it "handles params when the params contains true/false values" do @app.use SpecialConstsMiddleware get '/' end class KeywordArgumentIntializationMiddleware < MockMiddleware def initialize(app, **) super app end end it "handles keyword arguments" do @app.use KeywordArgumentIntializationMiddleware, argument: "argument" get '/' end end sinatra-3.0.5/test/nokogiri_test.rb000066400000000000000000000031471434717561400173650ustar00rootroot00000000000000require File.expand_path('helper', __dir__) begin require 'nokogiri' class NokogiriTest < Minitest::Test def nokogiri_app(&block) mock_app do set :views, __dir__ + '/views' get('/', &block) end get '/' end it 'renders inline Nokogiri strings' do nokogiri_app { nokogiri '' } assert ok? assert_body %(\n) end it 'renders inline blocks' do nokogiri_app do @name = "Frank & Mary" nokogiri { |xml| xml.couple @name } end assert ok? assert_body %(\nFrank & Mary\n) end it 'renders .nokogiri files in views path' do nokogiri_app do @name = "Blue" nokogiri :hello end assert ok? assert_body "\nYou're my boy, Blue!\n" end it "renders with inline layouts" do next if Tilt::VERSION <= "1.1" mock_app do layout { %(xml.layout { xml << yield }) } get('/') { nokogiri %(xml.em 'Hello World') } end get '/' assert ok? assert_body %(\n\n Hello World\n\n) end it "renders with file layouts" do next if Tilt::VERSION <= "1.1" nokogiri_app { nokogiri %(xml.em 'Hello World'), :layout => :layout2 } assert ok? assert_body %(\n\n Hello World\n\n) end it "raises error if template not found" do mock_app { get('/') { nokogiri :no_such_template } } assert_raises(Errno::ENOENT) { get('/') } end end rescue LoadError warn "#{$!}: skipping nokogiri tests" end sinatra-3.0.5/test/public/000077500000000000000000000000001434717561400154315ustar00rootroot00000000000000sinatra-3.0.5/test/public/favicon.ico000066400000000000000000000000001434717561400175400ustar00rootroot00000000000000sinatra-3.0.5/test/public/hello+world.txt000066400000000000000000000001011434717561400204100ustar00rootroot00000000000000This is a test intended for the + sign in urls for static servingsinatra-3.0.5/test/rabl_test.rb000066400000000000000000000037471434717561400164720ustar00rootroot00000000000000require File.expand_path('helper', __dir__) begin require 'rabl' require 'ostruct' require 'json' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/hash/conversions' class RablTest < Minitest::Test def rabl_app(&block) mock_app { set :views, __dir__ + '/views' get '/', &block } get '/' end it 'renders inline rabl strings' do rabl_app do @foo = OpenStruct.new(:baz => 'w00t') rabl %q{ object @foo attributes :baz } end assert ok? assert_equal '{"openstruct":{"baz":"w00t"}}', body end it 'renders .rabl files in views path' do rabl_app do @foo = OpenStruct.new(:bar => 'baz') rabl :hello end assert ok? assert_equal '{"openstruct":{"bar":"baz"}}', body end it "renders with file layouts" do rabl_app { @foo = OpenStruct.new(:bar => 'baz') rabl :hello, :layout => :layout2 } assert ok? assert_equal '{"qux":{"openstruct":{"bar":"baz"}}}', body end it "raises error if template not found" do mock_app { get('/') { rabl :no_such_template } } assert_raises(Errno::ENOENT) { get('/') } end it "passes rabl options to the rabl engine" do mock_app do get('/') do @foo = OpenStruct.new(:bar => 'baz') rabl %q{ object @foo attributes :bar }, :format => 'xml' end end get '/' assert ok? assert_body 'baz' end it "passes default rabl options to the rabl engine" do mock_app do set :rabl, :format => 'xml' get('/') do @foo = OpenStruct.new(:bar => 'baz') rabl %q{ object @foo attributes :bar } end end get '/' assert ok? assert_body 'baz' end end rescue LoadError warn "#{$!}: skipping rabl tests" end sinatra-3.0.5/test/rack_test.rb000066400000000000000000000020351434717561400164570ustar00rootroot00000000000000require File.expand_path('helper', __dir__) require 'rack' class RackTest < Minitest::Test setup do @foo = Sinatra.new { get('/foo') { 'foo' }} @bar = Sinatra.new { get('/bar') { 'bar' }} end def build(*middleware) endpoint = middleware.pop @app = Rack::Builder.app do middleware.each { |m| use m } run endpoint end end def check(*middleware) build(*middleware) assert get('/foo').ok? assert_body 'foo' assert get('/bar').ok? assert_body 'bar' end it 'works as middleware in front of Rack::Lock, with lock enabled' do @foo.enable :lock check(@foo, Rack::Lock, @bar) end it 'works as middleware behind Rack::Lock, with lock enabled' do @foo.enable :lock check(Rack::Lock, @foo, @bar) end it 'works as middleware in front of Rack::Lock, with lock disabled' do @foo.disable :lock check(@foo, Rack::Lock, @bar) end it 'works as middleware behind Rack::Lock, with lock disabled' do @foo.disable :lock check(Rack::Lock, @foo, @bar) end end sinatra-3.0.5/test/rdoc_test.rb000066400000000000000000000032401434717561400164650ustar00rootroot00000000000000require File.expand_path('helper', __dir__) begin require 'rdoc' require 'rdoc/markup/to_html' class RdocTest < Minitest::Test def rdoc_app(&block) mock_app do set :views, __dir__ + '/views' get('/', &block) end get '/' end it 'renders inline rdoc strings' do rdoc_app { rdoc '= Hiya' } assert ok? assert_body(/]*>Hiya(¶<\/a> ↑<\/a><\/span>)?<\/h1>/) end it 'renders .rdoc files in views path' do rdoc_app { rdoc :hello } assert ok? assert_body(/]*>Hello From RDoc(¶<\/a> ↑<\/a><\/span>)?<\/h1>/) end it "raises error if template not found" do mock_app { get('/') { rdoc :no_such_template } } assert_raises(Errno::ENOENT) { get('/') } end it "renders with inline layouts" do mock_app do layout { 'THIS. IS. #{yield.upcase}!' } get('/') { rdoc 'Sparta', :layout_engine => :str } end get '/' assert ok? assert_like 'THIS. IS.

SPARTA

!', body end it "renders with file layouts" do rdoc_app { rdoc 'Hello World', :layout => :layout2, :layout_engine => :erb } assert ok? assert_body "ERB Layout!\n

Hello World

" end it "can be used in a nested fashion for partials and whatnot" do mock_app do template(:inner) { "hi" } template(:outer) { "<%= rdoc :inner %>" } get('/') { erb :outer } end get '/' assert ok? assert_like '

hi

', body end end rescue LoadError warn "#{$!}: skipping rdoc tests" end sinatra-3.0.5/test/readme_test.rb000066400000000000000000000052611434717561400170000ustar00rootroot00000000000000# Tests to check if all the README examples work. require File.expand_path('helper', __dir__) class ReadmeTest < Minitest::Test example do mock_app { get('/') { 'Hello world!' } } get '/' assert_body 'Hello world!' end section "Routes" do example do mock_app do get('/') { ".. show something .." } post('/') { ".. create something .." } put('/') { ".. replace something .." } patch('/') { ".. modify something .." } delete('/') { ".. annihilate something .." } options('/') { ".. appease something .." } link('/') { ".. affiliate something .." } unlink('/') { ".. separate something .." } end get '/' assert_body '.. show something ..' post '/' assert_body '.. create something ..' put '/' assert_body '.. replace something ..' patch '/' assert_body '.. modify something ..' delete '/' assert_body '.. annihilate something ..' options '/' assert_body '.. appease something ..' link '/' assert_body '.. affiliate something ..' unlink '/' assert_body '.. separate something ..' end example do mock_app do get('/hello/:name') do # matches "GET /hello/foo" and "GET /hello/bar" # params[:name] is 'foo' or 'bar' "Hello #{params[:name]}!" end end get '/hello/foo' assert_body 'Hello foo!' get '/hello/bar' assert_body 'Hello bar!' end example do mock_app { get('/hello/:name') { |n| "Hello #{n}!" } } get '/hello/foo' assert_body 'Hello foo!' get '/hello/bar' assert_body 'Hello bar!' end example do mock_app do get('/say/*/to/*') do # matches /say/hello/to/world params[:splat].inspect # => ["hello", "world"] end get('/download/*.*') do # matches /download/path/to/file.xml params[:splat].inspect # => ["path/to/file", "xml"] end end get "/say/hello/to/world" assert_body '["hello", "world"]' get "/download/path/to/file.xml" assert_body '["path/to/file", "xml"]' end example do mock_app do get(%r{/hello/([\w]+)}) { "Hello, #{params[:captures].first}!" } end get '/hello/foo' assert_body 'Hello, foo!' get '/hello/bar' assert_body 'Hello, bar!' end example do mock_app do get( %r{/hello/([\w]+)}) { |c| "Hello, #{c}!" } end get '/hello/foo' assert_body 'Hello, foo!' get '/hello/bar' assert_body 'Hello, bar!' end end end sinatra-3.0.5/test/request_test.rb000066400000000000000000000122411434717561400172270ustar00rootroot00000000000000require File.expand_path('helper', __dir__) require 'stringio' class RequestTest < Minitest::Test it 'responds to #user_agent' do request = Sinatra::Request.new({'HTTP_USER_AGENT' => 'Test'}) assert request.respond_to?(:user_agent) assert_equal 'Test', request.user_agent end it 'parses POST params when Content-Type is form-dataish' do request = Sinatra::Request.new( 'REQUEST_METHOD' => 'PUT', 'CONTENT_TYPE' => 'application/x-www-form-urlencoded', 'rack.input' => StringIO.new('foo=bar') ) assert_equal 'bar', request.params['foo'] end it 'raises Sinatra::BadRequest when multipart/form-data request has no content' do request = Sinatra::Request.new( 'REQUEST_METHOD' => 'POST', 'CONTENT_TYPE' => 'multipart/form-data; boundary=dummy', 'rack.input' => StringIO.new('') ) assert_raises(Sinatra::BadRequest) { request.params } end it 'is secure when the url scheme is https' do request = Sinatra::Request.new('rack.url_scheme' => 'https') assert request.secure? end it 'is not secure when the url scheme is http' do request = Sinatra::Request.new('rack.url_scheme' => 'http') assert !request.secure? end it 'respects X-Forwarded-Proto header for proxied SSL' do request = Sinatra::Request.new('HTTP_X_FORWARDED_PROTO' => 'https') assert request.secure? end it 'is possible to marshal params' do request = Sinatra::Request.new( 'REQUEST_METHOD' => 'PUT', 'CONTENT_TYPE' => 'application/x-www-form-urlencoded', 'rack.input' => StringIO.new('foo=bar') ) Sinatra::IndifferentHash[request.params] dumped = Marshal.dump(request.params) assert_equal 'bar', Marshal.load(dumped)['foo'] end it "exposes the preferred type's parameters" do request = Sinatra::Request.new( 'HTTP_ACCEPT' => 'image/jpeg; compress=0.25' ) assert_equal({ 'compress' => '0.25' }, request.preferred_type.params) end it "raises Sinatra::BadRequest when params contain conflicting types" do request = Sinatra::Request.new 'QUERY_STRING' => 'foo=&foo[]=' assert_raises(Sinatra::BadRequest) { request.params } end it "makes accept types behave like strings" do request = Sinatra::Request.new('HTTP_ACCEPT' => 'image/jpeg; compress=0.25') assert request.accept?('image/jpeg') assert_equal 'image/jpeg', request.preferred_type.to_s assert_equal 'image/jpeg; compress=0.25', request.preferred_type.to_s(true) assert_equal 'image/jpeg', request.preferred_type.to_str assert_equal 'image', request.preferred_type.split('/').first String.instance_methods.each do |method| next unless "".respond_to? method assert request.preferred_type.respond_to?(method), "responds to #{method}" end end it "accepts types when wildcards are requested" do request = Sinatra::Request.new('HTTP_ACCEPT' => 'image/*') assert request.accept?('image/jpeg') end it "properly decodes MIME type parameters" do request = Sinatra::Request.new( 'HTTP_ACCEPT' => 'image/jpeg;unquoted=0.25;quoted="0.25";chartest="\";,\x"' ) expected = { 'unquoted' => '0.25', 'quoted' => '0.25', 'chartest' => '";,x' } assert_equal(expected, request.preferred_type.params) end it 'accepts */* when HTTP_ACCEPT is not present in the request' do request = Sinatra::Request.new Hash.new assert_equal 1, request.accept.size assert request.accept?('text/html') assert_equal '*/*', request.preferred_type.to_s assert_equal '*/*', request.preferred_type.to_s(true) end it 'accepts */* when HTTP_ACCEPT is blank in the request' do request = Sinatra::Request.new 'HTTP_ACCEPT' => '' assert_equal 1, request.accept.size assert request.accept?('text/html') assert_equal '*/*', request.preferred_type.to_s assert_equal '*/*', request.preferred_type.to_s(true) end it 'will not accept types not specified in HTTP_ACCEPT when HTTP_ACCEPT is provided' do request = Sinatra::Request.new 'HTTP_ACCEPT' => 'application/json' assert !request.accept?('text/html') end it 'will accept types that fulfill HTTP_ACCEPT parameters' do request = Sinatra::Request.new 'HTTP_ACCEPT' => 'application/rss+xml; version="http://purl.org/rss/1.0/"' assert request.accept?('application/rss+xml; version="http://purl.org/rss/1.0/"') assert request.accept?('application/rss+xml; version="http://purl.org/rss/1.0/"; charset=utf-8') assert !request.accept?('application/rss+xml; version="https://cyber.harvard.edu/rss/rss.html"') end it 'will accept more generic types that include HTTP_ACCEPT parameters' do request = Sinatra::Request.new 'HTTP_ACCEPT' => 'application/rss+xml; charset=utf-8; version="http://purl.org/rss/1.0/"' assert request.accept?('application/rss+xml') assert request.accept?('application/rss+xml; version="http://purl.org/rss/1.0/"') end it 'will accept types matching HTTP_ACCEPT when parameters in arbitrary order' do request = Sinatra::Request.new 'HTTP_ACCEPT' => 'application/rss+xml; charset=utf-8; version="http://purl.org/rss/1.0/"' assert request.accept?('application/rss+xml; version="http://purl.org/rss/1.0/"; charset=utf-8') end end sinatra-3.0.5/test/response_test.rb000066400000000000000000000044761434717561400174100ustar00rootroot00000000000000# encoding: utf-8 require File.expand_path('helper', __dir__) class ResponseTest < Minitest::Test setup { @response = Sinatra::Response.new([], 200, { 'Content-Type' => 'text/html' }) } def assert_same_body(a, b) assert_equal a.to_enum(:each).to_a, b.to_enum(:each).to_a end it "initializes with 200, text/html, and empty body" do assert_equal 200, @response.status assert_equal 'text/html', @response['Content-Type'] assert_equal [], @response.body end it 'uses case insensitive headers' do @response['content-type'] = 'application/foo' assert_equal 'application/foo', @response['Content-Type'] assert_equal 'application/foo', @response['CONTENT-TYPE'] end it 'writes to body' do @response.body = 'Hello' @response.write ' World' assert_equal 'Hello World', @response.body.join end [204, 304].each do |status_code| it "removes the Content-Type header and body when response status is #{status_code}" do @response.status = status_code @response.body = ['Hello World'] assert_equal [status_code, {}, []], @response.finish end end [200, 201, 202, 301, 302, 400, 401, 403, 404, 500].each do |status_code| it "will not removes the Content-Type header and body when response status is #{status_code}" do @response.status = status_code @response.body = ['Hello World'] assert_equal [ status_code, { 'Content-Type' => 'text/html', 'Content-Length' => '11' }, ['Hello World'] ], @response.finish end end it 'Calculates the Content-Length using the bytesize of the body' do @response.body = ['Hello', 'World!', '✈'] _, headers, body = @response.finish assert_equal '14', headers['Content-Length'] assert_same_body @response.body, body end it 'does not call #to_ary or #inject on the body' do object = Object.new def object.inject(*) fail 'called' end def object.to_ary(*) fail 'called' end def object.each(*) end @response.body = object assert @response.finish end it 'does not nest a Sinatra::Response' do @response.body = Sinatra::Response.new ["foo"] assert_same_body @response.body, ["foo"] end it 'does not nest a Rack::Response' do @response.body = Rack::Response.new ["foo"] assert_same_body @response.body, ["foo"] end end sinatra-3.0.5/test/result_test.rb000066400000000000000000000040621434717561400170570ustar00rootroot00000000000000require File.expand_path('helper', __dir__) class ThirdPartyError < RuntimeError def http_status; 400 end end class ResultTest < Minitest::Test it "sets response.body when result is a String" do mock_app { get('/') { 'Hello World' } } get '/' assert ok? assert_equal 'Hello World', body end it "sets response.body when result is an Array of Strings" do mock_app { get('/') { ['Hello', 'World'] } } get '/' assert ok? assert_equal 'HelloWorld', body end it "sets response.body when result responds to #each" do mock_app do get('/') do res = lambda { 'Hello World' } def res.each ; yield call ; end return res end end get '/' assert ok? assert_equal 'Hello World', body end it "sets response.body to [] when result is nil" do mock_app { get( '/') { nil } } get '/' assert ok? assert_equal '', body end it "sets status, headers, and body when result is a Rack response tuple" do mock_app { get('/') { [203, {'Content-Type' => 'foo/bar'}, 'Hello World'] } } get '/' assert_equal 203, status assert_equal 'foo/bar', response['Content-Type'] assert_equal 'Hello World', body end it "sets status and body when result is a two-tuple" do mock_app { get('/') { [409, 'formula of'] } } get '/' assert_equal 409, status assert_equal 'formula of', body end it "raises a ArgumentError when result is a non two or three tuple Array" do mock_app { get('/') { [409, 'formula of', 'something else', 'even more'] } } assert_raises(ArgumentError) { get '/' } end it "sets status when result is a Integer status code" do mock_app { get('/') { 205 } } get '/' assert_equal 205, status assert_equal '', body end it "sets status to 500 when raised error is not Sinatra::Error" do mock_app do set :raise_errors, false get('/') { raise ThirdPartyError } end get '/' assert_equal 500, status assert_equal '

Internal Server Error

', body end end sinatra-3.0.5/test/route_added_hook_test.rb000066400000000000000000000025101434717561400210340ustar00rootroot00000000000000require File.expand_path('helper', __dir__) module RouteAddedTest @routes, @procs = [], [] def self.routes ; @routes ; end def self.procs ; @procs ; end def self.route_added(verb, path, proc) @routes << [verb, path] @procs << proc end end class RouteAddedHookTest < Minitest::Test setup do RouteAddedTest.routes.clear RouteAddedTest.procs.clear end it "should be notified of an added route" do mock_app(Class.new(Sinatra::Base)) do register RouteAddedTest get('/') {} end assert_equal [["GET", "/"], ["HEAD", "/"]], RouteAddedTest.routes end it "should include hooks from superclass" do a = Class.new(Class.new(Sinatra::Base)) b = Class.new(a) a.register RouteAddedTest b.class_eval { post("/sub_app_route") {} } assert_equal [["POST", "/sub_app_route"]], RouteAddedTest.routes end it "should only run once per extension" do mock_app(Class.new(Sinatra::Base)) do register RouteAddedTest register RouteAddedTest get('/') {} end assert_equal [["GET", "/"], ["HEAD", "/"]], RouteAddedTest.routes end it "should pass route blocks as an argument" do mock_app(Class.new(Sinatra::Base)) do register RouteAddedTest get('/') {} end assert_kind_of Proc, RouteAddedTest.procs.first end end sinatra-3.0.5/test/routing_test.rb000066400000000000000000001152501434717561400172320ustar00rootroot00000000000000# I like coding: UTF-8 require File.expand_path('helper', __dir__) # Helper method for easy route pattern matching testing def route_def(pattern) mock_app { get(pattern) { } } end class PatternLookAlike def to_pattern(*) self end def params(input) { "one" => "this", "two" => "is", "three" => "a", "four" => "test" } end end class RoutingTest < Minitest::Test %w[get put post delete options patch link unlink].each do |verb| it "defines #{verb.upcase} request handlers with #{verb}" do mock_app { send verb, '/hello' do 'Hello World' end } request = Rack::MockRequest.new(@app) response = request.request(verb.upcase, '/hello', {}) assert response.ok? assert_equal 'Hello World', response.body end end it "defines HEAD request handlers with HEAD" do mock_app { head '/hello' do response['X-Hello'] = 'World!' 'remove me' end } request = Rack::MockRequest.new(@app) response = request.request('HEAD', '/hello', {}) assert response.ok? assert_equal 'World!', response['X-Hello'] assert_equal '', response.body end it "400s when request params contain conflicting types" do mock_app { get('/foo') { } } request = Rack::MockRequest.new(@app) response = request.request('GET', '/foo?bar=&bar[]=', {}) assert response.bad_request? end it "404s when no route satisfies the request" do mock_app { get('/foo') { } } get '/bar' assert_equal 404, status end it "404s and sets X-Cascade header when no route satisfies the request" do mock_app { get('/foo') { } } get '/bar' assert_equal 404, status assert_equal 'pass', response.headers['X-Cascade'] end it "404s and does not set X-Cascade header when no route satisfies the request and x_cascade has been disabled" do mock_app { disable :x_cascade get('/foo') { } } get '/bar' assert_equal 404, status assert_nil response.headers['X-Cascade'] end it "allows using unicode" do mock_app do get('/föö') { } end get '/f%C3%B6%C3%B6' assert_equal 200, status end it "it handles encoded slashes correctly" do mock_app { set :protection, :except => :path_traversal get("/:a") { |a| a } } get '/foo%2Fbar' assert_equal 200, status assert_body "foo/bar" end it "it handles encoded colons correctly" do mock_app { get("/\\:") { 'a' } get("/a/\\:") { 'b' } get("/a/\\:/b") { 'c' } get("/a/b\\:") { 'd' } get("/a/b\\: ") { 'e' } } get '/:' assert_equal 200, status assert_body "a" get '/%3a' assert_equal 200, status assert_body "a" get '/a/:' assert_equal 200, status assert_body "b" get '/a/%3a' assert_equal 200, status assert_body "b" get '/a/:/b' assert_equal 200, status assert_body "c" get '/a/%3A/b' assert_equal 200, status assert_body "c" get '/a/b:' assert_equal 200, status assert_body "d" get '/a/b%3a' assert_equal 200, status assert_body "d" get '/a/b%3a%20' assert_equal 200, status assert_body "e" get '/a/b%3a+' assert_equal 200, status assert_body "e" end it "overrides the content-type in error handlers" do mock_app { before { content_type 'text/plain' } error Sinatra::NotFound do content_type "text/html" "

Not Found

" end } get '/foo' assert_equal 404, status assert_equal 'text/html;charset=utf-8', response["Content-Type"] assert_equal "

Not Found

", response.body end it "recalculates body length correctly for 404 response" do mock_app { get '/' do @response["Content-Length"] = "30" raise Sinatra::NotFound end } get "/" assert_equal "18", response["Content-Length"] assert_equal 404, status end it "captures the exception message of a raised NotFound" do mock_app { get '/' do raise Sinatra::NotFound, "This is not a drill" end } get "/" assert_equal "19", response["Content-Length"] assert_equal 404, status assert_equal "This is not a drill", response.body end it "captures the exception message of a raised BadRequest" do mock_app { get '/' do raise Sinatra::BadRequest, "This is not a drill either" end } get "/" assert_equal "26", response["Content-Length"] assert_equal 400, status assert_equal "This is not a drill either", response.body end it "captures the custom exception message of a BadRequest" do mock_app { get('/') {} error Sinatra::BadRequest do 'This is not a drill either' end } get "/", "foo" => "", "foo[]" => "" assert_equal "26", response["Content-Length"] assert_equal 400, status assert_equal "This is not a drill either", response.body end it "returns empty when unmatched with any regex captures" do mock_app do before do # noop end get '/hello' do params.to_s end end assert get('/hello').ok? assert_body '{}' end it "uses 404 error handler for not matching route" do mock_app { not_found do "nf" end error 404 do "e" end } get "/" assert_equal "e", body assert_equal 404, status end it 'matches empty PATH_INFO to "/" if no route is defined for ""' do mock_app do get '/' do 'worked' end end get '/', {}, "PATH_INFO" => "" assert ok? assert_equal 'worked', body end it 'matches empty PATH_INFO to "" if a route is defined for ""' do mock_app do disable :protection get '/' do 'did not work' end get '' do 'worked' end end get '/', {}, "PATH_INFO" => "" assert ok? assert_equal 'worked', body end it 'takes multiple definitions of a route' do mock_app { user_agent(/Foo/) get '/foo' do 'foo' end get '/foo' do 'not foo' end } get '/foo', {}, 'HTTP_USER_AGENT' => 'Foo' assert ok? assert_equal 'foo', body get '/foo' assert ok? assert_equal 'not foo', body end it "exposes params with indifferent hash" do mock_app { get '/:foo' do assert_equal 'bar', params['foo'] assert params.has_key?('foo') assert_equal 'bar', params[:foo] assert params.has_key?(:foo) 'well, alright' end } get '/bar' assert_equal 'well, alright', body end it "handles params without a value" do mock_app { get '/' do assert_nil params.fetch('foo') "Given: #{params.keys.sort.join(',')}" end } get '/?foo' assert_equal 'Given: foo', body end it "merges named params and query string params in params" do mock_app { get '/:foo' do assert_equal 'bar', params['foo'] assert_equal 'biz', params['baz'] end } get '/bar?baz=biz' assert ok? end it "supports named params like /hello/:person" do mock_app { get '/hello/:person' do "Hello #{params['person']}" end } get '/hello/Frank' assert_equal 'Hello Frank', body end it "supports optional named params like /?:foo?/?:bar?" do mock_app { get '/?:foo?/?:bar?' do "foo=#{params[:foo]};bar=#{params[:bar]}" end } get '/hello/world' assert ok? assert_equal "foo=hello;bar=world", body get '/hello' assert ok? assert_equal "foo=hello;bar=", body get '/hello?bar=baz' assert ok? assert_equal "foo=hello;bar=baz", body get '/' assert ok? assert_equal "foo=;bar=", body end it "uses the default encoding for named params" do mock_app { set :default_encoding ,'ISO-8859-1' get '/:foo/:bar' do "foo=#{params[:foo].encoding};bar=#{params[:bar].encoding}" end } get '/f%C3%B6%C3%B6/b%C3%B6%C3%B6' assert ok? assert_equal 'foo=ISO-8859-1;bar=ISO-8859-1', body end it "supports named captures like %r{/hello/(?[^/?#]+)}" do mock_app { get Regexp.new('/hello/(?[^/?#]+)') do "Hello #{params['person']}" end } get '/hello/Frank' assert_equal 'Hello Frank', body end it "supports optional named captures like %r{/page(?.[^/?#]+)?}" do mock_app { get Regexp.new('/page(?.[^/?#]+)?') do "format=#{params[:format]}" end } get '/page.html' assert ok? assert_equal "format=.html", body get '/page.xml' assert ok? assert_equal "format=.xml", body get '/page' assert ok? assert_equal "format=", body end it 'uses the default encoding for named captures' do mock_app { set :default_encoding ,'ISO-8859-1' get Regexp.new('/page(?.[^/?#]+)?') do "format=#{params[:format].encoding};captures=#{params[:captures][0].encoding}" end } get '/page.f%C3%B6' assert ok? assert_equal 'format=ISO-8859-1;captures=ISO-8859-1', body end it 'does not concatenate params with the same name' do mock_app { get('/:foo') { params[:foo] } } get '/a?foo=b' assert_body 'a' end it "supports single splat params like /*" do mock_app { get '/*' do assert params['splat'].kind_of?(Array) params['splat'].join "\n" end } get '/foo' assert_equal "foo", body get '/foo/bar/baz' assert_equal "foo/bar/baz", body end it "supports mixing multiple splat params like /*/foo/*/*" do mock_app { get '/*/foo/*/*' do assert params['splat'].kind_of?(Array) params['splat'].join "\n" end } get '/bar/foo/bling/baz/boom' assert_equal "bar\nbling\nbaz/boom", body get '/bar/foo/baz' assert not_found? end it "supports mixing named and splat params like /:foo/*" do mock_app { get '/:foo/*' do assert_equal 'foo', params['foo'] assert_equal ['bar/baz'], params['splat'] end } get '/foo/bar/baz' assert ok? end it "matches a dot ('.') as part of a named param" do mock_app { get '/:foo/:bar' do params[:foo] end } get '/user@example.com/name' assert_equal 200, response.status assert_equal 'user@example.com', body end it "matches a literal dot ('.') outside of named params" do mock_app { get '/:file.:ext' do assert_equal 'pony', params[:file] assert_equal 'jpg', params[:ext] 'right on' end } get '/pony.jpg' assert_equal 200, response.status assert_equal 'right on', body end it "literally matches dot in paths" do route_def '/test.bar' get '/test.bar' assert ok? get 'test0bar' assert not_found? end it "literally matches dollar sign in paths" do route_def '/test$/' get '/test$/' assert ok? end it "literally matches plus sign in paths" do route_def '/te+st/' get '/te%2Bst/' assert ok? get '/teeeeeeest/' assert not_found? end it "does not convert plus sign into space as the value of a named param" do mock_app do get '/:test' do params["test"] end end get '/bob+ross' assert ok? assert_equal 'bob+ross', body end it "literally matches parens in paths when escaped" do route_def '/test\(bar\)/' get '/test(bar)/' assert ok? end it "supports basic nested params" do mock_app { get '/hi' do params["person"]["name"] end } get "/hi?person[name]=John+Doe" assert ok? assert_equal "John Doe", body end it "exposes nested params with indifferent hash" do mock_app { get '/testme' do assert_equal 'baz', params['bar']['foo'] assert_equal 'baz', params['bar'][:foo] 'well, alright' end } get '/testme?bar[foo]=baz' assert_equal 'well, alright', body end it "exposes params nested within arrays with indifferent hash" do mock_app { get '/testme' do assert_equal 'baz', params['bar'][0]['foo'] assert_equal 'baz', params['bar'][0][:foo] 'well, alright' end } get '/testme?bar[][foo]=baz' assert_equal 'well, alright', body end it "supports arrays within params" do mock_app { get '/foo' do assert_equal ['A', 'B'], params['bar'] 'looks good' end } get '/foo?bar[]=A&bar[]=B' assert ok? assert_equal 'looks good', body end it "supports deeply nested params" do expected_params = { "emacs" => { "map" => { "goto-line" => "M-g g" }, "version" => "22.3.1" }, "browser" => { "firefox" => {"engine" => {"name"=>"spidermonkey", "version"=>"1.7.0"}}, "chrome" => {"engine" => {"name"=>"V8", "version"=>"1.0"}} }, "paste" => {"name"=>"hello world", "syntax"=>"ruby"} } mock_app { get '/foo' do assert_equal expected_params, params 'looks good' end } get '/foo', expected_params assert ok? assert_equal 'looks good', body end it "preserves non-nested params" do mock_app { get '/foo' do assert_equal "2", params["article_id"] assert_equal "awesome", params['comment']['body'] assert_nil params['comment[body]'] 'looks good' end } get '/foo?article_id=2&comment[body]=awesome' assert ok? assert_equal 'looks good', body end it "matches paths that include spaces encoded with %20" do mock_app { get '/path with spaces' do 'looks good' end } get '/path%20with%20spaces' assert ok? assert_equal 'looks good', body end it "matches paths that include spaces encoded with +" do mock_app { get '/path with spaces' do 'looks good' end } get '/path+with+spaces' assert ok? assert_equal 'looks good', body end it "matches paths that include ampersands" do mock_app { get '/:name' do 'looks good' end } get '/foo&bar' assert ok? assert_equal 'looks good', body end it "URL decodes named parameters and splats" do mock_app { get '/:foo/*' do assert_equal 'hello world', params['foo'] assert_equal ['how are you'], params['splat'] nil end } get '/hello%20world/how%20are%20you' assert ok? end it 'unescapes named parameters and splats' do mock_app { get '/:foo/*' do |a, b| assert_equal "foo\xE2\x80\x8Cbar", params['foo'] assert_predicate params['foo'], :valid_encoding? assert_equal ["bar\xE2\x80\x8Cbaz"], params['splat'] end } get '/foo%e2%80%8cbar/bar%e2%80%8cbaz' assert ok? end it 'supports regular expressions' do mock_app { get(/\/foo...\/bar/) do 'Hello World' end } get '/foooom/bar' assert ok? assert_equal 'Hello World', body end it 'unescapes regular expression captures' do mock_app { get(/\/foo\/(.+)/) do |path| path end } get '/foo/bar%e2%80%8cbaz' assert ok? assert_equal "bar\xE2\x80\x8Cbaz", body assert_predicate body, :valid_encoding? end it 'makes regular expression captures available in params[:captures]' do mock_app { get(/\/fo(.*)\/ba(.*)/) do assert_equal ['orooomma', 'f'], params[:captures] 'right on' end } get '/foorooomma/baf' assert ok? assert_equal 'right on', body end it 'makes regular expression captures available in params[:captures] for concatenated routes' do with_regexp = Mustermann.new('/prefix') + Mustermann.new("/fo(.*)/ba(.*)", type: :regexp) without_regexp = Mustermann.new('/prefix', type: :identity) + Mustermann.new('/baz') mock_app { get(with_regexp) do assert_equal ['orooomma', 'f'], params[:captures] 'right on' end get(without_regexp) do assert !params.keys.include?(:captures) 'no captures here' end } get '/prefix/foorooomma/baf' assert ok? assert_equal 'right on', body get '/prefix/baz' assert ok? assert_equal 'no captures here', body end it 'supports regular expression look-alike routes' do mock_app { get(PatternLookAlike.new) do assert_equal 'this', params[:one] assert_equal 'is', params[:two] assert_equal 'a', params[:three] assert_equal 'test', params[:four] 'right on' end } get '/this/is/a/test/' assert ok? assert_equal 'right on', body end it 'raises a TypeError when pattern is not a String or Regexp' do assert_raises(TypeError) { mock_app { get(42){} } } end it "returns response immediately on halt" do mock_app { get '/' do halt 'Hello World' 'Boo-hoo World' end } get '/' assert ok? assert_equal 'Hello World', body end it "halts with a response tuple" do mock_app { get '/' do halt 295, {'Content-Type' => 'text/plain'}, 'Hello World' end } get '/' assert_equal 295, status assert_equal 'text/plain', response['Content-Type'] assert_equal 'Hello World', body end it "halts with an array of strings" do mock_app { get '/' do halt %w[Hello World How Are You] end } get '/' assert_equal 'HelloWorldHowAreYou', body end it 'sets response.status with halt' do status_was = nil mock_app do after { status_was = status } get('/') { halt 500, 'error' } end get '/' assert_status 500 assert_equal 500, status_was end it "transitions to the next matching route on pass" do mock_app { get '/:foo' do pass 'Hello Foo' end get '/*' do assert !params.include?('foo') 'Hello World' end } get '/bar' assert ok? assert_equal 'Hello World', body end it "makes original request params available in error handler" do mock_app { disable :raise_errors get '/:foo' do raise ArgumentError, "foo" end error do "Hello #{params['foo']}2" end } get '/bar' assert_equal 'Hello bar2', body end it "transitions to 404 when passed and no subsequent route matches" do mock_app { get '/:foo' do pass 'Hello Foo' end } get '/bar' assert not_found? end it "transitions to 404 and sets X-Cascade header when passed and no subsequent route matches" do mock_app { get '/:foo' do pass 'Hello Foo' end get '/bar' do 'Hello Bar' end } get '/foo' assert not_found? assert_equal 'pass', response.headers['X-Cascade'] end it "uses optional block passed to pass as route block if no other route is found" do mock_app { get "/" do pass do "this" end "not this" end } get "/" assert ok? assert "this", body end it "uses optional block passed to pass as route block if no other route is found and superclass has non-matching routes" do base = Class.new(Sinatra::Base) base.get('/foo') { 'foo in baseclass' } mock_app(base) { get "/" do pass do "this" end "not this" end } get "/" assert_equal 200, status assert "this", body end it "passes when matching condition returns false" do mock_app { condition { params[:foo] == 'bar' } get '/:foo' do 'Hello World' end } get '/bar' assert ok? assert_equal 'Hello World', body get '/foo' assert not_found? end it "does not pass when matching condition returns nil" do mock_app { condition { nil } get '/:foo' do 'Hello World' end } get '/bar' assert ok? assert_equal 'Hello World', body end it "passes to next route when condition calls pass explicitly" do mock_app { condition { pass unless params[:foo] == 'bar' } get '/:foo' do 'Hello World' end } get '/bar' assert ok? assert_equal 'Hello World', body get '/foo' assert not_found? end it "passes to the next route when host_name does not match" do mock_app { host_name 'example.com' get '/foo' do 'Hello World' end } get '/foo' assert not_found? get '/foo', {}, { 'HTTP_HOST' => 'example.com' } assert_equal 200, status assert_equal 'Hello World', body end it "passes to the next route when user_agent does not match" do mock_app { user_agent(/Foo/) get '/foo' do 'Hello World' end } get '/foo' assert not_found? get '/foo', {}, { 'HTTP_USER_AGENT' => 'Foo Bar' } assert_equal 200, status assert_equal 'Hello World', body end it "treats missing user agent like an empty string" do mock_app do user_agent(/.*/) get '/' do "Hello World" end end get '/' assert_equal 200, status assert_equal 'Hello World', body end it "makes captures in user agent pattern available in params[:agent]" do mock_app { user_agent(/Foo (.*)/) get '/foo' do 'Hello ' + params[:agent].first end } get '/foo', {}, { 'HTTP_USER_AGENT' => 'Foo Bar' } assert_equal 200, status assert_equal 'Hello Bar', body end it 'matches mime_types with dots, hyphens and plus signs' do mime_types = %w( application/atom+xml application/ecmascript application/EDI-X12 application/EDIFACT application/json application/javascript application/octet-stream application/ogg application/pdf application/postscript application/rdf+xml application/rss+xml application/soap+xml application/font-woff application/xhtml+xml application/xml application/xml-dtd application/xop+xml application/zip application/gzip audio/basic audio/L24 audio/mp4 audio/mpeg audio/ogg audio/vorbis audio/vnd.rn-realaudio audio/vnd.wave audio/webm image/gif image/jpeg image/pjpeg image/png image/svg+xml image/tiff image/vnd.microsoft.icon message/http message/imdn+xml message/partial message/rfc822 model/example model/iges model/mesh model/vrml model/x3d+binary model/x3d+vrml model/x3d+xml multipart/mixed multipart/alternative multipart/related multipart/form-data multipart/signed multipart/encrypted text/cmd text/css text/csv text/html text/javascript application/javascript text/plain text/vcard text/xml video/mpeg video/mp4 video/ogg video/quicktime video/webm video/x-matroska video/x-ms-wmv video/x-flv application/vnd.oasis.opendocument.text application/vnd.oasis.opendocument.spreadsheet application/vnd.oasis.opendocument.presentation application/vnd.oasis.opendocument.graphics application/vnd.ms-excel application/vnd.openxmlformats-officedocument.spreadsheetml.sheet application/vnd.ms-powerpoint application/vnd.openxmlformats-officedocument.presentationml.presentation application/vnd.openxmlformats-officedocument.wordprocessingml.document application/vnd.mozilla.xul+xml application/vnd.google-earth.kml+xml application/x-deb application/x-dvi application/x-font-ttf application/x-javascript application/x-latex application/x-mpegURL application/x-rar-compressed application/x-shockwave-flash application/x-stuffit application/x-tar application/x-www-form-urlencoded application/x-xpinstall audio/x-aac audio/x-caf image/x-xcf text/x-gwt-rpc text/x-jquery-tmpl application/x-pkcs12 application/x-pkcs12 application/x-pkcs7-certificates application/x-pkcs7-certificates application/x-pkcs7-certreqresp application/x-pkcs7-mime application/x-pkcs7-mime application/x-pkcs7-signature ) mime_types.each { |mime_type| assert mime_type.match(Sinatra::Request::HEADER_VALUE_WITH_PARAMS) } end it "filters by accept header" do mock_app { get '/', :provides => :xml do env['HTTP_ACCEPT'] end get '/foo', :provides => :html do env['HTTP_ACCEPT'] end get '/stream', :provides => 'text/event-stream' do env['HTTP_ACCEPT'] end } get '/', {}, { 'HTTP_ACCEPT' => 'application/xml' } assert ok? assert_equal 'application/xml', body assert_equal 'application/xml;charset=utf-8', response.headers['Content-Type'] get '/', {}, {} assert ok? assert_equal '', body assert_equal 'application/xml;charset=utf-8', response.headers['Content-Type'] get '/', {}, { 'HTTP_ACCEPT' => '*/*' } assert ok? assert_equal '*/*', body assert_equal 'application/xml;charset=utf-8', response.headers['Content-Type'] get '/', {}, { 'HTTP_ACCEPT' => 'text/html;q=0.9' } assert !ok? get '/foo', {}, { 'HTTP_ACCEPT' => 'text/html;q=0.9' } assert ok? assert_equal 'text/html;q=0.9', body get '/foo', {}, { 'HTTP_ACCEPT' => '' } assert ok? assert_equal '', body get '/foo', {}, { 'HTTP_ACCEPT' => '*/*' } assert ok? assert_equal '*/*', body get '/foo', {}, { 'HTTP_ACCEPT' => 'application/xml' } assert !ok? get '/stream', {}, { 'HTTP_ACCEPT' => 'text/event-stream' } assert ok? assert_equal 'text/event-stream', body get '/stream', {}, { 'HTTP_ACCEPT' => '' } assert ok? assert_equal '', body get '/stream', {}, { 'HTTP_ACCEPT' => '*/*' } assert ok? assert_equal '*/*', body get '/stream', {}, { 'HTTP_ACCEPT' => 'application/xml' } assert !ok? end it "filters by current Content-Type" do mock_app do before('/txt') { content_type :txt } get('*', :provides => :txt) { 'txt' } before('/html') { content_type :html } get('*', :provides => :html) { 'html' } end get '/', {}, { 'HTTP_ACCEPT' => '*/*' } assert ok? assert_equal 'text/plain;charset=utf-8', response.headers['Content-Type'] assert_body 'txt' get '/txt', {}, { 'HTTP_ACCEPT' => 'text/plain' } assert ok? assert_equal 'text/plain;charset=utf-8', response.headers['Content-Type'] assert_body 'txt' get '/', {}, { 'HTTP_ACCEPT' => 'text/html' } assert ok? assert_equal 'text/html;charset=utf-8', response.headers['Content-Type'] assert_body 'html' end it "doesn't allow provides of passed routes to interfere with provides of other routes" do mock_app do get('/:foo', :provides => :txt) do pass if params[:foo] != 'foo' 'foo' end get('/bar', :provides => :html) { 'bar' } end get '/foo', {}, { 'HTTP_ACCEPT' => '*/*' } assert ok? assert_equal 'text/plain;charset=utf-8', response.headers['Content-Type'] assert_body 'foo' get '/bar', {}, { 'HTTP_ACCEPT' => '*/*' } assert ok? assert_equal 'text/html;charset=utf-8', response.headers['Content-Type'] assert_body 'bar' end it "allows multiple mime types for accept header" do types = ['image/jpeg', 'image/pjpeg'] mock_app { get '/', :provides => types do env['HTTP_ACCEPT'] end } types.each do |type| get '/', {}, { 'HTTP_ACCEPT' => type } assert ok? assert_equal type, body assert_equal type, response.headers['Content-Type'] end end it 'respects user agent preferences for the content type' do mock_app { get('/', :provides => [:png, :html]) { content_type }} get '/', {}, { 'HTTP_ACCEPT' => 'image/png;q=0.5,text/html;q=0.8' } assert_body 'text/html;charset=utf-8' get '/', {}, { 'HTTP_ACCEPT' => 'image/png;q=0.8,text/html;q=0.5' } assert_body 'image/png' end it 'accepts generic types' do mock_app do get('/', :provides => :xml) { content_type } get('/') { 'no match' } end get '/', {}, { 'HTTP_ACCEPT' => 'foo/*' } assert_body 'no match' get '/', {}, { 'HTTP_ACCEPT' => 'application/*' } assert_body 'application/xml;charset=utf-8' get '/', {}, { 'HTTP_ACCEPT' => '*/*' } assert_body 'application/xml;charset=utf-8' end it 'prefers concrete over partly generic types' do mock_app { get('/', :provides => [:png, :html]) { content_type }} get '/', {}, { 'HTTP_ACCEPT' => 'image/*, text/html' } assert_body 'text/html;charset=utf-8' get '/', {}, { 'HTTP_ACCEPT' => 'image/png, text/*' } assert_body 'image/png' end it 'prefers concrete over fully generic types' do mock_app { get('/', :provides => [:png, :html]) { content_type }} get '/', {}, { 'HTTP_ACCEPT' => '*/*, text/html' } assert_body 'text/html;charset=utf-8' get '/', {}, { 'HTTP_ACCEPT' => 'image/png, */*' } assert_body 'image/png' end it 'prefers partly generic over fully generic types' do mock_app { get('/', :provides => [:png, :html]) { content_type }} get '/', {}, { 'HTTP_ACCEPT' => '*/*, text/*' } assert_body 'text/html;charset=utf-8' get '/', {}, { 'HTTP_ACCEPT' => 'image/*, */*' } assert_body 'image/png' end it 'respects quality with generic types' do mock_app { get('/', :provides => [:png, :html]) { content_type }} get '/', {}, { 'HTTP_ACCEPT' => 'image/*;q=1, text/html;q=0' } assert_body 'image/png' get '/', {}, { 'HTTP_ACCEPT' => 'image/png;q=0.5, text/*;q=0.7' } assert_body 'text/html;charset=utf-8' end it 'supplies a default quality of 1.0' do mock_app { get('/', :provides => [:png, :html]) { content_type }} get '/', {}, { 'HTTP_ACCEPT' => 'image/png;q=0.5, text/*' } assert_body 'text/html;charset=utf-8' end it 'orders types with equal quality by parameter count' do mock_app do get('/', :provides => [:png, :jpg]) { content_type } end lo_png = 'image/png;q=0.5' hi_png = 'image/png;q=0.5;profile=FOGRA40;gamma=0.8' jpeg = 'image/jpeg;q=0.5;compress=0.25' get '/', {}, { 'HTTP_ACCEPT' => "#{lo_png}, #{jpeg}" } assert_body 'image/jpeg' get '/', {}, { 'HTTP_ACCEPT' => "#{hi_png}, #{jpeg}" } assert_body 'image/png' end it 'ignores the quality parameter when ordering by parameter count' do mock_app do get('/', :provides => [:png, :jpg]) { content_type } end lo_png = 'image/png' hi_png = 'image/png;profile=FOGRA40;gamma=0.8' jpeg = 'image/jpeg;q=1.0;compress=0.25' get '/', {}, { 'HTTP_ACCEPT' => "#{jpeg}, #{lo_png}" } assert_body 'image/jpeg' get '/', {}, { 'HTTP_ACCEPT' => "#{jpeg}, #{hi_png}" } assert_body 'image/png' end it 'properly handles quoted strings in parameters' do mock_app do get('/', :provides => [:png, :jpg]) { content_type } end get '/', {}, { 'HTTP_ACCEPT' => 'image/png;q=0.5;profile=",image/jpeg,"' } assert_body 'image/png' get '/', {}, { 'HTTP_ACCEPT' => 'image/png;q=0.5,image/jpeg;q=0;x=";q=1.0"' } assert_body 'image/png' get '/', {}, { 'HTTP_ACCEPT' => 'image/png;q=0.5,image/jpeg;q=0;x="\";q=1.0"' } assert_body 'image/png' end it 'accepts both text/javascript and application/javascript for js' do mock_app { get('/', :provides => :js) { content_type }} get '/', {}, { 'HTTP_ACCEPT' => 'application/javascript' } assert_body 'application/javascript;charset=utf-8' get '/', {}, { 'HTTP_ACCEPT' => 'text/javascript' } assert_body 'text/javascript;charset=utf-8' end it 'accepts both text/xml and application/xml for xml' do mock_app { get('/', :provides => :xml) { content_type }} get '/', {}, { 'HTTP_ACCEPT' => 'application/xml' } assert_body 'application/xml;charset=utf-8' get '/', {}, { 'HTTP_ACCEPT' => 'text/xml' } assert_body 'text/xml;charset=utf-8' end it 'matches content-type to mime_type' do mime_type = 'application/rss+xml;version="http://purl.org/rss/1.0/"' mock_app do configure { mime_type(:rss10, mime_type) } get('/', :provides => [:rss10]) { content_type } end get '/', {}, { 'HTTP_ACCEPT' => 'application/rss+xml' } assert ok? assert_body mime_type end it 'handles missing mime_types with 404' do mock_app do configure { mime_type(:rss10, 'application/rss+xml') } get('/', :provides => [:jpg]) { content_type } end get '/', {}, { 'HTTP_ACCEPT' => 'application/rss+xml' } assert_equal 404, status assert_equal 'text/html;charset=utf-8', response['Content-Type'] end it 'passes a single url param as block parameters when one param is specified' do mock_app { get '/:foo' do |foo| assert_equal 'bar', foo end } get '/bar' assert ok? end it 'passes multiple params as block parameters when many are specified' do mock_app { get '/:foo/:bar/:baz' do |foo, bar, baz| assert_equal 'abc', foo assert_equal 'def', bar assert_equal 'ghi', baz end } get '/abc/def/ghi' assert ok? end it 'passes regular expression captures as block parameters' do mock_app { get(/\/fo(.*)\/ba(.*)/) do |foo, bar| assert_equal 'orooomma', foo assert_equal 'f', bar 'looks good' end } get '/foorooomma/baf' assert ok? assert_equal 'looks good', body end it "supports mixing multiple splat params like /*/foo/*/* as block parameters" do mock_app { get '/*/foo/*/*' do |foo, bar, baz| assert_equal 'bar', foo assert_equal 'bling', bar assert_equal 'baz/boom', baz 'looks good' end } get '/bar/foo/bling/baz/boom' assert ok? assert_equal 'looks good', body end it "uses the default encoding for block parameters" do mock_app { set :default_encoding ,'ISO-8859-1' get '/:foo/:bar' do |foo, bar| "foo=#{foo.encoding};bar=#{bar.encoding}" end } get '/f%C3%B6%C3%B6/b%C3%B6%C3%B6' assert ok? assert_equal 'foo=ISO-8859-1;bar=ISO-8859-1', body end it 'raises an ArgumentError with block arity > 1 and too many values' do mock_app do get '/:foo/:bar/:baz' do |foo, bar| 'quux' end end assert_raises(ArgumentError) { get '/a/b/c' } end it 'raises an ArgumentError with block param arity > 1 and too few values' do mock_app { get '/:foo/:bar' do |foo, bar, baz| 'quux' end } assert_raises(ArgumentError) { get '/a/b' } end it 'succeeds if no block parameters are specified' do mock_app { get '/:foo/:bar' do 'quux' end } get '/a/b' assert ok? assert_equal 'quux', body end it 'passes all params with block param arity -1 (splat args)' do mock_app { get '/:foo/:bar' do |*args| args.join end } get '/a/b' assert ok? assert_equal 'ab', body end it 'allows custom route-conditions to be set via route options' do protector = Module.new { def protect(*args) condition { unless authorize(params["user"], params["password"]) halt 403, "go away" end } end } mock_app { register protector helpers do def authorize(username, password) username == "foo" && password == "bar" end end get "/", :protect => true do "hey" end } get "/" assert forbidden? assert_equal "go away", body get "/", :user => "foo", :password => "bar" assert ok? assert_equal "hey", body end it 'raises an ArgumentError with block param arity 1 and no values' do mock_app { get '/foo' do |foo| 'quux' end } assert_raises(ArgumentError) { get '/foo' } end it 'raises an ArgumentError with block param arity 1 and too many values' do mock_app { get '/:foo/:bar/:baz' do |foo| 'quux' end } assert_raises(ArgumentError) { get '/a/b/c' } end it "matches routes defined in superclasses" do base = Class.new(Sinatra::Base) base.get('/foo') { 'foo in baseclass' } mock_app(base) { get('/bar') { 'bar in subclass' } } get '/foo' assert ok? assert_equal 'foo in baseclass', body get '/bar' assert ok? assert_equal 'bar in subclass', body end it "matches routes in subclasses before superclasses" do base = Class.new(Sinatra::Base) base.get('/foo') { 'foo in baseclass' } base.get('/bar') { 'bar in baseclass' } mock_app(base) { get('/foo') { 'foo in subclass' } } get '/foo' assert ok? assert_equal 'foo in subclass', body get '/bar' assert ok? assert_equal 'bar in baseclass', body end it "adds hostname condition when it is in options" do mock_app { get '/foo', :host => 'host' do 'foo' end } get '/foo' assert not_found? end it 'allows using call to fire another request internally' do mock_app do get '/foo' do status, headers, body = call env.merge("PATH_INFO" => '/bar') [status, headers, body.each.map(&:upcase)] end get '/bar' do "bar" end end get '/foo' assert ok? assert_body "BAR" end it 'plays well with other routing middleware' do middleware = Sinatra.new inner_app = Sinatra.new { get('/foo') { 'hello' } } builder = Rack::Builder.new do use middleware map('/test') { run inner_app } end @app = builder.to_app get '/test/foo' assert ok? assert_body 'hello' end it 'returns the route signature' do signature = list = nil mock_app do signature = post('/') { } list = routes['POST'] end assert_equal Array, signature.class assert_equal 3, signature.length assert list.include?(signature) end it "sets env['sinatra.route'] to the matched route" do mock_app do after do assert_equal 'GET /users/:id/status', env['sinatra.route'] end get('/users/:id/status') { 'ok' } end get '/users/1/status' end it 'treats routes with and without trailing slashes differently' do mock_app do get '/foo' do 'Foo' end get '/foo/' do 'Foo with a slash' end end get '/foo' assert_equal 'Foo', body refute_equal 'Foo with a slash', body get '/foo/' assert_equal 'Foo with a slash', body end it 'does not treat routes with and without trailing slashes differently if :strict_paths is disabled' do mock_app do disable :strict_paths get '/foo' do 'foo' end end get '/foo' assert_equal 'foo', body get '/foo/' assert_equal 'foo', body end end sinatra-3.0.5/test/server_test.rb000066400000000000000000000035561434717561400170560ustar00rootroot00000000000000require File.expand_path('helper', __dir__) require 'stringio' module Rack::Handler class Mock extend Minitest::Assertions # Allow assertions in request context def self.assertions @assertions ||= 0 end def self.assertions= assertions @assertions = assertions end def self.run(app, options={}) assert(app < Sinatra::Base) assert_equal 9001, options[:Port] assert_equal 'foo.local', options[:Host] yield new end def stop end end register 'mock', 'Rack::Handler::Mock' end class ServerTest < Minitest::Test setup do mock_app do set :server, 'mock' set :bind, 'foo.local' set :port, 9001 end $stderr = StringIO.new end def teardown $stderr = STDERR end it "locates the appropriate Rack handler and calls ::run" do @app.run! end it "sets options on the app before running" do @app.run! :sessions => true assert @app.sessions? end it "falls back on the next server handler when not found" do @app.run! :server => %w[foo bar mock] end it "initializes Rack middleware immediately on server run" do class MyMiddleware @@initialized = false def initialize(app) @@initialized = true end def self.initialized @@initialized end def call(env) end end @app.use MyMiddleware assert_equal(MyMiddleware.initialized, false) @app.run! assert_equal(MyMiddleware.initialized, true) end describe "Quiet mode" do it "sends data to stderr when server starts and stops" do @app.run! assert_match(/\=\= Sinatra/, $stderr.string) end context "when quiet mode is activated" do it "does not generate Sinatra start and stop messages" do @app.run! quiet: true refute_match(/\=\= Sinatra/, $stderr.string) end end end end sinatra-3.0.5/test/settings_test.rb000066400000000000000000000412661434717561400174100ustar00rootroot00000000000000require File.expand_path('helper', __dir__) class SettingsTest < Minitest::Test setup do @base = Sinatra.new(Sinatra::Base) @base.set :environment => :foo, :app_file => nil @application = Sinatra.new(Sinatra::Application) @application.set :environment => :foo, :app_file => nil end it 'sets settings to literal values' do @base.set(:foo, 'bar') assert @base.respond_to?(:foo) assert_equal 'bar', @base.foo end it 'sets settings to Procs' do @base.set(:foo, Proc.new { 'baz' }) assert @base.respond_to?(:foo) assert_equal 'baz', @base.foo end it 'sets settings using a block' do @base.set(:foo){ 'baz' } assert @base.respond_to?(:foo) assert_equal 'baz', @base.foo end it 'raises an error with a value and a block' do assert_raises ArgumentError do @base.set(:fiz, 'boom!'){ 'baz' } end assert !@base.respond_to?(:fiz) end it 'raises an error without value and block' do assert_raises(ArgumentError) { @base.set(:fiz) } assert !@base.respond_to?(:fiz) end it 'allows setting a value to the app class' do @base.set :base, @base assert @base.respond_to?(:base) assert_equal @base, @base.base end it 'raises an error with the app class as value and a block' do assert_raises ArgumentError do @base.set(:fiz, @base) { 'baz' } end assert !@base.respond_to?(:fiz) end it "sets multiple settings with a Hash" do @base.set :foo => 1234, :bar => 'Hello World', :baz => Proc.new { 'bizzle' } assert_equal 1234, @base.foo assert_equal 'Hello World', @base.bar assert_equal 'bizzle', @base.baz end it 'sets multiple settings using #each' do @base.set [["foo", "bar"]] assert_equal "bar", @base.foo end it 'inherits settings methods when subclassed' do @base.set :foo, 'bar' @base.set :biz, Proc.new { 'baz' } sub = Class.new(@base) assert sub.respond_to?(:foo) assert_equal 'bar', sub.foo assert sub.respond_to?(:biz) assert_equal 'baz', sub.biz end it 'overrides settings in subclass' do @base.set :foo, 'bar' @base.set :biz, Proc.new { 'baz' } sub = Class.new(@base) sub.set :foo, 'bling' assert_equal 'bling', sub.foo assert_equal 'bar', @base.foo end it 'creates setter methods when first defined' do @base.set :foo, 'bar' assert @base.respond_to?('foo=') @base.foo = 'biz' assert_equal 'biz', @base.foo end it 'creates predicate methods when first defined' do @base.set :foo, 'hello world' assert @base.respond_to?(:foo?) assert @base.foo? @base.set :foo, nil assert !@base.foo? end it 'uses existing setter methods if detected' do class << @base def foo @foo end def foo=(value) @foo = 'oops' end end @base.set :foo, 'bam' assert_equal 'oops', @base.foo end it 'merges values of multiple set calls if those are hashes' do @base.set :foo, :a => 1 sub = Class.new(@base) sub.set :foo, :b => 2 assert_equal({:a => 1, :b => 2}, sub.foo) end it 'merging does not affect the superclass' do @base.set :foo, :a => 1 sub = Class.new(@base) sub.set :foo, :b => 2 assert_equal({:a => 1}, @base.foo) end it 'is possible to change a value from a hash to something else' do @base.set :foo, :a => 1 @base.set :foo, :bar assert_equal(:bar, @base.foo) end it 'merges values with values of the superclass if those are hashes' do @base.set :foo, :a => 1 @base.set :foo, :b => 2 assert_equal({:a => 1, :b => 2}, @base.foo) end it "sets multiple settings to true with #enable" do @base.enable :sessions, :foo, :bar assert @base.sessions assert @base.foo assert @base.bar end it "sets multiple settings to false with #disable" do @base.disable :sessions, :foo, :bar assert !@base.sessions assert !@base.foo assert !@base.bar end it 'is accessible from instances via #settings' do assert_equal :foo, @base.new!.settings.environment end it 'is accessible from class via #settings' do assert_equal :foo, @base.settings.environment end describe 'default_content_type' do it 'defaults to html' do assert_equal 'text/html', @base.default_content_type end it 'can be changed' do @base.set :default_content_type, 'application/json' @base.get('/') { '{"a":1}' } @app = @base get '/' assert_equal 200, status assert_equal 'application/json', response.content_type end it 'can be disabled' do @base.set :default_content_type, nil @base.error(404) { "" } @app = @base get '/' assert_equal 404, status assert_nil response.content_type assert_empty body end it 'may emit content without a content-type (to be sniffed)' do @base.set :default_content_type, nil @base.get('/') { raise Sinatra::BadRequest, "This is a drill" } @app = @base get '/' assert_equal 400, status assert_nil response.content_type assert_body "This is a drill" end end describe 'methodoverride' do it 'is disabled on Base' do assert ! @base.method_override? end it 'is enabled on Application' do assert @application.method_override? end it 'enables MethodOverride middleware' do @base.set :method_override, true @base.put('/') { 'okay' } @app = @base post '/', {'_method'=>'PUT'}, {} assert_equal 200, status assert_equal 'okay', body end it 'is backward compatible with methodoverride' do assert ! @base.methodoverride? @base.enable :methodoverride assert @base.methodoverride? end it 'ignores bundler/inline from callers' do @application.stub(:caller, ->(_){ ['/path/to/bundler/inline.rb', $0] }) do assert_equal File.expand_path($0), File.expand_path(@application.send(:caller_files).first) end end end describe 'run' do it 'is disabled on Base' do assert ! @base.run? end it 'is enabled on Application except in test environment' do assert @application.run? @application.set :environment, :test assert ! @application.run? end end describe 'raise_errors' do it 'is enabled on Base only in test' do assert ! @base.raise_errors? @base.set(:environment, :test) assert @base.raise_errors? end it 'is enabled on Application only in test' do assert ! @application.raise_errors? @application.set(:environment, :test) assert @application.raise_errors? end end describe 'show_exceptions' do it 'is disabled on Base except under development' do assert ! @base.show_exceptions? @base.environment = :development assert @base.show_exceptions? end it 'is disabled on Application except in development' do assert ! @application.show_exceptions? @application.set(:environment, :development) assert @application.show_exceptions? end it 'returns a friendly 500' do klass = Sinatra.new(Sinatra::Application) mock_app(klass) { enable :show_exceptions get '/' do raise StandardError end } get '/' assert_equal 500, status assert body.include?("StandardError") assert body.include?("show_exceptions setting") end it 'does not attempt to show unparseable query parameters' do klass = Sinatra.new(Sinatra::Application) mock_app(klass) { enable :show_exceptions get '/' do raise Sinatra::BadRequest end } get '/' assert_equal 400, status refute body.include?('
') refute body.include?('
') end it 'does not override app-specified error handling when set to :after_handler' do ran = false mock_app do set :show_exceptions, :after_handler error(RuntimeError) { ran = true } get('/') { raise RuntimeError } end get '/' assert_equal 500, status assert ran end it 'does catch any other exceptions when set to :after_handler' do ran = false mock_app do set :show_exceptions, :after_handler error(RuntimeError) { ran = true } get('/') { raise ArgumentError } end get '/' assert_equal 500, status assert !ran end end describe 'dump_errors' do it 'is disabled on Base in test' do @base.environment = :test assert ! @base.dump_errors? @base.environment = :development assert @base.dump_errors? @base.environment = :production assert @base.dump_errors? end it 'dumps exception with backtrace to rack.errors' do klass = Sinatra.new(Sinatra::Application) mock_app(klass) { enable :dump_errors disable :raise_errors error do error = @env['rack.errors'].instance_variable_get(:@error) error.rewind error.read end get '/' do raise end } get '/' assert body.include?("RuntimeError") && body.include?("settings_test.rb") end it 'does not dump 404 errors' do klass = Sinatra.new(Sinatra::Application) mock_app(klass) { enable :dump_errors disable :raise_errors error do error = @env['rack.errors'].instance_variable_get(:@error) error.rewind error.read end get '/' do raise Sinatra::NotFound end } get '/' assert !body.include?("NotFound") && !body.include?("settings_test.rb") end end describe 'sessions' do it 'is disabled on Base' do assert ! @base.sessions? end it 'is disabled on Application' do assert ! @application.sessions? end end describe 'logging' do it 'is disabled on Base' do assert ! @base.logging? end it 'is enabled on Application except in test environment' do assert @application.logging? @application.set :environment, :test assert ! @application.logging end end describe 'static' do it 'is disabled on Base by default' do assert ! @base.static? end it 'is enabled on Base when public_folder is set and exists' do @base.set :environment, :development @base.set :public_folder, __dir__ assert @base.static? end it 'is enabled on Base when root is set and root/public_folder exists' do @base.set :environment, :development @base.set :root, __dir__ assert @base.static? end it 'is disabled on Application by default' do assert ! @application.static? end it 'is enabled on Application when public_folder is set and exists' do @application.set :environment, :development @application.set :public_folder, __dir__ assert @application.static? end it 'is enabled on Application when root is set and root/public_folder exists' do @application.set :environment, :development @application.set :root, __dir__ assert @application.static? end it 'is possible to use Module#public' do @base.send(:define_method, :foo) { } @base.send(:private, :foo) assert !@base.public_method_defined?(:foo) @base.send(:public, :foo) assert @base.public_method_defined?(:foo) end it 'is possible to use the keyword public in a sinatra app' do app = Sinatra.new do private def priv; end public def pub; end end assert !app.public_method_defined?(:priv) assert app.public_method_defined?(:pub) end end describe 'bind' do it 'defaults to 0.0.0.0' do assert_equal '0.0.0.0', @base.bind assert_equal '0.0.0.0', @application.bind end end describe 'port' do it 'defaults to 4567' do assert_equal 4567, @base.port assert_equal 4567, @application.port end end describe 'server' do it 'includes webrick' do assert @base.server.include?('webrick') assert @application.server.include?('webrick') end it 'includes puma' do assert @base.server.include?('puma') assert @application.server.include?('puma') end it 'includes falcon on non-jruby' do if RUBY_ENGINE != 'jruby' assert @base.server.include?('falcon') assert @application.server.include?('falcon') else assert !@base.server.include?('falcon') assert !@application.server.include?('falcon') end end end describe 'app_file' do it 'is nil for base classes' do assert_nil Sinatra::Base.app_file assert_nil Sinatra::Application.app_file end it 'defaults to the file subclassing' do assert_equal File.expand_path(__FILE__), Sinatra.new.app_file end end describe 'root' do it 'is nil if app_file is not set' do assert_nil @base.root assert_nil @application.root end it 'is equal to the expanded basename of app_file' do @base.app_file = __FILE__ assert_equal File.expand_path(__dir__), @base.root @application.app_file = __FILE__ assert_equal File.expand_path(__dir__), @application.root end end describe 'views' do it 'is nil if root is not set' do assert_nil @base.views assert_nil @application.views end it 'is set to root joined with views/' do @base.root = __dir__ assert_equal __dir__ + "/views", @base.views @application.root = __dir__ assert_equal __dir__ + "/views", @application.views end end describe 'public_folder' do it 'is nil if root is not set' do assert_nil @base.public_folder assert_nil @application.public_folder end it 'is set to root joined with public/' do @base.root = __dir__ assert_equal __dir__ + "/public", @base.public_folder @application.root = __dir__ assert_equal __dir__ + "/public", @application.public_folder end end describe 'public_dir' do it 'is an alias for public_folder' do @base.public_dir = __dir__ assert_equal __dir__, @base.public_dir assert_equal @base.public_folder, @base.public_dir @application.public_dir = __dir__ assert_equal __dir__, @application.public_dir assert_equal @application.public_folder, @application.public_dir end end describe 'lock' do it 'is disabled by default' do assert ! @base.lock? assert ! @application.lock? end end describe 'protection' do class MiddlewareTracker < Rack::Builder def self.track Rack.send :remove_const, :Builder Rack.const_set :Builder, MiddlewareTracker MiddlewareTracker.used.clear yield ensure Rack.send :remove_const, :Builder Rack.const_set :Builder, MiddlewareTracker.superclass end def self.used @used ||= [] end def use(middleware, *) MiddlewareTracker.used << middleware super end end it 'sets up Rack::Protection' do MiddlewareTracker.track do Sinatra::Base.new assert_include MiddlewareTracker.used, Rack::Protection end end it 'sets up Rack::Protection::PathTraversal' do MiddlewareTracker.track do Sinatra::Base.new assert_include MiddlewareTracker.used, Rack::Protection::PathTraversal end end it 'does not set up Rack::Protection::PathTraversal when disabling it' do MiddlewareTracker.track do Sinatra.new { set :protection, :except => :path_traversal }.new assert_include MiddlewareTracker.used, Rack::Protection assert !MiddlewareTracker.used.include?(Rack::Protection::PathTraversal) end end it 'sets up RemoteToken if sessions are enabled' do MiddlewareTracker.track do Sinatra.new { enable :sessions }.new assert_include MiddlewareTracker.used, Rack::Protection::RemoteToken end end it 'sets up RemoteToken if sessions are enabled with a custom session store' do MiddlewareTracker.track do Sinatra.new { enable :sessions set :session_store, Rack::Session::Pool }.new assert_include MiddlewareTracker.used, Rack::Session::Pool assert_include MiddlewareTracker.used, Rack::Protection::RemoteToken end end it 'does not set up RemoteToken if sessions are disabled' do MiddlewareTracker.track do Sinatra.new.new assert !MiddlewareTracker.used.include?(Rack::Protection::RemoteToken) end end it 'sets up RemoteToken if it is configured to' do MiddlewareTracker.track do Sinatra.new { set :protection, :session => true }.new assert_include MiddlewareTracker.used, Rack::Protection::RemoteToken end end end end sinatra-3.0.5/test/sinatra_test.rb000066400000000000000000000005421434717561400172010ustar00rootroot00000000000000require File.expand_path('helper', __dir__) class SinatraTest < Minitest::Test it 'creates a new Sinatra::Base subclass on new' do app = Sinatra.new { get('/') { 'Hello World' } } assert_same Sinatra::Base, app.superclass end it "responds to #template_cache" do assert_kind_of Tilt::Cache, Sinatra::Base.new!.template_cache end end sinatra-3.0.5/test/slim_test.rb000066400000000000000000000051231434717561400165040ustar00rootroot00000000000000require File.expand_path('helper', __dir__) begin require 'slim' class SlimTest < Minitest::Test def slim_app(&block) mock_app do set :views, __dir__ + '/views' get('/', &block) end get '/' end it 'renders inline slim strings' do slim_app { slim "h1 Hiya\n" } assert ok? assert_equal "

Hiya

", body end it 'renders .slim files in views path' do slim_app { slim :hello } assert ok? assert_equal "

Hello From Slim

", body end it "renders with inline layouts" do mock_app do layout { %(h1\n | THIS. IS. \n == yield.upcase ) } get('/') { slim 'em Sparta' } end get '/' assert ok? assert_equal "

THIS. IS. SPARTA

", body end it "renders with file layouts" do slim_app { slim('| Hello World', :layout => :layout2) } assert ok? assert_equal "

Slim Layout!

Hello World

", body end it "raises error if template not found" do mock_app { get('/') { slim(:no_such_template) } } assert_raises(Errno::ENOENT) { get('/') } end HTML4_DOCTYPE = "" it "passes slim options to the slim engine" do mock_app { get('/') { slim("x foo='bar'", :attr_quote => "'") }} get '/' assert ok? assert_body "" end it "passes default slim options to the slim engine" do mock_app do set :slim, :attr_quote => "'" get('/') { slim("x foo='bar'") } end get '/' assert ok? assert_body "" end it "merges the default slim options with the overrides and passes them to the slim engine" do mock_app do set :slim, :attr_quote => "'" get('/') { slim("x foo='bar'") } get('/other') { slim("x foo='bar'", :attr_quote => '"') } end get '/' assert ok? assert_body "" get '/other' assert ok? assert_body '' end it "can render truly nested layouts by accepting a layout and a block with the contents" do mock_app do template(:main_outer_layout) { "h1 Title\n== yield" } template(:an_inner_layout) { "h2 Subtitle\n== yield" } template(:a_page) { "p Contents." } get('/') do slim :main_outer_layout, :layout => false do slim :an_inner_layout do slim :a_page end end end end get '/' assert ok? assert_body "

Title

\n

Subtitle

\n

Contents.

\n" end end rescue LoadError warn "#{$!}: skipping slim tests" end sinatra-3.0.5/test/static_test.rb000066400000000000000000000210621434717561400170270ustar00rootroot00000000000000require File.expand_path('helper', __dir__) class StaticTest < Minitest::Test setup do mock_app do set :static, true set :public_folder, __dir__ end end it 'serves GET requests for files in the public directory' do get "/#{File.basename(__FILE__)}" assert ok? assert_equal File.read(__FILE__), body assert_equal File.size(__FILE__).to_s, response['Content-Length'] assert response.headers.include?('Last-Modified') end it 'produces a body that can be iterated over multiple times' do env = Rack::MockRequest.env_for("/#{File.basename(__FILE__)}") _, _, body = @app.call(env) buf1, buf2 = [], [] body.each { |part| buf1 << part } body.each { |part| buf2 << part } assert_equal buf1.join, buf2.join assert_equal File.read(__FILE__), buf1.join end it 'sets the sinatra.static_file env variable if served' do env = Rack::MockRequest.env_for("/#{File.basename(__FILE__)}") @app.call(env) assert_equal File.expand_path(__FILE__), env['sinatra.static_file'] end it 'serves HEAD requests for files in the public directory' do head "/#{File.basename(__FILE__)}" assert ok? assert_equal '', body assert response.headers.include?('Last-Modified') assert_equal File.size(__FILE__).to_s, response['Content-Length'] end %w[POST PUT DELETE].each do |verb| it "does not serve #{verb} requests" do send verb.downcase, "/#{File.basename(__FILE__)}" assert_equal 404, status end end it 'serves files in preference to custom routes' do @app.get("/#{File.basename(__FILE__)}") { 'Hello World' } get "/#{File.basename(__FILE__)}" assert ok? assert body != 'Hello World' end it 'does not serve directories' do get "/" assert not_found? end it 'passes to the next handler when the path contains null bytes' do get "/foo%00" assert not_found? end it 'passes to the next handler when the static option is disabled' do @app.set :static, false get "/#{File.basename(__FILE__)}" assert not_found? end it 'passes to the next handler when the public option is nil' do @app.set :public_folder, nil get "/#{File.basename(__FILE__)}" assert not_found? end it '404s when a file is not found' do get "/foobarbaz.txt" assert not_found? end it 'there is no path is 404 error pages' do env = Rack::MockRequest.env_for("/dummy").tap { |env| env["PATH_INFO"] = "/