diff options
681 files changed, 16420 insertions, 4261 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml index 139ee8013b..908a988e69 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -23,7 +23,7 @@ checks: engines: rubocop: enabled: true - channel: rubocop-0-60 + channel: rubocop-0-63 ratings: paths: diff --git a/.github/autolabeler.yml b/.github/autolabeler.yml index d73b2e3362..6eb6f354e5 100644 --- a/.github/autolabeler.yml +++ b/.github/autolabeler.yml @@ -1,9 +1,13 @@ actioncable: - "actioncable/**/*" +actionmailbox: + - "actionmailbox/**/*" actionmailer: - "actionmailer/**/*" actionpack: - "actionpack/**/*" +actiontext: + - "actiontext/**/*" actionview: - "actionview/**/*" activejob: diff --git a/.rubocop.yml b/.rubocop.yml index ce0b95ddd4..618817daca 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -9,6 +9,7 @@ AllCops: - 'actionpack/lib/action_dispatch/journey/parser.rb' - 'railties/test/fixtures/tmp/**/*' - 'actionmailbox/test/dummy/**/*' + - 'actiontext/test/dummy/**/*' - 'node_modules/**/*' Performance: @@ -134,7 +135,9 @@ Style/FrozenStringLiteralComment: - 'actionpack/test/**/*.builder' - 'actionpack/test/**/*.ruby' - 'activestorage/db/migrate/**/*.rb' + - 'activestorage/db/update_migrate/**/*.rb' - 'actionmailbox/db/migrate/**/*.rb' + - 'actiontext/db/migrate/**/*.rb' Style/RedundantFreeze: Enabled: true @@ -176,16 +179,28 @@ Layout/TrailingWhitespace: Style/UnneededPercentQ: Enabled: true +Lint/ErbNewArguments: + Enabled: true + # Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg. Lint/RequireParentheses: Enabled: true +Lint/ShadowingOuterLocalVariable: + Enabled: true + Lint/StringConversionInInterpolation: Enabled: true Lint/UriEscapeUnescape: Enabled: true +Lint/UselessAssignment: + Enabled: true + +Lint/DeprecatedClassMethods: + Enabled: true + Style/ParenthesesAroundCondition: Enabled: true diff --git a/.travis.yml b/.travis.yml index f3d3c2181a..74f8d4795f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +dist: xenial language: ruby sudo: false @@ -12,30 +13,26 @@ cache: services: - memcached - redis-server + - mysql addons: postgresql: 10 chrome: stable apt: sources: - - sourceline: "ppa:mc3man/trusty-media" + - sourceline: "ppa:jonathonf/ffmpeg-3" - sourceline: "ppa:ubuntuhandbook1/apps" - - mysql-5.7-trusty packages: - ffmpeg - mupdf - mupdf-tools - poppler-utils - - mysql-server - - mysql-client - - postgresql-10 - - postgresql-client-10 bundler_args: --jobs 3 --retry 3 before_install: - "rm ${BUNDLE_GEMFILE}.lock" - "travis_retry gem update --system" - - "travis_retry gem install bundler -v '2.0.0.pre.2'" + - "travis_retry gem install bundler" - "[[ -z $encrypted_0fb9444d0374_key && -z $encrypted_0fb9444d0374_iv ]] || openssl aes-256-cbc -K $encrypted_0fb9444d0374_key -iv $encrypted_0fb9444d0374_iv -in activestorage/test/service/configurations.yml.enc -out activestorage/test/service/configurations.yml -d" - "[[ $GEM != 'actioncable:integration' ]] || yarn install" - "[[ $GEM != 'actionview:ujs' ]] || nvm install node" @@ -43,6 +40,7 @@ before_install: - "[[ $GEM != 'actionview:ujs' ]] || (cd actionview && npm install)" - "[[ $GEM != 'railties' ]] || (curl -o- -L https://yarnpkg.com/install.sh | bash)" - "[[ $GEM != 'railties' ]] || export PATH=$HOME/.yarn/bin:$PATH" + - "[[ $GEM != 'activerecord:postgresql' ]] || sudo mount -o remount,size=50% /var/ramfs" before_script: # Set Sauce Labs username and access key. Obfuscated, purposefully not encrypted. @@ -56,10 +54,13 @@ env: global: - "JRUBY_OPTS='--dev -J-Xmx1024M'" matrix: + - "GEM=railties" - "GEM=actionpack,actioncable" - - "GEM=actionmailer,activemodel,activesupport,actionview,activejob,activestorage,actionmailbox" + - "GEM=actionmailer,activemodel,activesupport,actionview,activejob,activestorage,actionmailbox,actiontext" - "GEM=activesupport PRESERVE_TIMEZONES=1" - "GEM=activerecord:sqlite3" + - "GEM=activerecord:postgresql" + - "GEM=activerecord:mysql2" - "GEM=guides" - "GEM=actioncable:integration" @@ -71,126 +72,72 @@ rvm: matrix: include: - rvm: 2.5.3 - env: "GEM=railties" - sudo: required - before_install: - - "rm ${BUNDLE_GEMFILE}.lock" - - "travis_retry gem update --system" - - "travis_retry gem install bundler" - - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf" - - "sudo service postgresql restart 10" - - rvm: 2.6.0 - env: "GEM=railties" - sudo: required - before_install: - - "rm ${BUNDLE_GEMFILE}.lock" - - "travis_retry gem update --system" - - "travis_retry gem install bundler" - - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf" - - "sudo service postgresql restart 10" - - rvm: ruby-head - env: "GEM=railties" - sudo: required - before_install: - - "rm ${BUNDLE_GEMFILE}.lock" - - "travis_retry gem update --system" - - "travis_retry gem install bundler" - - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf" - - "sudo service postgresql restart 10" - - rvm: 2.5.3 env: "GEM=actionview:ujs" - rvm: 2.5.3 sudo: required env: "GEM=activejob:integration" + addons: + postgresql: 10 + apt: + packages: + - rabbitmq-server services: - memcached - redis-server - - rabbitmq + - rabbitmq-server before_install: - - sudo sed -i -e '/local.*peer/s/postgres/all/' -e 's/peer\|md5/trust/g' /etc/postgresql/*/main/pg_hba.conf - - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf" - - "sudo service postgresql restart 10" - "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/beanstalkd/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)" - "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd" - rvm: 2.6.0 sudo: required env: "GEM=activejob:integration" + addons: + postgresql: 10 + apt: + packages: + - rabbitmq-server services: - memcached - redis-server - - rabbitmq + - rabbitmq-server before_install: - - sudo sed -i -e '/local.*peer/s/postgres/all/' -e 's/peer\|md5/trust/g' /etc/postgresql/*/main/pg_hba.conf - - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf" - - "sudo service postgresql restart 10" - "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/beanstalkd/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)" - "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd" - rvm: ruby-head sudo: required env: "GEM=activejob:integration" + addons: + postgresql: 10 + apt: + packages: + - rabbitmq-server services: - memcached - redis-server - - rabbitmq + - rabbitmq-server before_install: - - sudo sed -i -e '/local.*peer/s/postgres/all/' -e 's/peer\|md5/trust/g' /etc/postgresql/*/main/pg_hba.conf - - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf" - - "sudo service postgresql restart 10" + - "rm ${BUNDLE_GEMFILE}.lock" + - "travis_retry gem update --system" + - "travis_retry gem install bundler" - "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/beanstalkd/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)" - "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd" - rvm: 2.5.3 - env: "GEM=activerecord:mysql2" - sudo: required - before_install: - - "sudo mysql -e \"use mysql; update user set authentication_string='' where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;\"" - - "sudo mysql_upgrade" - - "sudo service mysql restart" - - rvm: 2.6.0 - env: "GEM=activerecord:mysql2" - sudo: required - before_install: - - "sudo mysql -e \"use mysql; update user set authentication_string='' where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;\"" - - "sudo mysql_upgrade" - - "sudo service mysql restart" - - rvm: ruby-head - env: "GEM=activerecord:mysql2" - sudo: required + env: + - "GEM=activerecord:mysql2 MYSQL=mariadb" before_install: - - "sudo mysql -e \"use mysql; update user set authentication_string='' where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;\"" - "sudo mysql_upgrade" - "sudo service mysql restart" - - rvm: 2.5.3 - env: - - "GEM=activerecord:mysql2 MYSQL=mariadb" addons: mariadb: 10.3 - rvm: 2.5.3 env: - "GEM=activerecord:sqlite3_mem" - - rvm: 2.5.3 - env: "GEM=activerecord:postgresql" - sudo: required - before_install: - - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf" - - "sudo service postgresql restart 10" - - rvm: 2.6.0 - env: "GEM=activerecord:postgresql" - sudo: required - before_install: - - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf" - - "sudo service postgresql restart 10" - - rvm: ruby-head - env: "GEM=activerecord:postgresql" - sudo: required - before_install: - - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf" - - "sudo service postgresql restart 10" - rvm: jruby-head - jdk: oraclejdk8 + jdk: oraclejdk11 env: - "GEM=actionpack" - rvm: jruby-head - jdk: oraclejdk8 + jdk: oraclejdk11 env: - "GEM=actionmailer,activemodel,activejob" allow_failures: @@ -10,11 +10,12 @@ gemspec gem "rake", ">= 11.1" gem "capybara", ">= 2.15" +gem "selenium-webdriver", ">= 3.5.0", "< 3.13.0" gem "rack-cache", "~> 1.2" gem "sass-rails" gem "turbolinks", "~> 5" -gem "webpacker", github: "rails/webpacker", require: ENV["SKIP_REQUIRE_WEBPACKER"] != "true" +gem "webpacker", ">= 4.0.0.rc.3", require: ENV["SKIP_REQUIRE_WEBPACKER"] != "true" # require: false so bcrypt is loaded only when has_secure_password is used. # This is to avoid Active Model (and by extension the entire framework) # being dependent on a binary library. @@ -29,9 +30,6 @@ gem "json", ">= 2.0.0" gem "rubocop", ">= 0.47", require: false -# https://github.com/guard/rb-inotify/pull/79 -gem "rb-inotify", github: "matthewd/rb-inotify", branch: "close-handling", require: false - group :doc do gem "sdoc", "~> 1.0" gem "redcarpet", "~> 3.2.3", platforms: :ruby @@ -55,7 +53,7 @@ group :job do gem "sidekiq", require: false gem "sucker_punch", require: false gem "delayed_job", require: false - gem "queue_classic", github: "rafaelfranca/queue_classic", branch: "update-pg", require: false, platforms: :ruby + gem "queue_classic", github: "QueueClassic/queue_classic", require: false, platforms: :ruby gem "sneakers", require: false gem "que", require: false gem "backburner", require: false @@ -120,7 +118,7 @@ platforms :ruby, :mswin, :mswin64, :mingw, :x64_mingw do gem "racc", ">=1.4.6", require: false # Active Record. - gem "sqlite3", "~> 1.3.6" + gem "sqlite3", "~> 1.3", ">= 1.3.6" group :db do gem "pg", ">= 0.18.0" diff --git a/Gemfile.lock b/Gemfile.lock index 22a58b80f1..b05ae7915e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,10 +1,9 @@ GIT - remote: https://github.com/matthewd/rb-inotify.git - revision: 856730aad4b285969e8dd621e44808a7c5af4242 - branch: close-handling + remote: https://github.com/QueueClassic/queue_classic.git + revision: 4cdc9b8e804badf7dea7078dd81092972d292c14 specs: - rb-inotify (0.9.9) - ffi (~> 1.0) + queue_classic (3.2.0.RC1) + pg (>= 0.17, < 2.0) GIT remote: https://github.com/matthewd/websocket-client-simple.git @@ -15,90 +14,80 @@ GIT event_emitter websocket -GIT - remote: https://github.com/rafaelfranca/queue_classic.git - revision: dee64b361355d56700ad7aa3b151bf653a617526 - branch: update-pg - specs: - queue_classic (3.2.0.RC1) - pg (>= 0.17, < 2.0) - -GIT - remote: https://github.com/rails/webpacker.git - revision: bb132d591da35095e3246082cba3d693f847e0b5 - specs: - webpacker (4.0.0.pre.3) - activesupport (>= 4.2) - rack-proxy (>= 0.6.1) - railties (>= 4.2) - PATH remote: . specs: - actioncable (6.0.0.alpha) - actionpack (= 6.0.0.alpha) + actioncable (6.0.0.beta1) + actionpack (= 6.0.0.beta1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.0.0.alpha) - actionpack (= 6.0.0.alpha) - activejob (= 6.0.0.alpha) - activerecord (= 6.0.0.alpha) - activestorage (= 6.0.0.alpha) - activesupport (= 6.0.0.alpha) + actionmailbox (6.0.0.beta1) + actionpack (= 6.0.0.beta1) + activejob (= 6.0.0.beta1) + activerecord (= 6.0.0.beta1) + activestorage (= 6.0.0.beta1) + activesupport (= 6.0.0.beta1) mail (>= 2.7.1) - actionmailer (6.0.0.alpha) - actionpack (= 6.0.0.alpha) - actionview (= 6.0.0.alpha) - activejob (= 6.0.0.alpha) + actionmailer (6.0.0.beta1) + actionpack (= 6.0.0.beta1) + actionview (= 6.0.0.beta1) + activejob (= 6.0.0.beta1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.0.0.alpha) - actionview (= 6.0.0.alpha) - activesupport (= 6.0.0.alpha) + actionpack (6.0.0.beta1) + actionview (= 6.0.0.beta1) + activesupport (= 6.0.0.beta1) rack (~> 2.0) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (6.0.0.alpha) - activesupport (= 6.0.0.alpha) + actiontext (6.0.0.beta1) + actionpack (= 6.0.0.beta1) + activerecord (= 6.0.0.beta1) + activestorage (= 6.0.0.beta1) + activesupport (= 6.0.0.beta1) + nokogiri (>= 1.8.5) + actionview (6.0.0.beta1) + activesupport (= 6.0.0.beta1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (6.0.0.alpha) - activesupport (= 6.0.0.alpha) + activejob (6.0.0.beta1) + activesupport (= 6.0.0.beta1) globalid (>= 0.3.6) - activemodel (6.0.0.alpha) - activesupport (= 6.0.0.alpha) - activerecord (6.0.0.alpha) - activemodel (= 6.0.0.alpha) - activesupport (= 6.0.0.alpha) - activestorage (6.0.0.alpha) - actionpack (= 6.0.0.alpha) - activerecord (= 6.0.0.alpha) + activemodel (6.0.0.beta1) + activesupport (= 6.0.0.beta1) + activerecord (6.0.0.beta1) + activemodel (= 6.0.0.beta1) + activesupport (= 6.0.0.beta1) + activestorage (6.0.0.beta1) + actionpack (= 6.0.0.beta1) + activerecord (= 6.0.0.beta1) marcel (~> 0.3.1) - activesupport (6.0.0.alpha) + activesupport (6.0.0.beta1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) - rails (6.0.0.alpha) - actioncable (= 6.0.0.alpha) - actionmailbox (= 6.0.0.alpha) - actionmailer (= 6.0.0.alpha) - actionpack (= 6.0.0.alpha) - actionview (= 6.0.0.alpha) - activejob (= 6.0.0.alpha) - activemodel (= 6.0.0.alpha) - activerecord (= 6.0.0.alpha) - activestorage (= 6.0.0.alpha) - activesupport (= 6.0.0.alpha) + rails (6.0.0.beta1) + actioncable (= 6.0.0.beta1) + actionmailbox (= 6.0.0.beta1) + actionmailer (= 6.0.0.beta1) + actionpack (= 6.0.0.beta1) + actiontext (= 6.0.0.beta1) + actionview (= 6.0.0.beta1) + activejob (= 6.0.0.beta1) + activemodel (= 6.0.0.beta1) + activerecord (= 6.0.0.beta1) + activestorage (= 6.0.0.beta1) + activesupport (= 6.0.0.beta1) bundler (>= 1.3.0) - railties (= 6.0.0.alpha) + railties (= 6.0.0.beta1) sprockets-rails (>= 2.0.0) - railties (6.0.0.alpha) - actionpack (= 6.0.0.alpha) - activesupport (= 6.0.0.alpha) + railties (6.0.0.beta1) + actionpack (= 6.0.0.beta1) + activesupport (= 6.0.0.beta1) method_source rake (>= 0.8.7) thor (>= 0.20.3, < 2.0) @@ -182,8 +171,8 @@ GEM bootsnap (1.3.2-java) msgpack (~> 1.0) builder (3.2.3) - bunny (2.9.2) - amq-protocol (~> 2.3.0) + bunny (2.13.0) + amq-protocol (~> 2.3, >= 2.3.0) byebug (10.0.2) capybara (3.10.1) addressable @@ -202,7 +191,7 @@ GEM coffee-script-source execjs coffee-script-source (1.12.2) - concurrent-ruby (1.1.3) + concurrent-ruby (1.1.4) connection_pool (2.2.2) cookiejar (0.3.3) crack (0.4.3) @@ -228,7 +217,7 @@ GEM http_parser.rb (>= 0.6.0) em-socksify (0.3.2) eventmachine (>= 1.0.0.beta.4) - erubi (1.7.1) + erubi (1.8.0) et-orbi (1.1.6) tzinfo event_emitter (0.2.6) @@ -249,10 +238,10 @@ GEM faye-websocket (0.10.7) eventmachine (>= 0.12.0) websocket-driver (>= 0.5.1) - ffi (1.9.25) - ffi (1.9.25-java) - ffi (1.9.25-x64-mingw32) - ffi (1.9.25-x86-mingw32) + ffi (1.10.0) + ffi (1.10.0-java) + ffi (1.10.0-x64-mingw32) + ffi (1.10.0-x86-mingw32) fugit (1.1.6) et-orbi (~> 1.1, >= 1.1.6) raabro (~> 1.1) @@ -287,14 +276,14 @@ GEM hiredis (0.6.3-java) http_parser.rb (0.6.0) httpclient (2.8.3) - i18n (1.1.1) + i18n (1.5.2) concurrent-ruby (~> 1.0) image_processing (1.7.1) mini_magick (~> 4.0) ruby-vips (>= 2.0.13, < 3) io-like (0.3.0) - jaro_winkler (1.5.1) - jaro_winkler (1.5.1-java) + jaro_winkler (1.5.2) + jaro_winkler (1.5.2-java) jdbc-mysql (5.1.46) jdbc-postgres (42.1.4) jdbc-sqlite3 (3.20.1) @@ -325,7 +314,7 @@ GEM mimemagic (0.3.3) mini_magick (4.9.2) mini_mime (1.0.1) - mini_portile2 (2.3.0) + mini_portile2 (2.4.0) minitest (5.11.3) minitest-bisect (1.4.0) minitest-server (~> 1.0) @@ -348,16 +337,16 @@ GEM mysql2 (0.5.2-x86-mingw32) nio4r (2.3.1) nio4r (2.3.1-java) - nokogiri (1.8.5) - mini_portile2 (~> 2.3.0) - nokogiri (1.8.5-java) - nokogiri (1.8.5-x64-mingw32) - mini_portile2 (~> 2.3.0) - nokogiri (1.8.5-x86-mingw32) - mini_portile2 (~> 2.3.0) + nokogiri (1.9.1) + mini_portile2 (~> 2.4.0) + nokogiri (1.9.1-java) + nokogiri (1.9.1-x64-mingw32) + mini_portile2 (~> 2.4.0) + nokogiri (1.9.1-x86-mingw32) + mini_portile2 (~> 2.4.0) os (1.0.0) - parallel (1.12.1) - parser (2.5.3.0) + parallel (1.13.0) + parser (2.6.0.0) ast (~> 2.4.0) path_expander (1.0.3) pg (1.1.3) @@ -391,6 +380,8 @@ GEM rainbow (3.0.0) rake (12.3.1) rb-fsevent (0.10.3) + rb-inotify (0.10.0) + ffi (~> 1.0) rdoc (6.0.4) redcarpet (3.2.3) redis (4.0.3) @@ -413,7 +404,7 @@ GEM resque (~> 1.26) rufus-scheduler (~> 3.2) retriable (3.1.2) - rubocop (0.61.1) + rubocop (0.63.0) jaro_winkler (~> 1.5.1) parallel (~> 1.10) parser (>= 2.5, != 2.5.1.1) @@ -442,9 +433,9 @@ GEM tilt (>= 1.1, < 3) sdoc (1.0.0) rdoc (>= 5.0) - selenium-webdriver (3.141.0) + selenium-webdriver (3.12.0) childprocess (~> 0.5) - rubyzip (~> 1.2, >= 1.2.2) + rubyzip (~> 1.2) sequel (5.14.0) serverengine (2.0.7) sigdump (~> 0.2.2) @@ -463,9 +454,10 @@ GEM rack (~> 2.0) rack-protection (= 2.0.4) tilt (~> 2.0) - sneakers (2.7.0) - bunny (~> 2.9.2) + sneakers (2.11.0) + bunny (~> 2.12) concurrent-ruby (~> 1.0) + rake serverengine (~> 2.0.5) thor sprockets (3.7.2) @@ -512,6 +504,10 @@ GEM addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff + webpacker (4.0.0.rc.3) + activesupport (>= 4.2) + rack-proxy (>= 0.6.1) + railties (>= 4.2) websocket (1.2.8) websocket-driver (0.7.0) websocket-extensions (>= 0.1.0) @@ -568,7 +564,6 @@ DEPENDENCIES rack-cache (~> 1.2) rails! rake (>= 11.1) - rb-inotify! redcarpet (~> 3.2.3) redis (~> 4.0) redis-namespace @@ -577,11 +572,12 @@ DEPENDENCIES rubocop (>= 0.47) sass-rails sdoc (~> 1.0) + selenium-webdriver (>= 3.5.0, < 3.13.0) sequel sidekiq sneakers sprockets-export - sqlite3 (~> 1.3.6) + sqlite3 (~> 1.3, >= 1.3.6) stackprof sucker_punch turbolinks (~> 5) @@ -590,7 +586,7 @@ DEPENDENCIES w3c_validators wdm (>= 0.1.0) webmock - webpacker! + webpacker (>= 4.0.0.rc.3) websocket-client-simple! BUNDLED WITH diff --git a/MIT-LICENSE b/MIT-LICENSE index 8f769c0767..315a4bc7c4 100644 --- a/MIT-LICENSE +++ b/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2005-2018 David Heinemeier Hansson +Copyright (c) 2005-2019 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/RAILS_VERSION b/RAILS_VERSION index 747766c587..248da9b7b5 100644 --- a/RAILS_VERSION +++ b/RAILS_VERSION @@ -1 +1 @@ -6.0.0.alpha +6.0.0.beta1 @@ -44,11 +44,11 @@ or to generate the body of an email. In Rails, View generation is handled by [Ac [Active Record](activerecord/README.rdoc), [Active Model](activemodel/README.rdoc), [Action Pack](actionpack/README.rdoc), and [Action View](actionview/README.rdoc) can each be used independently outside Rails. In addition to that, Rails also comes with [Action Mailer](actionmailer/README.rdoc), a library -to generate and send emails; [Active Job](activejob/README.md), a -framework for declaring jobs and making them run on a variety of queuing +to generate and send emails; [Action Mailbox](actionmailbox/README.md), a library to receive emails within a Rails application; +[Active Job](activejob/README.md), a framework for declaring jobs and making them run on a variety of queuing backends; [Action Cable](actioncable/README.md), a framework to integrate WebSockets with a Rails application; [Active Storage](activestorage/README.md), a library to attach cloud -and local files to Rails applications; +and local files to Rails applications; [Action Text](actiontext/README.md), a library to handle rich text content; and [Active Support](activesupport/README.rdoc), a collection of utility classes and standard library extensions that are useful for Rails, and may also be used independently outside Rails. diff --git a/RELEASING_RAILS.md b/RELEASING_RAILS.md index 287dd4fa12..7fa180cc43 100644 --- a/RELEASING_RAILS.md +++ b/RELEASING_RAILS.md @@ -109,13 +109,14 @@ browser. This will stop you from looking silly when you push an RC to rubygems.org and then realize it is broken. -### Release to RubyGems and NPM. - -IMPORTANT: The Action Cable client and Action View's UJS adapter are released -as NPM packages, so you must have Node.js installed, have an NPM account -(npmjs.com), and be a package owner for `actioncable` and `rails-ujs` (you can -check this via `npm owner ls actioncable` and `npm owner ls rails-ujs`) in -order to do a full release. Do not release until you're set up with NPM! +### Release to RubyGems and npm. + +IMPORTANT: Several gems have JavaScript components that are released as npm +packages, so you must have Node.js installed, have an npm account (npmjs.com), +and be a package owner for `@rails/actioncable`, `@rails/actiontext`, +`@rails/activestorage`, and `@rails/ujs`. You can check this by making sure your +npm user (`npm whoami`) is listed as an owner (`npm owner ls <pkg>`) of each +package. Do not release until you're set up with npm! The release task will sign the release tag. If you haven't got commit signing set up, use https://git-scm.com/book/tr/v2/Git-Tools-Signing-Your-Work as a diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md index 162de0df0b..42edbd6954 100644 --- a/actioncable/CHANGELOG.md +++ b/actioncable/CHANGELOG.md @@ -1,3 +1,47 @@ +* Add `:action_cable_connection` and `:action_cable_channel` load hooks. + + You can use them to extend `ActionCable::Connection::Base` and `ActionCable::Channel::Base` + functionality: + + ```ruby + ActiveSupport.on_load(:action_cable_channel) do + # do something in the context of ActionCable::Channel::Base + end + ``` + + *Vladimir Dementyev* + +* Add `Channel::Base#broadcast_to`. + + You can now call `broadcast_to` within a channel action, which equals to + the `self.class.broadcast_to`. + + *Vladimir Dementyev* + +* Make `Channel::Base.broadcasting_for` a public API. + + You can use `.broadcasting_for` to generate a unique stream identifier within + a channel for the specified target (e.g. Active Record model): + + ```ruby + ChatChannel.broadcasting_for(model) # => "chat:<model.to_gid_param>" + ``` + + *Vladimir Dementyev* + + +## Rails 6.0.0.beta1 (January 18, 2019) ## + +* [Rename npm package](https://github.com/rails/rails/pull/34905) from + [`actioncable`](https://www.npmjs.com/package/actioncable) to + [`@rails/actioncable`](https://www.npmjs.com/package/@rails/actioncable). + + *Javan Makhmali* + +* Merge [`action-cable-testing`](https://github.com/palkan/action-cable-testing) to Rails. + + *Vladimir Dementyev* + * The JavaScript WebSocket client will no longer try to reconnect when you call `reject_unauthorized_connection` on the connection. diff --git a/actioncable/MIT-LICENSE b/actioncable/MIT-LICENSE index a42759f024..bdb42d988a 100644 --- a/actioncable/MIT-LICENSE +++ b/actioncable/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015-2018 Basecamp, LLC +Copyright (c) 2015-2019 Basecamp, LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actioncable/README.md b/actioncable/README.md index a2783d6f45..60c879e1f4 100644 --- a/actioncable/README.md +++ b/actioncable/README.md @@ -7,550 +7,7 @@ and scalable. It's a full-stack offering that provides both a client-side JavaScript framework and a server-side Ruby framework. You have access to your full domain model written with Active Record or your ORM of choice. -## Terminology - -A single Action Cable server can handle multiple connection instances. It has one -connection instance per WebSocket connection. A single user may have multiple -WebSockets open to your application if they use multiple browser tabs or devices. -The client of a WebSocket connection is called the consumer. - -Each consumer can in turn subscribe to multiple cable channels. Each channel encapsulates -a logical unit of work, similar to what a controller does in a regular MVC setup. For example, -you could have a `ChatChannel` and an `AppearancesChannel`, and a consumer could be subscribed to either -or to both of these channels. At the very least, a consumer should be subscribed to one channel. - -When the consumer is subscribed to a channel, they act as a subscriber. The connection between -the subscriber and the channel is, surprise-surprise, called a subscription. A consumer -can act as a subscriber to a given channel any number of times. For example, a consumer -could subscribe to multiple chat rooms at the same time. (And remember that a physical user may -have multiple consumers, one per tab/device open to your connection). - -Each channel can then again be streaming zero or more broadcastings. A broadcasting is a -pubsub link where anything transmitted by the broadcaster is sent directly to the channel -subscribers who are streaming that named broadcasting. - -As you can see, this is a fairly deep architectural stack. There's a lot of new terminology -to identify the new pieces, and on top of that, you're dealing with both client and server side -reflections of each unit. - -## Examples - -### A full-stack example - -The first thing you must do is define your `ApplicationCable::Connection` class in Ruby. This -is the place where you authorize the incoming connection, and proceed to establish it, -if all is well. Here's the simplest example starting with the server-side connection class: - -```ruby -# app/channels/application_cable/connection.rb -module ApplicationCable - class Connection < ActionCable::Connection::Base - identified_by :current_user - - def connect - self.current_user = find_verified_user - end - - private - def find_verified_user - if verified_user = User.find_by(id: cookies.encrypted[:user_id]) - verified_user - else - reject_unauthorized_connection - end - end - end -end -``` -Here `identified_by` is a connection identifier that can be used to find the specific connection again or later. -Note that anything marked as an identifier will automatically create a delegate by the same name on any channel instances created off the connection. - -This relies on the fact that you will already have handled authentication of the user, and -that a successful authentication sets a signed cookie with the `user_id`. This cookie is then -automatically sent to the connection instance when a new connection is attempted, and you -use that to set the `current_user`. By identifying the connection by this same current_user, -you're also ensuring that you can later retrieve all open connections by a given user (and -potentially disconnect them all if the user is deleted or deauthorized). - -Next, you should define your `ApplicationCable::Channel` class in Ruby. This is the place where you put -shared logic between your channels. - -```ruby -# app/channels/application_cable/channel.rb -module ApplicationCable - class Channel < ActionCable::Channel::Base - end -end -``` - -The client-side needs to setup a consumer instance of this connection. That's done like so: - -```js -// app/assets/javascripts/cable.js -//= require action_cable -//= require_self -//= require_tree ./channels - -(function() { - this.App || (this.App = {}); - - App.cable = ActionCable.createConsumer("ws://cable.example.com"); -}).call(this); -``` - -The `ws://cable.example.com` address must point to your Action Cable server(s), and it -must share a cookie namespace with the rest of the application (which may live under http://example.com). -This ensures that the signed cookie will be correctly sent. - -That's all you need to establish the connection! But of course, this isn't very useful in -itself. This just gives you the plumbing. To make stuff happen, you need content. That content -is defined by declaring channels on the server and allowing the consumer to subscribe to them. - - -### Channel example 1: User appearances - -Here's a simple example of a channel that tracks whether a user is online or not, and also what page they are currently on. -(This is useful for creating presence features like showing a green dot next to a user's name if they're online). - -First you declare the server-side channel: - -```ruby -# app/channels/appearance_channel.rb -class AppearanceChannel < ApplicationCable::Channel - def subscribed - current_user.appear - end - - def unsubscribed - current_user.disappear - end - - def appear(data) - current_user.appear on: data['appearing_on'] - end - - def away - current_user.away - end -end -``` - -The `#subscribed` callback is invoked when, as we'll show below, a client-side subscription is initiated. In this case, -we take that opportunity to say "the current user has indeed appeared". That appear/disappear API could be backed by -Redis or a database or whatever else. Here's what the client-side of that looks like: - -```coffeescript -# app/assets/javascripts/cable/subscriptions/appearance.coffee -App.cable.subscriptions.create "AppearanceChannel", - # Called when the subscription is ready for use on the server - connected: -> - @install() - @appear() - - # Called when the WebSocket connection is closed - disconnected: -> - @uninstall() - - # Called when the subscription is rejected by the server - rejected: -> - @uninstall() - - appear: -> - # Calls `AppearanceChannel#appear(data)` on the server - @perform("appear", appearing_on: $("main").data("appearing-on")) - - away: -> - # Calls `AppearanceChannel#away` on the server - @perform("away") - - - buttonSelector = "[data-behavior~=appear_away]" - - install: -> - $(document).on "turbolinks:load.appearance", => - @appear() - - $(document).on "click.appearance", buttonSelector, => - @away() - false - - $(buttonSelector).show() - - uninstall: -> - $(document).off(".appearance") - $(buttonSelector).hide() -``` - -Simply calling `App.cable.subscriptions.create` will setup the subscription, which will call `AppearanceChannel#subscribed`, -which in turn is linked to the original `App.cable` -> `ApplicationCable::Connection` instances. - -Next, we link the client-side `appear` method to `AppearanceChannel#appear(data)`. This is possible because the server-side -channel instance will automatically expose the public methods declared on the class (minus the callbacks), so that these -can be reached as remote procedure calls via a subscription's `perform` method. - -### Channel example 2: Receiving new web notifications - -The appearance example was all about exposing server functionality to client-side invocation over the WebSocket connection. -But the great thing about WebSockets is that it's a two-way street. So now let's show an example where the server invokes -an action on the client. - -This is a web notification channel that allows you to trigger client-side web notifications when you broadcast to the right -streams: - -```ruby -# app/channels/web_notifications_channel.rb -class WebNotificationsChannel < ApplicationCable::Channel - def subscribed - stream_from "web_notifications_#{current_user.id}" - end -end -``` - -```coffeescript -# Client-side, which assumes you've already requested the right to send web notifications -App.cable.subscriptions.create "WebNotificationsChannel", - received: (data) -> - new Notification data["title"], body: data["body"] -``` - -```ruby -# Somewhere in your app this is called, perhaps from a NewCommentJob -ActionCable.server.broadcast \ - "web_notifications_#{current_user.id}", { title: 'New things!', body: 'All the news that is fit to print' } -``` - -The `ActionCable.server.broadcast` call places a message in the Action Cable pubsub queue under a separate broadcasting name for each user. For a user with an ID of 1, the broadcasting name would be `web_notifications_1`. -The channel has been instructed to stream everything that arrives at `web_notifications_1` directly to the client by invoking the -`#received(data)` callback. The data is the hash sent as the second parameter to the server-side broadcast call, JSON encoded for the trip -across the wire, and unpacked for the data argument arriving to `#received`. - - -### Passing Parameters to Channel - -You can pass parameters from the client side to the server side when creating a subscription. For example: - -```ruby -# app/channels/chat_channel.rb -class ChatChannel < ApplicationCable::Channel - def subscribed - stream_from "chat_#{params[:room]}" - end -end -``` - -If you pass an object as the first argument to `subscriptions.create`, that object will become the params hash in your cable channel. The keyword `channel` is required. - -```coffeescript -# Client-side, which assumes you've already requested the right to send web notifications -App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }, - received: (data) -> - @appendLine(data) - - appendLine: (data) -> - html = @createLine(data) - $("[data-chat-room='Best Room']").append(html) - - createLine: (data) -> - """ - <article class="chat-line"> - <span class="speaker">#{data["sent_by"]}</span> - <span class="body">#{data["body"]}</span> - </article> - """ -``` - -```ruby -# Somewhere in your app this is called, perhaps from a NewCommentJob -ActionCable.server.broadcast \ - "chat_#{room}", { sent_by: 'Paul', body: 'This is a cool chat app.' } -``` - - -### Rebroadcasting message - -A common use case is to rebroadcast a message sent by one client to any other connected clients. - -```ruby -# app/channels/chat_channel.rb -class ChatChannel < ApplicationCable::Channel - def subscribed - stream_from "chat_#{params[:room]}" - end - - def receive(data) - ActionCable.server.broadcast "chat_#{params[:room]}", data - end -end -``` - -```coffeescript -# Client-side, which assumes you've already requested the right to send web notifications -App.chatChannel = App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }, - received: (data) -> - # data => { sent_by: "Paul", body: "This is a cool chat app." } - -App.chatChannel.send({ sent_by: "Paul", body: "This is a cool chat app." }) -``` - -The rebroadcast will be received by all connected clients, _including_ the client that sent the message. Note that params are the same as they were when you subscribed to the channel. - - -### More complete examples - -See the [rails/actioncable-examples](https://github.com/rails/actioncable-examples) repository for a full example of how to setup Action Cable in a Rails app, and how to add channels. - -## Configuration - -Action Cable has three required configurations: a subscription adapter, allowed request origins, and the cable server URL (which can optionally be set on the client side). - -### Redis - -By default, `ActionCable::Server::Base` will look for a configuration file in `Rails.root.join('config/cable.yml')`. -This file must specify an adapter and a URL for each Rails environment. It may use the following format: - -```yaml -production: &production - adapter: redis - url: redis://10.10.3.153:6381 -development: &development - adapter: redis - url: redis://localhost:6379 -test: *development -``` - -You can also change the location of the Action Cable config file in a Rails initializer with something like: - -```ruby -Rails.application.paths.add "config/cable", with: "somewhere/else/cable.yml" -``` - -### Allowed Request Origins - -Action Cable will only accept requests from specific origins. - -By default, only an origin matching the cable server itself will be permitted. -Additional origins can be specified using strings or regular expressions, provided in an array. - -```ruby -Rails.application.config.action_cable.allowed_request_origins = ['http://rubyonrails.com', /http:\/\/ruby.*/] -``` - -When running in the development environment, this defaults to "http://localhost:3000". - -To disable protection and allow requests from any origin: - -```ruby -Rails.application.config.action_cable.disable_request_forgery_protection = true -``` - -To disable automatic access for same-origin requests, and strictly allow -only the configured origins: - -```ruby -Rails.application.config.action_cable.allow_same_origin_as_host = false -``` - -### Consumer Configuration - -Once you have decided how to run your cable server (see below), you must provide the server URL (or path) to your client-side setup. -There are two ways you can do this. - -The first is to simply pass it in when creating your consumer. For a standalone server, -this would be something like: `App.cable = ActionCable.createConsumer("ws://example.com:28080")`, and for an in-app server, -something like: `App.cable = ActionCable.createConsumer("/cable")`. - -The second option is to pass the server URL through the `action_cable_meta_tag` in your layout. -This uses a URL or path typically set via `config.action_cable.url` in the environment configuration files, or defaults to "/cable". - -This method is especially useful if your WebSocket URL might change between environments. If you host your production server via https, you will need to use the wss scheme -for your Action Cable server, but development might remain http and use the ws scheme. You might use localhost in development and your -domain in production. - -In any case, to vary the WebSocket URL between environments, add the following configuration to each environment: - -```ruby -config.action_cable.url = "ws://example.com:28080" -``` - -Then add the following line to your layout before your JavaScript tag: - -```erb -<%= action_cable_meta_tag %> -``` - -And finally, create your consumer like so: - -```coffeescript -App.cable = ActionCable.createConsumer() -``` - -### Other Configurations - -The other common option to configure is the log tags applied to the per-connection logger. Here's an example that uses the user account id if available, else "no-account" while tagging: - -```ruby -config.action_cable.log_tags = [ - -> request { request.env['user_account_id'] || "no-account" }, - :action_cable, - -> request { request.uuid } -] -``` - -For a full list of all configuration options, see the `ActionCable::Server::Configuration` class. - -Also note that your server must provide at least the same number of database connections as you have workers. The default worker pool is set to 4, so that means you have to make at least that available. You can change that in `config/database.yml` through the `pool` attribute. - - -## Running the cable server - -### Standalone -The cable server(s) is separated from your normal application server. It's still a Rack application, but it is its own Rack -application. The recommended basic setup is as follows: - -```ruby -# cable/config.ru -require_relative '../config/environment' -Rails.application.eager_load! - -run ActionCable.server -``` - -Then you start the server using a binstub in bin/cable ala: -```sh -#!/bin/bash -bundle exec puma -p 28080 cable/config.ru -``` - -The above will start a cable server on port 28080. - -### In app - -If you are using a server that supports the [Rack socket hijacking API](https://www.rubydoc.info/github/rack/rack/file/SPEC#label-Hijacking), Action Cable can run alongside your Rails application. For example, to listen for WebSocket requests on `/websocket`, specify that path to `config.action_cable.mount_path`: - -```ruby -# config/application.rb -class Application < Rails::Application - config.action_cable.mount_path = '/websocket' -end -``` - -For every instance of your server you create and for every worker your server spawns, you will also have a new instance of Action Cable, but the use of Redis keeps messages synced across connections. - -### Notes - -Beware that currently, the cable server will _not_ auto-reload any changes in the framework. As we've discussed, long-running cable connections mean long-running objects. We don't yet have a way of reloading the classes of those objects in a safe manner. So when you change your channels, or the model your channels use, you must restart the cable server. - -We'll get all this abstracted properly when the framework is integrated into Rails. - -The WebSocket server doesn't have access to the session, but it has access to the cookies. This can be used when you need to handle authentication. You can see one way of doing that with Devise in this [article](https://greg.molnar.io/blog/actioncable-devise-authentication/). - -## Dependencies - -Action Cable provides a subscription adapter interface to process its pubsub internals. By default, asynchronous, inline, PostgreSQL, and Redis adapters are included. The default adapter in new Rails applications is the asynchronous (`async`) adapter. To create your own adapter, you can look at `ActionCable::SubscriptionAdapter::Base` for all methods that must be implemented, and any of the adapters included within Action Cable as example implementations. - -The Ruby side of things is built on top of [websocket-driver](https://github.com/faye/websocket-driver-ruby), [nio4r](https://github.com/celluloid/nio4r), and [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby). - - -## Deployment - -Action Cable is powered by a combination of WebSockets and threads. All of the -connection management is handled internally by utilizing Ruby's native thread -support, which means you can use all your regular Rails models with no problems -as long as you haven't committed any thread-safety sins. - -The Action Cable server does _not_ need to be a multi-threaded application server. -This is because Action Cable uses the [Rack socket hijacking API](https://www.rubydoc.info/github/rack/rack/file/SPEC#label-Hijacking) -to take over control of connections from the application server. Action Cable -then manages connections internally, in a multithreaded manner, regardless of -whether the application server is multi-threaded or not. So Action Cable works -with all the popular application servers -- Unicorn, Puma and Passenger. - -Action Cable does not work with WEBrick, because WEBrick does not support the -Rack socket hijacking API. - -## Frontend assets - -Action Cable's frontend assets are distributed through two channels: the -official gem and npm package, both titled `actioncable`. - -### Gem usage - -Through the `actioncable` gem, Action Cable's frontend assets are -available through the Rails Asset Pipeline. Create a `cable.js` or -`cable.coffee` file (this is automatically done for you with Rails -generators), and then simply require the assets: - -In JavaScript... - -```javascript -//= require action_cable -``` - -... and in CoffeeScript: - -```coffeescript -#= require action_cable -``` - -### npm usage - -In addition to being available through the `actioncable` gem, Action Cable's -frontend JS assets are also bundled in an officially supported npm module, -intended for usage in standalone frontend applications that communicate with a -Rails application. A common use case for this could be if you have a decoupled -frontend application written in React, Ember.js, etc. and want to add real-time -WebSocket functionality. - -### Installation - -``` -npm install actioncable --save -``` - -### Usage - -The `ActionCable` constant is available as a `require`-able module, so -you only have to require the package to gain access to the API that is -provided. - -In JavaScript... - -```javascript -ActionCable = require('actioncable') - -var cable = ActionCable.createConsumer('wss://RAILS-API-PATH.com/cable') - -cable.subscriptions.create('AppearanceChannel', { - // normal channel code goes here... -}); -``` - -and in CoffeeScript... - -```coffeescript -ActionCable = require('actioncable') - -cable = ActionCable.createConsumer('wss://RAILS-API-PATH.com/cable') - -cable.subscriptions.create 'AppearanceChannel', - # normal channel code goes here... -``` - -## Download and Installation - -The latest version of Action Cable can be installed with [RubyGems](#gem-usage), -or with [npm](#npm-usage). - -Source code can be downloaded as part of the Rails project on GitHub - -* https://github.com/rails/rails/tree/master/actioncable - -## License - -Action Cable is released under the MIT license: - -* https://opensource.org/licenses/MIT - +You can read more about Action Cable in the [Action Cable Overview](https://edgeguides.rubyonrails.org/action_cable_overview.html) guide. ## Support diff --git a/actioncable/app/assets/javascripts/action_cable.js b/actioncable/app/assets/javascripts/action_cable.js index 65e32d6c3f..280adbfa83 100644 --- a/actioncable/app/assets/javascripts/action_cable.js +++ b/actioncable/app/assets/javascripts/action_cable.js @@ -3,8 +3,8 @@ })(this, function(exports) { "use strict"; var adapters = { - logger: window.console, - WebSocket: window.WebSocket + logger: self.console, + WebSocket: self.WebSocket }; var logger = { log: function log() { @@ -49,7 +49,7 @@ this.startedAt = now(); delete this.stoppedAt; this.startPolling(); - document.addEventListener("visibilitychange", this.visibilityDidChange); + addEventListener("visibilitychange", this.visibilityDidChange); logger.log("ConnectionMonitor started. pollInterval = " + this.getPollInterval() + " ms"); } }; @@ -57,7 +57,7 @@ if (this.isRunning()) { this.stoppedAt = now(); this.stopPolling(); - document.removeEventListener("visibilitychange", this.visibilityDidChange); + removeEventListener("visibilitychange", this.visibilityDidChange); logger.log("ConnectionMonitor stopped"); } }; @@ -192,7 +192,7 @@ this.monitor.stop(); } if (this.isActive()) { - return this.webSocket ? this.webSocket.close() : undefined; + return this.webSocket.close(); } }; Connection.prototype.reopen = function reopen() { @@ -211,7 +211,9 @@ } }; Connection.prototype.getProtocol = function getProtocol() { - return this.webSocket ? this.webSocket.protocol : undefined; + if (this.webSocket) { + return this.webSocket.protocol; + } }; Connection.prototype.isOpen = function isOpen() { return this.isState("open"); @@ -452,16 +454,15 @@ }; return Consumer; }(); - function createConsumer(url) { - if (url == null) { - var urlConfig = getConfig("url"); - url = urlConfig ? urlConfig : INTERNAL.default_mount_path; - } + function createConsumer() { + var url = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getConfig("url") || INTERNAL.default_mount_path; return new Consumer(createWebSocketURL(url)); } function getConfig(name) { var element = document.head.querySelector("meta[name='action-cable-" + name + "']"); - return element ? element.getAttribute("content") : undefined; + if (element) { + return element.getAttribute("content"); + } } function createWebSocketURL(url) { if (url && !/^wss?:/i.test(url)) { diff --git a/actioncable/app/javascript/action_cable/adapters.js b/actioncable/app/javascript/action_cable/adapters.js index 9ba6d338ee..4de8131438 100644 --- a/actioncable/app/javascript/action_cable/adapters.js +++ b/actioncable/app/javascript/action_cable/adapters.js @@ -1,4 +1,4 @@ export default { - logger: window.console, - WebSocket: window.WebSocket + logger: self.console, + WebSocket: self.WebSocket } diff --git a/actioncable/app/javascript/action_cable/connection.js b/actioncable/app/javascript/action_cable/connection.js index b2910cb2a6..96bac132c1 100644 --- a/actioncable/app/javascript/action_cable/connection.js +++ b/actioncable/app/javascript/action_cable/connection.js @@ -44,7 +44,9 @@ class Connection { close({allowReconnect} = {allowReconnect: true}) { if (!allowReconnect) { this.monitor.stop() } - if (this.isActive()) { return (this.webSocket ? this.webSocket.close() : undefined) } + if (this.isActive()) { + return this.webSocket.close() + } } reopen() { @@ -65,7 +67,9 @@ class Connection { } getProtocol() { - return (this.webSocket ? this.webSocket.protocol : undefined) + if (this.webSocket) { + return this.webSocket.protocol + } } isOpen() { diff --git a/actioncable/app/javascript/action_cable/connection_monitor.js b/actioncable/app/javascript/action_cable/connection_monitor.js index f0e75ae137..312a71d154 100644 --- a/actioncable/app/javascript/action_cable/connection_monitor.js +++ b/actioncable/app/javascript/action_cable/connection_monitor.js @@ -21,7 +21,7 @@ class ConnectionMonitor { this.startedAt = now() delete this.stoppedAt this.startPolling() - document.addEventListener("visibilitychange", this.visibilityDidChange) + addEventListener("visibilitychange", this.visibilityDidChange) logger.log(`ConnectionMonitor started. pollInterval = ${this.getPollInterval()} ms`) } } @@ -30,7 +30,7 @@ class ConnectionMonitor { if (this.isRunning()) { this.stoppedAt = now() this.stopPolling() - document.removeEventListener("visibilitychange", this.visibilityDidChange) + removeEventListener("visibilitychange", this.visibilityDidChange) logger.log("ConnectionMonitor stopped") } } diff --git a/actioncable/app/javascript/action_cable/index.js b/actioncable/app/javascript/action_cable/index.js index 9f41c14e94..659418396f 100644 --- a/actioncable/app/javascript/action_cable/index.js +++ b/actioncable/app/javascript/action_cable/index.js @@ -18,17 +18,15 @@ export { logger, } -export function createConsumer(url) { - if (url == null) { - const urlConfig = getConfig("url") - url = (urlConfig ? urlConfig : INTERNAL.default_mount_path) - } +export function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) { return new Consumer(createWebSocketURL(url)) } export function getConfig(name) { const element = document.head.querySelector(`meta[name='action-cable-${name}']`) - return (element ? element.getAttribute("content") : undefined) + if (element) { + return element.getAttribute("content") + } } export function createWebSocketURL(url) { diff --git a/actioncable/lib/action_cable.rb b/actioncable/lib/action_cable.rb index cb9dfa2268..ad5fd43155 100644 --- a/actioncable/lib/action_cable.rb +++ b/actioncable/lib/action_cable.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2015-2018 Basecamp, LLC +# Copyright (c) 2015-2019 Basecamp, LLC # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actioncable/lib/action_cable/channel/base.rb b/actioncable/lib/action_cable/channel/base.rb index ad0d3685cd..af061c843a 100644 --- a/actioncable/lib/action_cable/channel/base.rb +++ b/actioncable/lib/action_cable/channel/base.rb @@ -307,3 +307,5 @@ module ActionCable end end end + +ActiveSupport.run_load_hooks(:action_cable_channel, ActionCable::Channel::Base) diff --git a/actioncable/lib/action_cable/channel/broadcasting.rb b/actioncable/lib/action_cable/channel/broadcasting.rb index 9a96720f4a..9f702e425e 100644 --- a/actioncable/lib/action_cable/channel/broadcasting.rb +++ b/actioncable/lib/action_cable/channel/broadcasting.rb @@ -7,22 +7,32 @@ module ActionCable module Broadcasting extend ActiveSupport::Concern - delegate :broadcasting_for, to: :class + delegate :broadcasting_for, :broadcast_to, to: :class module ClassMethods # Broadcast a hash to a unique broadcasting for this <tt>model</tt> in this channel. def broadcast_to(model, message) - ActionCable.server.broadcast(broadcasting_for([ channel_name, model ]), message) + ActionCable.server.broadcast(broadcasting_for(model), message) end - def broadcasting_for(model) #:nodoc: + # Returns a unique broadcasting identifier for this <tt>model</tt> in this channel: + # + # CommentsChannel.broadcasting_for("all") # => "comments:all" + # + # You can pass any object as a target (e.g. Active Record model), and it + # would be serialized into a string under the hood. + def broadcasting_for(model) + serialize_broadcasting([ channel_name, model ]) + end + + def serialize_broadcasting(object) #:nodoc: case - when model.is_a?(Array) - model.map { |m| broadcasting_for(m) }.join(":") - when model.respond_to?(:to_gid_param) - model.to_gid_param + when object.is_a?(Array) + object.map { |m| serialize_broadcasting(m) }.join(":") + when object.respond_to?(:to_gid_param) + object.to_gid_param else - model.to_param + object.to_param end end end diff --git a/actioncable/lib/action_cable/channel/streams.rb b/actioncable/lib/action_cable/channel/streams.rb index 81c2c38064..7e1ed3c850 100644 --- a/actioncable/lib/action_cable/channel/streams.rb +++ b/actioncable/lib/action_cable/channel/streams.rb @@ -99,7 +99,7 @@ module ActionCable # Pass <tt>coder: ActiveSupport::JSON</tt> to decode messages as JSON before passing to the callback. # Defaults to <tt>coder: nil</tt> which does no decoding, passes raw messages. def stream_for(model, callback = nil, coder: nil, &block) - stream_from(broadcasting_for([ channel_name, model ]), callback || block, coder: coder) + stream_from(broadcasting_for(model), callback || block, coder: coder) end # Unsubscribes all streams associated with this channel from the pubsub queue. diff --git a/actioncable/lib/action_cable/channel/test_case.rb b/actioncable/lib/action_cable/channel/test_case.rb index dd2762ccd0..b05d51a61a 100644 --- a/actioncable/lib/action_cable/channel/test_case.rb +++ b/actioncable/lib/action_cable/channel/test_case.rb @@ -84,7 +84,18 @@ module ActionCable # assert subscription.confirmed? # # # Asserts that the channel subscribes connection to a stream - # assert_equal "chat_1", streams.last + # assert_has_stream "chat_1" + # + # # Asserts that the channel subscribes connection to a specific + # # stream created for a model + # assert_has_stream_for Room.find(1) + # end + # + # def test_does_not_stream_with_incorrect_room_number + # subscribe room_number: -1 + # + # # Asserts that not streams was started + # assert_no_streams # end # # def test_does_not_subscribe_without_room_number @@ -115,8 +126,6 @@ module ActionCable # An instance of the current channel, created when you call `subscribe`. # <b>transmissions</b>:: # A list of all messages that have been transmitted into the channel. - # <b>streams</b>:: - # A list of all created streams subscriptions (as identifiers) for the subscription. # # # == Channel is automatically inferred @@ -134,7 +143,7 @@ module ActionCable # You need to set up your connection manually to provide values for the identifiers. # To do this just use: # - # stub_connection(user: users[:john]) + # stub_connection(user: users(:john)) # # == Testing broadcasting # @@ -148,9 +157,9 @@ module ActionCable # end # # def test_speak - # subscribe room_id: rooms[:chat].id + # subscribe room_id: rooms(:chat).id # - # assert_broadcasts_on(rooms[:chat], text: "Hello, Rails!") do + # assert_broadcasts_on(rooms(:chat), text: "Hello, Rails!") do # perform :speak, message: "Hello, Rails!" # end # end @@ -167,7 +176,6 @@ module ActionCable class_attribute :_channel_class attr_reader :connection, :subscription - delegate :streams, to: :subscription ActiveSupport.run_load_hooks(:action_cable_channel_test_case, self) end @@ -251,6 +259,39 @@ module ActionCable super(broadcasting_for(stream_or_object), *args) end + # Asserts that no streams have been started. + # + # def test_assert_no_started_stream + # subscribe + # assert_no_streams + # end + # + def assert_no_streams + assert subscription.streams.empty?, "No streams started was expected, but #{subscription.streams.count} found" + end + + # Asserts that the specified stream has been started. + # + # def test_assert_started_stream + # subscribe + # assert_has_stream 'messages' + # end + # + def assert_has_stream(stream) + assert subscription.streams.include?(stream), "Stream #{stream} has not been started" + end + + # Asserts that the specified stream for a model has started. + # + # def test_assert_started_stream_for + # subscribe id: 42 + # assert_has_stream_for User.find(42) + # end + # + def assert_has_stream_for(object) + assert_has_stream(broadcasting_for(object)) + end + private def check_subscribed! raise "Must be subscribed!" if subscription.nil? || subscription.rejected? @@ -259,9 +300,7 @@ module ActionCable def broadcasting_for(stream_or_object) return stream_or_object if stream_or_object.is_a?(String) - self.class.channel_class.broadcasting_for( - [self.class.channel_class.channel_name, stream_or_object] - ) + self.class.channel_class.broadcasting_for(stream_or_object) end end diff --git a/actioncable/lib/action_cable/connection.rb b/actioncable/lib/action_cable/connection.rb index 804b89a707..20b5dbe78d 100644 --- a/actioncable/lib/action_cable/connection.rb +++ b/actioncable/lib/action_cable/connection.rb @@ -15,6 +15,7 @@ module ActionCable autoload :StreamEventLoop autoload :Subscriptions autoload :TaggedLoggerProxy + autoload :TestCase autoload :WebSocket end end diff --git a/actioncable/lib/action_cable/connection/base.rb b/actioncable/lib/action_cable/connection/base.rb index 0044afad98..c469f7066c 100644 --- a/actioncable/lib/action_cable/connection/base.rb +++ b/actioncable/lib/action_cable/connection/base.rb @@ -260,3 +260,5 @@ module ActionCable end end end + +ActiveSupport.run_load_hooks(:action_cable_connection, ActionCable::Connection::Base) diff --git a/actioncable/lib/action_cable/connection/test_case.rb b/actioncable/lib/action_cable/connection/test_case.rb new file mode 100644 index 0000000000..8d25a55c8a --- /dev/null +++ b/actioncable/lib/action_cable/connection/test_case.rb @@ -0,0 +1,234 @@ +# frozen_string_literal: true + +require "active_support" +require "active_support/test_case" +require "active_support/core_ext/hash/indifferent_access" +require "action_dispatch" +require "action_dispatch/http/headers" +require "action_dispatch/testing/test_request" + +module ActionCable + module Connection + class NonInferrableConnectionError < ::StandardError + def initialize(name) + super "Unable to determine the connection to test from #{name}. " + + "You'll need to specify it using `tests YourConnection` in your " + + "test case definition." + end + end + + module Assertions + # Asserts that the connection is rejected (via +reject_unauthorized_connection+). + # + # # Asserts that connection without user_id fails + # assert_reject_connection { connect params: { user_id: '' } } + def assert_reject_connection(&block) + assert_raises(Authorization::UnauthorizedError, "Expected to reject connection but no rejection was made", &block) + end + end + + # We don't want to use the whole "encryption stack" for connection + # unit-tests, but we want to make sure that users test against the correct types + # of cookies (i.e. signed or encrypted or plain) + class TestCookieJar < ActiveSupport::HashWithIndifferentAccess + def signed + self[:signed] ||= {}.with_indifferent_access + end + + def encrypted + self[:encrypted] ||= {}.with_indifferent_access + end + end + + class TestRequest < ActionDispatch::TestRequest + attr_accessor :session, :cookie_jar + end + + module TestConnection + attr_reader :logger, :request + + def initialize(request) + inner_logger = ActiveSupport::Logger.new(StringIO.new) + tagged_logging = ActiveSupport::TaggedLogging.new(inner_logger) + @logger = ActionCable::Connection::TaggedLoggerProxy.new(tagged_logging, tags: []) + @request = request + @env = request.env + end + end + + # Unit test Action Cable connections. + # + # Useful to check whether a connection's +identified_by+ gets assigned properly + # and that any improper connection requests are rejected. + # + # == Basic example + # + # Unit tests are written as follows: + # + # 1. Simulate a connection attempt by calling +connect+. + # 2. Assert state, e.g. identifiers, has been assigned. + # + # + # class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase + # def test_connects_with_proper_cookie + # # Simulate the connection request with a cookie. + # cookies["user_id"] = users(:john).id + # + # connect + # + # # Assert the connection identifier matches the fixture. + # assert_equal users(:john).id, connection.user.id + # end + # + # def test_rejects_connection_without_proper_cookie + # assert_reject_connection { connect } + # end + # end + # + # +connect+ accepts additional information the HTTP request with the + # +params+, +headers+, +session+ and Rack +env+ options. + # + # def test_connect_with_headers_and_query_string + # connect params: { user_id: 1 }, headers: { "X-API-TOKEN" => "secret-my" } + # + # assert_equal "1", connection.user.id + # assert_equal "secret-my", connection.token + # end + # + # def test_connect_with_params + # connect params: { user_id: 1 } + # + # assert_equal "1", connection.user.id + # end + # + # You can also setup the correct cookies before the connection request: + # + # def test_connect_with_cookies + # # Plain cookies: + # cookies["user_id"] = 1 + # + # # Or signed/encrypted: + # # cookies.signed["user_id"] = 1 + # # cookies.encrypted["user_id"] = 1 + # + # connect + # + # assert_equal "1", connection.user_id + # end + # + # == Connection is automatically inferred + # + # ActionCable::Connection::TestCase will automatically infer the connection under test + # from the test class name. If the channel cannot be inferred from the test + # class name, you can explicitly set it with +tests+. + # + # class ConnectionTest < ActionCable::Connection::TestCase + # tests ApplicationCable::Connection + # end + # + class TestCase < ActiveSupport::TestCase + module Behavior + extend ActiveSupport::Concern + + DEFAULT_PATH = "/cable" + + include ActiveSupport::Testing::ConstantLookup + include Assertions + + included do + class_attribute :_connection_class + + attr_reader :connection + + ActiveSupport.run_load_hooks(:action_cable_connection_test_case, self) + end + + module ClassMethods + def tests(connection) + case connection + when String, Symbol + self._connection_class = connection.to_s.camelize.constantize + when Module + self._connection_class = connection + else + raise NonInferrableConnectionError.new(connection) + end + end + + def connection_class + if connection = self._connection_class + connection + else + tests determine_default_connection(name) + end + end + + def determine_default_connection(name) + connection = determine_constant_from_test_name(name) do |constant| + Class === constant && constant < ActionCable::Connection::Base + end + raise NonInferrableConnectionError.new(name) if connection.nil? + connection + end + end + + # Performs connection attempt to exert #connect on the connection under test. + # + # Accepts request path as the first argument and the following request options: + # + # - params – url parameters (Hash) + # - headers – request headers (Hash) + # - session – session data (Hash) + # - env – additional Rack env configuration (Hash) + def connect(path = ActionCable.server.config.mount_path, **request_params) + path ||= DEFAULT_PATH + + connection = self.class.connection_class.allocate + connection.singleton_class.include(TestConnection) + connection.send(:initialize, build_test_request(path, request_params)) + connection.connect if connection.respond_to?(:connect) + + # Only set instance variable if connected successfully + @connection = connection + end + + # Exert #disconnect on the connection under test. + def disconnect + raise "Must be connected!" if connection.nil? + + connection.disconnect if connection.respond_to?(:disconnect) + @connection = nil + end + + def cookies + @cookie_jar ||= TestCookieJar.new + end + + private + def build_test_request(path, params: nil, headers: {}, session: {}, env: {}) + wrapped_headers = ActionDispatch::Http::Headers.from_hash(headers) + + uri = URI.parse(path) + + query_string = params.nil? ? uri.query : params.to_query + + request_env = { + "QUERY_STRING" => query_string, + "PATH_INFO" => uri.path + }.merge(env) + + if wrapped_headers.present? + ActionDispatch::Http::Headers.from_hash(request_env).merge!(wrapped_headers) + end + + TestRequest.create(request_env).tap do |request| + request.session = session.with_indifferent_access + request.cookie_jar = cookies + end + end + end + + include Behavior + end + end +end diff --git a/actioncable/lib/action_cable/gem_version.rb b/actioncable/lib/action_cable/gem_version.rb index cd1d9bccef..5082827417 100644 --- a/actioncable/lib/action_cable/gem_version.rb +++ b/actioncable/lib/action_cable/gem_version.rb @@ -10,7 +10,7 @@ module ActionCable MAJOR = 6 MINOR = 0 TINY = 0 - PRE = "alpha" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/actioncable/lib/action_cable/test_helper.rb b/actioncable/lib/action_cable/test_helper.rb index 7bc877663c..26b849a273 100644 --- a/actioncable/lib/action_cable/test_helper.rb +++ b/actioncable/lib/action_cable/test_helper.rb @@ -81,7 +81,7 @@ module ActionCable # Asserts that the specified message has been sent to the stream. # - # def test_assert_transmited_message + # def test_assert_transmitted_message # ActionCable.server.broadcast 'messages', text: 'hello' # assert_broadcast_on('messages', text: 'hello') # end diff --git a/actioncable/lib/rails/generators/channel/USAGE b/actioncable/lib/rails/generators/channel/USAGE index ea9662436c..bb5dd7e2db 100644 --- a/actioncable/lib/rails/generators/channel/USAGE +++ b/actioncable/lib/rails/generators/channel/USAGE @@ -7,6 +7,7 @@ Example: ======== rails generate channel Chat speak - creates a Chat channel class and JavaScript asset: + creates a Chat channel class, test and JavaScript asset: Channel: app/channels/chat_channel.rb + Test: test/channels/chat_channel_test.rb Assets: app/javascript/channels/chat_channel.js diff --git a/actioncable/lib/rails/generators/channel/channel_generator.rb b/actioncable/lib/rails/generators/channel/channel_generator.rb index ef51981e89..0b80d1f96b 100644 --- a/actioncable/lib/rails/generators/channel/channel_generator.rb +++ b/actioncable/lib/rails/generators/channel/channel_generator.rb @@ -11,6 +11,8 @@ module Rails check_class_collision suffix: "Channel" + hook_for :test_framework + def create_channel_file template "channel.rb", File.join("app/channels", class_path, "#{file_name}_channel.rb") diff --git a/actioncable/lib/rails/generators/channel/templates/javascript/channel.js.tt b/actioncable/lib/rails/generators/channel/templates/javascript/channel.js.tt index 33baaa5a22..ddf6b2d79b 100644 --- a/actioncable/lib/rails/generators/channel/templates/javascript/channel.js.tt +++ b/actioncable/lib/rails/generators/channel/templates/javascript/channel.js.tt @@ -1,15 +1,15 @@ import consumer from "./consumer" consumer.subscriptions.create("<%= class_name %>Channel", { - connected: function() { + connected() { // Called when the subscription is ready for use on the server }, - disconnected: function() { + disconnected() { // Called when the subscription has been terminated by the server }, - received: function(data) { + received(data) { // Called when there's incoming data on the websocket for this channel }<%= actions.any? ? ",\n" : '' %> <% actions.each do |action| -%> diff --git a/actioncable/lib/rails/generators/channel/templates/javascript/consumer.js.tt b/actioncable/lib/rails/generators/channel/templates/javascript/consumer.js.tt index 76ca3d0f2f..0eceb59b18 100644 --- a/actioncable/lib/rails/generators/channel/templates/javascript/consumer.js.tt +++ b/actioncable/lib/rails/generators/channel/templates/javascript/consumer.js.tt @@ -1,6 +1,6 @@ // Action Cable provides the framework to deal with WebSockets in Rails. // You can generate new channels where WebSocket features live using the `rails generate channel` command. -import ActionCable from "actioncable" +import { createConsumer } from "@rails/actioncable" -export default ActionCable.createConsumer() +export default createConsumer() diff --git a/actioncable/lib/rails/generators/test_unit/channel_generator.rb b/actioncable/lib/rails/generators/test_unit/channel_generator.rb new file mode 100644 index 0000000000..7d13a12f0a --- /dev/null +++ b/actioncable/lib/rails/generators/test_unit/channel_generator.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module TestUnit + module Generators + class ChannelGenerator < ::Rails::Generators::NamedBase + source_root File.expand_path("templates", __dir__) + + check_class_collision suffix: "ChannelTest" + + def create_test_files + template "channel_test.rb", File.join("test/channels", class_path, "#{file_name}_channel_test.rb") + end + + private + def file_name # :doc: + @_file_name ||= super.sub(/_channel\z/i, "") + end + end + end +end diff --git a/actioncable/lib/rails/generators/test_unit/templates/channel_test.rb.tt b/actioncable/lib/rails/generators/test_unit/templates/channel_test.rb.tt new file mode 100644 index 0000000000..7307654611 --- /dev/null +++ b/actioncable/lib/rails/generators/test_unit/templates/channel_test.rb.tt @@ -0,0 +1,8 @@ +require "test_helper" + +class <%= class_name %>ChannelTest < ActionCable::Channel::TestCase + # test "subscribes" do + # subscribe + # assert subscription.confirmed? + # end +end diff --git a/actioncable/package.json b/actioncable/package.json index db78c1a09a..801fcc0c22 100644 --- a/actioncable/package.json +++ b/actioncable/package.json @@ -1,6 +1,6 @@ { - "name": "actioncable", - "version": "6.0.0-alpha", + "name": "@rails/actioncable", + "version": "6.0.0-beta1", "description": "WebSocket framework for Ruby on Rails.", "main": "app/assets/javascripts/action_cable.js", "files": [ diff --git a/actioncable/test/channel/broadcasting_test.rb b/actioncable/test/channel/broadcasting_test.rb index 2cbfabc1d0..fb501a1bc2 100644 --- a/actioncable/test/channel/broadcasting_test.rb +++ b/actioncable/test/channel/broadcasting_test.rb @@ -26,14 +26,23 @@ class ActionCable::Channel::BroadcastingTest < ActionCable::TestCase end test "broadcasting_for with an object" do - assert_equal "Room#1-Campfire", ChatChannel.broadcasting_for(Room.new(1)) + assert_equal( + "action_cable:channel:broadcasting_test:chat:Room#1-Campfire", + ChatChannel.broadcasting_for(Room.new(1)) + ) end test "broadcasting_for with an array" do - assert_equal "Room#1-Campfire:Room#2-Campfire", ChatChannel.broadcasting_for([ Room.new(1), Room.new(2) ]) + assert_equal( + "action_cable:channel:broadcasting_test:chat:Room#1-Campfire:Room#2-Campfire", + ChatChannel.broadcasting_for([ Room.new(1), Room.new(2) ]) + ) end test "broadcasting_for with a string" do - assert_equal "hello", ChatChannel.broadcasting_for("hello") + assert_equal( + "action_cable:channel:broadcasting_test:chat:hello", + ChatChannel.broadcasting_for("hello") + ) end end diff --git a/actioncable/test/channel/test_case_test.rb b/actioncable/test/channel/test_case_test.rb index 63d0d6207e..a166c41e11 100644 --- a/actioncable/test/channel/test_case_test.rb +++ b/actioncable/test/channel/test_case_test.rb @@ -93,13 +93,39 @@ class StreamsTestChannelTest < ActionCable::Channel::TestCase def test_stream_without_params subscribe - assert_equal "test_0", streams.last + assert_has_stream "test_0" end def test_stream_with_params subscribe id: 42 - assert_equal "test_42", streams.last + assert_has_stream "test_42" + end +end + +class StreamsForTestChannel < ActionCable::Channel::Base + def subscribed + stream_for User.new(params[:id]) + end +end + +class StreamsForTestChannelTest < ActionCable::Channel::TestCase + def test_stream_with_params + subscribe id: 42 + + assert_has_stream_for User.new(42) + end +end + +class NoStreamsTestChannel < ActionCable::Channel::Base + def subscribed; end # no-op +end + +class NoStreamsTestChannelTest < ActionCable::Channel::TestCase + def test_stream_with_params + subscribe + + assert_no_streams end end @@ -154,7 +180,7 @@ class BroadcastsTestChannel < ActionCable::Channel::Base def broadcast_to_user(data) user = User.new user_id - self.class.broadcast_to user, text: data["message"] + broadcast_to user, text: data["message"] end end diff --git a/actioncable/test/client_test.rb b/actioncable/test/client_test.rb index e5f43488c4..bf141df458 100644 --- a/actioncable/test/client_test.rb +++ b/actioncable/test/client_test.rb @@ -140,7 +140,7 @@ class ClientTest < ActionCable::TestCase end end - ws.on(:close) do |event| + ws.on(:close) do |_| closed.set end end diff --git a/actioncable/test/connection/test_case_test.rb b/actioncable/test/connection/test_case_test.rb new file mode 100644 index 0000000000..3b19465d7b --- /dev/null +++ b/actioncable/test/connection/test_case_test.rb @@ -0,0 +1,197 @@ +# frozen_string_literal: true + +require "test_helper" + +class SimpleConnection < ActionCable::Connection::Base + identified_by :user_id + + class << self + attr_accessor :disconnected_user_id + end + + def connect + self.user_id = request.params[:user_id] || cookies[:user_id] + end + + def disconnect + self.class.disconnected_user_id = user_id + end +end + +class ConnectionSimpleTest < ActionCable::Connection::TestCase + tests SimpleConnection + + def test_connected + connect + + assert_nil connection.user_id + end + + def test_url_params + connect "/cable?user_id=323" + + assert_equal "323", connection.user_id + end + + def test_params + connect params: { user_id: 323 } + + assert_equal "323", connection.user_id + end + + def test_plain_cookie + cookies["user_id"] = "456" + + connect + + assert_equal "456", connection.user_id + end + + def test_disconnect + cookies["user_id"] = "456" + + connect + + assert_equal "456", connection.user_id + + disconnect + + assert_equal "456", SimpleConnection.disconnected_user_id + end +end + +class Connection < ActionCable::Connection::Base + identified_by :current_user_id + identified_by :token + + class << self + attr_accessor :disconnected_user_id + end + + def connect + self.current_user_id = verify_user + self.token = request.headers["X-API-TOKEN"] + logger.add_tags("ActionCable") + end + + private + def verify_user + cookies.signed[:user_id].presence || reject_unauthorized_connection + end +end + +class ConnectionTest < ActionCable::Connection::TestCase + def test_connected_with_signed_cookies_and_headers + cookies.signed["user_id"] = "456" + + connect headers: { "X-API-TOKEN" => "abc" } + + assert_equal "abc", connection.token + assert_equal "456", connection.current_user_id + end + + def test_connected_when_no_signed_cookies_set + cookies["user_id"] = "456" + + assert_reject_connection { connect } + end + + def test_connection_rejected + assert_reject_connection { connect } + end + + def test_connection_rejected_assertion_message + error = assert_raises Minitest::Assertion do + assert_reject_connection { "Intentionally doesn't connect." } + end + + assert_match(/Expected to reject connection/, error.message) + end +end + +class EncryptedCookiesConnection < ActionCable::Connection::Base + identified_by :user_id + + def connect + self.user_id = verify_user + end + + private + def verify_user + cookies.encrypted[:user_id].presence || reject_unauthorized_connection + end +end + +class EncryptedCookiesConnectionTest < ActionCable::Connection::TestCase + tests EncryptedCookiesConnection + + def test_connected_with_encrypted_cookies + cookies.encrypted["user_id"] = "456" + + connect + + assert_equal "456", connection.user_id + end + + def test_connection_rejected + assert_reject_connection { connect } + end +end + +class SessionConnection < ActionCable::Connection::Base + identified_by :user_id + + def connect + self.user_id = verify_user + end + + private + def verify_user + request.session[:user_id].presence || reject_unauthorized_connection + end +end + +class SessionConnectionTest < ActionCable::Connection::TestCase + tests SessionConnection + + def test_connected_with_encrypted_cookies + connect session: { user_id: "789" } + assert_equal "789", connection.user_id + end + + def test_connection_rejected + assert_reject_connection { connect } + end +end + +class EnvConnection < ActionCable::Connection::Base + identified_by :user + + def connect + self.user = verify_user + end + + private + def verify_user + # Warden-like authentication + env["authenticator"]&.user || reject_unauthorized_connection + end +end + +class EnvConnectionTest < ActionCable::Connection::TestCase + tests EnvConnection + + def test_connected_with_env + authenticator = Class.new do + def user; "David"; end + end + + connect env: { "authenticator" => authenticator.new } + + assert_equal "David", connection.user + end + + def test_connection_rejected + assert_reject_connection { connect } + end +end diff --git a/actioncable/test/javascript/src/unit/action_cable_test.js b/actioncable/test/javascript/src/unit/action_cable_test.js index daad900aca..83426fa32e 100644 --- a/actioncable/test/javascript/src/unit/action_cable_test.js +++ b/actioncable/test/javascript/src/unit/action_cable_test.js @@ -6,14 +6,14 @@ const {module, test} = QUnit module("ActionCable", () => { module("Adapters", () => { module("WebSocket", () => { - test("default is window.WebSocket", assert => { - assert.equal(ActionCable.adapters.WebSocket, window.WebSocket) + test("default is self.WebSocket", assert => { + assert.equal(ActionCable.adapters.WebSocket, self.WebSocket) }) }) module("logger", () => { - test("default is window.console", assert => { - assert.equal(ActionCable.adapters.logger, window.console) + test("default is self.console", assert => { + assert.equal(ActionCable.adapters.logger, self.console) }) }) }) diff --git a/actioncable/test/test_helper_test.rb b/actioncable/test/test_helper_test.rb index 90e3dbf01f..02eaefc4f8 100644 --- a/actioncable/test/test_helper_test.rb +++ b/actioncable/test/test_helper_test.rb @@ -74,7 +74,7 @@ class TransmissionsTest < ActionCable::TestCase end end -class TransmitedDataTest < ActionCable::TestCase +class TransmittedDataTest < ActionCable::TestCase include ActionCable::TestHelper def test_assert_broadcast_on diff --git a/actionmailbox/CHANGELOG.md b/actionmailbox/CHANGELOG.md index 358552313f..d92367dc17 100644 --- a/actionmailbox/CHANGELOG.md +++ b/actionmailbox/CHANGELOG.md @@ -1,3 +1,5 @@ +## Rails 6.0.0.beta1 (January 18, 2019) ## + * Added to Rails. *DHH* diff --git a/actionmailbox/MIT-LICENSE b/actionmailbox/MIT-LICENSE index 4a5fe6361d..e28efa78e1 100644 --- a/actionmailbox/MIT-LICENSE +++ b/actionmailbox/MIT-LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 Basecamp, LLC +Copyright (c) 2019 Basecamp, LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/actionmailbox/README.md b/actionmailbox/README.md index 705a4432ff..9a47223d3b 100644 --- a/actionmailbox/README.md +++ b/actionmailbox/README.md @@ -1,278 +1,12 @@ # Action Mailbox -Action Mailbox routes incoming emails to controller-like mailboxes for processing in Rails. It ships with ingresses for Amazon SES, Mailgun, Mandrill, and SendGrid. You can also handle inbound mails directly via the built-in Postfix ingress. +Action Mailbox routes incoming emails to controller-like mailboxes for processing in Rails. It ships with ingresses for Amazon SES, Mailgun, Mandrill, Postmark, and SendGrid. You can also handle inbound mails directly via the built-in Exim, Postfix, and Qmail ingresses. The inbound emails are turned into `InboundEmail` records using Active Record and feature lifecycle tracking, storage of the original email on cloud storage via Active Storage, and responsible data handling with on-by-default incineration. These inbound emails are routed asynchronously using Active Job to one or several dedicated mailboxes, which are capable of interacting directly with the rest of your domain model. - -## How does this compare to Action Mailer's inbound processing? - -Rails has long had an anemic way of [receiving emails using Action Mailer](https://guides.rubyonrails.org/action_mailer_basics.html#receiving-emails), but it was poorly fleshed out, lacked cohesion with the task of sending emails, and offered no help on integrating with popular inbound email processing platforms. Action Mailbox supersedes the receiving part of Action Mailer, which will be deprecated in due course. - - -## Installing - -Assumes a Rails 5.2+ application: - -1. Install the gem: - - ```ruby - # Gemfile - gem "actionmailbox", github: "rails/actionmailbox", require: "action_mailbox" - ``` - -1. Install migrations needed for InboundEmail (and ensure Active Storage is setup) - - ``` - ./bin/rails action_mailbox:install - ./bin/rails db:migrate - ``` - -## Configuring - -### Amazon SES - -1. Install the [`aws-sdk-sns`](https://rubygems.org/gems/aws-sdk-sns) gem: - - ```ruby - # Gemfile - gem "aws-sdk-sns", ">= 1.9.0", require: false - ``` - -2. Tell Action Mailbox to accept emails from SES: - - ```ruby - # config/environments/production.rb - config.action_mailbox.ingress = :amazon - ``` - -3. [Configure SES][ses-docs] to deliver emails to your application via POST requests - to `/rails/action_mailbox/amazon/inbound_emails`. If your application lived at `https://example.com`, you would specify - the fully-qualified URL `https://example.com/rails/action_mailbox/amazon/inbound_emails`. - -[ses-docs]: https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-notifications.html - -### Mailgun - -1. Give Action Mailbox your [Mailgun API key][mailgun-api-key] so it can authenticate requests to the Mailgun ingress. - - Use `rails credentials:edit` to add your API key to your application's encrypted credentials under - `action_mailbox.mailgun_api_key`, where Action Mailbox will automatically find it: - - ```yaml - action_mailbox: - mailgun_api_key: ... - ``` - - Alternatively, provide your API key in the `MAILGUN_INGRESS_API_KEY` environment variable. - -2. Tell Action Mailbox to accept emails from Mailgun: - - ```ruby - # config/environments/production.rb - config.action_mailbox.ingress = :mailgun - ``` - -3. [Configure Mailgun][mailgun-forwarding] to forward inbound emails to `/rails/action_mailbox/mailgun/inbound_emails/mime`. - If your application lived at `https://example.com`, you would specify the fully-qualified URL - `https://example.com/rails/action_mailbox/mailgun/inbound_emails/mime`. - -[mailgun-api-key]: https://help.mailgun.com/hc/en-us/articles/203380100-Where-can-I-find-my-API-key-and-SMTP-credentials- -[mailgun-forwarding]: https://documentation.mailgun.com/en/latest/user_manual.html#receiving-forwarding-and-storing-messages - -### Mandrill - -1. Give Action Mailbox your Mandrill API key so it can authenticate requests to the Mandrill ingress. - - Use `rails credentials:edit` to add your API key to your application's encrypted credentials under - `action_mailbox.mandrill_api_key`, where Action Mailbox will automatically find it: - - ```yaml - action_mailbox: - mandrill_api_key: ... - ``` - - Alternatively, provide your API key in the `MANDRILL_INGRESS_API_KEY` environment variable. - -2. Tell Action Mailbox to accept emails from Mandrill: - - ```ruby - # config/environments/production.rb - config.action_mailbox.ingress = :mandrill - ``` - -3. [Configure Mandrill][mandrill-routing] to route inbound emails to `/rails/action_mailbox/mandrill/inbound_emails`. - If your application lived at `https://example.com`, you would specify the fully-qualified URL - `https://example.com/rails/action_mailbox/mandrill/inbound_emails`. - -[mandrill-routing]: https://mandrill.zendesk.com/hc/en-us/articles/205583197-Inbound-Email-Processing-Overview - -### Postfix - -1. Tell Action Mailbox to accept emails from Postfix: - - ```ruby - # config/environments/production.rb - config.action_mailbox.ingress = :postfix - ``` - -2. Generate a strong password that Action Mailbox can use to authenticate requests to the Postfix ingress. - - Use `rails credentials:edit` to add the password to your application's encrypted credentials under - `action_mailbox.ingress_password`, where Action Mailbox will automatically find it: - - ```yaml - action_mailbox: - ingress_password: ... - ``` - - Alternatively, provide the password in the `RAILS_INBOUND_EMAIL_PASSWORD` environment variable. - -3. [Configure Postfix][postfix-config] to pipe inbound emails to `bin/rails action_mailbox:ingress:postfix`, providing - the `URL` of the Postfix ingress and the `INGRESS_PASSWORD` you previously generated. If your application lived at - `https://example.com`, the full command would look like this: - - ``` - URL=https://example.com/rails/action_mailbox/postfix/inbound_emails INGRESS_PASSWORD=... bin/rails action_mailbox:ingress:postfix - ``` - -[postfix-config]: https://serverfault.com/questions/258469/how-to-configure-postfix-to-pipe-all-incoming-email-to-a-script - -### SendGrid - -1. Tell Action Mailbox to accept emails from SendGrid: - - ```ruby - # config/environments/production.rb - config.action_mailbox.ingress = :sendgrid - ``` - -2. Generate a strong password that Action Mailbox can use to authenticate requests to the SendGrid ingress. - - Use `rails credentials:edit` to add the password to your application's encrypted credentials under - `action_mailbox.ingress_password`, where Action Mailbox will automatically find it: - - ```yaml - action_mailbox: - ingress_password: ... - ``` - - Alternatively, provide the password in the `RAILS_INBOUND_EMAIL_PASSWORD` environment variable. - -3. [Configure SendGrid Inbound Parse][sendgrid-config] to forward inbound emails to - `/rails/action_mailbox/sendgrid/inbound_emails` with the username `actionmailbox` and the password you previously - generated. If your application lived at `https://example.com`, you would configure SendGrid with the following URL: - - ``` - https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/sendgrid/inbound_emails - ``` - - **⚠️ Note:** When configuring your SendGrid Inbound Parse webhook, be sure to check the box labeled **“Post the raw, - full MIME message.”** Action Mailbox needs the raw MIME message to work. - -[sendgrid-config]: https://sendgrid.com/docs/for-developers/parsing-email/setting-up-the-inbound-parse-webhook/ - - -## Examples - -Configure basic routing: - -```ruby -# app/mailboxes/application_mailbox.rb -class ApplicationMailbox < ActionMailbox::Base - routing /^save@/i => :forwards - routing /@replies\./i => :replies -end -``` - -Then setup a mailbox: - -```ruby -# Generate new mailbox -bin/rails generate mailbox forwards -``` - -```ruby -# app/mailboxes/forwards_mailbox.rb -class ForwardsMailbox < ApplicationMailbox - # Callbacks specify prerequisites to processing - before_processing :require_forward - - def process - if forwarder.buckets.one? - record_forward - else - stage_forward_and_request_more_details - end - end - - private - def require_forward - unless message.forward? - # Use Action Mailers to bounce incoming emails back to sender – this halts processing - bounce_with Forwards::BounceMailer.missing_forward( - inbound_email, forwarder: forwarder - ) - end - end - - def forwarder - @forwarder ||= Person.where(email_address: mail.from) - end - - def record_forward - forwarder.buckets.first.record \ - Forward.new forwarder: forwarder, subject: message.subject, content: mail.content - end - - def stage_forward_and_request_more_details - Forwards::RoutingMailer.choose_project(mail).deliver_now - end -end -``` - -## Incineration of InboundEmails - -By default, an InboundEmail that has been successfully processed will be incinerated after 30 days. This ensures you're not holding on to people's data willy-nilly after they may have canceled their accounts or deleted their content. The intention is that after you've processed an email, you should have extracted all the data you needed and turned it into domain models and content on your side of the application. The InboundEmail simply stays in the system for the extra time to provide debugging and forensics options. - -The actual incineration is done via the `IncinerationJob` that's scheduled to run after `config.action_mailbox.incinerate_after` time. This value is by default set to `30.days`, but you can change it in your production.rb configuration. (Note that this far-future incineration scheduling relies on your job queue being able to hold jobs for that long.) - - -## Working with Action Mailbox in development - -It's helpful to be able to test incoming emails in development without actually sending and receiving real emails. To accomplish this, there's a conductor controller mounted at `/rails/conductor/action_mailbox/inbound_emails`, which gives you an index of all the InboundEmails in the system, their state of processing, and a form to create a new InboundEmail as well. - - -## Testing mailboxes - -Example: - -```ruby -class ForwardsMailboxTest < ActionMailbox::TestCase - test "directly recording a client forward for a forwarder and forwardee corresponding to one project" do - assert_difference -> { people(:david).buckets.first.recordings.count } do - receive_inbound_email_from_mail \ - to: 'save@example.com', - from: people(:david).email_address, - subject: "Fwd: Status update?", - body: <<~BODY - --- Begin forwarded message --- - From: Frank Holland <frank@microsoft.com> - - What's the status? - BODY - end - - recording = people(:david).buckets.first.recordings.last - assert_equal people(:david), recording.creator - assert_equal "Status update?", recording.forward.subject - assert_match "What's the status?", recording.forward.content.to_s - end -end -``` - +You can read more about Action Mailbox in the [Action Mailbox Basics](https://edgeguides.rubyonrails.org/action_mailbox_basics.html) guide. ## License diff --git a/actionmailbox/Rakefile b/actionmailbox/Rakefile index 5cda013f96..36aed17282 100644 --- a/actionmailbox/Rakefile +++ b/actionmailbox/Rakefile @@ -4,6 +4,8 @@ require "bundler/setup" require "bundler/gem_tasks" require "rake/testtask" +task :package + Rake::TestTask.new do |t| t.libs << "test" t.pattern = "test/**/*_test.rb" diff --git a/actionmailbox/actionmailbox.gemspec b/actionmailbox/actionmailbox.gemspec index 5e6ee72ff8..c38857f6d7 100644 --- a/actionmailbox/actionmailbox.gemspec +++ b/actionmailbox/actionmailbox.gemspec @@ -17,7 +17,7 @@ Gem::Specification.new do |s| s.email = ["david@loudthinking.com", "george@basecamp.com"] s.homepage = "https://rubyonrails.org" - s.files = Dir["MIT-LICENSE", "README.md", "lib/**/*", "app/**/*", "config/**/*", "db/**/*"] + s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*", "app/**/*", "config/**/*", "db/**/*"] s.require_path = "lib" s.metadata = { diff --git a/actionmailbox/app/controllers/action_mailbox/base_controller.rb b/actionmailbox/app/controllers/action_mailbox/base_controller.rb index cc22ea803c..f0f1f555e6 100644 --- a/actionmailbox/app/controllers/action_mailbox/base_controller.rb +++ b/actionmailbox/app/controllers/action_mailbox/base_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module ActionMailbox - # The base class for all Active Mailbox ingress controllers. + # The base class for all Action Mailbox ingress controllers. class BaseController < ActionController::Base skip_forgery_protection diff --git a/actionmailbox/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb b/actionmailbox/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb index cf45ac8408..e0a187054e 100644 --- a/actionmailbox/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb +++ b/actionmailbox/app/controllers/action_mailbox/ingresses/amazon/inbound_emails_controller.rb @@ -37,7 +37,7 @@ module ActionMailbox def self.prepare self.verifier ||= begin - require "aws-sdk-sns/message_verifier" + require "aws-sdk-sns" Aws::SNS::MessageVerifier.new end end diff --git a/actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb b/actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb index ebe83a9ec0..bf0fd562fe 100644 --- a/actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb +++ b/actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb @@ -38,7 +38,7 @@ module ActionMailbox # config.action_mailbox.ingress = :mailgun # # 3. {Configure Mailgun}[https://documentation.mailgun.com/en/latest/user_manual.html#receiving-forwarding-and-storing-messages] - # to forward inbound emails to `/rails/action_mailbox/mailgun/inbound_emails/mime`. + # to forward inbound emails to +/rails/action_mailbox/mailgun/inbound_emails/mime+. # # If your application lived at <tt>https://example.com</tt>, you would specify the fully-qualified URL # <tt>https://example.com/rails/action_mailbox/mailgun/inbound_emails/mime</tt>. diff --git a/actionmailbox/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb b/actionmailbox/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb new file mode 100644 index 0000000000..309085c82a --- /dev/null +++ b/actionmailbox/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module ActionMailbox + # Ingests inbound emails from Postmark. Requires a +RawEmail+ parameter containing a full RFC 822 message. + # + # Authenticates requests using HTTP basic access authentication. The username is always +actionmailbox+, and the + # password is read from the application's encrypted credentials or an environment variable. See the Usage section below. + # + # Note that basic authentication is insecure over unencrypted HTTP. An attacker that intercepts cleartext requests to + # the Postmark ingress can learn its password. You should only use the Postmark ingress over HTTPS. + # + # Returns: + # + # - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox + # - <tt>401 Unauthorized</tt> if the request's signature could not be validated + # - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from Postmark + # - <tt>422 Unprocessable Entity</tt> if the request is missing the required +RawEmail+ parameter + # - <tt>500 Server Error</tt> if the ingress password is not configured, or if one of the Active Record database, + # the Active Storage service, or the Active Job backend is misconfigured or unavailable + # + # == Usage + # + # 1. Tell Action Mailbox to accept emails from Postmark: + # + # # config/environments/production.rb + # config.action_mailbox.ingress = :postmark + # + # 2. Generate a strong password that Action Mailbox can use to authenticate requests to the Postmark ingress. + # + # Use <tt>rails credentials:edit</tt> to add the password to your application's encrypted credentials under + # +action_mailbox.ingress_password+, where Action Mailbox will automatically find it: + # + # action_mailbox: + # ingress_password: ... + # + # Alternatively, provide the password in the +RAILS_INBOUND_EMAIL_PASSWORD+ environment variable. + # + # 3. {Configure Postmark}[https://postmarkapp.com/manual#configure-your-inbound-webhook-url] to forward inbound emails + # to +/rails/action_mailbox/postmark/inbound_emails+ with the username +actionmailbox+ and the password you + # previously generated. If your application lived at <tt>https://example.com</tt>, you would configure your + # Postmark inbound webhook with the following fully-qualified URL: + # + # https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/postmark/inbound_emails + # + # *NOTE:* When configuring your Postmark inbound webhook, be sure to check the box labeled *"Include raw email + # content in JSON payload"*. Action Mailbox needs the raw email content to work. + class Ingresses::Postmark::InboundEmailsController < ActionMailbox::BaseController + before_action :authenticate_by_password + + def create + ActionMailbox::InboundEmail.create_and_extract_message_id! params.require("RawEmail") + rescue ActionController::ParameterMissing => error + logger.error <<~MESSAGE + #{error.message} + + When configuring your Postmark inbound webhook, be sure to check the box + labeled "Include raw email content in JSON payload". + MESSAGE + head :unprocessable_entity + end + end +end diff --git a/actionmailbox/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb b/actionmailbox/app/controllers/action_mailbox/ingresses/relay/inbound_emails_controller.rb index 1871893e99..2d91c968c8 100644 --- a/actionmailbox/app/controllers/action_mailbox/ingresses/postfix/inbound_emails_controller.rb +++ b/actionmailbox/app/controllers/action_mailbox/ingresses/relay/inbound_emails_controller.rb @@ -1,31 +1,31 @@ # frozen_string_literal: true module ActionMailbox - # Ingests inbound emails relayed from Postfix. + # Ingests inbound emails relayed from an SMTP server. # # Authenticates requests using HTTP basic access authentication. The username is always +actionmailbox+, and the # password is read from the application's encrypted credentials or an environment variable. See the Usage section below. # # Note that basic authentication is insecure over unencrypted HTTP. An attacker that intercepts cleartext requests to - # the Postfix ingress can learn its password. You should only use the Postfix ingress over HTTPS. + # the ingress can learn its password. You should only use this ingress over HTTPS. # # Returns: # # - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for routing to the appropriate mailbox # - <tt>401 Unauthorized</tt> if the request could not be authenticated - # - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails from Postfix + # - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails relayed from an SMTP server # - <tt>415 Unsupported Media Type</tt> if the request does not contain an RFC 822 message # - <tt>500 Server Error</tt> if the ingress password is not configured, or if one of the Active Record database, # the Active Storage service, or the Active Job backend is misconfigured or unavailable # # == Usage # - # 1. Tell Action Mailbox to accept emails from Postfix: + # 1. Tell Action Mailbox to accept emails from an SMTP relay: # # # config/environments/production.rb - # config.action_mailbox.ingress = :postfix + # config.action_mailbox.ingress = :relay # - # 2. Generate a strong password that Action Mailbox can use to authenticate requests to the Postfix ingress. + # 2. Generate a strong password that Action Mailbox can use to authenticate requests to the ingress. # # Use <tt>rails credentials:edit</tt> to add the password to your application's encrypted credentials under # +action_mailbox.ingress_password+, where Action Mailbox will automatically find it: @@ -35,14 +35,20 @@ module ActionMailbox # # Alternatively, provide the password in the +RAILS_INBOUND_EMAIL_PASSWORD+ environment variable. # - # 3. {Configure Postfix}{https://serverfault.com/questions/258469/how-to-configure-postfix-to-pipe-all-incoming-email-to-a-script} - # to pipe inbound emails to <tt>bin/rails action_mailbox:ingress:postfix</tt>, providing the +URL+ of the Postfix - # ingress and the +INGRESS_PASSWORD+ you previously generated. + # 3. Configure your SMTP server to pipe inbound emails to the appropriate ingress command, providing the +URL+ of the + # relay ingress and the +INGRESS_PASSWORD+ you previously generated. # - # If your application lived at <tt>https://example.com</tt>, the full command would look like this: + # If your application lives at <tt>https://example.com</tt>, you would configure the Postfix SMTP server to pipe + # inbound emails to the following command: # - # URL=https://example.com/rails/action_mailbox/postfix/inbound_emails INGRESS_PASSWORD=... bin/rails action_mailbox:ingress:postfix - class Ingresses::Postfix::InboundEmailsController < ActionMailbox::BaseController + # bin/rails action_mailbox:ingress:postfix URL=https://example.com/rails/action_mailbox/postfix/inbound_emails INGRESS_PASSWORD=... + # + # Built-in ingress commands are available for these popular SMTP servers: + # + # - Exim (<tt>bin/rails action_mailbox:ingress:exim) + # - Postfix (<tt>bin/rails action_mailbox:ingress:postfix) + # - Qmail (<tt>bin/rails action_mailbox:ingress:qmail) + class Ingresses::Relay::InboundEmailsController < ActionMailbox::BaseController before_action :authenticate_by_password, :require_valid_rfc822_message def create diff --git a/actionmailbox/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb b/actionmailbox/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb index ec4111fefa..58da33d85e 100644 --- a/actionmailbox/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb +++ b/actionmailbox/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb @@ -35,7 +35,7 @@ module ActionMailbox # # Alternatively, provide the password in the +RAILS_INBOUND_EMAIL_PASSWORD+ environment variable. # - # 3. {Configure SendGrid Inbound Parse}{https://sendgrid.com/docs/for-developers/parsing-email/setting-up-the-inbound-parse-webhook/} + # 3. {Configure SendGrid Inbound Parse}[https://sendgrid.com/docs/for-developers/parsing-email/setting-up-the-inbound-parse-webhook/] # to forward inbound emails to +/rails/action_mailbox/sendgrid/inbound_emails+ with the username +actionmailbox+ and # the password you previously generated. If your application lived at <tt>https://example.com</tt>, you would # configure SendGrid with the following fully-qualified URL: diff --git a/actionmailbox/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb b/actionmailbox/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb index 30d6afe0c0..d051dfe665 100644 --- a/actionmailbox/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb +++ b/actionmailbox/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb @@ -20,12 +20,15 @@ module Rails private def new_mail - Mail.new params.require(:mail).permit(:from, :to, :cc, :bcc, :in_reply_to, :subject, :body).to_h + Mail.new(params.require(:mail).permit(:from, :to, :cc, :bcc, :in_reply_to, :subject, :body).to_h).tap do |mail| + params[:mail][:attachments].to_a.each do |attachment| + mail.add_file(filename: attachment.path, content: attachment.read) + end + end end def create_inbound_email(mail) - ActionMailbox::InboundEmail.create! raw_email: \ - { io: StringIO.new(mail.to_s), filename: "inbound.eml", content_type: "message/rfc822" } + ActionMailbox::InboundEmail.create_and_extract_message_id!(mail.to_s) end end end diff --git a/actionmailbox/app/jobs/action_mailbox/incineration_job.rb b/actionmailbox/app/jobs/action_mailbox/incineration_job.rb index 224c9329a5..1579a3c7c8 100644 --- a/actionmailbox/app/jobs/action_mailbox/incineration_job.rb +++ b/actionmailbox/app/jobs/action_mailbox/incineration_job.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true module ActionMailbox - # You can configure when this `IncinerationJob` will be run as a time-after-processing using the - # `config.action_mailbox.incinerate_after` or `ActionMailbox.incinerate_after` setting. + # You can configure when this +IncinerationJob+ will be run as a time-after-processing using the + # +config.action_mailbox.incinerate_after+ or +ActionMailbox.incinerate_after+ setting. # - # Since this incineration is set for the future, it'll automatically ignore any `InboundEmail`s + # Since this incineration is set for the future, it'll automatically ignore any <tt>InboundEmail</tt>s # that have already been deleted and discard itself if so. class IncinerationJob < ActiveJob::Base queue_as { ActionMailbox.queues[:incineration] } diff --git a/actionmailbox/app/models/action_mailbox/inbound_email.rb b/actionmailbox/app/models/action_mailbox/inbound_email.rb index fbf75a6080..023de19ccc 100644 --- a/actionmailbox/app/models/action_mailbox/inbound_email.rb +++ b/actionmailbox/app/models/action_mailbox/inbound_email.rb @@ -3,22 +3,22 @@ require "mail" module ActionMailbox - # The `InboundEmail` is an Active Record that keeps a reference to the raw email stored in Active Storage + # The +InboundEmail+ is an Active Record that keeps a reference to the raw email stored in Active Storage # and tracks the status of processing. By default, incoming emails will go through the following lifecycle: # # * Pending: Just received by one of the ingress controllers and scheduled for routing. # * Processing: During active processing, while a specific mailbox is running its #process method. # * Delivered: Successfully processed by the specific mailbox. - # * Failed: An exception was raised during the specific mailbox's execution of the `#process` method. + # * Failed: An exception was raised during the specific mailbox's execution of the +#process+ method. # * Bounced: Rejected processing by the specific mailbox and bounced to sender. # - # Once the `InboundEmail` has reached the status of being either `delivered`, `failed`, or `bounced`, - # it'll count as having been `#processed?`. Once processed, the `InboundEmail` will be scheduled for + # Once the +InboundEmail+ has reached the status of being either +delivered+, +failed+, or +bounced+, + # it'll count as having been +#processed?+. Once processed, the +InboundEmail+ will be scheduled for # automatic incineration at a later point. # - # When working with an `InboundEmail`, you'll usually interact with the parsed version of the source, - # which is available as a `Mail` object from `#mail`. But you can also access the raw source directly - # using the `#source` method. + # When working with an +InboundEmail+, you'll usually interact with the parsed version of the source, + # which is available as a +Mail+ object from +#mail+. But you can also access the raw source directly + # using the +#source+ method. # # Examples: # @@ -45,3 +45,5 @@ module ActionMailbox end end end + +ActiveSupport.run_load_hooks :action_mailbox_inbound_email, ActionMailbox::InboundEmail diff --git a/actionmailbox/app/models/action_mailbox/inbound_email/incineratable.rb b/actionmailbox/app/models/action_mailbox/inbound_email/incineratable.rb index 825e300648..697331ede4 100644 --- a/actionmailbox/app/models/action_mailbox/inbound_email/incineratable.rb +++ b/actionmailbox/app/models/action_mailbox/inbound_email/incineratable.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -# Ensure that the `InboundEmail` is automatically scheduled for later incineration if the status has been -# changed to `processed`. The later incineration will be invoked at the time specified by the -# `ActionMailbox.incinerate_after` time using the `IncinerationJob`. +# Ensure that the +InboundEmail+ is automatically scheduled for later incineration if the status has been +# changed to +processed+. The later incineration will be invoked at the time specified by the +# +ActionMailbox.incinerate_after+ time using the +IncinerationJob+. module ActionMailbox::InboundEmail::Incineratable extend ActiveSupport::Concern diff --git a/actionmailbox/app/models/action_mailbox/inbound_email/incineratable/incineration.rb b/actionmailbox/app/models/action_mailbox/inbound_email/incineratable/incineration.rb index 685f7fceb6..dabc83fae6 100644 --- a/actionmailbox/app/models/action_mailbox/inbound_email/incineratable/incineration.rb +++ b/actionmailbox/app/models/action_mailbox/inbound_email/incineratable/incineration.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true module ActionMailbox - # Command class for carrying out the actual incineration of the `InboundMail` that's been scheduled - # for removal. Before the incineration – which really is just a call to `#destroy!` – is run, we verify + # Command class for carrying out the actual incineration of the +InboundMail+ that's been scheduled + # for removal. Before the incineration – which really is just a call to +#destroy!+ – is run, we verify # that it's both eligible (by virtue of having already been processed) and time to do so (that is, - # the `InboundEmail` was processed after the `incinerate_after` time). + # the +InboundEmail+ was processed after the +incinerate_after+ time). class InboundEmail::Incineratable::Incineration def initialize(inbound_email) @inbound_email = inbound_email diff --git a/actionmailbox/app/models/action_mailbox/inbound_email/message_id.rb b/actionmailbox/app/models/action_mailbox/inbound_email/message_id.rb index 2ad4525929..470b93ca20 100644 --- a/actionmailbox/app/models/action_mailbox/inbound_email/message_id.rb +++ b/actionmailbox/app/models/action_mailbox/inbound_email/message_id.rb @@ -1,38 +1,38 @@ # frozen_string_literal: true -# The `Message-ID` as specified by rfc822 is supposed to be a unique identifier for that individual email. -# That makes it an ideal tracking token for debugging and forensics, just like `X-Request-Id` does for +# The +Message-ID+ as specified by rfc822 is supposed to be a unique identifier for that individual email. +# That makes it an ideal tracking token for debugging and forensics, just like +X-Request-Id+ does for # web request. # # If an inbound email does not, against the rfc822 mandate, specify a Message-ID, one will be generated -# using the approach from `Mail::MessageIdField`. +# using the approach from <tt>Mail::MessageIdField</tt>. module ActionMailbox::InboundEmail::MessageId extend ActiveSupport::Concern - included do - before_save :generate_missing_message_id - end - class_methods do - # Create a new `InboundEmail` from the raw `source` of the email, which be uploaded as a Active Storage - # attachment called `raw_email`. Before the upload, extract the Message-ID from the `source` and set - # it as an attribute on the new `InboundEmail`. + # Create a new +InboundEmail+ from the raw +source+ of the email, which be uploaded as a Active Storage + # attachment called +raw_email+. Before the upload, extract the Message-ID from the +source+ and set + # it as an attribute on the new +InboundEmail+. def create_and_extract_message_id!(source, **options) - create! options.merge(message_id: extract_message_id(source)) do |inbound_email| + message_checksum = Digest::SHA1.hexdigest(source) + message_id = extract_message_id(source) || generate_missing_message_id(message_checksum) + + create! options.merge(message_id: message_id, message_checksum: message_checksum) do |inbound_email| inbound_email.raw_email.attach io: StringIO.new(source), filename: "message.eml", content_type: "message/rfc822" end + rescue ActiveRecord::RecordNotUnique + nil end private def extract_message_id(source) Mail.from_source(source).message_id rescue nil end - end - private - def generate_missing_message_id - self.message_id ||= Mail::MessageIdField.new.message_id.tap do |message_id| - logger.warn "Message-ID couldn't be parsed or is missing. Generated a new Message-ID: #{message_id}" + def generate_missing_message_id(message_checksum) + Mail::MessageIdField.new("<#{message_checksum}@#{::Socket.gethostname}.mail>").message_id.tap do |message_id| + logger.warn "Message-ID couldn't be parsed or is missing. Generated a new Message-ID: #{message_id}" + end end - end + end end diff --git a/actionmailbox/app/models/action_mailbox/inbound_email/routable.rb b/actionmailbox/app/models/action_mailbox/inbound_email/routable.rb index 58d67eb20c..39565df166 100644 --- a/actionmailbox/app/models/action_mailbox/inbound_email/routable.rb +++ b/actionmailbox/app/models/action_mailbox/inbound_email/routable.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -# A newly received `InboundEmail` will not be routed synchronously as part of ingress controller's receival. -# Instead, the routing will be done asynchronously, using a `RoutingJob`, to ensure maximum parallel capacity. +# A newly received +InboundEmail+ will not be routed synchronously as part of ingress controller's receival. +# Instead, the routing will be done asynchronously, using a +RoutingJob+, to ensure maximum parallel capacity. # -# By default, all newly created `InboundEmail` records that have the status of `pending`, which is the default, +# By default, all newly created +InboundEmail+ records that have the status of +pending+, which is the default, # will be scheduled for automatic, deferred routing. module ActionMailbox::InboundEmail::Routable extend ActiveSupport::Concern @@ -12,12 +12,12 @@ module ActionMailbox::InboundEmail::Routable after_create_commit :route_later, if: :pending? end - # Enqueue a `RoutingJob` for this `InboundEmail`. + # Enqueue a +RoutingJob+ for this +InboundEmail+. def route_later ActionMailbox::RoutingJob.perform_later self end - # Route this `InboundEmail` using the routing rules declared on the `ApplicationMailbox`. + # Route this +InboundEmail+ using the routing rules declared on the +ApplicationMailbox+. def route ApplicationMailbox.route self end diff --git a/actionmailbox/app/views/layouts/rails/conductor.html.erb b/actionmailbox/app/views/layouts/rails/conductor.html.erb index 75157feb78..1cad6560c4 100644 --- a/actionmailbox/app/views/layouts/rails/conductor.html.erb +++ b/actionmailbox/app/views/layouts/rails/conductor.html.erb @@ -4,4 +4,5 @@ </head> <body> <%= yield %> +</body> </html> diff --git a/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb b/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb index 7f1ec74496..a3c3d39064 100644 --- a/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb +++ b/actionmailbox/app/views/rails/conductor/action_mailbox/inbound_emails/new.html.erb @@ -38,5 +38,10 @@ <%= form.text_area :body, size: "40x20" %> </div> + <div> + <%= form.label :attachments, "Attachments" %><br> + <%= form.file_field :attachments, multiple: true %> + </div> + <%= form.submit "Deliver inbound email" %> <% end %> diff --git a/actionmailbox/config/routes.rb b/actionmailbox/config/routes.rb index f1bc9847f5..517d2835af 100644 --- a/actionmailbox/config/routes.rb +++ b/actionmailbox/config/routes.rb @@ -4,7 +4,8 @@ Rails.application.routes.draw do scope "/rails/action_mailbox", module: "action_mailbox/ingresses" do post "/amazon/inbound_emails" => "amazon/inbound_emails#create", as: :rails_amazon_inbound_emails post "/mandrill/inbound_emails" => "mandrill/inbound_emails#create", as: :rails_mandrill_inbound_emails - post "/postfix/inbound_emails" => "postfix/inbound_emails#create", as: :rails_postfix_inbound_emails + post "/postmark/inbound_emails" => "postmark/inbound_emails#create", as: :rails_postmark_inbound_emails + post "/relay/inbound_emails" => "relay/inbound_emails#create", as: :rails_relay_inbound_emails post "/sendgrid/inbound_emails" => "sendgrid/inbound_emails#create", as: :rails_sendgrid_inbound_emails # Mailgun requires that a webhook's URL end in 'mime' for it to receive the raw contents of emails. diff --git a/actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb b/actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb index 85e19bc3c6..2bf4335808 100644 --- a/actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb +++ b/actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb @@ -1,11 +1,13 @@ -class CreateActionMailboxTables < ActiveRecord::Migration[5.2] +class CreateActionMailboxTables < ActiveRecord::Migration[6.0] def change create_table :action_mailbox_inbound_emails do |t| t.integer :status, default: 0, null: false - t.string :message_id + t.string :message_id, null: false + t.string :message_checksum, null: false - t.datetime :created_at, precision: 6 - t.datetime :updated_at, precision: 6 + t.timestamps + + t.index [ :message_id, :message_checksum ], name: "index_action_mailbox_inbound_emails_uniqueness", unique: true end end end diff --git a/actionmailbox/lib/action_mailbox/base.rb b/actionmailbox/lib/action_mailbox/base.rb index 76c3a4886c..ff8587acd1 100644 --- a/actionmailbox/lib/action_mailbox/base.rb +++ b/actionmailbox/lib/action_mailbox/base.rb @@ -7,7 +7,7 @@ require "action_mailbox/routing" module ActionMailbox # The base class for all application mailboxes. Not intended to be inherited from directly. Inherit from - # `ApplicationMailbox` instead, as that's where the app-specific routing is configured. This routing + # +ApplicationMailbox+ instead, as that's where the app-specific routing is configured. This routing # is specified in the following ways: # # class ApplicationMailbox < ActionMailbox::Base @@ -27,15 +27,15 @@ module ActionMailbox # routing :all => :backstop # end # - # Application mailboxes need to overwrite the `#process` method, which is invoked by the framework after - # callbacks have been run. The callbacks available are: `before_processing`, `after_processing`, and - # `around_processing`. The primary use case is ensure certain preconditions to processing are fulfilled - # using `before_processing` callbacks. + # Application mailboxes need to overwrite the +#process+ method, which is invoked by the framework after + # callbacks have been run. The callbacks available are: +before_processing+, +after_processing+, and + # +around_processing+. The primary use case is ensure certain preconditions to processing are fulfilled + # using +before_processing+ callbacks. # - # If a precondition fails to be met, you can halt the processing using the `#bounced!` method, + # If a precondition fails to be met, you can halt the processing using the +#bounced!+ method, # which will silently prevent any further processing, but not actually send out any bounce notice. You # can also pair this behavior with the invocation of an Action Mailer class responsible for sending out - # an actual bounce email. This is done using the `#bounce_with` method, which takes the mail object returned + # an actual bounce email. This is done using the +#bounce_with+ method, which takes the mail object returned # by an Action Mailer method, like so: # # class ForwardsMailbox < ApplicationMailbox @@ -50,12 +50,12 @@ module ActionMailbox # end # # During the processing of the inbound email, the status will be tracked. Before processing begins, - # the email will normally have the `pending` status. Once processing begins, just before callbacks - # and the `#process` method is called, the status is changed to `processing`. If processing is allowed to - # complete, the status is changed to `delivered`. If a bounce is triggered, then `bounced`. If an unhandled - # exception is bubbled up, then `failed`. + # the email will normally have the +pending+ status. Once processing begins, just before callbacks + # and the +#process+ method is called, the status is changed to +processing+. If processing is allowed to + # complete, the status is changed to +delivered+. If a bounce is triggered, then +bounced+. If an unhandled + # exception is bubbled up, then +failed+. # - # Exceptions can be handled at the class level using the familiar `Rescuable` approach: + # Exceptions can be handled at the class level using the familiar +Rescuable+ approach: # # class ForwardsMailbox < ApplicationMailbox # rescue_from(ApplicationSpecificVerificationError) { bounced! } @@ -77,7 +77,7 @@ module ActionMailbox @inbound_email = inbound_email end - def perform_processing + def perform_processing #:nodoc: track_status_of_inbound_email do run_callbacks :process do process @@ -92,11 +92,12 @@ module ActionMailbox # Overwrite in subclasses end - def finished_processing? + def finished_processing? #:nodoc: inbound_email.delivered? || inbound_email.bounced? end + # Enqueues the given +message+ for delivery and changes the inbound email's status to +:bounced+. def bounce_with(message) inbound_email.bounced! message.deliver_later @@ -113,3 +114,5 @@ module ActionMailbox end end end + +ActiveSupport.run_load_hooks :action_mailbox, ActionMailbox::Base diff --git a/actionmailbox/lib/action_mailbox/engine.rb b/actionmailbox/lib/action_mailbox/engine.rb index 0400469ff7..d5a07a7dce 100644 --- a/actionmailbox/lib/action_mailbox/engine.rb +++ b/actionmailbox/lib/action_mailbox/engine.rb @@ -1,6 +1,11 @@ # frozen_string_literal: true -require "rails/engine" +require "rails" +require "action_controller/railtie" +require "active_job/railtie" +require "active_record/railtie" +require "active_storage/engine" + require "action_mailbox" module ActionMailbox diff --git a/actionmailbox/lib/action_mailbox/gem_version.rb b/actionmailbox/lib/action_mailbox/gem_version.rb index 3959de9ce1..1f9c4272b9 100644 --- a/actionmailbox/lib/action_mailbox/gem_version.rb +++ b/actionmailbox/lib/action_mailbox/gem_version.rb @@ -10,7 +10,7 @@ module ActionMailbox MAJOR = 6 MINOR = 0 TINY = 0 - PRE = "alpha" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/actionmailbox/lib/action_mailbox/postfix_relayer.rb b/actionmailbox/lib/action_mailbox/relayer.rb index d43c56ed2b..e2890acb60 100644 --- a/actionmailbox/lib/action_mailbox/postfix_relayer.rb +++ b/actionmailbox/lib/action_mailbox/relayer.rb @@ -5,19 +5,27 @@ require "net/http" require "uri" module ActionMailbox - class PostfixRelayer - class Result < Struct.new(:output) + class Relayer + class Result < Struct.new(:status_code, :message) def success? !failure? end def failure? - output.match?(/\A[45]\.\d{1,3}\.\d{1,3}(\s|\z)/) + transient_failure? || permanent_failure? + end + + def transient_failure? + status_code.start_with?("4.") + end + + def permanent_failure? + status_code.start_with?("5.") end end CONTENT_TYPE = "message/rfc822" - USER_AGENT = "Action Mailbox Postfix relayer v#{ActionMailbox.version}" + USER_AGENT = "Action Mailbox relayer v#{ActionMailbox.version}" attr_reader :uri, :username, :password @@ -28,18 +36,18 @@ module ActionMailbox def relay(source) case response = post(source) when Net::HTTPSuccess - Result.new "2.0.0 Successfully relayed message to Postfix ingress" + Result.new "2.0.0", "Successfully relayed message to ingress" when Net::HTTPUnauthorized - Result.new "4.7.0 Invalid credentials for Postfix ingress" + Result.new "4.7.0", "Invalid credentials for ingress" else - Result.new "4.0.0 HTTP #{response.code}" + Result.new "4.0.0", "HTTP #{response.code}" end rescue IOError, SocketError, SystemCallError => error - Result.new "4.4.2 Network error relaying to Postfix ingress: #{error.message}" + Result.new "4.4.2", "Network error relaying to ingress: #{error.message}" rescue Timeout::Error - Result.new "4.4.2 Timed out relaying to Postfix ingress" + Result.new "4.4.2", "Timed out relaying to ingress" rescue => error - Result.new "4.0.0 Error relaying to Postfix ingress: #{error.message}" + Result.new "4.0.0", "Error relaying to ingress: #{error.message}" end private diff --git a/actionmailbox/lib/action_mailbox/router/route.rb b/actionmailbox/lib/action_mailbox/router/route.rb index b681eb7ea8..7e98e83382 100644 --- a/actionmailbox/lib/action_mailbox/router/route.rb +++ b/actionmailbox/lib/action_mailbox/router/route.rb @@ -2,7 +2,7 @@ module ActionMailbox # Encapsulates a route, which can then be matched against an inbound_email and provide a lookup of the matching - # mailbox class. See examples for the different route addresses and how to use them in the `ActionMailbox::Base` + # mailbox class. See examples for the different route addresses and how to use them in the +ActionMailbox::Base+ # documentation. class Router::Route attr_reader :address, :mailbox_name diff --git a/actionmailbox/lib/action_mailbox/routing.rb b/actionmailbox/lib/action_mailbox/routing.rb index 1ea96c8a9d..58462a44c6 100644 --- a/actionmailbox/lib/action_mailbox/routing.rb +++ b/actionmailbox/lib/action_mailbox/routing.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module ActionMailbox - # See `ActionMailbox::Base` for how to specify routing. + # See +ActionMailbox::Base+ for how to specify routing. module Routing extend ActiveSupport::Concern diff --git a/actionmailbox/lib/action_mailbox/test_case.rb b/actionmailbox/lib/action_mailbox/test_case.rb index a501e8a7ca..5e78e428d3 100644 --- a/actionmailbox/lib/action_mailbox/test_case.rb +++ b/actionmailbox/lib/action_mailbox/test_case.rb @@ -8,3 +8,5 @@ module ActionMailbox include ActionMailbox::TestHelper end end + +ActiveSupport.run_load_hooks :action_mailbox_test_case, ActionMailbox::TestCase diff --git a/actionmailbox/lib/action_mailbox/test_helper.rb b/actionmailbox/lib/action_mailbox/test_helper.rb index 02c52fb779..0ec9152844 100644 --- a/actionmailbox/lib/action_mailbox/test_helper.rb +++ b/actionmailbox/lib/action_mailbox/test_helper.rb @@ -4,38 +4,38 @@ require "mail" module ActionMailbox module TestHelper - # Create an `InboundEmail` record using an eml fixture in the format of message/rfc822 + # Create an +InboundEmail+ record using an eml fixture in the format of message/rfc822 # referenced with +fixture_name+ located in +test/fixtures/files/fixture_name+. def create_inbound_email_from_fixture(fixture_name, status: :processing) create_inbound_email_from_source file_fixture(fixture_name).read, status: status end - # Create an `InboundEmail` by specifying it using `Mail.new` options. Example: + # Create an +InboundEmail+ by specifying it using +Mail.new+ options. Example: # # create_inbound_email_from_mail(from: "david@loudthinking.com", subject: "Hello!") def create_inbound_email_from_mail(status: :processing, **mail_options) create_inbound_email_from_source Mail.new(mail_options).to_s, status: status end - # Create an `InboundEmail` using the raw rfc822 `source` as text. + # Create an +InboundEmail+ using the raw rfc822 +source+ as text. def create_inbound_email_from_source(source, status: :processing) ActionMailbox::InboundEmail.create_and_extract_message_id! source, status: status end - # Create an `InboundEmail` from fixture using the same arguments as `create_inbound_email_from_fixture` + # Create an +InboundEmail+ from fixture using the same arguments as +create_inbound_email_from_fixture+ # and immediately route it to processing. def receive_inbound_email_from_fixture(*args) create_inbound_email_from_fixture(*args).tap(&:route) end - # Create an `InboundEmail` from fixture using the same arguments as `create_inbound_email_from_mail` + # Create an +InboundEmail+ from fixture using the same arguments as +create_inbound_email_from_mail+ # and immediately route it to processing. def receive_inbound_email_from_mail(**kwargs) create_inbound_email_from_mail(**kwargs).tap(&:route) end - # Create an `InboundEmail` from fixture using the same arguments as `create_inbound_email_from_source` + # Create an +InboundEmail+ from fixture using the same arguments as +create_inbound_email_from_source+ # and immediately route it to processing. def receive_inbound_email_from_source(**kwargs) create_inbound_email_from_source(**kwargs).tap(&:route) diff --git a/actionmailbox/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt b/actionmailbox/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt index be51eb3639..ac22d03cd2 100644 --- a/actionmailbox/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt +++ b/actionmailbox/lib/rails/generators/mailbox/templates/application_mailbox.rb.tt @@ -1,5 +1,3 @@ -# frozen_string_literal: true - class ApplicationMailbox < ActionMailbox::Base # routing /something/i => :somewhere end diff --git a/actionmailbox/lib/rails/generators/mailbox/templates/mailbox.rb.tt b/actionmailbox/lib/rails/generators/mailbox/templates/mailbox.rb.tt index 56b138e2d9..110b3b9d7e 100644 --- a/actionmailbox/lib/rails/generators/mailbox/templates/mailbox.rb.tt +++ b/actionmailbox/lib/rails/generators/mailbox/templates/mailbox.rb.tt @@ -1,5 +1,3 @@ -# frozen_string_literal: true - class <%= class_name %>Mailbox < ApplicationMailbox def process end diff --git a/actionmailbox/lib/rails/generators/test_unit/templates/mailbox_test.rb.tt b/actionmailbox/lib/rails/generators/test_unit/templates/mailbox_test.rb.tt index 41749808e3..0b51f29fe4 100644 --- a/actionmailbox/lib/rails/generators/test_unit/templates/mailbox_test.rb.tt +++ b/actionmailbox/lib/rails/generators/test_unit/templates/mailbox_test.rb.tt @@ -5,7 +5,7 @@ require "test_helper" class <%= class_name %>MailboxTest < ActionMailbox::TestCase # test "receive mail" do # receive_inbound_email_from_mail \ - # to: '"someone" <someone@example.com>, + # to: '"someone" <someone@example.com>', # from: '"else" <else@example.com>', # subject: "Hello world!", # body: "Hello?" diff --git a/actionmailbox/lib/tasks/ingress.rake b/actionmailbox/lib/tasks/ingress.rake index f775bbdfd7..43b613ea12 100644 --- a/actionmailbox/lib/tasks/ingress.rake +++ b/actionmailbox/lib/tasks/ingress.rake @@ -2,12 +2,37 @@ namespace :action_mailbox do namespace :ingress do - desc "Pipe an inbound email from STDIN to the Postfix ingress (URL and INGRESS_PASSWORD required)" - task :postfix do + task :environment do require "active_support" require "active_support/core_ext/object/blank" - require "action_mailbox/postfix_relayer" + require "action_mailbox/relayer" + end + + desc "Relay an inbound email from Exim to Action Mailbox (URL and INGRESS_PASSWORD required)" + task exim: "action_mailbox:ingress:environment" do + url, password = ENV.values_at("URL", "INGRESS_PASSWORD") + + if url.blank? || password.blank? + print "URL and INGRESS_PASSWORD are required" + exit 64 # EX_USAGE + end + + ActionMailbox::Relayer.new(url: url, password: password).relay(STDIN.read).tap do |result| + print result.message + + case + when result.success? + exit 0 + when result.transient_failure? + exit 75 # EX_TEMPFAIL + else + exit 69 # EX_UNAVAILABLE + end + end + end + desc "Relay an inbound email from Postfix to Action Mailbox (URL and INGRESS_PASSWORD required)" + task postfix: "action_mailbox:ingress:environment" do url, password = ENV.values_at("URL", "INGRESS_PASSWORD") if url.blank? || password.blank? @@ -15,10 +40,33 @@ namespace :action_mailbox do exit 1 end - ActionMailbox::PostfixRelayer.new(url: url, password: password).relay(STDIN.read).tap do |result| - print result.output + ActionMailbox::Relayer.new(url: url, password: password).relay(STDIN.read).tap do |result| + print "#{result.status_code} #{result.message}" exit result.success? end end + + desc "Relay an inbound email from Qmail to Action Mailbox (URL and INGRESS_PASSWORD required)" + task qmail: "action_mailbox:ingress:environment" do + url, password = ENV.values_at("URL", "INGRESS_PASSWORD") + + if url.blank? || password.blank? + print "URL and INGRESS_PASSWORD are required" + exit 111 + end + + ActionMailbox::Relayer.new(url: url, password: password).relay(STDIN.read).tap do |result| + print result.message + + case + when result.success? + exit 0 + when result.transient_failure? + exit 111 + else + exit 100 + end + end + end end end diff --git a/actionmailbox/test/controllers/ingresses/postmark/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/ingresses/postmark/inbound_emails_controller_test.rb new file mode 100644 index 0000000000..11b579b39c --- /dev/null +++ b/actionmailbox/test/controllers/ingresses/postmark/inbound_emails_controller_test.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require "test_helper" + +class ActionMailbox::Ingresses::Postmark::InboundEmailsControllerTest < ActionDispatch::IntegrationTest + setup { ActionMailbox.ingress = :postmark } + + test "receiving an inbound email from Postmark" do + assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do + post rails_postmark_inbound_emails_url, + headers: { authorization: credentials }, params: { RawEmail: file_fixture("../files/welcome.eml").read } + end + + assert_response :no_content + + inbound_email = ActionMailbox::InboundEmail.last + assert_equal file_fixture("../files/welcome.eml").read, inbound_email.raw_email.download + assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id + end + + test "rejecting when RawEmail param is missing" do + assert_no_difference -> { ActionMailbox::InboundEmail.count } do + post rails_postmark_inbound_emails_url, + headers: { authorization: credentials }, params: { From: "someone@example.com" } + end + + assert_response :unprocessable_entity + end + + test "rejecting an unauthorized inbound email from Postmark" do + assert_no_difference -> { ActionMailbox::InboundEmail.count } do + post rails_postmark_inbound_emails_url, params: { RawEmail: file_fixture("../files/welcome.eml").read } + end + + assert_response :unauthorized + end + + test "raising when the configured password is nil" do + switch_password_to nil do + assert_raises ArgumentError do + post rails_postmark_inbound_emails_url, + headers: { authorization: credentials }, params: { RawEmail: file_fixture("../files/welcome.eml").read } + end + end + end + + test "raising when the configured password is blank" do + switch_password_to "" do + assert_raises ArgumentError do + post rails_postmark_inbound_emails_url, + headers: { authorization: credentials }, params: { RawEmail: file_fixture("../files/welcome.eml").read } + end + end + end +end diff --git a/actionmailbox/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/ingresses/relay/inbound_emails_controller_test.rb index d646f5e859..67c5993f7f 100644 --- a/actionmailbox/test/controllers/ingresses/postfix/inbound_emails_controller_test.rb +++ b/actionmailbox/test/controllers/ingresses/relay/inbound_emails_controller_test.rb @@ -2,12 +2,12 @@ require "test_helper" -class ActionMailbox::Ingresses::Postfix::InboundEmailsControllerTest < ActionDispatch::IntegrationTest - setup { ActionMailbox.ingress = :postfix } +class ActionMailbox::Ingresses::Relay::InboundEmailsControllerTest < ActionDispatch::IntegrationTest + setup { ActionMailbox.ingress = :relay } - test "receiving an inbound email from Postfix" do + test "receiving an inbound email relayed from an SMTP server" do assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do - post rails_postfix_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" }, + post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" }, params: file_fixture("../files/welcome.eml").read end @@ -18,18 +18,18 @@ class ActionMailbox::Ingresses::Postfix::InboundEmailsControllerTest < ActionDis assert_equal "0CB459E0-0336-41DA-BC88-E6E28C697DDB@37signals.com", inbound_email.message_id end - test "rejecting an unauthorized inbound email from Postfix" do + test "rejecting an unauthorized inbound email" do assert_no_difference -> { ActionMailbox::InboundEmail.count } do - post rails_postfix_inbound_emails_url, headers: { "Content-Type" => "message/rfc822" }, + post rails_relay_inbound_emails_url, headers: { "Content-Type" => "message/rfc822" }, params: file_fixture("../files/welcome.eml").read end assert_response :unauthorized end - test "rejecting an inbound email of an unsupported media type from Postfix" do + test "rejecting an inbound email of an unsupported media type" do assert_no_difference -> { ActionMailbox::InboundEmail.count } do - post rails_postfix_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "text/plain" }, + post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "text/plain" }, params: file_fixture("../files/welcome.eml").read end @@ -39,7 +39,7 @@ class ActionMailbox::Ingresses::Postfix::InboundEmailsControllerTest < ActionDis test "raising when the configured password is nil" do switch_password_to nil do assert_raises ArgumentError do - post rails_postfix_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" }, + post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" }, params: file_fixture("../files/welcome.eml").read end end @@ -48,7 +48,7 @@ class ActionMailbox::Ingresses::Postfix::InboundEmailsControllerTest < ActionDis test "raising when the configured password is blank" do switch_password_to "" do assert_raises ArgumentError do - post rails_postfix_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" }, + post rails_relay_inbound_emails_url, headers: { "Authorization" => credentials, "Content-Type" => "message/rfc822" }, params: file_fixture("../files/welcome.eml").read end end diff --git a/actionmailbox/test/controllers/rails/action_mailbox/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/rails/action_mailbox/inbound_emails_controller_test.rb new file mode 100644 index 0000000000..fcd9ad839f --- /dev/null +++ b/actionmailbox/test/controllers/rails/action_mailbox/inbound_emails_controller_test.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require "test_helper" + +class Rails::Conductor::ActionMailbox::InboundEmailsControllerTest < ActionDispatch::IntegrationTest + test "create inbound email" do + with_rails_env("development") do + assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do + post rails_conductor_inbound_emails_path, params: { + mail: { + from: "Jason Fried <jason@37signals.com>", + to: "Replies <replies@example.com>", + in_reply_to: "<4e6e35f5a38b4_479f13bb90078178@small-app-01.mail>", + subject: "Hey there", + body: "How's it going?" + } + } + end + + mail = ActionMailbox::InboundEmail.last.mail + assert_equal %w[ jason@37signals.com ], mail.from + assert_equal %w[ replies@example.com ], mail.to + assert_equal "4e6e35f5a38b4_479f13bb90078178@small-app-01.mail", mail.in_reply_to + assert_equal "Hey there", mail.subject + assert_equal "How's it going?", mail.body.decoded + end + end + + test "create inbound email with attachments" do + with_rails_env("development") do + assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do + post rails_conductor_inbound_emails_path, params: { + mail: { + from: "Jason Fried <jason@37signals.com>", + to: "Replies <replies@example.com>", + subject: "Let's debate some attachments", + body: "Let's talk about these images:", + attachments: [ fixture_file_upload("files/avatar1.jpeg"), fixture_file_upload("files/avatar2.jpeg") ] + } + } + end + + mail = ActionMailbox::InboundEmail.last.mail + assert_equal "Let's talk about these images:", mail.text_part.decoded + assert_equal 2, mail.attachments.count + end + end + + private + def with_rails_env(env) + old_rails_env = Rails.env + Rails.env = env + yield + ensure + Rails.env = old_rails_env + end +end diff --git a/actionmailbox/test/dummy/config/application.rb b/actionmailbox/test/dummy/config/application.rb index ffba407fd7..ea8ab1fc5e 100644 --- a/actionmailbox/test/dummy/config/application.rb +++ b/actionmailbox/test/dummy/config/application.rb @@ -7,7 +7,7 @@ Bundler.require(*Rails.groups) module Dummy class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 5.2 + config.load_defaults 6.0 # Settings in config/environments/* take precedence over those specified here. # Application configuration can go into files in config/initializers diff --git a/actionmailbox/test/dummy/config/puma.rb b/actionmailbox/test/dummy/config/puma.rb index a5eccf816b..71ed69a895 100644 --- a/actionmailbox/test/dummy/config/puma.rb +++ b/actionmailbox/test/dummy/config/puma.rb @@ -16,7 +16,7 @@ port ENV.fetch("PORT") { 3000 } environment ENV.fetch("RAILS_ENV") { "development" } # Specifies the number of `workers` to boot in clustered mode. -# Workers are forked webserver processes. If using threads and workers together +# Workers are forked web server processes. If using threads and workers together # the concurrency of the application would be max `threads` * `workers`. # Workers do not work on JRuby or Windows (both of which do not support # processes). diff --git a/actionmailbox/test/dummy/db/migrate/20180208205311_create_action_mailbox_tables.rb b/actionmailbox/test/dummy/db/migrate/20180208205311_create_action_mailbox_tables.rb new file mode 100644 index 0000000000..2bf4335808 --- /dev/null +++ b/actionmailbox/test/dummy/db/migrate/20180208205311_create_action_mailbox_tables.rb @@ -0,0 +1,13 @@ +class CreateActionMailboxTables < ActiveRecord::Migration[6.0] + def change + create_table :action_mailbox_inbound_emails do |t| + t.integer :status, default: 0, null: false + t.string :message_id, null: false + t.string :message_checksum, null: false + + t.timestamps + + t.index [ :message_id, :message_checksum ], name: "index_action_mailbox_inbound_emails_uniqueness", unique: true + end + end +end diff --git a/actionmailbox/test/dummy/db/migrate/20180208205311_create_action_mailroom_tables.rb b/actionmailbox/test/dummy/db/migrate/20180208205311_create_action_mailroom_tables.rb deleted file mode 100644 index 85e19bc3c6..0000000000 --- a/actionmailbox/test/dummy/db/migrate/20180208205311_create_action_mailroom_tables.rb +++ /dev/null @@ -1,11 +0,0 @@ -class CreateActionMailboxTables < ActiveRecord::Migration[5.2] - def change - create_table :action_mailbox_inbound_emails do |t| - t.integer :status, default: 0, null: false - t.string :message_id - - t.datetime :created_at, precision: 6 - t.datetime :updated_at, precision: 6 - end - end -end diff --git a/actionmailbox/test/dummy/db/migrate/20180212164506_create_active_storage_tables.active_storage.rb b/actionmailbox/test/dummy/db/migrate/20180212164506_create_active_storage_tables.active_storage.rb index 360e0d1b7a..0b2ce257c8 100644 --- a/actionmailbox/test/dummy/db/migrate/20180212164506_create_active_storage_tables.active_storage.rb +++ b/actionmailbox/test/dummy/db/migrate/20180212164506_create_active_storage_tables.active_storage.rb @@ -21,6 +21,7 @@ class CreateActiveStorageTables < ActiveRecord::Migration[5.2] t.datetime :created_at, null: false t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true + t.foreign_key :active_storage_blobs, column: :blob_id end end end diff --git a/actionmailbox/test/dummy/db/schema.rb b/actionmailbox/test/dummy/db/schema.rb index 6cfe7de765..10d4111a89 100644 --- a/actionmailbox/test/dummy/db/schema.rb +++ b/actionmailbox/test/dummy/db/schema.rb @@ -2,11 +2,11 @@ # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # -# Note that this schema.rb definition is the authoritative source for your -# database schema. If you need to create the application database on another -# system, you should be using db:schema:load, not running all the migrations -# from scratch. The latter is a flawed and unsustainable approach (the more migrations -# you'll amass, the slower it'll run and the greater likelihood for issues). +# This file is the source Rails uses to define your schema when running `rails +# db:schema:load`. When creating a new database, `rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. # # It's strongly recommended that you check this file into your version control system. @@ -14,9 +14,11 @@ ActiveRecord::Schema.define(version: 2018_02_12_164506) do create_table "action_mailbox_inbound_emails", force: :cascade do |t| t.integer "status", default: 0, null: false - t.string "message_id" - t.datetime "created_at", precision: 6 - t.datetime "updated_at", precision: 6 + t.string "message_id", null: false + t.string "message_checksum", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["message_id", "message_checksum"], name: "index_action_mailbox_inbound_emails_uniqueness", unique: true end create_table "active_storage_attachments", force: :cascade do |t| diff --git a/actionmailbox/test/fixtures/files/avatar1.jpeg b/actionmailbox/test/fixtures/files/avatar1.jpeg Binary files differnew file mode 100644 index 0000000000..31111c3bc9 --- /dev/null +++ b/actionmailbox/test/fixtures/files/avatar1.jpeg diff --git a/actionmailbox/test/fixtures/files/avatar2.jpeg b/actionmailbox/test/fixtures/files/avatar2.jpeg Binary files differnew file mode 100644 index 0000000000..e844e7f3c6 --- /dev/null +++ b/actionmailbox/test/fixtures/files/avatar2.jpeg diff --git a/actionmailbox/test/generators/mailbox_generator_test.rb b/actionmailbox/test/generators/mailbox_generator_test.rb index 624fbef420..9ffa73321b 100644 --- a/actionmailbox/test/generators/mailbox_generator_test.rb +++ b/actionmailbox/test/generators/mailbox_generator_test.rb @@ -38,7 +38,7 @@ class MailboxGeneratorTest < Rails::Generators::TestCase assert_file "test/mailboxes/inceptions/inbox_mailbox_test.rb" do |mailbox| assert_match(/class Inceptions::InboxMailboxTest < ActionMailbox::TestCase/, mailbox) assert_match(/# test "receive mail" do/, mailbox) - assert_match(/# to: '"someone" <someone@example.com>,/, mailbox) + assert_match(/# to: '"someone" <someone@example.com>',/, mailbox) end assert_file "app/mailboxes/application_mailbox.rb" do |mailbox| @@ -62,7 +62,7 @@ class MailboxGeneratorTest < Rails::Generators::TestCase assert_file "test/mailboxes/inbox_mailbox_test.rb" do |test| assert_match(/class InboxMailboxTest < ActionMailbox::TestCase/, test) assert_match(/# test "receive mail" do/, test) - assert_match(/# to: '"someone" <someone@example.com>,/, test) + assert_match(/# to: '"someone" <someone@example.com>',/, test) end end diff --git a/actionmailbox/test/unit/inbound_email_test.rb b/actionmailbox/test/unit/inbound_email_test.rb index 993423406f..76f047eb61 100644 --- a/actionmailbox/test/unit/inbound_email_test.rb +++ b/actionmailbox/test/unit/inbound_email_test.rb @@ -11,5 +11,27 @@ module ActionMailbox test "source returns the contents of the raw email" do assert_equal file_fixture("welcome.eml").read, create_inbound_email_from_fixture("welcome.eml").source end + + test "email with message id is processed only once when received multiple times" do + mail = Mail.from_source(file_fixture("welcome.eml").read) + assert mail.message_id + + inbound_email_1 = create_inbound_email_from_source(mail.to_s) + assert inbound_email_1 + + inbound_email_2 = create_inbound_email_from_source(mail.to_s) + assert_nil inbound_email_2 + end + + test "email with missing message id is processed only once when received multiple times" do + mail = Mail.from_source("Date: Fri, 28 Sep 2018 11:08:55 -0700\r\nTo: a@example.com\r\nMime-Version: 1.0\r\nContent-Type: text/plain\r\nContent-Transfer-Encoding: 7bit\r\n\r\nHello!") + assert_nil mail.message_id + + inbound_email_1 = create_inbound_email_from_source(mail.to_s) + assert inbound_email_1 + + inbound_email_2 = create_inbound_email_from_source(mail.to_s) + assert_nil inbound_email_2 + end end end diff --git a/actionmailbox/test/unit/mailbox/routing_test.rb b/actionmailbox/test/unit/mailbox/routing_test.rb index d4dad7eafb..d4ba702ac5 100644 --- a/actionmailbox/test/unit/mailbox/routing_test.rb +++ b/actionmailbox/test/unit/mailbox/routing_test.rb @@ -15,11 +15,10 @@ end class ActionMailbox::Base::RoutingTest < ActiveSupport::TestCase setup do $processed = false - @inbound_email = create_inbound_email_from_fixture("welcome.eml") end test "string routing" do - ApplicationMailbox.route @inbound_email + ApplicationMailbox.route create_inbound_email_from_fixture("welcome.eml") assert_equal "Discussion: Let's debate these attachments", $processed end diff --git a/actionmailbox/test/unit/postfix_relayer_test.rb b/actionmailbox/test/unit/relayer_test.rb index 5f7496ec3f..fb2b48ea16 100644 --- a/actionmailbox/test/unit/postfix_relayer_test.rb +++ b/actionmailbox/test/unit/relayer_test.rb @@ -2,35 +2,37 @@ require_relative "../test_helper" -require "action_mailbox/postfix_relayer" +require "action_mailbox/relayer" module ActionMailbox - class PostfixRelayerTest < ActiveSupport::TestCase - URL = "https://example.com/rails/action_mailbox/postfix/inbound_emails" + class RelayerTest < ActiveSupport::TestCase + URL = "https://example.com/rails/action_mailbox/relay/inbound_emails" INGRESS_PASSWORD = "secret" setup do - @relayer = ActionMailbox::PostfixRelayer.new(url: URL, password: INGRESS_PASSWORD) + @relayer = ActionMailbox::Relayer.new(url: URL, password: INGRESS_PASSWORD) end test "successfully relaying an email" do stub_request(:post, URL).to_return status: 204 result = @relayer.relay(file_fixture("welcome.eml").read) - assert_equal "2.0.0 Successfully relayed message to Postfix ingress", result.output + assert_equal "2.0.0", result.status_code + assert_equal "Successfully relayed message to ingress", result.message assert result.success? assert_not result.failure? assert_requested :post, URL, body: file_fixture("welcome.eml").read, basic_auth: [ "actionmailbox", INGRESS_PASSWORD ], - headers: { "Content-Type" => "message/rfc822", "User-Agent" => /\AAction Mailbox Postfix relayer v\d+\./ } + headers: { "Content-Type" => "message/rfc822", "User-Agent" => /\AAction Mailbox relayer v\d+\./ } end test "unsuccessfully relaying with invalid credentials" do stub_request(:post, URL).to_return status: 401 result = @relayer.relay(file_fixture("welcome.eml").read) - assert_equal "4.7.0 Invalid credentials for Postfix ingress", result.output + assert_equal "4.7.0", result.status_code + assert_equal "Invalid credentials for ingress", result.message assert_not result.success? assert result.failure? end @@ -39,7 +41,8 @@ module ActionMailbox stub_request(:post, URL).to_return status: 500 result = @relayer.relay(file_fixture("welcome.eml").read) - assert_equal "4.0.0 HTTP 500", result.output + assert_equal "4.0.0", result.status_code + assert_equal "HTTP 500", result.message assert_not result.success? assert result.failure? end @@ -48,7 +51,8 @@ module ActionMailbox stub_request(:post, URL).to_return status: 504 result = @relayer.relay(file_fixture("welcome.eml").read) - assert_equal "4.0.0 HTTP 504", result.output + assert_equal "4.0.0", result.status_code + assert_equal "HTTP 504", result.message assert_not result.success? assert result.failure? end @@ -57,7 +61,8 @@ module ActionMailbox stub_request(:post, URL).to_raise Errno::ECONNRESET.new result = @relayer.relay(file_fixture("welcome.eml").read) - assert_equal "4.4.2 Network error relaying to Postfix ingress: Connection reset by peer", result.output + assert_equal "4.4.2", result.status_code + assert_equal "Network error relaying to ingress: Connection reset by peer", result.message assert_not result.success? assert result.failure? end @@ -66,7 +71,8 @@ module ActionMailbox stub_request(:post, URL).to_raise SocketError.new("Failed to open TCP connection to example.com:443") result = @relayer.relay(file_fixture("welcome.eml").read) - assert_equal "4.4.2 Network error relaying to Postfix ingress: Failed to open TCP connection to example.com:443", result.output + assert_equal "4.4.2", result.status_code + assert_equal "Network error relaying to ingress: Failed to open TCP connection to example.com:443", result.message assert_not result.success? assert result.failure? end @@ -75,7 +81,8 @@ module ActionMailbox stub_request(:post, URL).to_timeout result = @relayer.relay(file_fixture("welcome.eml").read) - assert_equal "4.4.2 Timed out relaying to Postfix ingress", result.output + assert_equal "4.4.2", result.status_code + assert_equal "Timed out relaying to ingress", result.message assert_not result.success? assert result.failure? end @@ -84,7 +91,8 @@ module ActionMailbox stub_request(:post, URL).to_raise StandardError.new("Something went wrong") result = @relayer.relay(file_fixture("welcome.eml").read) - assert_equal "4.0.0 Error relaying to Postfix ingress: Something went wrong", result.output + assert_equal "4.0.0", result.status_code + assert_equal "Error relaying to ingress: Something went wrong", result.message assert_not result.success? assert result.failure? end diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index 5f3fc44e6e..806e428481 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1,3 +1,9 @@ +## Rails 6.0.0.beta1 (January 18, 2019) ## + +* Deprecate `ActionMailer::Base.receive` in favor of [Action Mailbox](https://github.com/rails/rails/tree/master/actionmailbox). + + *George Claghorn* + * Add `MailDeliveryJob` for delivering both regular and parameterized mail. Deprecate using `DeliveryJob` and `Parameterized::DeliveryJob`. *Gannon McGibbon* diff --git a/actionmailer/MIT-LICENSE b/actionmailer/MIT-LICENSE index 1cb3add0fc..ab7c27c209 100644 --- a/actionmailer/MIT-LICENSE +++ b/actionmailer/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2018 David Heinemeier Hansson +Copyright (c) 2004-2019 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionmailer/README.rdoc b/actionmailer/README.rdoc index 14dfb82234..74d7ea65ce 100644 --- a/actionmailer/README.rdoc +++ b/actionmailer/README.rdoc @@ -93,42 +93,6 @@ Example: ..... end -== Receiving emails - -To receive emails, you need to implement a public instance method called -+receive+ that takes an email object as its single parameter. The Action Mailer -framework has a corresponding class method, which is also called +receive+, that -accepts a raw, unprocessed email as a string, which it then turns into the email -object and calls the receive instance method. - -Example: - - class Mailman < ActionMailer::Base - def receive(email) - page = Page.find_by(address: email.to.first) - page.emails.create( - subject: email.subject, body: email.body - ) - - if email.has_attachments? - email.attachments.each do |attachment| - page.attachments.create({ - file: attachment, description: email.subject - }) - end - end - end - end - -This Mailman can be the target for Postfix or other MTAs. In Rails, you would use -the runner in the trivial case like this: - - rails runner 'Mailman.receive(STDIN.read)' - -However, invoking Rails in the runner for each mail to be received is very -resource intensive. A single instance of Rails should be run within a daemon, if -it is going to process more than just a limited amount of email. - == Configuration The Base class has the full list of configuration options. Here's an example: diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index a6c482f1a0..c76cb3ec72 100644 --- a/actionmailer/actionmailer.gemspec +++ b/actionmailer/actionmailer.gemspec @@ -6,8 +6,8 @@ Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY s.name = "actionmailer" s.version = version - s.summary = "Email composition, delivery, and receiving framework (part of Rails)." - s.description = "Email on Rails. Compose, deliver, receive, and test emails using the familiar controller/view pattern. First-class support for multipart email and attachments." + s.summary = "Email composition and delivery framework (part of Rails)." + s.description = "Email on Rails. Compose, deliver, and test emails using the familiar controller/view pattern. First-class support for multipart email and attachments." s.required_ruby_version = ">= 2.5.0" diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb index 48f16180be..953c5fb508 100644 --- a/actionmailer/lib/action_mailer.rb +++ b/actionmailer/lib/action_mailer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2018 David Heinemeier Hansson +# Copyright (c) 2004-2019 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 8ddc90b9df..5610212fad 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -461,7 +461,7 @@ module ActionMailer helper ActionMailer::MailHelper - class_attribute :delivery_job, default: ::ActionMailer::MailDeliveryJob + class_attribute :delivery_job, default: ::ActionMailer::DeliveryJob class_attribute :default_params, default: { mime_version: "1.0", charset: "UTF-8", @@ -565,6 +565,11 @@ module ActionMailer # end # end def receive(raw_mail) + ActiveSupport::Deprecation.warn(<<~MESSAGE.squish) + ActionMailer::Base.receive is deprecated and will be removed in Rails 6.1. + Use Action Mailbox to process inbound email. + MESSAGE + ActiveSupport::Notifications.instrument("receive.action_mailer") do |payload| mail = Mail.new(raw_mail) set_payload_for_mail(payload, mail) diff --git a/actionmailer/lib/action_mailer/gem_version.rb b/actionmailer/lib/action_mailer/gem_version.rb index 72eb5d61e8..4cab52d1f3 100644 --- a/actionmailer/lib/action_mailer/gem_version.rb +++ b/actionmailer/lib/action_mailer/gem_version.rb @@ -10,7 +10,7 @@ module ActionMailer MAJOR = 6 MINOR = 0 TINY = 0 - PRE = "alpha" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/actionmailer/lib/action_mailer/parameterized.rb b/actionmailer/lib/action_mailer/parameterized.rb index 999435919e..0a97af8105 100644 --- a/actionmailer/lib/action_mailer/parameterized.rb +++ b/actionmailer/lib/action_mailer/parameterized.rb @@ -145,12 +145,20 @@ module ActionMailer if processed? super else - job = @mailer_class.delivery_job + job = delivery_job_class args = arguments_for(job, delivery_method) job.set(options).perform_later(*args) end end + def delivery_job_class + if @mailer_class.delivery_job <= MailDeliveryJob + @mailer_class.delivery_job + else + Parameterized::DeliveryJob + end + end + def arguments_for(delivery_job, delivery_method) if delivery_job <= MailDeliveryJob [@mailer_class.name, @action.to_s, delivery_method.to_s, params: @params, args: @args] diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb index bb6141b406..23488db790 100644 --- a/actionmailer/lib/action_mailer/railtie.rb +++ b/actionmailer/lib/action_mailer/railtie.rb @@ -46,6 +46,10 @@ module ActionMailer register_preview_interceptors(options.delete(:preview_interceptors)) register_observers(options.delete(:observers)) + if delivery_job = options.delete(:delivery_job) + self.delivery_job = delivery_job.constantize + end + options.each { |k, v| send("#{k}=", v) } end diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb index f647896374..448807c144 100644 --- a/actionmailer/test/abstract_unit.rb +++ b/actionmailer/test/abstract_unit.rb @@ -33,6 +33,8 @@ I18n.enforce_available_locales = false FIXTURE_LOAD_PATH = File.expand_path("fixtures", __dir__) ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH +ActionMailer::Base.delivery_job = ActionMailer::MailDeliveryJob + class ActiveSupport::TestCase include ActiveSupport::Testing::MethodCallAssertions diff --git a/actionmailer/test/legacy_delivery_job_test.rb b/actionmailer/test/legacy_delivery_job_test.rb index 112c842beb..3a3872df47 100644 --- a/actionmailer/test/legacy_delivery_job_test.rb +++ b/actionmailer/test/legacy_delivery_job_test.rb @@ -11,9 +11,6 @@ class LegacyDeliveryJobTest < ActiveSupport::TestCase class LegacyDeliveryJob < ActionMailer::DeliveryJob end - class LegacyParmeterizedDeliveryJob < ActionMailer::Parameterized::DeliveryJob - end - setup do @previous_logger = ActiveJob::Base.logger ActiveJob::Base.logger = Logger.new(nil) @@ -42,9 +39,9 @@ class LegacyDeliveryJobTest < ActiveSupport::TestCase { inviter: "david@basecamp.com", invitee: "jason@basecamp.com" }, ] - with_delivery_job(LegacyParmeterizedDeliveryJob) do + with_delivery_job(LegacyDeliveryJob) do assert_deprecated do - assert_performed_with(job: LegacyParmeterizedDeliveryJob, args: args) do + assert_performed_with(job: ActionMailer::Parameterized::DeliveryJob, args: args) do mail.deliver_later end end diff --git a/actionmailer/test/log_subscriber_test.rb b/actionmailer/test/log_subscriber_test.rb index 7686fd10c9..fb569ce45f 100644 --- a/actionmailer/test/log_subscriber_test.rb +++ b/actionmailer/test/log_subscriber_test.rb @@ -53,7 +53,9 @@ class AMLogSubscriberTest < ActionMailer::TestCase def test_receive_is_notified fixture = File.read(File.expand_path("fixtures/raw_email", __dir__)) - TestMailer.receive(fixture) + assert_deprecated do + TestMailer.receive(fixture) + end wait assert_equal(1, @logger.logged(:info).size) assert_match(/Received mail/, @logger.logged(:info).first) diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 95da4265a4..6348bef4cb 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,7 +1,26 @@ +* `ActionDispatch::SystemTestCase.driven_by` can now be called with a block + to define specific browser capabilities. + + *Edouard Chin* + + +## Rails 6.0.0.beta1 (January 18, 2019) ## + +* Remove deprecated `fragment_cache_key` helper in favor of `combined_fragment_cache_key`. + + *Rafael Mendonça França* + +* Remove deprecated methods in `ActionDispatch::TestResponse`. + + `#success?`, `missing?` and `error?` were deprecated in Rails 5.2 in favor of + `#successful?`, `not_found?` and `server_error?`. + + *Rafael Mendonça França* + * Introduce ActionDispatch::HostAuthorization This is a new middleware that guards against DNS rebinding attacks by - white-listing the allowed hosts a request can be made to. + explicitly permitting the hosts a request can be made to. Each host is checked with the case operator (`#===`) to support `RegExp`, `Proc`, `IPAddr` and custom objects as host allowances. @@ -63,7 +82,7 @@ * Apply mapping to symbols returned from dynamic CSP sources Previously if a dynamic source returned a symbol such as :self it - would be converted to a string implicity, e.g: + would be converted to a string implicitly, e.g: policy.default_src -> { :self } @@ -85,10 +104,6 @@ *Yoshiyuki Kinjo* -* Remove undocumented `params` option from `url_for` helper. - - *Ilkka Oksanen* - * Encode Content-Disposition filenames on `send_data` and `send_file`. Previously, `send_data 'data', filename: "\u{3042}.txt"` sends `"filename=\"\u{3042}.txt\""` as Content-Disposition and it can be @@ -120,7 +135,7 @@ *Assain Jaleel* -* Raises `ActionController::RespondToMismatchError` with confliciting `respond_to` invocations. +* Raises `ActionController::RespondToMismatchError` with conflicting `respond_to` invocations. `respond_to` can match multiple types and lead to undefined behavior when multiple invocations are made and the types do not match: @@ -145,7 +160,7 @@ *Aaron Kromer* -* Pass along arguments to underlying `get` method in `follow_redirect!`. +* Pass along arguments to underlying `get` method in `follow_redirect!` Now all arguments passed to `follow_redirect!` are passed to the underlying `get` method. This for example allows to set custom headers for the diff --git a/actionpack/MIT-LICENSE b/actionpack/MIT-LICENSE index 1cb3add0fc..ab7c27c209 100644 --- a/actionpack/MIT-LICENSE +++ b/actionpack/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2018 David Heinemeier Hansson +Copyright (c) 2004-2019 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionpack/lib/abstract_controller/caching/fragments.rb b/actionpack/lib/abstract_controller/caching/fragments.rb index 95078a2a28..18677ddd18 100644 --- a/actionpack/lib/abstract_controller/caching/fragments.rb +++ b/actionpack/lib/abstract_controller/caching/fragments.rb @@ -28,7 +28,6 @@ module AbstractController self.fragment_cache_keys = [] if respond_to?(:helper_method) - helper_method :fragment_cache_key helper_method :combined_fragment_cache_key end end @@ -61,25 +60,6 @@ module AbstractController end # Given a key (as described in +expire_fragment+), returns - # a key suitable for use in reading, writing, or expiring a - # cached fragment. All keys begin with <tt>views/</tt>, - # followed by any controller-wide key prefix values, ending - # with the specified +key+ value. The key is expanded using - # ActiveSupport::Cache.expand_cache_key. - def fragment_cache_key(key) - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Calling fragment_cache_key directly is deprecated and will be removed in Rails 6.0. - All fragment accessors now use the combined_fragment_cache_key method that retains the key as an array, - such that the caching stores can interrogate the parts for cache versions used in - recyclable cache keys. - MSG - - head = self.class.fragment_cache_keys.map { |k| instance_exec(&k) } - tail = key.is_a?(Hash) ? url_for(key).split("://").last : key - ActiveSupport::Cache.expand_cache_key([*head, *tail], :views) - end - - # Given a key (as described in +expire_fragment+), returns # a key array suitable for use in reading, writing, or expiring a # cached fragment. All keys begin with <tt>:views</tt>, # followed by <tt>ENV["RAILS_CACHE_ID"]</tt> or <tt>ENV["RAILS_APP_VERSION"]</tt> if set, diff --git a/actionpack/lib/action_controller/metal/basic_implicit_render.rb b/actionpack/lib/action_controller/metal/basic_implicit_render.rb index 2dc990f303..f9a758ff0e 100644 --- a/actionpack/lib/action_controller/metal/basic_implicit_render.rb +++ b/actionpack/lib/action_controller/metal/basic_implicit_render.rb @@ -6,7 +6,7 @@ module ActionController super.tap { default_render unless performed? } end - def default_render(*args) + def default_render head :no_content end end diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb index 30034be018..e1e0c6f456 100644 --- a/actionpack/lib/action_controller/metal/exceptions.rb +++ b/actionpack/lib/action_controller/metal/exceptions.rb @@ -52,7 +52,7 @@ module ActionController end # Raised when a nested respond_to is triggered and the content types of each - # are incompatible. For exampe: + # are incompatible. For example: # # respond_to do |outer_type| # outer_type.js do diff --git a/actionpack/lib/action_controller/metal/flash.rb b/actionpack/lib/action_controller/metal/flash.rb index 380f2e9591..a4861dc2c0 100644 --- a/actionpack/lib/action_controller/metal/flash.rb +++ b/actionpack/lib/action_controller/metal/flash.rb @@ -44,18 +44,18 @@ module ActionController #:nodoc: end private - def redirect_to(options = {}, response_status_and_flash = {}) #:doc: + def redirect_to(options = {}, response_options_and_flash = {}) #:doc: self.class._flash_types.each do |flash_type| - if type = response_status_and_flash.delete(flash_type) + if type = response_options_and_flash.delete(flash_type) flash[flash_type] = type end end - if other_flashes = response_status_and_flash.delete(:flash) + if other_flashes = response_options_and_flash.delete(:flash) flash.update(other_flashes) end - super(options, response_status_and_flash) + super(options, response_options_and_flash) end end end diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index 26e6f72b66..93fd57b640 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -40,7 +40,7 @@ module ActionController protocol: "https://", host: request.host, path: request.fullpath, - status: :moved_permanently + status: :moved_permanently, } if host_or_options.is_a?(Hash) diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index 0faaac1ce4..f1fb7ab0f7 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -75,7 +75,7 @@ module ActionController # Provides a proxy to access helper methods from outside the view. def helpers @helper_proxy ||= begin - proxy = ActionView::Base.new + proxy = ActionView::Base.empty proxy.config = config.inheritable_copy proxy.extend(_helpers) end diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb index d3bb58f48b..8365ddca57 100644 --- a/actionpack/lib/action_controller/metal/implicit_render.rb +++ b/actionpack/lib/action_controller/metal/implicit_render.rb @@ -30,9 +30,9 @@ module ActionController # :stopdoc: include BasicImplicitRender - def default_render(*args) + def default_render if template_exists?(action_name.to_s, _prefixes, variants: request.variant) - render(*args) + render elsif any_templates?(action_name.to_s, _prefixes) message = "#{self.class.name}\##{action_name} is missing a template " \ "for this request format and variant.\n" \ diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 118da11990..bf5e7a433f 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -124,6 +124,14 @@ module ActionController #:nodoc: # # render json: @people # + # +any+ can also be used with no arguments, in which case it will be used for any format requested by + # the user: + # + # respond_to do |format| + # format.html + # format.any { redirect_to support_path } + # end + # # Formats can have different variants. # # The request variant is a specialization of the request format, like <tt>:tablet</tt>, diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index 7361946de5..09716f7588 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -241,22 +241,8 @@ module ActionController # Performs parameters wrapping upon the request. Called automatically # by the metal call stack. def process_action(*args) - if _wrapper_enabled? - wrapped_hash = _wrap_parameters request.request_parameters - wrapped_keys = request.request_parameters.keys - wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys) - - # This will make the wrapped hash accessible from controller and view. - request.parameters.merge! wrapped_hash - request.request_parameters.merge! wrapped_hash - - # This will display the wrapped hash in the log file. - request.filtered_parameters.merge! wrapped_filtered_hash - end - ensure - # NOTE: Rescues all exceptions so they - # may be caught in ActionController::Rescue. - return super + _perform_parameter_wrapping if _wrapper_enabled? + super end private @@ -292,5 +278,20 @@ module ActionController ref = request.content_mime_type.ref _wrapper_formats.include?(ref) && _wrapper_key && !request.parameters.key?(_wrapper_key) end + + def _perform_parameter_wrapping + wrapped_hash = _wrap_parameters request.request_parameters + wrapped_keys = request.request_parameters.keys + wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys) + + # This will make the wrapped hash accessible from controller and view. + request.parameters.merge! wrapped_hash + request.request_parameters.merge! wrapped_hash + + # This will display the wrapped hash in the log file. + request.filtered_parameters.merge! wrapped_filtered_hash + rescue ActionDispatch::Http::Parameters::ParseError + # swallow parse error exception + end end end diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 2804a06a58..67c198d150 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -55,11 +55,11 @@ module ActionController # Statements after +redirect_to+ in our controller get executed, so +redirect_to+ doesn't stop the execution of the function. # To terminate the execution of the function immediately after the +redirect_to+, use return. # redirect_to post_url(@post) and return - def redirect_to(options = {}, response_status = {}) + def redirect_to(options = {}, response_options = {}) raise ActionControllerError.new("Cannot redirect to nil!") unless options raise AbstractController::DoubleRenderError if response_body - self.status = _extract_redirect_to_status(options, response_status) + self.status = _extract_redirect_to_status(options, response_options) self.location = _compute_redirect_to_location(request, options) self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>" end @@ -114,11 +114,11 @@ module ActionController public :_compute_redirect_to_location private - def _extract_redirect_to_status(options, response_status) + def _extract_redirect_to_status(options, response_options) if options.is_a?(Hash) && options.key?(:status) Rack::Utils.status_code(options.delete(:status)) - elsif response_status.key?(:status) - Rack::Utils.status_code(response_status[:status]) + elsif response_options.key?(:status) + Rack::Utils.status_code(response_options[:status]) else 302 end diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 04922b0715..815f82a1f2 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -795,7 +795,7 @@ module ActionController @permitted = coder.map["ivars"][:@permitted] when "!ruby/object:ActionController::Parameters" # YAML's Object format. Only needed because of the format - # backwardscompability above, otherwise equivalent to YAML's initialization. + # backwards compatibility above, otherwise equivalent to YAML's initialization. @parameters, @permitted = coder.map["parameters"], coder.map["permitted"] end end diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index a643484d96..57921f32b7 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -26,7 +26,7 @@ module ActionController end end - # ActionController::TestCase will be deprecated and moved to a gem in Rails 5.1. + # ActionController::TestCase will be deprecated and moved to a gem in the future. # Please use ActionDispatch::IntegrationTest going forward. class TestRequest < ActionDispatch::TestRequest #:nodoc: DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup @@ -276,9 +276,6 @@ module ActionController # after calling +post+. If the various assert methods are not sufficient, then you # may use this object to inspect the HTTP response in detail. # - # (Earlier versions of \Rails required each functional test to subclass - # Test::Unit::TestCase and define @controller, @request, @response in +setup+.) - # # == Controller is automatically inferred # # ActionController::TestCase will automatically infer the controller under test diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index a92d336214..8f39b88d56 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2018 David Heinemeier Hansson +# Copyright (c) 2004-2019 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index f67b13f657..8cc84ff36c 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -197,10 +197,12 @@ module ActionDispatch if control.empty? # Let middleware handle default behavior elsif control[:no_cache] - self._cache_control = NO_CACHE - if control[:extras] - self._cache_control = _cache_control + ", #{control[:extras].join(', ')}" - end + options = [] + options << PUBLIC if control[:public] + options << NO_CACHE + options.concat(control[:extras]) if control[:extras] + + self._cache_control = options.join(", ") else extras = control[:extras] max_age = control[:max_age] diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 26d3fd936f..1611a8b3dd 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -61,10 +61,6 @@ module ActionDispatch get_header Cookies::SIGNED_COOKIE_DIGEST end - def secret_token - get_header Cookies::SECRET_TOKEN - end - def secret_key_base get_header Cookies::SECRET_KEY_BASE end @@ -181,7 +177,6 @@ module ActionDispatch USE_AUTHENTICATED_COOKIE_ENCRYPTION = "action_dispatch.use_authenticated_cookie_encryption" ENCRYPTED_COOKIE_CIPHER = "action_dispatch.encrypted_cookie_cipher" SIGNED_COOKIE_DIGEST = "action_dispatch.signed_cookie_digest" - SECRET_TOKEN = "action_dispatch.secret_token" SECRET_KEY_BASE = "action_dispatch.secret_key_base" COOKIES_SERIALIZER = "action_dispatch.cookies_serializer" COOKIES_DIGEST = "action_dispatch.cookies_digest" @@ -215,9 +210,6 @@ module ActionDispatch # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed # cookie was tampered with by the user (or a 3rd party), +nil+ will be returned. # - # If +secret_key_base+ and +secrets.secret_token+ (deprecated) are both set, - # legacy cookies signed with the old key generator will be transparently upgraded. - # # This jar requires that you set a suitable secret for the verification on your app's +secret_key_base+. # # Example: @@ -233,9 +225,6 @@ module ActionDispatch # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read. # If the cookie was tampered with by the user (or a 3rd party), +nil+ will be returned. # - # If +secret_key_base+ and +secrets.secret_token+ (deprecated) are both set, - # legacy cookies signed with the old key generator will be transparently upgraded. - # # If +config.action_dispatch.encrypted_cookie_salt+ and +config.action_dispatch.encrypted_signed_cookie_salt+ # are both set, legacy cookies encrypted with HMAC AES-256-CBC will be transparently upgraded. # @@ -264,10 +253,6 @@ module ActionDispatch private - def upgrade_legacy_signed_cookies? - request.secret_token.present? && request.secret_key_base.present? - end - def upgrade_legacy_hmac_aes_cbc_cookies? request.secret_key_base.present? && request.encrypted_signed_cookie_salt.present? && @@ -503,13 +488,8 @@ module ActionDispatch end def cookie_metadata(name, options) - if request.use_cookies_with_metadata - metadata = expiry_options(options) - metadata[:purpose] = "cookie.#{name}" - - metadata - else - {} + expiry_options(options).tap do |metadata| + metadata[:purpose] = "cookie.#{name}" if request.use_cookies_with_metadata end end @@ -592,10 +572,6 @@ module ActionDispatch request.cookies_rotations.signed.each do |*secrets, **options| @verifier.rotate(*secrets, serializer: SERIALIZER, **options) end - - if upgrade_legacy_signed_cookies? - @verifier.rotate request.secret_token, serializer: SERIALIZER - end end private @@ -640,10 +616,6 @@ module ActionDispatch @encryptor.rotate(secret, sign_secret, cipher: legacy_cipher, digest: digest, serializer: SERIALIZER) end - - if upgrade_legacy_signed_cookies? - @legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, digest: digest, serializer: SERIALIZER) - end end private @@ -652,7 +624,7 @@ module ActionDispatch @encryptor.decrypt_and_verify(encrypted_message, on_rotation: rotate, purpose: purpose) end rescue ActiveSupport::MessageEncryptor::InvalidMessage, ActiveSupport::MessageVerifier::InvalidSignature - parse_legacy_signed_message(name, encrypted_message) + nil end def commit(name, options) @@ -660,16 +632,6 @@ module ActionDispatch raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE end - - def parse_legacy_signed_message(name, legacy_signed_message) - if defined?(@legacy_verifier) - deserialize(name) do |rotate| - rotate.call - - @legacy_verifier.verified(legacy_signed_message) - end - end - end end def initialize(app) diff --git a/actionpack/lib/action_dispatch/middleware/debug_view.rb b/actionpack/lib/action_dispatch/middleware/debug_view.rb index ac12dc13a1..5a7010a1c2 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_view.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_view.rb @@ -10,7 +10,9 @@ module ActionDispatch RESCUES_TEMPLATE_PATH = File.expand_path("templates", __dir__) def initialize(assigns) - super([RESCUES_TEMPLATE_PATH], assigns) + paths = [RESCUES_TEMPLATE_PATH] + renderer = ActionView::Renderer.new ActionView::LookupContext.new(paths) + super(renderer, assigns) end def debug_params(params) diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index fb2b2bd3b0..1fb3e9db00 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -31,22 +31,34 @@ module ActionDispatch "ActionController::MissingExactTemplate" => "missing_exact_template", ) + cattr_accessor :wrapper_exceptions, default: [ + "ActionView::Template::Error" + ] + attr_reader :backtrace_cleaner, :exception, :wrapped_causes, :line_number, :file def initialize(backtrace_cleaner, exception) @backtrace_cleaner = backtrace_cleaner - @exception = original_exception(exception) + @exception = exception @wrapped_causes = wrapped_causes_for(exception, backtrace_cleaner) expand_backtrace if exception.is_a?(SyntaxError) || exception.cause.is_a?(SyntaxError) end + def unwrapped_exception + if wrapper_exceptions.include?(exception.class.to_s) + exception.cause + else + exception + end + end + def rescue_template @@rescue_templates[@exception.class.name] end def status_code - self.class.status_code_for_exception(@exception.class.name) + self.class.status_code_for_exception(unwrapped_exception.class.name) end def application_trace @@ -122,14 +134,6 @@ module ActionDispatch Array(@exception.backtrace) end - def original_exception(exception) - if @@rescue_responses.has_key?(exception.cause.class.name) - exception.cause - else - exception - end - end - def causes_for(exception) return enum_for(__method__, exception) unless block_given? diff --git a/actionpack/lib/action_dispatch/middleware/host_authorization.rb b/actionpack/lib/action_dispatch/middleware/host_authorization.rb index 447b70112a..b7dff1df41 100644 --- a/actionpack/lib/action_dispatch/middleware/host_authorization.rb +++ b/actionpack/lib/action_dispatch/middleware/host_authorization.rb @@ -3,8 +3,8 @@ require "action_dispatch/http/request" module ActionDispatch - # This middleware guards from DNS rebinding attacks by white-listing the - # hosts a request can be sent to. + # This middleware guards from DNS rebinding attacks by explicitly permitting + # the hosts a request can be sent to. # # When a request comes to an unauthorized host, the +response_app+ # application will be executed and rendered. If no +response_app+ is given, a diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index df680c1c5f..02ccfbc81a 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -16,11 +16,6 @@ module ActionDispatch # The cookie jar used for storage is automatically configured to be the # best possible option given your application's configuration. # - # If you only have secret_token set, your cookies will be signed, but - # not encrypted. This means a user cannot alter their +user_id+ without - # knowing your app's secret key, but can easily read their +user_id+. This - # was the default for Rails 3 apps. - # # Your cookies will be encrypted using your apps secret_key_base. This # goes a step further than signed cookies in that encrypted cookies cannot # be altered or read by users. This is the default starting in Rails 4. diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index 3c88afd4d3..767143a368 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -45,7 +45,7 @@ module ActionDispatch backtrace_cleaner = request.get_header "action_dispatch.backtrace_cleaner" wrapper = ExceptionWrapper.new(backtrace_cleaner, exception) status = wrapper.status_code - request.set_header "action_dispatch.exception", wrapper.exception + request.set_header "action_dispatch.exception", wrapper.unwrapped_exception request.set_header "action_dispatch.original_path", request.path_info request.path_info = "/#{status}" response = @exceptions_app.call(request.env) diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb index 2fa78dd385..1fbc107e28 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb @@ -2,6 +2,6 @@ <h1>Blocked host: <%= @host %></h1> </header> <div id="container"> - <h2>To allow requests to <%= @host %>, add the following configuration:</h2> - <pre>Rails.application.config.hosts << "<%= @host %>"</pre> + <h2>To allow requests to <%= @host %>, add the following to your environment configuration:</h2> + <pre>config.hosts << "<%= @host %>"</pre> </div> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb index 4e2d1d0b08..a94dd982a7 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb @@ -1,5 +1,5 @@ Blocked host: <%= @host %> -To allow requests to <%= @host %>, add the following configuration: +To allow requests to <%= @host %>, add the following to your environment configuration: - Rails.application.config.hosts << "<%= @host %>" + config.hosts << "<%= @host %>" diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 5cde677051..f832719f19 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -74,8 +74,8 @@ module ActionDispatch # For routes that don't fit the <tt>resources</tt> mold, you can use the HTTP helper # methods <tt>get</tt>, <tt>post</tt>, <tt>patch</tt>, <tt>put</tt> and <tt>delete</tt>. # - # get 'post/:id' => 'posts#show' - # post 'post/:id' => 'posts#create_comment' + # get 'post/:id', to: 'posts#show' + # post 'post/:id', to: 'posts#create_comment' # # Now, if you POST to <tt>/posts/:id</tt>, it will route to the <tt>create_comment</tt> action. A GET on the same # URL will route to the <tt>show</tt> action. @@ -83,7 +83,7 @@ module ActionDispatch # If your route needs to respond to more than one HTTP method (or all methods) then using the # <tt>:via</tt> option on <tt>match</tt> is preferable. # - # match 'post/:id' => 'posts#show', via: [:get, :post] + # match 'post/:id', to: 'posts#show', via: [:get, :post] # # == Named routes # @@ -94,7 +94,7 @@ module ActionDispatch # Example: # # # In config/routes.rb - # get '/login' => 'accounts#login', as: 'login' + # get '/login', to: 'accounts#login', as: 'login' # # # With render, redirect_to, tests, etc. # redirect_to login_url @@ -120,9 +120,9 @@ module ActionDispatch # # # In config/routes.rb # controller :blog do - # get 'blog/show' => :list - # get 'blog/delete' => :delete - # get 'blog/edit' => :edit + # get 'blog/show', to: :list + # get 'blog/delete', to: :delete + # get 'blog/edit', to: :edit # end # # # provides named routes for show, delete, and edit @@ -132,7 +132,7 @@ module ActionDispatch # # Routes can generate pretty URLs. For example: # - # get '/articles/:year/:month/:day' => 'articles#find_by_id', constraints: { + # get '/articles/:year/:month/:day', to: 'articles#find_by_id', constraints: { # year: /\d{4}/, # month: /\d{1,2}/, # day: /\d{1,2}/ @@ -147,7 +147,7 @@ module ActionDispatch # You can specify a regular expression to define a format for a parameter. # # controller 'geocode' do - # get 'geocode/:postalcode' => :show, constraints: { + # get 'geocode/:postalcode', to: :show, constraints: { # postalcode: /\d{5}(-\d{4})?/ # } # end @@ -156,13 +156,13 @@ module ActionDispatch # expression modifiers: # # controller 'geocode' do - # get 'geocode/:postalcode' => :show, constraints: { + # get 'geocode/:postalcode', to: :show, constraints: { # postalcode: /hx\d\d\s\d[a-z]{2}/i # } # end # # controller 'geocode' do - # get 'geocode/:postalcode' => :show, constraints: { + # get 'geocode/:postalcode', to: :show, constraints: { # postalcode: /# Postalcode format # \d{5} #Prefix # (-\d{4})? #Suffix @@ -178,13 +178,13 @@ module ActionDispatch # # You can redirect any path to another path using the redirect helper in your router: # - # get "/stories" => redirect("/posts") + # get "/stories", to: redirect("/posts") # # == Unicode character routes # # You can specify unicode character routes in your router: # - # get "こんにちは" => "welcome#index" + # get "こんにちは", to: "welcome#index" # # == Routing to Rack Applications # @@ -192,7 +192,7 @@ module ActionDispatch # index action in the PostsController, you can specify any Rack application # as the endpoint for a matcher: # - # get "/application.js" => Sprockets + # get "/application.js", to: Sprockets # # == Reloading routes # diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 2966c969f6..972953d4f3 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -820,6 +820,10 @@ module ActionDispatch path, params = generate(route_name, path_options, recall) + if options.key? :params + params.merge! options[:params] + end + options[:path] = path options[:script_name] = script_name options[:params] = params diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index 1a31c7dbb8..fcb8ae296b 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -133,6 +133,7 @@ module ActionDispatch # <tt>ActionDispatch::Http::URL.tld_length</tt>, which in turn defaults to 1. # * <tt>:port</tt> - Optionally specify the port to connect to. # * <tt>:anchor</tt> - An anchor name to be appended to the path. + # * <tt>:params</tt> - The query parameters to be appended to the path. # * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/" # * <tt>:script_name</tt> - Specifies application path relative to domain root. If provided, prepends application path. # diff --git a/actionpack/lib/action_dispatch/system_test_case.rb b/actionpack/lib/action_dispatch/system_test_case.rb index c74c0ccced..066daa4a12 100644 --- a/actionpack/lib/action_dispatch/system_test_case.rb +++ b/actionpack/lib/action_dispatch/system_test_case.rb @@ -89,6 +89,24 @@ module ActionDispatch # { js_errors: true } # end # + # Some drivers require browser capabilities to be passed as a block instead + # of through the +options+ hash. + # + # As an example, if you want to add mobile emulation on chrome, you'll have to + # create an instance of selenium's +Chrome::Options+ object and add + # capabilities with a block. + # + # The block will be passed an instance of <tt><Driver>::Options</tt> where you can + # define the capabilities you want. Please refer to your driver documentation + # to learn about supported options. + # + # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + # driven_by :selenium, using: :chrome, screen_size: [1024, 768] do |driver_option| + # driver_option.add_emulation(device_name: 'iPhone 6') + # driver_option.add_extension('path/to/chrome_extension.crx') + # end + # end + # # Because <tt>ActionDispatch::SystemTestCase</tt> is a shim between Capybara # and Rails, any driver that is supported by Capybara is supported by system # tests as long as you include the required gems and files. @@ -134,8 +152,10 @@ module ActionDispatch # driven_by :selenium, using: :firefox # # driven_by :selenium, using: :headless_firefox - def self.driven_by(driver, using: :chrome, screen_size: [1400, 1400], options: {}) - self.driver = SystemTesting::Driver.new(driver, using: using, screen_size: screen_size, options: options) + def self.driven_by(driver, using: :chrome, screen_size: [1400, 1400], options: {}, &capabilities) + driver_options = { using: using, screen_size: screen_size, options: options } + + self.driver = SystemTesting::Driver.new(driver, driver_options, &capabilities) end driven_by :selenium diff --git a/actionpack/lib/action_dispatch/system_testing/browser.rb b/actionpack/lib/action_dispatch/system_testing/browser.rb index 1b0bce6b9e..c34907b6cb 100644 --- a/actionpack/lib/action_dispatch/system_testing/browser.rb +++ b/actionpack/lib/action_dispatch/system_testing/browser.rb @@ -29,20 +29,28 @@ module ActionDispatch end end + def capabilities + @option ||= + case type + when :chrome + ::Selenium::WebDriver::Chrome::Options.new + when :firefox + ::Selenium::WebDriver::Firefox::Options.new + end + end + private def headless_chrome_browser_options - options = Selenium::WebDriver::Chrome::Options.new - options.args << "--headless" - options.args << "--disable-gpu" if Gem.win_platform? + capabilities.args << "--headless" + capabilities.args << "--disable-gpu" if Gem.win_platform? - options + capabilities end def headless_firefox_browser_options - options = Selenium::WebDriver::Firefox::Options.new - options.args << "-headless" + capabilities.args << "-headless" - options + capabilities end end end diff --git a/actionpack/lib/action_dispatch/system_testing/driver.rb b/actionpack/lib/action_dispatch/system_testing/driver.rb index 5252ff6746..25a09dd918 100644 --- a/actionpack/lib/action_dispatch/system_testing/driver.rb +++ b/actionpack/lib/action_dispatch/system_testing/driver.rb @@ -3,11 +3,12 @@ module ActionDispatch module SystemTesting class Driver # :nodoc: - def initialize(name, **options) + def initialize(name, **options, &capabilities) @name = name @browser = Browser.new(options[:using]) @screen_size = options[:screen_size] @options = options[:options] + @capabilities = capabilities end def use @@ -22,6 +23,8 @@ module ActionDispatch end def register + define_browser_capabilities(@browser.capabilities) + Capybara.register_driver @name do |app| case @name when :selenium then register_selenium(app) @@ -31,6 +34,10 @@ module ActionDispatch end end + def define_browser_capabilities(capabilities) + @capabilities.call(capabilities) if @capabilities + end + def browser_options @options.merge(options: @browser.options).compact end diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index af41521c5c..28cde6704e 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -160,9 +160,16 @@ module ActionDispatch @controller.singleton_class.include(_routes.url_helpers) if @controller.respond_to? :view_context_class - @controller.view_context_class = Class.new(@controller.view_context_class) do + view_context_class = Class.new(@controller.view_context_class) do include _routes.url_helpers end + + custom_view_context = Module.new { + define_method(:view_context_class) do + view_context_class + end + } + @controller.extend(custom_view_context) end end yield @routes diff --git a/actionpack/lib/action_dispatch/testing/test_response.rb b/actionpack/lib/action_dispatch/testing/test_response.rb index 7c1202dc0e..6f7c86fdcf 100644 --- a/actionpack/lib/action_dispatch/testing/test_response.rb +++ b/actionpack/lib/action_dispatch/testing/test_response.rb @@ -14,33 +14,6 @@ module ActionDispatch new response.status, response.headers, response.body end - # Was the response successful? - def success? - ActiveSupport::Deprecation.warn(<<-MSG.squish) - The success? predicate is deprecated and will be removed in Rails 6.0. - Please use successful? as provided by Rack::Response::Helpers. - MSG - successful? - end - - # Was the URL not found? - def missing? - ActiveSupport::Deprecation.warn(<<-MSG.squish) - The missing? predicate is deprecated and will be removed in Rails 6.0. - Please use not_found? as provided by Rack::Response::Helpers. - MSG - not_found? - end - - # Was there a server-side error? - def error? - ActiveSupport::Deprecation.warn(<<-MSG.squish) - The error? predicate is deprecated and will be removed in Rails 6.0. - Please use server_error? as provided by Rack::Response::Helpers. - MSG - server_error? - end - def parsed_body @parsed_body ||= response_parser.call(body) end diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb index 3f69109633..36ee77c693 100644 --- a/actionpack/lib/action_pack.rb +++ b/actionpack/lib/action_pack.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2018 David Heinemeier Hansson +# Copyright (c) 2004-2019 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionpack/lib/action_pack/gem_version.rb b/actionpack/lib/action_pack/gem_version.rb index 37969fcb57..c7cb3cb91e 100644 --- a/actionpack/lib/action_pack/gem_version.rb +++ b/actionpack/lib/action_pack/gem_version.rb @@ -10,7 +10,7 @@ module ActionPack MAJOR = 6 MINOR = 0 TINY = 0 - PRE = "alpha" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/actionpack/test/controller/base_test.rb b/actionpack/test/controller/base_test.rb index 558e710df9..d8cea10153 100644 --- a/actionpack/test/controller/base_test.rb +++ b/actionpack/test/controller/base_test.rb @@ -193,7 +193,7 @@ class UrlOptionsTest < ActionController::TestCase action: "home", controller: "pages", only_path: true, - token: "secret" + params: { "token" => "secret" } } assert_equal "/home?token=secret", rs.url_for(options) diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index 6fe036dd15..5543f9120f 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -60,14 +60,6 @@ class FragmentCachingTest < ActionController::TestCase @m2v2 = ModelWithKeyAndVersion.new("model/2", "2") end - def test_fragment_cache_key - assert_deprecated do - assert_equal "views/what a key", @controller.fragment_cache_key("what a key") - assert_equal "views/test.host/fragment_caching_test/some_action", - @controller.fragment_cache_key(controller: "fragment_caching_test", action: "some_action") - end - end - def test_combined_fragment_cache_key assert_equal [ :views, "what a key" ], @controller.combined_fragment_cache_key("what a key") assert_equal [ :views, "test.host/fragment_caching_test/some_action" ], diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb index 104c9eeade..fcee812ee4 100644 --- a/actionpack/test/controller/filters_test.rb +++ b/actionpack/test/controller/filters_test.rb @@ -888,7 +888,7 @@ class ControllerWithSymbolAsFilter < PostsController yield # Do stuff... - wtf += 1 + wtf + 1 end end diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb index 409a4ec2e6..bf95c633e5 100644 --- a/actionpack/test/controller/flash_test.rb +++ b/actionpack/test/controller/flash_test.rb @@ -242,8 +242,11 @@ end class FlashIntegrationTest < ActionDispatch::IntegrationTest SessionKey = "_myapp_session" - Generator = ActiveSupport::LegacyKeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33") - Rotations = ActiveSupport::Messages::RotationConfiguration.new + Generator = ActiveSupport::CachingKeyGenerator.new( + ActiveSupport::KeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33", iterations: 1000) + ) + Rotations = ActiveSupport::Messages::RotationConfiguration.new + SIGNED_COOKIE_SALT = "signed cookie" class TestController < ActionController::Base add_flash_types :bar @@ -365,6 +368,7 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest args[0][:env] ||= {} args[0][:env]["action_dispatch.key_generator"] ||= Generator args[0][:env]["action_dispatch.cookies_rotations"] = Rotations + args[0][:env]["action_dispatch.signed_cookie_salt"] = SIGNED_COOKIE_SALT super(path, *args) end diff --git a/actionpack/test/controller/http_digest_authentication_test.rb b/actionpack/test/controller/http_digest_authentication_test.rb index b133afb343..dd4ff85d11 100644 --- a/actionpack/test/controller/http_digest_authentication_test.rb +++ b/actionpack/test/controller/http_digest_authentication_test.rb @@ -44,7 +44,10 @@ class HttpDigestAuthenticationTest < ActionController::TestCase setup do # Used as secret in generating nonce to prevent tampering of timestamp @secret = "4fb45da9e4ab4ddeb7580d6a35503d99" - @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new(@secret) + @request.env["action_dispatch.key_generator"] = ActiveSupport::CachingKeyGenerator.new( + ActiveSupport::KeyGenerator.new(@secret) + ) + @request.env["action_dispatch.http_auth_salt"] = "http authentication" end teardown do diff --git a/actionpack/test/controller/new_base/render_context_test.rb b/actionpack/test/controller/new_base/render_context_test.rb deleted file mode 100644 index 5e570a1d79..0000000000 --- a/actionpack/test/controller/new_base/render_context_test.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -require "abstract_unit" - -# This is testing the decoupling of view renderer and view context -# by allowing the controller to be used as view context. This is -# similar to the way sinatra renders templates. -module RenderContext - class BasicController < ActionController::Base - self.view_paths = [ActionView::FixtureResolver.new( - "render_context/basic/hello_world.html.erb" => "<%= @value %> from <%= self.__controller_method__ %>", - "layouts/basic.html.erb" => "?<%= yield %>?" - )] - - # 1) Include ActionView::Context to bring the required dependencies - include ActionView::Context - - # 2) Call _prepare_context that will do the required initialization - before_action :_prepare_context - - def hello_world - @value = "Hello" - render action: "hello_world", layout: false - end - - def with_layout - @value = "Hello" - render action: "hello_world", layout: "basic" - end - - protected def __controller_method__ - "controller context!" - end - - private - # 3) Set view_context to self - def view_context - self - end - end - - class RenderContextTest < Rack::TestCase - test "rendering using the controller as context" do - get "/render_context/basic/hello_world" - assert_body "Hello from controller context!" - assert_status 200 - end - - test "rendering using the controller as context with layout" do - get "/render_context/basic/with_layout" - assert_body "?Hello from controller context!?" - assert_status 200 - end - end -end diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb index 998498e1b2..7f1c41787a 100644 --- a/actionpack/test/controller/redirect_test.rb +++ b/actionpack/test/controller/redirect_test.rb @@ -68,10 +68,18 @@ class RedirectController < ActionController::Base redirect_back(fallback_location: "/things/stuff", status: 307) end + def redirect_back_with_status_and_fallback_location_to_another_host + redirect_back(fallback_location: "http://www.rubyonrails.org/", status: 307) + end + def safe_redirect_back_with_status redirect_back(fallback_location: "/things/stuff", status: 307, allow_other_host: false) end + def safe_redirect_back_with_status_and_fallback_location_to_another_host + redirect_back(fallback_location: "http://www.rubyonrails.org/", status: 307, allow_other_host: false) + end + def host_redirect redirect_to action: "other_host", only_path: false, host: "other.test.host" end @@ -280,6 +288,13 @@ class RedirectTest < ActionController::TestCase assert_equal "http://test.host/things/stuff", redirect_to_url end + def test_redirect_back_with_no_referer_redirects_to_another_host + get :redirect_back_with_status_and_fallback_location_to_another_host + + assert_response 307 + assert_equal "http://www.rubyonrails.org/", redirect_to_url + end + def test_safe_redirect_back_from_other_host @request.env["HTTP_REFERER"] = "http://another.host/coming/from" get :safe_redirect_back_with_status @@ -297,6 +312,20 @@ class RedirectTest < ActionController::TestCase assert_equal referer, redirect_to_url end + def test_safe_redirect_back_with_no_referer + get :safe_redirect_back_with_status + + assert_response 307 + assert_equal "http://test.host/things/stuff", redirect_to_url + end + + def test_safe_redirect_back_with_no_referer_redirects_to_another_host + get :safe_redirect_back_with_status_and_fallback_location_to_another_host + + assert_response 307 + assert_equal "http://www.rubyonrails.org/", redirect_to_url + end + def test_redirect_to_record with_routing do |set| set.draw do diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index 306b245bd1..4750093c5c 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -183,6 +183,11 @@ class TestController < ActionController::Base render action: "hello_world" end + def conditional_hello_without_expires_and_public_header + response.headers["Cache-Control"] = "public, no-cache" + render action: "hello_world" + end + def conditional_hello_with_bangs render action: "hello_world" end @@ -418,6 +423,11 @@ class ExpiresInRenderTest < ActionController::TestCase assert_equal "no-cache", @response.headers["Cache-Control"] end + def test_no_expires_now_with_public + get :conditional_hello_without_expires_and_public_header + assert_equal "public, no-cache", @response.headers["Cache-Control"] + end + def test_date_header_when_expires_in time = Time.mktime(2011, 10, 30) Time.stub :now, time do diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb index e381abee36..9222250b9c 100644 --- a/actionpack/test/controller/url_for_test.rb +++ b/actionpack/test/controller/url_for_test.rb @@ -354,6 +354,14 @@ module AbstractController assert_equal({ p2: "Y2" }.to_query, params[1]) end + def test_params_option + url = W.new.url_for(only_path: true, controller: "c", action: "a", params: { domain: "foo", id: "1" }) + params = extract_params(url) + assert_equal("/c/a?domain=foo&id=1", url) + assert_equal({ domain: "foo" }.to_query, params[0]) + assert_equal({ id: "1" }.to_query, params[1]) + end + def test_hash_parameter url = W.new.url_for(only_path: true, controller: "c", action: "a", query: { name: "Bob", category: "prof" }) params = extract_params(url) diff --git a/actionpack/test/controller/webservice_test.rb b/actionpack/test/controller/webservice_test.rb index 4a10637b54..23a46df5cd 100644 --- a/actionpack/test/controller/webservice_test.rb +++ b/actionpack/test/controller/webservice_test.rb @@ -14,7 +14,7 @@ class WebServiceTest < ActionDispatch::IntegrationTest end def dump_params_keys(hash = params) - hash.keys.sort.inject("") do |s, k| + hash.keys.sort.each_with_object(+"") do |k, s| value = hash[k] if value.is_a?(Hash) || value.is_a?(ActionController::Parameters) @@ -23,8 +23,8 @@ class WebServiceTest < ActionDispatch::IntegrationTest value = "" end - s += ", " unless s.empty? - s += "#{k}#{value}" + s << ", " unless s.empty? + s << "#{k}#{value}" end end end diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 6637c2cae9..4aaac1320e 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -525,21 +525,6 @@ class CookiesTest < ActionController::TestCase assert_equal 45, verifier.verify(@response.cookies["user_id"]) end - def test_signed_cookie_with_legacy_secret_scheme - @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" - - old_message = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", digest: "SHA1", serializer: Marshal).generate(45) - - @request.headers["Cookie"] = "user_id=#{old_message}" - get :get_signed_cookie - assert_equal 45, @controller.send(:cookies).signed[:user_id] - - key_generator = @request.env["action_dispatch.key_generator"] - secret = key_generator.generate_key("signed cookie") - verifier = ActiveSupport::MessageVerifier.new(secret, digest: "SHA1", serializer: Marshal) - assert_equal 45, verifier.verify(@response.cookies["user_id"]) - end - def test_tampered_with_signed_cookie key_generator = @request.env["action_dispatch.key_generator"] secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) @@ -759,175 +744,7 @@ class CookiesTest < ActionController::TestCase assert_equal ["user_name", "user_id"], @request.cookie_jar.instance_variable_get(:@cookies).keys end - def test_raises_argument_error_if_missing_secret - assert_raise(ArgumentError, nil.inspect) { - @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new(nil) - get :set_signed_cookie - } - - assert_raise(ArgumentError, "".inspect) { - @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new("") - get :set_signed_cookie - } - end - - def test_raises_argument_error_if_secret_is_probably_insecure - assert_raise(ArgumentError, "password".inspect) { - @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new("password") - get :set_signed_cookie - } - - assert_raise(ArgumentError, "secret".inspect) { - @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new("secret") - get :set_signed_cookie - } - - assert_raise(ArgumentError, "12345678901234567890123456789".inspect) { - @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new("12345678901234567890123456789") - get :set_signed_cookie - } - end - - def test_legacy_signed_cookie_is_read_and_transparently_upgraded_by_signed_cookie_jar_if_both_secret_token_and_secret_key_base_are_set - @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" - - legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate(45) - - @request.headers["Cookie"] = "user_id=#{legacy_value}" - get :get_signed_cookie - - assert_equal 45, @controller.send(:cookies).signed[:user_id] - - key_generator = @request.env["action_dispatch.key_generator"] - secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) - verifier = ActiveSupport::MessageVerifier.new(secret) - assert_equal 45, verifier.verify(@response.cookies["user_id"]) - end - - def test_legacy_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_cookie_jar_if_both_secret_token_and_secret_key_base_are_set - @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" - - legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate("bar") - - @request.headers["Cookie"] = "foo=#{legacy_value}" - get :get_encrypted_cookie - - assert_equal "bar", @controller.send(:cookies).encrypted[:foo] - - secret = @request.env["action_dispatch.key_generator"].generate_key(@request.env["action_dispatch.authenticated_encrypted_cookie_salt"], 32) - encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal) - assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) - end - - def test_legacy_json_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_cookie_jar_if_both_secret_token_and_secret_key_base_are_set - @request.env["action_dispatch.cookies_serializer"] = :json - @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" - - legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate(45) - - @request.headers["Cookie"] = "user_id=#{legacy_value}" - get :get_signed_cookie - - assert_equal 45, @controller.send(:cookies).signed[:user_id] - - key_generator = @request.env["action_dispatch.key_generator"] - secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) - verifier = ActiveSupport::MessageVerifier.new(secret, serializer: JSON) - assert_equal 45, verifier.verify(@response.cookies["user_id"]) - end - - def test_legacy_json_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_json_cookie_jar_if_both_secret_token_and_secret_key_base_are_set - @request.env["action_dispatch.cookies_serializer"] = :json - @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" - - legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate("bar") - - @request.headers["Cookie"] = "foo=#{legacy_value}" - get :get_encrypted_cookie - - assert_equal "bar", @controller.send(:cookies).encrypted[:foo] - - cipher = "aes-256-gcm" - salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] - secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len(cipher)] - encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: cipher, serializer: JSON) - assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) - end - - def test_legacy_json_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_hybrid_jar_if_both_secret_token_and_secret_key_base_are_set - @request.env["action_dispatch.cookies_serializer"] = :hybrid - @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" - - legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate(45) - - @request.headers["Cookie"] = "user_id=#{legacy_value}" - get :get_signed_cookie - - assert_equal 45, @controller.send(:cookies).signed[:user_id] - - key_generator = @request.env["action_dispatch.key_generator"] - secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) - verifier = ActiveSupport::MessageVerifier.new(secret, serializer: JSON) - assert_equal 45, verifier.verify(@response.cookies["user_id"]) - end - - def test_legacy_json_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_hybrid_cookie_jar_if_both_secret_token_and_secret_key_base_are_set - @request.env["action_dispatch.cookies_serializer"] = :hybrid - @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" - - legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate("bar") - - @request.headers["Cookie"] = "foo=#{legacy_value}" - get :get_encrypted_cookie - - assert_equal "bar", @controller.send(:cookies).encrypted[:foo] - - salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] - secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm")] - encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: JSON) - assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) - end - - def test_legacy_marshal_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_hybrid_jar_if_both_secret_token_and_secret_key_base_are_set - @request.env["action_dispatch.cookies_serializer"] = :hybrid - @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" - - legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate(45) - - @request.headers["Cookie"] = "user_id=#{legacy_value}" - get :get_signed_cookie - - assert_equal 45, @controller.send(:cookies).signed[:user_id] - - key_generator = @request.env["action_dispatch.key_generator"] - secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"]) - verifier = ActiveSupport::MessageVerifier.new(secret, serializer: JSON) - assert_equal 45, verifier.verify(@response.cookies["user_id"]) - end - - def test_legacy_marshal_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_hybrid_cookie_jar_if_both_secret_token_and_secret_key_base_are_set - @request.env["action_dispatch.cookies_serializer"] = :hybrid - - @request.env["action_dispatch.use_authenticated_cookie_encryption"] = true - @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" - @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff" - - legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate("bar") - - @request.headers["Cookie"] = "foo=#{legacy_value}" - get :get_encrypted_cookie - - assert_equal "bar", @controller.send(:cookies).encrypted[:foo] - - salt = @request.env["action_dispatch.authenticated_encrypted_cookie_salt"] - secret = @request.env["action_dispatch.key_generator"].generate_key(salt)[0, ActiveSupport::MessageEncryptor.key_len("aes-256-gcm")] - encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: JSON) - assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"]) - end - def test_legacy_signed_cookie_is_treated_as_nil_by_signed_cookie_jar_if_tampered - @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" - @request.headers["Cookie"] = "user_id=45" get :get_signed_cookie @@ -936,8 +753,6 @@ class CookiesTest < ActionController::TestCase end def test_legacy_signed_cookie_is_treated_as_nil_by_encrypted_cookie_jar_if_tampered - @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33" - @request.headers["Cookie"] = "foo=baz" get :get_encrypted_cookie @@ -1378,11 +1193,7 @@ class CookiesTest < ActionController::TestCase get :encrypted_discount_and_user_id_cookie travel 2.hours - assert_equal 50, cookies.encrypted[:user_id] - - cookies[:discount_percentage] = cookies[:user_id] - assert_not_equal 10, cookies.encrypted[:discount_percentage] - assert_equal 50, cookies.encrypted[:discount_percentage] + assert_nil cookies.signed[:user_id] end def test_switch_off_metadata_for_signed_cookies_if_config_is_false @@ -1391,11 +1202,8 @@ class CookiesTest < ActionController::TestCase get :signed_discount_and_user_id_cookie travel 2.hours - assert_equal 50, cookies.signed[:user_id] - cookies[:discount_percentage] = cookies[:user_id] - assert_not_equal 10, cookies.signed[:discount_percentage] - assert_equal 50, cookies.signed[:discount_percentage] + assert_nil cookies.signed[:user_id] end def test_read_rails_5_2_stable_encrypted_cookies_if_config_is_false diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb index c326f276bf..6914fb66f9 100644 --- a/actionpack/test/dispatch/debug_exceptions_test.rb +++ b/actionpack/test/dispatch/debug_exceptions_test.rb @@ -8,7 +8,7 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest class Boomer attr_accessor :closed - def initialize(detailed = false) + def initialize(detailed = false) @detailed = detailed @closed = false end @@ -39,52 +39,56 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest def call(env) env["action_dispatch.show_detailed_exceptions"] = @detailed req = ActionDispatch::Request.new(env) + template = ActionView::Template.new(File.read(__FILE__), __FILE__, ActionView::Template::Handlers::Raw.new, {}) + case req.path - when %r{/pass} + when "/pass" [404, { "X-Cascade" => "pass" }, self] - when %r{/not_found} + when "/not_found" raise AbstractController::ActionNotFound - when %r{/runtime_error} + when "/runtime_error" raise RuntimeError - when %r{/method_not_allowed} + when "/method_not_allowed" raise ActionController::MethodNotAllowed - when %r{/intercepted_error} + when "/intercepted_error" raise InterceptedErrorInstance - when %r{/unknown_http_method} + when "/unknown_http_method" raise ActionController::UnknownHttpMethod - when %r{/not_implemented} + when "/not_implemented" raise ActionController::NotImplemented - when %r{/unprocessable_entity} + when "/unprocessable_entity" raise ActionController::InvalidAuthenticityToken - when %r{/not_found_original_exception} + when "/not_found_original_exception" begin raise AbstractController::ActionNotFound.new rescue - raise ActionView::Template::Error.new("template") + raise ActionView::Template::Error.new(template) + end + when "/cause_mapped_to_rescue_responses" + begin + raise ActionController::ParameterMissing, :missing_param_key + rescue + raise NameError.new("uninitialized constant Userr") end - when %r{/missing_template} + when "/missing_template" raise ActionView::MissingTemplate.new(%w(foo), "foo/index", %w(foo), false, "mailer") - when %r{/bad_request} + when "/bad_request" raise ActionController::BadRequest - when %r{/missing_keys} + when "/missing_keys" raise ActionController::UrlGenerationError, "No route matches" - when %r{/parameter_missing} + when "/parameter_missing" raise ActionController::ParameterMissing, :missing_param_key - when %r{/original_syntax_error} + when "/original_syntax_error" eval "broke_syntax =" # `eval` need for raise native SyntaxError at runtime - when %r{/syntax_error_into_view} + when "/syntax_error_into_view" begin eval "broke_syntax =" rescue Exception - template = ActionView::Template.new(File.read(__FILE__), - __FILE__, - ActionView::Template::Handlers::Raw.new, - {}) raise ActionView::Template::Error.new(template) end - when %r{/framework_raises} + when "/framework_raises" method_that_raises - when %r{/nested_exceptions} + when "/nested_exceptions" raise_nested_exceptions else raise "puke!" @@ -313,12 +317,22 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest assert_match(""foo"=>"[FILTERED]"", body) end - test "show registered original exception for wrapped exceptions" do + test "show registered original exception if the last exception is TemplateError" do @app = DevelopmentApp get "/not_found_original_exception", headers: { "action_dispatch.show_exceptions" => true } assert_response 404 - assert_match(/AbstractController::ActionNotFound/, body) + assert_match %r{AbstractController::ActionNotFound}, body + assert_match %r{Showing <i>.*test/dispatch/debug_exceptions_test.rb</i>}, body + end + + test "show the last exception and cause even when the cause is mapped to resque_responses" do + @app = DevelopmentApp + + get "/cause_mapped_to_rescue_responses", headers: { "action_dispatch.show_exceptions" => true } + assert_response 500 + assert_match %r{ActionController::ParameterMissing}, body + assert_match %r{NameError}, body end test "named urls missing keys raise 500 level error" do @@ -480,6 +494,7 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest assert_select "#Application-Trace-0" do assert_select "code", /syntax error, unexpected/ end + assert_match %r{Showing <i>.*test/dispatch/debug_exceptions_test.rb</i>}, body end test "debug exceptions app shows user code that caused the error in source view" do diff --git a/actionpack/test/dispatch/host_authorization_test.rb b/actionpack/test/dispatch/host_authorization_test.rb index dae7b08ec1..5263dd2597 100644 --- a/actionpack/test/dispatch/host_authorization_test.rb +++ b/actionpack/test/dispatch/host_authorization_test.rb @@ -15,7 +15,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest assert_match "Blocked host: www.example.com", response.body end - test "passes all requests to if the whitelist is empty" do + test "allows all requests if hosts is empty" do @app = ActionDispatch::HostAuthorization.new(App, nil) get "/" @@ -24,7 +24,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest assert_equal "Success", body end - test "passes requests to allowed host" do + test "hosts can be a single element array" do @app = ActionDispatch::HostAuthorization.new(App, %w(www.example.com)) get "/" @@ -33,7 +33,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest assert_equal "Success", body end - test "the whitelist could be a single element" do + test "hosts can be a string" do @app = ActionDispatch::HostAuthorization.new(App, "www.example.com") get "/" diff --git a/actionpack/test/dispatch/live_response_test.rb b/actionpack/test/dispatch/live_response_test.rb index a9a56f205f..b673fd3805 100644 --- a/actionpack/test/dispatch/live_response_test.rb +++ b/actionpack/test/dispatch/live_response_test.rb @@ -62,7 +62,7 @@ module ActionController assert_nil @response.headers["Content-Length"] end - def test_headers_cannot_be_written_after_webserver_reads + def test_headers_cannot_be_written_after_web_server_reads @response.stream.write "omg" latch = Concurrent::CountDownLatch.new diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 9d1246b3a4..2a4d59affe 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -763,7 +763,6 @@ class RequestMethod < BaseRequestTest test "post uneffected by local inflections" do existing_acronyms = ActiveSupport::Inflector.inflections.acronyms.dup - assert_deprecated { ActiveSupport::Inflector.inflections.acronym_regex.dup } begin ActiveSupport::Inflector.inflections do |inflect| inflect.acronym "POS" diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index 4dffbd0db1..897d17885e 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -4991,8 +4991,12 @@ end class FlashRedirectTest < ActionDispatch::IntegrationTest SessionKey = "_myapp_session" - Generator = ActiveSupport::LegacyKeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33") - Rotations = ActiveSupport::Messages::RotationConfiguration.new + Generator = ActiveSupport::CachingKeyGenerator.new( + ActiveSupport::KeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33", iterations: 1000) + ) + Rotations = ActiveSupport::Messages::RotationConfiguration.new + SIGNED_COOKIE_SALT = "signed cookie" + ENCRYPTED_SIGNED_COOKIE_SALT = "sigend encrypted cookie" class KeyGeneratorMiddleware def initialize(app) @@ -5002,6 +5006,8 @@ class FlashRedirectTest < ActionDispatch::IntegrationTest def call(env) env["action_dispatch.key_generator"] ||= Generator env["action_dispatch.cookies_rotations"] ||= Rotations + env["action_dispatch.signed_cookie_salt"] = SIGNED_COOKIE_SALT + env["action_dispatch.encrypted_signed_cookie_salt"] = ENCRYPTED_SIGNED_COOKIE_SALT @app.call(env) end diff --git a/actionpack/test/dispatch/system_testing/driver_test.rb b/actionpack/test/dispatch/system_testing/driver_test.rb index a824ee0c84..0d08f17af3 100644 --- a/actionpack/test/dispatch/system_testing/driver_test.rb +++ b/actionpack/test/dispatch/system_testing/driver_test.rb @@ -2,6 +2,7 @@ require "abstract_unit" require "action_dispatch/system_testing/driver" +require "selenium/webdriver" class DriverTest < ActiveSupport::TestCase test "initializing the driver" do @@ -22,6 +23,7 @@ class DriverTest < ActiveSupport::TestCase driver = ActionDispatch::SystemTesting::Driver.new(:selenium, using: :headless_chrome, screen_size: [1400, 1400], options: { url: "http://example.com/wd/hub" }) assert_equal :selenium, driver.instance_variable_get(:@name) assert_equal :headless_chrome, driver.instance_variable_get(:@browser).name + assert_instance_of Selenium::WebDriver::Chrome::Options, driver.instance_variable_get(:@browser).options assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size) assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options) end @@ -30,6 +32,7 @@ class DriverTest < ActiveSupport::TestCase driver = ActionDispatch::SystemTesting::Driver.new(:selenium, using: :headless_firefox, screen_size: [1400, 1400], options: { url: "http://example.com/wd/hub" }) assert_equal :selenium, driver.instance_variable_get(:@name) assert_equal :headless_firefox, driver.instance_variable_get(:@browser).name + assert_instance_of Selenium::WebDriver::Firefox::Options, driver.instance_variable_get(:@browser).options assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size) assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options) end @@ -51,4 +54,70 @@ class DriverTest < ActiveSupport::TestCase test "registerable? returns false if driver is rack_test" do assert_not ActionDispatch::SystemTesting::Driver.new(:rack_test).send(:registerable?) end + + test "define extra capabilities using chrome" do + driver_option = nil + driver = ActionDispatch::SystemTesting::Driver.new(:selenium, screen_size: [1400, 1400], using: :chrome) do |option| + option.add_argument("start-maximized") + option.add_emulation(device_name: "iphone 6") + option.add_preference(:detach, true) + + driver_option = option + end + driver.use + + expected = { args: ["start-maximized"], mobileEmulation: { deviceName: "iphone 6" }, prefs: { detach: true } } + assert_equal expected, driver_option.as_json + end + + test "define extra capabilities using headless_chrome" do + driver_option = nil + driver = ActionDispatch::SystemTesting::Driver.new(:selenium, screen_size: [1400, 1400], using: :headless_chrome) do |option| + option.add_argument("start-maximized") + option.add_emulation(device_name: "iphone 6") + option.add_preference(:detach, true) + + driver_option = option + end + driver.use + + expected = { args: ["start-maximized"], mobileEmulation: { deviceName: "iphone 6" }, prefs: { detach: true } } + assert_equal expected, driver_option.as_json + end + + test "define extra capabilities using firefox" do + driver_option = nil + driver = ActionDispatch::SystemTesting::Driver.new(:selenium, screen_size: [1400, 1400], using: :firefox) do |option| + option.add_preference("browser.startup.homepage", "http://www.seleniumhq.com/") + option.add_argument("--host=127.0.0.1") + + driver_option = option + end + driver.use + + expected = { "moz:firefoxOptions" => { args: ["--host=127.0.0.1"], prefs: { "browser.startup.homepage" => "http://www.seleniumhq.com/" } } } + assert_equal expected, driver_option.as_json + end + + test "define extra capabilities using headless_firefox" do + driver_option = nil + driver = ActionDispatch::SystemTesting::Driver.new(:selenium, screen_size: [1400, 1400], using: :headless_firefox) do |option| + option.add_preference("browser.startup.homepage", "http://www.seleniumhq.com/") + option.add_argument("--host=127.0.0.1") + + driver_option = option + end + driver.use + + expected = { "moz:firefoxOptions" => { args: ["--host=127.0.0.1"], prefs: { "browser.startup.homepage" => "http://www.seleniumhq.com/" } } } + assert_equal expected, driver_option.as_json + end + + test "does not define extra capabilities" do + driver = ActionDispatch::SystemTesting::Driver.new(:selenium, screen_size: [1400, 1400], using: :firefox) + + assert_nothing_raised do + driver.use + end + end end diff --git a/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb index 097ef8af29..b756b91379 100644 --- a/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb +++ b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb @@ -3,6 +3,7 @@ require "abstract_unit" require "action_dispatch/system_testing/test_helpers/screenshot_helper" require "capybara/dsl" +require "selenium/webdriver" class ScreenshotHelperTest < ActiveSupport::TestCase test "image path is saved in tmp directory" do diff --git a/actionpack/test/dispatch/system_testing/system_test_case_test.rb b/actionpack/test/dispatch/system_testing/system_test_case_test.rb index b078a5abc5..847b09dcfe 100644 --- a/actionpack/test/dispatch/system_testing/system_test_case_test.rb +++ b/actionpack/test/dispatch/system_testing/system_test_case_test.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "abstract_unit" +require "selenium/webdriver" class SetDriverToRackTestTest < DrivenByRackTest test "uses rack_test" do diff --git a/actionpack/test/dispatch/test_response_test.rb b/actionpack/test/dispatch/test_response_test.rb index f0b8f7785d..2629a61057 100644 --- a/actionpack/test/dispatch/test_response_test.rb +++ b/actionpack/test/dispatch/test_response_test.rb @@ -27,11 +27,4 @@ class TestResponseTest < ActiveSupport::TestCase response = ActionDispatch::TestResponse.create(200, { "Content-Type" => "application/json" }, '{ "foo": "fighters" }') assert_equal({ "foo" => "fighters" }, response.parsed_body) end - - test "response status aliases deprecated" do - response = ActionDispatch::TestResponse.create - assert_deprecated { response.success? } - assert_deprecated { response.missing? } - assert_deprecated { response.error? } - end end diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb index 1f4e14aef6..f8d89def6a 100644 --- a/actionpack/test/journey/router_test.rb +++ b/actionpack/test/journey/router_test.rb @@ -284,7 +284,7 @@ module ActionDispatch def test_generate_missing_keys_no_matches_different_format_keys get "/:controller/:action/:name", to: "foo#bar" - primarty_parameters = { + primary_parameters = { id: 1, controller: "tasks", action: "show", @@ -297,9 +297,9 @@ module ActionDispatch missing_parameters = { missing_key => "task_1" } - request_parameters = primarty_parameters.merge(redirection_parameters).merge(missing_parameters) + request_parameters = primary_parameters.merge(redirection_parameters).merge(missing_parameters) - message = "No route matches #{Hash[request_parameters.sort_by { |k, v|k.to_s }].inspect}, missing required keys: #{[missing_key.to_sym].inspect}" + message = "No route matches #{Hash[request_parameters.sort_by { |k, _|k.to_s }].inspect}, missing required keys: #{[missing_key.to_sym].inspect}" error = assert_raises(ActionController::UrlGenerationError) do @formatter.generate( diff --git a/actiontext/.gitignore b/actiontext/.gitignore new file mode 100644 index 0000000000..739e00a2cb --- /dev/null +++ b/actiontext/.gitignore @@ -0,0 +1,5 @@ +/test/dummy/db/*.sqlite3 +/test/dummy/db/*.sqlite3-journal +/test/dummy/log/*.log +/test/dummy/tmp/ +/tmp/ diff --git a/actiontext/CHANGELOG.md b/actiontext/CHANGELOG.md new file mode 100644 index 0000000000..d92367dc17 --- /dev/null +++ b/actiontext/CHANGELOG.md @@ -0,0 +1,5 @@ +## Rails 6.0.0.beta1 (January 18, 2019) ## + +* Added to Rails. + + *DHH* diff --git a/actiontext/MIT-LICENSE b/actiontext/MIT-LICENSE new file mode 100644 index 0000000000..e28efa78e1 --- /dev/null +++ b/actiontext/MIT-LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Basecamp, LLC + +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. diff --git a/actiontext/README.md b/actiontext/README.md new file mode 100644 index 0000000000..aa1ad0153b --- /dev/null +++ b/actiontext/README.md @@ -0,0 +1,9 @@ +# Action Text + +Action Text brings rich text content and editing to Rails. It includes the [Trix editor](https://trix-editor.org) that handles everything from formatting to links to quotes to lists to embedded images and galleries. The rich text content generated by the Trix editor is saved in its own RichText model that's associated with any existing Active Record model in the application. Any embedded images (or other attachments) are automatically stored using Active Storage and associated with the included RichText model. + +You can read more about Action Text in the [Action Text Overview](https://edgeguides.rubyonrails.org/action_text_overview.html) guide. + +## License + +Action Text is released under the [MIT License](https://opensource.org/licenses/MIT). diff --git a/actiontext/Rakefile b/actiontext/Rakefile new file mode 100644 index 0000000000..36aed17282 --- /dev/null +++ b/actiontext/Rakefile @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "bundler/setup" +require "bundler/gem_tasks" +require "rake/testtask" + +task :package + +Rake::TestTask.new do |t| + t.libs << "test" + t.pattern = "test/**/*_test.rb" + t.verbose = true +end + +task default: :test diff --git a/actiontext/actiontext.gemspec b/actiontext/actiontext.gemspec new file mode 100644 index 0000000000..5583178e12 --- /dev/null +++ b/actiontext/actiontext.gemspec @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip + +Gem::Specification.new do |s| + s.platform = Gem::Platform::RUBY + s.name = "actiontext" + s.version = version + s.summary = "Rich text framework." + s.description = "Edit and display rich text in Rails applications." + + s.required_ruby_version = ">= 2.5.0" + + s.license = "MIT" + + s.authors = ["Javan Makhmali", "Sam Stephenson", "David Heinemeier Hansson"] + s.email = ["javan@javan.us", "sstephenson@gmail.com", "david@loudthinking.com"] + s.homepage = "https://rubyonrails.org" + + s.files = Dir["CHANGELOG.md", "MIT-LICENSE", "README.md", "lib/**/*", "app/**/*", "config/**/*", "db/**/*", "package.json"] + s.require_path = "lib" + + s.metadata = { + "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/actiontext", + "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/actiontext/CHANGELOG.md" + } + + # NOTE: Please read our dependency guidelines before updating versions: + # https://edgeguides.rubyonrails.org/security.html#dependency-management-and-cves + + s.add_dependency "activesupport", version + s.add_dependency "activerecord", version + s.add_dependency "activestorage", version + s.add_dependency "actionpack", version + + s.add_dependency "nokogiri", ">= 1.8.5" +end diff --git a/actiontext/app/helpers/action_text/content_helper.rb b/actiontext/app/helpers/action_text/content_helper.rb new file mode 100644 index 0000000000..b3335601f9 --- /dev/null +++ b/actiontext/app/helpers/action_text/content_helper.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module ActionText + module ContentHelper + SANITIZER = Rails::Html::Sanitizer.white_list_sanitizer + ALLOWED_TAGS = SANITIZER.allowed_tags + [ ActionText::Attachment::TAG_NAME, "figure", "figcaption" ] + ALLOWED_ATTRIBUTES = SANITIZER.allowed_attributes + ActionText::Attachment::ATTRIBUTES + + def render_action_text_content(content) + content = content.render_attachments do |attachment| + unless attachment.in?(content.gallery_attachments) + attachment.node.tap do |node| + node.inner_html = render(attachment, in_gallery: false).chomp + end + end + end + + content = content.render_attachment_galleries do |attachment_gallery| + render(layout: attachment_gallery, object: attachment_gallery) do + attachment_gallery.attachments.map do |attachment| + attachment.node.inner_html = render(attachment, in_gallery: true).chomp + attachment.to_html + end.join("").html_safe + end.chomp + end + + sanitize content.to_html, tags: ALLOWED_TAGS, attributes: ALLOWED_ATTRIBUTES + end + end +end diff --git a/actiontext/app/helpers/action_text/tag_helper.rb b/actiontext/app/helpers/action_text/tag_helper.rb new file mode 100644 index 0000000000..8434f2c611 --- /dev/null +++ b/actiontext/app/helpers/action_text/tag_helper.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module ActionText + module TagHelper + cattr_accessor(:id, instance_accessor: false) { 0 } + + # Returns a +trix-editor+ tag that instantiates the Trix JavaScript editor as well as a hidden field + # that Trix will write to on changes, so the content will be sent on form submissions. + # + # ==== Options + # * <tt>:class</tt> - Defaults to "trix-content" which ensures default styling is applied. + # + # ==== Example + # + # rich_text_area_tag "content", message.content + # # <input type="hidden" name="content" id="trix_input_post_1"> + # # <trix-editor id="content" input="trix_input_post_1" class="trix-content" ...></trix-editor> + def rich_text_area_tag(name, value = nil, options = {}) + options = options.symbolize_keys + + options[:input] ||= "trix_input_#{ActionText::TagHelper.id += 1}" + options[:class] ||= "trix-content" + + options[:data] ||= {} + options[:data][:direct_upload_url] = main_app.rails_direct_uploads_url + options[:data][:blob_url_template] = main_app.rails_service_blob_url(":signed_id", ":filename") + + editor_tag = content_tag("trix-editor", "", options) + input_tag = hidden_field_tag(name, value, id: options[:input]) + + input_tag + editor_tag + end + end +end + +module ActionView::Helpers + class Tags::ActionText < Tags::Base + delegate :dom_id, to: ActionView::RecordIdentifier + + def render + options = @options.stringify_keys + add_default_name_and_id(options) + options["input"] ||= dom_id(object, [options["id"], :trix_input].compact.join("_")) if object + @template_object.rich_text_area_tag(options.delete("name"), editable_value, options) + end + + def editable_value + value&.body.try(:to_trix_html) + end + end + + module FormHelper + # Returns a +trix-editor+ tag that instantiates the Trix JavaScript editor as well as a hidden field + # that Trix will write to on changes, so the content will be sent on form submissions. + # + # ==== Options + # * <tt>:class</tt> - Defaults to "trix-content" which ensures default styling is applied. + # + # ==== Example + # form_with(model: @message) do |form| + # form.rich_text_area :content + # end + # # <input type="hidden" name="message[content]" id="message_content_trix_input_message_1"> + # # <trix-editor id="content" input="message_content_trix_input_message_1" class="trix-content" ...></trix-editor> + def rich_text_area(object_name, method, options = {}) + Tags::ActionText.new(object_name, method, self, options).render + end + end + + class FormBuilder + def rich_text_area(method, options = {}) + @template.rich_text_area(@object_name, method, objectify_options(options)) + end + end +end diff --git a/actiontext/app/javascript/actiontext/attachment_upload.js b/actiontext/app/javascript/actiontext/attachment_upload.js new file mode 100644 index 0000000000..77fbc97df6 --- /dev/null +++ b/actiontext/app/javascript/actiontext/attachment_upload.js @@ -0,0 +1,45 @@ +import { DirectUpload } from "@rails/activestorage" + +export class AttachmentUpload { + constructor(attachment, element) { + this.attachment = attachment + this.element = element + this.directUpload = new DirectUpload(attachment.file, this.directUploadUrl, this) + } + + start() { + this.directUpload.create(this.directUploadDidComplete.bind(this)) + } + + directUploadWillStoreFileWithXHR(xhr) { + xhr.upload.addEventListener("progress", event => { + const progress = event.loaded / event.total * 100 + this.attachment.setUploadProgress(progress) + }) + } + + directUploadDidComplete(error, attributes) { + if (error) { + throw new Error(`Direct upload failed: ${error}`) + } + + this.attachment.setAttributes({ + sgid: attributes.attachable_sgid, + url: this.createBlobUrl(attributes.signed_id, attributes.filename) + }) + } + + createBlobUrl(signedId, filename) { + return this.blobUrlTemplate + .replace(":signed_id", signedId) + .replace(":filename", encodeURIComponent(filename)) + } + + get directUploadUrl() { + return this.element.dataset.directUploadUrl + } + + get blobUrlTemplate() { + return this.element.dataset.blobUrlTemplate + } +} diff --git a/actiontext/app/javascript/actiontext/index.js b/actiontext/app/javascript/actiontext/index.js new file mode 100644 index 0000000000..0e9251018a --- /dev/null +++ b/actiontext/app/javascript/actiontext/index.js @@ -0,0 +1,10 @@ +import { AttachmentUpload } from "./attachment_upload" + +addEventListener("trix-attachment-add", event => { + const { attachment, target } = event + + if (attachment.file) { + const upload = new AttachmentUpload(attachment, target) + upload.start() + } +}) diff --git a/actiontext/app/models/action_text/rich_text.rb b/actiontext/app/models/action_text/rich_text.rb new file mode 100644 index 0000000000..19fa3e030e --- /dev/null +++ b/actiontext/app/models/action_text/rich_text.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module ActionText + # The RichText record holds the content produced by the Trix editor in a serialized +body+ attribute. + # It also holds all the references to the embedded files, which are stored using Active Storage. + # This record is then associated with the Active Record model the application desires to have + # rich text content using the +has_rich_text+ class method. + class RichText < ActiveRecord::Base + self.table_name = "action_text_rich_texts" + + serialize :body, ActionText::Content + delegate :to_s, :nil?, to: :body + + belongs_to :record, polymorphic: true, touch: true + has_many_attached :embeds + + before_save do + self.embeds = body.attachables.grep(ActiveStorage::Blob) if body.present? + end + + def to_plain_text + body&.to_plain_text.to_s + end + + delegate :blank?, :empty?, :present?, to: :to_plain_text + end +end + +ActiveSupport.run_load_hooks :action_text_rich_text, ActionText::RichText diff --git a/actiontext/app/views/action_text/attachables/_missing_attachable.html.erb b/actiontext/app/views/action_text/attachables/_missing_attachable.html.erb new file mode 100644 index 0000000000..5ffd93b89e --- /dev/null +++ b/actiontext/app/views/action_text/attachables/_missing_attachable.html.erb @@ -0,0 +1 @@ +<%= "☒" -%> diff --git a/actiontext/app/views/action_text/attachables/_remote_image.html.erb b/actiontext/app/views/action_text/attachables/_remote_image.html.erb new file mode 100644 index 0000000000..3372f8d940 --- /dev/null +++ b/actiontext/app/views/action_text/attachables/_remote_image.html.erb @@ -0,0 +1,8 @@ +<figure class="attachment attachment--preview"> + <%= image_tag(remote_image.url, width: remote_image.width, height: remote_image.height) %> + <% if caption = remote_image.try(:caption) %> + <figcaption class="attachment__caption"> + <%= caption %> + </figcaption> + <% end %> +</figure> diff --git a/actiontext/app/views/action_text/attachment_galleries/_attachment_gallery.html.erb b/actiontext/app/views/action_text/attachment_galleries/_attachment_gallery.html.erb new file mode 100644 index 0000000000..6bc8674dc5 --- /dev/null +++ b/actiontext/app/views/action_text/attachment_galleries/_attachment_gallery.html.erb @@ -0,0 +1,3 @@ +<div class="attachment-gallery attachment-gallery--<%= attachment_gallery.size %>"> + <%= yield %> +</div> diff --git a/actiontext/app/views/action_text/content/_layout.html.erb b/actiontext/app/views/action_text/content/_layout.html.erb new file mode 100644 index 0000000000..55cb708ac4 --- /dev/null +++ b/actiontext/app/views/action_text/content/_layout.html.erb @@ -0,0 +1,3 @@ +<div class="trix-content"> + <%= render_action_text_content(content) %> +</div> diff --git a/actiontext/app/views/active_storage/blobs/_blob.html.erb b/actiontext/app/views/active_storage/blobs/_blob.html.erb new file mode 100644 index 0000000000..49ba357dd1 --- /dev/null +++ b/actiontext/app/views/active_storage/blobs/_blob.html.erb @@ -0,0 +1,14 @@ +<figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> attachment--<%= blob.filename.extension %>"> + <% if blob.representable? %> + <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %> + <% end %> + + <figcaption class="attachment__caption"> + <% if caption = blob.try(:caption) %> + <%= caption %> + <% else %> + <span class="attachment__name"><%= blob.filename %></span> + <span class="attachment__size"><%= number_to_human_size blob.byte_size %></span> + <% end %> + </figcaption> +</figure> diff --git a/actiontext/bin/test b/actiontext/bin/test new file mode 100755 index 0000000000..c53377cc97 --- /dev/null +++ b/actiontext/bin/test @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +COMPONENT_ROOT = File.expand_path("..", __dir__) +require_relative "../../tools/test" diff --git a/actiontext/bin/webpack b/actiontext/bin/webpack new file mode 100755 index 0000000000..ac2adfb6b9 --- /dev/null +++ b/actiontext/bin/webpack @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'webpack' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", + Pathname.new(__FILE__).realpath) + +bundle_binstub = File.expand_path("../bundle", __FILE__) + +if File.file?(bundle_binstub) + if /This file was generated by Bundler/.match?(File.read(bundle_binstub, 150)) + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("webpacker", "webpack") diff --git a/actiontext/bin/webpack-dev-server b/actiontext/bin/webpack-dev-server new file mode 100755 index 0000000000..c1ac52d675 --- /dev/null +++ b/actiontext/bin/webpack-dev-server @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'webpack-dev-server' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", + Pathname.new(__FILE__).realpath) + +bundle_binstub = File.expand_path("../bundle", __FILE__) + +if File.file?(bundle_binstub) + if /This file was generated by Bundler/.match?(File.read(bundle_binstub, 150)) + load(bundle_binstub) + else + abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. +Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") + end +end + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("webpacker", "webpack-dev-server") diff --git a/actiontext/db/migrate/20180528164100_create_action_text_tables.rb b/actiontext/db/migrate/20180528164100_create_action_text_tables.rb new file mode 100644 index 0000000000..e7c66ea6ae --- /dev/null +++ b/actiontext/db/migrate/20180528164100_create_action_text_tables.rb @@ -0,0 +1,13 @@ +class CreateActionTextTables < ActiveRecord::Migration[6.0] + def change + create_table :action_text_rich_texts do |t| + t.string :name, null: false + t.text :body, size: :long + t.references :record, null: false, polymorphic: true, index: false + + t.timestamps + + t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true + end + end +end diff --git a/actiontext/lib/action_text.rb b/actiontext/lib/action_text.rb new file mode 100644 index 0000000000..0dd6318f89 --- /dev/null +++ b/actiontext/lib/action_text.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "active_support" +require "active_support/rails" + +require "nokogiri" + +module ActionText + extend ActiveSupport::Autoload + + autoload :Attachable + autoload :AttachmentGallery + autoload :Attachment + autoload :Attribute + autoload :Content + autoload :Fragment + autoload :HtmlConversion + autoload :PlainTextConversion + autoload :Serialization + autoload :TrixAttachment + + module Attachables + extend ActiveSupport::Autoload + + autoload :ContentAttachment + autoload :MissingAttachable + autoload :RemoteImage + end + + module Attachments + extend ActiveSupport::Autoload + + autoload :Caching + autoload :Minification + autoload :TrixConversion + end +end diff --git a/actiontext/lib/action_text/attachable.rb b/actiontext/lib/action_text/attachable.rb new file mode 100644 index 0000000000..38cd24aa8d --- /dev/null +++ b/actiontext/lib/action_text/attachable.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +module ActionText + module Attachable + extend ActiveSupport::Concern + + LOCATOR_NAME = "attachable" + + class << self + def from_node(node) + if attachable = attachable_from_sgid(node["sgid"]) + attachable + elsif attachable = ActionText::Attachables::ContentAttachment.from_node(node) + attachable + elsif attachable = ActionText::Attachables::RemoteImage.from_node(node) + attachable + else + ActionText::Attachables::MissingAttachable + end + end + + def from_attachable_sgid(sgid, options = {}) + method = sgid.is_a?(Array) ? :locate_many_signed : :locate_signed + record = GlobalID::Locator.public_send(method, sgid, options.merge(for: LOCATOR_NAME)) + record || raise(ActiveRecord::RecordNotFound) + end + + private + def attachable_from_sgid(sgid) + from_attachable_sgid(sgid) + rescue ActiveRecord::RecordNotFound + nil + end + end + + class_methods do + def from_attachable_sgid(sgid) + ActionText::Attachable.from_attachable_sgid(sgid, only: self) + end + end + + def attachable_sgid + to_sgid(expires_in: nil, for: LOCATOR_NAME).to_s + end + + def attachable_content_type + try(:content_type) || "application/octet-stream" + end + + def attachable_filename + filename.to_s if respond_to?(:filename) + end + + def attachable_filesize + try(:byte_size) || try(:filesize) + end + + def attachable_metadata + try(:metadata) || {} + end + + def previewable_attachable? + false + end + + def as_json(*) + super.merge(attachable_sgid: attachable_sgid) + end + + def to_rich_text_attributes(attributes = {}) + attributes.dup.tap do |attrs| + attrs[:sgid] = attachable_sgid + attrs[:content_type] = attachable_content_type + attrs[:previewable] = true if previewable_attachable? + attrs[:filename] = attachable_filename + attrs[:filesize] = attachable_filesize + attrs[:width] = attachable_metadata[:width] + attrs[:height] = attachable_metadata[:height] + end.compact + end + end +end diff --git a/actiontext/lib/action_text/attachables/content_attachment.rb b/actiontext/lib/action_text/attachables/content_attachment.rb new file mode 100644 index 0000000000..804f74713f --- /dev/null +++ b/actiontext/lib/action_text/attachables/content_attachment.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module ActionText + module Attachables + class ContentAttachment + include ActiveModel::Model + + def self.from_node(node) + if node["content-type"] + if matches = node["content-type"].match(/vnd\.rubyonrails\.(.+)\.html/) + attachment = new(name: matches[1]) + attachment if attachment.valid? + end + end + end + + attr_accessor :name + validates_inclusion_of :name, in: %w( horizontal-rule ) + + def attachable_plain_text_representation(caption) + case name + when "horizontal-rule" + " ┄ " + else + " " + end + end + + def to_partial_path + "action_text/attachables/content_attachment" + end + + def to_trix_content_attachment_partial_path + "action_text/attachables/content_attachments/#{name.underscore}" + end + end + end +end diff --git a/actiontext/lib/action_text/attachables/missing_attachable.rb b/actiontext/lib/action_text/attachables/missing_attachable.rb new file mode 100644 index 0000000000..2f3bd40563 --- /dev/null +++ b/actiontext/lib/action_text/attachables/missing_attachable.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module ActionText + module Attachables + module MissingAttachable + extend ActiveModel::Naming + + def self.to_partial_path + "action_text/attachables/missing_attachable" + end + end + end +end diff --git a/actiontext/lib/action_text/attachables/remote_image.rb b/actiontext/lib/action_text/attachables/remote_image.rb new file mode 100644 index 0000000000..650b11862b --- /dev/null +++ b/actiontext/lib/action_text/attachables/remote_image.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module ActionText + module Attachables + class RemoteImage + extend ActiveModel::Naming + + class << self + def from_node(node) + if node["url"] && content_type_is_image?(node["content-type"]) + new(attributes_from_node(node)) + end + end + + private + def content_type_is_image?(content_type) + content_type.to_s =~ /^image(\/.+|$)/ + end + + def attributes_from_node(node) + { url: node["url"], + content_type: node["content-type"], + width: node["width"], + height: node["height"] } + end + end + + attr_reader :url, :content_type, :width, :height + + def initialize(attributes = {}) + @url = attributes[:url] + @content_type = attributes[:content_type] + @width = attributes[:width] + @height = attributes[:height] + end + + def attachable_plain_text_representation(caption) + "[#{caption || "Image"}]" + end + + def to_partial_path + "action_text/attachables/remote_image" + end + end + end +end diff --git a/actiontext/lib/action_text/attachment.rb b/actiontext/lib/action_text/attachment.rb new file mode 100644 index 0000000000..e90a3e7d48 --- /dev/null +++ b/actiontext/lib/action_text/attachment.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +module ActionText + class Attachment + include Attachments::TrixConversion, Attachments::Minification, Attachments::Caching + + TAG_NAME = "action-text-attachment" + SELECTOR = TAG_NAME + ATTRIBUTES = %w( sgid content-type url href filename filesize width height previewable presentation caption ) + + class << self + def fragment_by_canonicalizing_attachments(content) + fragment_by_minifying_attachments(fragment_by_converting_trix_attachments(content)) + end + + def from_node(node, attachable = nil) + new(node, attachable || ActionText::Attachable.from_node(node)) + end + + def from_attachables(attachables) + Array(attachables).map { |attachable| from_attachable(attachable) }.compact + end + + def from_attachable(attachable, attributes = {}) + if node = node_from_attributes(attachable.to_rich_text_attributes(attributes)) + new(node, attachable) + end + end + + def from_attributes(attributes, attachable = nil) + if node = node_from_attributes(attributes) + from_node(node, attachable) + end + end + + private + def node_from_attributes(attributes) + if attributes = process_attributes(attributes).presence + ActionText::HtmlConversion.create_element(TAG_NAME, attributes) + end + end + + def process_attributes(attributes) + attributes.transform_keys { |key| key.to_s.underscore.dasherize }.slice(*ATTRIBUTES) + end + end + + attr_reader :node, :attachable + + delegate :to_param, to: :attachable + delegate_missing_to :attachable + + def initialize(node, attachable) + @node = node + @attachable = attachable + end + + def caption + node_attributes["caption"].presence + end + + def full_attributes + node_attributes.merge(attachable_attributes).merge(sgid_attributes) + end + + def with_full_attributes + self.class.from_attributes(full_attributes, attachable) + end + + def to_plain_text + if respond_to?(:attachable_plain_text_representation) + attachable_plain_text_representation(caption) + else + caption.to_s + end + end + + def to_html + HtmlConversion.node_to_html(node) + end + + def to_s + to_html + end + + def inspect + "#<#{self.class.name} attachable=#{attachable.inspect}>" + end + + private + def node_attributes + @node_attributes ||= ATTRIBUTES.map { |name| [ name.underscore, node[name] ] }.to_h.compact + end + + def attachable_attributes + @attachable_attributes ||= (attachable.try(:to_rich_text_attributes) || {}).stringify_keys + end + + def sgid_attributes + @sgid_attributes ||= node_attributes.slice("sgid").presence || attachable_attributes.slice("sgid") + end + end +end diff --git a/actiontext/lib/action_text/attachment_gallery.rb b/actiontext/lib/action_text/attachment_gallery.rb new file mode 100644 index 0000000000..45afbff058 --- /dev/null +++ b/actiontext/lib/action_text/attachment_gallery.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module ActionText + class AttachmentGallery + include ActiveModel::Model + + class << self + def fragment_by_canonicalizing_attachment_galleries(content) + fragment_by_replacing_attachment_gallery_nodes(content) do |node| + "<#{TAG_NAME}>#{node.inner_html}</#{TAG_NAME}>" + end + end + + def fragment_by_replacing_attachment_gallery_nodes(content) + Fragment.wrap(content).update do |source| + find_attachment_gallery_nodes(source).each do |node| + node.replace(yield(node).to_s) + end + end + end + + def find_attachment_gallery_nodes(content) + Fragment.wrap(content).find_all(SELECTOR).select do |node| + node.children.all? do |child| + if child.text? + child.text =~ /\A(\n|\ )*\z/ + else + child.matches? ATTACHMENT_SELECTOR + end + end + end + end + + def from_node(node) + new(node) + end + end + + attr_reader :node + + def initialize(node) + @node = node + end + + def attachments + @attachments ||= node.css(ATTACHMENT_SELECTOR).map do |node| + ActionText::Attachment.from_node(node).with_full_attributes + end + end + + def size + attachments.size + end + + def inspect + "#<#{self.class.name} size=#{size.inspect}>" + end + + TAG_NAME = "div" + ATTACHMENT_SELECTOR = "#{ActionText::Attachment::SELECTOR}[presentation=gallery]" + SELECTOR = "#{TAG_NAME}:has(#{ATTACHMENT_SELECTOR} + #{ATTACHMENT_SELECTOR})" + + private_constant :TAG_NAME, :ATTACHMENT_SELECTOR, :SELECTOR + end +end diff --git a/actiontext/lib/action_text/attachments/caching.rb b/actiontext/lib/action_text/attachments/caching.rb new file mode 100644 index 0000000000..7c727bfc26 --- /dev/null +++ b/actiontext/lib/action_text/attachments/caching.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module ActionText + module Attachments + module Caching + def cache_key(*args) + [self.class.name, cache_digest, *attachable.cache_key(*args)].join("/") + end + + private + def cache_digest + Digest::SHA256.hexdigest(node.to_s) + end + end + end +end diff --git a/actiontext/lib/action_text/attachments/minification.rb b/actiontext/lib/action_text/attachments/minification.rb new file mode 100644 index 0000000000..edc8f876d6 --- /dev/null +++ b/actiontext/lib/action_text/attachments/minification.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module ActionText + module Attachments + module Minification + extend ActiveSupport::Concern + + class_methods do + def fragment_by_minifying_attachments(content) + Fragment.wrap(content).replace(ActionText::Attachment::SELECTOR) do |node| + node.tap { |n| n.inner_html = "" } + end + end + end + end + end +end diff --git a/actiontext/lib/action_text/attachments/trix_conversion.rb b/actiontext/lib/action_text/attachments/trix_conversion.rb new file mode 100644 index 0000000000..24937d6c22 --- /dev/null +++ b/actiontext/lib/action_text/attachments/trix_conversion.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module ActionText + module Attachments + module TrixConversion + extend ActiveSupport::Concern + + class_methods do + def fragment_by_converting_trix_attachments(content) + Fragment.wrap(content).replace(TrixAttachment::SELECTOR) do |node| + from_trix_attachment(TrixAttachment.new(node)) + end + end + + def from_trix_attachment(trix_attachment) + from_attributes(trix_attachment.attributes) + end + end + + def to_trix_attachment(content = trix_attachment_content) + attributes = full_attributes.dup + attributes["content"] = content if content + TrixAttachment.from_attributes(attributes) + end + + private + def trix_attachment_content + if partial_path = attachable.try(:to_trix_content_attachment_partial_path) + ActionText::Content.renderer.render(partial: partial_path, object: self, as: model_name.element) + end + end + end + end +end diff --git a/actiontext/lib/action_text/attribute.rb b/actiontext/lib/action_text/attribute.rb new file mode 100644 index 0000000000..f226dd21bd --- /dev/null +++ b/actiontext/lib/action_text/attribute.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module ActionText + module Attribute + extend ActiveSupport::Concern + + class_methods do + # Provides access to a dependent RichText model that holds the body and attachments for a single named rich text attribute. + # This dependent attribute is lazily instantiated and will be auto-saved when it's been changed. Example: + # + # class Message < ActiveRecord::Base + # has_rich_text :content + # end + # + # message = Message.create!(content: "<h1>Funny times!</h1>") + # message.content.to_s # => "<h1>Funny times!</h1>" + # message.content.to_plain_text # => "Funny times!" + # + # The dependent RichText model will also automatically process attachments links as sent via the Trix-powered editor. + # These attachments are associated with the RichText model using Active Storage. + # + # If you wish to preload the dependent RichText model, you can use the named scope: + # + # Message.all.with_rich_text_content # Avoids N+1 queries when you just want the body, not the attachments. + # Message.all.with_rich_text_content_and_embeds # Avoids N+1 queries when you just want the body and attachments. + def has_rich_text(name) + class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name} + self.rich_text_#{name} ||= ActionText::RichText.new(name: "#{name}", record: self) + end + + def #{name}=(body) + self.#{name}.body = body + end + CODE + + has_one :"rich_text_#{name}", -> { where(name: name) }, class_name: "ActionText::RichText", as: :record, inverse_of: :record, dependent: :destroy + + scope :"with_rich_text_#{name}", -> { includes("rich_text_#{name}") } + scope :"with_rich_text_#{name}_and_embeds", -> { includes("rich_text_#{name}": { embeds_attachments: :blob }) } + + after_save do + public_send(name).save if public_send(name).changed? + end + end + end + end +end diff --git a/actiontext/lib/action_text/content.rb b/actiontext/lib/action_text/content.rb new file mode 100644 index 0000000000..16bc6fe031 --- /dev/null +++ b/actiontext/lib/action_text/content.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true + +require "active_support/core_ext/module/attribute_accessors_per_thread" + +module ActionText + class Content + include Serialization + + thread_cattr_accessor :renderer + + attr_reader :fragment + + delegate :blank?, :empty?, :html_safe, :present?, to: :to_html # Delegating to to_html to avoid including the layout + + class << self + def fragment_by_canonicalizing_content(content) + fragment = ActionText::Attachment.fragment_by_canonicalizing_attachments(content) + fragment = ActionText::AttachmentGallery.fragment_by_canonicalizing_attachment_galleries(fragment) + fragment + end + end + + def initialize(content = nil, options = {}) + options.with_defaults! canonicalize: true + + if options[:canonicalize] + @fragment = self.class.fragment_by_canonicalizing_content(content) + else + @fragment = ActionText::Fragment.wrap(content) + end + end + + def links + @links ||= fragment.find_all("a[href]").map { |a| a["href"] }.uniq + end + + def attachments + @attachments ||= attachment_nodes.map do |node| + attachment_for_node(node) + end + end + + def attachment_galleries + @attachment_galleries ||= attachment_gallery_nodes.map do |node| + attachment_gallery_for_node(node) + end + end + + def gallery_attachments + @gallery_attachments ||= attachment_galleries.flat_map(&:attachments) + end + + def attachables + @attachables ||= attachment_nodes.map do |node| + ActionText::Attachable.from_node(node) + end + end + + def append_attachables(attachables) + attachments = ActionText::Attachment.from_attachables(attachables) + self.class.new([self.to_s.presence, *attachments].compact.join("\n")) + end + + def render_attachments(**options, &block) + content = fragment.replace(ActionText::Attachment::SELECTOR) do |node| + block.call(attachment_for_node(node, **options)) + end + self.class.new(content, canonicalize: false) + end + + def render_attachment_galleries(&block) + content = ActionText::AttachmentGallery.fragment_by_replacing_attachment_gallery_nodes(fragment) do |node| + block.call(attachment_gallery_for_node(node)) + end + self.class.new(content, canonicalize: false) + end + + def to_plain_text + render_attachments(with_full_attributes: false, &:to_plain_text).fragment.to_plain_text + end + + def to_trix_html + render_attachments(&:to_trix_attachment).to_html + end + + def to_html + fragment.to_html + end + + def to_rendered_html_with_layout + renderer.render(partial: "action_text/content/layout", locals: { content: self }) + end + + def to_s + to_rendered_html_with_layout + end + + def as_json(*) + to_html + end + + def inspect + "#<#{self.class.name} #{to_s.truncate(25).inspect}>" + end + + def ==(other) + if other.is_a?(self.class) + to_s == other.to_s + end + end + + private + def attachment_nodes + @attachment_nodes ||= fragment.find_all(ActionText::Attachment::SELECTOR) + end + + def attachment_gallery_nodes + @attachment_gallery_nodes ||= ActionText::AttachmentGallery.find_attachment_gallery_nodes(fragment) + end + + def attachment_for_node(node, with_full_attributes: true) + attachment = ActionText::Attachment.from_node(node) + with_full_attributes ? attachment.with_full_attributes : attachment + end + + def attachment_gallery_for_node(node) + ActionText::AttachmentGallery.from_node(node) + end + end +end + +ActiveSupport.run_load_hooks :action_text_content, ActionText::Content diff --git a/actiontext/lib/action_text/engine.rb b/actiontext/lib/action_text/engine.rb new file mode 100644 index 0000000000..51ff5b575b --- /dev/null +++ b/actiontext/lib/action_text/engine.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require "rails" +require "action_controller/railtie" +require "active_record/railtie" +require "active_storage/engine" + +require "action_text" + +module ActionText + class Engine < Rails::Engine + isolate_namespace ActionText + config.eager_load_namespaces << ActionText + + initializer "action_text.attribute" do + ActiveSupport.on_load(:active_record) do + include ActionText::Attribute + end + end + + initializer "action_text.attachable" do + ActiveSupport.on_load(:active_storage_blob) do + include ActionText::Attachable + + def previewable_attachable? + representable? + end + end + end + + initializer "action_text.helper" do + ActiveSupport.on_load(:action_controller_base) do + helper ActionText::Engine.helpers + end + end + + initializer "action_text.renderer" do |app| + app.executor.to_run { ActionText::Content.renderer = ApplicationController.renderer } + app.executor.to_complete { ActionText::Content.renderer = ApplicationController.renderer } + + ActiveSupport.on_load(:action_text_content) do + self.renderer = ApplicationController.renderer + end + + ActiveSupport.on_load(:action_controller_base) do + before_action { ActionText::Content.renderer = ApplicationController.renderer.new(request.env) } + end + end + end +end diff --git a/actiontext/lib/action_text/fragment.rb b/actiontext/lib/action_text/fragment.rb new file mode 100644 index 0000000000..af276b2b26 --- /dev/null +++ b/actiontext/lib/action_text/fragment.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module ActionText + class Fragment + class << self + def wrap(fragment_or_html) + case fragment_or_html + when self + fragment_or_html + when Nokogiri::HTML::DocumentFragment + new(fragment_or_html) + else + from_html(fragment_or_html) + end + end + + def from_html(html) + new(ActionText::HtmlConversion.fragment_for_html(html.to_s.strip)) + end + end + + attr_reader :source + + def initialize(source) + @source = source + end + + def find_all(selector) + source.css(selector) + end + + def update + yield source = self.source.clone + self.class.new(source) + end + + def replace(selector) + update do |source| + source.css(selector).each do |node| + node.replace(yield(node).to_s) + end + end + end + + def to_plain_text + @plain_text ||= PlainTextConversion.node_to_plain_text(source) + end + + def to_html + @html ||= HtmlConversion.node_to_html(source) + end + + def to_s + to_html + end + end +end diff --git a/actiontext/lib/action_text/gem_version.rb b/actiontext/lib/action_text/gem_version.rb new file mode 100644 index 0000000000..5404ac54e9 --- /dev/null +++ b/actiontext/lib/action_text/gem_version.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module ActionText + # Returns the currently-loaded version of Action Text as a <tt>Gem::Version</tt>. + def self.gem_version + Gem::Version.new VERSION::STRING + end + + module VERSION + MAJOR = 6 + MINOR = 0 + TINY = 0 + PRE = "beta1" + + STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") + end +end diff --git a/actiontext/lib/action_text/html_conversion.rb b/actiontext/lib/action_text/html_conversion.rb new file mode 100644 index 0000000000..1e1062ea3f --- /dev/null +++ b/actiontext/lib/action_text/html_conversion.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module ActionText + module HtmlConversion + extend self + + def node_to_html(node) + node.to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_HTML) + end + + def fragment_for_html(html) + document.fragment(html) + end + + def create_element(tag_name, attributes = {}) + document.create_element(tag_name, attributes) + end + + private + def document + Nokogiri::HTML::Document.new.tap { |doc| doc.encoding = "UTF-8" } + end + end +end diff --git a/actiontext/lib/action_text/plain_text_conversion.rb b/actiontext/lib/action_text/plain_text_conversion.rb new file mode 100644 index 0000000000..0eb4e2e7da --- /dev/null +++ b/actiontext/lib/action_text/plain_text_conversion.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module ActionText + module PlainTextConversion + extend self + + def node_to_plain_text(node) + remove_trailing_newlines(plain_text_for_node(node)) + end + + private + def plain_text_for_node(node, index = 0) + if respond_to?(plain_text_method_for_node(node), true) + send(plain_text_method_for_node(node), node, index) + else + plain_text_for_node_children(node) + end + end + + def plain_text_for_node_children(node) + node.children.each_with_index.map do |child, index| + plain_text_for_node(child, index) + end.compact.join("") + end + + def plain_text_method_for_node(node) + :"plain_text_for_#{node.name}_node" + end + + def plain_text_for_block(node, index = 0) + "#{remove_trailing_newlines(plain_text_for_node_children(node))}\n\n" + end + + %i[ h1 p ul ol ].each do |element| + alias_method :"plain_text_for_#{element}_node", :plain_text_for_block + end + + def plain_text_for_br_node(node, index) + "\n" + end + + def plain_text_for_text_node(node, index) + remove_trailing_newlines(node.text) + end + + def plain_text_for_div_node(node, index) + "#{remove_trailing_newlines(plain_text_for_node_children(node))}\n" + end + + def plain_text_for_figcaption_node(node, index) + "[#{remove_trailing_newlines(plain_text_for_node_children(node))}]" + end + + def plain_text_for_blockquote_node(node, index) + text = plain_text_for_block(node) + text.sub(/\A(\s*)(.+?)(\s*)\Z/m, '\1“\2”\3') + end + + def plain_text_for_li_node(node, index) + bullet = bullet_for_li_node(node, index) + text = remove_trailing_newlines(plain_text_for_node_children(node)) + "#{bullet} #{text}\n" + end + + def remove_trailing_newlines(text) + text.chomp("") + end + + def bullet_for_li_node(node, index) + if list_node_name_for_li_node(node) == "ol" + "#{index + 1}." + else + "•" + end + end + + def list_node_name_for_li_node(node) + node.ancestors.lazy.map(&:name).grep(/^[uo]l$/).first + end + end +end diff --git a/actiontext/lib/action_text/serialization.rb b/actiontext/lib/action_text/serialization.rb new file mode 100644 index 0000000000..8ecf8c9157 --- /dev/null +++ b/actiontext/lib/action_text/serialization.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module ActionText + module Serialization + extend ActiveSupport::Concern + + class_methods do + def load(content) + new(content) if content + end + + def dump(content) + case content + when nil + nil + when self + content.to_html + else + new(content).to_html + end + end + end + + # Marshal compatibility + + class_methods do + alias_method :_load, :load + end + + def _dump(*) + self.class.dump(self) + end + end +end diff --git a/actiontext/lib/action_text/trix_attachment.rb b/actiontext/lib/action_text/trix_attachment.rb new file mode 100644 index 0000000000..c16c1c090d --- /dev/null +++ b/actiontext/lib/action_text/trix_attachment.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +module ActionText + class TrixAttachment + TAG_NAME = "figure" + SELECTOR = "[data-trix-attachment]" + + COMPOSED_ATTRIBUTES = %w( caption presentation ) + ATTRIBUTES = %w( sgid contentType url href filename filesize width height previewable content ) + COMPOSED_ATTRIBUTES + ATTRIBUTE_TYPES = { + "previewable" => ->(value) { value.to_s == "true" }, + "filesize" => ->(value) { Integer(value.to_s) rescue value }, + "width" => ->(value) { Integer(value.to_s) rescue nil }, + "height" => ->(value) { Integer(value.to_s) rescue nil }, + :default => ->(value) { value.to_s } + } + + class << self + def from_attributes(attributes) + attributes = process_attributes(attributes) + + trix_attachment_attributes = attributes.except(*COMPOSED_ATTRIBUTES) + trix_attributes = attributes.slice(*COMPOSED_ATTRIBUTES) + + node = ActionText::HtmlConversion.create_element(TAG_NAME) + node["data-trix-attachment"] = JSON.generate(trix_attachment_attributes) + node["data-trix-attributes"] = JSON.generate(trix_attributes) if trix_attributes.any? + + new(node) + end + + private + def process_attributes(attributes) + typecast_attribute_values(transform_attribute_keys(attributes)) + end + + def transform_attribute_keys(attributes) + attributes.transform_keys { |key| key.to_s.underscore.camelize(:lower) } + end + + def typecast_attribute_values(attributes) + attributes.map do |key, value| + typecast = ATTRIBUTE_TYPES[key] || ATTRIBUTE_TYPES[:default] + [key, typecast.call(value)] + end.to_h + end + end + + attr_reader :node + + def initialize(node) + @node = node + end + + def attributes + @attributes ||= attachment_attributes.merge(composed_attributes).slice(*ATTRIBUTES) + end + + def to_html + ActionText::HtmlConversion.node_to_html(node) + end + + def to_s + to_html + end + + private + def attachment_attributes + read_json_object_attribute("data-trix-attachment") + end + + def composed_attributes + read_json_object_attribute("data-trix-attributes") + end + + def read_json_object_attribute(name) + read_json_attribute(name) || {} + end + + def read_json_attribute(name) + if value = node[name] + begin + JSON.parse(value) + rescue => e + Rails.logger.error "[#{self.class.name}] Couldn't parse JSON #{value} from NODE #{node.inspect}" + Rails.logger.error "[#{self.class.name}] Failed with #{e.class}: #{e.backtrace}" + nil + end + end + end + end +end diff --git a/actiontext/lib/action_text/version.rb b/actiontext/lib/action_text/version.rb new file mode 100644 index 0000000000..ed72859fa4 --- /dev/null +++ b/actiontext/lib/action_text/version.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require_relative "gem_version" + +module ActionText + # Returns the currently-loaded version of Action Text as a <tt>Gem::Version</tt>. + def self.version + gem_version + end +end diff --git a/actiontext/lib/tasks/actiontext.rake b/actiontext/lib/tasks/actiontext.rake new file mode 100644 index 0000000000..4f90e4930c --- /dev/null +++ b/actiontext/lib/tasks/actiontext.rake @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +namespace :action_text do + # Prevent migration installation task from showing up twice. + Rake::Task["install:migrations"].clear_comments + + desc "Copy over the migration, stylesheet, and JavaScript files" + task install: %w( environment run_installer copy_migrations ) + + task :run_installer do + installer_template = File.expand_path("../templates/installer.rb", __dir__) + system "#{RbConfig.ruby} ./bin/rails app:template LOCATION=#{installer_template}" + end + + task :copy_migrations do + Rake::Task["active_storage:install:migrations"].invoke + Rake::Task["railties:install:migrations"].reenable # Otherwise you can't run 2 migration copy tasks in one invocation + Rake::Task["action_text:install:migrations"].invoke + end +end diff --git a/actiontext/lib/templates/actiontext.scss b/actiontext/lib/templates/actiontext.scss new file mode 100644 index 0000000000..7cb26e74ac --- /dev/null +++ b/actiontext/lib/templates/actiontext.scss @@ -0,0 +1,36 @@ +// +// Provides a drop-in pointer for the default Trix stylesheet that will format the toolbar and +// the trix-editor content (whether displayed or under editing). Feel free to incorporate this +// inclusion directly in any other asset bundle and remove this file. +// +//= require trix/dist/trix + +// We need to override trix.css’s image gallery styles to accommodate the +// <action-text-attachment> element we wrap around attachments. Otherwise, +// images in galleries will be squished by the max-width: 33%; rule. +.trix-content { + .attachment-gallery { + > action-text-attachment, + > .attachment { + flex: 1 0 33%; + padding: 0 0.5em; + max-width: 33%; + } + + &.attachment-gallery--2, + &.attachment-gallery--4 { + > action-text-attachment, + > .attachment { + flex-basis: 50%; + max-width: 50%; + } + } + } + + action-text-attachment { + .attachment { + padding: 0 !important; + max-width: 100% !important; + } + } +} diff --git a/actiontext/lib/templates/fixtures.yml b/actiontext/lib/templates/fixtures.yml new file mode 100644 index 0000000000..8b371ea604 --- /dev/null +++ b/actiontext/lib/templates/fixtures.yml @@ -0,0 +1,4 @@ +# one: +# record: name_of_fixture (ClassOfFixture) +# name: content +# body: <p>In a <i>million</i> stars!</p> diff --git a/actiontext/lib/templates/installer.rb b/actiontext/lib/templates/installer.rb new file mode 100644 index 0000000000..a8000eb9fc --- /dev/null +++ b/actiontext/lib/templates/installer.rb @@ -0,0 +1,32 @@ +require "pathname" +require "json" + +APPLICATION_PACK_PATH = Pathname.new("app/javascript/packs/application.js") +JS_PACKAGE_PATH = Pathname.new("#{__dir__}/../../package.json") + +JS_PACKAGE = JSON.load(JS_PACKAGE_PATH) +JS_DEPENDENCIES = JS_PACKAGE["peerDependencies"].dup.merge \ + JS_PACKAGE["name"] => "^#{JS_PACKAGE["version"]}" + +say "Copying actiontext.scss to app/assets/stylesheets" +copy_file "#{__dir__}/actiontext.scss", "app/assets/stylesheets/actiontext.scss" + +say "Copying fixtures to test/fixtures/action_text/rich_texts.yml" +copy_file "#{__dir__}/fixtures.yml", "test/fixtures/action_text/rich_texts.yml" + +say "Copying blob rendering partial to app/views/active_storage/blobs/_blob.html.erb" +copy_file "#{__dir__}/../../app/views/active_storage/blobs/_blob.html.erb", + "app/views/active_storage/blobs/_blob.html.erb" + +say "Installing JavaScript dependencies" +run "yarn add #{JS_DEPENDENCIES.map { |name, version| "#{name}@#{version}" }.join(" ")}" + +if APPLICATION_PACK_PATH.exist? + JS_DEPENDENCIES.keys.each do |name| + line = %[require("#{name}")] + unless APPLICATION_PACK_PATH.read.include? line + say "Adding #{name} to #{APPLICATION_PACK_PATH}" + append_to_file APPLICATION_PACK_PATH, "\n#{line}" + end + end +end diff --git a/actiontext/package.json b/actiontext/package.json new file mode 100644 index 0000000000..a24026bc51 --- /dev/null +++ b/actiontext/package.json @@ -0,0 +1,29 @@ +{ + "name": "@rails/actiontext", + "version": "6.0.0-beta1", + "description": "Edit and display rich text in Rails applications", + "main": "app/javascript/actiontext/index.js", + "files": [ + "app/javascript/actiontext/*.js" + ], + "homepage": "http://rubyonrails.org/", + "repository": { + "type": "git", + "url": "git+https://github.com/rails/rails.git" + }, + "bugs": { + "url": "https://github.com/rails/rails/issues" + }, + "author": "Basecamp, LLC", + "contributors": [ + "Javan Makhmali <javan@javan.us>", + "Sam Stephenson <sstephenson@gmail.com>" + ], + "license": "MIT", + "dependencies": { + "@rails/activestorage": "^6.0.0-alpha" + }, + "peerDependencies": { + "trix": "^1.0.0" + } +} diff --git a/actiontext/test/dummy/.babelrc b/actiontext/test/dummy/.babelrc new file mode 100644 index 0000000000..ded31c0d80 --- /dev/null +++ b/actiontext/test/dummy/.babelrc @@ -0,0 +1,18 @@ +{ + "presets": [ + ["env", { + "modules": false, + "targets": { + "browsers": "> 1%", + "uglify": true + }, + "useBuiltIns": true + }] + ], + + "plugins": [ + "syntax-dynamic-import", + "transform-object-rest-spread", + ["transform-class-properties", { "spec": true }] + ] +} diff --git a/actiontext/test/dummy/.postcssrc.yml b/actiontext/test/dummy/.postcssrc.yml new file mode 100644 index 0000000000..150dac3c6c --- /dev/null +++ b/actiontext/test/dummy/.postcssrc.yml @@ -0,0 +1,3 @@ +plugins: + postcss-import: {} + postcss-cssnext: {} diff --git a/actiontext/test/dummy/Rakefile b/actiontext/test/dummy/Rakefile new file mode 100644 index 0000000000..e85f913914 --- /dev/null +++ b/actiontext/test/dummy/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative 'config/application' + +Rails.application.load_tasks diff --git a/actiontext/test/dummy/app/assets/config/manifest.js b/actiontext/test/dummy/app/assets/config/manifest.js new file mode 100644 index 0000000000..b16e53d6d5 --- /dev/null +++ b/actiontext/test/dummy/app/assets/config/manifest.js @@ -0,0 +1,3 @@ +//= link_tree ../images +//= link_directory ../javascripts .js +//= link_directory ../stylesheets .css diff --git a/actiontext/test/dummy/app/assets/images/.keep b/actiontext/test/dummy/app/assets/images/.keep new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actiontext/test/dummy/app/assets/images/.keep diff --git a/actiontext/test/dummy/app/assets/stylesheets/application.css b/actiontext/test/dummy/app/assets/stylesheets/application.css new file mode 100644 index 0000000000..e4b10eb997 --- /dev/null +++ b/actiontext/test/dummy/app/assets/stylesheets/application.css @@ -0,0 +1,16 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, + * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the bottom of the + * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. + * + *= require trix/dist/trix.css + *= require_tree . + *= require_self + */ diff --git a/actiontext/test/dummy/app/assets/stylesheets/messages.css b/actiontext/test/dummy/app/assets/stylesheets/messages.css new file mode 100644 index 0000000000..afad32db02 --- /dev/null +++ b/actiontext/test/dummy/app/assets/stylesheets/messages.css @@ -0,0 +1,4 @@ +/* + Place all the styles related to the matching controller here. + They will automatically be included in application.css. +*/ diff --git a/actiontext/test/dummy/app/assets/stylesheets/scaffold.css b/actiontext/test/dummy/app/assets/stylesheets/scaffold.css new file mode 100644 index 0000000000..cd4f3de38d --- /dev/null +++ b/actiontext/test/dummy/app/assets/stylesheets/scaffold.css @@ -0,0 +1,80 @@ +body { + background-color: #fff; + color: #333; + margin: 33px; +} + +body, p, ol, ul, td { + font-family: verdana, arial, helvetica, sans-serif; + font-size: 13px; + line-height: 18px; +} + +pre { + background-color: #eee; + padding: 10px; + font-size: 11px; +} + +a { + color: #000; +} + +a:visited { + color: #666; +} + +a:hover { + color: #fff; + background-color: #000; +} + +th { + padding-bottom: 5px; +} + +td { + padding: 0 5px 7px; +} + +div.field, +div.actions { + margin-bottom: 10px; +} + +#notice { + color: green; +} + +.field_with_errors { + padding: 2px; + background-color: red; + display: table; +} + +#error_explanation { + width: 450px; + border: 2px solid red; + padding: 7px 7px 0; + margin-bottom: 20px; + background-color: #f0f0f0; +} + +#error_explanation h2 { + text-align: left; + font-weight: bold; + padding: 5px 5px 5px 15px; + font-size: 12px; + margin: -7px -7px 0; + background-color: #c00; + color: #fff; +} + +#error_explanation ul li { + font-size: 12px; + list-style: square; +} + +label { + display: block; +} diff --git a/actiontext/test/dummy/app/channels/application_cable/channel.rb b/actiontext/test/dummy/app/channels/application_cable/channel.rb new file mode 100644 index 0000000000..d672697283 --- /dev/null +++ b/actiontext/test/dummy/app/channels/application_cable/channel.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/actiontext/test/dummy/app/channels/application_cable/connection.rb b/actiontext/test/dummy/app/channels/application_cable/connection.rb new file mode 100644 index 0000000000..0ff5442f47 --- /dev/null +++ b/actiontext/test/dummy/app/channels/application_cable/connection.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/actiontext/test/dummy/app/controllers/application_controller.rb b/actiontext/test/dummy/app/controllers/application_controller.rb new file mode 100644 index 0000000000..09705d12ab --- /dev/null +++ b/actiontext/test/dummy/app/controllers/application_controller.rb @@ -0,0 +1,2 @@ +class ApplicationController < ActionController::Base +end diff --git a/actiontext/test/dummy/app/controllers/concerns/.keep b/actiontext/test/dummy/app/controllers/concerns/.keep new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actiontext/test/dummy/app/controllers/concerns/.keep diff --git a/actiontext/test/dummy/app/controllers/messages_controller.rb b/actiontext/test/dummy/app/controllers/messages_controller.rb new file mode 100644 index 0000000000..81c79a2d1e --- /dev/null +++ b/actiontext/test/dummy/app/controllers/messages_controller.rb @@ -0,0 +1,58 @@ +class MessagesController < ApplicationController + before_action :set_message, only: [:show, :edit, :update, :destroy] + + # GET /messages + def index + @messages = Message.all + end + + # GET /messages/1 + def show + end + + # GET /messages/new + def new + @message = Message.new + end + + # GET /messages/1/edit + def edit + end + + # POST /messages + def create + @message = Message.new(message_params) + + if @message.save + redirect_to @message, notice: 'Message was successfully created.' + else + render :new + end + end + + # PATCH/PUT /messages/1 + def update + if @message.update(message_params) + redirect_to @message, notice: 'Message was successfully updated.' + else + render :edit + end + end + + # DELETE /messages/1 + def destroy + @message.destroy + redirect_to messages_url, notice: 'Message was successfully destroyed.' + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_message + @message = Message.find(params[:id]) + end + + # Only allow a trusted parameter "white list" through. + def message_params + params.require(:message).permit(:subject, :content) + end +end diff --git a/actiontext/test/dummy/app/helpers/application_helper.rb b/actiontext/test/dummy/app/helpers/application_helper.rb new file mode 100644 index 0000000000..de6be7945c --- /dev/null +++ b/actiontext/test/dummy/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/actiontext/test/dummy/app/helpers/messages_helper.rb b/actiontext/test/dummy/app/helpers/messages_helper.rb new file mode 100644 index 0000000000..f1bca9f6ca --- /dev/null +++ b/actiontext/test/dummy/app/helpers/messages_helper.rb @@ -0,0 +1,2 @@ +module MessagesHelper +end diff --git a/actiontext/test/dummy/app/javascript/packs/application.js b/actiontext/test/dummy/app/javascript/packs/application.js new file mode 100644 index 0000000000..13ac17ed58 --- /dev/null +++ b/actiontext/test/dummy/app/javascript/packs/application.js @@ -0,0 +1 @@ +import "@rails/actiontext" diff --git a/actiontext/test/dummy/app/jobs/application_job.rb b/actiontext/test/dummy/app/jobs/application_job.rb new file mode 100644 index 0000000000..a009ace51c --- /dev/null +++ b/actiontext/test/dummy/app/jobs/application_job.rb @@ -0,0 +1,2 @@ +class ApplicationJob < ActiveJob::Base +end diff --git a/actiontext/test/dummy/app/mailers/application_mailer.rb b/actiontext/test/dummy/app/mailers/application_mailer.rb new file mode 100644 index 0000000000..286b2239d1 --- /dev/null +++ b/actiontext/test/dummy/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: 'from@example.com' + layout 'mailer' +end diff --git a/actiontext/test/dummy/app/models/application_record.rb b/actiontext/test/dummy/app/models/application_record.rb new file mode 100644 index 0000000000..10a4cba84d --- /dev/null +++ b/actiontext/test/dummy/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/actiontext/test/dummy/app/models/concerns/.keep b/actiontext/test/dummy/app/models/concerns/.keep new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actiontext/test/dummy/app/models/concerns/.keep diff --git a/actiontext/test/dummy/app/models/message.rb b/actiontext/test/dummy/app/models/message.rb new file mode 100644 index 0000000000..9ea4dbfe78 --- /dev/null +++ b/actiontext/test/dummy/app/models/message.rb @@ -0,0 +1,4 @@ +class Message < ApplicationRecord + has_rich_text :content + has_rich_text :body +end diff --git a/actiontext/test/dummy/app/models/person.rb b/actiontext/test/dummy/app/models/person.rb new file mode 100644 index 0000000000..0ded356d5b --- /dev/null +++ b/actiontext/test/dummy/app/models/person.rb @@ -0,0 +1,7 @@ +class Person < ApplicationRecord + include ActionText::Attachable + + def to_trix_content_attachment_partial_path + "people/trix_content_attachment" + end +end diff --git a/actiontext/test/dummy/app/views/layouts/application.html.erb b/actiontext/test/dummy/app/views/layouts/application.html.erb new file mode 100644 index 0000000000..f221f512b7 --- /dev/null +++ b/actiontext/test/dummy/app/views/layouts/application.html.erb @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> + <head> + <title>Dummy</title> + <%= csrf_meta_tags %> + + <%= stylesheet_link_tag 'application', media: 'all' %> + <%= javascript_pack_tag 'application' %> + </head> + + <body> + <%= yield %> + </body> +</html> diff --git a/actiontext/test/dummy/app/views/layouts/mailer.html.erb b/actiontext/test/dummy/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000000..cbd34d2e9d --- /dev/null +++ b/actiontext/test/dummy/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> + <style> + /* Email styles need to be inline */ + </style> + </head> + + <body> + <%= yield %> + </body> +</html> diff --git a/actiontext/test/dummy/app/views/layouts/mailer.text.erb b/actiontext/test/dummy/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000000..37f0bddbd7 --- /dev/null +++ b/actiontext/test/dummy/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/actiontext/test/dummy/app/views/messages/_form.html.erb b/actiontext/test/dummy/app/views/messages/_form.html.erb new file mode 100644 index 0000000000..3b8a174884 --- /dev/null +++ b/actiontext/test/dummy/app/views/messages/_form.html.erb @@ -0,0 +1,27 @@ +<%= form_with(model: message, local: true) do |form| %> + <% if message.errors.any? %> + <div id="error_explanation"> + <h2><%= pluralize(message.errors.count, "error") %> prohibited this message from being saved:</h2> + + <ul> + <% message.errors.full_messages.each do |message| %> + <li><%= message %></li> + <% end %> + </ul> + </div> + <% end %> + + <div class="field"> + <%= form.label :subject %> + <%= form.text_field :subject %> + </div> + + <div class="field"> + <%= form.label :content %> + <%= form.rich_text_area :content, class: "trix-content" %> + </div> + + <div class="actions"> + <%= form.submit %> + </div> +<% end %> diff --git a/actiontext/test/dummy/app/views/messages/edit.html.erb b/actiontext/test/dummy/app/views/messages/edit.html.erb new file mode 100644 index 0000000000..90ad68c788 --- /dev/null +++ b/actiontext/test/dummy/app/views/messages/edit.html.erb @@ -0,0 +1,6 @@ +<h1>Editing Message</h1> + +<%= render 'form', message: @message %> + +<%= link_to 'Show', @message %> | +<%= link_to 'Back', messages_path %> diff --git a/actiontext/test/dummy/app/views/messages/index.html.erb b/actiontext/test/dummy/app/views/messages/index.html.erb new file mode 100644 index 0000000000..a8c97468c6 --- /dev/null +++ b/actiontext/test/dummy/app/views/messages/index.html.erb @@ -0,0 +1,29 @@ +<p id="notice"><%= notice %></p> + +<h1>Messages</h1> + +<table> + <thead> + <tr> + <th>Subject</th> + <th>Content</th> + <th colspan="3"></th> + </tr> + </thead> + + <tbody> + <% @messages.each do |message| %> + <tr> + <td><%= message.subject %></td> + <td><%= message.content %></td> + <td><%= link_to 'Show', message %></td> + <td><%= link_to 'Edit', edit_message_path(message) %></td> + <td><%= link_to 'Destroy', message, method: :delete, data: { confirm: 'Are you sure?' } %></td> + </tr> + <% end %> + </tbody> +</table> + +<br> + +<%= link_to 'New Message', new_message_path %> diff --git a/actiontext/test/dummy/app/views/messages/new.html.erb b/actiontext/test/dummy/app/views/messages/new.html.erb new file mode 100644 index 0000000000..6cbd3b8ffe --- /dev/null +++ b/actiontext/test/dummy/app/views/messages/new.html.erb @@ -0,0 +1,5 @@ +<h1>New Message</h1> + +<%= render 'form', message: @message %> + +<%= link_to 'Back', messages_path %> diff --git a/actiontext/test/dummy/app/views/messages/show.html.erb b/actiontext/test/dummy/app/views/messages/show.html.erb new file mode 100644 index 0000000000..25fad1efba --- /dev/null +++ b/actiontext/test/dummy/app/views/messages/show.html.erb @@ -0,0 +1,13 @@ +<p id="notice"><%= notice %></p> + +<p> + <strong>Subject:</strong> + <%= @message.subject %> +</p> + +<div class="trix-content"> + <%= @message.content.html_safe %> +</div> + +<%= link_to 'Edit', edit_message_path(@message) %> | +<%= link_to 'Back', messages_path %> diff --git a/actiontext/test/dummy/app/views/people/_trix_content_attachment.html.erb b/actiontext/test/dummy/app/views/people/_trix_content_attachment.html.erb new file mode 100644 index 0000000000..7db2334126 --- /dev/null +++ b/actiontext/test/dummy/app/views/people/_trix_content_attachment.html.erb @@ -0,0 +1,3 @@ +<span class="mentionable-person" gid="<%= person.to_gid %>"> + <%= person.name %> +</span> diff --git a/actiontext/test/dummy/bin/bundle b/actiontext/test/dummy/bin/bundle new file mode 100755 index 0000000000..f19acf5b5c --- /dev/null +++ b/actiontext/test/dummy/bin/bundle @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) +load Gem.bin_path('bundler', 'bundle') diff --git a/actiontext/test/dummy/bin/rails b/actiontext/test/dummy/bin/rails new file mode 100755 index 0000000000..0739660237 --- /dev/null +++ b/actiontext/test/dummy/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path('../config/application', __dir__) +require_relative '../config/boot' +require 'rails/commands' diff --git a/actiontext/test/dummy/bin/rake b/actiontext/test/dummy/bin/rake new file mode 100755 index 0000000000..17240489f6 --- /dev/null +++ b/actiontext/test/dummy/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative '../config/boot' +require 'rake' +Rake.application.run diff --git a/actiontext/test/dummy/bin/setup b/actiontext/test/dummy/bin/setup new file mode 100755 index 0000000000..94fd4d7977 --- /dev/null +++ b/actiontext/test/dummy/bin/setup @@ -0,0 +1,36 @@ +#!/usr/bin/env ruby +require 'fileutils' +include FileUtils + +# path to your application root. +APP_ROOT = File.expand_path('..', __dir__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +chdir APP_ROOT do + # This script is a starting point to setup your application. + # Add necessary setup steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + # Install JavaScript dependencies if using Yarn + # system('bin/yarn') + + # puts "\n== Copying sample files ==" + # unless File.exist?('config/database.yml') + # cp 'config/database.yml.sample', 'config/database.yml' + # end + + puts "\n== Preparing database ==" + system! 'bin/rails db:setup' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/actiontext/test/dummy/bin/update b/actiontext/test/dummy/bin/update new file mode 100755 index 0000000000..58bfaed518 --- /dev/null +++ b/actiontext/test/dummy/bin/update @@ -0,0 +1,31 @@ +#!/usr/bin/env ruby +require 'fileutils' +include FileUtils + +# path to your application root. +APP_ROOT = File.expand_path('..', __dir__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +chdir APP_ROOT do + # This script is a way to update your development environment automatically. + # Add necessary update steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + # Install JavaScript dependencies if using Yarn + # system('bin/yarn') + + puts "\n== Updating database ==" + system! 'bin/rails db:migrate' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/actiontext/test/dummy/bin/yarn b/actiontext/test/dummy/bin/yarn new file mode 100755 index 0000000000..460dd565b4 --- /dev/null +++ b/actiontext/test/dummy/bin/yarn @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +APP_ROOT = File.expand_path('..', __dir__) +Dir.chdir(APP_ROOT) do + begin + exec "yarnpkg", *ARGV + rescue Errno::ENOENT + $stderr.puts "Yarn executable was not detected in the system." + $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" + exit 1 + end +end diff --git a/actiontext/test/dummy/config.ru b/actiontext/test/dummy/config.ru new file mode 100644 index 0000000000..f7ba0b527b --- /dev/null +++ b/actiontext/test/dummy/config.ru @@ -0,0 +1,5 @@ +# This file is used by Rack-based servers to start the application. + +require_relative 'config/environment' + +run Rails.application diff --git a/actiontext/test/dummy/config/application.rb b/actiontext/test/dummy/config/application.rb new file mode 100644 index 0000000000..79eeb6ba0e --- /dev/null +++ b/actiontext/test/dummy/config/application.rb @@ -0,0 +1,19 @@ +require_relative 'boot' + +require 'rails/all' + +Bundler.require(*Rails.groups) +require "action_text" + +module Dummy + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 6.0 + + # Settings in config/environments/* take precedence over those specified here. + # Application configuration can go into files in config/initializers + # -- all .rb files in that directory are automatically loaded after loading + # the framework and any gems in your application. + end +end + diff --git a/actiontext/test/dummy/config/boot.rb b/actiontext/test/dummy/config/boot.rb new file mode 100644 index 0000000000..c9aef85d40 --- /dev/null +++ b/actiontext/test/dummy/config/boot.rb @@ -0,0 +1,5 @@ +# Set up gems listed in the Gemfile. +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__) + +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) +$LOAD_PATH.unshift File.expand_path('../../../lib', __dir__) diff --git a/actiontext/test/dummy/config/cable.yml b/actiontext/test/dummy/config/cable.yml new file mode 100644 index 0000000000..1cd0f8363e --- /dev/null +++ b/actiontext/test/dummy/config/cable.yml @@ -0,0 +1,10 @@ +development: + adapter: async + +test: + adapter: async + +production: + adapter: redis + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: dummy_production diff --git a/actiontext/test/dummy/config/database.yml b/actiontext/test/dummy/config/database.yml new file mode 100644 index 0000000000..0d02f24980 --- /dev/null +++ b/actiontext/test/dummy/config/database.yml @@ -0,0 +1,25 @@ +# SQLite version 3.x +# gem install sqlite3 +# +# Ensure the SQLite 3 gem is defined in your Gemfile +# gem 'sqlite3' +# +default: &default + adapter: sqlite3 + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + timeout: 5000 + +development: + <<: *default + database: db/development.sqlite3 + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: db/test.sqlite3 + +production: + <<: *default + database: db/production.sqlite3 diff --git a/actiontext/test/dummy/config/environment.rb b/actiontext/test/dummy/config/environment.rb new file mode 100644 index 0000000000..426333bb46 --- /dev/null +++ b/actiontext/test/dummy/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative 'application' + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/actiontext/test/dummy/config/environments/development.rb b/actiontext/test/dummy/config/environments/development.rb new file mode 100644 index 0000000000..f09b9a00c4 --- /dev/null +++ b/actiontext/test/dummy/config/environments/development.rb @@ -0,0 +1,63 @@ +Rails.application.configure do + # Verifies that versions and hashed value of the package contents in the project's package.json + # config.webpacker.check_yarn_integrity = true + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded on + # every request. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.cache_classes = false + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable/disable caching. By default caching is disabled. + # Run rails dev:cache to toggle caching. + if Rails.root.join('tmp', 'caching-dev.txt').exist? + config.action_controller.perform_caching = true + + config.cache_store = :memory_store + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{2.days.to_i}" + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + + # Store uploaded files on the local file system (see config/storage.yml for options) + config.active_storage.service = :local + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + config.action_mailer.perform_caching = false + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Debug mode disables concatenation and preprocessing of assets. + # This option may cause significant delays in view rendering with a large + # number of complex assets. + config.assets.debug = true + + # Suppress logger output for asset requests. + config.assets.quiet = true + + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true + + # Use an evented file watcher to asynchronously detect changes in source code, + # routes, locales, etc. This feature depends on the listen gem. + # config.file_watcher = ActiveSupport::EventedFileUpdateChecker +end diff --git a/actiontext/test/dummy/config/environments/production.rb b/actiontext/test/dummy/config/environments/production.rb new file mode 100644 index 0000000000..2aaa79f620 --- /dev/null +++ b/actiontext/test/dummy/config/environments/production.rb @@ -0,0 +1,96 @@ +Rails.application.configure do + # Verifies that versions and hashed value of the package contents in the project's package.json + config.webpacker.check_yarn_integrity = false + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.cache_classes = true + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] + # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true + + # Disable serving static files from the `/public` folder by default since + # Apache or NGINX already handles this. + config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? + + # Compress JavaScripts and CSS. + config.assets.js_compressor = :uglifier + # config.assets.css_compressor = :sass + + # Do not fallback to assets pipeline if a precompiled asset is missed. + config.assets.compile = false + + # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.action_controller.asset_host = 'http://assets.example.com' + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + + # Store uploaded files on the local file system (see config/storage.yml for options) + config.active_storage.service = :local + + # Mount Action Cable outside main process or domain + # config.action_cable.mount_path = nil + # config.action_cable.url = 'wss://example.com/cable' + # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # Use the lowest log level to ensure availability of diagnostic information + # when problems arise. + config.log_level = :debug + + # Prepend all log lines with the following tags. + config.log_tags = [ :request_id ] + + # Use a different cache store in production. + # config.cache_store = :mem_cache_store + + # Use a real queuing backend for Active Job (and separate queues per environment) + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "dummy_#{Rails.env}" + + config.action_mailer.perform_caching = false + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Send deprecation notices to registered listeners. + config.active_support.deprecation = :notify + + # Use default logging formatter so that PID and timestamp are not suppressed. + config.log_formatter = ::Logger::Formatter.new + + # Use a different logger for distributed setups. + # require 'syslog/logger' + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') + + if ENV["RAILS_LOG_TO_STDOUT"].present? + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false +end diff --git a/actiontext/test/dummy/config/environments/test.rb b/actiontext/test/dummy/config/environments/test.rb new file mode 100644 index 0000000000..0a38fd3ce9 --- /dev/null +++ b/actiontext/test/dummy/config/environments/test.rb @@ -0,0 +1,46 @@ +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # The test environment is used exclusively to run your application's + # test suite. You never need to work with it otherwise. Remember that + # your test database is "scratch space" for the test suite and is wiped + # and recreated between test runs. Don't rely on the data there! + config.cache_classes = true + + # Do not eager load code on boot. This avoids loading your whole application + # just for the purpose of running a single test. If you are using a tool that + # preloads Rails for running tests, you may have to set it to true. + config.eager_load = false + + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{1.hour.to_i}" + } + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + + # Raise exceptions instead of rendering exception templates. + config.action_dispatch.show_exceptions = false + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Store uploaded files on the local file system in a temporary directory + config.active_storage.service = :test + + config.action_mailer.perform_caching = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true +end diff --git a/actiontext/test/dummy/config/initializers/application_controller_renderer.rb b/actiontext/test/dummy/config/initializers/application_controller_renderer.rb new file mode 100644 index 0000000000..89d2efab2b --- /dev/null +++ b/actiontext/test/dummy/config/initializers/application_controller_renderer.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# ActiveSupport::Reloader.to_prepare do +# ApplicationController.renderer.defaults.merge!( +# http_host: 'example.org', +# https: false +# ) +# end diff --git a/actiontext/test/dummy/config/initializers/assets.rb b/actiontext/test/dummy/config/initializers/assets.rb new file mode 100644 index 0000000000..4b828e80cb --- /dev/null +++ b/actiontext/test/dummy/config/initializers/assets.rb @@ -0,0 +1,14 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = '1.0' + +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path +# Add Yarn node_modules folder to the asset load path. +Rails.application.config.assets.paths << Rails.root.join('node_modules') + +# Precompile additional assets. +# application.js, application.css, and all non-JS/CSS in the app/assets +# folder are already added. +# Rails.application.config.assets.precompile += %w( admin.js admin.css ) diff --git a/actiontext/test/dummy/config/initializers/backtrace_silencers.rb b/actiontext/test/dummy/config/initializers/backtrace_silencers.rb new file mode 100644 index 0000000000..59385cdf37 --- /dev/null +++ b/actiontext/test/dummy/config/initializers/backtrace_silencers.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. +# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } + +# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. +# Rails.backtrace_cleaner.remove_silencers! diff --git a/actiontext/test/dummy/config/initializers/content_security_policy.rb b/actiontext/test/dummy/config/initializers/content_security_policy.rb new file mode 100644 index 0000000000..edde7f42b8 --- /dev/null +++ b/actiontext/test/dummy/config/initializers/content_security_policy.rb @@ -0,0 +1,22 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy +# For further information see the following documentation +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + +# Rails.application.config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https, :unsafe_inline + +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end + +# Report CSP violations to a specified URI +# For further information see the following documentation: +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only +# Rails.application.config.content_security_policy_report_only = true diff --git a/actiontext/test/dummy/config/initializers/cookies_serializer.rb b/actiontext/test/dummy/config/initializers/cookies_serializer.rb new file mode 100644 index 0000000000..5a6a32d371 --- /dev/null +++ b/actiontext/test/dummy/config/initializers/cookies_serializer.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. + +# Specify a serializer for the signed and encrypted cookie jars. +# Valid options are :json, :marshal, and :hybrid. +Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/actiontext/test/dummy/config/initializers/filter_parameter_logging.rb b/actiontext/test/dummy/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000000..4a994e1e7b --- /dev/null +++ b/actiontext/test/dummy/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Configure sensitive parameters which will be filtered from the log file. +Rails.application.config.filter_parameters += [:password] diff --git a/actiontext/test/dummy/config/initializers/inflections.rb b/actiontext/test/dummy/config/initializers/inflections.rb new file mode 100644 index 0000000000..ac033bf9dc --- /dev/null +++ b/actiontext/test/dummy/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, '\1en' +# inflect.singular /^(ox)en/i, '\1' +# inflect.irregular 'person', 'people' +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym 'RESTful' +# end diff --git a/actiontext/test/dummy/config/initializers/mime_types.rb b/actiontext/test/dummy/config/initializers/mime_types.rb new file mode 100644 index 0000000000..dc1899682b --- /dev/null +++ b/actiontext/test/dummy/config/initializers/mime_types.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Add new mime types for use in respond_to blocks: +# Mime::Type.register "text/richtext", :rtf diff --git a/actiontext/test/dummy/config/initializers/wrap_parameters.rb b/actiontext/test/dummy/config/initializers/wrap_parameters.rb new file mode 100644 index 0000000000..bbfc3961bf --- /dev/null +++ b/actiontext/test/dummy/config/initializers/wrap_parameters.rb @@ -0,0 +1,14 @@ +# Be sure to restart your server when you modify this file. + +# This file contains settings for ActionController::ParamsWrapper which +# is enabled by default. + +# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. +ActiveSupport.on_load(:action_controller) do + wrap_parameters format: [:json] +end + +# To enable root element in JSON for ActiveRecord objects. +# ActiveSupport.on_load(:active_record) do +# self.include_root_in_json = true +# end diff --git a/actiontext/test/dummy/config/locales/en.yml b/actiontext/test/dummy/config/locales/en.yml new file mode 100644 index 0000000000..decc5a8573 --- /dev/null +++ b/actiontext/test/dummy/config/locales/en.yml @@ -0,0 +1,33 @@ +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t 'hello' +# +# In views, this is aliased to just `t`: +# +# <%= t('hello') %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# The following keys must be escaped otherwise they will not be retrieved by +# the default I18n backend: +# +# true, false, on, off, yes, no +# +# Instead, surround them with single quotes. +# +# en: +# 'true': 'foo' +# +# To learn more, please read the Rails Internationalization guide +# available at http://guides.rubyonrails.org/i18n.html. + +en: + hello: "Hello world" diff --git a/actiontext/test/dummy/config/puma.rb b/actiontext/test/dummy/config/puma.rb new file mode 100644 index 0000000000..71ed69a895 --- /dev/null +++ b/actiontext/test/dummy/config/puma.rb @@ -0,0 +1,34 @@ +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers: a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum; this matches the default thread size of Active Record. +# +threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +# +port ENV.fetch("PORT") { 3000 } + +# Specifies the `environment` that Puma will run in. +# +environment ENV.fetch("RAILS_ENV") { "development" } + +# Specifies the number of `workers` to boot in clustered mode. +# Workers are forked web server processes. If using threads and workers together +# the concurrency of the application would be max `threads` * `workers`. +# Workers do not work on JRuby or Windows (both of which do not support +# processes). +# +# workers ENV.fetch("WEB_CONCURRENCY") { 2 } + +# Use the `preload_app!` method when specifying a `workers` number. +# This directive tells Puma to first boot the application and load code +# before forking the application. This takes advantage of Copy On Write +# process behavior so workers use less memory. +# +# preload_app! + +# Allow puma to be restarted by `rails restart` command. +plugin :tmp_restart diff --git a/actiontext/test/dummy/config/routes.rb b/actiontext/test/dummy/config/routes.rb new file mode 100644 index 0000000000..30b05169b3 --- /dev/null +++ b/actiontext/test/dummy/config/routes.rb @@ -0,0 +1,4 @@ +Rails.application.routes.draw do + resources :messages + # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html +end diff --git a/actiontext/test/dummy/config/spring.rb b/actiontext/test/dummy/config/spring.rb new file mode 100644 index 0000000000..9fa7863f99 --- /dev/null +++ b/actiontext/test/dummy/config/spring.rb @@ -0,0 +1,6 @@ +%w[ + .ruby-version + .rbenv-vars + tmp/restart.txt + tmp/caching-dev.txt +].each { |path| Spring.watch(path) } diff --git a/actiontext/test/dummy/config/storage.yml b/actiontext/test/dummy/config/storage.yml new file mode 100644 index 0000000000..53e562e0fc --- /dev/null +++ b/actiontext/test/dummy/config/storage.yml @@ -0,0 +1,35 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket + +# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# microsoft: +# service: AzureStorage +# path: your_azure_storage_path +# storage_account_name: your_account_name +# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> +# container: your_container_name + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/actiontext/test/dummy/config/webpack/development.js b/actiontext/test/dummy/config/webpack/development.js new file mode 100644 index 0000000000..81269f6513 --- /dev/null +++ b/actiontext/test/dummy/config/webpack/development.js @@ -0,0 +1,3 @@ +const environment = require('./environment') + +module.exports = environment.toWebpackConfig() diff --git a/actiontext/test/dummy/config/webpack/environment.js b/actiontext/test/dummy/config/webpack/environment.js new file mode 100644 index 0000000000..d16d9af743 --- /dev/null +++ b/actiontext/test/dummy/config/webpack/environment.js @@ -0,0 +1,3 @@ +const { environment } = require('@rails/webpacker') + +module.exports = environment diff --git a/actiontext/test/dummy/config/webpack/production.js b/actiontext/test/dummy/config/webpack/production.js new file mode 100644 index 0000000000..81269f6513 --- /dev/null +++ b/actiontext/test/dummy/config/webpack/production.js @@ -0,0 +1,3 @@ +const environment = require('./environment') + +module.exports = environment.toWebpackConfig() diff --git a/actiontext/test/dummy/config/webpack/test.js b/actiontext/test/dummy/config/webpack/test.js new file mode 100644 index 0000000000..81269f6513 --- /dev/null +++ b/actiontext/test/dummy/config/webpack/test.js @@ -0,0 +1,3 @@ +const environment = require('./environment') + +module.exports = environment.toWebpackConfig() diff --git a/actiontext/test/dummy/config/webpacker.yml b/actiontext/test/dummy/config/webpacker.yml new file mode 100644 index 0000000000..d3f24e1b4b --- /dev/null +++ b/actiontext/test/dummy/config/webpacker.yml @@ -0,0 +1,65 @@ +# Note: You must restart bin/webpack-dev-server for changes to take effect + +default: &default + source_path: app/javascript + source_entry_path: packs + public_output_path: packs + cache_path: tmp/cache/webpacker + + # Additional paths webpack should lookup modules + # ['app/assets', 'engine/foo/app/assets'] + resolved_paths: [] + + # Reload manifest.json on all requests so we reload latest compiled packs + cache_manifest: false + + extensions: + - .js + - .sass + - .scss + - .css + - .png + - .svg + - .gif + - .jpeg + - .jpg + +development: + <<: *default + compile: true + + # Reference: https://webpack.js.org/configuration/dev-server/ + dev_server: + https: false + host: localhost + port: 3035 + public: localhost:3035 + hmr: false + # Inline should be set to true if using HMR + inline: true + overlay: true + compress: true + disable_host_check: true + use_local_ip: false + quiet: false + headers: + 'Access-Control-Allow-Origin': '*' + watch_options: + ignored: /node_modules/ + + +test: + <<: *default + compile: true + + # Compile test packs to a separate directory + public_output_path: packs-test + +production: + <<: *default + + # Production depends on precompilation of packs prior to booting for performance. + compile: false + + # Cache manifest.json for performance + cache_manifest: true diff --git a/actiontext/test/dummy/db/migrate/20180208205311_create_messages.rb b/actiontext/test/dummy/db/migrate/20180208205311_create_messages.rb new file mode 100644 index 0000000000..3552840824 --- /dev/null +++ b/actiontext/test/dummy/db/migrate/20180208205311_create_messages.rb @@ -0,0 +1,8 @@ +class CreateMessages < ActiveRecord::Migration[6.0] + def change + create_table :messages do |t| + t.string :subject + t.timestamps + end + end +end diff --git a/actiontext/test/dummy/db/migrate/20180212164506_create_active_storage_tables.rb b/actiontext/test/dummy/db/migrate/20180212164506_create_active_storage_tables.rb new file mode 100644 index 0000000000..0b2ce257c8 --- /dev/null +++ b/actiontext/test/dummy/db/migrate/20180212164506_create_active_storage_tables.rb @@ -0,0 +1,27 @@ +# This migration comes from active_storage (originally 20170806125915) +class CreateActiveStorageTables < ActiveRecord::Migration[5.2] + def change + create_table :active_storage_blobs do |t| + t.string :key, null: false + t.string :filename, null: false + t.string :content_type + t.text :metadata + t.bigint :byte_size, null: false + t.string :checksum, null: false + t.datetime :created_at, null: false + + t.index [ :key ], unique: true + end + + create_table :active_storage_attachments do |t| + t.string :name, null: false + t.references :record, null: false, polymorphic: true, index: false + t.references :blob, null: false + + t.datetime :created_at, null: false + + t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + end +end diff --git a/actiontext/test/dummy/db/migrate/20180528164100_create_action_text_tables.rb b/actiontext/test/dummy/db/migrate/20180528164100_create_action_text_tables.rb new file mode 100644 index 0000000000..e7c66ea6ae --- /dev/null +++ b/actiontext/test/dummy/db/migrate/20180528164100_create_action_text_tables.rb @@ -0,0 +1,13 @@ +class CreateActionTextTables < ActiveRecord::Migration[6.0] + def change + create_table :action_text_rich_texts do |t| + t.string :name, null: false + t.text :body, size: :long + t.references :record, null: false, polymorphic: true, index: false + + t.timestamps + + t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true + end + end +end diff --git a/actiontext/test/dummy/db/migrate/20181003185713_create_people.rb b/actiontext/test/dummy/db/migrate/20181003185713_create_people.rb new file mode 100644 index 0000000000..6928a8e20d --- /dev/null +++ b/actiontext/test/dummy/db/migrate/20181003185713_create_people.rb @@ -0,0 +1,9 @@ +class CreatePeople < ActiveRecord::Migration[6.0] + def change + create_table :people do |t| + t.string :name + + t.timestamps + end + end +end diff --git a/actiontext/test/dummy/db/schema.rb b/actiontext/test/dummy/db/schema.rb new file mode 100644 index 0000000000..5216c5fd33 --- /dev/null +++ b/actiontext/test/dummy/db/schema.rb @@ -0,0 +1,58 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `rails +# db:schema:load`. When creating a new database, `rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 2018_10_03_185713) do + + create_table "action_text_rich_texts", force: :cascade do |t| + t.string "name", null: false + t.text "body" + t.string "record_type", null: false + t.integer "record_id", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true + end + + create_table "active_storage_attachments", force: :cascade do |t| + t.string "name", null: false + t.string "record_type", null: false + t.integer "record_id", null: false + t.integer "blob_id", null: false + t.datetime "created_at", null: false + t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" + t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true + end + + create_table "active_storage_blobs", force: :cascade do |t| + t.string "key", null: false + t.string "filename", null: false + t.string "content_type" + t.text "metadata" + t.bigint "byte_size", null: false + t.string "checksum", null: false + t.datetime "created_at", null: false + t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true + end + + create_table "messages", force: :cascade do |t| + t.string "subject" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + end + + create_table "people", force: :cascade do |t| + t.string "name" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + end + +end diff --git a/actiontext/test/dummy/lib/assets/.keep b/actiontext/test/dummy/lib/assets/.keep new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actiontext/test/dummy/lib/assets/.keep diff --git a/actiontext/test/dummy/log/.keep b/actiontext/test/dummy/log/.keep new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actiontext/test/dummy/log/.keep diff --git a/actiontext/test/dummy/package.json b/actiontext/test/dummy/package.json new file mode 100644 index 0000000000..ff2b802bba --- /dev/null +++ b/actiontext/test/dummy/package.json @@ -0,0 +1,11 @@ +{ + "name": "dummy", + "private": true, + "dependencies": { + "@rails/webpacker": "^3.2.2", + "actiontext": "file:../.." + }, + "devDependencies": { + "webpack-dev-server": "^2.11.1" + } +} diff --git a/actiontext/test/dummy/public/404.html b/actiontext/test/dummy/public/404.html new file mode 100644 index 0000000000..2be3af26fc --- /dev/null +++ b/actiontext/test/dummy/public/404.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<html> +<head> + <title>The page you were looking for doesn't exist (404)</title> + <meta name="viewport" content="width=device-width,initial-scale=1"> + <style> + .rails-default-error-page { + background-color: #EFEFEF; + color: #2E2F30; + text-align: center; + font-family: arial, sans-serif; + margin: 0; + } + + .rails-default-error-page div.dialog { + width: 95%; + max-width: 33em; + margin: 4em auto 0; + } + + .rails-default-error-page div.dialog > div { + border: 1px solid #CCC; + border-right-color: #999; + border-left-color: #999; + border-bottom-color: #BBB; + border-top: #B00100 solid 4px; + border-top-left-radius: 9px; + border-top-right-radius: 9px; + background-color: white; + padding: 7px 12% 0; + box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); + } + + .rails-default-error-page h1 { + font-size: 100%; + color: #730E15; + line-height: 1.5em; + } + + .rails-default-error-page div.dialog > p { + margin: 0 0 1em; + padding: 1em; + background-color: #F7F7F7; + border: 1px solid #CCC; + border-right-color: #999; + border-left-color: #999; + border-bottom-color: #999; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-top-color: #DADADA; + color: #666; + box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); + } + </style> +</head> + +<body class="rails-default-error-page"> + <!-- This file lives in public/404.html --> + <div class="dialog"> + <div> + <h1>The page you were looking for doesn't exist.</h1> + <p>You may have mistyped the address or the page may have moved.</p> + </div> + <p>If you are the application owner check the logs for more information.</p> + </div> +</body> +</html> diff --git a/actiontext/test/dummy/public/422.html b/actiontext/test/dummy/public/422.html new file mode 100644 index 0000000000..c08eac0d1d --- /dev/null +++ b/actiontext/test/dummy/public/422.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<html> +<head> + <title>The change you wanted was rejected (422)</title> + <meta name="viewport" content="width=device-width,initial-scale=1"> + <style> + .rails-default-error-page { + background-color: #EFEFEF; + color: #2E2F30; + text-align: center; + font-family: arial, sans-serif; + margin: 0; + } + + .rails-default-error-page div.dialog { + width: 95%; + max-width: 33em; + margin: 4em auto 0; + } + + .rails-default-error-page div.dialog > div { + border: 1px solid #CCC; + border-right-color: #999; + border-left-color: #999; + border-bottom-color: #BBB; + border-top: #B00100 solid 4px; + border-top-left-radius: 9px; + border-top-right-radius: 9px; + background-color: white; + padding: 7px 12% 0; + box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); + } + + .rails-default-error-page h1 { + font-size: 100%; + color: #730E15; + line-height: 1.5em; + } + + .rails-default-error-page div.dialog > p { + margin: 0 0 1em; + padding: 1em; + background-color: #F7F7F7; + border: 1px solid #CCC; + border-right-color: #999; + border-left-color: #999; + border-bottom-color: #999; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-top-color: #DADADA; + color: #666; + box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); + } + </style> +</head> + +<body class="rails-default-error-page"> + <!-- This file lives in public/422.html --> + <div class="dialog"> + <div> + <h1>The change you wanted was rejected.</h1> + <p>Maybe you tried to change something you didn't have access to.</p> + </div> + <p>If you are the application owner check the logs for more information.</p> + </div> +</body> +</html> diff --git a/actiontext/test/dummy/public/500.html b/actiontext/test/dummy/public/500.html new file mode 100644 index 0000000000..78a030af22 --- /dev/null +++ b/actiontext/test/dummy/public/500.html @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<html> +<head> + <title>We're sorry, but something went wrong (500)</title> + <meta name="viewport" content="width=device-width,initial-scale=1"> + <style> + .rails-default-error-page { + background-color: #EFEFEF; + color: #2E2F30; + text-align: center; + font-family: arial, sans-serif; + margin: 0; + } + + .rails-default-error-page div.dialog { + width: 95%; + max-width: 33em; + margin: 4em auto 0; + } + + .rails-default-error-page div.dialog > div { + border: 1px solid #CCC; + border-right-color: #999; + border-left-color: #999; + border-bottom-color: #BBB; + border-top: #B00100 solid 4px; + border-top-left-radius: 9px; + border-top-right-radius: 9px; + background-color: white; + padding: 7px 12% 0; + box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); + } + + .rails-default-error-page h1 { + font-size: 100%; + color: #730E15; + line-height: 1.5em; + } + + .rails-default-error-page div.dialog > p { + margin: 0 0 1em; + padding: 1em; + background-color: #F7F7F7; + border: 1px solid #CCC; + border-right-color: #999; + border-left-color: #999; + border-bottom-color: #999; + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-top-color: #DADADA; + color: #666; + box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17); + } + </style> +</head> + +<body class="rails-default-error-page"> + <!-- This file lives in public/500.html --> + <div class="dialog"> + <div> + <h1>We're sorry, but something went wrong.</h1> + </div> + <p>If you are the application owner check the logs for more information.</p> + </div> +</body> +</html> diff --git a/actiontext/test/dummy/public/apple-touch-icon-precomposed.png b/actiontext/test/dummy/public/apple-touch-icon-precomposed.png new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actiontext/test/dummy/public/apple-touch-icon-precomposed.png diff --git a/actiontext/test/dummy/public/apple-touch-icon.png b/actiontext/test/dummy/public/apple-touch-icon.png new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actiontext/test/dummy/public/apple-touch-icon.png diff --git a/actiontext/test/dummy/public/favicon.ico b/actiontext/test/dummy/public/favicon.ico new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actiontext/test/dummy/public/favicon.ico diff --git a/actiontext/test/dummy/storage/.keep b/actiontext/test/dummy/storage/.keep new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actiontext/test/dummy/storage/.keep diff --git a/actiontext/test/dummy/tmp/.keep b/actiontext/test/dummy/tmp/.keep new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actiontext/test/dummy/tmp/.keep diff --git a/actiontext/test/dummy/tmp/storage/.keep b/actiontext/test/dummy/tmp/storage/.keep new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/actiontext/test/dummy/tmp/storage/.keep diff --git a/actiontext/test/dummy/yarn.lock b/actiontext/test/dummy/yarn.lock new file mode 100644 index 0000000000..40444165f0 --- /dev/null +++ b/actiontext/test/dummy/yarn.lock @@ -0,0 +1,6071 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@rails/webpacker@^3.2.2": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@rails/webpacker/-/webpacker-3.2.2.tgz#6d60e1cf729dc2ccc52053c9b6b8d30c9a48a297" + dependencies: + babel-core "^6.26.0" + babel-loader "^7.1.2" + babel-plugin-syntax-dynamic-import "^6.18.0" + babel-plugin-transform-class-properties "^6.24.1" + babel-plugin-transform-object-rest-spread "^6.26.0" + babel-polyfill "^6.26.0" + babel-preset-env "^1.6.1" + case-sensitive-paths-webpack-plugin "^2.1.1" + compression-webpack-plugin "^1.1.6" + css-loader "^0.28.9" + extract-text-webpack-plugin "^3.0.2" + file-loader "^1.1.6" + glob "^7.1.2" + js-yaml "^3.10.0" + node-sass "^4.7.2" + path-complete-extname "^0.1.0" + postcss-cssnext "^3.1.0" + postcss-import "^11.0.0" + postcss-loader "^2.1.0" + sass-loader "^6.0.6" + style-loader "^0.20.1" + uglifyjs-webpack-plugin "^1.1.8" + webpack "^3.10.0" + webpack-manifest-plugin "^1.3.2" + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + +accepts@~1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f" + dependencies: + mime-types "~2.1.16" + negotiator "0.6.1" + +acorn-dynamic-import@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4" + dependencies: + acorn "^4.0.3" + +acorn@^4.0.3: + version "4.0.13" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" + +acorn@^5.0.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.4.1.tgz#fdc58d9d17f4a4e98d102ded826a9b9759125102" + +"activestorage@>= 5.2.0-rc1": + version "5.2.0-rc1" + resolved "https://registry.yarnpkg.com/activestorage/-/activestorage-5.2.0-rc1.tgz#79898996eceb0f13575eff41fb109051fbfa49b0" + +"actiontext@file:../..": + version "0.1" + dependencies: + activestorage ">= 5.2.0-rc1" + trix "^0.11.1" + +ajv-keywords@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762" + +ajv-keywords@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.1.0.tgz#ac2b27939c543e95d2c06e7f7f5c27be4aa543be" + +ajv@^4.9.1: + version "4.11.8" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" + dependencies: + co "^4.6.0" + json-stable-stringify "^1.0.1" + +ajv@^5.0.0, ajv@^5.1.0: + version "5.5.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + +ajv@^6.1.0: + version "6.1.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.1.1.tgz#978d597fbc2b7d0e5a5c3ddeb149a682f2abfa0e" + dependencies: + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + +align-text@^0.1.1, align-text@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" + dependencies: + kind-of "^3.0.2" + longest "^1.0.1" + repeat-string "^1.5.2" + +alphanum-sort@^1.0.1, alphanum-sort@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" + +amdefine@>=0.0.4: + version "1.0.1" + resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" + +ansi-html@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +ansi-styles@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" + dependencies: + color-convert "^1.9.0" + +anymatch@^1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" + dependencies: + micromatch "^2.1.5" + normalize-path "^2.0.0" + +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" + +aproba@^1.0.3, aproba@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + +are-we-there-yet@~1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d" + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +argparse@^1.0.7: + version "1.0.9" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" + dependencies: + sprintf-js "~1.0.2" + +arr-diff@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" + dependencies: + arr-flatten "^1.0.1" + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + +arr-flatten@^1.0.1, arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + +array-flatten@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.1.tgz#426bb9da84090c1838d812c8150af20a8331e296" + +array-includes@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d" + dependencies: + define-properties "^1.1.2" + es-abstract "^1.7.0" + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + +array-unique@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + +asn1.js@^4.0.0: + version "4.9.2" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.2.tgz#8117ef4f7ed87cd8f89044b5bff97ac243a16c9a" + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +asn1@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +assert-plus@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" + +assert@^1.1.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" + dependencies: + util "0.10.3" + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + +async-each@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" + +async-foreach@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" + +async@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + +async@^2.1.2, async@^2.1.5, async@^2.4.1: + version "2.6.0" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" + dependencies: + lodash "^4.14.0" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +atob@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.0.3.tgz#19c7a760473774468f20b2d2d03372ad7d4cbf5d" + +autoprefixer@^6.3.1: + version "6.7.7" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014" + dependencies: + browserslist "^1.7.6" + caniuse-db "^1.0.30000634" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^5.2.16" + postcss-value-parser "^3.2.3" + +autoprefixer@^7.1.1: + version "7.2.6" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-7.2.6.tgz#256672f86f7c735da849c4f07d008abb056067dc" + dependencies: + browserslist "^2.11.3" + caniuse-lite "^1.0.30000805" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^6.0.17" + postcss-value-parser "^3.2.3" + +aws-sign2@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + +aws4@^1.2.1, aws4@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" + +babel-code-frame@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" + dependencies: + chalk "^1.1.3" + esutils "^2.0.2" + js-tokens "^3.0.2" + +babel-core@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8" + dependencies: + babel-code-frame "^6.26.0" + babel-generator "^6.26.0" + babel-helpers "^6.24.1" + babel-messages "^6.23.0" + babel-register "^6.26.0" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + convert-source-map "^1.5.0" + debug "^2.6.8" + json5 "^0.5.1" + lodash "^4.17.4" + minimatch "^3.0.4" + path-is-absolute "^1.0.1" + private "^0.1.7" + slash "^1.0.0" + source-map "^0.5.6" + +babel-generator@^6.26.0: + version "6.26.1" + resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" + dependencies: + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + detect-indent "^4.0.0" + jsesc "^1.3.0" + lodash "^4.17.4" + source-map "^0.5.7" + trim-right "^1.0.1" + +babel-helper-builder-binary-assignment-operator-visitor@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664" + dependencies: + babel-helper-explode-assignable-expression "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-call-delegate@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d" + dependencies: + babel-helper-hoist-variables "^6.24.1" + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-define-map@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-helper-explode-assignable-expression@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa" + dependencies: + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-function-name@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" + dependencies: + babel-helper-get-function-arity "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-get-function-arity@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-hoist-variables@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-optimise-call-expression@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-regex@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz#325c59f902f82f24b74faceed0363954f6495e72" + dependencies: + babel-runtime "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-helper-remap-async-to-generator@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz#5ec581827ad723fecdd381f1c928390676e4551b" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-replace-supers@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a" + dependencies: + babel-helper-optimise-call-expression "^6.24.1" + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helpers@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-loader@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.2.tgz#f6cbe122710f1aa2af4d881c6d5b54358ca24126" + dependencies: + find-cache-dir "^1.0.0" + loader-utils "^1.0.2" + mkdirp "^0.5.1" + +babel-messages@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-check-es2015-constants@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-syntax-async-functions@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" + +babel-plugin-syntax-class-properties@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de" + +babel-plugin-syntax-dynamic-import@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz#8d6a26229c83745a9982a441051572caa179b1da" + +babel-plugin-syntax-exponentiation-operator@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" + +babel-plugin-syntax-object-rest-spread@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" + +babel-plugin-syntax-trailing-function-commas@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" + +babel-plugin-transform-async-to-generator@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761" + dependencies: + babel-helper-remap-async-to-generator "^6.24.1" + babel-plugin-syntax-async-functions "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-class-properties@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz#6a79763ea61d33d36f37b611aa9def81a81b46ac" + dependencies: + babel-helper-function-name "^6.24.1" + babel-plugin-syntax-class-properties "^6.8.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-arrow-functions@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-block-scoped-functions@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-block-scoping@^6.23.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f" + dependencies: + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-plugin-transform-es2015-classes@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db" + dependencies: + babel-helper-define-map "^6.24.1" + babel-helper-function-name "^6.24.1" + babel-helper-optimise-call-expression "^6.24.1" + babel-helper-replace-supers "^6.24.1" + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-computed-properties@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3" + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-destructuring@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-duplicate-keys@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-for-of@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-function-name@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-literals@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015-modules-amd@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154" + dependencies: + babel-plugin-transform-es2015-modules-commonjs "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz#0d8394029b7dc6abe1a97ef181e00758dd2e5d8a" + dependencies: + babel-plugin-transform-strict-mode "^6.24.1" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-types "^6.26.0" + +babel-plugin-transform-es2015-modules-systemjs@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23" + dependencies: + babel-helper-hoist-variables "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-modules-umd@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468" + dependencies: + babel-plugin-transform-es2015-modules-amd "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-object-super@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d" + dependencies: + babel-helper-replace-supers "^6.24.1" + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-parameters@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b" + dependencies: + babel-helper-call-delegate "^6.24.1" + babel-helper-get-function-arity "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-shorthand-properties@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-spread@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-sticky-regex@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc" + dependencies: + babel-helper-regex "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-template-literals@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-typeof-symbol@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-unicode-regex@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9" + dependencies: + babel-helper-regex "^6.24.1" + babel-runtime "^6.22.0" + regexpu-core "^2.0.0" + +babel-plugin-transform-exponentiation-operator@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e" + dependencies: + babel-helper-builder-binary-assignment-operator-visitor "^6.24.1" + babel-plugin-syntax-exponentiation-operator "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-object-rest-spread@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06" + dependencies: + babel-plugin-syntax-object-rest-spread "^6.8.0" + babel-runtime "^6.26.0" + +babel-plugin-transform-regenerator@^6.22.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f" + dependencies: + regenerator-transform "^0.10.0" + +babel-plugin-transform-strict-mode@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-polyfill@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.26.0.tgz#379937abc67d7895970adc621f284cd966cf2153" + dependencies: + babel-runtime "^6.26.0" + core-js "^2.5.0" + regenerator-runtime "^0.10.5" + +babel-preset-env@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.6.1.tgz#a18b564cc9b9afdf4aae57ae3c1b0d99188e6f48" + dependencies: + babel-plugin-check-es2015-constants "^6.22.0" + babel-plugin-syntax-trailing-function-commas "^6.22.0" + babel-plugin-transform-async-to-generator "^6.22.0" + babel-plugin-transform-es2015-arrow-functions "^6.22.0" + babel-plugin-transform-es2015-block-scoped-functions "^6.22.0" + babel-plugin-transform-es2015-block-scoping "^6.23.0" + babel-plugin-transform-es2015-classes "^6.23.0" + babel-plugin-transform-es2015-computed-properties "^6.22.0" + babel-plugin-transform-es2015-destructuring "^6.23.0" + babel-plugin-transform-es2015-duplicate-keys "^6.22.0" + babel-plugin-transform-es2015-for-of "^6.23.0" + babel-plugin-transform-es2015-function-name "^6.22.0" + babel-plugin-transform-es2015-literals "^6.22.0" + babel-plugin-transform-es2015-modules-amd "^6.22.0" + babel-plugin-transform-es2015-modules-commonjs "^6.23.0" + babel-plugin-transform-es2015-modules-systemjs "^6.23.0" + babel-plugin-transform-es2015-modules-umd "^6.23.0" + babel-plugin-transform-es2015-object-super "^6.22.0" + babel-plugin-transform-es2015-parameters "^6.23.0" + babel-plugin-transform-es2015-shorthand-properties "^6.22.0" + babel-plugin-transform-es2015-spread "^6.22.0" + babel-plugin-transform-es2015-sticky-regex "^6.22.0" + babel-plugin-transform-es2015-template-literals "^6.22.0" + babel-plugin-transform-es2015-typeof-symbol "^6.23.0" + babel-plugin-transform-es2015-unicode-regex "^6.22.0" + babel-plugin-transform-exponentiation-operator "^6.22.0" + babel-plugin-transform-regenerator "^6.22.0" + browserslist "^2.1.2" + invariant "^2.2.2" + semver "^5.3.0" + +babel-register@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" + dependencies: + babel-core "^6.26.0" + babel-runtime "^6.26.0" + core-js "^2.5.0" + home-or-tmp "^2.0.0" + lodash "^4.17.4" + mkdirp "^0.5.1" + source-map-support "^0.4.15" + +babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.0" + +babel-template@^6.24.1, babel-template@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" + dependencies: + babel-runtime "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + lodash "^4.17.4" + +babel-traverse@^6.24.1, babel-traverse@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" + dependencies: + babel-code-frame "^6.26.0" + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + debug "^2.6.8" + globals "^9.18.0" + invariant "^2.2.2" + lodash "^4.17.4" + +babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" + dependencies: + babel-runtime "^6.26.0" + esutils "^2.0.2" + lodash "^4.17.4" + to-fast-properties "^1.0.3" + +babylon@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" + +balanced-match@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.1.0.tgz#b504bd05869b39259dd0c5efc35d843176dccc4a" + +balanced-match@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +base64-js@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886" + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + +bcrypt-pbkdf@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" + dependencies: + tweetnacl "^0.14.3" + +big.js@^3.1.3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" + +binary-extensions@^1.0.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205" + +block-stream@*: + version "0.0.9" + resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" + dependencies: + inherits "~2.0.0" + +bluebird@^3.5.0: + version "3.5.1" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" + +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: + version "4.11.8" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" + +body-parser@1.18.2: + version "1.18.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454" + dependencies: + bytes "3.0.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.1" + http-errors "~1.6.2" + iconv-lite "0.4.19" + on-finished "~2.3.0" + qs "6.5.1" + raw-body "2.3.2" + type-is "~1.6.15" + +bonjour@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" + dependencies: + array-flatten "^2.1.0" + deep-equal "^1.0.1" + dns-equal "^1.0.0" + dns-txt "^2.0.2" + multicast-dns "^6.0.1" + multicast-dns-service-types "^1.1.0" + +boom@2.x.x: + version "2.10.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" + dependencies: + hoek "2.x.x" + +boom@4.x.x: + version "4.3.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31" + dependencies: + hoek "4.x.x" + +boom@5.x.x: + version "5.2.0" + resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02" + dependencies: + hoek "4.x.x" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^1.8.2: + version "1.8.5" + resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" + dependencies: + expand-range "^1.8.1" + preserve "^0.2.0" + repeat-element "^1.1.2" + +braces@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.0.tgz#a46941cb5fb492156b3d6a656e06c35364e3e66e" + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + define-property "^1.0.0" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +brorand@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.1.1" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.1.1.tgz#38b7ab55edb806ff2dcda1a7f1620773a477c49f" + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +browserify-cipher@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a" + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd" + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + +browserify-rsa@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" + dependencies: + bn.js "^4.1.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298" + dependencies: + bn.js "^4.1.1" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.2" + elliptic "^6.0.0" + inherits "^2.0.1" + parse-asn1 "^5.0.0" + +browserify-zlib@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" + dependencies: + pako "~1.0.5" + +browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6: + version "1.7.7" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9" + dependencies: + caniuse-db "^1.0.30000639" + electron-to-chromium "^1.2.7" + +browserslist@^2.0.0, browserslist@^2.1.2, browserslist@^2.11.3: + version "2.11.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.11.3.tgz#fe36167aed1bbcde4827ebfe71347a2cc70b99b2" + dependencies: + caniuse-lite "^1.0.30000792" + electron-to-chromium "^1.3.30" + +buffer-indexof@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" + +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + +buffer@^4.3.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +builtin-modules@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + +cacache@^10.0.1: + version "10.0.2" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.2.tgz#105a93a162bbedf3a25da42e1939ed99ffb145f8" + dependencies: + bluebird "^3.5.0" + chownr "^1.0.1" + glob "^7.1.2" + graceful-fs "^4.1.11" + lru-cache "^4.1.1" + mississippi "^1.3.0" + mkdirp "^0.5.1" + move-concurrently "^1.0.1" + promise-inflight "^1.0.1" + rimraf "^2.6.1" + ssri "^5.0.0" + unique-filename "^1.1.0" + y18n "^3.2.1" + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + +camelcase-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + dependencies: + camelcase "^2.0.0" + map-obj "^1.0.0" + +camelcase@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" + +camelcase@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + +camelcase@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" + +camelcase@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + +caniuse-api@^1.5.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-1.6.1.tgz#b534e7c734c4f81ec5fbe8aca2ad24354b962c6c" + dependencies: + browserslist "^1.3.6" + caniuse-db "^1.0.30000529" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-api@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-2.0.0.tgz#b1ddb5a5966b16f48dc4998444d4bbc6c7d9d834" + dependencies: + browserslist "^2.0.0" + caniuse-lite "^1.0.0" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: + version "1.0.30000808" + resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000808.tgz#30dfd83009d5704f02dffb37725068ed12a366bb" + +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000792, caniuse-lite@^1.0.30000805: + version "1.0.30000808" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000808.tgz#7d759b5518529ea08b6705a19e70dbf401628ffc" + +case-sensitive-paths-webpack-plugin@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.1.1.tgz#3d29ced8c1f124bf6f53846fb3f5894731fdc909" + +caseless@~0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + +center-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" + dependencies: + align-text "^0.1.3" + lazy-cache "^1.0.3" + +chalk@^1.1.1, chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chalk@^2.0.1, chalk@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.1.tgz#523fe2678aec7b04e8041909292fe8b17059b796" + dependencies: + ansi-styles "^3.2.0" + escape-string-regexp "^1.0.5" + supports-color "^5.2.0" + +chokidar@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" + dependencies: + anymatch "^1.3.0" + async-each "^1.0.0" + glob-parent "^2.0.0" + inherits "^2.0.1" + is-binary-path "^1.0.0" + is-glob "^2.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.0.0" + optionalDependencies: + fsevents "^1.0.0" + +chokidar@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.1.tgz#6e67e9998fe10e8f651e975ca62460456ff8e297" + dependencies: + anymatch "^2.0.0" + async-each "^1.0.0" + braces "^2.3.0" + glob-parent "^3.1.0" + inherits "^2.0.1" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + normalize-path "^2.1.1" + path-is-absolute "^1.0.0" + readdirp "^2.0.0" + upath "1.0.0" + optionalDependencies: + fsevents "^1.0.0" + +chownr@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" + +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +clap@^1.0.9: + version "1.2.3" + resolved "https://registry.yarnpkg.com/clap/-/clap-1.2.3.tgz#4f36745b32008492557f46412d66d50cb99bce51" + dependencies: + chalk "^1.1.3" + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + +cliui@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" + dependencies: + center-align "^0.1.1" + right-align "^0.1.1" + wordwrap "0.0.2" + +cliui@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + wrap-ansi "^2.0.0" + +clone-deep@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-0.3.0.tgz#348c61ae9cdbe0edfe053d91ff4cc521d790ede8" + dependencies: + for-own "^1.0.0" + is-plain-object "^2.0.1" + kind-of "^3.2.2" + shallow-clone "^0.1.2" + +clone@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.3.tgz#298d7e2231660f40c003c2ed3140decf3f53085f" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +coa@~1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.4.tgz#a9ef153660d6a86a8bdec0289a5c684d217432fd" + dependencies: + q "^1.1.2" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^1.3.0, color-convert@^1.8.2, color-convert@^1.9.0, color-convert@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed" + dependencies: + color-name "^1.1.1" + +color-name@^1.0.0, color-name@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + +color-string@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991" + dependencies: + color-name "^1.0.0" + +color-string@^1.4.0, color-string@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.2.tgz#26e45814bc3c9a7cbd6751648a41434514a773a9" + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^0.11.0: + version "0.11.4" + resolved "https://registry.yarnpkg.com/color/-/color-0.11.4.tgz#6d7b5c74fb65e841cd48792ad1ed5e07b904d764" + dependencies: + clone "^1.0.2" + color-convert "^1.3.0" + color-string "^0.3.0" + +color@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/color/-/color-1.0.3.tgz#e48e832d85f14ef694fb468811c2d5cfe729b55d" + dependencies: + color-convert "^1.8.2" + color-string "^1.4.0" + +color@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color/-/color-2.0.1.tgz#e4ed78a3c4603d0891eba5430b04b86314f4c839" + dependencies: + color-convert "^1.9.1" + color-string "^1.5.2" + +colormin@^1.0.5: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colormin/-/colormin-1.1.2.tgz#ea2f7420a72b96881a38aae59ec124a6f7298133" + dependencies: + color "^0.11.0" + css-color-names "0.0.4" + has "^1.0.1" + +colors@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" + +combined-stream@^1.0.5, combined-stream@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" + dependencies: + delayed-stream "~1.0.0" + +commander@^2.9.0, commander@~2.14.1: + version "2.14.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.14.1.tgz#2235123e37af8ca3c65df45b026dbd357b01b9aa" + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + +component-emitter@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" + +compressible@~2.0.11: + version "2.0.12" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.12.tgz#c59a5c99db76767e9876500e271ef63b3493bd66" + dependencies: + mime-db ">= 1.30.0 < 2" + +compression-webpack-plugin@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-1.1.6.tgz#450808fe143b4c5216a14f0c315c47bec3d83cec" + dependencies: + async "^2.4.1" + cacache "^10.0.1" + find-cache-dir "^1.0.0" + serialize-javascript "^1.4.0" + webpack-sources "^1.0.1" + +compression@^1.5.2: + version "1.7.1" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.1.tgz#eff2603efc2e22cf86f35d2eb93589f9875373db" + dependencies: + accepts "~1.3.4" + bytes "3.0.0" + compressible "~2.0.11" + debug "2.6.9" + on-headers "~1.0.1" + safe-buffer "5.1.1" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +concat-stream@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7" + dependencies: + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +connect-history-api-fallback@^1.3.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz#b06873934bc5e344fef611a196a6faae0aee015a" + +console-browserify@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" + dependencies: + date-now "^0.1.4" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + +constants-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + +content-disposition@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + +convert-source-map@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + +copy-concurrently@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" + dependencies: + aproba "^1.1.1" + fs-write-stream-atomic "^1.0.8" + iferr "^0.1.5" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.0" + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + +core-js@^2.4.0, core-js@^2.5.0: + version "2.5.3" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e" + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +cosmiconfig@^2.1.0, cosmiconfig@^2.1.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-2.2.2.tgz#6173cebd56fac042c1f4390edf7af6c07c7cb892" + dependencies: + is-directory "^0.3.1" + js-yaml "^3.4.3" + minimist "^1.2.0" + object-assign "^4.1.0" + os-homedir "^1.0.1" + parse-json "^2.2.0" + require-from-string "^1.1.0" + +create-ecdh@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d" + dependencies: + bn.js "^4.1.0" + elliptic "^6.0.0" + +create-hash@^1.1.0, create-hash@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd" + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + ripemd160 "^2.0.0" + sha.js "^2.4.0" + +create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: + version "1.1.6" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06" + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +cross-spawn@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" + dependencies: + lru-cache "^4.0.1" + which "^1.2.9" + +cross-spawn@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +cryptiles@2.x.x: + version "2.0.5" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" + dependencies: + boom "2.x.x" + +cryptiles@3.x.x: + version "3.1.2" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" + dependencies: + boom "5.x.x" + +crypto-browserify@^3.11.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + randomfill "^1.0.3" + +css-color-function@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/css-color-function/-/css-color-function-1.3.3.tgz#8ed24c2c0205073339fafa004bc8c141fccb282e" + dependencies: + balanced-match "0.1.0" + color "^0.11.0" + debug "^3.1.0" + rgb "~0.1.0" + +css-color-names@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" + +css-loader@^0.28.9: + version "0.28.9" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-0.28.9.tgz#68064b85f4e271d7ce4c48a58300928e535d1c95" + dependencies: + babel-code-frame "^6.26.0" + css-selector-tokenizer "^0.7.0" + cssnano "^3.10.0" + icss-utils "^2.1.0" + loader-utils "^1.0.2" + lodash.camelcase "^4.3.0" + object-assign "^4.1.1" + postcss "^5.0.6" + postcss-modules-extract-imports "^1.2.0" + postcss-modules-local-by-default "^1.2.0" + postcss-modules-scope "^1.1.0" + postcss-modules-values "^1.3.0" + postcss-value-parser "^3.3.0" + source-list-map "^2.0.0" + +css-selector-tokenizer@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz#e6988474ae8c953477bf5e7efecfceccd9cf4c86" + dependencies: + cssesc "^0.1.0" + fastparse "^1.1.1" + regexpu-core "^1.0.0" + +css-unit-converter@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.1.tgz#d9b9281adcfd8ced935bdbaba83786897f64e996" + +cssesc@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4" + +cssnano@^3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-3.10.0.tgz#4f38f6cea2b9b17fa01490f23f1dc68ea65c1c38" + dependencies: + autoprefixer "^6.3.1" + decamelize "^1.1.2" + defined "^1.0.0" + has "^1.0.1" + object-assign "^4.0.1" + postcss "^5.0.14" + postcss-calc "^5.2.0" + postcss-colormin "^2.1.8" + postcss-convert-values "^2.3.4" + postcss-discard-comments "^2.0.4" + postcss-discard-duplicates "^2.0.1" + postcss-discard-empty "^2.0.1" + postcss-discard-overridden "^0.1.1" + postcss-discard-unused "^2.2.1" + postcss-filter-plugins "^2.0.0" + postcss-merge-idents "^2.1.5" + postcss-merge-longhand "^2.0.1" + postcss-merge-rules "^2.0.3" + postcss-minify-font-values "^1.0.2" + postcss-minify-gradients "^1.0.1" + postcss-minify-params "^1.0.4" + postcss-minify-selectors "^2.0.4" + postcss-normalize-charset "^1.1.0" + postcss-normalize-url "^3.0.7" + postcss-ordered-values "^2.1.0" + postcss-reduce-idents "^2.2.2" + postcss-reduce-initial "^1.0.0" + postcss-reduce-transforms "^1.0.3" + postcss-svgo "^2.1.1" + postcss-unique-selectors "^2.0.2" + postcss-value-parser "^3.2.3" + postcss-zindex "^2.0.1" + +csso@~2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/csso/-/csso-2.3.2.tgz#ddd52c587033f49e94b71fc55569f252e8ff5f85" + dependencies: + clap "^1.0.9" + source-map "^0.5.3" + +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + dependencies: + array-find-index "^1.0.1" + +cyclist@~0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" + +d@1: + version "1.0.0" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" + dependencies: + es5-ext "^0.10.9" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + dependencies: + assert-plus "^1.0.0" + +date-now@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" + +debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.6, debug@^2.6.8: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + dependencies: + ms "2.0.0" + +debug@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + dependencies: + ms "2.0.0" + +decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + +deep-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + +deep-extend@~0.4.0: + version "0.4.2" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" + +define-properties@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94" + dependencies: + foreach "^2.0.5" + object-keys "^1.0.8" + +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + dependencies: + is-descriptor "^1.0.0" + +defined@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" + +del@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5" + dependencies: + globby "^6.1.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + p-map "^1.1.1" + pify "^3.0.0" + rimraf "^2.2.8" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + +depd@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" + +depd@~1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + +des.js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + +detect-indent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" + dependencies: + repeating "^2.0.0" + +detect-libc@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + +detect-node@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.3.tgz#a2033c09cc8e158d37748fbde7507832bd6ce127" + +diffie-hellman@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + +dns-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" + +dns-packet@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a" + dependencies: + ip "^1.1.0" + safe-buffer "^5.0.1" + +dns-txt@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" + dependencies: + buffer-indexof "^1.0.0" + +domain-browser@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" + +duplexify@^3.4.2, duplexify@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.3.tgz#8b5818800df92fd0125b27ab896491912858243e" + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" + dependencies: + jsbn "~0.1.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + +electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.30: + version "1.3.33" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.33.tgz#bf00703d62a7c65238136578c352d6c5c042a545" + +elliptic@^6.0.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" + dependencies: + bn.js "^4.4.0" + brorand "^1.0.1" + hash.js "^1.0.0" + hmac-drbg "^1.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.0" + +emojis-list@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" + +encodeurl@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + +end-of-stream@^1.0.0, end-of-stream@^1.1.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" + dependencies: + once "^1.4.0" + +enhanced-resolve@^3.4.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e" + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.4.0" + object-assign "^4.0.1" + tapable "^0.2.7" + +errno@^0.1.3, errno@^0.1.4: + version "0.1.6" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.6.tgz#c386ce8a6283f14fc09563b71560908c9bf53026" + dependencies: + prr "~1.0.1" + +error-ex@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.7.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.10.0.tgz#1ecb36c197842a00d8ee4c2dfd8646bb97d60864" + dependencies: + es-to-primitive "^1.1.1" + function-bind "^1.1.1" + has "^1.0.1" + is-callable "^1.1.3" + is-regex "^1.0.4" + +es-to-primitive@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.1.1.tgz#45355248a88979034b6792e19bb81f2b7975dd0d" + dependencies: + is-callable "^1.1.1" + is-date-object "^1.0.1" + is-symbol "^1.0.1" + +es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14: + version "0.10.38" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.38.tgz#fa7d40d65bbc9bb8a67e1d3f9cc656a00530eed3" + dependencies: + es6-iterator "~2.0.3" + es6-symbol "~3.1.1" + +es6-iterator@^2.0.1, es6-iterator@~2.0.1, es6-iterator@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-map@^0.1.3: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0" + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-set "~0.1.5" + es6-symbol "~3.1.1" + event-emitter "~0.3.5" + +es6-set@~0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1" + dependencies: + d "1" + es5-ext "~0.10.14" + es6-iterator "~2.0.1" + es6-symbol "3.1.1" + event-emitter "~0.3.5" + +es6-symbol@3.1.1, es6-symbol@^3.1.1, es6-symbol@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" + dependencies: + d "1" + es5-ext "~0.10.14" + +es6-weak-map@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f" + dependencies: + d "1" + es5-ext "^0.10.14" + es6-iterator "^2.0.1" + es6-symbol "^3.1.1" + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +escope@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/escope/-/escope-3.6.0.tgz#e01975e812781a163a6dadfdd80398dc64c889c3" + dependencies: + es6-map "^0.1.3" + es6-weak-map "^2.0.1" + esrecurse "^4.1.0" + estraverse "^4.1.1" + +esprima@^2.6.0: + version "2.7.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" + +esprima@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" + +esrecurse@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163" + dependencies: + estraverse "^4.1.0" + object-assign "^4.0.1" + +estraverse@^4.1.0, estraverse@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + +esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + +event-emitter@~0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + dependencies: + d "1" + es5-ext "~0.10.14" + +eventemitter3@1.x.x: + version "1.2.0" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" + +events@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + +eventsource@0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-0.1.6.tgz#0acede849ed7dd1ccc32c811bb11b944d4f29232" + dependencies: + original ">=0.0.5" + +evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + +execa@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +expand-brackets@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" + dependencies: + is-posix-bracket "^0.1.0" + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +expand-range@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" + dependencies: + fill-range "^2.1.0" + +express@^4.16.2: + version "4.16.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c" + dependencies: + accepts "~1.3.4" + array-flatten "1.1.1" + body-parser "1.18.2" + content-disposition "0.5.2" + content-type "~1.0.4" + cookie "0.3.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.1" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.1.0" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.2" + path-to-regexp "0.1.7" + proxy-addr "~2.0.2" + qs "6.5.1" + range-parser "~1.2.0" + safe-buffer "5.1.1" + send "0.16.1" + serve-static "1.13.1" + setprototypeof "1.1.0" + statuses "~1.3.1" + type-is "~1.6.15" + utils-merge "1.0.1" + vary "~1.1.2" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend@~3.0.0, extend@~3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" + +extglob@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" + dependencies: + is-extglob "^1.0.0" + +extglob@^2.0.2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +extract-text-webpack-plugin@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.2.tgz#5f043eaa02f9750a9258b78c0a6e0dc1408fb2f7" + dependencies: + async "^2.4.1" + loader-utils "^1.1.0" + schema-utils "^0.3.0" + webpack-sources "^1.0.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + +fast-deep-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + +fastparse@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" + +faye-websocket@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" + dependencies: + websocket-driver ">=0.5.1" + +faye-websocket@~0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.1.tgz#f0efe18c4f56e4f40afc7e06c719fd5ee6188f38" + dependencies: + websocket-driver ">=0.5.1" + +file-loader@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-1.1.6.tgz#7b9a8f2c58f00a77fddf49e940f7ac978a3ea0e8" + dependencies: + loader-utils "^1.0.2" + schema-utils "^0.3.0" + +filename-regex@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" + +fill-range@^2.1.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723" + dependencies: + is-number "^2.1.0" + isobject "^2.0.0" + randomatic "^1.1.3" + repeat-element "^1.1.2" + repeat-string "^1.5.2" + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +finalhandler@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5" + dependencies: + debug "2.6.9" + encodeurl "~1.0.1" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.2" + statuses "~1.3.1" + unpipe "~1.0.0" + +find-cache-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f" + dependencies: + commondir "^1.0.1" + make-dir "^1.0.0" + pkg-dir "^2.0.0" + +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +find-up@^2.0.0, find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + dependencies: + locate-path "^2.0.0" + +flatten@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" + +flush-write-stream@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.2.tgz#c81b90d8746766f1a609a46809946c45dd8ae417" + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.4" + +for-in@^0.1.3: + version "0.1.8" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" + +for-in@^1.0.1, for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + +for-own@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" + dependencies: + for-in "^1.0.1" + +for-own@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" + dependencies: + for-in "^1.0.1" + +foreach@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@~2.1.1: + version "2.1.4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + +form-data@~2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + dependencies: + map-cache "^0.2.2" + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + +from2@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + +fs-extra@^0.30.0: + version "0.30.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + klaw "^1.0.0" + path-is-absolute "^1.0.0" + rimraf "^2.2.8" + +fs-write-stream-atomic@^1.0.8: + version "1.0.10" + resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" + dependencies: + graceful-fs "^4.1.2" + iferr "^0.1.5" + imurmurhash "^0.1.4" + readable-stream "1 || 2" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +fsevents@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.3.tgz#11f82318f5fe7bb2cd22965a108e9306208216d8" + dependencies: + nan "^2.3.0" + node-pre-gyp "^0.6.39" + +fstream-ignore@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105" + dependencies: + fstream "^1.0.0" + inherits "2" + minimatch "^3.0.0" + +fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2: + version "1.0.11" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + +function-bind@^1.0.2, function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +gaze@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.2.tgz#847224677adb8870d679257ed3388fdb61e40105" + dependencies: + globule "^1.0.0" + +generate-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74" + +generate-object-property@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" + dependencies: + is-property "^1.0.0" + +get-caller-file@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" + +get-stdin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + dependencies: + assert-plus "^1.0.0" + +glob-base@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" + dependencies: + glob-parent "^2.0.0" + is-glob "^2.0.0" + +glob-parent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" + dependencies: + is-glob "^2.0.0" + +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + +glob@^6.0.4: + version "6.0.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.2, glob@~7.1.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^9.18.0: + version "9.18.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" + +globby@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" + dependencies: + array-union "^1.0.1" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +globule@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.0.tgz#1dc49c6822dd9e8a2fa00ba2a295006e8664bd09" + dependencies: + glob "~7.1.1" + lodash "~4.17.4" + minimatch "~3.0.2" + +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +handle-thing@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4" + +har-schema@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + +har-validator@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" + dependencies: + chalk "^1.1.1" + commander "^2.9.0" + is-my-json-valid "^2.12.4" + pinkie-promise "^2.0.0" + +har-validator@~4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a" + dependencies: + ajv "^4.9.1" + har-schema "^1.0.5" + +har-validator@~5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd" + dependencies: + ajv "^5.1.0" + har-schema "^2.0.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +has-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" + +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +has@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" + dependencies: + function-bind "^1.0.2" + +hash-base@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1" + dependencies: + inherits "^2.0.1" + +hash-base@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846" + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.0" + +hawk@3.1.3, hawk@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" + dependencies: + boom "2.x.x" + cryptiles "2.x.x" + hoek "2.x.x" + sntp "1.x.x" + +hawk@~6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038" + dependencies: + boom "4.x.x" + cryptiles "3.x.x" + hoek "4.x.x" + sntp "2.x.x" + +hmac-drbg@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +hoek@2.x.x: + version "2.16.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" + +hoek@4.x.x: + version "4.2.0" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" + +home-or-tmp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.1" + +hosted-git-info@^2.1.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +html-comment-regex@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e" + +html-entities@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + +http-errors@1.6.2, http-errors@~1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" + dependencies: + depd "1.1.1" + inherits "2.0.3" + setprototypeof "1.0.3" + statuses ">= 1.3.1 < 2" + +http-parser-js@>=0.4.0: + version "0.4.10" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4" + +http-proxy-middleware@~0.17.4: + version "0.17.4" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz#642e8848851d66f09d4f124912846dbaeb41b833" + dependencies: + http-proxy "^1.16.2" + is-glob "^3.1.0" + lodash "^4.17.2" + micromatch "^2.3.11" + +http-proxy@^1.16.2: + version "1.16.2" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.16.2.tgz#06dff292952bf64dbe8471fa9df73066d4f37742" + dependencies: + eventemitter3 "1.x.x" + requires-port "1.x.x" + +http-signature@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" + dependencies: + assert-plus "^0.2.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" + +iconv-lite@0.4.19: + version "0.4.19" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" + +icss-replace-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" + +icss-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-2.1.0.tgz#83f0a0ec378bf3246178b6c2ad9136f135b1c962" + dependencies: + postcss "^6.0.1" + +ieee754@^1.1.4: + version "1.1.8" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" + +iferr@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" + +import-local@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-1.0.0.tgz#5e4ffdc03f4fe6c009c6729beb29631c2f8227bc" + dependencies: + pkg-dir "^2.0.0" + resolve-cwd "^2.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + +in-publish@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51" + +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + dependencies: + repeating "^2.0.0" + +indexes-of@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" + +indexof@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + +ini@~1.3.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + +internal-ip@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-1.2.0.tgz#ae9fbf93b984878785d50a8de1b356956058cf5c" + dependencies: + meow "^3.3.0" + +interpret@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" + +invariant@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" + dependencies: + loose-envify "^1.0.0" + +invert-kv@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" + +ip@^1.1.0, ip@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" + +ipaddr.js@1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.5.2.tgz#d4b505bde9946987ccf0fc58d9010ff9607e3fa0" + +is-absolute-url@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + dependencies: + kind-of "^6.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + +is-arrayish@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.1.tgz#c2dfc386abaa0c3e33c48db3fe87059e69065efd" + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + dependencies: + binary-extensions "^1.0.0" + +is-buffer@^1.0.2, is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + +is-builtin-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" + dependencies: + builtin-modules "^1.0.0" + +is-callable@^1.1.1, is-callable@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2" + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + dependencies: + kind-of "^6.0.0" + +is-date-object@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-directory@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + +is-dotfile@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" + +is-equal-shallow@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" + dependencies: + is-primitive "^2.0.0" + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" + +is-extglob@^2.1.0, is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + +is-finite@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-glob@^2.0.0, is-glob@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" + dependencies: + is-extglob "^1.0.0" + +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + dependencies: + is-extglob "^2.1.0" + +is-glob@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0" + dependencies: + is-extglob "^2.1.1" + +is-my-json-valid@^2.12.4: + version "2.17.1" + resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.17.1.tgz#3da98914a70a22f0a8563ef1511a246c6fc55471" + dependencies: + generate-function "^2.0.0" + generate-object-property "^1.1.0" + jsonpointer "^4.0.0" + xtend "^4.0.0" + +is-number@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" + dependencies: + kind-of "^3.0.2" + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + dependencies: + kind-of "^3.0.2" + +is-odd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-1.0.0.tgz#3b8a932eb028b3775c39bb09e91767accdb69088" + dependencies: + is-number "^3.0.0" + +is-path-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" + +is-path-in-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" + dependencies: + is-path-inside "^1.0.0" + +is-path-inside@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" + dependencies: + path-is-inside "^1.0.1" + +is-plain-obj@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + +is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + dependencies: + isobject "^3.0.1" + +is-posix-bracket@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" + +is-primitive@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" + +is-property@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + +is-regex@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" + dependencies: + has "^1.0.1" + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +is-svg@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-2.1.0.tgz#cf61090da0d9efbcab8722deba6f032208dbb0e9" + dependencies: + html-comment-regex "^1.1.0" + +is-symbol@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.1.tgz#3cc59f00025194b6ab2e38dbae6689256b660572" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + +isnumeric@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/isnumeric/-/isnumeric-0.2.0.tgz#a2347ba360de19e33d0ffd590fddf7755cbf2e64" + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +js-base64@^2.1.8, js-base64@^2.1.9: + version "2.4.3" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.3.tgz#2e545ec2b0f2957f41356510205214e98fad6582" + +js-tokens@^3.0.0, js-tokens@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + +js-yaml@^3.10.0, js-yaml@^3.4.3: + version "3.10.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@~3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80" + dependencies: + argparse "^1.0.7" + esprima "^2.6.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + +jsesc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + +json-loader@^0.5.4: + version "0.5.7" + resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d" + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stable-stringify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" + dependencies: + jsonify "~0.0.0" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +json3@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" + +json5@^0.5.0, json5@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + +jsonfile@^2.1.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonify@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" + +jsonpointer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +killable@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.0.tgz#da8b84bd47de5395878f95d64d02f2449fe05e6b" + +kind-of@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-2.0.1.tgz#018ec7a4ce7e3a86cb9141be519d24c8faa981b5" + dependencies: + is-buffer "^1.0.2" + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0, kind-of@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0, kind-of@^5.0.2: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" + +klaw@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" + optionalDependencies: + graceful-fs "^4.1.9" + +lazy-cache@^0.2.3: + version "0.2.7" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-0.2.7.tgz#7feddf2dcb6edb77d11ef1d117ab5ffdf0ab1b65" + +lazy-cache@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" + +lazy-cache@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-2.0.2.tgz#b9190a4f913354694840859f8a8f7084d8822264" + dependencies: + set-getter "^0.1.0" + +lcid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" + dependencies: + invert-kv "^1.0.0" + +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +load-json-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + +loader-runner@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" + +loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +lodash._reinterpolate@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + +lodash.assign@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + +lodash.clonedeep@^4.3.2: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + +lodash.mergewith@^4.6.0: + version "4.6.1" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" + +lodash.tail@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" + +lodash.template@^4.2.4: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0" + dependencies: + lodash._reinterpolate "~3.0.0" + lodash.templatesettings "^4.0.0" + +lodash.templatesettings@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz#2b4d4e95ba440d915ff08bc899e4553666713316" + dependencies: + lodash._reinterpolate "~3.0.0" + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + +lodash@3.x: + version "3.10.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" + +"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, lodash@~4.17.4: + version "4.17.5" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" + +loglevel@^1.4.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa" + +longest@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" + +loose-envify@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" + dependencies: + js-tokens "^3.0.0" + +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + +lru-cache@^4.0.1, lru-cache@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +macaddress@^0.2.8: + version "0.2.8" + resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12" + +make-dir@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.1.0.tgz#19b4369fe48c116f53c2af95ad102c0e39e85d51" + dependencies: + pify "^3.0.0" + +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + +map-obj@^1.0.0, map-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + dependencies: + object-visit "^1.0.0" + +math-expression-evaluator@^1.2.14: + version "1.2.17" + resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz#de819fdbcd84dccd8fae59c6aeb79615b9d266ac" + +md5.js@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d" + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + +mem@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" + dependencies: + mimic-fn "^1.0.0" + +memory-fs@^0.4.0, memory-fs@~0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +meow@^3.3.0, meow@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + dependencies: + camelcase-keys "^2.0.0" + decamelize "^1.1.2" + loud-rejection "^1.0.0" + map-obj "^1.0.1" + minimist "^1.1.3" + normalize-package-data "^2.3.4" + object-assign "^4.0.1" + read-pkg-up "^1.0.1" + redent "^1.0.0" + trim-newlines "^1.0.0" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + +micromatch@^2.1.5, micromatch@^2.3.11: + version "2.3.11" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" + dependencies: + arr-diff "^2.0.0" + array-unique "^0.2.1" + braces "^1.8.2" + expand-brackets "^0.1.4" + extglob "^0.3.1" + filename-regex "^2.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.1" + kind-of "^3.0.2" + normalize-path "^2.0.1" + object.omit "^2.0.0" + parse-glob "^3.0.4" + regex-cache "^0.4.2" + +micromatch@^3.1.4: + version "3.1.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.5.tgz#d05e168c206472dfbca985bfef4f57797b4cd4ba" + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.0" + define-property "^1.0.0" + extend-shallow "^2.0.1" + extglob "^2.0.2" + fragment-cache "^0.2.1" + kind-of "^6.0.0" + nanomatch "^1.2.5" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +miller-rabin@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + +"mime-db@>= 1.30.0 < 2": + version "1.32.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.32.0.tgz#485b3848b01a3cda5f968b4882c0771e58e09414" + +mime-db@~1.30.0: + version "1.30.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" + +mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.17, mime-types@~2.1.7: + version "2.1.17" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" + dependencies: + mime-db "~1.30.0" + +mime@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" + +mime@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + +mimic-fn@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" + +minimalistic-assert@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" + +minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + +"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +minimist@^1.1.3, minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + +mississippi@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-1.3.1.tgz#2a8bb465e86550ac8b36a7b6f45599171d78671e" + dependencies: + concat-stream "^1.5.0" + duplexify "^3.4.2" + end-of-stream "^1.1.0" + flush-write-stream "^1.0.0" + from2 "^2.1.0" + parallel-transform "^1.1.0" + pump "^1.0.0" + pumpify "^1.3.3" + stream-each "^1.1.0" + through2 "^2.0.0" + +mixin-deep@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe" + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +mixin-object@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e" + dependencies: + for-in "^0.1.3" + is-extendable "^0.1.1" + +mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +move-concurrently@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" + dependencies: + aproba "^1.1.1" + copy-concurrently "^1.0.0" + fs-write-stream-atomic "^1.0.8" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.3" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +multicast-dns-service-types@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" + +multicast-dns@^6.0.1: + version "6.2.3" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229" + dependencies: + dns-packet "^1.3.1" + thunky "^1.0.2" + +nan@^2.3.0, nan@^2.3.2: + version "2.8.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a" + +nanomatch@^1.2.5: + version "1.2.7" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.7.tgz#53cd4aa109ff68b7f869591fdc9d10daeeea3e79" + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^1.0.0" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + is-odd "^1.0.0" + kind-of "^5.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +negotiator@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" + +node-forge@0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.1.tgz#9da611ea08982f4b94206b3beb4cc9665f20c300" + +node-gyp@^3.3.1: + version "3.6.2" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.2.tgz#9bfbe54562286284838e750eac05295853fa1c60" + dependencies: + fstream "^1.0.0" + glob "^7.0.3" + graceful-fs "^4.1.2" + minimatch "^3.0.2" + mkdirp "^0.5.0" + nopt "2 || 3" + npmlog "0 || 1 || 2 || 3 || 4" + osenv "0" + request "2" + rimraf "2" + semver "~5.3.0" + tar "^2.0.0" + which "1" + +node-libs-browser@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df" + dependencies: + assert "^1.1.1" + browserify-zlib "^0.2.0" + buffer "^4.3.0" + console-browserify "^1.1.0" + constants-browserify "^1.0.0" + crypto-browserify "^3.11.0" + domain-browser "^1.1.1" + events "^1.0.0" + https-browserify "^1.0.0" + os-browserify "^0.3.0" + path-browserify "0.0.0" + process "^0.11.10" + punycode "^1.2.4" + querystring-es3 "^0.2.0" + readable-stream "^2.3.3" + stream-browserify "^2.0.1" + stream-http "^2.7.2" + string_decoder "^1.0.0" + timers-browserify "^2.0.4" + tty-browserify "0.0.0" + url "^0.11.0" + util "^0.10.3" + vm-browserify "0.0.4" + +node-pre-gyp@^0.6.39: + version "0.6.39" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649" + dependencies: + detect-libc "^1.0.2" + hawk "3.1.3" + mkdirp "^0.5.1" + nopt "^4.0.1" + npmlog "^4.0.2" + rc "^1.1.7" + request "2.81.0" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^2.2.1" + tar-pack "^3.4.0" + +node-sass@^4.7.2: + version "4.7.2" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.7.2.tgz#9366778ba1469eb01438a9e8592f4262bcb6794e" + dependencies: + async-foreach "^0.1.3" + chalk "^1.1.1" + cross-spawn "^3.0.0" + gaze "^1.0.0" + get-stdin "^4.0.1" + glob "^7.0.3" + in-publish "^2.0.0" + lodash.assign "^4.2.0" + lodash.clonedeep "^4.3.2" + lodash.mergewith "^4.6.0" + meow "^3.7.0" + mkdirp "^0.5.1" + nan "^2.3.2" + node-gyp "^3.3.1" + npmlog "^4.0.0" + request "~2.79.0" + sass-graph "^2.2.4" + stdout-stream "^1.4.0" + "true-case-path" "^1.0.2" + +"nopt@2 || 3": + version "3.0.6" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" + dependencies: + abbrev "1" + +nopt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" + dependencies: + abbrev "1" + osenv "^0.1.4" + +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: + version "2.4.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" + dependencies: + hosted-git-info "^2.1.4" + is-builtin-module "^1.0.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + dependencies: + remove-trailing-separator "^1.0.1" + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + +normalize-url@^1.4.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" + dependencies: + object-assign "^4.0.1" + prepend-http "^1.0.0" + query-string "^4.1.0" + sort-keys "^1.0.0" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + dependencies: + path-key "^2.0.0" + +"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +num2fraction@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +oauth-sign@~0.8.1, oauth-sign@~0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + +object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-keys@^1.0.8: + version "1.0.11" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + dependencies: + isobject "^3.0.0" + +object.omit@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" + dependencies: + for-own "^0.1.4" + is-extendable "^0.1.1" + +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + dependencies: + isobject "^3.0.1" + +obuf@^1.0.0, obuf@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.1.tgz#104124b6c602c6796881a042541d36db43a5264e" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" + +once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +onecolor@^3.0.4: + version "3.0.5" + resolved "https://registry.yarnpkg.com/onecolor/-/onecolor-3.0.5.tgz#36eff32201379efdf1180fb445e51a8e2425f9f6" + +opn@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/opn/-/opn-5.2.0.tgz#71fdf934d6827d676cecbea1531f95d354641225" + dependencies: + is-wsl "^1.1.0" + +original@>=0.0.5: + version "1.0.0" + resolved "https://registry.yarnpkg.com/original/-/original-1.0.0.tgz#9147f93fa1696d04be61e01bd50baeaca656bd3b" + dependencies: + url-parse "1.0.x" + +os-browserify@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" + +os-homedir@^1.0.0, os-homedir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +os-locale@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" + dependencies: + lcid "^1.0.0" + +os-locale@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" + dependencies: + execa "^0.7.0" + lcid "^1.0.0" + mem "^1.1.0" + +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +osenv@0, osenv@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + +p-limit@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c" + dependencies: + p-try "^1.0.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + dependencies: + p-limit "^1.1.0" + +p-map@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + +pako@~1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" + +parallel-transform@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06" + dependencies: + cyclist "~0.2.2" + inherits "^2.0.3" + readable-stream "^2.1.5" + +parse-asn1@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712" + dependencies: + asn1.js "^4.0.0" + browserify-aes "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + +parse-glob@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" + dependencies: + glob-base "^0.3.0" + is-dotfile "^1.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.0" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + dependencies: + error-ex "^1.2.0" + +parseurl@~1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + +path-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" + +path-complete-extname@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/path-complete-extname/-/path-complete-extname-0.1.0.tgz#c454702669f31452f8193aa6168915fa31692f4a" + +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + dependencies: + pinkie-promise "^2.0.0" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + +path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +path-is-inside@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + +path-key@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + +path-parse@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + dependencies: + pify "^2.0.0" + +pbkdf2@^3.0.3: + version "3.0.14" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.14.tgz#a35e13c64799b06ce15320f459c230e68e73bade" + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +performance-now@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + +pify@^2.0.0, pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + +pixrem@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pixrem/-/pixrem-4.0.1.tgz#2da4a1de6ec4423c5fc3794e930b81d4490ec686" + dependencies: + browserslist "^2.0.0" + postcss "^6.0.0" + reduce-css-calc "^1.2.7" + +pkg-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" + dependencies: + find-up "^2.1.0" + +pleeease-filters@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pleeease-filters/-/pleeease-filters-4.0.0.tgz#6632b2fb05648d2758d865384fbced79e1ccaec7" + dependencies: + onecolor "^3.0.4" + postcss "^6.0.1" + +portfinder@^1.0.9: + version "1.0.13" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9" + dependencies: + async "^1.5.2" + debug "^2.2.0" + mkdirp "0.5.x" + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + +postcss-apply@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/postcss-apply/-/postcss-apply-0.8.0.tgz#14e544bbb5cb6f1c1e048857965d79ae066b1343" + dependencies: + babel-runtime "^6.23.0" + balanced-match "^0.4.2" + postcss "^6.0.0" + +postcss-attribute-case-insensitive@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-2.0.0.tgz#94dc422c8f90997f16bd33a3654bbbec084963b4" + dependencies: + postcss "^6.0.0" + postcss-selector-parser "^2.2.3" + +postcss-calc@^5.2.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-5.3.1.tgz#77bae7ca928ad85716e2fda42f261bf7c1d65b5e" + dependencies: + postcss "^5.0.2" + postcss-message-helpers "^2.0.0" + reduce-css-calc "^1.2.6" + +postcss-calc@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-6.0.1.tgz#3d24171bbf6e7629d422a436ebfe6dd9511f4330" + dependencies: + css-unit-converter "^1.1.1" + postcss "^6.0.0" + postcss-selector-parser "^2.2.2" + reduce-css-calc "^2.0.0" + +postcss-color-function@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-color-function/-/postcss-color-function-4.0.1.tgz#402b3f2cebc3f6947e618fb6be3654fbecef6444" + dependencies: + css-color-function "~1.3.3" + postcss "^6.0.1" + postcss-message-helpers "^2.0.0" + postcss-value-parser "^3.3.0" + +postcss-color-gray@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-color-gray/-/postcss-color-gray-4.1.0.tgz#e5581ed57eaa826fb652ca11b1e2b7b136a9f9df" + dependencies: + color "^2.0.1" + postcss "^6.0.14" + postcss-message-helpers "^2.0.0" + reduce-function-call "^1.0.2" + +postcss-color-hex-alpha@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-3.0.0.tgz#1e53e6c8acb237955e8fd08b7ecdb1b8b8309f95" + dependencies: + color "^1.0.3" + postcss "^6.0.1" + postcss-message-helpers "^2.0.0" + +postcss-color-hsl@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-hsl/-/postcss-color-hsl-2.0.0.tgz#12703666fa310430e3f30a454dac1386317d5844" + dependencies: + postcss "^6.0.1" + postcss-value-parser "^3.3.0" + units-css "^0.4.0" + +postcss-color-hwb@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-hwb/-/postcss-color-hwb-3.0.0.tgz#3402b19ef4d8497540c1fb5072be9863ca95571e" + dependencies: + color "^1.0.3" + postcss "^6.0.1" + postcss-message-helpers "^2.0.0" + reduce-function-call "^1.0.2" + +postcss-color-rebeccapurple@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-3.0.0.tgz#eebaf03d363b4300b96792bd3081c19ed66513d3" + dependencies: + postcss "^6.0.1" + postcss-value-parser "^3.3.0" + +postcss-color-rgb@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-rgb/-/postcss-color-rgb-2.0.0.tgz#14539c8a7131494b482e0dd1cc265ff6514b5263" + dependencies: + postcss "^6.0.1" + postcss-value-parser "^3.3.0" + +postcss-color-rgba-fallback@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-rgba-fallback/-/postcss-color-rgba-fallback-3.0.0.tgz#37d5c9353a07a09270912a82606bb42a0d702c04" + dependencies: + postcss "^6.0.6" + postcss-value-parser "^3.3.0" + rgb-hex "^2.1.0" + +postcss-colormin@^2.1.8: + version "2.2.2" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-2.2.2.tgz#6631417d5f0e909a3d7ec26b24c8a8d1e4f96e4b" + dependencies: + colormin "^1.0.5" + postcss "^5.0.13" + postcss-value-parser "^3.2.3" + +postcss-convert-values@^2.3.4: + version "2.6.1" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz#bbd8593c5c1fd2e3d1c322bb925dcae8dae4d62d" + dependencies: + postcss "^5.0.11" + postcss-value-parser "^3.1.2" + +postcss-cssnext@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/postcss-cssnext/-/postcss-cssnext-3.1.0.tgz#927dc29341a938254cde38ea60a923b9dfedead9" + dependencies: + autoprefixer "^7.1.1" + caniuse-api "^2.0.0" + chalk "^2.0.1" + pixrem "^4.0.0" + pleeease-filters "^4.0.0" + postcss "^6.0.5" + postcss-apply "^0.8.0" + postcss-attribute-case-insensitive "^2.0.0" + postcss-calc "^6.0.0" + postcss-color-function "^4.0.0" + postcss-color-gray "^4.0.0" + postcss-color-hex-alpha "^3.0.0" + postcss-color-hsl "^2.0.0" + postcss-color-hwb "^3.0.0" + postcss-color-rebeccapurple "^3.0.0" + postcss-color-rgb "^2.0.0" + postcss-color-rgba-fallback "^3.0.0" + postcss-custom-media "^6.0.0" + postcss-custom-properties "^6.1.0" + postcss-custom-selectors "^4.0.1" + postcss-font-family-system-ui "^3.0.0" + postcss-font-variant "^3.0.0" + postcss-image-set-polyfill "^0.3.5" + postcss-initial "^2.0.0" + postcss-media-minmax "^3.0.0" + postcss-nesting "^4.0.1" + postcss-pseudo-class-any-link "^4.0.0" + postcss-pseudoelements "^5.0.0" + postcss-replace-overflow-wrap "^2.0.0" + postcss-selector-matches "^3.0.1" + postcss-selector-not "^3.0.1" + +postcss-custom-media@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-6.0.0.tgz#be532784110ecb295044fb5395a18006eb21a737" + dependencies: + postcss "^6.0.1" + +postcss-custom-properties@^6.1.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-6.2.0.tgz#5d929a7f06e9b84e0f11334194c0ba9a30acfbe9" + dependencies: + balanced-match "^1.0.0" + postcss "^6.0.13" + +postcss-custom-selectors@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-4.0.1.tgz#781382f94c52e727ef5ca4776ea2adf49a611382" + dependencies: + postcss "^6.0.1" + postcss-selector-matches "^3.0.0" + +postcss-discard-comments@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz#befe89fafd5b3dace5ccce51b76b81514be00e3d" + dependencies: + postcss "^5.0.14" + +postcss-discard-duplicates@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-2.1.0.tgz#b9abf27b88ac188158a5eb12abcae20263b91932" + dependencies: + postcss "^5.0.4" + +postcss-discard-empty@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz#d2b4bd9d5ced5ebd8dcade7640c7d7cd7f4f92b5" + dependencies: + postcss "^5.0.14" + +postcss-discard-overridden@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz#8b1eaf554f686fb288cd874c55667b0aa3668d58" + dependencies: + postcss "^5.0.16" + +postcss-discard-unused@^2.2.1: + version "2.2.3" + resolved "https://registry.yarnpkg.com/postcss-discard-unused/-/postcss-discard-unused-2.2.3.tgz#bce30b2cc591ffc634322b5fb3464b6d934f4433" + dependencies: + postcss "^5.0.14" + uniqs "^2.0.0" + +postcss-filter-plugins@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-filter-plugins/-/postcss-filter-plugins-2.0.2.tgz#6d85862534d735ac420e4a85806e1f5d4286d84c" + dependencies: + postcss "^5.0.4" + uniqid "^4.0.0" + +postcss-font-family-system-ui@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-font-family-system-ui/-/postcss-font-family-system-ui-3.0.0.tgz#675fe7a9e029669f05f8dba2e44c2225ede80623" + dependencies: + postcss "^6.0" + +postcss-font-variant@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-3.0.0.tgz#08ccc88f6050ba82ed8ef2cc76c0c6a6b41f183e" + dependencies: + postcss "^6.0.1" + +postcss-image-set-polyfill@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/postcss-image-set-polyfill/-/postcss-image-set-polyfill-0.3.5.tgz#0f193413700cf1f82bd39066ef016d65a4a18181" + dependencies: + postcss "^6.0.1" + postcss-media-query-parser "^0.2.3" + +postcss-import@^11.0.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-11.1.0.tgz#55c9362c9192994ec68865d224419df1db2981f0" + dependencies: + postcss "^6.0.1" + postcss-value-parser "^3.2.3" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-initial@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-initial/-/postcss-initial-2.0.0.tgz#72715f7336e0bb79351d99ee65c4a253a8441ba4" + dependencies: + lodash.template "^4.2.4" + postcss "^6.0.1" + +postcss-load-config@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-1.2.0.tgz#539e9afc9ddc8620121ebf9d8c3673e0ce50d28a" + dependencies: + cosmiconfig "^2.1.0" + object-assign "^4.1.0" + postcss-load-options "^1.2.0" + postcss-load-plugins "^2.3.0" + +postcss-load-options@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-load-options/-/postcss-load-options-1.2.0.tgz#b098b1559ddac2df04bc0bb375f99a5cfe2b6d8c" + dependencies: + cosmiconfig "^2.1.0" + object-assign "^4.1.0" + +postcss-load-plugins@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz#745768116599aca2f009fad426b00175049d8d92" + dependencies: + cosmiconfig "^2.1.1" + object-assign "^4.1.0" + +postcss-loader@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-2.1.0.tgz#038c2d6d59753fef4667827fd3ae03f5dc5e6a7a" + dependencies: + loader-utils "^1.1.0" + postcss "^6.0.0" + postcss-load-config "^1.2.0" + schema-utils "^0.4.0" + +postcss-media-minmax@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-3.0.0.tgz#675256037a43ef40bc4f0760bfd06d4dc69d48d2" + dependencies: + postcss "^6.0.1" + +postcss-media-query-parser@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244" + +postcss-merge-idents@^2.1.5: + version "2.1.7" + resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-2.1.7.tgz#4c5530313c08e1d5b3bbf3d2bbc747e278eea270" + dependencies: + has "^1.0.1" + postcss "^5.0.10" + postcss-value-parser "^3.1.1" + +postcss-merge-longhand@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-2.0.2.tgz#23d90cd127b0a77994915332739034a1a4f3d658" + dependencies: + postcss "^5.0.4" + +postcss-merge-rules@^2.0.3: + version "2.1.2" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-2.1.2.tgz#d1df5dfaa7b1acc3be553f0e9e10e87c61b5f721" + dependencies: + browserslist "^1.5.2" + caniuse-api "^1.5.2" + postcss "^5.0.4" + postcss-selector-parser "^2.2.2" + vendors "^1.0.0" + +postcss-message-helpers@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz#a4f2f4fab6e4fe002f0aed000478cdf52f9ba60e" + +postcss-minify-font-values@^1.0.2: + version "1.0.5" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz#4b58edb56641eba7c8474ab3526cafd7bbdecb69" + dependencies: + object-assign "^4.0.1" + postcss "^5.0.4" + postcss-value-parser "^3.0.2" + +postcss-minify-gradients@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-1.0.5.tgz#5dbda11373703f83cfb4a3ea3881d8d75ff5e6e1" + dependencies: + postcss "^5.0.12" + postcss-value-parser "^3.3.0" + +postcss-minify-params@^1.0.4: + version "1.2.2" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-1.2.2.tgz#ad2ce071373b943b3d930a3fa59a358c28d6f1f3" + dependencies: + alphanum-sort "^1.0.1" + postcss "^5.0.2" + postcss-value-parser "^3.0.2" + uniqs "^2.0.0" + +postcss-minify-selectors@^2.0.4: + version "2.1.1" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-2.1.1.tgz#b2c6a98c0072cf91b932d1a496508114311735bf" + dependencies: + alphanum-sort "^1.0.2" + has "^1.0.1" + postcss "^5.0.14" + postcss-selector-parser "^2.0.0" + +postcss-modules-extract-imports@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.0.tgz#66140ecece38ef06bf0d3e355d69bf59d141ea85" + dependencies: + postcss "^6.0.1" + +postcss-modules-local-by-default@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz#f7d80c398c5a393fa7964466bd19500a7d61c069" + dependencies: + css-selector-tokenizer "^0.7.0" + postcss "^6.0.1" + +postcss-modules-scope@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz#d6ea64994c79f97b62a72b426fbe6056a194bb90" + dependencies: + css-selector-tokenizer "^0.7.0" + postcss "^6.0.1" + +postcss-modules-values@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz#ecffa9d7e192518389f42ad0e83f72aec456ea20" + dependencies: + icss-replace-symbols "^1.1.0" + postcss "^6.0.1" + +postcss-nesting@^4.0.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-4.2.1.tgz#0483bce338b3f0828ced90ff530b29b98b00300d" + dependencies: + postcss "^6.0.11" + +postcss-normalize-charset@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-1.1.1.tgz#ef9ee71212d7fe759c78ed162f61ed62b5cb93f1" + dependencies: + postcss "^5.0.5" + +postcss-normalize-url@^3.0.7: + version "3.0.8" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-3.0.8.tgz#108f74b3f2fcdaf891a2ffa3ea4592279fc78222" + dependencies: + is-absolute-url "^2.0.0" + normalize-url "^1.4.0" + postcss "^5.0.14" + postcss-value-parser "^3.2.3" + +postcss-ordered-values@^2.1.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-2.2.3.tgz#eec6c2a67b6c412a8db2042e77fe8da43f95c11d" + dependencies: + postcss "^5.0.4" + postcss-value-parser "^3.0.1" + +postcss-pseudo-class-any-link@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-4.0.0.tgz#9152a0613d3450720513e8892854bae42d0ee68e" + dependencies: + postcss "^6.0.1" + postcss-selector-parser "^2.2.3" + +postcss-pseudoelements@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-pseudoelements/-/postcss-pseudoelements-5.0.0.tgz#eef194e8d524645ca520a949e95e518e812402cb" + dependencies: + postcss "^6.0.0" + +postcss-reduce-idents@^2.2.2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-2.4.0.tgz#c2c6d20cc958284f6abfbe63f7609bf409059ad3" + dependencies: + postcss "^5.0.4" + postcss-value-parser "^3.0.2" + +postcss-reduce-initial@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-1.0.1.tgz#68f80695f045d08263a879ad240df8dd64f644ea" + dependencies: + postcss "^5.0.4" + +postcss-reduce-transforms@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.4.tgz#ff76f4d8212437b31c298a42d2e1444025771ae1" + dependencies: + has "^1.0.1" + postcss "^5.0.8" + postcss-value-parser "^3.0.1" + +postcss-replace-overflow-wrap@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-2.0.0.tgz#794db6faa54f8db100854392a93af45768b4e25b" + dependencies: + postcss "^6.0.1" + +postcss-selector-matches@^3.0.0, postcss-selector-matches@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/postcss-selector-matches/-/postcss-selector-matches-3.0.1.tgz#e5634011e13950881861bbdd58c2d0111ffc96ab" + dependencies: + balanced-match "^0.4.2" + postcss "^6.0.1" + +postcss-selector-not@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-3.0.1.tgz#2e4db2f0965336c01e7cec7db6c60dff767335d9" + dependencies: + balanced-match "^0.4.2" + postcss "^6.0.1" + +postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2, postcss-selector-parser@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz#f9437788606c3c9acee16ffe8d8b16297f27bb90" + dependencies: + flatten "^1.0.2" + indexes-of "^1.0.1" + uniq "^1.0.1" + +postcss-svgo@^2.1.1: + version "2.1.6" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-2.1.6.tgz#b6df18aa613b666e133f08adb5219c2684ac108d" + dependencies: + is-svg "^2.0.0" + postcss "^5.0.14" + postcss-value-parser "^3.2.3" + svgo "^0.7.0" + +postcss-unique-selectors@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz#981d57d29ddcb33e7b1dfe1fd43b8649f933ca1d" + dependencies: + alphanum-sort "^1.0.1" + postcss "^5.0.4" + uniqs "^2.0.0" + +postcss-value-parser@^3.0.1, postcss-value-parser@^3.0.2, postcss-value-parser@^3.1.1, postcss-value-parser@^3.1.2, postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15" + +postcss-zindex@^2.0.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-2.2.0.tgz#d2109ddc055b91af67fc4cb3b025946639d2af22" + dependencies: + has "^1.0.1" + postcss "^5.0.4" + uniqs "^2.0.0" + +postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.2.16: + version "5.2.18" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.18.tgz#badfa1497d46244f6390f58b319830d9107853c5" + dependencies: + chalk "^1.1.3" + js-base64 "^2.1.9" + source-map "^0.5.6" + supports-color "^3.2.3" + +postcss@^6.0, postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.11, postcss@^6.0.13, postcss@^6.0.14, postcss@^6.0.17, postcss@^6.0.5, postcss@^6.0.6: + version "6.0.17" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.17.tgz#e259a051ca513f81e9afd0c21f7f82eda50c65c5" + dependencies: + chalk "^2.3.0" + source-map "^0.6.1" + supports-color "^5.1.0" + +prepend-http@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + +preserve@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" + +private@^0.1.6, private@^0.1.7: + version "0.1.8" + resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" + +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + +proxy-addr@~2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.2.tgz#6571504f47bb988ec8180253f85dd7e14952bdec" + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.5.2" + +prr@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + +public-encrypt@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6" + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + +pump@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954" + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pump@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pumpify@^1.3.3: + version "1.4.0" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.4.0.tgz#80b7c5df7e24153d03f0e7ac8a05a5d068bd07fb" + dependencies: + duplexify "^3.5.3" + inherits "^2.0.3" + pump "^2.0.0" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + +punycode@^1.2.4, punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +q@^1.1.2: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + +qs@6.5.1, qs@~6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" + +qs@~6.3.0: + version "6.3.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c" + +qs@~6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" + +query-string@^4.1.0: + version "4.3.4" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" + dependencies: + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + +querystring-es3@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + +querystringify@0.0.x: + version "0.0.4" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-0.0.4.tgz#0cf7f84f9463ff0ae51c4c4b142d95be37724d9c" + +querystringify@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-1.0.0.tgz#6286242112c5b712fa654e526652bf6a13ff05cb" + +randomatic@^1.1.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80" + dependencies: + safe-buffer "^5.1.0" + +randomfill@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.3.tgz#b96b7df587f01dd91726c418f30553b1418e3d62" + dependencies: + randombytes "^2.0.5" + safe-buffer "^5.1.0" + +range-parser@^1.0.3, range-parser@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" + +raw-body@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" + dependencies: + bytes "3.0.0" + http-errors "1.6.2" + iconv-lite "0.4.19" + unpipe "1.0.0" + +rc@^1.1.7: + version "1.2.5" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.5.tgz#275cd687f6e3b36cc756baa26dfee80a790301fd" + dependencies: + deep-extend "~0.4.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" + dependencies: + pify "^2.3.0" + +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.3: + version "2.3.4" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.4.tgz#c946c3f47fa7d8eabc0b6150f4a12f69a4574071" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.0.3" + util-deprecate "~1.0.1" + +readdirp@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" + dependencies: + graceful-fs "^4.1.2" + minimatch "^3.0.2" + readable-stream "^2.0.2" + set-immediate-shim "^1.0.1" + +redent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + dependencies: + indent-string "^2.1.0" + strip-indent "^1.0.1" + +reduce-css-calc@^1.2.6, reduce-css-calc@^1.2.7: + version "1.3.0" + resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716" + dependencies: + balanced-match "^0.4.2" + math-expression-evaluator "^1.2.14" + reduce-function-call "^1.0.1" + +reduce-css-calc@^2.0.0: + version "2.1.4" + resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-2.1.4.tgz#c20e9cda8445ad73d4ff4bea960c6f8353791708" + dependencies: + css-unit-converter "^1.1.1" + postcss-value-parser "^3.3.0" + +reduce-function-call@^1.0.1, reduce-function-call@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/reduce-function-call/-/reduce-function-call-1.0.2.tgz#5a200bf92e0e37751752fe45b0ab330fd4b6be99" + dependencies: + balanced-match "^0.4.2" + +regenerate@^1.2.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f" + +regenerator-runtime@^0.10.5: + version "0.10.5" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658" + +regenerator-runtime@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" + +regenerator-transform@^0.10.0: + version "0.10.1" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd" + dependencies: + babel-runtime "^6.18.0" + babel-types "^6.19.0" + private "^0.1.6" + +regex-cache@^0.4.2: + version "0.4.4" + resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd" + dependencies: + is-equal-shallow "^0.1.3" + +regex-not@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.0.tgz#42f83e39771622df826b02af176525d6a5f157f9" + dependencies: + extend-shallow "^2.0.1" + +regexpu-core@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b" + dependencies: + regenerate "^1.2.1" + regjsgen "^0.2.0" + regjsparser "^0.1.4" + +regexpu-core@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" + dependencies: + regenerate "^1.2.1" + regjsgen "^0.2.0" + regjsparser "^0.1.4" + +regjsgen@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" + +regjsparser@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" + dependencies: + jsesc "~0.5.0" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + +repeat-element@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" + +repeat-string@^1.5.2, repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + dependencies: + is-finite "^1.0.0" + +request@2: + version "2.83.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.6.0" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.1" + forever-agent "~0.6.1" + form-data "~2.3.1" + har-validator "~5.0.3" + hawk "~6.0.2" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.17" + oauth-sign "~0.8.2" + performance-now "^2.1.0" + qs "~6.5.1" + safe-buffer "^5.1.1" + stringstream "~0.0.5" + tough-cookie "~2.3.3" + tunnel-agent "^0.6.0" + uuid "^3.1.0" + +request@2.81.0: + version "2.81.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + caseless "~0.12.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.1.1" + har-validator "~4.2.1" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + oauth-sign "~0.8.1" + performance-now "^0.2.0" + qs "~6.4.0" + safe-buffer "^5.0.1" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "^0.6.0" + uuid "^3.0.0" + +request@~2.79.0: + version "2.79.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + caseless "~0.11.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.1.1" + har-validator "~2.0.6" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + oauth-sign "~0.8.1" + qs "~6.3.0" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "~0.4.1" + uuid "^3.0.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + +require-from-string@^1.1.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418" + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + +requires-port@1.0.x, requires-port@1.x.x, requires-port@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + +resolve-cwd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" + dependencies: + resolve-from "^3.0.0" + +resolve-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + +resolve@^1.1.7: + version "1.5.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36" + dependencies: + path-parse "^1.0.5" + +rgb-hex@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/rgb-hex/-/rgb-hex-2.1.0.tgz#c773c5fe2268a25578d92539a82a7a5ce53beda6" + +rgb@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/rgb/-/rgb-0.1.0.tgz#be27b291e8feffeac1bd99729721bfa40fc037b5" + +right-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" + dependencies: + align-text "^0.1.1" + +rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.1: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" + dependencies: + glob "^7.0.5" + +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7" + dependencies: + hash-base "^2.0.0" + inherits "^2.0.1" + +run-queue@^1.0.0, run-queue@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" + dependencies: + aproba "^1.1.1" + +safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" + +sass-graph@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49" + dependencies: + glob "^7.0.0" + lodash "^4.0.0" + scss-tokenizer "^0.2.3" + yargs "^7.0.0" + +sass-loader@^6.0.6: + version "6.0.6" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-6.0.6.tgz#e9d5e6c1f155faa32a4b26d7a9b7107c225e40f9" + dependencies: + async "^2.1.5" + clone-deep "^0.3.0" + loader-utils "^1.0.1" + lodash.tail "^4.1.1" + pify "^3.0.0" + +sax@~1.2.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + +schema-utils@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf" + dependencies: + ajv "^5.0.0" + +schema-utils@^0.4.0, schema-utils@^0.4.2, schema-utils@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.3.tgz#e2a594d3395834d5e15da22b48be13517859458e" + dependencies: + ajv "^5.0.0" + ajv-keywords "^2.1.0" + +scss-tokenizer@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" + dependencies: + js-base64 "^2.1.8" + source-map "^0.4.2" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + +selfsigned@^1.9.1: + version "1.10.2" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.2.tgz#b4449580d99929b65b10a48389301a6592088758" + dependencies: + node-forge "0.7.1" + +"semver@2 || 3 || 4 || 5", semver@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" + +semver@~5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" + +send@0.16.1: + version "0.16.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.16.1.tgz#a70e1ca21d1382c11d0d9f6231deb281080d7ab3" + dependencies: + debug "2.6.9" + depd "~1.1.1" + destroy "~1.0.4" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.6.2" + mime "1.4.1" + ms "2.0.0" + on-finished "~2.3.0" + range-parser "~1.2.0" + statuses "~1.3.1" + +serialize-javascript@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.4.0.tgz#7c958514db6ac2443a8abc062dc9f7886a7f6005" + +serve-index@^1.7.2: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.1.tgz#4c57d53404a761d8f2e7c1e8a18a47dbf278a719" + dependencies: + encodeurl "~1.0.1" + escape-html "~1.0.3" + parseurl "~1.3.2" + send "0.16.1" + +set-blocking@^2.0.0, set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + +set-getter@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/set-getter/-/set-getter-0.1.0.tgz#d769c182c9d5a51f409145f2fba82e5e86e80376" + dependencies: + to-object-path "^0.3.0" + +set-immediate-shim@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + +set-value@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.1" + to-object-path "^0.3.0" + +set-value@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274" + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +setimmediate@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + +setprototypeof@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + +sha.js@^2.4.0, sha.js@^2.4.8: + version "2.4.10" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.10.tgz#b1fde5cd7d11a5626638a07c604ab909cfa31f9b" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +shallow-clone@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-0.1.2.tgz#5909e874ba77106d73ac414cfec1ffca87d97060" + dependencies: + is-extendable "^0.1.1" + kind-of "^2.0.1" + lazy-cache "^0.2.3" + mixin-object "^2.0.1" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + +signal-exit@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + dependencies: + is-arrayish "^0.3.1" + +slash@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.1.tgz#e12b5487faded3e3dea0ac91e9400bf75b401370" + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^2.0.0" + +sntp@1.x.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" + dependencies: + hoek "2.x.x" + +sntp@2.x.x: + version "2.1.0" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8" + dependencies: + hoek "4.x.x" + +sockjs-client@1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.4.tgz#5babe386b775e4cf14e7520911452654016c8b12" + dependencies: + debug "^2.6.6" + eventsource "0.1.6" + faye-websocket "~0.11.0" + inherits "^2.0.1" + json3 "^3.3.2" + url-parse "^1.1.8" + +sockjs@0.3.19: + version "0.3.19" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.19.tgz#d976bbe800af7bd20ae08598d582393508993c0d" + dependencies: + faye-websocket "^0.10.0" + uuid "^3.0.1" + +sort-keys@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" + dependencies: + is-plain-obj "^1.0.0" + +source-list-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085" + +source-map-resolve@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.1.tgz#7ad0f593f2281598e854df80f19aae4b92d7a11a" + dependencies: + atob "^2.0.0" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-support@^0.4.15: + version "0.4.18" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" + dependencies: + source-map "^0.5.6" + +source-map-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + +source-map@^0.4.2: + version "0.4.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" + dependencies: + amdefine ">=0.0.4" + +source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + +source-map@^0.6.1, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + +spdx-correct@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" + dependencies: + spdx-license-ids "^1.0.2" + +spdx-expression-parse@~1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" + +spdx-license-ids@^1.0.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" + +spdy-transport@^2.0.18: + version "2.0.20" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-2.0.20.tgz#735e72054c486b2354fe89e702256004a39ace4d" + dependencies: + debug "^2.6.8" + detect-node "^2.0.3" + hpack.js "^2.1.6" + obuf "^1.1.1" + readable-stream "^2.2.9" + safe-buffer "^5.0.1" + wbuf "^1.7.2" + +spdy@^3.4.1: + version "3.4.7" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-3.4.7.tgz#42ff41ece5cc0f99a3a6c28aabb73f5c3b03acbc" + dependencies: + debug "^2.6.8" + handle-thing "^1.2.5" + http-deceiver "^1.2.7" + safe-buffer "^5.0.1" + select-hose "^2.0.0" + spdy-transport "^2.0.18" + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + dependencies: + extend-shallow "^3.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + +sshpk@^1.7.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +ssri@^5.0.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.2.1.tgz#8b6eb873688759bd3c75a88dee74593d179bb73c" + dependencies: + safe-buffer "^5.1.1" + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +"statuses@>= 1.3.1 < 2": + version "1.4.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" + +statuses@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" + +stdout-stream@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.0.tgz#a2c7c8587e54d9427ea9edb3ac3f2cd522df378b" + dependencies: + readable-stream "^2.0.1" + +stream-browserify@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-each@^1.1.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.2.tgz#8e8c463f91da8991778765873fe4d960d8f616bd" + dependencies: + end-of-stream "^1.1.0" + stream-shift "^1.0.0" + +stream-http@^2.7.2: + version "2.8.0" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.0.tgz#fd86546dac9b1c91aff8fc5d287b98fafb41bc10" + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.3.3" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + +stream-shift@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" + +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + +string-width@^1.0.1, string-width@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +string-width@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@^1.0.0, string_decoder@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" + dependencies: + safe-buffer "~5.1.0" + +stringstream@~0.0.4, stringstream@~0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + dependencies: + is-utf8 "^0.2.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + +strip-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + dependencies: + get-stdin "^4.0.1" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +style-loader@^0.20.1: + version "0.20.1" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.20.1.tgz#33ac2bf4d5c65a8906bc586ad253334c246998d0" + dependencies: + loader-utils "^1.1.0" + schema-utils "^0.4.3" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +supports-color@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" + dependencies: + has-flag "^1.0.0" + +supports-color@^4.2.1: + version "4.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" + dependencies: + has-flag "^2.0.0" + +supports-color@^5.1.0, supports-color@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.2.0.tgz#b0d5333b1184dd3666cbe5aa0b45c5ac7ac17a4a" + dependencies: + has-flag "^3.0.0" + +svgo@^0.7.0: + version "0.7.2" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" + dependencies: + coa "~1.0.1" + colors "~1.1.2" + csso "~2.3.1" + js-yaml "~3.7.0" + mkdirp "~0.5.1" + sax "~1.2.1" + whet.extend "~0.9.9" + +tapable@^0.2.7: + version "0.2.8" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22" + +tar-pack@^3.4.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f" + dependencies: + debug "^2.2.0" + fstream "^1.0.10" + fstream-ignore "^1.0.5" + once "^1.3.3" + readable-stream "^2.1.4" + rimraf "^2.5.1" + tar "^2.2.1" + uid-number "^0.0.6" + +tar@^2.0.0, tar@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1" + dependencies: + block-stream "*" + fstream "^1.0.2" + inherits "2" + +through2@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" + dependencies: + readable-stream "^2.1.5" + xtend "~4.0.1" + +thunky@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.0.2.tgz#a862e018e3fb1ea2ec3fce5d55605cf57f247371" + +time-stamp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-2.0.0.tgz#95c6a44530e15ba8d6f4a3ecb8c3a3fac46da357" + +timers-browserify@^2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.6.tgz#241e76927d9ca05f4d959819022f5b3664b64bae" + dependencies: + setimmediate "^1.0.4" + +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + +to-fast-properties@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.1.tgz#15358bee4a2c83bd76377ba1dc049d0f18837aae" + dependencies: + define-property "^0.2.5" + extend-shallow "^2.0.1" + regex-not "^1.0.0" + +tough-cookie@~2.3.0, tough-cookie@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" + dependencies: + punycode "^1.4.1" + +trim-newlines@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + +trim-right@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" + +trix@^0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/trix/-/trix-0.11.1.tgz#ffe54f2757c2c2385b8424fd5c5d2ab712a09acc" + +"true-case-path@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.2.tgz#7ec91130924766c7f573be3020c34f8fdfd00d62" + dependencies: + glob "^6.0.4" + +tty-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + dependencies: + safe-buffer "^5.0.1" + +tunnel-agent@~0.4.1: + version "0.4.3" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + +type-is@~1.6.15: + version "1.6.15" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" + dependencies: + media-typer "0.3.0" + mime-types "~2.1.15" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + +uglify-es@^3.3.4: + version "3.3.10" + resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.10.tgz#8b0b7992cebe20edc26de1bf325cef797b8f3fa5" + dependencies: + commander "~2.14.1" + source-map "~0.6.1" + +uglify-js@^2.8.29: + version "2.8.29" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" + dependencies: + source-map "~0.5.1" + yargs "~3.10.0" + optionalDependencies: + uglify-to-browserify "~1.0.0" + +uglify-to-browserify@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" + +uglifyjs-webpack-plugin@^0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz#b951f4abb6bd617e66f63eb891498e391763e309" + dependencies: + source-map "^0.5.6" + uglify-js "^2.8.29" + webpack-sources "^1.0.1" + +uglifyjs-webpack-plugin@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.1.8.tgz#1302fb9471a7daf3d0a5174da6d65f0f415e75ad" + dependencies: + cacache "^10.0.1" + find-cache-dir "^1.0.0" + schema-utils "^0.4.2" + serialize-javascript "^1.4.0" + source-map "^0.6.1" + uglify-es "^3.3.4" + webpack-sources "^1.1.0" + worker-farm "^1.5.2" + +uid-number@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" + +underscore.string@2.3.x: + version "2.3.3" + resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-2.3.3.tgz#71c08bf6b428b1133f37e78fa3a21c82f7329b0d" + +union-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^0.4.3" + +uniq@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" + +uniqid@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/uniqid/-/uniqid-4.1.1.tgz#89220ddf6b751ae52b5f72484863528596bb84c1" + dependencies: + macaddress "^0.2.8" + +uniqs@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" + +unique-filename@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.0.tgz#d05f2fe4032560871f30e93cbe735eea201514f3" + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.0.tgz#db6676e7c7cc0629878ff196097c78855ae9f4ab" + dependencies: + imurmurhash "^0.1.4" + +units-css@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/units-css/-/units-css-0.4.0.tgz#d6228653a51983d7c16ff28f8b9dc3b1ffed3a07" + dependencies: + isnumeric "^0.2.0" + viewport-dimensions "^0.2.0" + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +upath@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.0.0.tgz#b4706b9461ca8473adf89133d235689ca17f3656" + dependencies: + lodash "3.x" + underscore.string "2.3.x" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + +url-parse@1.0.x: + version "1.0.5" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.0.5.tgz#0854860422afdcfefeb6c965c662d4800169927b" + dependencies: + querystringify "0.0.x" + requires-port "1.0.x" + +url-parse@^1.1.8: + version "1.2.0" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.2.0.tgz#3a19e8aaa6d023ddd27dcc44cb4fc8f7fec23986" + dependencies: + querystringify "~1.0.0" + requires-port "~1.0.0" + +url@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +use@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/use/-/use-2.0.2.tgz#ae28a0d72f93bf22422a18a2e379993112dec8e8" + dependencies: + define-property "^0.2.5" + isobject "^3.0.0" + lazy-cache "^2.0.2" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +util@0.10.3, util@^0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + dependencies: + inherits "2.0.1" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + +uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" + +validate-npm-package-license@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" + dependencies: + spdx-correct "~1.0.0" + spdx-expression-parse "~1.0.0" + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + +vendors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.1.tgz#37ad73c8ee417fb3d580e785312307d274847f22" + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +viewport-dimensions@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/viewport-dimensions/-/viewport-dimensions-0.2.0.tgz#de740747db5387fd1725f5175e91bac76afdf36c" + +vm-browserify@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" + dependencies: + indexof "0.0.1" + +watchpack@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac" + dependencies: + async "^2.1.2" + chokidar "^1.7.0" + graceful-fs "^4.1.2" + +wbuf@^1.1.0, wbuf@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.2.tgz#d697b99f1f59512df2751be42769c1580b5801fe" + dependencies: + minimalistic-assert "^1.0.0" + +webpack-dev-middleware@1.12.2: + version "1.12.2" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.12.2.tgz#f8fc1120ce3b4fc5680ceecb43d777966b21105e" + dependencies: + memory-fs "~0.4.1" + mime "^1.5.0" + path-is-absolute "^1.0.0" + range-parser "^1.0.3" + time-stamp "^2.0.0" + +webpack-dev-server@^2.11.1: + version "2.11.1" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.11.1.tgz#6f9358a002db8403f016e336816f4485384e5ec0" + dependencies: + ansi-html "0.0.7" + array-includes "^3.0.3" + bonjour "^3.5.0" + chokidar "^2.0.0" + compression "^1.5.2" + connect-history-api-fallback "^1.3.0" + debug "^3.1.0" + del "^3.0.0" + express "^4.16.2" + html-entities "^1.2.0" + http-proxy-middleware "~0.17.4" + import-local "^1.0.0" + internal-ip "1.2.0" + ip "^1.1.5" + killable "^1.0.0" + loglevel "^1.4.1" + opn "^5.1.0" + portfinder "^1.0.9" + selfsigned "^1.9.1" + serve-index "^1.7.2" + sockjs "0.3.19" + sockjs-client "1.1.4" + spdy "^3.4.1" + strip-ansi "^3.0.0" + supports-color "^5.1.0" + webpack-dev-middleware "1.12.2" + yargs "6.6.0" + +webpack-manifest-plugin@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/webpack-manifest-plugin/-/webpack-manifest-plugin-1.3.2.tgz#5ea8ee5756359ddc1d98814324fe43496349a7d4" + dependencies: + fs-extra "^0.30.0" + lodash ">=3.5 <5" + +webpack-sources@^1.0.1, webpack-sources@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54" + dependencies: + source-list-map "^2.0.0" + source-map "~0.6.1" + +webpack@^3.10.0: + version "3.11.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.11.0.tgz#77da451b1d7b4b117adaf41a1a93b5742f24d894" + dependencies: + acorn "^5.0.0" + acorn-dynamic-import "^2.0.0" + ajv "^6.1.0" + ajv-keywords "^3.1.0" + async "^2.1.2" + enhanced-resolve "^3.4.0" + escope "^3.6.0" + interpret "^1.0.0" + json-loader "^0.5.4" + json5 "^0.5.1" + loader-runner "^2.3.0" + loader-utils "^1.1.0" + memory-fs "~0.4.1" + mkdirp "~0.5.0" + node-libs-browser "^2.0.0" + source-map "^0.5.3" + supports-color "^4.2.1" + tapable "^0.2.7" + uglifyjs-webpack-plugin "^0.4.6" + watchpack "^1.4.0" + webpack-sources "^1.0.1" + yargs "^8.0.2" + +websocket-driver@>=0.5.1: + version "0.7.0" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.0.tgz#0caf9d2d755d93aee049d4bdd0d3fe2cca2a24eb" + dependencies: + http-parser-js ">=0.4.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" + +whet.extend@~0.9.9: + version "0.9.9" + resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1" + +which-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + +which@1, which@^1.2.9: + version "1.3.0" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710" + dependencies: + string-width "^1.0.2" + +window-size@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" + +wordwrap@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" + +worker-farm@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.2.tgz#32b312e5dc3d5d45d79ef44acc2587491cd729ae" + dependencies: + errno "^0.1.4" + xtend "^4.0.1" + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + +y18n@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + +yargs-parser@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-4.2.1.tgz#29cceac0dc4f03c6c87b4a9f217dd18c9f74871c" + dependencies: + camelcase "^3.0.0" + +yargs-parser@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a" + dependencies: + camelcase "^3.0.0" + +yargs-parser@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9" + dependencies: + camelcase "^4.1.0" + +yargs@6.6.0: + version "6.6.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208" + dependencies: + camelcase "^3.0.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.2" + which-module "^1.0.0" + y18n "^3.2.1" + yargs-parser "^4.2.0" + +yargs@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8" + dependencies: + camelcase "^3.0.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^1.4.0" + read-pkg-up "^1.0.1" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^1.0.2" + which-module "^1.0.0" + y18n "^3.2.1" + yargs-parser "^5.0.0" + +yargs@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360" + dependencies: + camelcase "^4.1.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^2.0.0" + read-pkg-up "^2.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1" + yargs-parser "^7.0.0" + +yargs@~3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" + dependencies: + camelcase "^1.0.2" + cliui "^2.1.0" + decamelize "^1.0.0" + window-size "0.1.0" diff --git a/actiontext/test/fixtures/files/racecar.jpg b/actiontext/test/fixtures/files/racecar.jpg Binary files differnew file mode 100644 index 0000000000..934b4caa22 --- /dev/null +++ b/actiontext/test/fixtures/files/racecar.jpg diff --git a/actiontext/test/template/form_helper_test.rb b/actiontext/test/template/form_helper_test.rb new file mode 100644 index 0000000000..a8c7a4dae2 --- /dev/null +++ b/actiontext/test/template/form_helper_test.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require "test_helper" + +class ActionText::FormHelperTest < ActionView::TestCase + tests ActionText::TagHelper + + test "form with rich text area" do + form_with model: Message.new, scope: :message do |form| + form.rich_text_area :content + end + + assert_dom_equal \ + '<form action="/messages" accept-charset="UTF-8" data-remote="true" method="post">' \ + '<input type="hidden" name="message[content]" id="message_content_trix_input_message" />' \ + '<trix-editor id="message_content" input="message_content_trix_input_message" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/:signed_id/:filename">' \ + "</trix-editor>" \ + "</form>", + output_buffer + end + + test "form with rich text area having class" do + form_with model: Message.new, scope: :message do |form| + form.rich_text_area :content, class: "custom-class" + end + + assert_dom_equal \ + '<form action="/messages" accept-charset="UTF-8" data-remote="true" method="post">' \ + '<input type="hidden" name="message[content]" id="message_content_trix_input_message" />' \ + '<trix-editor id="message_content" input="message_content_trix_input_message" class="custom-class" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/:signed_id/:filename">' \ + "</trix-editor>" \ + "</form>", + output_buffer + end + + test "form with rich text area for non-attribute" do + form_with model: Message.new, scope: :message do |form| + form.rich_text_area :not_an_attribute + end + + assert_dom_equal \ + '<form action="/messages" accept-charset="UTF-8" data-remote="true" method="post">' \ + '<input type="hidden" name="message[not_an_attribute]" id="message_not_an_attribute_trix_input_message" />' \ + '<trix-editor id="message_not_an_attribute" input="message_not_an_attribute_trix_input_message" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/:signed_id/:filename">' \ + "</trix-editor>" \ + "</form>", + output_buffer + end + + test "modelless form with rich text area" do + form_with url: "/messages", scope: :message do |form| + form.rich_text_area :content + end + + assert_dom_equal \ + '<form action="/messages" accept-charset="UTF-8" data-remote="true" method="post">' \ + '<input type="hidden" name="message[content]" id="trix_input_1" />' \ + '<trix-editor id="message_content" input="trix_input_1" class="trix-content" data-direct-upload-url="http://test.host/rails/active_storage/direct_uploads" data-blob-url-template="http://test.host/rails/active_storage/blobs/:signed_id/:filename">' \ + "</trix-editor>" \ + "</form>", + output_buffer + end + + def form_with(*) + @output_buffer = super + end +end diff --git a/actiontext/test/test_helper.rb b/actiontext/test/test_helper.rb new file mode 100644 index 0000000000..fd1c859349 --- /dev/null +++ b/actiontext/test/test_helper.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# Configure Rails Environment +ENV["RAILS_ENV"] = "test" + +require_relative "../test/dummy/config/environment" +ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)] +require "rails/test_help" + +# Filter out Minitest backtrace while allowing backtrace from other libraries +# to be shown. +Minitest.backtrace_filter = Minitest::BacktraceFilter.new + +require "rails/test_unit/reporter" +Rails::TestUnitReporter.executable = "bin/test" + +# Load fixtures from the engine +if ActiveSupport::TestCase.respond_to?(:fixture_path=) + ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__) + ActionDispatch::IntegrationTest.fixture_path = ActiveSupport::TestCase.fixture_path + ActiveSupport::TestCase.file_fixture_path = ActiveSupport::TestCase.fixture_path + "/files" + ActiveSupport::TestCase.fixtures :all +end + +class ActiveSupport::TestCase + private + def create_file_blob(filename:, content_type:, metadata: nil) + ActiveStorage::Blob.create_after_upload! io: file_fixture(filename).open, filename: filename, content_type: content_type, metadata: metadata + end +end diff --git a/actiontext/test/unit/attachment_test.rb b/actiontext/test/unit/attachment_test.rb new file mode 100644 index 0000000000..65e9d69e84 --- /dev/null +++ b/actiontext/test/unit/attachment_test.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require "test_helper" + +class ActionText::AttachmentTest < ActiveSupport::TestCase + test "from_attachable" do + attachment = ActionText::Attachment.from_attachable(attachable, caption: "Captioned") + assert_equal attachable, attachment.attachable + assert_equal "Captioned", attachment.caption + end + + test "proxies missing methods to attachable" do + attachable.instance_eval { def proxied; "proxied"; end } + attachment = ActionText::Attachment.from_attachable(attachable) + assert_equal "proxied", attachment.proxied + end + + test "proxies #to_param to attachable" do + attachment = ActionText::Attachment.from_attachable(attachable) + assert_equal attachable.to_param, attachment.to_param + end + + test "converts to TrixAttachment" do + attachment = attachment_from_html(%Q(<action-text-attachment sgid="#{attachable.attachable_sgid}" caption="Captioned"></action-text-attachment>)) + + trix_attachment = attachment.to_trix_attachment + assert_kind_of ActionText::TrixAttachment, trix_attachment + + assert_equal attachable.attachable_sgid, trix_attachment.attributes["sgid"] + assert_equal attachable.attachable_content_type, trix_attachment.attributes["contentType"] + assert_equal attachable.filename.to_s, trix_attachment.attributes["filename"] + assert_equal attachable.byte_size, trix_attachment.attributes["filesize"] + assert_equal "Captioned", trix_attachment.attributes["caption"] + + assert_nil trix_attachment.attributes["content"] + end + + test "converts to TrixAttachment with content" do + attachable = Person.create! name: "Javan" + attachment = attachment_from_html(%Q(<action-text-attachment sgid="#{attachable.attachable_sgid}"></action-text-attachment>)) + + trix_attachment = attachment.to_trix_attachment + assert_kind_of ActionText::TrixAttachment, trix_attachment + + assert_equal attachable.attachable_sgid, trix_attachment.attributes["sgid"] + assert_equal attachable.attachable_content_type, trix_attachment.attributes["contentType"] + + assert_not_nil attachable.to_trix_content_attachment_partial_path + assert_not_nil trix_attachment.attributes["content"] + end + + private + def attachment_from_html(html) + ActionText::Content.new(html).attachments.first + end + + def attachable + @attachment ||= create_file_blob(filename: "racecar.jpg", content_type: "image/jpg") + end +end diff --git a/actiontext/test/unit/content_test.rb b/actiontext/test/unit/content_test.rb new file mode 100644 index 0000000000..f77f48df14 --- /dev/null +++ b/actiontext/test/unit/content_test.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +require "test_helper" + +class ActionText::ContentTest < ActiveSupport::TestCase + test "equality" do + html = "<div>test</div>" + content = content_from_html(html) + assert_equal content, content_from_html(html) + assert_not_equal content, html + end + + test "marshal serialization" do + content = content_from_html("Hello!") + assert_equal content, Marshal.load(Marshal.dump(content)) + end + + test "roundtrips HTML without additional newlines" do + html = "<div>a<br></div>" + content = content_from_html(html) + assert_equal html, content.to_html + end + + test "extracts links" do + html = '<a href="http://example.com/1">1</a><br><a href="http://example.com/1">1</a>' + content = content_from_html(html) + assert_equal ["http://example.com/1"], content.links + end + + test "extracts attachables" do + attachable = create_file_blob(filename: "racecar.jpg", content_type: "image/jpg") + html = %Q(<action-text-attachment sgid="#{attachable.attachable_sgid}" caption="Captioned"></action-text-attachment>) + + content = content_from_html(html) + assert_equal 1, content.attachments.size + + attachment = content.attachments.first + assert_equal "Captioned", attachment.caption + assert_equal attachable, attachment.attachable + end + + test "extracts remote image attachables" do + html = '<action-text-attachment content-type="image" url="http://example.com/cat.jpg" width="100" height="100" caption="Captioned"></action-text-attachment>' + + content = content_from_html(html) + assert_equal 1, content.attachments.size + + attachment = content.attachments.first + assert_equal "Captioned", attachment.caption + + attachable = attachment.attachable + assert_kind_of ActionText::Attachables::RemoteImage, attachable + assert_equal "http://example.com/cat.jpg", attachable.url + assert_equal "100", attachable.width + assert_equal "100", attachable.height + end + + test "identifies destroyed attachables as missing" do + attachable = create_file_blob(filename: "racecar.jpg", content_type: "image/jpg") + html = %Q(<action-text-attachment sgid="#{attachable.attachable_sgid}"></action-text-attachment>) + attachable.destroy! + content = content_from_html(html) + assert_equal 1, content.attachments.size + assert_equal ActionText::Attachables::MissingAttachable, content.attachments.first.attachable + end + + test "extracts missing attachables" do + html = '<action-text-attachment sgid="missing"></action-text-attachment>' + content = content_from_html(html) + assert_equal 1, content.attachments.size + assert_equal ActionText::Attachables::MissingAttachable, content.attachments.first.attachable + end + + test "converts Trix-formatted attachments" do + html = %Q(<figure data-trix-attachment='{"sgid":"123","contentType":"text/plain","width":100,"height":100}' data-trix-attributes='{"caption":"Captioned"}'></figure>) + content = content_from_html(html) + assert_equal 1, content.attachments.size + assert_equal '<action-text-attachment sgid="123" content-type="text/plain" width="100" height="100" caption="Captioned"></action-text-attachment>', content.to_html + end + + test "ignores Trix-formatted attachments with malformed JSON" do + html = %Q(<div data-trix-attachment='{"sgid":"garbage...'></div>) + content = content_from_html(html) + assert_equal 0, content.attachments.size + end + + test "minifies attachment markup" do + html = '<action-text-attachment sgid="123"><div>HTML</div></action-text-attachment>' + assert_equal '<action-text-attachment sgid="123"></action-text-attachment>', content_from_html(html).to_html + end + + test "canonicalizes attachment gallery markup" do + attachment_html = '<action-text-attachment sgid="1" presentation="gallery"></action-text-attachment><action-text-attachment sgid="2" presentation="gallery"></action-text-attachment>' + html = %Q(<div class="attachment-gallery attachment-gallery--2">#{attachment_html}</div>) + assert_equal "<div>#{attachment_html}</div>", content_from_html(html).to_html + end + + test "canonicalizes attachment gallery markup with whitespace" do + attachment_html = %Q(\n <action-text-attachment sgid="1" presentation="gallery"></action-text-attachment>\n <action-text-attachment sgid="2" presentation="gallery"></action-text-attachment>\n) + html = %Q(<div class="attachment-gallery attachment-gallery--2">#{attachment_html}</div>) + assert_equal "<div>#{attachment_html}</div>", content_from_html(html).to_html + end + + test "canonicalizes nested attachment gallery markup" do + attachment_html = '<action-text-attachment sgid="1" presentation="gallery"></action-text-attachment><action-text-attachment sgid="2" presentation="gallery"></action-text-attachment>' + html = %Q(<blockquote><div class="attachment-gallery attachment-gallery--2">#{attachment_html}</div></blockquote>) + assert_equal "<blockquote><div>#{attachment_html}</div></blockquote>", content_from_html(html).to_html + end + + private + def content_from_html(html) + ActionText::Content.new(html).tap do |content| + assert_nothing_raised { content.to_s } + end + end +end diff --git a/actiontext/test/unit/model_test.rb b/actiontext/test/unit/model_test.rb new file mode 100644 index 0000000000..d56363adc0 --- /dev/null +++ b/actiontext/test/unit/model_test.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require "test_helper" + +class ActionText::ModelTest < ActiveSupport::TestCase + test "html conversion" do + message = Message.new(subject: "Greetings", content: "<h1>Hello world</h1>") + assert_equal %Q(<div class="trix-content">\n <h1>Hello world</h1>\n</div>\n), "#{message.content}" + end + + test "plain text conversion" do + message = Message.new(subject: "Greetings", content: "<h1>Hello world</h1>") + assert_equal "Hello world", message.content.to_plain_text + end + + test "without content" do + message = Message.create!(subject: "Greetings") + assert message.content.nil? + assert message.content.blank? + assert message.content.empty? + assert_not message.content.present? + end + + test "with blank content" do + message = Message.create!(subject: "Greetings", content: "") + assert_not message.content.nil? + assert message.content.blank? + assert message.content.empty? + assert_not message.content.present? + end + + test "embed extraction" do + blob = create_file_blob(filename: "racecar.jpg", content_type: "image/jpg") + message = Message.create!(subject: "Greetings", content: ActionText::Content.new("Hello world").append_attachables(blob)) + assert_equal "racecar.jpg", message.content.embeds.first.filename.to_s + end + + test "embed extraction only extracts file attachments" do + remote_image_html = '<action-text-attachment content-type="image" url="http://example.com/cat.jpg"></action-text-attachment>' + blob = create_file_blob(filename: "racecar.jpg", content_type: "image/jpg") + content = ActionText::Content.new(remote_image_html).append_attachables(blob) + message = Message.create!(subject: "Greetings", content: content) + assert_equal [ActionText::Attachables::RemoteImage, ActiveStorage::Blob], message.content.body.attachables.map(&:class) + assert_equal [ActiveStorage::Attachment], message.content.embeds.map(&:class) + end + + test "saving content" do + message = Message.create!(subject: "Greetings", content: "<h1>Hello world</h1>") + assert_equal "Hello world", message.content.to_plain_text + end + + test "save body" do + message = Message.create(subject: "Greetings", body: "<h1>Hello world</h1>") + assert_equal "Hello world", message.body.to_plain_text + end +end diff --git a/actiontext/test/unit/plain_text_conversion_test.rb b/actiontext/test/unit/plain_text_conversion_test.rb new file mode 100644 index 0000000000..53a1029caf --- /dev/null +++ b/actiontext/test/unit/plain_text_conversion_test.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require "test_helper" + +class ActionText::PlainTextConversionTest < ActiveSupport::TestCase + test "<p> tags are separated by two new lines" do + assert_converted_to( + "Hello world!\n\nHow are you?", + "<p>Hello world!</p><p>How are you?</p>" + ) + end + + test "<blockquote> tags are separated by two new lines" do + assert_converted_to( + "“Hello world!”\n\n“How are you?”", + "<blockquote>Hello world!</blockquote><blockquote>How are you?</blockquote>" + ) + end + + test "<ol> tags are separated by two new lines" do + assert_converted_to( + "Hello world!\n\n1. list1\n\n1. list2\n\nHow are you?", + "<p>Hello world!</p><ol><li>list1</li></ol><ol><li>list2</li></ol><p>How are you?</p>" + ) + end + + test "<ul> tags are separated by two new lines" do + assert_converted_to( + "Hello world!\n\n• list1\n\n• list2\n\nHow are you?", + "<p>Hello world!</p><ul><li>list1</li></ul><ul><li>list2</li></ul><p>How are you?</p>" + ) + end + + test "<h1> tags are separated by two new lines" do + assert_converted_to( + "Hello world!\n\nHow are you?", + "<h1>Hello world!</h1><div>How are you?</div>" + ) + end + + test "<li> tags are separated by one new line" do + assert_converted_to( + "• one\n• two\n• three", + "<ul><li>one</li><li>two</li><li>three</li></ul>" + ) + end + + test "<li> tags without a parent list" do + assert_converted_to( + "• one\n• two\n• three", + "<li>one</li><li>two</li><li>three</li>" + ) + end + + test "<br> tags are separated by one new line" do + assert_converted_to( + "Hello world!\none\ntwo\nthree", + "<p>Hello world!<br>one<br>two<br>three</p>" + ) + end + + test "<div> tags are separated by one new line" do + assert_converted_to( + "Hello world!\nHow are you?", + "<div>Hello world!</div><div>How are you?</div>" + ) + end + + test "<action-text-attachment> tags are converted to their plain-text representation" do + assert_converted_to( + "Hello world! [Cat]", + 'Hello world! <action-text-attachment url="http://example.com/cat.jpg" content-type="image" caption="Cat"></action-text-attachment>' + ) + end + + test "preserves non-linebreak whitespace after text" do + assert_converted_to( + "Hello world!", + "<div><strong>Hello </strong>world!</div>" + ) + end + + test "preserves trailing linebreaks after text" do + assert_converted_to( + "Hello\nHow are you?", + "<strong>Hello<br></strong>How are you?" + ) + end + + private + def assert_converted_to(plain_text, html) + assert_equal plain_text, ActionText::Content.new(html).to_plain_text + end +end diff --git a/actiontext/test/unit/trix_attachment_test.rb b/actiontext/test/unit/trix_attachment_test.rb new file mode 100644 index 0000000000..81d015750e --- /dev/null +++ b/actiontext/test/unit/trix_attachment_test.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require "test_helper" + +class ActionText::TrixAttachmentTest < ActiveSupport::TestCase + test "from_attributes" do + attributes = { + "data-trix-attachment" => { + "sgid" => "123", + "contentType" => "text/plain", + "href" => "http://example.com/", + "filename" => "example.txt", + "filesize" => 12345, + "previewable" => true + }, + "data-trix-attributes" => { + "caption" => "hello" + } + } + + attachment = attachment( + sgid: "123", + content_type: "text/plain", + href: "http://example.com/", + filename: "example.txt", + filesize: "12345", + previewable: "true", + caption: "hello" + ) + + assert_attachment_json_attributes(attachment, attributes) + end + + test "previewable is typecast" do + assert_attachment_attribute(attachment(previewable: ""), "previewable", false) + assert_attachment_attribute(attachment(previewable: false), "previewable", false) + assert_attachment_attribute(attachment(previewable: "false"), "previewable", false) + assert_attachment_attribute(attachment(previewable: "garbage"), "previewable", false) + assert_attachment_attribute(attachment(previewable: true), "previewable", true) + assert_attachment_attribute(attachment(previewable: "true"), "previewable", true) + end + + test "filesize is typecast when integer-like" do + assert_attachment_attribute(attachment(filesize: 123), "filesize", 123) + assert_attachment_attribute(attachment(filesize: "123"), "filesize", 123) + assert_attachment_attribute(attachment(filesize: "3.5 MB"), "filesize", "3.5 MB") + assert_attachment_attribute(attachment(filesize: nil), "filesize", nil) + assert_attachment_attribute(attachment(filesize: ""), "filesize", "") + end + + test "#attributes strips unmappable attributes" do + attributes = { + "sgid" => "123", + "caption" => "hello" + } + + attachment = attachment(sgid: "123", caption: "hello", nonexistent: "garbage") + assert_attachment_attributes(attachment, attributes) + end + + def assert_attachment_attribute(attachment, name, value) + if value.nil? + assert_nil(attachment.attributes[name]) + else + assert_equal(value, attachment.attributes[name]) + end + end + + def assert_attachment_attributes(attachment, attributes) + assert_equal(attributes, attachment.attributes) + end + + def assert_attachment_json_attributes(attachment, attributes) + attributes.each do |name, expected| + actual = JSON.parse(attachment.node[name]) + assert_equal(expected, actual) + end + end + + def attachment(**attributes) + ActionText::TrixAttachment.from_attributes(attributes) + end +end diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index df4036a5a7..e2cd9f7558 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,8 +1,25 @@ +## Rails 6.0.0.beta1 (January 18, 2019) ## + +* [Rename npm package](https://github.com/rails/rails/pull/34905) from + [`rails-ujs`](https://www.npmjs.com/package/rails-ujs) to + [`@rails/ujs`](https://www.npmjs.com/package/@rails/ujs). + + *Javan Makhmali* + +* Remove deprecated `image_alt` helper. + + *Rafael Mendonça França* + +* Fix the need of `#protect_against_forgery?` method defined in + `ActionView::Base` subclasses. This prevents the use of forms and buttons. + + *Genadi Samokovarov* + * Fix UJS permanently showing disabled text in a[data-remote][data-disable-with] elements within forms. - Fixes #33889 - *Wolfgang Hobmaier* + Fixes #33889. + *Wolfgang Hobmaier* * Prevent non-primary mouse keys from triggering Rails UJS click handlers. Firefox fires click events even if the click was triggered by non-primary mouse keys such as right- or scroll-wheel-clicks. @@ -12,11 +29,10 @@ <%= link_to 'Remote', remote_path, class: 'remote', remote: true, data: { type: :json } %> ``` - Fixes #34541 + Fixes #34541. *Wolfgang Hobmaier* - * Prevent `ActionView::TextHelper#word_wrap` from unexpectedly stripping white space from the _left_ side of lines. For example, given input like this: @@ -30,11 +46,10 @@ Calling `word_wrap` should not trim the indents on the first and last lines. - Fixes #34487 + Fixes #34487. *Lyle Mullican* - * Add allocations to template rendering instrumentation. Adds the allocations for template and partial rendering to the server output on render. diff --git a/actionview/MIT-LICENSE b/actionview/MIT-LICENSE index 1cb3add0fc..ab7c27c209 100644 --- a/actionview/MIT-LICENSE +++ b/actionview/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2018 David Heinemeier Hansson +Copyright (c) 2004-2019 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionview/app/assets/javascripts/MIT-LICENSE b/actionview/app/assets/javascripts/MIT-LICENSE index 28e1b12496..03319ea365 100644 --- a/actionview/app/assets/javascripts/MIT-LICENSE +++ b/actionview/app/assets/javascripts/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2007-2018 Rails Core team +Copyright (c) 2007-2019 Rails Core team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionview/app/assets/javascripts/README.md b/actionview/app/assets/javascripts/README.md index b74fa1afad..b9682b61e2 100644 --- a/actionview/app/assets/javascripts/README.md +++ b/actionview/app/assets/javascripts/README.md @@ -17,11 +17,11 @@ Note that the `data` attributes this library adds are a feature of HTML5. If you ### NPM - npm install rails-ujs --save - + npm install @rails/ujs --save + ### Yarn - - yarn add rails-ujs + + yarn add @rails/ujs Ensure that `.yarnclean` does not include `assets` if you use [yarn autoclean](https://yarnpkg.com/lang/en/docs/cli/autoclean/). @@ -40,8 +40,7 @@ In a conventional Rails application that uses the asset pipeline, require `rails If you're using the Webpacker gem or some other JavaScript bundler, add the following to your main JS file: ```javascript -import Rails from 'rails-ujs'; -Rails.start() +require("@rails/ujs").start() ``` ## How to run tests diff --git a/actionview/lib/action_view.rb b/actionview/lib/action_view.rb index c1eeda75f5..6ff2d70e35 100644 --- a/actionview/lib/action_view.rb +++ b/actionview/lib/action_view.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2018 David Heinemeier Hansson +# Copyright (c) 2004-2019 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -45,6 +45,7 @@ module ActionView autoload :Rendering autoload :RoutingUrlFor autoload :Template + autoload :FileTemplate autoload :ViewPaths autoload_under "renderer" do diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb index d41fe2a608..b143e13c96 100644 --- a/actionview/lib/action_view/base.rb +++ b/actionview/lib/action_view/base.rb @@ -3,6 +3,7 @@ require "active_support/core_ext/module/attr_internal" require "active_support/core_ext/module/attribute_accessors" require "active_support/ordered_options" +require "active_support/deprecation" require "action_view/log_subscriber" require "action_view/helpers" require "action_view/context" @@ -10,6 +11,10 @@ require "action_view/template" require "action_view/lookup_context" module ActionView #:nodoc: + module CompiledTemplates #:nodoc: + # holds compiled template code + end + # = Action View Base # # Action View templates can be written in several ways. @@ -141,6 +146,8 @@ module ActionView #:nodoc: class Base include Helpers, ::ERB::Util, Context + include CompiledTemplates + # Specify the proc used to decorate input tags that refer to attributes with errors. cattr_accessor :field_error_proc, default: Proc.new { |html_tag, instance| "<div class=\"field_with_errors\">#{html_tag}</div>".html_safe } @@ -181,7 +188,7 @@ module ActionView #:nodoc: end end - attr_accessor :view_renderer + attr_reader :view_renderer attr_internal :config, :assigns delegate :lookup_context, to: :view_renderer @@ -191,17 +198,51 @@ module ActionView #:nodoc: @_assigns = new_assigns.each { |key, value| instance_variable_set("@#{key}", value) } end - def initialize(context = nil, assigns = {}, controller = nil, formats = nil) #:nodoc: + # :stopdoc: + + def self.build_renderer(context, controller, formats) + lookup_context = context.is_a?(ActionView::LookupContext) ? + context : ActionView::LookupContext.new(context) + lookup_context.formats = formats if formats + lookup_context.prefixes = controller._prefixes if controller + ActionView::Renderer.new(lookup_context) + end + + def self.empty + with_view_paths([]) + end + + def self.with_view_paths(view_paths, assigns = {}, controller = nil) + with_context ActionView::LookupContext.new(view_paths), assigns, controller + end + + def self.with_context(context, assigns = {}, controller = nil) + new ActionView::Renderer.new(context), assigns, controller + end + + NULL = Object.new + + # :startdoc: + + def initialize(renderer = nil, assigns = {}, controller = nil, formats = NULL) #:nodoc: @_config = ActiveSupport::InheritableOptions.new - if context.is_a?(ActionView::Renderer) - @view_renderer = context + if formats == NULL + formats = nil + else + ActiveSupport::Deprecation.warn <<~eowarn + Passing formats to ActionView::Base.new is deprecated + eowarn + end + + if renderer.is_a?(ActionView::Renderer) + @view_renderer = renderer else - lookup_context = context.is_a?(ActionView::LookupContext) ? - context : ActionView::LookupContext.new(context) - lookup_context.formats = formats if formats - lookup_context.prefixes = controller._prefixes if controller - @view_renderer = ActionView::Renderer.new(lookup_context) + ActiveSupport::Deprecation.warn <<~eowarn + ActionView::Base instances should be constructed with a view renderer, + assigments, and a controller. + eowarn + @view_renderer = self.class.build_renderer(renderer, controller, formats) end @cache_hit = {} @@ -210,6 +251,18 @@ module ActionView #:nodoc: _prepare_context end + def run(method, locals, buffer, &block) + _old_output_buffer, _old_virtual_path = @output_buffer, @virtual_path + @output_buffer = buffer + send(method, locals, buffer, &block) + ensure + @output_buffer, @virtual_path = _old_output_buffer, _old_virtual_path + end + + def compiled_method_container + CompiledTemplates + end + ActiveSupport.run_load_hooks(:action_view, self) end end diff --git a/actionview/lib/action_view/context.rb b/actionview/lib/action_view/context.rb index 3c605c3ee3..2b22c30a3a 100644 --- a/actionview/lib/action_view/context.rb +++ b/actionview/lib/action_view/context.rb @@ -1,10 +1,6 @@ # frozen_string_literal: true module ActionView - module CompiledTemplates #:nodoc: - # holds compiled template code - end - # = Action View Context # # Action View contexts are supplied to Action Controller to render a template. @@ -16,7 +12,6 @@ module ActionView # object that includes this module (although you can call _prepare_context # defined below). module Context - include CompiledTemplates attr_accessor :output_buffer, :view_flow # Prepares the context by setting the appropriate instance variables. diff --git a/actionview/lib/action_view/file_template.rb b/actionview/lib/action_view/file_template.rb new file mode 100644 index 0000000000..4921074383 --- /dev/null +++ b/actionview/lib/action_view/file_template.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require "action_view/template" + +module ActionView + class FileTemplate < Template + def initialize(filename, handler, details) + @filename = filename + + super(nil, filename, handler, details) + end + + def source + File.binread @filename + end + + def refresh(_) + self + end + + # Exceptions are marshalled when using the parallel test runner with DRb, so we need + # to ensure that references to the template object can be marshalled as well. This means forgoing + # the marshalling of the compiler mutex and instantiating that again on unmarshalling. + def marshal_dump # :nodoc: + [ @identifier, @handler, @compiled, @original_encoding, @locals, @virtual_path, @updated_at, @formats, @variants ] + end + + def marshal_load(array) # :nodoc: + @identifier, @handler, @compiled, @original_encoding, @locals, @virtual_path, @updated_at, @formats, @variants = *array + @compile_mutex = Mutex.new + end + end +end diff --git a/actionview/lib/action_view/gem_version.rb b/actionview/lib/action_view/gem_version.rb index 77ae444a58..02de3eeec2 100644 --- a/actionview/lib/action_view/gem_version.rb +++ b/actionview/lib/action_view/gem_version.rb @@ -10,7 +10,7 @@ module ActionView MAJOR = 6 MINOR = 0 TINY = 0 - PRE = "alpha" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index 3d7c8dae75..59d70a1dc4 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -329,14 +329,14 @@ module ActionView # image_tag("pic.jpg", srcset: [["pic_1024.jpg", "1024w"], ["pic_1980.jpg", "1980w"]], sizes: "100vw") # # => <img src="/assets/pic.jpg" srcset="/assets/pic_1024.jpg 1024w, /assets/pic_1980.jpg 1980w" sizes="100vw"> # - # Active Storage (images that are uploaded by the users of your app): + # Active Storage blobs (images that are uploaded by the users of your app): # # image_tag(user.avatar) # # => <img src="/rails/active_storage/blobs/.../tiger.jpg" /> - # image_tag(user.avatar.variant(resize_to_fit: [100, 100])) - # # => <img src="/rails/active_storage/variants/.../tiger.jpg" /> - # image_tag(user.avatar.variant(resize_to_fit: [100, 100]), size: '100') - # # => <img width="100" height="100" src="/rails/active_storage/variants/.../tiger.jpg" /> + # image_tag(user.avatar.variant(resize_to_limit: [100, 100])) + # # => <img src="/rails/active_storage/representations/.../tiger.jpg" /> + # image_tag(user.avatar.variant(resize_to_limit: [100, 100]), size: '100') + # # => <img width="100" height="100" src="/rails/active_storage/representations/.../tiger.jpg" /> def image_tag(source, options = {}) options = options.symbolize_keys check_for_image_tag_errors(options) @@ -355,29 +355,6 @@ module ActionView tag("img", options) end - # Returns a string suitable for an HTML image tag alt attribute. - # The +src+ argument is meant to be an image file path. - # The method removes the basename of the file path and the digest, - # if any. It also removes hyphens and underscores from file names and - # replaces them with spaces, returning a space-separated, titleized - # string. - # - # ==== Examples - # - # image_alt('rails.png') - # # => Rails - # - # image_alt('hyphenated-file-name.png') - # # => Hyphenated file name - # - # image_alt('underscored_file_name.png') - # # => Underscored file name - def image_alt(src) - ActiveSupport::Deprecation.warn("image_alt is deprecated and will be removed from Rails 6.0. You must explicitly set alt text on images.") - - File.basename(src, ".*").sub(/-[[:xdigit:]]{32,64}\z/, "").tr("-_", " ").capitalize - end - # Returns an HTML video tag for the +sources+. If +sources+ is a string, # a single video tag will be returned. If +sources+ is an array, a video # tag with nested source tags for each source will be returned. The diff --git a/actionview/lib/action_view/helpers/csrf_helper.rb b/actionview/lib/action_view/helpers/csrf_helper.rb index 69c59844a6..c0422c6ff5 100644 --- a/actionview/lib/action_view/helpers/csrf_helper.rb +++ b/actionview/lib/action_view/helpers/csrf_helper.rb @@ -20,7 +20,7 @@ module ActionView # "X-CSRF-Token" HTTP header. If you are using rails-ujs this happens automatically. # def csrf_meta_tags - if protect_against_forgery? + if defined?(protect_against_forgery?) && protect_against_forgery? [ tag("meta", name: "csrf-param", content: request_forgery_protection_token), tag("meta", name: "csrf-token", content: form_authenticity_token) diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb index ebdd96f570..a7747456a4 100644 --- a/actionview/lib/action_view/helpers/form_options_helper.rb +++ b/actionview/lib/action_view/helpers/form_options_helper.rb @@ -654,7 +654,7 @@ module ActionView # # ==== Gotcha # - # The HTML specification says when nothing is select on a collection of radio buttons + # The HTML specification says when nothing is selected on a collection of radio buttons # web browsers do not send any value to server. # Unfortunately this introduces a gotcha: # if a +User+ model has a +category_id+ field and in the form no category is selected, no +category_id+ parameter is sent. So, diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index 948dd1551f..d63ada3890 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -618,7 +618,7 @@ module ActionView end def token_tag(token = nil, form_options: {}) - if token != false && protect_against_forgery? + if token != false && defined?(protect_against_forgery?) && protect_against_forgery? token ||= form_authenticity_token(form_options: form_options) tag(:input, type: "hidden", name: request_forgery_protection_token.to_s, value: token) else diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index 554d223c0e..c2f6439e26 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -3,6 +3,7 @@ require "concurrent/map" require "active_support/core_ext/module/remove_method" require "active_support/core_ext/module/attribute_accessors" +require "active_support/deprecation" require "action_view/template/resolver" module ActionView @@ -106,12 +107,6 @@ module ActionView module ViewPaths attr_reader :view_paths, :html_fallback_for_js - # Whenever setting view paths, makes a copy so that we can manipulate them in - # instance objects as we wish. - def view_paths=(paths) - @view_paths = ActionView::PathSet.new(Array(paths)) - end - def find(name, prefixes = [], partial = false, keys = [], options = {}) @view_paths.find(*args_for_lookup(name, prefixes, partial, keys, options)) end @@ -138,19 +133,34 @@ module ActionView # Adds fallbacks to the view paths. Useful in cases when you are rendering # a :file. def with_fallbacks - added_resolvers = 0 - self.class.fallbacks.each do |resolver| - next if view_paths.include?(resolver) - view_paths.push(resolver) - added_resolvers += 1 + view_paths = build_view_paths((@view_paths.paths + self.class.fallbacks).uniq) + + if block_given? + ActiveSupport::Deprecation.warn <<~eowarn + Calling `with_fallbacks` with a block is deprecated. Call methods on + the lookup context returned by `with_fallbacks` instead. + eowarn + + begin + _view_paths = @view_paths + @view_paths = view_paths + yield + ensure + @view_paths = _view_paths + end + else + ActionView::LookupContext.new(view_paths, @details, @prefixes) end - yield - ensure - added_resolvers.times { view_paths.pop } end private + # Whenever setting view paths, makes a copy so that we can manipulate them in + # instance objects as we wish. + def build_view_paths(paths) + ActionView::PathSet.new(Array(paths)) + end + def args_for_lookup(name, prefixes, partial, keys, details_options) name, prefixes = normalize_name(name, prefixes) details, details_key = detail_args_for(details_options) @@ -226,7 +236,7 @@ module ActionView @rendered_format = nil @details = initialize_details({}, details) - self.view_paths = view_paths + @view_paths = build_view_paths(view_paths) end def digest_cache diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb index 20b2523cac..ae366ce54a 100644 --- a/actionview/lib/action_view/renderer/abstract_renderer.rb +++ b/actionview/lib/action_view/renderer/abstract_renderer.rb @@ -17,7 +17,7 @@ module ActionView # that new object is called in turn. This abstracts the setup and rendering # into a separate classes for partials and templates. class AbstractRenderer #:nodoc: - delegate :find_template, :find_file, :template_exists?, :any_templates?, :with_fallbacks, :with_layout_format, :formats, to: :@lookup_context + delegate :template_exists?, :any_templates?, :formats, to: :@lookup_context def initialize(lookup_context) @lookup_context = lookup_context @@ -38,8 +38,6 @@ module ActionView end def instrument(name, **options) # :doc: - options[:identifier] ||= (@template && @template.identifier) || @path - ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload| yield payload end diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index cb850d75ee..478400e016 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -296,42 +296,43 @@ module ActionView def render(context, options, block) setup(context, options, block) - @template = find_partial + template = find_partial @lookup_context.rendered_format ||= begin - if @template && @template.formats.present? - @template.formats.first + if template && template.formats.first + template.formats.first else formats.first end end if @collection - render_collection + render_collection(context, template) else - render_partial + render_partial(context, template) end end private - def render_collection - instrument(:collection, count: @collection.size) do |payload| + def render_collection(view, template) + identifier = (template && template.identifier) || @path + instrument(:collection, identifier: identifier, count: @collection.size) do |payload| return nil if @collection.blank? if @options.key?(:spacer_template) - spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals) + spacer = find_template(@options[:spacer_template], @locals.keys).render(view, @locals) end - cache_collection_render(payload) do - @template ? collection_with_template : collection_without_template + cache_collection_render(payload, view, template) do + template ? collection_with_template(view, template) : collection_without_template(view) end.join(spacer).html_safe end end - def render_partial - instrument(:partial) do |payload| - view, locals, block = @view, @locals, @block + def render_partial(view, template) + instrument(:partial, identifier: template.identifier) do |payload| + locals, block = @locals, @block object, as = @object, @variable if !block && (layout = @options[:layout]) @@ -341,12 +342,12 @@ module ActionView object = locals[as] if object.nil? # Respect object when object is false locals[as] = object if @has_object - content = @template.render(view, locals) do |*name| + content = template.render(view, locals) do |*name| view._layout_for(*name, &block) end content = layout.render(view, locals) { content } if layout - payload[:cache_hit] = view.view_renderer.cache_hits[@template.virtual_path] + payload[:cache_hit] = view.view_renderer.cache_hits[template.virtual_path] content end end @@ -359,11 +360,10 @@ module ActionView # set to that string. Otherwise, the +options[:partial]+ object must # respond to +to_partial_path+ in order to setup the path. def setup(context, options, block) - @view = context @options = options @block = block - @locals = options[:locals] ? options[:locals].symbolize_keys : {} + @locals = options[:locals] || {} @details = extract_details(options) prepend_formats(options[:formats]) @@ -381,10 +381,10 @@ module ActionView @collection = collection_from_object || collection_from_options if @collection - paths = @collection_data = @collection.map { |o| partial_path(o) } + paths = @collection_data = @collection.map { |o| partial_path(o, context) } @path = paths.uniq.one? ? paths.first : nil else - @path = partial_path + @path = partial_path(@object, context) end end @@ -423,8 +423,8 @@ module ActionView @lookup_context.find_template(path, prefixes, true, locals, @details) end - def collection_with_template - view, locals, template = @view, @locals, @template + def collection_with_template(view, template) + locals = @locals as, counter, iteration = @variable, @variable_counter, @variable_iteration if layout = @options[:layout] @@ -445,8 +445,8 @@ module ActionView end end - def collection_without_template - view, locals, collection_data = @view, @locals, @collection_data + def collection_without_template(view) + locals, collection_data = @locals, @collection_data cache = {} keys = @locals.keys @@ -474,7 +474,7 @@ module ActionView # # If +prefix_partial_path_with_controller_namespace+ is true, then this # method will prefix the partial paths with a namespace. - def partial_path(object = @object) + def partial_path(object, view) object = object.to_model if object.respond_to?(:to_model) path = if object.respond_to?(:to_partial_path) @@ -483,7 +483,7 @@ module ActionView raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.") end - if @view.prefix_partial_path_with_controller_namespace + if view.prefix_partial_path_with_controller_namespace prefixed_partial_names[path] ||= merge_prefix_into_object_path(@context_prefix, path.dup) else path diff --git a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb index 5aa6f77902..388f9e5e56 100644 --- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb +++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb @@ -11,13 +11,13 @@ module ActionView end private - def cache_collection_render(instrumentation_payload) + def cache_collection_render(instrumentation_payload, view, template) return yield unless @options[:cached] # Result is a hash with the key represents the # key used for cache lookup and the value is the item # on which the partial is being rendered - keyed_collection = collection_by_cache_keys + keyed_collection = collection_by_cache_keys(view, template) # Pull all partials from cache # Result is a hash, key matches the entry in @@ -51,23 +51,21 @@ module ActionView @options[:cached].respond_to?(:call) end - def collection_by_cache_keys + def collection_by_cache_keys(view, template) seed = callable_cache_key? ? @options[:cached] : ->(i) { i } + digest_path = view.digest_path_from_virtual(template.virtual_path) + @collection.each_with_object({}) do |item, hash| - hash[expanded_cache_key(seed.call(item))] = item + hash[expanded_cache_key(seed.call(item), view, template, digest_path)] = item end end - def expanded_cache_key(key) - key = @view.combined_fragment_cache_key(@view.cache_fragment_name(key, virtual_path: @template.virtual_path, digest_path: digest_path)) + def expanded_cache_key(key, view, template, digest_path) + key = view.combined_fragment_cache_key(view.cache_fragment_name(key, virtual_path: template.virtual_path, digest_path: digest_path)) key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0. end - def digest_path - @digest_path ||= @view.digest_path_from_virtual(@template.virtual_path) - end - # `order_by` is an enumerable object containing keys of the cache, # all keys are passed in whether found already or not. # diff --git a/actionview/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb index bb9db21e32..f414620923 100644 --- a/actionview/lib/action_view/renderer/streaming_template_renderer.rb +++ b/actionview/lib/action_view/renderer/streaming_template_renderer.rb @@ -43,14 +43,14 @@ module ActionView # For streaming, instead of rendering a given a template, we return a Body # object that responds to each. This object is initialized with a block # that knows how to render the template. - def render_template(template, layout_name = nil, locals = {}) #:nodoc: + def render_template(view, template, layout_name = nil, locals = {}) #:nodoc: return [super] unless layout_name && template.supports_streaming? locals ||= {} layout = layout_name && find_layout(layout_name, locals.keys, [formats.first]) Body.new do |buffer| - delayed_render(buffer, template, layout, @view, locals) + delayed_render(buffer, template, layout, view, locals) end end diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb index ce8908924a..c36baeffcd 100644 --- a/actionview/lib/action_view/renderer/template_renderer.rb +++ b/actionview/lib/action_view/renderer/template_renderer.rb @@ -5,7 +5,6 @@ require "active_support/core_ext/object/try" module ActionView class TemplateRenderer < AbstractRenderer #:nodoc: def render(context, options) - @view = context @details = extract_details(options) template = determine_template(options) @@ -13,7 +12,7 @@ module ActionView @lookup_context.rendered_format ||= (template.formats.first || formats.first) - render_template(template, options[:layout], options[:locals]) + render_template(context, template, options[:layout], options[:locals] || {}) end private @@ -29,7 +28,7 @@ module ActionView elsif options.key?(:html) Template::HTML.new(options[:html], formats.first) elsif options.key?(:file) - with_fallbacks { find_file(options[:file], nil, false, keys, @details) } + @lookup_context.with_fallbacks.find_file(options[:file], nil, false, keys, @details) elsif options.key?(:inline) handler = Template.handler_for_extension(options[:type] || "erb") Template.new(options[:inline], "inline template", handler, locals: keys) @@ -37,7 +36,7 @@ module ActionView if options[:template].respond_to?(:render) options[:template] else - find_template(options[:template], options[:prefixes], false, keys, @details) + @lookup_context.find_template(options[:template], options[:prefixes], false, keys, @details) end else raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :html or :body option." @@ -46,22 +45,19 @@ module ActionView # Renders the given template. A string representing the layout can be # supplied as well. - def render_template(template, layout_name = nil, locals = nil) - view, locals = @view, locals || {} - - render_with_layout(layout_name, locals) do |layout| + def render_template(view, template, layout_name, locals) + render_with_layout(view, layout_name, locals) do |layout| instrument(:template, identifier: template.identifier, layout: layout.try(:virtual_path)) do template.render(view, locals) { |*name| view._layout_for(*name) } end end end - def render_with_layout(path, locals) + def render_with_layout(view, path, locals) layout = path && find_layout(path, locals.keys, [formats.first]) content = yield(layout) if layout - view = @view view.view_flow.set(:layout, content) layout.render(view, locals) { |*name| view._layout_for(*name) } else @@ -84,9 +80,9 @@ module ActionView when String begin if layout.start_with?("/") - with_fallbacks { find_template(layout, nil, false, [], details) } + @lookup_context.with_fallbacks.find_template(layout, nil, false, [], details) else - find_template(layout, nil, false, [], details) + @lookup_context.find_template(layout, nil, false, [], details) end rescue ActionView::MissingTemplate all_details = @details.merge(formats: @lookup_context.default_formats) diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb index cb4327cf16..205665a8c6 100644 --- a/actionview/lib/action_view/rendering.rb +++ b/actionview/lib/action_view/rendering.rb @@ -55,10 +55,8 @@ module ActionView end end - attr_internal_writer :view_context_class - def view_context_class - @_view_context_class ||= self.class.view_context_class + self.class.view_context_class end # An instance of a view class. The default view class is ActionView::Base. diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb index 070d82cf17..3b2c264ed4 100644 --- a/actionview/lib/action_view/template.rb +++ b/actionview/lib/action_view/template.rb @@ -3,6 +3,7 @@ require "active_support/core_ext/object/try" require "active_support/core_ext/kernel/singleton_class" require "thread" +require "delegate" module ActionView # = Action View Template @@ -155,10 +156,10 @@ module ActionView # This method is instrumented as "!render_template.action_view". Notice that # we use a bang in this instrumentation because you don't want to # consume this in production. This is only slow if it's being listened to. - def render(view, locals, buffer = nil, &block) + def render(view, locals, buffer = ActionView::OutputBuffer.new, &block) instrument_render_template do compile!(view) - view.send(method_name, locals, buffer, &block) + view.run(method_name, locals, buffer, &block) end rescue => e handle_render_error(view, e) @@ -202,7 +203,9 @@ module ActionView # before passing the source on to the template engine, leaving a # blank line in its stead. def encode! - return unless source.encoding == Encoding::BINARY + source = self.source + + return source unless source.encoding == Encoding::BINARY # Look for # encoding: *. If we find one, we'll encode the # String in that encoding, otherwise, we'll use the @@ -264,11 +267,7 @@ module ActionView # re-compilation return if @compiled - if view.is_a?(ActionView::CompiledTemplates) - mod = ActionView::CompiledTemplates - else - mod = view.singleton_class - end + mod = view.compiled_method_container instrument("!compile_template") do compile(mod) @@ -281,6 +280,15 @@ module ActionView end end + class LegacyTemplate < DelegateClass(Template) # :nodoc: + attr_reader :source + + def initialize(template, source) + super(template) + @source = source + end + end + # Among other things, this method is responsible for properly setting # the encoding of the compiled template. # @@ -294,16 +302,14 @@ module ActionView # In general, this means that templates will be UTF-8 inside of Rails, # regardless of the original source encoding. def compile(mod) - encode! - code = @handler.call(self) + source = encode! + code = @handler.call(self, source) # Make sure that the resulting String to be eval'd is in the # encoding of the code source = +<<-end_src def #{method_name}(local_assigns, output_buffer) - _old_virtual_path, @virtual_path = @virtual_path, #{@virtual_path.inspect};_old_output_buffer = @output_buffer;#{locals_code};#{code} - ensure - @virtual_path, @output_buffer = _old_virtual_path, _old_output_buffer + @virtual_path = #{@virtual_path.inspect};#{locals_code};#{code} end end_src @@ -318,7 +324,7 @@ module ActionView # handler is valid in the default_internal. This is for handlers # that handle encoding but screw up unless source.valid_encoding? - raise WrongEncodingError.new(@source, Encoding.default_internal) + raise WrongEncodingError.new(source, Encoding.default_internal) end mod.module_eval(source, identifier, 0) diff --git a/actionview/lib/action_view/template/handlers.rb b/actionview/lib/action_view/template/handlers.rb index 7ec76dcc3f..ddaac7a100 100644 --- a/actionview/lib/action_view/template/handlers.rb +++ b/actionview/lib/action_view/template/handlers.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/deprecation" + module ActionView #:nodoc: # = Action View Template Handlers class Template #:nodoc: @@ -14,7 +16,7 @@ module ActionView #:nodoc: base.register_template_handler :erb, ERB.new base.register_template_handler :html, Html.new base.register_template_handler :builder, Builder.new - base.register_template_handler :ruby, :source.to_proc + base.register_template_handler :ruby, lambda { |_, source| source } end @@template_handlers = {} @@ -24,11 +26,35 @@ module ActionView #:nodoc: @@template_extensions ||= @@template_handlers.keys end + class LegacyHandlerWrapper < SimpleDelegator # :nodoc: + def call(view, source) + __getobj__.call(ActionView::Template::LegacyTemplate.new(view, source)) + end + end + # Register an object that knows how to handle template files with the given # extensions. This can be used to implement new template types. # The handler must respond to +:call+, which will be passed the template # and should return the rendered template as a String. def register_template_handler(*extensions, handler) + params = if handler.is_a?(Proc) + handler.parameters + else + handler.method(:call).parameters + end + + unless params.find_all { |type, _| type == :req || type == :opt }.length >= 2 + ActiveSupport::Deprecation.warn <<~eowarn + Single arity template handlers are deprecated. Template handlers must + now accept two parameters, the view object and the source for the view object. + Change: + >> #{handler.class}#call(#{params.map(&:last).join(", ")}) + To: + >> #{handler.class}#call(#{params.map(&:last).join(", ")}, source) + eowarn + handler = LegacyHandlerWrapper.new(handler) + end + raise(ArgumentError, "Extension is required") if extensions.empty? extensions.each do |extension| @@template_handlers[extension.to_sym] = handler diff --git a/actionview/lib/action_view/template/handlers/builder.rb b/actionview/lib/action_view/template/handlers/builder.rb index 61492ce448..e5413cd2a0 100644 --- a/actionview/lib/action_view/template/handlers/builder.rb +++ b/actionview/lib/action_view/template/handlers/builder.rb @@ -5,11 +5,11 @@ module ActionView class Builder class_attribute :default_format, default: :xml - def call(template) + def call(template, source) require_engine "xml = ::Builder::XmlMarkup.new(:indent => 2);" \ "self.output_buffer = xml.target!;" + - template.source + + source + ";xml.target!;" end diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb index 7d1a6767d7..b6314a5bc3 100644 --- a/actionview/lib/action_view/template/handlers/erb.rb +++ b/actionview/lib/action_view/template/handlers/erb.rb @@ -28,8 +28,8 @@ module ActionView ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*") - def self.call(template) - new.call(template) + def self.call(template, source) + new.call(template, source) end def supports_streaming? @@ -40,17 +40,17 @@ module ActionView true end - def call(template) + def call(template, source) # First, convert to BINARY, so in case the encoding is # wrong, we can still find an encoding tag # (<%# encoding %>) inside the String using a regular # expression - template_source = template.source.dup.force_encoding(Encoding::ASCII_8BIT) + template_source = source.dup.force_encoding(Encoding::ASCII_8BIT) erb = template_source.gsub(ENCODING_TAG, "") encoding = $2 - erb.force_encoding valid_encoding(template.source.dup, encoding) + erb.force_encoding valid_encoding(source.dup, encoding) # Always make sure we return a String in the default_internal erb.encode! diff --git a/actionview/lib/action_view/template/handlers/erb/erubi.rb b/actionview/lib/action_view/template/handlers/erb/erubi.rb index db75f028ed..15ca202024 100644 --- a/actionview/lib/action_view/template/handlers/erb/erubi.rb +++ b/actionview/lib/action_view/template/handlers/erb/erubi.rb @@ -13,7 +13,7 @@ module ActionView # Dup properties so that we don't modify argument properties = Hash[properties] - properties[:preamble] = "@output_buffer = output_buffer || ActionView::OutputBuffer.new;" + properties[:preamble] = "" properties[:postamble] = "@output_buffer.to_s" properties[:bufvar] = "@output_buffer" properties[:escapefunc] = "" @@ -22,8 +22,12 @@ module ActionView end def evaluate(action_view_erb_handler_context) - pr = eval("proc { #{@src} }", binding, @filename || "(erubi)") - action_view_erb_handler_context.instance_eval(&pr) + src = @src + view = Class.new(ActionView::Base) { + include action_view_erb_handler_context._routes.url_helpers + class_eval("define_method(:_template) { |local_assigns, output_buffer| #{src} }", @filename || "(erubi)", 0) + }.with_context(action_view_erb_handler_context) + view.run(:_template, {}, ActionView::OutputBuffer.new) end private diff --git a/actionview/lib/action_view/template/handlers/html.rb b/actionview/lib/action_view/template/handlers/html.rb index 27004a318c..65857d8587 100644 --- a/actionview/lib/action_view/template/handlers/html.rb +++ b/actionview/lib/action_view/template/handlers/html.rb @@ -3,7 +3,7 @@ module ActionView module Template::Handlers class Html < Raw - def call(template) + def call(template, source) "ActionView::OutputBuffer.new #{super}" end end diff --git a/actionview/lib/action_view/template/handlers/raw.rb b/actionview/lib/action_view/template/handlers/raw.rb index 5cd23a0060..57ebb169fc 100644 --- a/actionview/lib/action_view/template/handlers/raw.rb +++ b/actionview/lib/action_view/template/handlers/raw.rb @@ -3,8 +3,8 @@ module ActionView module Template::Handlers class Raw - def call(template) - "#{template.source.inspect}.html_safe;" + def call(template, source) + "#{source.inspect}.html_safe;" end end end diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb index 12ae82f8c5..3b4594942b 100644 --- a/actionview/lib/action_view/template/resolver.rb +++ b/actionview/lib/action_view/template/resolver.rb @@ -226,9 +226,8 @@ module ActionView template_paths.map do |template| handler, format, variant = extract_handler_and_format_and_variant(template) - contents = File.binread(template) - Template.new(contents, File.expand_path(template), handler, + FileTemplate.new(File.expand_path(template), handler, virtual_path: path.virtual, format: format, variant: variant, diff --git a/actionview/lib/action_view/testing/resolvers.rb b/actionview/lib/action_view/testing/resolvers.rb index 1fad08a689..d6203b95c5 100644 --- a/actionview/lib/action_view/testing/resolvers.rb +++ b/actionview/lib/action_view/testing/resolvers.rb @@ -8,13 +8,15 @@ module ActionView #:nodoc: # useful for testing extensions that have no way of knowing what the file # system will look like at runtime. class FixtureResolver < PathResolver - attr_reader :hash - def initialize(hash = {}, pattern = nil) super(pattern) @hash = hash end + def data + @hash + end + def to_s @hash.keys.join(", ") end diff --git a/actionview/package.json b/actionview/package.json index 1f74df79d3..eb2a9f879e 100644 --- a/actionview/package.json +++ b/actionview/package.json @@ -1,6 +1,6 @@ { - "name": "rails-ujs", - "version": "6.0.0-alpha", + "name": "@rails/ujs", + "version": "6.0.0-beta1", "description": "Ruby on Rails unobtrusive scripting adapter", "main": "lib/assets/compiled/rails-ujs.js", "files": [ diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb index f90626ad9e..acc2ed029b 100644 --- a/actionview/test/abstract_unit.rb +++ b/actionview/test/abstract_unit.rb @@ -48,7 +48,7 @@ module RenderERBUtils @view ||= begin path = ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH) view_paths = ActionView::PathSet.new([path]) - ActionView::Base.new(view_paths) + ActionView::Base.with_view_paths(view_paths) end end @@ -58,10 +58,10 @@ module RenderERBUtils template = ActionView::Template.new( string.strip, "test template", - ActionView::Template::Handlers::ERB, + ActionView::Template.handler_for_extension(:erb), {}) - template.render(self, {}).strip + template.render(ActionView::Base.empty, {}).strip end end diff --git a/actionview/test/actionpack/controller/layout_test.rb b/actionview/test/actionpack/controller/layout_test.rb index 6d5c97b7fd..838b564c5d 100644 --- a/actionview/test/actionpack/controller/layout_test.rb +++ b/actionview/test/actionpack/controller/layout_test.rb @@ -70,7 +70,7 @@ class LayoutAutoDiscoveryTest < ActionController::TestCase end def test_third_party_template_library_auto_discovers_layout - with_template_handler :mab, lambda { |template| template.source.inspect } do + with_template_handler :mab, lambda { |template, source| source.inspect } do @controller = ThirdPartyTemplateLibraryController.new get :hello assert_response :success @@ -212,7 +212,7 @@ class LayoutSetInResponseTest < ActionController::TestCase end def test_layout_set_when_using_render - with_template_handler :mab, lambda { |template| template.source.inspect } do + with_template_handler :mab, lambda { |template, source| source.inspect } do @controller = SetsLayoutInRenderController.new get :hello assert_includes @response.body, "layouts/third_party_template_library.mab" diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb index 204903c60c..727d3fbc1a 100644 --- a/actionview/test/actionpack/controller/render_test.rb +++ b/actionview/test/actionpack/controller/render_test.rb @@ -485,8 +485,8 @@ class TestController < ActionController::Base render partial: "customer", locals: { customer: Customer.new("david") } end - def partial_with_string_locals - render partial: "customer", locals: { "customer" => Customer.new("david") } + def partial_with_hashlike_locals + render partial: "customer", locals: ActionController::Parameters.new(customer: Customer.new("david")) end def partial_with_form_builder @@ -691,7 +691,7 @@ class RenderTest < ActionController::TestCase get :partial_with_locals, to: "test#partial_with_locals" get :partial_with_nested_object, to: "test#partial_with_nested_object" get :partial_with_nested_object_shorthand, to: "test#partial_with_nested_object_shorthand" - get :partial_with_string_locals, to: "test#partial_with_string_locals" + get :partial_with_hashlike_locals, to: "test#partial_with_hashlike_locals" get :partials_list, to: "test#partials_list" get :render_action_hello_world, to: "test#render_action_hello_world" get :render_action_hello_world_as_string, to: "test#render_action_hello_world_as_string" @@ -1292,8 +1292,8 @@ class RenderTest < ActionController::TestCase assert_equal "Hello: david", @response.body end - def test_partial_with_string_locals - get :partial_with_string_locals + def test_partial_with_hashlike_locals + get :partial_with_hashlike_locals assert_equal "Hello: david", @response.body end diff --git a/actionview/test/actionpack/controller/view_paths_test.rb b/actionview/test/actionpack/controller/view_paths_test.rb index 7f3fe0fa08..c5238dd746 100644 --- a/actionview/test/actionpack/controller/view_paths_test.rb +++ b/actionview/test/actionpack/controller/view_paths_test.rb @@ -77,7 +77,7 @@ class ViewLoadPathsTest < ActionController::TestCase end def test_template_appends_view_path_correctly - @controller.instance_variable_set :@template, ActionView::Base.new(TestController.view_paths, {}, @controller) + @controller.instance_variable_set :@template, ActionView::Base.with_view_paths(TestController.view_paths, {}, @controller) class_view_paths = TestController.view_paths @controller.append_view_path "foo" @@ -89,7 +89,7 @@ class ViewLoadPathsTest < ActionController::TestCase end def test_template_prepends_view_path_correctly - @controller.instance_variable_set :@template, ActionView::Base.new(TestController.view_paths, {}, @controller) + @controller.instance_variable_set :@template, ActionView::Base.with_view_paths(TestController.view_paths, {}, @controller) class_view_paths = TestController.view_paths @controller.prepend_view_path "baz" diff --git a/actionview/test/activerecord/multifetch_cache_test.rb b/actionview/test/activerecord/multifetch_cache_test.rb index 12be069e69..229b4e56d0 100644 --- a/actionview/test/activerecord/multifetch_cache_test.rb +++ b/actionview/test/activerecord/multifetch_cache_test.rb @@ -19,7 +19,7 @@ class MultifetchCacheTest < ActiveRecordTestCase def combined_fragment_cache_key(key) [ :views, key ] end - end.new(view_paths, {}) + end.with_view_paths(view_paths, {}) end def test_only_preloading_for_records_that_miss_the_cache diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb index e68f03d1f4..e371a87614 100644 --- a/actionview/test/template/asset_tag_helper_test.rb +++ b/actionview/test/template/asset_tag_helper_test.rb @@ -512,26 +512,6 @@ class AssetTagHelperTest < ActionView::TestCase UrlToImageToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end - def test_image_alt - [nil, "/", "/foo/bar/", "foo/bar/"].each do |prefix| - assert_deprecated do - assert_equal "Rails", image_alt("#{prefix}rails.png") - end - assert_deprecated do - assert_equal "Rails", image_alt("#{prefix}rails-9c0a079bdd7701d7e729bd956823d153.png") - end - assert_deprecated do - assert_equal "Rails", image_alt("#{prefix}rails-f56ef62bc41b040664e801a38f068082a75d506d9048307e8096737463503d0b.png") - end - assert_deprecated do - assert_equal "Long file name with hyphens", image_alt("#{prefix}long-file-name-with-hyphens.png") - end - assert_deprecated do - assert_equal "Long file name with underscores", image_alt("#{prefix}long_file_name_with_underscores.png") - end - end - end - def test_image_tag ImageLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end diff --git a/actionview/test/template/capture_helper_test.rb b/actionview/test/template/capture_helper_test.rb index e172497c88..45070674ad 100644 --- a/actionview/test/template/capture_helper_test.rb +++ b/actionview/test/template/capture_helper_test.rb @@ -5,7 +5,7 @@ require "abstract_unit" class CaptureHelperTest < ActionView::TestCase def setup super - @av = ActionView::Base.new + @av = ActionView::Base.empty @view_flow = ActionView::OutputFlow.new end diff --git a/actionview/test/template/compiled_templates_test.rb b/actionview/test/template/compiled_templates_test.rb index 3cd6448e38..ded4786e62 100644 --- a/actionview/test/template/compiled_templates_test.rb +++ b/actionview/test/template/compiled_templates_test.rb @@ -72,13 +72,13 @@ class CompiledTemplatesTest < ActiveSupport::TestCase def render_with_cache(*args) view_paths = ActionController::Base.view_paths - ActionView::Base.new(view_paths, {}).render(*args) + ActionView::Base.with_view_paths(view_paths, {}).render(*args) end def render_without_cache(*args) path = ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH) view_paths = ActionView::PathSet.new([path]) - ActionView::Base.new(view_paths, {}).render(*args) + ActionView::Base.with_view_paths(view_paths, {}).render(*args) end def modify_template(template, content) diff --git a/actionview/test/template/csrf_helper_test.rb b/actionview/test/template/csrf_helper_test.rb new file mode 100644 index 0000000000..dd9821eb6c --- /dev/null +++ b/actionview/test/template/csrf_helper_test.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class CsrfHelperTest < ActiveSupport::TestCase + cattr_accessor :request_forgery, default: false + + include ActionView::Helpers::CsrfHelper + include ActionView::Helpers::TagHelper + include Rails::Dom::Testing::Assertions::DomAssertions + + def test_csrf_meta_tags_without_request_forgery_protection + assert_dom_equal "", csrf_meta_tags + end + + def test_csrf_meta_tags_with_request_forgery_protection + self.request_forgery = true + + assert_dom_equal <<~DOM.chomp, csrf_meta_tags + <meta name="csrf-param" content="form_token" /> + <meta name="csrf-token" content="secret" /> + DOM + ensure + self.request_forgery = false + end + + def test_csrf_meta_tags_without_protect_against_forgery_method + self.class.undef_method(:protect_against_forgery?) + + assert_dom_equal "", csrf_meta_tags + ensure + self.class.define_method(:protect_against_forgery?) { request_forgery } + end + + def protect_against_forgery? + request_forgery + end + + def form_authenticity_token(*args) + "secret" + end + + def request_forgery_protection_token + "form_token" + end +end diff --git a/actionview/test/template/dependency_tracker_test.rb b/actionview/test/template/dependency_tracker_test.rb index ef7aeac039..42cb14a18a 100644 --- a/actionview/test/template/dependency_tracker_test.rb +++ b/actionview/test/template/dependency_tracker_test.rb @@ -17,8 +17,8 @@ class FakeTemplate end end -Neckbeard = lambda { |template| template.source } -Bowtie = lambda { |template| template.source } +Neckbeard = lambda { |template, source| source } +Bowtie = lambda { |template, source| source } class DependencyTrackerTest < ActionView::TestCase def tracker diff --git a/actionview/test/template/lookup_context_test.rb b/actionview/test/template/lookup_context_test.rb index 68e151f154..290f832794 100644 --- a/actionview/test/template/lookup_context_test.rb +++ b/actionview/test/template/lookup_context_test.rb @@ -5,10 +5,14 @@ require "abstract_controller/rendering" class LookupContextTest < ActiveSupport::TestCase def setup - @lookup_context = ActionView::LookupContext.new(FIXTURE_LOAD_PATH, {}) + @lookup_context = build_lookup_context(FIXTURE_LOAD_PATH, {}) ActionView::LookupContext::DetailsKey.clear end + def build_lookup_context(paths, details) + ActionView::LookupContext.new(paths, details) + end + def teardown I18n.locale = :en end @@ -118,19 +122,32 @@ class LookupContextTest < ActiveSupport::TestCase test "adds fallbacks to view paths when required" do assert_equal 1, @lookup_context.view_paths.size - @lookup_context.with_fallbacks do - assert_equal 3, @lookup_context.view_paths.size - assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("") - assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("/") + assert_deprecated do + @lookup_context.with_fallbacks do + assert_equal 3, @lookup_context.view_paths.size + assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("") + assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("/") + end end + + @lookup_context = @lookup_context.with_fallbacks + + assert_equal 3, @lookup_context.view_paths.size + assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("") + assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("/") end test "add fallbacks just once in nested fallbacks calls" do - @lookup_context.with_fallbacks do + assert_deprecated do @lookup_context.with_fallbacks do - assert_equal 3, @lookup_context.view_paths.size + @lookup_context.with_fallbacks do + assert_equal 3, @lookup_context.view_paths.size + end end end + + @lookup_context = @lookup_context.with_fallbacks.with_fallbacks + assert_equal 3, @lookup_context.view_paths.size end test "generates a new details key for each details hash" do @@ -156,13 +173,13 @@ class LookupContextTest < ActiveSupport::TestCase end test "gives the key forward to the resolver, so it can be used as cache key" do - @lookup_context.view_paths = ActionView::FixtureResolver.new("test/_foo.erb" => "Foo") + @lookup_context = build_lookup_context(ActionView::FixtureResolver.new("test/_foo.erb" => "Foo"), {}) template = @lookup_context.find("foo", %w(test), true) assert_equal "Foo", template.source # Now we are going to change the template, but it won't change the returned template # since we will hit the cache. - @lookup_context.view_paths.first.hash["test/_foo.erb"] = "Bar" + @lookup_context.view_paths.first.data["test/_foo.erb"] = "Bar" template = @lookup_context.find("foo", %w(test), true) assert_equal "Foo", template.source @@ -185,7 +202,7 @@ class LookupContextTest < ActiveSupport::TestCase end test "can disable the cache on demand" do - @lookup_context.view_paths = ActionView::FixtureResolver.new("test/_foo.erb" => "Foo") + @lookup_context = build_lookup_context(ActionView::FixtureResolver.new("test/_foo.erb" => "Foo"), {}) old_template = @lookup_context.find("foo", %w(test), true) template = @lookup_context.find("foo", %w(test), true) @@ -221,12 +238,12 @@ class LookupContextWithFalseCaching < ActiveSupport::TestCase # Now we are going to change the template, but it won't change the returned template # since the timestamp is the same. - @resolver.hash["test/_foo.erb"][0] = "Bar" + @resolver.data["test/_foo.erb"][0] = "Bar" template = @lookup_context.find("foo", %w(test), true) assert_equal "Foo", template.source # Now update the timestamp. - @resolver.hash["test/_foo.erb"][1] = Time.now.utc + @resolver.data["test/_foo.erb"][1] = Time.now.utc template = @lookup_context.find("foo", %w(test), true) assert_equal "Bar", template.source end @@ -237,7 +254,7 @@ class LookupContextWithFalseCaching < ActiveSupport::TestCase template = @lookup_context.find("foo", %w(test), true) assert_equal "Foo", template.source - @resolver.hash.clear + @resolver.data.clear assert_raise ActionView::MissingTemplate do @lookup_context.find("foo", %w(test), true) end @@ -246,12 +263,12 @@ class LookupContextWithFalseCaching < ActiveSupport::TestCase test "if no template was cached in the first lookup, retrieval should work in the second call" do ActionView::Resolver.stub(:caching?, false) do - @resolver.hash.clear + @resolver.data.clear assert_raise ActionView::MissingTemplate do @lookup_context.find("foo", %w(test), true) end - @resolver.hash["test/_foo.erb"] = ["Foo", Time.utc(2000)] + @resolver.data["test/_foo.erb"] = ["Foo", Time.utc(2000)] template = @lookup_context.find("foo", %w(test), true) assert_equal "Foo", template.source end diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb index afe68b7ff0..f89d087c34 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -15,7 +15,7 @@ module RenderTestCases def combined_fragment_cache_key(key) [ :views, key ] end - end.new(paths, @assigns) + end.with_view_paths(paths, @assigns) @controller_view = TestController.new.view_context @@ -336,6 +336,16 @@ module RenderTestCases assert_equal "Hello: davidHello: mary", @view.render(partial: "test/customer", collection: customers) end + def test_deprecated_constructor + assert_deprecated do + ActionView::Base.new + end + + assert_deprecated do + ActionView::Base.new ["/a"] + end + end + def test_render_partial_without_object_does_not_put_partial_name_to_local_assigns assert_equal "false", @view.render(partial: "test/partial_name_in_local_assigns") end @@ -440,13 +450,31 @@ module RenderTestCases assert_equal "Hello, World!", @view.render(inline: "Hello, World!", type: :bar) end - CustomHandler = lambda do |template| + CustomHandler = lambda do |template, source| "@output_buffer = ''.dup\n" \ - "@output_buffer << 'source: #{template.source.inspect}'\n" + "@output_buffer << 'source: #{source.inspect}'\n" end def test_render_inline_with_render_from_to_proc - ActionView::Template.register_template_handler :ruby_handler, :source.to_proc + ActionView::Template.register_template_handler :ruby_handler, lambda { |_, source| source } + assert_equal "3", @view.render(inline: "(1 + 2).to_s", type: :ruby_handler) + ensure + ActionView::Template.unregister_template_handler :ruby_handler + end + + def test_render_inline_with_render_from_to_proc_deprecated + assert_deprecated do + ActionView::Template.register_template_handler :ruby_handler, :source.to_proc + end + assert_equal "3", @view.render(inline: "(1 + 2).to_s", type: :ruby_handler) + ensure + ActionView::Template.unregister_template_handler :ruby_handler + end + + def test_optional_second_arg_works_without_deprecation + assert_not_deprecated do + ActionView::Template.register_template_handler :ruby_handler, ->(view, source = nil) { source } + end assert_equal "3", @view.render(inline: "(1 + 2).to_s", type: :ruby_handler) ensure ActionView::Template.unregister_template_handler :ruby_handler diff --git a/actionview/test/template/streaming_render_test.rb b/actionview/test/template/streaming_render_test.rb index f196c42c4f..dda2095013 100644 --- a/actionview/test/template/streaming_render_test.rb +++ b/actionview/test/template/streaming_render_test.rb @@ -9,7 +9,7 @@ class SetupFiberedBase < ActiveSupport::TestCase def setup view_paths = ActionController::Base.view_paths @assigns = { secret: "in the sauce", name: nil } - @view = ActionView::Base.new(view_paths, @assigns) + @view = ActionView::Base.with_view_paths(view_paths, @assigns) @controller_view = TestController.new.view_context end diff --git a/actionview/test/template/template_test.rb b/actionview/test/template/template_test.rb index b348d1f17b..8257d97b7c 100644 --- a/actionview/test/template/template_test.rb +++ b/actionview/test/template/template_test.rb @@ -18,7 +18,7 @@ class TestERBTemplate < ActiveSupport::TestCase attr_accessor :formats end - class Context + class Context < ActionView::Base def initialize @output_buffer = "original" @virtual_path = nil diff --git a/actionview/test/template/test_case_test.rb b/actionview/test/template/test_case_test.rb index 976b6bc77e..ab3ababba4 100644 --- a/actionview/test/template/test_case_test.rb +++ b/actionview/test/template/test_case_test.rb @@ -52,7 +52,7 @@ module ActionView end test "retrieve non existing config values" do - assert_nil ActionView::Base.new.config.something_odd + assert_nil ActionView::Base.empty.config.something_odd end test "works without testing a helper module" do diff --git a/actionview/test/template/translation_helper_test.rb b/actionview/test/template/translation_helper_test.rb index e756348938..d04e68e182 100644 --- a/actionview/test/template/translation_helper_test.rb +++ b/actionview/test/template/translation_helper_test.rb @@ -36,7 +36,7 @@ class TranslationHelperTest < ActiveSupport::TestCase } } ) - @view = ::ActionView::Base.new(ActionController::Base.view_paths, {}) + @view = ::ActionView::Base.with_view_paths(ActionController::Base.view_paths, {}) end teardown do diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index 1ab28e4749..632b32f09f 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -119,6 +119,16 @@ class UrlHelperTest < ActiveSupport::TestCase ) end + def test_button_to_without_protect_against_forgery_method + self.class.undef_method(:protect_against_forgery?) + assert_dom_equal( + %{<form method="post" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /></form>}, + button_to("Hello", "http://www.example.com") + ) + ensure + self.class.define_method(:protect_against_forgery?) { request_forgery } + end + def test_button_to_with_straight_url assert_dom_equal %{<form method="post" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com") end diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md index 31253855d7..0ed937f4cc 100644 --- a/activejob/CHANGELOG.md +++ b/activejob/CHANGELOG.md @@ -1,3 +1,5 @@ +## Rails 6.0.0.beta1 (January 18, 2019) ## + * Return false instead of the job instance when `enqueue` is aborted. This will be the behavior in Rails 6.1 but it can be controlled now with diff --git a/activejob/MIT-LICENSE b/activejob/MIT-LICENSE index 274211f710..aedc21bca2 100644 --- a/activejob/MIT-LICENSE +++ b/activejob/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014-2018 David Heinemeier Hansson +Copyright (c) 2014-2019 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activejob/lib/active_job.rb b/activejob/lib/active_job.rb index 01fab4d918..5f20ef9b9f 100644 --- a/activejob/lib/active_job.rb +++ b/activejob/lib/active_job.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2014-2018 David Heinemeier Hansson +# Copyright (c) 2014-2019 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb index 4ab62f89b0..487cdd6d38 100644 --- a/activejob/lib/active_job/core.rb +++ b/activejob/lib/active_job/core.rb @@ -81,7 +81,7 @@ module ActiveJob @queue_name = self.class.queue_name @priority = self.class.priority @executions = 0 - @exception_executions = Hash.new(0) + @exception_executions = {} end # Returns a hash with the job data that can safely be passed to the diff --git a/activejob/lib/active_job/enqueuing.rb b/activejob/lib/active_job/enqueuing.rb index ce118c1e8a..5609d13f5f 100644 --- a/activejob/lib/active_job/enqueuing.rb +++ b/activejob/lib/active_job/enqueuing.rb @@ -67,7 +67,7 @@ module ActiveJob false else ActiveSupport::Deprecation.warn( - "Rails 6.0 will return false when the enqueing is aborted. Make sure your code doesn't depend on it" \ + "Rails 6.0 will return false when the enqueuing is aborted. Make sure your code doesn't depend on it" \ " returning the instance of the job and set `config.active_job.return_false_on_aborted_enqueue = true`" \ " to remove the deprecations." ) diff --git a/activejob/lib/active_job/exceptions.rb b/activejob/lib/active_job/exceptions.rb index 53984a4e49..9e00942a1c 100644 --- a/activejob/lib/active_job/exceptions.rb +++ b/activejob/lib/active_job/exceptions.rb @@ -30,21 +30,28 @@ module ActiveJob # class RemoteServiceJob < ActiveJob::Base # retry_on CustomAppException # defaults to 3s wait, 5 attempts # retry_on AnotherCustomAppException, wait: ->(executions) { executions * 2 } + # + # retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3 + # retry_on Net::OpenTimeout, Timeout::Error, wait: :exponentially_longer, attempts: 10 # retries at most 10 times for Net::OpenTimeout and Timeout::Error combined + # # To retry at most 10 times for each individual exception: + # # retry_on Net::OpenTimeout, wait: :exponentially_longer, attempts: 10 + # # retry_on Timeout::Error, wait: :exponentially_longer, attempts: 10 + # # retry_on(YetAnotherCustomAppException) do |job, error| # ExceptionNotifier.caught(error) # end - # retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3 - # retry_on Net::OpenTimeout, wait: :exponentially_longer, attempts: 10 # # def perform(*args) # # Might raise CustomAppException, AnotherCustomAppException, or YetAnotherCustomAppException for something domain specific # # Might raise ActiveRecord::Deadlocked when a local db deadlock is detected - # # Might raise Net::OpenTimeout when the remote service is down + # # Might raise Net::OpenTimeout or Timeout::Error when the remote service is down # end # end def retry_on(*exceptions, wait: 3.seconds, attempts: 5, queue: nil, priority: nil) rescue_from(*exceptions) do |error| - exception_executions[exceptions.to_s] += 1 + # Guard against jobs that were persisted before we started having individual executions counters per retry_on + self.exception_executions ||= {} + self.exception_executions[exceptions.to_s] = (exception_executions[exceptions.to_s] || 0) + 1 if exception_executions[exceptions.to_s] < attempts retry_job wait: determine_delay(wait), queue: queue, priority: priority, error: error diff --git a/activejob/lib/active_job/gem_version.rb b/activejob/lib/active_job/gem_version.rb index 770f70dc5e..2b894397e3 100644 --- a/activejob/lib/active_job/gem_version.rb +++ b/activejob/lib/active_job/gem_version.rb @@ -10,7 +10,7 @@ module ActiveJob MAJOR = 6 MINOR = 0 TINY = 0 - PRE = "alpha" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activejob/test/cases/exceptions_test.rb b/activejob/test/cases/exceptions_test.rb index 67327e0bbd..c88162bf58 100644 --- a/activejob/test/cases/exceptions_test.rb +++ b/activejob/test/cases/exceptions_test.rb @@ -4,157 +4,167 @@ require "helper" require "jobs/retry_job" require "models/person" -class ExceptionsTest < ActiveJob::TestCase +class ExceptionsTest < ActiveSupport::TestCase setup do JobBuffer.clear - skip if ActiveJob::Base.queue_adapter.is_a?(ActiveJob::QueueAdapters::InlineAdapter) + skip if adapter_skips_scheduling?(ActiveJob::Base.queue_adapter) end test "successfully retry job throwing exception against defaults" do - perform_enqueued_jobs do - RetryJob.perform_later "DefaultsError", 5 + RetryJob.perform_later "DefaultsError", 5 - assert_equal [ - "Raised DefaultsError for the 1st time", - "Raised DefaultsError for the 2nd time", - "Raised DefaultsError for the 3rd time", - "Raised DefaultsError for the 4th time", - "Successfully completed job" ], JobBuffer.values - end + assert_equal [ + "Raised DefaultsError for the 1st time", + "Raised DefaultsError for the 2nd time", + "Raised DefaultsError for the 3rd time", + "Raised DefaultsError for the 4th time", + "Successfully completed job" ], JobBuffer.values end test "successfully retry job throwing exception against higher limit" do - perform_enqueued_jobs do - RetryJob.perform_later "ShortWaitTenAttemptsError", 9 - assert_equal 9, JobBuffer.values.count - end + RetryJob.perform_later "ShortWaitTenAttemptsError", 9 + assert_equal 9, JobBuffer.values.count end - test "keeps the same attempts counter when several exceptions are listed in the same declaration" do + test "keeps the same attempts counter for several exceptions listed in the same retry_on declaration" do exceptions_to_raise = %w(FirstRetryableErrorOfTwo FirstRetryableErrorOfTwo FirstRetryableErrorOfTwo SecondRetryableErrorOfTwo SecondRetryableErrorOfTwo) assert_raises SecondRetryableErrorOfTwo do - perform_enqueued_jobs do - ExceptionRetryJob.perform_later(exceptions_to_raise) - end + RetryJob.perform_later(exceptions_to_raise, 5) + + assert_equal [ + "Raised FirstRetryableErrorOfTwo for the 1st time", + "Raised FirstRetryableErrorOfTwo for the 2nd time", + "Raised FirstRetryableErrorOfTwo for the 3rd time", + "Raised SecondRetryableErrorOfTwo for the 4th time", + "Raised SecondRetryableErrorOfTwo for the 5th time", + ], JobBuffer.values end end - test "keeps a separate attempts counter for each individual declaration" do - exceptions_to_raise = %w(FirstRetryableErrorOfTwo FirstRetryableErrorOfTwo FirstRetryableErrorOfTwo - DefaultsError DefaultsError) + test "keeps a separate attempts counter for each individual retry_on declaration" do + exceptions_to_raise = %w(DefaultsError DefaultsError DefaultsError DefaultsError + FirstRetryableErrorOfTwo FirstRetryableErrorOfTwo FirstRetryableErrorOfTwo) assert_nothing_raised do - perform_enqueued_jobs do - ExceptionRetryJob.perform_later(exceptions_to_raise) - end + RetryJob.perform_later(exceptions_to_raise, 10) + + assert_equal [ + "Raised DefaultsError for the 1st time", + "Raised DefaultsError for the 2nd time", + "Raised DefaultsError for the 3rd time", + "Raised DefaultsError for the 4th time", + "Raised FirstRetryableErrorOfTwo for the 5th time", + "Raised FirstRetryableErrorOfTwo for the 6th time", + "Raised FirstRetryableErrorOfTwo for the 7th time", + "Successfully completed job" + ], JobBuffer.values end end test "failed retry job when exception kept occurring against defaults" do - perform_enqueued_jobs do - RetryJob.perform_later "DefaultsError", 6 - assert_equal "Raised DefaultsError for the 5th time", JobBuffer.last_value - rescue DefaultsError - pass - end + RetryJob.perform_later "DefaultsError", 6 + assert_equal "Raised DefaultsError for the 5th time", JobBuffer.last_value + rescue DefaultsError + pass end test "failed retry job when exception kept occurring against higher limit" do - perform_enqueued_jobs do - RetryJob.perform_later "ShortWaitTenAttemptsError", 11 - assert_equal "Raised ShortWaitTenAttemptsError for the 10th time", JobBuffer.last_value - rescue ShortWaitTenAttemptsError - pass - end + RetryJob.perform_later "ShortWaitTenAttemptsError", 11 + assert_equal "Raised ShortWaitTenAttemptsError for the 10th time", JobBuffer.last_value + rescue ShortWaitTenAttemptsError + pass end test "discard job" do - perform_enqueued_jobs do - RetryJob.perform_later "DiscardableError", 2 - assert_equal "Raised DiscardableError for the 1st time", JobBuffer.last_value - end + RetryJob.perform_later "DiscardableError", 2 + assert_equal "Raised DiscardableError for the 1st time", JobBuffer.last_value end test "custom handling of discarded job" do - perform_enqueued_jobs do - RetryJob.perform_later "CustomDiscardableError", 2 - assert_equal "Dealt with a job that was discarded in a custom way. Message: CustomDiscardableError", JobBuffer.last_value - end + RetryJob.perform_later "CustomDiscardableError", 2 + assert_equal "Dealt with a job that was discarded in a custom way. Message: CustomDiscardableError", JobBuffer.last_value end test "custom handling of job that exceeds retry attempts" do - perform_enqueued_jobs do - RetryJob.perform_later "CustomCatchError", 6 - assert_equal "Dealt with a job that failed to retry in a custom way after 6 attempts. Message: CustomCatchError", JobBuffer.last_value - end + RetryJob.perform_later "CustomCatchError", 6 + assert_equal "Dealt with a job that failed to retry in a custom way after 6 attempts. Message: CustomCatchError", JobBuffer.last_value end test "long wait job" do travel_to Time.now - perform_enqueued_jobs do - assert_performed_with at: (Time.now + 3600.seconds).to_i do - RetryJob.perform_later "LongWaitError", 5 - end - end + RetryJob.perform_later "LongWaitError", 2, :log_scheduled_at + + assert_equal [ + "Raised LongWaitError for the 1st time", + "Next execution scheduled at #{(Time.now + 3600.seconds).to_f}", + "Successfully completed job" + ], JobBuffer.values end test "exponentially retrying job" do travel_to Time.now - perform_enqueued_jobs do - assert_performed_with at: (Time.now + 3.seconds).to_i do - assert_performed_with at: (Time.now + 18.seconds).to_i do - assert_performed_with at: (Time.now + 83.seconds).to_i do - assert_performed_with at: (Time.now + 258.seconds).to_i do - RetryJob.perform_later "ExponentialWaitTenAttemptsError", 5 - end - end - end - end - end + RetryJob.perform_later "ExponentialWaitTenAttemptsError", 5, :log_scheduled_at + + assert_equal [ + "Raised ExponentialWaitTenAttemptsError for the 1st time", + "Next execution scheduled at #{(Time.now + 3.seconds).to_f}", + "Raised ExponentialWaitTenAttemptsError for the 2nd time", + "Next execution scheduled at #{(Time.now + 18.seconds).to_f}", + "Raised ExponentialWaitTenAttemptsError for the 3rd time", + "Next execution scheduled at #{(Time.now + 83.seconds).to_f}", + "Raised ExponentialWaitTenAttemptsError for the 4th time", + "Next execution scheduled at #{(Time.now + 258.seconds).to_f}", + "Successfully completed job" + ], JobBuffer.values end test "custom wait retrying job" do travel_to Time.now - perform_enqueued_jobs do - assert_performed_with at: (Time.now + 2.seconds).to_i do - assert_performed_with at: (Time.now + 4.seconds).to_i do - assert_performed_with at: (Time.now + 6.seconds).to_i do - assert_performed_with at: (Time.now + 8.seconds).to_i do - RetryJob.perform_later "CustomWaitTenAttemptsError", 5 - end - end - end - end - end + RetryJob.perform_later "CustomWaitTenAttemptsError", 5, :log_scheduled_at + + assert_equal [ + "Raised CustomWaitTenAttemptsError for the 1st time", + "Next execution scheduled at #{(Time.now + 2.seconds).to_f}", + "Raised CustomWaitTenAttemptsError for the 2nd time", + "Next execution scheduled at #{(Time.now + 4.seconds).to_f}", + "Raised CustomWaitTenAttemptsError for the 3rd time", + "Next execution scheduled at #{(Time.now + 6.seconds).to_f}", + "Raised CustomWaitTenAttemptsError for the 4th time", + "Next execution scheduled at #{(Time.now + 8.seconds).to_f}", + "Successfully completed job" + ], JobBuffer.values end test "successfully retry job throwing one of two retryable exceptions" do - perform_enqueued_jobs do - RetryJob.perform_later "SecondRetryableErrorOfTwo", 3 + RetryJob.perform_later "SecondRetryableErrorOfTwo", 3 - assert_equal [ - "Raised SecondRetryableErrorOfTwo for the 1st time", - "Raised SecondRetryableErrorOfTwo for the 2nd time", - "Successfully completed job" ], JobBuffer.values - end + assert_equal [ + "Raised SecondRetryableErrorOfTwo for the 1st time", + "Raised SecondRetryableErrorOfTwo for the 2nd time", + "Successfully completed job" ], JobBuffer.values end test "discard job throwing one of two discardable exceptions" do - perform_enqueued_jobs do - RetryJob.perform_later "SecondDiscardableErrorOfTwo", 2 - assert_equal [ "Raised SecondDiscardableErrorOfTwo for the 1st time" ], JobBuffer.values - end + RetryJob.perform_later "SecondDiscardableErrorOfTwo", 2 + assert_equal [ "Raised SecondDiscardableErrorOfTwo for the 1st time" ], JobBuffer.values end test "successfully retry job throwing DeserializationError" do - perform_enqueued_jobs do - RetryJob.perform_later Person.new(404), 5 - assert_equal ["Raised ActiveJob::DeserializationError for the 5 time"], JobBuffer.values - end + RetryJob.perform_later Person.new(404), 5 + assert_equal ["Raised ActiveJob::DeserializationError for the 5 time"], JobBuffer.values end + + private + def adapter_skips_scheduling?(queue_adapter) + [ + ActiveJob::QueueAdapters::InlineAdapter, + ActiveJob::QueueAdapters::AsyncAdapter, + ActiveJob::QueueAdapters::SneakersAdapter + ].include?(queue_adapter.class) + end end diff --git a/activejob/test/cases/test_helper_test.rb b/activejob/test/cases/test_helper_test.rb index 046033921d..4d934df31b 100644 --- a/activejob/test/cases/test_helper_test.rb +++ b/activejob/test/cases/test_helper_test.rb @@ -1688,7 +1688,7 @@ class PerformedJobsTest < ActiveJob::TestCase end end - def test_assert_performed_with_without_bllock_with_global_id_args + def test_assert_performed_with_without_block_with_global_id_args ricardo = Person.new(9) HelloJob.perform_later(ricardo) perform_enqueued_jobs diff --git a/activejob/test/jobs/retry_job.rb b/activejob/test/jobs/retry_job.rb index 2d19d4c41e..112d672006 100644 --- a/activejob/test/jobs/retry_job.rb +++ b/activejob/test/jobs/retry_job.rb @@ -18,7 +18,7 @@ class CustomDiscardableError < StandardError; end class RetryJob < ActiveJob::Base retry_on DefaultsError - retry_on FirstRetryableErrorOfTwo, SecondRetryableErrorOfTwo + retry_on FirstRetryableErrorOfTwo, SecondRetryableErrorOfTwo, attempts: 4 retry_on LongWaitError, wait: 1.hour, attempts: 10 retry_on ShortWaitTenAttemptsError, wait: 1.second, attempts: 10 retry_on ExponentialWaitTenAttemptsError, wait: :exponentially_longer, attempts: 10 @@ -30,8 +30,15 @@ class RetryJob < ActiveJob::Base discard_on FirstDiscardableErrorOfTwo, SecondDiscardableErrorOfTwo discard_on(CustomDiscardableError) { |job, error| JobBuffer.add("Dealt with a job that was discarded in a custom way. Message: #{error.message}") } - def perform(raising, attempts) - if executions < attempts + before_enqueue do |job| + if job.arguments.include?(:log_scheduled_at) && job.scheduled_at + JobBuffer.add("Next execution scheduled at #{job.scheduled_at}") + end + end + + def perform(raising, attempts, *) + raising = raising.shift if raising.is_a?(Array) + if raising && executions < attempts JobBuffer.add("Raised #{raising} for the #{executions.ordinalize} time") raise raising.constantize else @@ -39,12 +46,3 @@ class RetryJob < ActiveJob::Base end end end - -class ExceptionRetryJob < ActiveJob::Base - retry_on FirstRetryableErrorOfTwo, SecondRetryableErrorOfTwo, attempts: 4 - retry_on DefaultsError - - def perform(exceptions) - raise exceptions.shift.constantize.new unless exceptions.empty? - end -end diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 84567dcc18..912b307683 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,3 +1,37 @@ +* Fix year value when casting a multiparameter time hash + + When assigning a hash to a time attribute that's missing a year component + (e.g. a `time_select` with `:ignore_date` set to `true`) then the year + defaults to 1970 instead of the expected 2000. This results in the attribute + changing as a result of the save. + + Before: + ``` + event = Event.new(start_time: { 4 => 20, 5 => 30 }) + event.start_time # => 1970-01-01 20:30:00 UTC + event.save + event.reload + event.start_time # => 2000-01-01 20:30:00 UTC + ``` + + After: + ``` + event = Event.new(start_time: { 4 => 20, 5 => 30 }) + event.start_time # => 2000-01-01 20:30:00 UTC + event.save + event.reload + event.start_time # => 2000-01-01 20:30:00 UTC + ``` + + *Andrew White* + + +## Rails 6.0.0.beta1 (January 18, 2019) ## + +* Add `ActiveModel::Errors#of_kind?`. + + *bogdanvlviv*, *Rafael Mendonça França* + * Fix numericality equality validation of `BigDecimal` and `Float` by casting to `BigDecimal` on both ends of the validation. diff --git a/activemodel/MIT-LICENSE b/activemodel/MIT-LICENSE index 1cb3add0fc..ab7c27c209 100644 --- a/activemodel/MIT-LICENSE +++ b/activemodel/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2018 David Heinemeier Hansson +Copyright (c) 2004-2019 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index bc10d6b4b9..c9140dc582 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2018 David Heinemeier Hansson +# Copyright (c) 2004-2019 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index fde3381df2..ea2ed7dff7 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "active_support/core_ext/array/extract_options" +require "active_support/core_ext/hash/keys" module ActiveModel # == Active \Model \Callbacks diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 969effdc20..9fd6f2d89c 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -328,15 +328,15 @@ module ActiveModel # person.errors.added? :name, :blank # => true # person.errors.added? :name, "can't be blank" # => true # - # If the error message requires an option, then it returns +true+ with - # the correct option, or +false+ with an incorrect or missing option. - # - # person.errors.add :name, :too_long, { count: 25 } - # person.errors.added? :name, :too_long, count: 25 # => true - # person.errors.added? :name, "is too long (maximum is 25 characters)" # => true - # person.errors.added? :name, :too_long, count: 24 # => false - # person.errors.added? :name, :too_long # => false - # person.errors.added? :name, "is too long" # => false + # If the error message requires options, then it returns +true+ with + # the correct options, or +false+ with incorrect or missing options. + # + # person.errors.add :name, :too_long, { count: 25 } + # person.errors.added? :name, :too_long, count: 25 # => true + # person.errors.added? :name, "is too long (maximum is 25 characters)" # => true + # person.errors.added? :name, :too_long, count: 24 # => false + # person.errors.added? :name, :too_long # => false + # person.errors.added? :name, "is too long" # => false def added?(attribute, message = :invalid, options = {}) message = message.call if message.respond_to?(:call) @@ -347,6 +347,27 @@ module ActiveModel end end + # Returns +true+ if an error on the attribute with the given message is + # present, or +false+ otherwise. +message+ is treated the same as for +add+. + # + # person.errors.add :age + # person.errors.add :name, :too_long, { count: 25 } + # person.errors.of_kind? :age # => true + # person.errors.of_kind? :name # => false + # person.errors.of_kind? :name, :too_long # => true + # person.errors.of_kind? :name, "is too long (maximum is 25 characters)" # => true + # person.errors.of_kind? :name, :not_too_long # => false + # person.errors.of_kind? :name, "is too long" # => false + def of_kind?(attribute, message = :invalid) + message = message.call if message.respond_to?(:call) + + if message.is_a? Symbol + details[attribute.to_sym].map { |e| e[:error] }.include? message + else + self[attribute].include? message + end + end + # Returns all the full error messages in an array. # # class Person diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb index cef5441e4a..b0322f62c7 100644 --- a/activemodel/lib/active_model/gem_version.rb +++ b/activemodel/lib/active_model/gem_version.rb @@ -10,7 +10,7 @@ module ActiveModel MAJOR = 6 MINOR = 0 TINY = 0 - PRE = "alpha" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activemodel/lib/active_model/type/time.rb b/activemodel/lib/active_model/type/time.rb index b3056b1333..8fba01b86a 100644 --- a/activemodel/lib/active_model/type/time.rb +++ b/activemodel/lib/active_model/type/time.rb @@ -5,7 +5,7 @@ module ActiveModel class Time < Value # :nodoc: include Helpers::TimeValue include Helpers::AcceptsMultiparameterTime.new( - defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 } + defaults: { 1 => 2000, 2 => 1, 3 => 1, 4 => 0, 5 => 0 } ) def type diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index d6c80b2c5d..02759b4ccb 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -32,7 +32,7 @@ module ActiveModel value = options[key] unless (value.is_a?(Integer) && value >= 0) || value == Float::INFINITY || value.is_a?(Symbol) || value.is_a?(Proc) - raise ArgumentError, ":#{key} must be a nonnegative Integer, Infinity, Symbol, or Proc" + raise ArgumentError, ":#{key} must be a non-negative Integer, Infinity, Symbol, or Proc" end end end diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index c5997283ea..51e224d5cd 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "bigdecimal/util" + module ActiveModel module Validations class NumericalityValidator < EachValidator # :nodoc: @@ -10,7 +12,6 @@ module ActiveModel RESERVED_OPTIONS = CHECKS.keys + [:only_integer] INTEGER_REGEX = /\A[+-]?\d+\z/ - DECIMAL_REGEX = /\A[+-]?\d+\.?\d*(e|e[+-])?\d+\z/ def check_validity! keys = CHECKS.keys - [:odd, :even] @@ -92,8 +93,8 @@ module ActiveModel raw_value elsif is_integer?(raw_value) raw_value.to_i - elsif is_decimal?(raw_value) && !is_hexadecimal_literal?(raw_value) - BigDecimal(raw_value) + elsif !is_hexadecimal_literal?(raw_value) + Kernel.Float(raw_value).to_d end end @@ -101,12 +102,8 @@ module ActiveModel INTEGER_REGEX.match?(raw_value.to_s) end - def is_decimal?(raw_value) - DECIMAL_REGEX.match?(raw_value.to_s) - end - def is_hexadecimal_literal?(raw_value) - /\A0[xX]/.match?(raw_value) + /\A0[xX]/.match?(raw_value.to_s) end def filtered_options(value) diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index f9015b869d..947f9bf99b 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -209,6 +209,8 @@ class ErrorsTest < ActiveModel::TestCase person.errors.add(:name, "cannot be blank") person.errors.add(:name, "is invalid") assert person.errors.added?(:name, "cannot be blank") + assert person.errors.added?(:name, "is invalid") + assert_not person.errors.added?(:name, "incorrect") end test "added? returns false when no errors are present" do @@ -222,7 +224,7 @@ class ErrorsTest < ActiveModel::TestCase assert_not person.errors.added?(:name, "cannot be blank") end - test "added? returns false when checking for an error, but not providing message arguments" do + test "added? returns false when checking for an error, but not providing message argument" do person = Person.new person.errors.add(:name, "cannot be blank") assert_not person.errors.added?(:name) @@ -233,6 +235,7 @@ class ErrorsTest < ActiveModel::TestCase person.errors.add :name, :too_long, count: 25 assert person.errors.added? :name, :too_long, count: 25 + assert person.errors.added? :name, "is too long (maximum is 25 characters)" assert_not person.errors.added? :name, :too_long, count: 24 assert_not person.errors.added? :name, :too_long assert_not person.errors.added? :name, "is too long" @@ -243,6 +246,81 @@ class ErrorsTest < ActiveModel::TestCase person = Person.new person.errors.add(:name, :wrong) assert_not person.errors.added?(:name, :used) + assert person.errors.added?(:name, :wrong) + end + + test "of_kind? returns false when checking for an error, but not providing message argument" do + person = Person.new + person.errors.add(:name, "cannot be blank") + assert_not person.errors.of_kind?(:name) + end + + test "of_kind? returns false when checking a nonexisting error and other errors are present for the given attribute" do + person = Person.new + person.errors.add(:name, "is invalid") + assert_not person.errors.of_kind?(:name, "cannot be blank") + end + + test "of_kind? returns false when no errors are present" do + person = Person.new + assert_not person.errors.of_kind?(:name) + end + + test "of_kind? matches the given message when several errors are present for the same attribute" do + person = Person.new + person.errors.add(:name, "cannot be blank") + person.errors.add(:name, "is invalid") + assert person.errors.of_kind?(:name, "cannot be blank") + assert person.errors.of_kind?(:name, "is invalid") + assert_not person.errors.of_kind?(:name, "incorrect") + end + + test "of_kind? defaults message to :invalid" do + person = Person.new + person.errors.add(:name) + assert person.errors.of_kind?(:name) + end + + test "of_kind? handles proc messages" do + person = Person.new + message = Proc.new { "cannot be blank" } + person.errors.add(:name, message) + assert person.errors.of_kind?(:name, message) + end + + test "of_kind? returns true when string attribute is used with a symbol message" do + person = Person.new + person.errors.add(:name, :blank) + assert person.errors.of_kind?("name", :blank) + end + + test "of_kind? handles symbol message" do + person = Person.new + person.errors.add(:name, :blank) + assert person.errors.of_kind?(:name, :blank) + end + + test "of_kind? detects indifferent if a specific error was added to the object" do + person = Person.new + person.errors.add(:name, "cannot be blank") + assert person.errors.of_kind?(:name, "cannot be blank") + assert person.errors.of_kind?("name", "cannot be blank") + end + + test "of_kind? ignores options" do + person = Person.new + person.errors.add :name, :too_long, count: 25 + + assert person.errors.of_kind? :name, :too_long + assert person.errors.of_kind? :name, "is too long (maximum is 25 characters)" + end + + test "of_kind? returns false when checking for an error by symbol and a different error with same message is present" do + I18n.backend.store_translations("en", errors: { attributes: { name: { wrong: "is wrong", used: "is wrong" } } }) + person = Person.new + person.errors.add(:name, :wrong) + assert_not person.errors.of_kind?(:name, :used) + assert person.errors.of_kind?(:name, :wrong) end test "size calculates the number of error messages" do diff --git a/activemodel/test/cases/type/time_test.rb b/activemodel/test/cases/type/time_test.rb index 3fbae1a169..5c6271241d 100644 --- a/activemodel/test/cases/type/time_test.rb +++ b/activemodel/test/cases/type/time_test.rb @@ -16,6 +16,7 @@ module ActiveModel assert_equal ::Time.utc(2000, 1, 1, 16, 45, 54), type.cast("2015-06-13T19:45:54+03:00") assert_equal ::Time.utc(1999, 12, 31, 21, 7, 8), type.cast("06:07:08+09:00") + assert_equal ::Time.utc(2000, 1, 1, 16, 45, 54), type.cast(4 => 16, 5 => 45, 6 => 54) end def test_user_input_in_time_zone diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb index eb4b02df93..16c44762cb 100644 --- a/activemodel/test/cases/validations/numericality_validation_test.rb +++ b/activemodel/test/cases/validations/numericality_validation_test.rb @@ -281,6 +281,19 @@ class NumericalityValidationTest < ActiveModel::TestCase assert_predicate topic, :invalid? end + def test_validates_numericality_with_object_acting_as_numeric + klass = Class.new do + def to_f + 123.54 + end + end + + Topic.validates_numericality_of :price + topic = Topic.new(price: klass.new) + + assert_predicate topic, :valid? + end + def test_validates_numericality_with_invalid_args assert_raise(ArgumentError) { Topic.validates_numericality_of :approved, greater_than_or_equal_to: "foo" } assert_raise(ArgumentError) { Topic.validates_numericality_of :approved, less_than_or_equal_to: "foo" } diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 1aa756cc7e..654caafc92 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,117 @@ +* Allow applications to automatically switch connections. + + Adds a middleware and configuration options that can be used in your + application to automatically switch between the writing and reading + database connections. + + `GET` and `HEAD` requests will read from the replica unless there was + a write in the last 2 seconds, otherwise they will read from the primary. + Non-get requests will always write to the primary. The middleware accepts + an argument for a Resolver class and a Operations class where you are able + to change how the auto-switcher works to be most beneficial for your + application. + + To use the middleware in your application you can use the following + configuration options: + + ``` + config.active_record.database_selector = { delay: 2.seconds } + config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver + config.active_record.database_operations = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session + ``` + + To change the database selection strategy, pass a custom class to the + configuration options: + + ``` + config.active_record.database_selector = { delay: 10.seconds } + config.active_record.database_resolver = MyResolver + config.active_record.database_operations = MyResolver::MyCookies + ``` + + *Eileen M. Uchitelle* + +* MySQL: Support `:size` option to change text and blob size. + + *Ryuta Kamizono* + +* Make `t.timestamps` with precision by default. + + *Ryuta Kamizono* + + +## Rails 6.0.0.beta1 (January 18, 2019) ## + +* Remove deprecated `#set_state` from the transaction object. + + *Rafael Mendonça França* + +* Remove deprecated `#supports_statement_cache?` from the database adapters. + + *Rafael Mendonça França* + +* Remove deprecated `#insert_fixtures` from the database adapters. + + *Rafael Mendonça França* + +* Remove deprecated `ActiveRecord::ConnectionAdapters::SQLite3Adapter#valid_alter_table_type?`. + + *Rafael Mendonça França* + +* Do not allow passing the column name to `sum` when a block is passed. + + *Rafael Mendonça França* + +* Do not allow passing the column name to `count` when a block is passed. + + *Rafael Mendonça França* + +* Remove delegation of missing methods in a relation to arel. + + *Rafael Mendonça França* + +* Remove delegation of missing methods in a relation to private methods of the class. + + *Rafael Mendonça França* + +* Deprecate `config.activerecord.sqlite3.represent_boolean_as_integer`. + + *Rafael Mendonça França* + +* Change `SQLite3Adapter` to always represent boolean values as integers. + + *Rafael Mendonça França* + +* Remove ability to specify a timestamp name for `#cache_key`. + + *Rafael Mendonça França* + +* Remove deprecated `ActiveRecord::Migrator.migrations_path=`. + + *Rafael Mendonça França* + +* Remove deprecated `expand_hash_conditions_for_aggregates`. + + *Rafael Mendonça França* + +* Set polymorphic type column to NULL on `dependent: :nullify` strategy. + + On polymorphic associations both the foreign key and the foreign type columns will be set to NULL. + + *Laerti Papa* + +* Allow permitted instance of `ActionController::Parameters` as argument of `ActiveRecord::Relation#exists?`. + + *Gannon McGibbon* + +* Add support for endless ranges introduces in Ruby 2.6. + + *Greg Navis* + +* Deprecate passing `migrations_paths` to `connection.assume_migrated_upto_version`. + + *Ryuta Kamizono* + * MySQL: `ROW_FORMAT=DYNAMIC` create table option by default. Since MySQL 5.7.9, the `innodb_default_row_format` option defines the default row diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE index 04ba107c48..79e52c53af 100644 --- a/activerecord/MIT-LICENSE +++ b/activerecord/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2018 David Heinemeier Hansson +Copyright (c) 2004-2019 David Heinemeier Hansson Arel originally copyright (c) 2007-2016 Nick Kallen, Bryan Helmkamp, Emilio Tagua, Aaron Patterson diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index d43378c64f..7d66158f47 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2018 David Heinemeier Hansson +# Copyright (c) 2004-2019 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -74,6 +74,7 @@ module ActiveRecord autoload :Translation autoload :Validations autoload :SecureToken + autoload :DatabaseSelector, "active_record/middleware/database_selector" eager_autoload do autoload :ActiveRecordError, "active_record/errors" @@ -153,6 +154,12 @@ module ActiveRecord end end + module Middleware + extend ActiveSupport::Autoload + + autoload :DatabaseSelector, "active_record/middleware/database_selector" + end + module Tasks extend ActiveSupport::Autoload diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index fb1df00dc8..7bdbd8ce69 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1293,7 +1293,8 @@ module ActiveRecord # # * <tt>:destroy</tt> causes all the associated objects to also be destroyed. # * <tt>:delete_all</tt> causes all the associated objects to be deleted directly from the database (so callbacks will not be executed). - # * <tt>:nullify</tt> causes the foreign keys to be set to +NULL+. Callbacks are not executed. + # * <tt>:nullify</tt> causes the foreign keys to be set to +NULL+. Polymorphic type will also be nullified + # on polymorphic associations. Callbacks are not executed. # * <tt>:restrict_with_exception</tt> causes an <tt>ActiveRecord::DeleteRestrictionError</tt> exception to be raised if there are any associated records. # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are any associated objects. # @@ -1436,7 +1437,8 @@ module ActiveRecord # # * <tt>:destroy</tt> causes the associated object to also be destroyed # * <tt>:delete</tt> causes the associated object to be deleted directly from the database (so callbacks will not execute) - # * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Callbacks are not executed. + # * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Polymorphic type column is also nullified + # on polymorphic associations. Callbacks are not executed. # * <tt>:restrict_with_exception</tt> causes an <tt>ActiveRecord::DeleteRestrictionError</tt> exception to be raised if there is an associated record # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an associated object # diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index fb205d9ba5..5d0927f17d 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -190,9 +190,7 @@ module ActiveRecord end binds = AssociationScope.get_bind_values(owner, reflection.chain) - sc.execute(binds, conn) do |record| - set_inverse_instance(record) - end + sc.execute(binds, conn) { |record| set_inverse_instance(record) } || [] end # The scope for this association. diff --git a/activerecord/lib/active_record/associations/foreign_association.rb b/activerecord/lib/active_record/associations/foreign_association.rb index 40010cde03..59af6f54c3 100644 --- a/activerecord/lib/active_record/associations/foreign_association.rb +++ b/activerecord/lib/active_record/associations/foreign_association.rb @@ -9,5 +9,12 @@ module ActiveRecord::Associations false end end + + def nullified_owner_attributes + Hash.new.tap do |attrs| + attrs[reflection.foreign_key] = nil + attrs[reflection.type] = nil if reflection.type.present? + end + end end end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 4be2edbf30..6f67934a79 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -88,7 +88,7 @@ module ActiveRecord if method == :delete_all scope.delete_all else - scope.update_all(reflection.foreign_key => nil) + scope.update_all(nullified_owner_attributes) end end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 390bfd8b08..99971286a3 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -33,7 +33,7 @@ module ActiveRecord target.destroy throw(:abort) unless target.destroyed? when :nullify - target.update_columns(reflection.foreign_key => nil) if target.persisted? + target.update_columns(nullified_owner_attributes) if target.persisted? end end end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index c296f9882e..a92932fa4b 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -37,8 +37,6 @@ module ActiveRecord def find_target super.first - rescue ::RangeError - nil end def replace(record) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 99934a0e31..c8d5f679a8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -1006,7 +1006,16 @@ module ActiveRecord # for (not necessarily the current class). def retrieve_connection(spec_name) #:nodoc: pool = retrieve_connection_pool(spec_name) - raise ConnectionNotEstablished, "No connection pool with '#{spec_name}' found." unless pool + + unless pool + # multiple database application + if ActiveRecord::Base.connection_handler != ActiveRecord::Base.default_connection_handler + raise ConnectionNotEstablished, "No connection pool with '#{spec_name}' found for the '#{ActiveRecord::Base.current_role}' role." + else + raise ConnectionNotEstablished, "No connection pool with '#{spec_name}' found." + end + end + pool.connection end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 2299fc0214..aa2ecee74a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -123,7 +123,7 @@ module ActiveRecord # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil) - sql, binds = sql_for_insert(sql, pk, nil, sequence_name, binds) + sql, binds = sql_for_insert(sql, pk, sequence_name, binds) exec_query(sql, name, binds) end @@ -173,13 +173,6 @@ module ActiveRecord exec_delete(sql, name, binds) end - # Returns +true+ when the connection adapter supports prepared statement - # caching, otherwise returns +false+ - def supports_statement_cache? # :nodoc: - true - end - deprecate :supports_statement_cache? - # Runs the given block in a database transaction, and returns the result # of the block. # @@ -336,7 +329,7 @@ module ActiveRecord # Inserts the given fixture into the table. Overridden in adapters that require # something beyond a simple insert (eg. Oracle). - # Most of adapters should implement `insert_fixtures` that leverages bulk SQL insert. + # Most of adapters should implement `insert_fixtures_set` that leverages bulk SQL insert. # We keep this method to provide fallback # for databases like sqlite that do not support bulk inserts. def insert_fixture(fixture, table_name) @@ -365,18 +358,6 @@ module ActiveRecord execute manager.to_sql, "Fixture Insert" end - # Inserts a set of fixtures into the table. Overridden in adapters that require - # something beyond a simple insert (eg. Oracle). - def insert_fixtures(fixtures, table_name) - ActiveSupport::Deprecation.warn(<<-MSG.squish) - `insert_fixtures` is deprecated and will be removed in the next version of Rails. - Consider using `insert_fixtures_set` for performance improvement. - MSG - return if fixtures.empty? - - execute(build_fixture_sql(fixtures, table_name), "Fixtures Insert") - end - def insert_fixtures_set(fixture_set, tables_to_delete = []) fixture_inserts = fixture_set.map do |table_name, fixtures| next if fixtures.empty? @@ -464,7 +445,7 @@ module ActiveRecord exec_query(sql, name, binds, prepare: true) end - def sql_for_insert(sql, pk, id_value, sequence_name, binds) + def sql_for_insert(sql, pk, sequence_name, binds) [sql, binds] end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 8aeb934ec2..4e55fcae2f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -17,7 +17,7 @@ module ActiveRecord method_names.each do |method_name| base.class_eval <<-end_code, __FILE__, __LINE__ + 1 def #{method_name}(*) - clear_query_cache if @query_cache_enabled + ActiveRecord::Base.clear_query_caches_for_current_thread if @query_cache_enabled super end end_code diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 07e86afe9a..eefe621feb 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -157,13 +157,9 @@ module ActiveRecord end end - def types_which_need_no_typecasting - [nil, Numeric, String] - end - def _quote(value) case value - when String, ActiveSupport::Multibyte::Chars + when String, Symbol, ActiveSupport::Multibyte::Chars "'#{quote_string(value.to_s)}'" when true then quoted_true when false then quoted_false @@ -174,7 +170,6 @@ module ActiveRecord when Type::Binary::Data then quoted_binary(value) when Type::Time::Value then "'#{quoted_time(value)}'" when Date, Time then "'#{quoted_date(value)}'" - when Symbol then "'#{quote_string(value.to_s)}'" when Class then "'#{value}'" else raise TypeError, "can't quote #{value.class.name}" end @@ -188,10 +183,9 @@ module ActiveRecord when false then unquoted_false # BigDecimals need to be put in a non-normalized form and quoted. when BigDecimal then value.to_s("F") + when nil, Numeric, String then value when Type::Time::Value then quoted_time(value) when Date, Time then quoted_date(value) - when *types_which_need_no_typecasting - value else raise TypeError end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index db489143af..b2a6109548 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "active_support/deprecation" - module ActiveRecord module ConnectionAdapters #:nodoc: # Abstract representation of an index definition on a table. Instances of @@ -259,10 +257,9 @@ module ActiveRecord include ColumnMethods attr_reader :name, :temporary, :if_not_exists, :options, :as, :comment, :indexes, :foreign_keys - attr_writer :indexes - deprecate :indexes= def initialize( + conn, name, temporary: false, if_not_exists: false, @@ -271,6 +268,7 @@ module ActiveRecord comment: nil, ** ) + @conn = conn @columns_hash = {} @indexes = [] @foreign_keys = [] @@ -410,6 +408,10 @@ module ActiveRecord def timestamps(**options) options[:null] = false if options[:null].nil? + if !options.key?(:precision) && @conn.supports_datetime_with_precision? + options[:precision] = 6 + end + column(:created_at, :datetime, options) column(:updated_at, :datetime, options) end @@ -680,9 +682,10 @@ module ActiveRecord end alias :remove_belongs_to :remove_references - # Adds a foreign key. + # Adds a foreign key to the table using a supplied table name. # # t.foreign_key(:authors) + # t.foreign_key(:authors, column: :author_id, primary_key: "id") # # See {connection.add_foreign_key}[rdoc-ref:SchemaStatements#add_foreign_key] def foreign_key(*args) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 38cfc3a241..d88e75d692 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -2,6 +2,7 @@ require "active_record/migration/join_table" require "active_support/core_ext/string/access" +require "active_support/deprecation" require "digest/sha2" module ActiveRecord @@ -129,11 +130,11 @@ module ActiveRecord # column_exists?(:suppliers, :name, :string, null: false) # column_exists?(:suppliers, :tax, :decimal, precision: 8, scale: 2) # - def column_exists?(table_name, column_name, type = nil, options = {}) + def column_exists?(table_name, column_name, type = nil, **options) column_name = column_name.to_s checks = [] checks << lambda { |c| c.name == column_name } - checks << lambda { |c| c.type == type } if type + checks << lambda { |c| c.type == type.to_sym rescue nil } if type column_options_keys.each do |attr| checks << lambda { |c| c.send(attr) == options[attr] } if options.key?(attr) end @@ -1050,15 +1051,18 @@ module ActiveRecord { primary_key: true } end - def assume_migrated_upto_version(version, migrations_paths) - migrations_paths = Array(migrations_paths) + def assume_migrated_upto_version(version, migrations_paths = nil) + unless migrations_paths.nil? + ActiveSupport::Deprecation.warn(<<~MSG) + Passing migrations_paths to #assume_migrated_upto_version is deprecated and will be removed in Rails 6.1. + MSG + end + version = version.to_i sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name) - migrated = ActiveRecord::SchemaMigration.all_versions.map(&:to_i) - versions = migration_context.migration_files.map do |file| - migration_context.parse_migration_filename(file).first.to_i - end + migrated = migration_context.get_all_versions + versions = migration_context.migrations.map(&:version) unless migrated.include?(version) execute "INSERT INTO #{sm_table} (version) VALUES (#{quote(version)})" @@ -1125,6 +1129,10 @@ module ActiveRecord def add_timestamps(table_name, options = {}) options[:null] = false if options[:null].nil? + if !options.key?(:precision) && supports_datetime_with_precision? + options[:precision] = 6 + end + add_column table_name, :created_at, :datetime, options add_column table_name, :updated_at, :datetime, options end @@ -1286,7 +1294,7 @@ module ActiveRecord end def create_table_definition(*args) - TableDefinition.new(*args) + TableDefinition.new(self, *args) end def create_alter_table(name) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 718910b090..c9e84e48cc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -40,24 +40,6 @@ module ActiveRecord committed? || rolledback? end - def set_state(state) - ActiveSupport::Deprecation.warn(<<-MSG.squish) - The set_state method is deprecated and will be removed in - Rails 6.0. Please use rollback! or commit! to set transaction - state directly. - MSG - case state - when :rolledback - rollback! - when :committed - commit! - when nil - nullify! - else - raise ArgumentError, "Invalid transaction state: #{state}" - end - end - def rollback! @children.each { |c| c.rollback! } @state = :rolledback @@ -223,9 +205,12 @@ module ActiveRecord run_commit_callbacks: run_commit_callbacks) end - transaction.materialize! unless @connection.supports_lazy_transactions? && lazy_transactions_enabled? + if @connection.supports_lazy_transactions? && lazy_transactions_enabled? && options[:_lazy] != false + @has_unmaterialized_transactions = true + else + transaction.materialize! + end @stack.push(transaction) - @has_unmaterialized_transactions = true if @connection.supports_lazy_transactions? transaction end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 1243236c09..9a7d7285f2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -6,6 +6,7 @@ require "active_record/connection_adapters/sql_type_metadata" require "active_record/connection_adapters/abstract/schema_dumper" require "active_record/connection_adapters/abstract/schema_creation" require "active_support/concurrency/load_interlock_aware_monitor" +require "active_support/deprecation" require "arel/collectors/bind" require "arel/collectors/composite" require "arel/collectors/sql_string" @@ -76,8 +77,8 @@ module ActiveRecord SIMPLE_INT = /\A\d+\z/ - attr_accessor :visitor, :pool, :prevent_writes - attr_reader :schema_cache, :owner, :logger, :prepared_statements, :lock + attr_accessor :pool + attr_reader :schema_cache, :visitor, :owner, :logger, :lock, :prepared_statements, :prevent_writes alias :in_use? :owner set_callback :checkin, :after, :enable_lazy_transactions! @@ -117,6 +118,7 @@ module ActiveRecord @idle_since = Concurrent.monotonic_time @schema_cache = SchemaCache.new self @quoted_column_names, @quoted_table_names = {}, {} + @prevent_writes = false @visitor = arel_visitor @lock = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new @@ -146,17 +148,16 @@ module ActiveRecord replica? || prevent_writes end - # Prevent writing to the database regardless of role. + # Prevent writing to the database regardless of role. # # In some cases you may want to prevent writes to the database # even if you are on a database that can write. `while_preventing_writes` # will prevent writes to the database for the duration of the block. def while_preventing_writes - original = self.prevent_writes - self.prevent_writes = true + original, @prevent_writes = @prevent_writes, true yield ensure - self.prevent_writes = original + @prevent_writes = original end def migrations_paths # :nodoc: @@ -504,15 +505,17 @@ module ActiveRecord @connection end - def case_sensitive_comparison(table, attribute, column, value) # :nodoc: - table[attribute].eq(value) + def case_sensitive_comparison(attribute, value) # :nodoc: + attribute.eq(value) end - def case_insensitive_comparison(table, attribute, column, value) # :nodoc: + def case_insensitive_comparison(attribute, value) # :nodoc: + column = column_for_attribute(attribute) + if can_perform_case_insensitive_comparison_for?(column) - table[attribute].lower.eq(table.lower(value)) + attribute.lower.eq(attribute.relation.lower(value)) else - table[attribute].eq(value) + attribute.eq(value) end end @@ -659,6 +662,11 @@ module ActiveRecord raise(ActiveRecordError, "No such column: #{table_name}.#{column_name}") end + def column_for_attribute(attribute) + table_name = attribute.relation.name + schema_cache.columns_hash(table_name)[attribute.name.to_s] + end + def collector if prepared_statements Arel::Collectors::Composite.new( diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index dbc6614b93..7b69a63f6e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -29,7 +29,7 @@ module ActiveRecord NATIVE_DATABASE_TYPES = { primary_key: "bigint auto_increment PRIMARY KEY", string: { name: "varchar", limit: 255 }, - text: { name: "text", limit: 65535 }, + text: { name: "text" }, integer: { name: "int", limit: 4 }, float: { name: "float", limit: 24 }, decimal: { name: "decimal" }, @@ -37,7 +37,8 @@ module ActiveRecord timestamp: { name: "timestamp" }, time: { name: "time" }, date: { name: "date" }, - binary: { name: "blob", limit: 65535 }, + binary: { name: "blob" }, + blob: { name: "blob" }, boolean: { name: "tinyint", limit: 1 }, json: { name: "json" }, } @@ -97,19 +98,11 @@ module ActiveRecord end def supports_datetime_with_precision? - if mariadb? - version >= "5.3.0" - else - version >= "5.6.4" - end + mariadb? || version >= "5.6.4" end def supports_virtual_columns? - if mariadb? - version >= "5.2.0" - else - version >= "5.7.5" - end + mariadb? || version >= "5.7.5" end def supports_advisory_locks? @@ -484,9 +477,11 @@ module ActiveRecord SQL end - def case_sensitive_comparison(table, attribute, column, value) # :nodoc: + def case_sensitive_comparison(attribute, value) # :nodoc: + column = column_for_attribute(attribute) + if column.collation && !column.case_sensitive? - table[attribute].eq(Arel::Nodes::Bin.new(value)) + attribute.eq(Arel::Nodes::Bin.new(value)) else super end @@ -587,13 +582,13 @@ module ActiveRecord m.alias_type %r(bit)i, "binary" m.register_type(%r(enum)i) do |sql_type| - limit = sql_type[/^enum\((.+)\)/i, 1] + limit = sql_type[/^enum\s*\((.+)\)/i, 1] .split(",").map { |enum| enum.strip.length - 2 }.max MysqlString.new(limit: limit) end m.register_type(%r(^set)i) do |sql_type| - limit = sql_type[/^set\((.+)\)/i, 1] + limit = sql_type[/^set\s*\((.+)\)/i, 1] .split(",").map { |set| set.strip.length - 1 }.sum - 1 MysqlString.new(limit: limit) end @@ -633,6 +628,7 @@ module ActiveRecord ER_LOCK_WAIT_TIMEOUT = 1205 ER_QUERY_INTERRUPTED = 1317 ER_QUERY_TIMEOUT = 3024 + ER_FK_INCOMPATIBLE_COLUMNS = 3780 def translate_exception(exception, message:, sql:, binds:) case error_number(exception) @@ -640,7 +636,7 @@ module ActiveRecord RecordNotUnique.new(message, sql: sql, binds: binds) when ER_NO_REFERENCED_ROW, ER_ROW_IS_REFERENCED, ER_ROW_IS_REFERENCED_2, ER_NO_REFERENCED_ROW_2 InvalidForeignKey.new(message, sql: sql, binds: binds) - when ER_CANNOT_ADD_FOREIGN + when ER_CANNOT_ADD_FOREIGN, ER_FK_INCOMPATIBLE_COLUMNS mismatched_foreign_key(message, sql: sql, binds: binds) when ER_CANNOT_CREATE_TABLE if message.include?("errno: 150") @@ -714,6 +710,12 @@ module ActiveRecord end def add_timestamps_for_alter(table_name, options = {}) + options[:null] = false if options[:null].nil? + + if !options.key?(:precision) && supports_datetime_with_precision? + options[:precision] = 6 + end + [add_column_for_alter(table_name, :created_at, :datetime, options), add_column_for_alter(table_name, :updated_at, :datetime, options)] end diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index f60d8469cc..9eaf9d9a89 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -248,10 +248,29 @@ module ActiveRecord if db_config resolve_connection(db_config.config).merge("name" => pool_name.to_s) else - raise(AdapterNotSpecified, "'#{env_name}' database is not configured. Available: #{configurations.configurations.map(&:env_name).join(", ")}") + raise AdapterNotSpecified, <<~MSG + The `#{env_name}` database is not configured for the `#{ActiveRecord::ConnectionHandling::DEFAULT_ENV.call}` environment. + + Available databases configurations are: + + #{build_configuration_sentence} + MSG end end + def build_configuration_sentence # :nodoc: + configs = configurations.configs_for(include_replicas: true) + + configs.group_by(&:env_name).map do |env, config| + namespaces = config.map(&:spec_name) + if namespaces.size > 1 + "#{env}: #{namespaces.join(", ")}" + else + env + end + end.join("\n") + end + # Accepts a hash. Expands the "url" key that contains a # URL database connection to a full connection # hash and merges with the rest of the hash. diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb index 2ed4ad16ae..90bcdf3297 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb @@ -56,6 +56,16 @@ module ActiveRecord case type when :virtual type = options[:type] + when :text, :blob, :binary + case (size = options[:size])&.to_s + when "tiny", "medium", "long" + sql_type = @conn.native_database_types[type][:name] + type = "#{size}#{sql_type}" + else + raise ArgumentError, <<~MSG unless size.nil? + #{size.inspect} is invalid :size value. Only :tiny, :medium, and :long are allowed. + MSG + end when :primary_key type = :integer options[:limit] ||= 8 diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb index d23178e43c..57518b02fa 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb @@ -10,6 +10,10 @@ module ActiveRecord spec[:unsigned] = "true" if column.unsigned? spec[:auto_increment] = "true" if column.auto_increment? + if /\A(?<size>tiny|medium|long)(?:text|blob)/ =~ column.sql_type + spec = { size: size.to_sym.inspect }.merge!(spec) + end + if @connection.supports_virtual_columns? && column.virtual? spec[:as] = extract_expression_for_virtual_column(column) spec[:stored] = "true" if /\b(?:STORED|PERSISTENT)\b/.match?(column.extra) @@ -37,13 +41,15 @@ module ActiveRecord case column.sql_type when /\Atimestamp\b/ :timestamp - when "tinyblob" - :blob else super end end + def schema_limit(column) + super unless /\A(?:tiny|medium|long)?(?:text|blob)/.match?(column.sql_type) + end + def schema_precision(column) super unless /\A(?:date)?time(?:stamp)?\b/.match?(column.sql_type) && column.precision == 0 end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb index 47b5c4b9ec..e9484a08de 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb @@ -127,7 +127,7 @@ module ActiveRecord end def create_table_definition(*args) - MySQL::TableDefinition.new(*args) + MySQL::TableDefinition.new(self, *args) end def new_column_from_field(table_name, field) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index c70a4fa875..41633872e2 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -110,7 +110,7 @@ module ActiveRecord end alias :exec_update :exec_delete - def sql_for_insert(sql, pk, id_value, sequence_name, binds) # :nodoc: + def sql_for_insert(sql, pk, sequence_name, binds) # :nodoc: if pk.nil? # Extract the table from the insert sql. Yuck. table_ref = extract_table_ref_from_insert_sql(sql) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb index ceb8b40bd9..84dd28907b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb @@ -17,6 +17,42 @@ module ActiveRecord "VALIDATE CONSTRAINT #{quote_column_name(name)}" end + def visit_ChangeColumnDefinition(o) + column = o.column + column.sql_type = type_to_sql(column.type, column.options) + quoted_column_name = quote_column_name(o.name) + + change_column_sql = +"ALTER COLUMN #{quoted_column_name} TYPE #{column.sql_type}" + + options = column_options(column) + + if options[:collation] + change_column_sql << " COLLATE \"#{options[:collation]}\"" + end + + if options[:using] + change_column_sql << " USING #{options[:using]}" + elsif options[:cast_as] + cast_as_type = type_to_sql(options[:cast_as], options) + change_column_sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})" + end + + if options.key?(:default) + if options[:default].nil? + change_column_sql << ", ALTER COLUMN #{quoted_column_name} DROP DEFAULT" + else + quoted_default = quote_default_expression(options[:default], column) + change_column_sql << ", ALTER COLUMN #{quoted_column_name} SET DEFAULT #{quoted_default}" + end + end + + if options.key?(:null) + change_column_sql << ", ALTER COLUMN #{quoted_column_name} #{options[:null] ? 'DROP' : 'SET'} NOT NULL" + end + + change_column_sql + end + def add_column_options!(sql, options) if options[:collation] sql << " COLLATE \"#{options[:collation]}\"" diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 16260fe565..946436f7f9 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -22,8 +22,8 @@ module ActiveRecord def create_database(name, options = {}) options = { encoding: "utf8" }.merge!(options.symbolize_keys) - option_string = options.inject("") do |memo, (key, value)| - memo += case key + option_string = options.each_with_object(+"") do |(key, value), memo| + memo << case key when :owner " OWNER = \"#{value}\"" when :template @@ -637,7 +637,7 @@ module ActiveRecord end def create_table_definition(*args) - PostgreSQL::TableDefinition.new(*args) + PostgreSQL::TableDefinition.new(self, *args) end def create_alter_table(name) @@ -683,38 +683,20 @@ module ActiveRecord end end - def change_column_sql(table_name, column_name, type, options = {}) - quoted_column_name = quote_column_name(column_name) - sql_type = type_to_sql(type, options) - sql = +"ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}" - if options[:collation] - sql << " COLLATE \"#{options[:collation]}\"" - end - if options[:using] - sql << " USING #{options[:using]}" - elsif options[:cast_as] - cast_as_type = type_to_sql(options[:cast_as], options) - sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})" - end - - sql - end - def add_column_for_alter(table_name, column_name, type, options = {}) return super unless options.key?(:comment) [super, Proc.new { change_column_comment(table_name, column_name, options[:comment]) }] end def change_column_for_alter(table_name, column_name, type, options = {}) - sqls = [change_column_sql(table_name, column_name, type, options)] - sqls << change_column_default_for_alter(table_name, column_name, options[:default]) if options.key?(:default) - sqls << change_column_null_for_alter(table_name, column_name, options[:null], options[:default]) if options.key?(:null) + td = create_table_definition(table_name) + cd = td.new_column_definition(column_name, type, options) + sqls = [schema_creation.accept(ChangeColumnDefinition.new(cd, column_name))] sqls << Proc.new { change_column_comment(table_name, column_name, options[:comment]) } if options.key?(:comment) sqls end - # Changes the default value of a table column. - def change_column_default_for_alter(table_name, column_name, default_or_changes) # :nodoc: + def change_column_default_for_alter(table_name, column_name, default_or_changes) column = column_for(table_name, column_name) return unless column @@ -729,11 +711,17 @@ module ActiveRecord end end - def change_column_null_for_alter(table_name, column_name, null, default = nil) #:nodoc: - "ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL" + def change_column_null_for_alter(table_name, column_name, null, default = nil) + "ALTER COLUMN #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL" end def add_timestamps_for_alter(table_name, options = {}) + options[:null] = false if options[:null].nil? + + if !options.key?(:precision) && supports_datetime_with_precision? + options[:precision] = 6 + end + [add_column_for_alter(table_name, :created_at, :datetime, options), add_column_for_alter(table_name, :updated_at, :datetime, options)] end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 381d5ab29b..95beeb4cae 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -236,6 +236,8 @@ module ActiveRecord # @local_tz is initialized as nil to avoid warnings when connect tries to use it @local_tz = nil + @default_timezone = nil + @timestamp_decoder = nil @max_identifier_length = nil configure_connection @@ -628,6 +630,10 @@ module ActiveRecord def exec_no_cache(sql, name, binds) materialize_transactions + # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been + # made since we established the connection + update_typemap_for_default_timezone + type_casted_binds = type_casted_binds(binds) log(sql, name, binds, type_casted_binds) do ActiveSupport::Dependencies.interlock.permit_concurrent_loads do @@ -638,6 +644,7 @@ module ActiveRecord def exec_cache(sql, name, binds) materialize_transactions + update_typemap_for_default_timezone stmt_key = prepare_statement(sql, binds) type_casted_binds = type_casted_binds(binds) @@ -826,6 +833,18 @@ module ActiveRecord @connection.type_map_for_queries = map end + def update_typemap_for_default_timezone + if @default_timezone != ActiveRecord::Base.default_timezone && @timestamp_decoder + decoder_class = ActiveRecord::Base.default_timezone == :utc ? + PG::TextDecoder::TimestampUtc : + PG::TextDecoder::TimestampWithoutTimeZone + + @timestamp_decoder = decoder_class.new(@timestamp_decoder.to_h) + @connection.type_map_for_results.add_coder(@timestamp_decoder) + @default_timezone = ActiveRecord::Base.default_timezone + end + end + def add_pg_decoders coders_by_name = { "int2" => PG::TextDecoder::Integer, @@ -836,6 +855,13 @@ module ActiveRecord "float8" => PG::TextDecoder::Float, "bool" => PG::TextDecoder::Boolean, } + + if defined?(PG::TextDecoder::TimestampUtc) + # Use native PG encoders available since pg-1.1 + coders_by_name["timestamp"] = PG::TextDecoder::TimestampUtc + coders_by_name["timestamptz"] = PG::TextDecoder::TimestampWithTimeZone + end + known_coder_types = coders_by_name.keys.map { |n| quote(n) } query = <<~SQL % known_coder_types.join(", ") SELECT t.oid, t.typname @@ -851,6 +877,10 @@ module ActiveRecord map = PG::TypeMapByOid.new coders.each { |coder| map.add_coder(coder) } @connection.type_map_for_results = map + + # extract timestamp decoder for use in update_typemap_for_default_timezone + @timestamp_decoder = coders.find { |coder| coder.name == "timestamp" } + update_typemap_for_default_timezone end def construct_coder(row, coder_class) diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index c29cf1f9a1..c10765f42d 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -77,6 +77,11 @@ module ActiveRecord }] end + # Checks whether the columns hash is already cached for a table. + def columns_hash?(table_name) + @columns_hash.key?(table_name) + end + # Clears out internal caches def clear! @columns.clear diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb index 29f0e19a98..cb9d32a577 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb @@ -30,19 +30,19 @@ module ActiveRecord end def quoted_true - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? "1" : "'t'" + "1" end def unquoted_true - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? 1 : "t" + 1 end def quoted_false - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? "0" : "'f'" + "0" end def unquoted_false - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? 0 : "f" + 0 end private diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb index 8650c07bab..2394982a7d 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb @@ -62,7 +62,7 @@ module ActiveRecord end def create_table_definition(*args) - SQLite3::TableDefinition.new(*args) + SQLite3::TableDefinition.new(self, *args) end def new_column_from_field(table_name, field) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 44c6e99112..7b3630662b 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -9,7 +9,7 @@ require "active_record/connection_adapters/sqlite3/schema_definitions" require "active_record/connection_adapters/sqlite3/schema_dumper" require "active_record/connection_adapters/sqlite3/schema_statements" -gem "sqlite3", "~> 1.3.6" +gem "sqlite3", "~> 1.3", ">= 1.3.6" require "sqlite3" module ActiveRecord @@ -76,22 +76,15 @@ module ActiveRecord json: { name: "json" }, } - ## - # :singleton-method: - # Indicates whether boolean values are stored in sqlite3 databases as 1 - # and 0 or 't' and 'f'. Leaving <tt>ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer</tt> - # set to false is deprecated. SQLite databases have used 't' and 'f' to - # serialize boolean values and must have old data converted to 1 and 0 - # (its native boolean serialization) before setting this flag to true. - # Conversion can be accomplished by setting up a rake task which runs - # - # ExampleModel.where("boolean_column = 't'").update_all(boolean_column: 1) - # ExampleModel.where("boolean_column = 'f'").update_all(boolean_column: 0) - # for all models and all boolean columns, after which the flag must be set - # to true by adding the following to your <tt>application.rb</tt> file: - # - # Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true - class_attribute :represent_boolean_as_integer, default: false + def self.represent_boolean_as_integer=(value) # :nodoc: + if value == false + raise "`.represent_boolean_as_integer=` is now always true, so make sure your application can work with it and remove this settings." + end + + ActiveSupport::Deprecation.warn( + "`.represent_boolean_as_integer=` is now always true, so setting this is deprecated and will be removed in Rails 6.1." + ) + end class StatementPool < ConnectionAdapters::StatementPool # :nodoc: private @@ -314,11 +307,6 @@ module ActiveRecord rename_table_indexes(table_name, new_name) end - def valid_alter_table_type?(type, options = {}) - !invalid_alter_table_type?(type, options) - end - deprecate :valid_alter_table_type? - def add_column(table_name, column_name, type, options = {}) #:nodoc: if invalid_alter_table_type?(type, options) alter_table(table_name) do |definition| @@ -390,14 +378,6 @@ module ActiveRecord end end - def insert_fixtures(rows, table_name) - ActiveSupport::Deprecation.warn(<<-MSG.squish) - `insert_fixtures` is deprecated and will be removed in the next version of Rails. - Consider using `insert_fixtures_set` for performance improvement. - MSG - insert_fixtures_set(table_name => rows) - end - def insert_fixtures_set(fixture_set, tables_to_delete = []) disable_referential_integrity do transaction(requires_new: true) do diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 4a941055d1..53069cd899 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -158,10 +158,6 @@ module ActiveRecord end def with_handler(handler_key, &blk) # :nodoc: - unless ActiveRecord::Base.connection_handlers.keys.include?(handler_key) - raise ArgumentError, "The #{handler_key} role does not exist. Add it by establishing a connection with `connects_to` or use an existing role (#{ActiveRecord::Base.connection_handlers.keys.join(", ")})." - end - handler = lookup_connection_handler(handler_key) swap_connection_handler(handler, &blk) end @@ -180,6 +176,15 @@ module ActiveRecord config_hash end + # Clears the query cache for all connections associated with the current thread. + def clear_query_caches_for_current_thread + ActiveRecord::Base.connection_handlers.each_value do |handler| + handler.connection_pool_list.each do |pool| + pool.connection.clear_query_cache if pool.active_connection? + end + end + end + # Returns the connection currently associated with the class. This can # also be used to "borrow" the connection to do database work unrelated # to any of the specific Active Records. diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 8f4d292a4b..c67980173f 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -101,6 +101,7 @@ module ActiveRecord # environment where dumping schema is rarely needed. mattr_accessor :dump_schema_after_migration, instance_writer: false, default: true + mattr_accessor :database_selector, instance_writer: false ## # :singleton-method: # Specifies which database schemas to dump when calling db:structure:dump. @@ -124,6 +125,10 @@ module ActiveRecord mattr_accessor :connection_handlers, instance_accessor: false, default: {} + mattr_accessor :writing_role, instance_accessor: false, default: :writing + + mattr_accessor :reading_role, instance_accessor: false, default: :reading + class_attribute :default_connection_handler, instance_writer: false self.filter_attributes = [] @@ -137,7 +142,6 @@ module ActiveRecord end self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new - self.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler } end module ClassMethods @@ -169,15 +173,12 @@ module ActiveRecord where(key => params.bind).limit(1) } - record = statement.execute([id], connection).first + record = statement.execute([id], connection)&.first unless record raise RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id}", name, primary_key, id) end record - rescue ::RangeError - raise RecordNotFound.new("Couldn't find #{name} with an out of range value for '#{primary_key}'", - name, primary_key) end def find_by(*args) # :nodoc: @@ -201,11 +202,9 @@ module ActiveRecord where(wheres).limit(1) } begin - statement.execute(hash.values, connection).first + statement.execute(hash.values, connection)&.first rescue TypeError raise ActiveRecord::StatementInvalid - rescue ::RangeError - nil end end @@ -282,6 +281,10 @@ module ActiveRecord TypeCaster::Map.new(self) end + def _internal? # :nodoc: + false + end + private def cached_find_by_statement(key, &block) @@ -350,9 +353,7 @@ module ActiveRecord # Initialize an empty model object from +attributes+. # +attributes+ should be an attributes object, and unlike the # `initialize` method, no assignment calls are made per attribute. - # - # :nodoc: - def init_with_attributes(attributes, new_record = false) + def init_with_attributes(attributes, new_record = false) # :nodoc: init_internals @new_record = new_record @@ -476,6 +477,14 @@ module ActiveRecord end end + def present? # :nodoc: + true + end + + def blank? # :nodoc: + false + end + # Returns +true+ if the record is read only. Records loaded through joins with piggy-back # attributes will be marked as read only since they cannot be saved. def readonly? diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb index 11aed6c002..73adf66684 100644 --- a/activerecord/lib/active_record/database_configurations.rb +++ b/activerecord/lib/active_record/database_configurations.rb @@ -134,9 +134,11 @@ module ActiveRecord end def build_db_config_from_hash(env_name, spec_name, config) - if url = config["url"] + if config.has_key?("url") + url = config["url"] config_without_url = config.dup config_without_url.delete "url" + ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url, config_without_url) elsif config["database"] || (config.size == 1 && config.values.all? { |v| v.is_a? String }) ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config) diff --git a/activerecord/lib/active_record/database_configurations/url_config.rb b/activerecord/lib/active_record/database_configurations/url_config.rb index 81917fc4c1..8e8aa69478 100644 --- a/activerecord/lib/active_record/database_configurations/url_config.rb +++ b/activerecord/lib/active_record/database_configurations/url_config.rb @@ -56,12 +56,17 @@ module ActiveRecord end private - def build_config(original_config, url) - if /^jdbc:/.match?(url) - hash = { "url" => url } + + def build_url_hash(url) + if url.nil? || /^jdbc:/.match?(url) + { "url" => url } else - hash = ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(url).to_hash + ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(url).to_hash end + end + + def build_config(original_config, url) + hash = build_url_hash(url) if original_config[env_name] original_config[env_name].merge(hash) diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb index 72035a986b..16dff9c2b0 100644 --- a/activerecord/lib/active_record/gem_version.rb +++ b/activerecord/lib/active_record/gem_version.rb @@ -10,7 +10,7 @@ module ActiveRecord MAJOR = 6 MINOR = 0 TINY = 0 - PRE = "alpha" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 90fb10a1f1..fa6f0d36ec 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -61,23 +61,14 @@ module ActiveRecord # # Product.cache_versioning = false # Product.find(5).cache_key # => "products/5-20071224150000" (updated_at available) - def cache_key(*timestamp_names) + def cache_key if new_record? "#{model_name.cache_key}/new" else - if cache_version && timestamp_names.none? + if cache_version "#{model_name.cache_key}/#{id}" else - timestamp = if timestamp_names.any? - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Specifying a timestamp name for #cache_key has been deprecated in favor of - the explicit #cache_version method that can be overwritten. - MSG - - max_updated_column_timestamp(timestamp_names) - else - max_updated_column_timestamp - end + timestamp = max_updated_column_timestamp if timestamp timestamp = timestamp.utc.to_s(cache_timestamp_format) diff --git a/activerecord/lib/active_record/internal_metadata.rb b/activerecord/lib/active_record/internal_metadata.rb index 3626a13d7c..88b0c828ae 100644 --- a/activerecord/lib/active_record/internal_metadata.rb +++ b/activerecord/lib/active_record/internal_metadata.rb @@ -8,6 +8,10 @@ module ActiveRecord # as which environment migrations were run in. class InternalMetadata < ActiveRecord::Base # :nodoc: class << self + def _internal? + true + end + def primary_key "key" end diff --git a/activerecord/lib/active_record/middleware/database_selector.rb b/activerecord/lib/active_record/middleware/database_selector.rb new file mode 100644 index 0000000000..3ab50f5f6b --- /dev/null +++ b/activerecord/lib/active_record/middleware/database_selector.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require "active_record/middleware/database_selector/resolver" + +module ActiveRecord + module Middleware + # The DatabaseSelector Middleware provides a framework for automatically + # swapping from the primary to the replica database connection. Rails + # provides a basic framework to determine when to swap and allows for + # applications to write custom strategy classes to override the default + # behavior. + # + # The resolver class defines when the application should switch (i.e. read + # from the primary if a write occurred less than 2 seconds ago) and an + # operations class that sets a value that helps the resolver class decide + # when to switch. + # + # Rails default middleware uses the request's session to set a timestamp + # that informs the application when to read from a primary or read from a + # replica. + # + # To use the DatabaseSelector in your application with default settings add + # the following options to your environment config: + # + # config.active_record.database_selector = { delay: 2.seconds } + # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver + # config.active_record.database_operations = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session + # + # New applications will include these lines commented out in the production.rb. + # + # The default behavior can be changed by setting the config options to a + # custom class: + # + # config.active_record.database_selector = { delay: 2.seconds } + # config.active_record.database_resolver = MyResolver + # config.active_record.database_operations = MyResolver::MySession + class DatabaseSelector + def initialize(app, resolver_klass = Resolver, operations_klass = Resolver::Session, options = {}) + @app = app + @resolver_klass = resolver_klass + @operations_klass = operations_klass + @options = options + end + + attr_reader :resolver_klass, :operations_klass, :options + + # Middleware that determines which database connection to use in a multiple + # database application. + def call(env) + request = ActionDispatch::Request.new(env) + + select_database(request) do + @app.call(env) + end + end + + private + + def select_database(request, &blk) + operations = operations_klass.build(request) + database_resolver = resolver_klass.call(operations, options) + + if reading_request?(request) + database_resolver.read(&blk) + else + database_resolver.write(&blk) + end + end + + def reading_request?(request) + request.get? || request.head? + end + end + end +end diff --git a/activerecord/lib/active_record/middleware/database_selector/resolver.rb b/activerecord/lib/active_record/middleware/database_selector/resolver.rb new file mode 100644 index 0000000000..a84c292714 --- /dev/null +++ b/activerecord/lib/active_record/middleware/database_selector/resolver.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require "active_record/middleware/database_selector/resolver/session" + +module ActiveRecord + module Middleware + class DatabaseSelector + # The Resolver class is used by the DatabaseSelector middleware to + # determine which database the request should use. + # + # To change the behavior of the Resolver class in your application, + # create a custom resolver class that inherits from + # DatabaseSelector::Resolver and implements the methods that need to + # be changed. + # + # By default the Resolver class will send read traffic to the replica + # if it's been 2 seconds since the last write. + class Resolver # :nodoc: + SEND_TO_REPLICA_DELAY = 2.seconds + + def self.call(resolver, options = {}) + new(resolver, options) + end + + def initialize(resolver, options = {}) + @resolver = resolver + @options = options + @delay = @options && @options[:delay] ? @options[:delay] : SEND_TO_REPLICA_DELAY + @instrumenter = ActiveSupport::Notifications.instrumenter + end + + attr_reader :resolver, :delay, :instrumenter + + def read(&blk) + if read_from_primary? + read_from_primary(&blk) + else + read_from_replica(&blk) + end + end + + def write(&blk) + write_to_primary(&blk) + end + + private + + def read_from_primary(&blk) + ActiveRecord::Base.connection.while_preventing_writes do + ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do + instrumenter.instrument("database_selector.active_record.read_from_primary") do + yield + end + end + end + end + + def read_from_replica(&blk) + ActiveRecord::Base.connected_to(role: ActiveRecord::Base.reading_role) do + instrumenter.instrument("database_selector.active_record.read_from_replica") do + yield + end + end + end + + def write_to_primary(&blk) + ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do + instrumenter.instrument("database_selector.active_record.wrote_to_primary") do + yield + ensure + resolver.update_last_write_timestamp + end + end + end + + def read_from_primary? + !time_since_last_write_ok? + end + + def send_to_replica_delay + delay + end + + def time_since_last_write_ok? + Time.now - resolver.last_write_timestamp >= send_to_replica_delay + end + end + end + end +end diff --git a/activerecord/lib/active_record/middleware/database_selector/resolver/session.rb b/activerecord/lib/active_record/middleware/database_selector/resolver/session.rb new file mode 100644 index 0000000000..33e0af5ee4 --- /dev/null +++ b/activerecord/lib/active_record/middleware/database_selector/resolver/session.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module ActiveRecord + module Middleware + class DatabaseSelector + class Resolver + # The session class is used by the DatabaseSelector::Resolver to save + # timestamps of the last write in the session. + # + # The last_write is used to determine whether it's safe to read + # from the replica or the request needs to be sent to the primary. + class Session # :nodoc: + def self.build(request) + new(request.session) + end + + # Converts time to a timestamp that represents milliseconds since + # epoch. + def self.convert_time_to_timestamp(time) + time.to_i * 1000 + time.usec / 1000 + end + + # Converts milliseconds since epoch timestamp into a time object. + def self.convert_timestamp_to_time(timestamp) + timestamp ? Time.at(timestamp / 1000, (timestamp % 1000) * 1000) : Time.at(0) + end + + def initialize(session) + @session = session + end + + attr_reader :session + + def last_write_timestamp + self.class.convert_timestamp_to_time(session[:last_write]) + end + + def update_last_write_timestamp + session[:last_write] = self.class.convert_time_to_timestamp(Time.now) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 24782f8748..4b2e9ed81c 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1087,10 +1087,6 @@ module ActiveRecord migrations.last || NullMigration.new end - def parse_migration_filename(filename) # :nodoc: - File.basename(filename).scan(Migration::MigrationFilenameRegexp).first - end - def migrations migrations = migration_files.map do |file| version, name, scope = parse_migration_filename(file) @@ -1122,11 +1118,6 @@ module ActiveRecord (db_list + file_list).sort_by { |_, version, _| version } end - def migration_files - paths = Array(migrations_paths) - Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }] - end - def current_environment ActiveRecord::ConnectionHandling::DEFAULT_ENV.call end @@ -1145,6 +1136,15 @@ module ActiveRecord end private + def migration_files + paths = Array(migrations_paths) + Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }] + end + + def parse_migration_filename(filename) + File.basename(filename).scan(Migration::MigrationFilenameRegexp).first + end + def move(direction, steps) migrator = Migrator.new(direction, migrations) @@ -1169,13 +1169,6 @@ module ActiveRecord class << self attr_accessor :migrations_paths - def migrations_path=(path) - ActiveSupport::Deprecation.warn \ - "`ActiveRecord::Migrator.migrations_path=` is now deprecated and will be removed in Rails 6.0. " \ - "You can set the `migrations_paths` on the `connection` instead through the `database.yml`." - self.migrations_paths = [path] - end - # For cases where a table doesn't exist like loading from schema cache def current_version MigrationContext.new(migrations_paths).current_version diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb index 8f6fcfcaea..94906a2943 100644 --- a/activerecord/lib/active_record/migration/compatibility.rb +++ b/activerecord/lib/active_record/migration/compatibility.rb @@ -16,13 +16,55 @@ module ActiveRecord V6_0 = Current class V5_2 < V6_0 + module TableDefinition + def timestamps(**options) + options[:precision] ||= nil + super + end + end + module CommandRecorder def invert_transaction(args, &block) [:transaction, args, block] end end + def create_table(table_name, **options) + if block_given? + super { |t| yield compatible_table_definition(t) } + else + super + end + end + + def change_table(table_name, **options) + if block_given? + super { |t| yield compatible_table_definition(t) } + else + super + end + end + + def create_join_table(table_1, table_2, **options) + if block_given? + super { |t| yield compatible_table_definition(t) } + else + super + end + end + + def add_timestamps(table_name, **options) + options[:precision] ||= nil + super + end + private + def compatible_table_definition(t) + class << t + prepend TableDefinition + end + t + end def command_recorder recorder = super @@ -36,9 +78,7 @@ module ActiveRecord class V5_1 < V5_2 def change_column(table_name, column_name, type, options = {}) if adapter_name == "PostgreSQL" - clear_cache! - sql = connection.send(:change_column_sql, table_name, column_name, type, options) - execute "ALTER TABLE #{quote_table_name(table_name)} #{sql}" + super(table_name, column_name, type, options.except(:default, :null, :comment)) change_column_default(table_name, column_name, options[:default]) if options.key?(:default) change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment) @@ -89,35 +129,12 @@ module ActiveRecord options[:id] = :integer end - if block_given? - super do |t| - yield compatible_table_definition(t) - end - else - super - end - end - - def change_table(table_name, options = {}) - if block_given? - super do |t| - yield compatible_table_definition(t) - end - else - super - end + super end def create_join_table(table_1, table_2, column_options: {}, **options) column_options.reverse_merge!(type: :integer) - - if block_given? - super do |t| - yield compatible_table_definition(t) - end - else - super - end + super end def add_column(table_name, column_name, type, options = {}) @@ -138,7 +155,7 @@ module ActiveRecord class << t prepend TableDefinition end - t + super end end @@ -156,33 +173,13 @@ module ActiveRecord end end - def create_table(table_name, options = {}) - if block_given? - super do |t| - yield compatible_table_definition(t) - end - else - super - end - end - - def change_table(table_name, options = {}) - if block_given? - super do |t| - yield compatible_table_definition(t) - end - else - super - end - end - - def add_reference(*, **options) + def add_reference(table_name, ref_name, **options) options[:index] ||= false super end alias :add_belongs_to :add_reference - def add_timestamps(_, **options) + def add_timestamps(table_name, **options) options[:null] = true if options[:null].nil? super end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 7bf8d568df..2213fbefb4 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -96,11 +96,13 @@ module ActiveRecord # When running callbacks is not needed for each record update, # it is preferred to use {update_all}[rdoc-ref:Relation#update_all] # for updating all records in a single query. - def update(id, attributes) + def update(id = :all, attributes) if id.is_a?(Array) id.map { |one_id| find(one_id) }.each_with_index { |object, idx| object.update(attributes[idx]) } + elsif id == :all + all.each { |record| record.update(attributes) } else if ActiveRecord::Base === id raise ArgumentError, @@ -434,7 +436,7 @@ module ActiveRecord end alias update_attributes update - deprecate :update_attributes + deprecate update_attributes: "please, use update instead" # Updates its receiver just like #update but calls #save! instead # of +save+, so an exception is raised if the record is invalid and saving will fail. @@ -448,7 +450,7 @@ module ActiveRecord end alias update_attributes! update! - deprecate :update_attributes! + deprecate update_attributes!: "please, use update! instead" # Equivalent to <code>update_columns(name => value)</code>. def update_column(name, value) diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 538659d6bd..aac49a92b4 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -88,6 +88,14 @@ module ActiveRecord end end + initializer "active_record.database_selector" do + if options = config.active_record.delete(:database_selector) + resolver = config.active_record.delete(:database_resolver) + operations = config.active_record.delete(:database_operations) + config.app_middleware.use ActiveRecord::Middleware::DatabaseSelector, resolver, operations, options + end + end + initializer "Check for cache versioning support" do config.after_initialize do |app| ActiveSupport.on_load(:active_record) do @@ -140,7 +148,19 @@ end_error initializer "active_record.define_attribute_methods" do |app| config.after_initialize do ActiveSupport.on_load(:active_record) do - descendants.each(&:define_attribute_methods) if app.config.eager_load + if app.config.eager_load + descendants.each do |model| + # SchemaMigration and InternalMetadata both override `table_exists?` + # to bypass the schema cache, so skip them to avoid the extra queries. + next if model._internal? + + # If there's no connection yet, or the schema cache doesn't have the columns + # hash for the model cached, `define_attribute_methods` would trigger a query. + next unless model.connected? && model.connection.schema_cache.columns_hash?(model.table_name) + + model.define_attribute_methods + end + end end end end @@ -155,8 +175,18 @@ end_error initializer "active_record.set_configs" do |app| ActiveSupport.on_load(:active_record) do - configs = app.config.active_record.dup + configs = app.config.active_record + + represent_boolean_as_integer = configs.sqlite3.delete(:represent_boolean_as_integer) + + unless represent_boolean_as_integer.nil? + ActiveSupport.on_load(:active_record_sqlite3adapter) do + ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = represent_boolean_as_integer + end + end + configs.delete(:sqlite3) + configs.each do |k, v| send "#{k}=", v end @@ -167,6 +197,7 @@ end_error # and then establishes the connection. initializer "active_record.initialize_database" do ActiveSupport.on_load(:active_record) do + self.connection_handlers = { writing_role => ActiveRecord::Base.default_connection_handler } self.configurations = Rails.application.config.database_configuration establish_connection end @@ -224,35 +255,6 @@ end_error end end - initializer "active_record.check_represent_sqlite3_boolean_as_integer" do - config.after_initialize do - ActiveSupport.on_load(:active_record_sqlite3adapter) do - represent_boolean_as_integer = Rails.application.config.active_record.sqlite3.delete(:represent_boolean_as_integer) - unless represent_boolean_as_integer.nil? - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = represent_boolean_as_integer - end - - unless ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer - ActiveSupport::Deprecation.warn <<-MSG -Leaving `ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer` -set to false is deprecated. SQLite databases have used 't' and 'f' to serialize -boolean values and must have old data converted to 1 and 0 (its native boolean -serialization) before setting this flag to true. Conversion can be accomplished -by setting up a rake task which runs - - ExampleModel.where("boolean_column = 't'").update_all(boolean_column: 1) - ExampleModel.where("boolean_column = 'f'").update_all(boolean_column: 0) - -for all models and all boolean columns, after which the flag must be set to -true by adding the following to your application.rb file: - - Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true -MSG - end - end - end - end - initializer "active_record.set_filter_attributes" do ActiveSupport.on_load(:active_record) do self.filter_attributes += Rails.application.config.filter_parameters diff --git a/activerecord/lib/active_record/railties/collection_cache_association_loading.rb b/activerecord/lib/active_record/railties/collection_cache_association_loading.rb index b5129e4239..dfaac4eefb 100644 --- a/activerecord/lib/active_record/railties/collection_cache_association_loading.rb +++ b/activerecord/lib/active_record/railties/collection_cache_association_loading.rb @@ -20,12 +20,12 @@ module ActiveRecord end end - def collection_without_template + def collection_without_template(*) @relation.preload_associations(@collection) if @relation super end - def collection_with_template + def collection_with_template(*) @relation.preload_associations(@collection) if @relation super end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index d24324ecce..8de06e8466 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -407,6 +407,10 @@ namespace :railties do if railtie.respond_to?(:paths) && (path = railtie.paths["db/migrate"].first) railties[railtie.railtie_name] = path end + + unless ENV["MIGRATIONS_PATH"].blank? + railties[railtie.railtie_name] = railtie.root + ENV["MIGRATIONS_PATH"] + end end on_skip = Proc.new do |name, migration| diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index b2110f727c..6d2f75a3ae 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -612,21 +612,9 @@ module ActiveRecord # returns either +nil+ or the inverse association name that it finds. def automatic_inverse_of - return unless can_find_inverse_of_automatically?(self) + if can_find_inverse_of_automatically?(self) + inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym - inverse_name_candidates = - if options[:as] - [options[:as]] - else - active_record_name = active_record.name.demodulize - [active_record_name, ActiveSupport::Inflector.pluralize(active_record_name)] - end - - inverse_name_candidates.map! do |candidate| - ActiveSupport::Inflector.underscore(candidate).to_sym - end - - inverse_name_candidates.detect do |inverse_name| begin reflection = klass._reflect_on_association(inverse_name) rescue NameError @@ -635,7 +623,9 @@ module ActiveRecord reflection = false end - valid_inverse_reflection?(reflection) + if valid_inverse_reflection?(reflection) + return inverse_name + end end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index ba221a333b..a863227276 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -44,6 +44,11 @@ module ActiveRecord end def bind_attribute(name, value) # :nodoc: + if reflection = klass._reflect_on_association(name) + name = reflection.foreign_key + value = value.read_attribute(reflection.klass.primary_key) unless value.nil? + end + attr = arel_attribute(name) bind = predicate_builder.build_bind_attribute(attr.name, value) yield attr, bind diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 0fa5ba2e50..cef31bea94 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -41,15 +41,13 @@ module ActiveRecord def count(column_name = nil) if block_given? unless column_name.nil? - ActiveSupport::Deprecation.warn \ - "When `count' is called with a block, it ignores other arguments. " \ - "This behavior is now deprecated and will result in an ArgumentError in Rails 6.0." + raise ArgumentError, "Column name argument is not supported when a block is passed." end - return super() + super() + else + calculate(:count, column_name) end - - calculate(:count, column_name) end # Calculates the average value on a given column. Returns +nil+ if there's @@ -86,15 +84,13 @@ module ActiveRecord def sum(column_name = nil) if block_given? unless column_name.nil? - ActiveSupport::Deprecation.warn \ - "When `sum' is called with a block, it ignores other arguments. " \ - "This behavior is now deprecated and will result in an ArgumentError in Rails 6.0." + raise ArgumentError, "Column name argument is not supported when a block is passed." end - return super() + super() + else + calculate(:sum, column_name) end - - calculate(:sum, column_name) end # This calculates aggregate values in the given column. Methods for #count, #sum, #average, @@ -245,7 +241,7 @@ module ActiveRecord if distinct && (group_values.any? || select_values.empty? && order_values.empty?) column_name = primary_key end - elsif /\s*DISTINCT[\s(]+/i.match?(column_name.to_s) + elsif column_name.is_a?(::String) && /\bDISTINCT[\s(]/i.match?(column_name) distinct = nil end end @@ -401,7 +397,7 @@ module ActiveRecord case operation when "count" then value.to_i when "sum" then type.deserialize(value || 0) - when "average" then value.respond_to?(:to_d) ? value.to_d : value + when "average" then value&.respond_to?(:to_d) ? value.to_d : value else type.deserialize(value) end end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 6f67dd3784..6e8a1fcad4 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -112,15 +112,6 @@ module ActiveRecord if @klass.respond_to?(method) self.class.delegate_to_scoped_klass(method) scoping { @klass.public_send(method, *args, &block) } - elsif @delegate_to_klass && @klass.respond_to?(method, true) - ActiveSupport::Deprecation.warn \ - "Delegating missing #{method} method to #{@klass}. " \ - "Accessibility of private/protected class methods in :scope is deprecated and will be removed in Rails 6.0." - @klass.send(method, *args, &block) - elsif arel.respond_to?(method) - ActiveSupport::Deprecation.warn \ - "Delegating #{method} to arel is deprecated and will be removed in Rails 6.0." - arel.public_send(method, *args, &block) else super end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index dc03b196f4..8f1065c1e7 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -79,17 +79,12 @@ module ActiveRecord # Post.find_by "published_at < ?", 2.weeks.ago def find_by(arg, *args) where(arg, *args).take - rescue ::RangeError - nil end # Like #find_by, except that if no record is found, raises # an ActiveRecord::RecordNotFound error. def find_by!(arg, *args) where(arg, *args).take! - rescue ::RangeError - raise RecordNotFound.new("Couldn't find #{@klass.name} with an out of range value", - @klass.name, @klass.primary_key) end # Gives a record (or N records if a parameter is supplied) without any implied @@ -312,6 +307,8 @@ module ActiveRecord return false if !conditions || limit_value == 0 + conditions = sanitize_forbidden_attributes(conditions) + if eager_loading? relation = apply_join_dependency(eager_loading: false) return relation.exists?(conditions) @@ -320,8 +317,6 @@ module ActiveRecord relation = construct_relation_for_exists(conditions) skip_query_cache_if_necessary { connection.select_value(relation.arel, "#{name} Exists") } ? true : false - rescue ::RangeError - false end # This method is called whenever no records are found with either a single @@ -432,9 +427,6 @@ module ActiveRecord else find_some(ids) end - rescue ::RangeError - error_message = "Couldn't find #{model_name} with an out of range ID" - raise RecordNotFound.new(error_message, model_name, primary_key, ids) end def find_one(id) diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index b59ff912fe..240de3bb69 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -90,16 +90,21 @@ module ActiveRecord queries.reduce(&:or) elsif table.aggregated_with?(key) mapping = table.reflect_on_aggregation(key).mapping - queries = Array.wrap(value).map do |object| - mapping.map do |field_attr, aggregate_attr| - if mapping.size == 1 && !object.respond_to?(aggregate_attr) - build(table.arel_attribute(field_attr), object) - else - build(table.arel_attribute(field_attr), object.send(aggregate_attr)) - end - end.reduce(&:and) + values = value.nil? ? [nil] : Array.wrap(value) + if mapping.length == 1 || values.empty? + column_name, aggr_attr = mapping.first + values = values.map do |object| + object.respond_to?(aggr_attr) ? object.public_send(aggr_attr) : object + end + build(table.arel_attribute(column_name), values) + else + queries = values.map do |object| + mapping.map do |field_attr, aggregate_attr| + build(table.arel_attribute(field_attr), object.try!(aggregate_attr)) + end.reduce(&:and) + end + queries.reduce(&:or) end - queries.reduce(&:or) else build(table.arel_attribute(key), value) end diff --git a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb index 44bb2c7ab6..2ea27c8490 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb @@ -3,11 +3,7 @@ module ActiveRecord class PredicateBuilder class RangeHandler # :nodoc: - class RangeWithBinds < Struct.new(:begin, :end) - def exclude_end? - false - end - end + RangeWithBinds = Struct.new(:begin, :end, :exclude_end?) def initialize(predicate_builder) @predicate_builder = predicate_builder @@ -16,22 +12,7 @@ module ActiveRecord def call(attribute, value) begin_bind = predicate_builder.build_bind_attribute(attribute.name, value.begin) end_bind = predicate_builder.build_bind_attribute(attribute.name, value.end) - - if begin_bind.value.infinity? - if end_bind.value.infinity? - attribute.not_in([]) - elsif value.exclude_end? - attribute.lt(end_bind) - else - attribute.lteq(end_bind) - end - elsif end_bind.value.infinity? - attribute.gteq(begin_bind) - elsif value.exclude_end? - attribute.gteq(begin_bind).and(attribute.lt(end_bind)) - else - attribute.between(RangeWithBinds.new(begin_bind, end_bind)) - end + attribute.between(RangeWithBinds.new(begin_bind, end_bind, value.exclude_end?)) end private diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb index f64bd30d38..1dd6462d8d 100644 --- a/activerecord/lib/active_record/relation/query_attribute.rb +++ b/activerecord/lib/active_record/relation/query_attribute.rb @@ -20,22 +20,27 @@ module ActiveRecord def nil? !value_before_type_cast.is_a?(StatementCache::Substitute) && (value_before_type_cast.nil? || value_for_database.nil?) + rescue ::RangeError end - def boundable? - return @_boundable if defined?(@_boundable) - nil? - @_boundable = true + def infinite? + infinity?(value_before_type_cast) || infinity?(value_for_database) rescue ::RangeError - @_boundable = false end - def infinity? - _infinity?(value_before_type_cast) || boundable? && _infinity?(value_for_database) + def unboundable? + if defined?(@_unboundable) + @_unboundable + else + value_for_database unless value_before_type_cast.is_a?(StatementCache::Substitute) + @_unboundable = nil + end + rescue ::RangeError + @_unboundable = type.cast(value_before_type_cast) <=> 0 end private - def _infinity?(value) + def infinity?(value) value.respond_to?(:infinite?) && value.infinite? end end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 3485d9e557..e6197752bc 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -134,43 +134,6 @@ module ActiveRecord end private - # Accepts a hash of SQL conditions and replaces those attributes - # that correspond to a {#composed_of}[rdoc-ref:Aggregations::ClassMethods#composed_of] - # relationship with their expanded aggregate attribute values. - # - # Given: - # - # class Person < ActiveRecord::Base - # composed_of :address, class_name: "Address", - # mapping: [%w(address_street street), %w(address_city city)] - # end - # - # Then: - # - # { address: Address.new("813 abc st.", "chicago") } - # # => { address_street: "813 abc st.", address_city: "chicago" } - def expand_hash_conditions_for_aggregates(attrs) # :doc: - expanded_attrs = {} - attrs.each do |attr, value| - if aggregation = reflect_on_aggregation(attr.to_sym) - mapping = aggregation.mapping - mapping.each do |field_attr, aggregate_attr| - expanded_attrs[field_attr] = if value.is_a?(Array) - value.map { |it| it.send(aggregate_attr) } - elsif mapping.size == 1 && !value.respond_to?(aggregate_attr) - value - else - value.send(aggregate_attr) - end - end - else - expanded_attrs[attr] = value - end - end - expanded_attrs - end - deprecate :expand_hash_conditions_for_aggregates - def replace_bind_variables(statement, values) raise_if_bind_arity_mismatch(statement, statement.count("?"), values.size) bound = values.dup diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 216359867c..76bf53387d 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -51,20 +51,11 @@ module ActiveRecord if info[:version].present? ActiveRecord::SchemaMigration.create_table - connection.assume_migrated_upto_version(info[:version], migrations_paths) + connection.assume_migrated_upto_version(info[:version]) end ActiveRecord::InternalMetadata.create_table ActiveRecord::InternalMetadata[:environment] = connection.migration_context.current_environment end - - private - # Returns the migrations paths. - # - # ActiveRecord::Schema.new.migrations_paths - # # => ["db/migrate"] # Rails migration path by default. - def migrations_paths - ActiveRecord::Migrator.migrations_paths - end end end diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index f2d8b038fa..1fca1a18f6 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -10,6 +10,10 @@ module ActiveRecord # to be executed the next time. class SchemaMigration < ActiveRecord::Base # :nodoc: class << self + def _internal? + true + end + def primary_key "version" end diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index 1b1736dcab..95984e7ada 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -132,6 +132,8 @@ module ActiveRecord sql = query_builder.sql_for bind_values, connection klass.find_by_sql(sql, bind_values, preparable: true, &block) + rescue ::RangeError + nil end def self.unsupported_value?(value) diff --git a/activerecord/lib/active_record/test_fixtures.rb b/activerecord/lib/active_record/test_fixtures.rb index 7b7b3f7112..8c60d71669 100644 --- a/activerecord/lib/active_record/test_fixtures.rb +++ b/activerecord/lib/active_record/test_fixtures.rb @@ -122,7 +122,7 @@ module ActiveRecord # Begin transactions for connections already established @fixture_connections = enlist_fixture_connections @fixture_connections.each do |connection| - connection.begin_transaction joinable: false + connection.begin_transaction joinable: false, _lazy: false connection.pool.lock_thread = true if lock_threads end @@ -138,7 +138,7 @@ module ActiveRecord end if connection && !@fixture_connections.include?(connection) - connection.begin_transaction joinable: false + connection.begin_transaction joinable: false, _lazy: false connection.pool.lock_thread = true if lock_threads @fixture_connections << connection end @@ -173,10 +173,33 @@ module ActiveRecord end def enlist_fixture_connections + setup_shared_connection_pool + ActiveRecord::Base.connection_handler.connection_pool_list.map(&:connection) end private + + # Shares the writing connection pool with connections on + # other handlers. + # + # In an application with a primary and replica the test fixtures + # need to share a connection pool so that the reading connection + # can see data in the open transaction on the writing connection. + def setup_shared_connection_pool + writing_handler = ActiveRecord::Base.connection_handler + + ActiveRecord::Base.connection_handlers.values.each do |handler| + if handler != writing_handler + handler.connection_pool_list.each do |pool| + name = pool.spec.name + writing_connection = writing_handler.retrieve_connection_pool(name) + handler.send(:owner_to_pool)[name] = writing_connection + end + end + end + end + def load_fixtures(config) fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names, config) Hash[fixtures.map { |f| [f.name, f] }] diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index d32f971ad1..2345db7138 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -56,7 +56,7 @@ module ActiveRecord def touch_attributes_with_time(*names, time: nil) attribute_names = timestamp_attributes_for_update_in_model attribute_names |= names.map(&:to_s) - attribute_names.index_with(time ||= current_time_from_proper_timezone) + attribute_names.index_with(time || current_time_from_proper_timezone) end private @@ -133,11 +133,10 @@ module ActiveRecord self.class.send(:current_time_from_proper_timezone) end - def max_updated_column_timestamp(timestamp_names = timestamp_attributes_for_update_in_model) - timestamp_names - .map { |attr| self[attr] } + def max_updated_column_timestamp + timestamp_attributes_for_update_in_model + .map { |attr| self[attr]&.to_time } .compact - .map(&:to_time) .max end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 5a1dbc8e53..fb745af125 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -56,33 +56,21 @@ module ActiveRecord end def build_relation(klass, attribute, value) - if reflection = klass._reflect_on_association(attribute) - attribute = reflection.foreign_key - value = value.attributes[reflection.klass.primary_key] unless value.nil? - end - - if value.nil? - return klass.unscoped.where!(attribute => value) - end - - # the attribute may be an aliased attribute - if klass.attribute_alias?(attribute) - attribute = klass.attribute_alias(attribute) + relation = klass.unscoped + comparison = relation.bind_attribute(attribute, value) do |attr, bind| + return relation.none! if bind.unboundable? + + if bind.nil? + attr.eq(bind) + elsif options[:case_sensitive] + klass.connection.case_sensitive_comparison(attr, bind) + else + # will use SQL LOWER function before comparison, unless it detects a case insensitive collation + klass.connection.case_insensitive_comparison(attr, bind) + end end - attribute_name = attribute.to_s - value = klass.predicate_builder.build_bind_attribute(attribute_name, value) - - table = klass.arel_table - column = klass.columns_hash[attribute_name] - - comparison = if !options[:case_sensitive] - # will use SQL LOWER function before comparison, unless it detects a case insensitive collation - klass.connection.case_insensitive_comparison(table, attribute, column, value) - else - klass.connection.case_sensitive_comparison(table, attribute, column, value) - end - klass.unscoped.where!(comparison) + relation.where!(comparison) end def scope_relation(record, relation) diff --git a/activerecord/lib/arel.rb b/activerecord/lib/arel.rb index dab785738e..7411b5c41b 100644 --- a/activerecord/lib/arel.rb +++ b/activerecord/lib/arel.rb @@ -13,7 +13,6 @@ require "arel/alias_predication" require "arel/order_predications" require "arel/table" require "arel/attributes" -require "arel/compatibility/wheres" require "arel/visitors" require "arel/collectors/sql_string" diff --git a/activerecord/lib/arel/compatibility/wheres.rb b/activerecord/lib/arel/compatibility/wheres.rb deleted file mode 100644 index c8a73f0dae..0000000000 --- a/activerecord/lib/arel/compatibility/wheres.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -module Arel # :nodoc: all - module Compatibility # :nodoc: - class Wheres # :nodoc: - include Enumerable - - module Value # :nodoc: - attr_accessor :visitor - def value - visitor.accept self - end - - def name - super.to_sym - end - end - - def initialize(engine, collection) - @engine = engine - @collection = collection - end - - def each - to_sql = Visitors::ToSql.new @engine - - @collection.each { |c| - c.extend(Value) - c.visitor = to_sql - yield c - } - end - end - end -end diff --git a/activerecord/lib/arel/nodes/and.rb b/activerecord/lib/arel/nodes/and.rb index c530a77bfb..bf516db35f 100644 --- a/activerecord/lib/arel/nodes/and.rb +++ b/activerecord/lib/arel/nodes/and.rb @@ -2,7 +2,7 @@ module Arel # :nodoc: all module Nodes - class And < Arel::Nodes::Node + class And < Arel::Nodes::NodeExpression attr_reader :children def initialize(children) diff --git a/activerecord/lib/arel/nodes/bind_param.rb b/activerecord/lib/arel/nodes/bind_param.rb index ba8340558a..344e46479f 100644 --- a/activerecord/lib/arel/nodes/bind_param.rb +++ b/activerecord/lib/arel/nodes/bind_param.rb @@ -24,8 +24,12 @@ module Arel # :nodoc: all value.nil? end - def boundable? - !value.respond_to?(:boundable?) || value.boundable? + def infinite? + value.respond_to?(:infinite?) && value.infinite? + end + + def unboundable? + value.respond_to?(:unboundable?) && value.unboundable? end end end diff --git a/activerecord/lib/arel/nodes/case.rb b/activerecord/lib/arel/nodes/case.rb index 654a54825e..1c4b727bf6 100644 --- a/activerecord/lib/arel/nodes/case.rb +++ b/activerecord/lib/arel/nodes/case.rb @@ -2,7 +2,7 @@ module Arel # :nodoc: all module Nodes - class Case < Arel::Nodes::Node + class Case < Arel::Nodes::NodeExpression attr_accessor :case, :conditions, :default def initialize(expression = nil, default = nil) diff --git a/activerecord/lib/arel/nodes/casted.rb b/activerecord/lib/arel/nodes/casted.rb index c1e6e97d6d..6e911b717d 100644 --- a/activerecord/lib/arel/nodes/casted.rb +++ b/activerecord/lib/arel/nodes/casted.rb @@ -27,6 +27,10 @@ module Arel # :nodoc: all class Quoted < Arel::Nodes::Unary # :nodoc: alias :val :value def nil?; val.nil?; end + + def infinite? + value.respond_to?(:infinite?) && value.infinite? + end end def self.build_quoted(other, attribute = nil) diff --git a/activerecord/lib/arel/predications.rb b/activerecord/lib/arel/predications.rb index 77502dd199..7dafde4952 100644 --- a/activerecord/lib/arel/predications.rb +++ b/activerecord/lib/arel/predications.rb @@ -35,15 +35,17 @@ module Arel # :nodoc: all end def between(other) - if equals_quoted?(other.begin, -Float::INFINITY) - if equals_quoted?(other.end, Float::INFINITY) + if unboundable?(other.begin) == 1 || unboundable?(other.end) == -1 + self.in([]) + elsif open_ended?(other.begin) + if other.end.nil? || open_ended?(other.end) not_in([]) elsif other.exclude_end? lt(other.end) else lteq(other.end) end - elsif equals_quoted?(other.end, Float::INFINITY) + elsif other.end.nil? || open_ended?(other.end) gteq(other.begin) elsif other.exclude_end? gteq(other.begin).and(lt(other.end)) @@ -81,15 +83,17 @@ Passing a range to `#in` is deprecated. Call `#between`, instead. end def not_between(other) - if equals_quoted?(other.begin, -Float::INFINITY) - if equals_quoted?(other.end, Float::INFINITY) + if unboundable?(other.begin) == 1 || unboundable?(other.end) == -1 + not_in([]) + elsif open_ended?(other.begin) + if other.end.nil? || open_ended?(other.end) self.in([]) elsif other.exclude_end? gteq(other.end) else gt(other.end) end - elsif equals_quoted?(other.end, Float::INFINITY) + elsif other.end.nil? || open_ended?(other.end) lt(other.begin) else left = lt(other.begin) @@ -238,12 +242,16 @@ Passing a range to `#not_in` is deprecated. Call `#not_between`, instead. others.map { |v| quoted_node(v) } end - def equals_quoted?(maybe_quoted, value) - if maybe_quoted.is_a?(Nodes::Quoted) - maybe_quoted.val == value - else - maybe_quoted == value - end + def infinity?(value) + value.respond_to?(:infinite?) && value.infinite? + end + + def unboundable?(value) + value.respond_to?(:unboundable?) && value.unboundable? + end + + def open_ended?(value) + infinity?(value) || unboundable?(value) end end end diff --git a/activerecord/lib/arel/visitors/informix.rb b/activerecord/lib/arel/visitors/informix.rb index 0a9713794e..208fa15aef 100644 --- a/activerecord/lib/arel/visitors/informix.rb +++ b/activerecord/lib/arel/visitors/informix.rb @@ -15,8 +15,9 @@ module Arel # :nodoc: all collector << "ORDER BY " collector = inject_join o.orders, collector, ", " end - collector = maybe_visit o.lock, collector + maybe_visit o.lock, collector end + def visit_Arel_Nodes_SelectCore(o, collector) collector = inject_join o.projections, collector, ", " if o.source && !o.source.empty? diff --git a/activerecord/lib/arel/visitors/oracle12.rb b/activerecord/lib/arel/visitors/oracle12.rb index b092aa95e0..9a7fe4d626 100644 --- a/activerecord/lib/arel/visitors/oracle12.rb +++ b/activerecord/lib/arel/visitors/oracle12.rb @@ -20,7 +20,7 @@ module Arel # :nodoc: all def visit_Arel_Nodes_SelectOptions(o, collector) collector = maybe_visit o.offset, collector collector = maybe_visit o.limit, collector - collector = maybe_visit o.lock, collector + maybe_visit o.lock, collector end def visit_Arel_Nodes_Limit(o, collector) diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb index f9fe4404eb..d0dec63860 100644 --- a/activerecord/lib/arel/visitors/to_sql.rb +++ b/activerecord/lib/arel/visitors/to_sql.rb @@ -208,14 +208,12 @@ module Arel # :nodoc: all end visit_Arel_Nodes_SelectOptions(o, collector) - - collector end def visit_Arel_Nodes_SelectOptions(o, collector) collector = maybe_visit o.limit, collector collector = maybe_visit o.offset, collector - collector = maybe_visit o.lock, collector + maybe_visit o.lock, collector end def visit_Arel_Nodes_SelectCore(o, collector) @@ -578,7 +576,7 @@ module Arel # :nodoc: all def visit_Arel_Nodes_In(o, collector) if Array === o.right && !o.right.empty? - o.right.keep_if { |value| boundable?(value) } + o.right.delete_if { |value| unboundable?(value) } end if Array === o.right && o.right.empty? @@ -592,7 +590,7 @@ module Arel # :nodoc: all def visit_Arel_Nodes_NotIn(o, collector) if Array === o.right && !o.right.empty? - o.right.keep_if { |value| boundable?(value) } + o.right.delete_if { |value| unboundable?(value) } end if Array === o.right && o.right.empty? @@ -631,6 +629,8 @@ module Arel # :nodoc: all def visit_Arel_Nodes_Equality(o, collector) right = o.right + return collector << "1=0" if unboundable?(right) + collector = visit o.left, collector if right.nil? @@ -664,6 +664,8 @@ module Arel # :nodoc: all def visit_Arel_Nodes_NotEqual(o, collector) right = o.right + return collector << "1=1" if unboundable?(right) + collector = visit o.left, collector if right.nil? @@ -814,8 +816,8 @@ module Arel # :nodoc: all } end - def boundable?(value) - !value.respond_to?(:boundable?) || value.boundable? + def unboundable?(value) + value.respond_to?(:unboundable?) && value.unboundable? end def has_join_sources?(o) diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 66e594d771..05d8aa59c4 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -163,6 +163,16 @@ module ActiveRecord end end + def test_preventing_writes_predicate + assert_not_predicate @connection, :preventing_writes? + + @connection.while_preventing_writes do + assert_predicate @connection, :preventing_writes? + end + + assert_not_predicate @connection, :preventing_writes? + end + def test_errors_when_an_insert_query_is_called_while_preventing_writes assert_no_queries do assert_raises(ActiveRecord::ReadOnlyError) do diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index 2d71ee2f15..88c2ac5d0a 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -139,8 +139,8 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase with_real_execute do ActiveRecord::Base.connection.create_table :delete_me ActiveRecord::Base.connection.add_timestamps :delete_me, null: true - assert column_present?("delete_me", "updated_at", "datetime") - assert column_present?("delete_me", "created_at", "datetime") + assert column_exists?("delete_me", "updated_at", "datetime") + assert column_exists?("delete_me", "created_at", "datetime") ensure ActiveRecord::Base.connection.drop_table :delete_me rescue nil end @@ -152,8 +152,8 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase t.timestamps null: true end ActiveRecord::Base.connection.remove_timestamps :delete_me, null: true - assert_not column_present?("delete_me", "updated_at", "datetime") - assert_not column_present?("delete_me", "created_at", "datetime") + assert_not column_exists?("delete_me", "updated_at", "datetime") + assert_not column_exists?("delete_me", "created_at", "datetime") ensure ActiveRecord::Base.connection.drop_table :delete_me rescue nil end @@ -194,9 +194,4 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase def method_missing(method_symbol, *arguments) ActiveRecord::Base.connection.send(method_symbol, *arguments) end - - def column_present?(table_name, column_name, type) - results = ActiveRecord::Base.connection.select_all("SHOW FIELDS FROM #{table_name} LIKE '#{column_name}'") - results.first && results.first["Type"] == type - end end diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 42618c2ec3..2e7a4b498f 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -17,7 +17,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase enable_extension!("hstore", @connection) @connection.transaction do - @connection.create_table("pg_arrays") do |t| + @connection.create_table "pg_arrays", force: true do |t| t.string "tags", array: true, limit: 255 t.integer "ratings", array: true t.datetime :datetimes, array: true @@ -112,6 +112,18 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase assert_predicate column, :array? end + def test_change_column_from_non_array_to_array + @connection.add_column :pg_arrays, :snippets, :string + @connection.change_column :pg_arrays, :snippets, :text, array: true, default: [], using: "string_to_array(\"snippets\", ',')" + + PgArray.reset_column_information + column = PgArray.columns_hash["snippets"] + + assert_equal :text, column.type + assert_equal [], PgArray.column_defaults["snippets"] + assert_predicate column, :array? + end + def test_change_column_cant_make_non_array_column_to_array @connection.add_column :pg_arrays, :a_string, :string assert_raises ActiveRecord::StatementInvalid do @@ -226,14 +238,6 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase assert_equal(PgArray.last.tags, tag_values) end - def test_insert_fixtures - tag_values = ["val1", "val2", "val3_with_'_multiple_quote_'_chars"] - assert_deprecated do - @connection.insert_fixtures([{ "tags" => tag_values }], "pg_arrays") - end - assert_equal(PgArray.last.tags, tag_values) - end - def test_attribute_for_inspect_for_array_field record = PgArray.new { |a| a.ratings = (1..10).to_a } assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]", record.attribute_for_inspect(:ratings)) diff --git a/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb b/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb index 305e033642..79e9efcf06 100644 --- a/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb +++ b/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb @@ -7,22 +7,21 @@ class PostgresqlCaseInsensitiveTest < ActiveRecord::PostgreSQLTestCase def test_case_insensitiveness connection = ActiveRecord::Base.connection - table = Default.arel_table - column = Default.columns_hash["char1"] - comparison = connection.case_insensitive_comparison table, :char1, column, nil + attr = Default.arel_attribute(:char1) + comparison = connection.case_insensitive_comparison(attr, nil) assert_match(/lower/i, comparison.to_sql) - column = Default.columns_hash["char2"] - comparison = connection.case_insensitive_comparison table, :char2, column, nil + attr = Default.arel_attribute(:char2) + comparison = connection.case_insensitive_comparison(attr, nil) assert_match(/lower/i, comparison.to_sql) - column = Default.columns_hash["char3"] - comparison = connection.case_insensitive_comparison table, :char3, column, nil + attr = Default.arel_attribute(:char3) + comparison = connection.case_insensitive_comparison(attr, nil) assert_match(/lower/i, comparison.to_sql) - column = Default.columns_hash["multiline_default"] - comparison = connection.case_insensitive_comparison table, :multiline_default, column, nil + attr = Default.arel_attribute(:multiline_default) + comparison = connection.case_insensitive_comparison(attr, nil) assert_match(/lower/i, comparison.to_sql) end end diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index 40b58e86bf..9d26f32102 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -6,12 +6,8 @@ require "securerandom" class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase def setup + super @conn = ActiveRecord::Base.connection - @initial_represent_boolean_as_integer = ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer - end - - def teardown - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = @initial_represent_boolean_as_integer end def test_type_cast_binary_encoding_without_logger @@ -22,18 +18,10 @@ class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase end def test_type_cast_true - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = false - assert_equal "t", @conn.type_cast(true) - - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = true assert_equal 1, @conn.type_cast(true) end def test_type_cast_false - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = false - assert_equal "f", @conn.type_cast(false) - - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = true assert_equal 0, @conn.type_cast(false) end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 56ceb45040..5c41c14171 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -536,10 +536,6 @@ module ActiveRecord end end - def test_deprecate_valid_alter_table_type - assert_deprecated { @conn.valid_alter_table_type?(:string) } - end - def test_db_is_not_readonly_when_readonly_option_is_false conn = Base.sqlite3_connection database: ":memory:", adapter: "sqlite3", diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index f05dcac7dd..9d88b14dab 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -116,8 +116,8 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase end end - assert_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null - assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + assert @connection.column_exists?(:has_timestamps, :created_at, null: false) + assert @connection.column_exists?(:has_timestamps, :updated_at, null: false) end def test_timestamps_without_null_set_null_to_false_on_change_table @@ -129,8 +129,23 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase end end - assert_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null - assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + assert @connection.column_exists?(:has_timestamps, :created_at, null: false) + assert @connection.column_exists?(:has_timestamps, :updated_at, null: false) + end + + if ActiveRecord::Base.connection.supports_bulk_alter? + def test_timestamps_without_null_set_null_to_false_on_change_table_with_bulk + ActiveRecord::Schema.define do + create_table :has_timestamps + + change_table :has_timestamps, bulk: true do |t| + t.timestamps default: Time.now + end + end + + assert @connection.column_exists?(:has_timestamps, :created_at, null: false) + assert @connection.column_exists?(:has_timestamps, :updated_at, null: false) + end end def test_timestamps_without_null_set_null_to_false_on_add_timestamps @@ -139,7 +154,58 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase add_timestamps :has_timestamps, default: Time.now end - assert_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null - assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + assert @connection.column_exists?(:has_timestamps, :created_at, null: false) + assert @connection.column_exists?(:has_timestamps, :updated_at, null: false) + end + + if subsecond_precision_supported? + def test_timestamps_sets_presicion_on_create_table + ActiveRecord::Schema.define do + create_table :has_timestamps do |t| + t.timestamps + end + end + + assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false) + assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false) + end + + def test_timestamps_sets_presicion_on_change_table + ActiveRecord::Schema.define do + create_table :has_timestamps + + change_table :has_timestamps do |t| + t.timestamps default: Time.now + end + end + + assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false) + assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false) + end + + if ActiveRecord::Base.connection.supports_bulk_alter? + def test_timestamps_sets_presicion_on_change_table_with_bulk + ActiveRecord::Schema.define do + create_table :has_timestamps + + change_table :has_timestamps, bulk: true do |t| + t.timestamps default: Time.now + end + end + + assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false) + assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false) + end + end + + def test_timestamps_sets_presicion_on_add_timestamps + ActiveRecord::Schema.define do + create_table :has_timestamps + add_timestamps :has_timestamps, default: Time.now + end + + assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false) + assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false) + end end end diff --git a/activerecord/test/cases/arel/attributes/attribute_test.rb b/activerecord/test/cases/arel/attributes/attribute_test.rb index 671e273543..c7bd0a053b 100644 --- a/activerecord/test/cases/arel/attributes/attribute_test.rb +++ b/activerecord/test/cases/arel/attributes/attribute_test.rb @@ -560,7 +560,7 @@ module Arel end end - describe "with a range" do + describe "#between" do it "can be constructed with a standard range" do attribute = Attribute.new nil, nil node = attribute.between(1..3) @@ -628,7 +628,6 @@ module Arel node.must_equal Nodes::NotIn.new(attribute, []) end - it "can be constructed with a range ending at Infinity" do attribute = Attribute.new nil, nil node = attribute.between(0..::Float::INFINITY) @@ -639,6 +638,18 @@ module Arel ) end + if Gem::Version.new("2.6.0") <= Gem::Version.new(RUBY_VERSION) + it "can be constructed with a range implicitly ending at Infinity" do + attribute = Attribute.new nil, nil + node = attribute.between(eval("0..")) # Use eval for compatibility with Ruby < 2.6 parser + + node.must_equal Nodes::GreaterThanOrEqual.new( + attribute, + Nodes::Casted.new(0, attribute) + ) + end + end + it "can be constructed with a quoted range ending at Infinity" do attribute = Attribute.new nil, nil node = attribute.between(quoted_range(0, ::Float::INFINITY, false)) @@ -664,14 +675,6 @@ module Arel ) ]) end - - def quoted_range(begin_val, end_val, exclude) - OpenStruct.new( - begin: Nodes::Quoted.new(begin_val), - end: Nodes::Quoted.new(end_val), - exclude_end?: exclude, - ) - end end describe "#in" do @@ -753,21 +756,23 @@ module Arel end end - describe "with a range" do + describe "#not_between" do it "can be constructed with a standard range" do attribute = Attribute.new nil, nil node = attribute.not_between(1..3) - node.must_equal Nodes::Grouping.new(Nodes::Or.new( - Nodes::LessThan.new( - attribute, - Nodes::Casted.new(1, attribute) - ), - Nodes::GreaterThan.new( - attribute, - Nodes::Casted.new(3, attribute) + node.must_equal Nodes::Grouping.new( + Nodes::Or.new( + Nodes::LessThan.new( + attribute, + Nodes::Casted.new(1, attribute) + ), + Nodes::GreaterThan.new( + attribute, + Nodes::Casted.new(3, attribute) + ) ) - )) + ) end it "can be constructed with a range starting from -Infinity" do @@ -780,6 +785,16 @@ module Arel ) end + it "can be constructed with a quoted range starting from -Infinity" do + attribute = Attribute.new nil, nil + node = attribute.not_between(quoted_range(-::Float::INFINITY, 3, false)) + + node.must_equal Nodes::GreaterThan.new( + attribute, + Nodes::Quoted.new(3) + ) + end + it "can be constructed with an exclusive range starting from -Infinity" do attribute = Attribute.new nil, nil node = attribute.not_between(-::Float::INFINITY...3) @@ -790,6 +805,16 @@ module Arel ) end + it "can be constructed with a quoted exclusive range starting from -Infinity" do + attribute = Attribute.new nil, nil + node = attribute.not_between(quoted_range(-::Float::INFINITY, 3, true)) + + node.must_equal Nodes::GreaterThanOrEqual.new( + attribute, + Nodes::Quoted.new(3) + ) + end + it "can be constructed with an infinite range" do attribute = Attribute.new nil, nil node = attribute.not_between(-::Float::INFINITY..::Float::INFINITY) @@ -797,6 +822,13 @@ module Arel node.must_equal Nodes::In.new(attribute, []) end + it "can be constructed with a quoted infinite range" do + attribute = Attribute.new nil, nil + node = attribute.not_between(quoted_range(-::Float::INFINITY, ::Float::INFINITY, false)) + + node.must_equal Nodes::In.new(attribute, []) + end + it "can be constructed with a range ending at Infinity" do attribute = Attribute.new nil, nil node = attribute.not_between(0..::Float::INFINITY) @@ -807,20 +839,44 @@ module Arel ) end + if Gem::Version.new("2.6.0") <= Gem::Version.new(RUBY_VERSION) + it "can be constructed with a range implicitly ending at Infinity" do + attribute = Attribute.new nil, nil + node = attribute.not_between(eval("0..")) # Use eval for compatibility with Ruby < 2.6 parser + + node.must_equal Nodes::LessThan.new( + attribute, + Nodes::Casted.new(0, attribute) + ) + end + end + + it "can be constructed with a quoted range ending at Infinity" do + attribute = Attribute.new nil, nil + node = attribute.not_between(quoted_range(0, ::Float::INFINITY, false)) + + node.must_equal Nodes::LessThan.new( + attribute, + Nodes::Quoted.new(0) + ) + end + it "can be constructed with an exclusive range" do attribute = Attribute.new nil, nil node = attribute.not_between(0...3) - node.must_equal Nodes::Grouping.new(Nodes::Or.new( - Nodes::LessThan.new( - attribute, - Nodes::Casted.new(0, attribute) - ), - Nodes::GreaterThanOrEqual.new( - attribute, - Nodes::Casted.new(3, attribute) + node.must_equal Nodes::Grouping.new( + Nodes::Or.new( + Nodes::LessThan.new( + attribute, + Nodes::Casted.new(0, attribute) + ), + Nodes::GreaterThanOrEqual.new( + attribute, + Nodes::Casted.new(3, attribute) + ) ) - )) + ) end end @@ -1010,6 +1066,15 @@ module Arel condition.to_sql.must_equal %("foo"."id" = (select 1)) end end + + private + def quoted_range(begin_val, end_val, exclude) + OpenStruct.new( + begin: Nodes::Quoted.new(begin_val), + end: Nodes::Quoted.new(end_val), + exclude_end?: exclude, + ) + end end end end diff --git a/activerecord/test/cases/arel/nodes/and_test.rb b/activerecord/test/cases/arel/nodes/and_test.rb index eff54abd91..d123ca9fd0 100644 --- a/activerecord/test/cases/arel/nodes/and_test.rb +++ b/activerecord/test/cases/arel/nodes/and_test.rb @@ -16,6 +16,15 @@ module Arel assert_equal 2, array.uniq.size end end + + describe "functions as node expression" do + it "allows aliasing" do + aliased = And.new(["foo", "bar"]).as("baz") + + assert_kind_of As, aliased + assert_kind_of SqlLiteral, aliased.right + end + end end end end diff --git a/activerecord/test/cases/arel/nodes/case_test.rb b/activerecord/test/cases/arel/nodes/case_test.rb index 89861488df..946c2b0453 100644 --- a/activerecord/test/cases/arel/nodes/case_test.rb +++ b/activerecord/test/cases/arel/nodes/case_test.rb @@ -80,6 +80,16 @@ module Arel assert_equal 2, array.uniq.size end end + + describe "#as" do + it "allows aliasing" do + node = Case.new "foo" + as = node.as("bar") + + assert_equal node, as.left + assert_kind_of Arel::Nodes::SqlLiteral, as.right + end + end end end end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index acafbe0b4d..a61569420e 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -1293,17 +1293,17 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end def test_belongs_to_with_out_of_range_value_assigning - model = Class.new(Comment) do + model = Class.new(Author) do def self.name; "Temp"; end - validates :post, presence: true + validates :author_address, presence: true end - comment = model.new - comment.post_id = 9223372036854775808 # out of range in the bigint + author = model.new + author.author_address_id = 9223372036854775808 # out of range in the bigint - assert_nil comment.post - assert_not_predicate comment, :valid? - assert_equal [{ error: :blank }], comment.errors.details[:post] + assert_nil author.author_address + assert_not_predicate author, :valid? + assert_equal [{ error: :blank }], author.errors.details[:author_address] end def test_polymorphic_with_custom_primary_key diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 23a6dc04ad..4c9e4d0ad2 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -27,6 +27,7 @@ require "models/categorization" require "models/minivan" require "models/speedometer" require "models/reference" +require "models/job" require "models/college" require "models/student" require "models/pirate" @@ -1832,6 +1833,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal num_accounts, Account.count end + def test_depends_and_nullify_on_polymorphic_assoc + author = PersonWithPolymorphicDependentNullifyComments.create!(first_name: "Laertis") + comment = posts(:welcome).comments.first + comment.author = author + comment.save! + + assert_equal comment.author_id, author.id + assert_equal comment.author_type, author.class.name + + author.destroy + comment.reload + + assert_nil comment.author_id + assert_nil comment.author_type + end + def test_restrict_with_exception firm = RestrictedWithExceptionFirm.create!(name: "restrict") firm.companies.create(name: "child") @@ -2940,6 +2957,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end + def test_has_many_with_out_of_range_value + reference = Reference.create!(id: 2147483648) # out of range in the integer + assert_equal [], reference.ideal_jobs + end + private def force_signal37_to_load_all_clients_of_firm diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index bd535357ee..0133beccec 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -46,6 +46,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase Reader.create person_id: 0, post_id: 0 end + def test_has_many_through_create_record + assert books(:awdr).subscribers.create!(nick: "bob") + end + def test_marshal_dump preloaded = Post.includes(:first_blue_tags).first assert_equal preloaded, Marshal.load(Marshal.dump(preloaded)) diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index bf574f6637..3e5b5c1275 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -12,6 +12,9 @@ require "models/bulb" require "models/author" require "models/image" require "models/post" +require "models/drink_designer" +require "models/chef" +require "models/department" class HasOneAssociationsTest < ActiveRecord::TestCase self.use_transactional_tests = false unless supports_savepoints? @@ -114,6 +117,21 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_nil Account.find(old_account_id).firm_id end + def test_nullify_on_polymorphic_association + department = Department.create! + designer = DrinkDesignerWithPolymorphicDependentNullifyChef.create! + chef = department.chefs.create!(employable: designer) + + assert_equal chef.employable_id, designer.id + assert_equal chef.employable_type, designer.class.name + + designer.destroy! + chef.reload + + assert_nil chef.employable_id + assert_nil chef.employable_type + end + def test_nullification_on_destroyed_association developer = Developer.create!(name: "Someone") ship = Ship.create!(name: "Planet Caravan", developer: developer) diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index eb4dc73423..da3a42e2b5 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -20,8 +20,6 @@ require "models/company" require "models/project" require "models/author" require "models/post" -require "models/department" -require "models/hotel" class AutomaticInverseFindingTests < ActiveRecord::TestCase fixtures :ratings, :comments, :cars @@ -726,16 +724,6 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase # fails because Interest does have the correct inverse_of assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_man = Interest.first } end - - def test_favors_has_one_associations_for_inverse_of - inverse_name = Post.reflect_on_association(:author).inverse_of.name - assert_equal :post, inverse_name - end - - def test_finds_inverse_of_for_plural_associations - inverse_name = Department.reflect_on_association(:hotel).inverse_of.name - assert_equal :departments, inverse_name - end end # NOTE - these tests might not be meaningful, ripped as they were from the parental_control plugin diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index d21218a997..cf6e280898 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -430,7 +430,7 @@ class EachTest < ActiveRecord::TestCase assert_kind_of ActiveRecord::Relation, relation assert_kind_of Post, relation.first - relation = [not_a_post] * relation.count + [not_a_post] * relation.count end end end diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index bd5f157ca1..22a98036f3 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -77,10 +77,6 @@ if ActiveRecord::Base.connection.prepared_statements assert_logs_binds(binds) end - def test_deprecate_supports_statement_cache - assert_deprecated { ActiveRecord::Base.connection.supports_statement_cache? } - end - private def assert_logs_binds(binds) payload = { diff --git a/activerecord/test/cases/cache_key_test.rb b/activerecord/test/cases/cache_key_test.rb index 3a06b1c795..c27eb8a65d 100644 --- a/activerecord/test/cases/cache_key_test.rb +++ b/activerecord/test/cases/cache_key_test.rb @@ -51,7 +51,7 @@ module ActiveRecord end test "cache_version is the same when it comes from the DB or from the user" do - skip("Mysql2 does not return a string value for updated_at") if current_adapter?(:Mysql2Adapter) + skip("Mysql2 and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) record = CacheMeWithVersion.create record_from_db = CacheMeWithVersion.find(record.id) @@ -63,7 +63,7 @@ module ActiveRecord end test "cache_version does not truncate zeros when timestamp ends in zeros" do - skip("Mysql2 does not return a string value for updated_at") if current_adapter?(:Mysql2Adapter) + skip("Mysql2 and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) travel_to Time.now.beginning_of_day do record = CacheMeWithVersion.create @@ -84,7 +84,7 @@ module ActiveRecord end test "cache_version does NOT call updated_at when value is from the database" do - skip("Mysql2 does not return a string value for updated_at") if current_adapter?(:Mysql2Adapter) + skip("Mysql2 and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) record = CacheMeWithVersion.create record_from_db = CacheMeWithVersion.find(record.id) diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 4d3db912c5..850bc49676 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -57,12 +57,8 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 3, value end - def test_should_return_nil_to_d_as_average - if nil.respond_to?(:to_d) - assert_equal BigDecimal(0), NumericData.average(:bank_balance) - else - assert_nil NumericData.average(:bank_balance) - end + def test_should_return_nil_as_average + assert_nil NumericData.average(:bank_balance) end def test_should_get_maximum_of_field @@ -432,6 +428,8 @@ class CalculationsTest < ActiveRecord::TestCase def test_should_count_selected_field_with_include assert_equal 6, Account.includes(:firm).distinct.count assert_equal 4, Account.includes(:firm).distinct.select(:credit_limit).count + assert_equal 4, Account.includes(:firm).distinct.count("DISTINCT credit_limit") + assert_equal 4, Account.includes(:firm).distinct.count("DISTINCT(credit_limit)") end def test_should_not_perform_joined_include_by_default @@ -840,13 +838,13 @@ class CalculationsTest < ActiveRecord::TestCase def test_pick_one assert_equal "The First Topic", Topic.order(:id).pick(:heading) assert_nil Topic.none.pick(:heading) - assert_nil Topic.where("1=0").pick(:heading) + assert_nil Topic.where(id: 9999999999999999999).pick(:heading) end def test_pick_two assert_equal ["David", "david@loudthinking.com"], Topic.order(:id).pick(:author_name, :author_email_address) assert_nil Topic.none.pick(:author_name, :author_email_address) - assert_nil Topic.where("1=0").pick(:author_name, :author_email_address) + assert_nil Topic.where(id: 9999999999999999999).pick(:author_name, :author_email_address) end def test_pick_delegate_to_all @@ -919,15 +917,15 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal({ "proposed" => 2, "published" => 2 }, Book.group(:status).count) end - def test_deprecate_count_with_block_and_column_name - assert_deprecated do - assert_equal 6, Account.count(:firm_id) { true } + def test_count_with_block_and_column_name_raises_an_error + assert_raises(ArgumentError) do + Account.count(:firm_id) { true } end end - def test_deprecate_sum_with_block_and_column_name - assert_deprecated do - assert_equal 6, Account.sum(:firm_id) { 1 } + def test_sum_with_block_and_column_name_raises_an_error + assert_raises(ArgumentError) do + Account.sum(:firm_id) { 1 } end end diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index 51d0cc3d12..6282759a10 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -382,6 +382,11 @@ module ActiveRecord assert_not_nil ActiveRecord::Base.connection assert_same klass2.connection, ActiveRecord::Base.connection end + + def test_default_handlers_are_writing_and_reading + assert_equal :writing, ActiveRecord::Base.writing_role + assert_equal :reading, ActiveRecord::Base.reading_role + end end end end diff --git a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb index 0b3fb82e12..36591097b6 100644 --- a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb @@ -108,6 +108,7 @@ module ActiveRecord ActiveRecord::Base.connected_to(role: :reading) do @ro_handler = ActiveRecord::Base.connection_handler assert_equal ActiveRecord::Base.connection_handler, ActiveRecord::Base.connection_handlers[:reading] + assert_equal :reading, ActiveRecord::Base.current_role assert ActiveRecord::Base.connected_to?(role: :reading) assert_not ActiveRecord::Base.connected_to?(role: :writing) end @@ -115,6 +116,7 @@ module ActiveRecord ActiveRecord::Base.connected_to(role: :writing) do assert_equal ActiveRecord::Base.connection_handler, ActiveRecord::Base.connection_handlers[:writing] assert_not_equal @ro_handler, ActiveRecord::Base.connection_handler + assert_equal :writing, ActiveRecord::Base.current_role assert ActiveRecord::Base.connected_to?(role: :writing) assert_not ActiveRecord::Base.connected_to?(role: :reading) end @@ -124,11 +126,36 @@ module ActiveRecord ENV["RAILS_ENV"] = previous_env end + def test_establish_connection_using_3_levels_config_with_non_default_handlers + previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + + config = { + "default_env" => { + "readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" }, + "primary" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" } + } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + ActiveRecord::Base.connects_to(database: { default: :primary, readonly: :readonly }) + + assert_not_nil pool = ActiveRecord::Base.connection_handlers[:default].retrieve_connection_pool("primary") + assert_equal "db/primary.sqlite3", pool.spec.config[:database] + + assert_not_nil pool = ActiveRecord::Base.connection_handlers[:readonly].retrieve_connection_pool("primary") + assert_equal "db/readonly.sqlite3", pool.spec.config[:database] + ensure + ActiveRecord::Base.configurations = @prev_configs + ActiveRecord::Base.establish_connection(:arunit) + ENV["RAILS_ENV"] = previous_env + end + def test_switching_connections_with_database_url previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" previous_url, ENV["DATABASE_URL"] = ENV["DATABASE_URL"], "postgres://localhost/foo" ActiveRecord::Base.connected_to(database: { writing: "postgres://localhost/bar" }) do + assert_equal :writing, ActiveRecord::Base.current_role assert ActiveRecord::Base.connected_to?(role: :writing) handler = ActiveRecord::Base.connection_handler @@ -148,6 +175,7 @@ module ActiveRecord config = { adapter: "sqlite3", database: "db/readonly.sqlite3" } ActiveRecord::Base.connected_to(database: { writing: config }) do + assert_equal :writing, ActiveRecord::Base.current_role assert ActiveRecord::Base.connected_to?(role: :writing) handler = ActiveRecord::Base.connection_handler @@ -187,6 +215,7 @@ module ActiveRecord @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config ActiveRecord::Base.connected_to(database: :readonly) do + assert_equal :readonly, ActiveRecord::Base.current_role assert ActiveRecord::Base.connected_to?(role: :readonly) handler = ActiveRecord::Base.connection_handler @@ -211,6 +240,7 @@ module ActiveRecord assert_equal 1, ActiveRecord::Base.connection_handlers.size assert_equal ActiveRecord::Base.connection_handler, ActiveRecord::Base.connection_handlers[:writing] + assert_equal :writing, ActiveRecord::Base.current_role assert ActiveRecord::Base.connected_to?(role: :writing) ensure ActiveRecord::Base.configurations = @prev_configs @@ -330,13 +360,31 @@ module ActiveRecord end def test_calling_connected_to_on_a_non_existent_handler_raises - error = assert_raises ArgumentError do + error = assert_raises ActiveRecord::ConnectionNotEstablished do ActiveRecord::Base.connected_to(role: :reading) do - yield + Person.first end end - assert_equal "The reading role does not exist. Add it by establishing a connection with `connects_to` or use an existing role (writing).", error.message + assert_equal "No connection pool with 'primary' found for the 'reading' role.", error.message + end + + def test_default_handlers_are_writing_and_reading + assert_equal :writing, ActiveRecord::Base.writing_role + assert_equal :reading, ActiveRecord::Base.reading_role + end + + def test_an_application_can_change_the_default_handlers + old_writing = ActiveRecord::Base.writing_role + old_reading = ActiveRecord::Base.reading_role + ActiveRecord::Base.writing_role = :default + ActiveRecord::Base.reading_role = :readonly + + assert_equal :default, ActiveRecord::Base.writing_role + assert_equal :readonly, ActiveRecord::Base.reading_role + ensure + ActiveRecord::Base.writing_role = old_writing + ActiveRecord::Base.reading_role = old_reading end end end diff --git a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb index 06c1c51724..225cccc62c 100644 --- a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb +++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb @@ -46,6 +46,14 @@ module ActiveRecord assert_equal expected, actual end + def test_resolver_with_nil_database_url_and_current_env + ENV["RAILS_ENV"] = "foo" + config = { "foo" => { "adapter" => "postgres", "url" => ENV["DATABASE_URL"] } } + actual = resolve_spec(:foo, config) + expected = { "adapter" => "postgres", "url" => nil, "name" => "foo" } + assert_equal expected, actual + end + def test_resolver_with_database_uri_and_current_env_symbol_key_and_rack_env ENV["DATABASE_URL"] = "postgres://localhost/foo" ENV["RACK_ENV"] = "foo" diff --git a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb index 02e76ce146..38331aa641 100644 --- a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb @@ -27,8 +27,12 @@ if current_adapter?(:Mysql2Adapter) def test_string_types assert_lookup_type :string, "enum('one', 'two', 'three')" assert_lookup_type :string, "ENUM('one', 'two', 'three')" + assert_lookup_type :string, "enum ('one', 'two', 'three')" + assert_lookup_type :string, "ENUM ('one', 'two', 'three')" assert_lookup_type :string, "set('one', 'two', 'three')" assert_lookup_type :string, "SET('one', 'two', 'three')" + assert_lookup_type :string, "set ('one', 'two', 'three')" + assert_lookup_type :string, "SET ('one', 'two', 'three')" end def test_set_type_with_value_matching_other_type diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb index 67496381d1..727cab77f5 100644 --- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb +++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb @@ -91,6 +91,22 @@ module ActiveRecord @cache.clear_data_source_cache!("posts") end + test "#columns_hash? is populated by #columns_hash" do + assert_not @cache.columns_hash?("posts") + + @cache.columns_hash("posts") + + assert @cache.columns_hash?("posts") + end + + test "#columns_hash? is not populated by #data_source_exists?" do + assert_not @cache.columns_hash?("posts") + + @cache.data_source_exists?("posts") + + assert_not @cache.columns_hash?("posts") + end + private def schema_dump_path diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index 99d286dc52..cc4f86a0fb 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -144,7 +144,7 @@ class CounterCacheTest < ActiveRecord::TestCase test "update other counters on parent destroy" do david, joanna = dog_lovers(:david, :joanna) - joanna = joanna # squelch a warning + _ = joanna # squelch a warning assert_difference "joanna.reload.dogs_count", -1 do david.destroy diff --git a/activerecord/test/cases/database_selector_test.rb b/activerecord/test/cases/database_selector_test.rb new file mode 100644 index 0000000000..4106a6ec46 --- /dev/null +++ b/activerecord/test/cases/database_selector_test.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/person" +require "action_dispatch" + +module ActiveRecord + class DatabaseSelectorTest < ActiveRecord::TestCase + setup do + @session_store = {} + @session = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.new(@session_store) + end + + def test_empty_session + assert_equal Time.at(0), @session.last_write_timestamp + end + + def test_writing_the_session_timestamps + assert @session.update_last_write_timestamp + + session2 = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.new(@session_store) + assert_equal @session.last_write_timestamp, session2.last_write_timestamp + end + + def test_writing_session_time_changes + assert @session.update_last_write_timestamp + + before = @session.last_write_timestamp + sleep(0.1) + + assert @session.update_last_write_timestamp + assert_not_equal before, @session.last_write_timestamp + end + + def test_read_from_replicas + @session_store[:last_write] = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.convert_time_to_timestamp(Time.now - 5.seconds) + + resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session) + + called = false + resolver.read do + called = true + assert ActiveRecord::Base.connected_to?(role: :reading) + end + assert called + end + + def test_read_from_primary + @session_store[:last_write] = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.convert_time_to_timestamp(Time.now) + + resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session) + + called = false + resolver.read do + called = true + assert ActiveRecord::Base.connected_to?(role: :writing) + end + assert called + end + + def test_write_to_primary + resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session) + + # Session should start empty + assert_nil @session_store[:last_write] + + called = false + resolver.write do + assert ActiveRecord::Base.connected_to?(role: :writing) + called = true + end + assert called + + # and be populated by the last write time + assert @session_store[:last_write] + end + + def test_write_to_primary_with_exception + resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session) + + # Session should start empty + assert_nil @session_store[:last_write] + + called = false + assert_raises(ActiveRecord::RecordNotFound) do + resolver.write do + assert ActiveRecord::Base.connected_to?(role: :writing) + called = true + raise ActiveRecord::RecordNotFound + end + end + assert called + + # and be populated by the last write time + assert @session_store[:last_write] + end + + def test_read_from_primary_with_options + resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session, delay: 5.seconds) + + # Session should start empty + assert_nil @session_store[:last_write] + + called = false + resolver.write do + assert ActiveRecord::Base.connected_to?(role: :writing) + called = true + end + assert called + + # and be populated by the last write time + assert @session_store[:last_write] + + read = false + resolver.read do + assert ActiveRecord::Base.connected_to?(role: :writing) + read = true + end + assert read + end + + def test_read_from_replica_with_no_delay + resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session, delay: 0.seconds) + + # Session should start empty + assert_nil @session_store[:last_write] + + called = false + resolver.write do + assert ActiveRecord::Base.connected_to?(role: :writing) + called = true + end + assert called + + # and be populated by the last write time + assert @session_store[:last_write] + + read = false + resolver.read do + assert ActiveRecord::Base.connected_to?(role: :reading) + read = true + end + assert read + end + + def test_the_middleware_chooses_writing_role_with_POST_request + middleware = ActiveRecord::Middleware::DatabaseSelector.new(lambda { |env| + assert ActiveRecord::Base.connected_to?(role: :writing) + [200, {}, ["body"]] + }) + assert_equal [200, {}, ["body"]], middleware.call("REQUEST_METHOD" => "POST") + end + + def test_the_middleware_chooses_reading_role_with_GET_request + middleware = ActiveRecord::Middleware::DatabaseSelector.new(lambda { |env| + assert ActiveRecord::Base.connected_to?(role: :reading) + [200, {}, ["body"]] + }) + assert_equal [200, {}, ["body"]], middleware.call("REQUEST_METHOD" => "GET") + end + end +end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 961ae03a4c..6af2a43c7f 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -21,6 +21,7 @@ require "models/dog" require "models/car" require "models/tyre" require "models/subscriber" +require "support/stubs/strong_parameters" class FinderTest < ActiveRecord::TestCase fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :author_addresses, :customers, :categories, :categorizations, :cars @@ -224,6 +225,18 @@ class FinderTest < ActiveRecord::TestCase assert_equal true, Subscriber.exists?(" ") end + def test_exists_with_strong_parameters + assert_equal false, Subscriber.exists?(Parameters.new(nick: "foo").permit!) + + Subscriber.create!(nick: "foo") + + assert_equal true, Subscriber.exists?(Parameters.new(nick: "foo").permit!) + + assert_raises(ActiveModel::ForbiddenAttributesError) do + Subscriber.exists?(Parameters.new(nick: "foo")) + end + end + def test_exists_passing_active_record_object_is_not_permitted assert_raises(ArgumentError) do Topic.exists?(Topic.new) @@ -269,6 +282,17 @@ class FinderTest < ActiveRecord::TestCase assert_equal true, Topic.order(Arel.sql("invalid sql here")).exists? end + def test_exists_with_large_number + assert_equal true, Topic.where(id: [1, 9223372036854775808]).exists? + assert_equal true, Topic.where(id: 1..9223372036854775808).exists? + assert_equal true, Topic.where(id: -9223372036854775809..9223372036854775808).exists? + assert_equal false, Topic.where(id: 9223372036854775808..9223372036854775809).exists? + assert_equal false, Topic.where(id: -9223372036854775810..-9223372036854775809).exists? + assert_equal false, Topic.where(id: 9223372036854775808..1).exists? + assert_equal true, Topic.where(id: 1).or(Topic.where(id: 9223372036854775808)).exists? + assert_equal true, Topic.where.not(id: 9223372036854775808).exists? + end + def test_exists_with_joins assert_equal true, Topic.joins(:replies).where(replies_topics: { approved: true }).order("replies_topics.created_at DESC").exists? end @@ -370,16 +394,19 @@ class FinderTest < ActiveRecord::TestCase assert_raises(ActiveRecord::RecordNotFound) do Topic.where("1=1").find(9999999999999999999999999999999) end + assert_equal topics(:first), Topic.where(id: [1, 9999999999999999999999999999999]).find(1) end def test_find_by_on_relation_with_large_number assert_nil Topic.where("1=1").find_by(id: 9999999999999999999999999999999) + assert_equal topics(:first), Topic.where(id: [1, 9999999999999999999999999999999]).find_by(id: 1) end def test_find_by_bang_on_relation_with_large_number assert_raises(ActiveRecord::RecordNotFound) do Topic.where("1=1").find_by!(id: 9999999999999999999999999999999) end + assert_equal topics(:first), Topic.where(id: [1, 9999999999999999999999999999999]).find_by!(id: 1) end def test_find_an_empty_array @@ -938,6 +965,7 @@ class FinderTest < ActiveRecord::TestCase assert_kind_of Money, zaphod_balance found_customers = Customer.where(balance: [david_balance, zaphod_balance]) assert_equal [customers(:david), customers(:zaphod)], found_customers.sort_by(&:id) + assert_equal Customer.where(balance: [david_balance.amount, zaphod_balance.amount]).to_sql, found_customers.to_sql end def test_hash_condition_find_with_aggregate_attribute_having_same_name_as_field_and_key_value_being_aggregate @@ -975,6 +1003,24 @@ class FinderTest < ActiveRecord::TestCase assert_equal customers(:david), found_customer end + def test_hash_condition_find_nil_with_aggregate_having_one_mapping + assert_nil customers(:zaphod).gps_location + found_customer = Customer.where(gps_location: nil, name: customers(:zaphod).name).first + assert_equal customers(:zaphod), found_customer + end + + def test_hash_condition_find_nil_with_aggregate_having_multiple_mappings + customers(:david).update(address: nil) + assert_nil customers(:david).address_street + assert_nil customers(:david).address_city + found_customer = Customer.where(address: nil, name: customers(:david).name).first + assert_equal customers(:david), found_customer + end + + def test_hash_condition_find_empty_array_with_aggregate_having_multiple_mappings + assert_nil Customer.where(address: []).first + end + def test_condition_utc_time_interpolation_with_default_timezone_local with_env_tz "America/New_York" do with_timezone_config default: :local do diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index fe2f417a04..b4f28fbfd6 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -301,20 +301,6 @@ class FixturesTest < ActiveRecord::TestCase assert_equal fixtures, result.to_a end - def test_deprecated_insert_fixtures - fixtures = [ - { "name" => "first", "wheels_count" => 2 }, - { "name" => "second", "wheels_count" => 3 } - ] - conn = ActiveRecord::Base.connection - conn.delete("DELETE FROM aircraft") - assert_deprecated do - conn.insert_fixtures(fixtures, "aircraft") - end - result = conn.select_all("SELECT name, wheels_count FROM aircraft ORDER BY id") - assert_equal fixtures, result.to_a - end - def test_broken_yaml_exception badyaml = Tempfile.new ["foo", ".yml"] badyaml.write "a: : " @@ -938,7 +924,7 @@ class TransactionalFixturesOnConnectionNotification < ActiveRecord::TestCase def lock_thread=(lock_thread); end end.new - assert_called_with(connection, :begin_transaction, [joinable: false]) do + assert_called_with(connection, :begin_transaction, [joinable: false, _lazy: false]) do fire_connection_notification(connection) end end @@ -1362,3 +1348,37 @@ class NilFixturePathTest < ActiveRecord::TestCase MSG end end + +class MultipleDatabaseFixturesTest < ActiveRecord::TestCase + test "enlist_fixture_connections ensures multiple databases share a connection pool" do + with_temporary_connection_pool do + ActiveRecord::Base.connects_to database: { writing: :arunit, reading: :arunit2 } + + rw_conn = ActiveRecord::Base.connection + ro_conn = ActiveRecord::Base.connection_handlers[:reading].connection_pool_list.first.connection + + assert_not_equal rw_conn, ro_conn + + enlist_fixture_connections + + rw_conn = ActiveRecord::Base.connection + ro_conn = ActiveRecord::Base.connection_handlers[:reading].connection_pool_list.first.connection + + assert_equal rw_conn, ro_conn + end + ensure + ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.connection_handler } + end + + private + + def with_temporary_connection_pool + old_pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base.connection_specification_name) + new_pool = ActiveRecord::ConnectionAdapters::ConnectionPool.new ActiveRecord::Base.connection_pool.spec + ActiveRecord::Base.connection_handler.send(:owner_to_pool)["primary"] = new_pool + + yield + ensure + ActiveRecord::Base.connection_handler.send(:owner_to_pool)["primary"] = old_pool + end +end diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 3d3189900f..19655a2d38 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -240,7 +240,7 @@ class InheritanceTest < ActiveRecord::TestCase cabbage = vegetable.becomes!(Cabbage) assert_equal "Cabbage", cabbage.custom_type - vegetable = cabbage.becomes!(Vegetable) + cabbage.becomes!(Vegetable) assert_nil cabbage.custom_type end @@ -654,7 +654,7 @@ class InheritanceAttributeMappingTest < ActiveRecord::TestCase assert_equal ["omg_inheritance_attribute_mapping_test/company"], ActiveRecord::Base.connection.select_values("SELECT sponsorable_type FROM sponsors") - sponsor = Sponsor.first + sponsor = Sponsor.find(sponsor.id) assert_equal startup, sponsor.sponsorable end end diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb index 5687afbc71..4185e8d682 100644 --- a/activerecord/test/cases/integration_test.rb +++ b/activerecord/test/cases/integration_test.rb @@ -191,21 +191,6 @@ class IntegrationTest < ActiveRecord::TestCase end end - def test_named_timestamps_for_cache_key - assert_deprecated do - owner = owners(:blackbeard) - assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:usec)}", owner.cache_key(:updated_at, :happy_at) - end - end - - def test_cache_key_when_named_timestamp_is_nil - assert_deprecated do - owner = owners(:blackbeard) - owner.happy_at = nil - assert_equal "owners/#{owner.id}", owner.cache_key(:happy_at) - end - end - def test_cache_key_is_stable_with_versioning_on with_cache_versioning do developer = Developer.first diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb index 017ee7951e..5753bd7117 100644 --- a/activerecord/test/cases/migration/compatibility_test.rb +++ b/activerecord/test/cases/migration/compatibility_test.rb @@ -86,8 +86,8 @@ module ActiveRecord ActiveRecord::Migrator.new(:up, [migration]).migrate - assert connection.columns(:more_testings).find { |c| c.name == "created_at" }.null - assert connection.columns(:more_testings).find { |c| c.name == "updated_at" }.null + assert connection.column_exists?(:more_testings, :created_at, null: true) + assert connection.column_exists?(:more_testings, :updated_at, null: true) ensure connection.drop_table :more_testings rescue nil end @@ -103,8 +103,25 @@ module ActiveRecord ActiveRecord::Migrator.new(:up, [migration]).migrate - assert connection.columns(:testings).find { |c| c.name == "created_at" }.null - assert connection.columns(:testings).find { |c| c.name == "updated_at" }.null + assert connection.column_exists?(:testings, :created_at, null: true) + assert connection.column_exists?(:testings, :updated_at, null: true) + end + + if ActiveRecord::Base.connection.supports_bulk_alter? + def test_timestamps_have_null_constraints_if_not_present_in_migration_of_change_table_with_bulk + migration = Class.new(ActiveRecord::Migration[4.2]) { + def migrate(x) + change_table :testings, bulk: true do |t| + t.timestamps + end + end + }.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + assert connection.column_exists?(:testings, :created_at, null: true) + assert connection.column_exists?(:testings, :updated_at, null: true) + end end def test_timestamps_have_null_constraints_if_not_present_in_migration_for_adding_timestamps_to_existing_table @@ -116,8 +133,70 @@ module ActiveRecord ActiveRecord::Migrator.new(:up, [migration]).migrate - assert connection.columns(:testings).find { |c| c.name == "created_at" }.null - assert connection.columns(:testings).find { |c| c.name == "updated_at" }.null + assert connection.column_exists?(:testings, :created_at, null: true) + assert connection.column_exists?(:testings, :updated_at, null: true) + end + + def test_timestamps_doesnt_set_precision_on_create_table + migration = Class.new(ActiveRecord::Migration[5.2]) { + def migrate(x) + create_table :more_testings do |t| + t.timestamps + end + end + }.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + assert connection.column_exists?(:more_testings, :created_at, null: false, **precision_implicit_default) + assert connection.column_exists?(:more_testings, :updated_at, null: false, **precision_implicit_default) + ensure + connection.drop_table :more_testings rescue nil + end + + def test_timestamps_doesnt_set_precision_on_change_table + migration = Class.new(ActiveRecord::Migration[5.2]) { + def migrate(x) + change_table :testings do |t| + t.timestamps default: Time.now + end + end + }.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + assert connection.column_exists?(:testings, :created_at, null: false, **precision_implicit_default) + assert connection.column_exists?(:testings, :updated_at, null: false, **precision_implicit_default) + end + + if ActiveRecord::Base.connection.supports_bulk_alter? + def test_timestamps_doesnt_set_precision_on_change_table_with_bulk + migration = Class.new(ActiveRecord::Migration[5.2]) { + def migrate(x) + change_table :testings, bulk: true do |t| + t.timestamps + end + end + }.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + assert connection.column_exists?(:testings, :created_at, null: false, **precision_implicit_default) + assert connection.column_exists?(:testings, :updated_at, null: false, **precision_implicit_default) + end + end + + def test_timestamps_doesnt_set_precision_on_add_timestamps + migration = Class.new(ActiveRecord::Migration[5.2]) { + def migrate(x) + add_timestamps :testings, default: Time.now + end + }.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + assert connection.column_exists?(:testings, :created_at, null: false, **precision_implicit_default) + assert connection.column_exists?(:testings, :updated_at, null: false, **precision_implicit_default) end def test_legacy_migrations_raises_exception_when_inherited @@ -159,6 +238,15 @@ module ActiveRecord ActiveRecord::Base.clear_cache! end end + + private + def precision_implicit_default + if current_adapter?(:Mysql2Adapter) + { presicion: 0 } + else + { presicion: nil } + end + end end end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 8b0ecd2516..02031e51ef 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -71,13 +71,10 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Migration.verbose = @verbose_was end - def test_migrator_migrations_path_is_deprecated - assert_deprecated do - ActiveRecord::Migrator.migrations_path = "/whatever" - end - ensure + def test_passing_migrations_paths_to_assume_migrated_upto_version_is_deprecated + ActiveRecord::SchemaMigration.create_table assert_deprecated do - ActiveRecord::Migrator.migrations_path = "db/migrate" + ActiveRecord::Base.connection.assume_migrated_upto_version(0, []) end end @@ -442,7 +439,6 @@ class MigrationTest < ActiveRecord::TestCase current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call migrations_path = MIGRATIONS_ROOT + "/valid" - current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call migrator = ActiveRecord::MigrationContext.new(migrations_path) migrator.up assert_equal current_env, ActiveRecord::InternalMetadata[:environment] @@ -630,6 +626,18 @@ class MigrationTest < ActiveRecord::TestCase ensure Person.connection.drop_table :test_text_limits, if_exists: true end + + def test_invalid_text_size_should_raise + e = assert_raise(ArgumentError) do + Person.connection.create_table :test_text_sizes, force: true do |t| + t.text :bigtext, size: 0xfffffffff + end + end + + assert_match(/#{0xfffffffff} is invalid :size value\. Only :tiny, :medium, and :long are allowed\./, e.message) + ensure + Person.connection.drop_table :test_text_sizes, if_exists: true + end end if ActiveRecord::Base.connection.supports_advisory_locks? diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 4830ff2b5f..d5057ad381 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -53,6 +53,20 @@ class PersistenceTest < ActiveRecord::TestCase assert_not_equal "2 updated", Topic.find(2).content end + def test_class_level_update_without_ids + topics = Topic.all + assert_equal 5, topics.length + topics.each do |topic| + assert_not_equal "updated", topic.content + end + + updated = Topic.update(content: "updated") + assert_equal 5, updated.length + updated.each do |topic| + assert_equal "updated", topic.content + end + end + def test_class_level_update_is_affected_by_scoping topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } } diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 04bbc7d136..eb32b690aa 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -502,6 +502,44 @@ class QueryCacheTest < ActiveRecord::TestCase }.call({}) end + def test_clear_query_cache_is_called_on_all_connections + skip "with in memory db, reading role won't be able to see database on writing role" if in_memory_db? + with_temporary_connection_pool do + ActiveRecord::Base.connection_handlers = { + writing: ActiveRecord::Base.default_connection_handler, + reading: ActiveRecord::ConnectionAdapters::ConnectionHandler.new + } + + ActiveRecord::Base.connected_to(role: :reading) do + ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations["arunit"]) + end + + mw = middleware { |env| + ActiveRecord::Base.connected_to(role: :reading) do + @topic = Topic.first + end + + assert @topic + + ActiveRecord::Base.connected_to(role: :writing) do + @topic.title = "It doesn't have to be crazy at work" + @topic.save! + end + + assert_equal "It doesn't have to be crazy at work", @topic.title + + ActiveRecord::Base.connected_to(role: :reading) do + @topic = Topic.first + assert_equal "It doesn't have to be crazy at work", @topic.title + end + } + + mw.call({}) + end + ensure + ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler } + end + private def with_temporary_connection_pool diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb index b630f782bc..402ddcf05a 100644 --- a/activerecord/test/cases/reaper_test.rb +++ b/activerecord/test/cases/reaper_test.rb @@ -48,7 +48,7 @@ module ActiveRecord reaper = ConnectionPool::Reaper.new(fp, 0.0001) reaper.run - until fp.reaped + until fp.flushed Thread.pass end assert fp.reaped diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index a8030c2d64..b600c999a6 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -23,23 +23,8 @@ module ActiveRecord end end - module DeprecatedArelDelegationTests - AREL_METHODS = [ - :with, :orders, :froms, :project, :projections, :taken, :constraints, :exists, :locked, :where_sql, - :ast, :source, :join_sources, :to_dot, :create_insert, :create_true, :create_false - ] - - def test_deprecate_arel_delegation - AREL_METHODS.each do |method| - assert_deprecated { target.public_send(method) } - assert_deprecated { target.public_send(method) } - end - end - end - class DelegationAssociationTest < ActiveRecord::TestCase include ArrayDelegationTests - include DeprecatedArelDelegationTests def target Post.new.comments @@ -48,7 +33,6 @@ module ActiveRecord class DelegationRelationTest < ActiveRecord::TestCase include ArrayDelegationTests - include DeprecatedArelDelegationTests def target Comment.all diff --git a/activerecord/test/cases/relation/or_test.rb b/activerecord/test/cases/relation/or_test.rb index 065819e0f1..8623867864 100644 --- a/activerecord/test/cases/relation/or_test.rb +++ b/activerecord/test/cases/relation/or_test.rb @@ -30,6 +30,11 @@ module ActiveRecord assert_equal expected, Post.where("id = 1").or(Post.none).to_a end + def test_or_with_large_number + expected = Post.where("id = 1 or id = 9223372036854775808").to_a + assert_equal expected, Post.where(id: 1).or(Post.where(id: 9223372036854775808)).to_a + end + def test_or_with_bind_params assert_equal Post.find([1, 2]).sort_by(&:id), Post.where(id: 1).or(Post.where(id: 2)).sort_by(&:id) end diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index 99797528b2..d49ed092b2 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -359,6 +359,16 @@ module ActiveRecord assert_equal author, Author.where(params.permit!).first end + def test_where_with_large_number + assert_equal [authors(:bob)], Author.where(id: [3, 9223372036854775808]) + assert_equal [authors(:bob)], Author.where(id: 3..9223372036854775808) + end + + def test_to_sql_with_large_number + assert_equal [authors(:bob)], Author.find_by_sql(Author.where(id: [3, 9223372036854775808]).to_sql) + assert_equal [authors(:bob)], Author.find_by_sql(Author.where(id: 3..9223372036854775808).to_sql) + end + def test_where_with_unsupported_arguments assert_raises(ArgumentError) { Author.where(42) } end diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index 778cf86ac3..18b27bd6d1 100644 --- a/activerecord/test/cases/sanitize_test.rb +++ b/activerecord/test/cases/sanitize_test.rb @@ -168,12 +168,6 @@ class SanitizeTest < ActiveRecord::TestCase assert_equal "#{ActiveRecord::Base.connection.quote('10')}::integer '2009-01-01'::date", l.call end - def test_deprecated_expand_hash_conditions_for_aggregates - assert_deprecated do - assert_equal({ "balance" => 50 }, Customer.send(:expand_hash_conditions_for_aggregates, balance: Money.new(50))) - end - end - private def bind(statement, *vars) if vars.first.is_a?(Hash) diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index dda3efa47c..49e9be9565 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -245,25 +245,31 @@ class SchemaDumperTest < ActiveRecord::TestCase if current_adapter?(:Mysql2Adapter) def test_schema_dump_includes_length_for_mysql_binary_fields - output = standard_dump + output = dump_table_schema "binary_fields" assert_match %r{t\.binary\s+"var_binary",\s+limit: 255$}, output assert_match %r{t\.binary\s+"var_binary_large",\s+limit: 4095$}, output end def test_schema_dump_includes_length_for_mysql_blob_and_text_fields - output = standard_dump - assert_match %r{t\.blob\s+"tiny_blob",\s+limit: 255$}, output + output = dump_table_schema "binary_fields" + assert_match %r{t\.binary\s+"tiny_blob",\s+size: :tiny$}, output assert_match %r{t\.binary\s+"normal_blob"$}, output - assert_match %r{t\.binary\s+"medium_blob",\s+limit: 16777215$}, output - assert_match %r{t\.binary\s+"long_blob",\s+limit: 4294967295$}, output - assert_match %r{t\.text\s+"tiny_text",\s+limit: 255$}, output + assert_match %r{t\.binary\s+"medium_blob",\s+size: :medium$}, output + assert_match %r{t\.binary\s+"long_blob",\s+size: :long$}, output + assert_match %r{t\.text\s+"tiny_text",\s+size: :tiny$}, output assert_match %r{t\.text\s+"normal_text"$}, output - assert_match %r{t\.text\s+"medium_text",\s+limit: 16777215$}, output - assert_match %r{t\.text\s+"long_text",\s+limit: 4294967295$}, output + assert_match %r{t\.text\s+"medium_text",\s+size: :medium$}, output + assert_match %r{t\.text\s+"long_text",\s+size: :long$}, output + assert_match %r{t\.binary\s+"tiny_blob_2",\s+size: :tiny$}, output + assert_match %r{t\.binary\s+"medium_blob_2",\s+size: :medium$}, output + assert_match %r{t\.binary\s+"long_blob_2",\s+size: :long$}, output + assert_match %r{t\.text\s+"tiny_text_2",\s+size: :tiny$}, output + assert_match %r{t\.text\s+"medium_text_2",\s+size: :medium$}, output + assert_match %r{t\.text\s+"long_text_2",\s+size: :long$}, output end def test_schema_does_not_include_limit_for_emulated_mysql_boolean_fields - output = standard_dump + output = dump_table_schema "booleans" assert_no_match %r{t\.boolean\s+"has_fun",.+limit: 1}, output end diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index f707951a16..418a2ae04e 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -303,13 +303,6 @@ class NamedScopingTest < ActiveRecord::TestCase assert_equal "lifo", topic.author_name end - def test_deprecated_delegating_private_method - assert_deprecated do - scope = Topic.all.by_private_lifo - assert_not scope.instance_variable_get(:@delegate_to_klass) - end - end - def test_reserved_scope_names klass = Class.new(ActiveRecord::Base) do self.table_name = "topics" diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index f6cd4f85ee..fa136fe8da 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -22,7 +22,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase end def test_serialize_does_not_eagerly_load_columns - Topic.reset_column_information + reset_column_information_of(Topic) assert_no_queries do Topic.serialize(:content) end @@ -377,7 +377,8 @@ class SerializedAttributeTest < ActiveRecord::TestCase topic.update group: "1" model.serialize :group, JSON - model.reset_column_information + + reset_column_information_of(model) # This isn't strictly necessary for the test, but a little bit of # knowledge of internals allows us to make failures far more likely. @@ -397,4 +398,12 @@ class SerializedAttributeTest < ActiveRecord::TestCase # raw string ("1"), or raise an exception. assert_equal [1] * threads.size, threads.map(&:value) end + + private + + def reset_column_information_of(topic_class) + topic_class.reset_column_information + # reset original topic to undefine attribute methods + ::Topic.reset_column_information + end end diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb index e3c12f68fd..6a6d73dc38 100644 --- a/activerecord/test/cases/statement_cache_test.rb +++ b/activerecord/test/cases/statement_cache_test.rb @@ -4,6 +4,7 @@ require "cases/helper" require "models/book" require "models/liquid" require "models/molecule" +require "models/numeric_data" require "models/electron" module ActiveRecord @@ -74,6 +75,11 @@ module ActiveRecord assert_equal "salty", liquids[0].name end + def test_statement_cache_with_strictly_cast_attribute + row = NumericData.create(temperature: 1.5) + assert_equal row, NumericData.find_by(temperature: 1.5) + end + def test_statement_cache_values_differ cache = ActiveRecord::StatementCache.create(Book.connection) do |params| Book.where(name: "my book") diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 45c93ca949..1009dd0f99 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -884,17 +884,6 @@ class TransactionTest < ActiveRecord::TestCase assert_predicate transaction.state, :committed? end - def test_set_state_method_is_deprecated - connection = Topic.connection - transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction - - transaction.commit - - assert_deprecated do - transaction.state.set_state(:rolledback) - end - end - def test_mark_transaction_state_as_committed connection = Topic.connection transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction diff --git a/activerecord/test/cases/type/time_test.rb b/activerecord/test/cases/type/time_test.rb new file mode 100644 index 0000000000..1a2c47479f --- /dev/null +++ b/activerecord/test/cases/type/time_test.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/topic" + +module ActiveRecord + module Type + class TimeTest < ActiveRecord::TestCase + def test_default_year_is_correct + expected_time = ::Time.utc(2000, 1, 1, 10, 30, 0) + topic = Topic.new(bonus_time: { 4 => 10, 5 => 30 }) + + assert_equal expected_time, topic.bonus_time + + topic.save! + topic.reload + + assert_equal expected_time, topic.bonus_time + end + end + end +end diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb index 9eefc32745..f0a0e7f805 100644 --- a/activerecord/test/cases/unconnected_test.rb +++ b/activerecord/test/cases/unconnected_test.rb @@ -29,6 +29,14 @@ class TestUnconnectedAdapter < ActiveRecord::TestCase end end + def test_error_message_when_connection_not_established + error = assert_raise(ActiveRecord::ConnectionNotEstablished) do + TestRecord.find(1) + end + + assert_equal "No connection pool with 'primary' found.", error.message + end + def test_underlying_adapter_no_longer_active assert_not @underlying.active?, "Removed adapter should no longer be active" end diff --git a/activerecord/test/models/drink_designer.rb b/activerecord/test/models/drink_designer.rb index eb6701b84e..8258408f35 100644 --- a/activerecord/test/models/drink_designer.rb +++ b/activerecord/test/models/drink_designer.rb @@ -4,5 +4,11 @@ class DrinkDesigner < ActiveRecord::Base has_one :chef, as: :employable end +class DrinkDesignerWithPolymorphicDependentNullifyChef < ActiveRecord::Base + self.table_name = "drink_designers" + + has_one :chef, as: :employable, dependent: :nullify +end + class MocktailDesigner < DrinkDesigner end diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index 5cba1e440e..c3d15a571a 100644 --- a/activerecord/test/models/person.rb +++ b/activerecord/test/models/person.rb @@ -62,6 +62,11 @@ class PersonWithDependentNullifyJobs < ActiveRecord::Base has_many :jobs, source: :job, through: :references, dependent: :nullify end +class PersonWithPolymorphicDependentNullifyComments < ActiveRecord::Base + self.table_name = "people" + has_many :comments, as: :author, dependent: :nullify +end + class LoosePerson < ActiveRecord::Base self.table_name = "people" self.abstract_class = true diff --git a/activerecord/test/models/reference.rb b/activerecord/test/models/reference.rb index 2a7a1e3b77..82185040d6 100644 --- a/activerecord/test/models/reference.rb +++ b/activerecord/test/models/reference.rb @@ -4,6 +4,7 @@ class Reference < ActiveRecord::Base belongs_to :person belongs_to :job + has_many :ideal_jobs, class_name: "Job", foreign_key: :ideal_reference_id has_many :agents_posts_authors, through: :person class << self; attr_accessor :make_comments; end diff --git a/activerecord/test/models/subscription.rb b/activerecord/test/models/subscription.rb index d1d5d21621..f87315fcd1 100644 --- a/activerecord/test/models/subscription.rb +++ b/activerecord/test/models/subscription.rb @@ -3,4 +3,6 @@ class Subscription < ActiveRecord::Base belongs_to :subscriber, counter_cache: :books_count belongs_to :book + + validates_presence_of :subscriber_id, :book_id end diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 03430154db..a6a47687a2 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -12,17 +12,9 @@ class Topic < ActiveRecord::Base scope :scope_with_lambda, lambda { all } - scope :by_private_lifo, -> { where(author_name: private_lifo) } scope :by_lifo, -> { where(author_name: "lifo") } scope :replied, -> { where "replies_count > 0" } - class << self - private - def private_lifo - "lifo" - end - end - scope "approved_as_string", -> { where(approved: true) } scope :anonymous_extension, -> { } do def one diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index 61e9bc9af7..b143035213 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -27,6 +27,7 @@ ActiveRecord::Schema.define do create_table :binary_fields, force: true do |t| t.binary :var_binary, limit: 255 t.binary :var_binary_large, limit: 4095 + t.tinyblob :tiny_blob t.blob :normal_blob t.mediumblob :medium_blob @@ -36,6 +37,13 @@ ActiveRecord::Schema.define do t.mediumtext :medium_text t.longtext :long_text + t.binary :tiny_blob_2, size: :tiny + t.binary :medium_blob_2, size: :medium + t.binary :long_blob_2, size: :long + t.text :tiny_text_2, size: :tiny + t.text :medium_text_2, size: :medium + t.text :long_text_2, size: :long + t.index :var_binary end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 7034c773d2..86d5a67a13 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -682,11 +682,7 @@ ActiveRecord::Schema.define do create_table :pets, primary_key: :pet_id, force: true do |t| t.string :name t.integer :owner_id, :integer - if subsecond_precision_supported? - t.timestamps null: false, precision: 6 - else - t.timestamps null: false - end + t.timestamps end create_table :pets_treasures, force: true do |t| @@ -904,11 +900,7 @@ ActiveRecord::Schema.define do t.string :parent_title t.string :type t.string :group - if subsecond_precision_supported? - t.timestamps null: true, precision: 6 - else - t.timestamps null: true - end + t.timestamps null: true end create_table :toys, primary_key: :toy_id, force: true do |t| diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb index 2a4fa53460..367309dd85 100644 --- a/activerecord/test/support/connection.rb +++ b/activerecord/test/support/connection.rb @@ -21,6 +21,7 @@ module ARTest def self.connect puts "Using #{connection_name}" ActiveRecord::Base.logger = ActiveSupport::Logger.new("debug.log", 0, 100 * 1024 * 1024) + ActiveRecord::Base.connection_handlers = { ActiveRecord::Base.writing_role => ActiveRecord::Base.default_connection_handler } ActiveRecord::Base.configurations = connection_config ActiveRecord::Base.establish_connection :arunit ARUnit2Model.establish_connection :arunit2 diff --git a/activerecord/test/support/stubs/strong_parameters.rb b/activerecord/test/support/stubs/strong_parameters.rb new file mode 100644 index 0000000000..84f93a28b9 --- /dev/null +++ b/activerecord/test/support/stubs/strong_parameters.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class Parameters + def initialize(parameters = {}) + @parameters = parameters.with_indifferent_access + @permitted = false + end + + def permitted? + @permitted + end + + def permit! + @permitted = true + self + end + + def to_h + @parameters.to_h + end +end diff --git a/activestorage/CHANGELOG.md b/activestorage/CHANGELOG.md index 99f1ef9d86..0c7e0426ae 100644 --- a/activestorage/CHANGELOG.md +++ b/activestorage/CHANGELOG.md @@ -1,3 +1,37 @@ +## Rails 6.0.0.beta1 (January 18, 2019) ## + +* [Rename npm package](https://github.com/rails/rails/pull/34905) from + [`activestorage`](https://www.npmjs.com/package/activestorage) to + [`@rails/activestorage`](https://www.npmjs.com/package/@rails/activestorage). + + *Javan Makhmali* + +* Replace `config.active_storage.queue` with two options that indicate which + queues analysis and purge jobs should use, respectively: + + * `config.active_storage.queues.analysis` + * `config.active_storage.queues.purge` + + `config.active_storage.queue` is preferred over the new options when it's + set, but it is deprecated and will be removed in Rails 6.1. + + *George Claghorn* + +* Permit generating variants of TIFF images. + + *Luciano Sousa* + +* Use base36 (all lowercase) for all new Blob keys to prevent + collisions and undefined behavior with case-insensitive filesystems and + database indices. + + *Julik Tarkhanov* + +* It doesn’t include an `X-CSRF-Token` header if a meta tag is not found on + the page. It previously included one with a value of `undefined`. + + *Cameron Bothner* + * Fix `ArgumentError` when uploading to amazon s3 *Hiroki Sanpei* diff --git a/activestorage/MIT-LICENSE b/activestorage/MIT-LICENSE index eed89ac398..771376cc7e 100644 --- a/activestorage/MIT-LICENSE +++ b/activestorage/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2017-2018 David Heinemeier Hansson, Basecamp +Copyright (c) 2017-2019 David Heinemeier Hansson, Basecamp Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activestorage/README.md b/activestorage/README.md index bd31f0ea58..2886169ca7 100644 --- a/activestorage/README.md +++ b/activestorage/README.md @@ -101,7 +101,7 @@ Variation of image attachment: ```erb <%# Hitting the variant URL will lazy transform the original blob and then redirect to its new service location %> -<%= image_tag user.avatar.variant(resize_to_fit: [100, 100]) %> +<%= image_tag user.avatar.variant(resize_to_limit: [100, 100]) %> ``` ## Direct uploads @@ -118,8 +118,7 @@ Active Storage, with its included JavaScript library, supports uploading directl ``` Using the npm package: ```js - import * as ActiveStorage from "activestorage" - ActiveStorage.start() + require("@rails/activestorage").start() ``` 2. Annotate file inputs with the direct upload URL. diff --git a/activestorage/app/assets/javascripts/activestorage.js b/activestorage/app/assets/javascripts/activestorage.js index b71e251a11..e2bcb520b9 100644 --- a/activestorage/app/assets/javascripts/activestorage.js +++ b/activestorage/app/assets/javascripts/activestorage.js @@ -560,7 +560,10 @@ this.xhr.setRequestHeader("Content-Type", "application/json"); this.xhr.setRequestHeader("Accept", "application/json"); this.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); - this.xhr.setRequestHeader("X-CSRF-Token", getMetaValue("csrf-token")); + var csrfToken = getMetaValue("csrf-token"); + if (csrfToken != undefined) { + this.xhr.setRequestHeader("X-CSRF-Token", csrfToken); + } this.xhr.addEventListener("load", function(event) { return _this.requestDidLoad(event); }); diff --git a/activestorage/app/javascript/activestorage/blob_record.js b/activestorage/app/javascript/activestorage/blob_record.js index ff847892b2..7fbe315f76 100644 --- a/activestorage/app/javascript/activestorage/blob_record.js +++ b/activestorage/app/javascript/activestorage/blob_record.js @@ -17,7 +17,12 @@ export class BlobRecord { this.xhr.setRequestHeader("Content-Type", "application/json") this.xhr.setRequestHeader("Accept", "application/json") this.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest") - this.xhr.setRequestHeader("X-CSRF-Token", getMetaValue("csrf-token")) + + const csrfToken = getMetaValue("csrf-token") + if (csrfToken != undefined) { + this.xhr.setRequestHeader("X-CSRF-Token", csrfToken) + } + this.xhr.addEventListener("load", event => this.requestDidLoad(event)) this.xhr.addEventListener("error", event => this.requestDidError(event)) } diff --git a/activestorage/app/jobs/active_storage/analyze_job.rb b/activestorage/app/jobs/active_storage/analyze_job.rb index 804ee4557a..35d043d508 100644 --- a/activestorage/app/jobs/active_storage/analyze_job.rb +++ b/activestorage/app/jobs/active_storage/analyze_job.rb @@ -2,6 +2,8 @@ # Provides asynchronous analysis of ActiveStorage::Blob records via ActiveStorage::Blob#analyze_later. class ActiveStorage::AnalyzeJob < ActiveStorage::BaseJob + queue_as { ActiveStorage.queues[:analysis] } + retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :exponentially_longer def perform(blob) diff --git a/activestorage/app/jobs/active_storage/base_job.rb b/activestorage/app/jobs/active_storage/base_job.rb index 6caab42a2d..7bc2064dc5 100644 --- a/activestorage/app/jobs/active_storage/base_job.rb +++ b/activestorage/app/jobs/active_storage/base_job.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true class ActiveStorage::BaseJob < ActiveJob::Base - queue_as { ActiveStorage.queue } end diff --git a/activestorage/app/jobs/active_storage/purge_job.rb b/activestorage/app/jobs/active_storage/purge_job.rb index 2604977bf1..5ceb222005 100644 --- a/activestorage/app/jobs/active_storage/purge_job.rb +++ b/activestorage/app/jobs/active_storage/purge_job.rb @@ -2,6 +2,8 @@ # Provides asynchronous purging of ActiveStorage::Blob records via ActiveStorage::Blob#purge_later. class ActiveStorage::PurgeJob < ActiveStorage::BaseJob + queue_as { ActiveStorage.queues[:purge] } + discard_on ActiveRecord::RecordNotFound retry_on ActiveRecord::Deadlocked, attempts: 10, wait: :exponentially_longer diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb index 04f9dbff9f..6ca7d49bc1 100644 --- a/activestorage/app/models/active_storage/blob.rb +++ b/activestorage/app/models/active_storage/blob.rb @@ -79,6 +79,15 @@ class ActiveStorage::Blob < ActiveRecord::Base def create_before_direct_upload!(filename:, byte_size:, checksum:, content_type: nil, metadata: nil) create! filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata end + + # To prevent problems with case-insensitive filesystems, especially in combination + # with databases which treat indices as case-sensitive, all blob keys generated are going + # to only contain the base-36 character alphabet and will therefore be lowercase. To maintain + # the same or higher amount of entropy as in the base-58 encoding used by `has_secure_token` + # the number of bytes used is increased to 28 from the standard 24 + def generate_unique_secure_token + SecureRandom.base36(28) + end end # Returns a signed ID for this blob that's suitable for reference on the client-side without fear of tampering. @@ -87,9 +96,10 @@ class ActiveStorage::Blob < ActiveRecord::Base ActiveStorage.verifier.generate(id, purpose: :blob_id) end - # Returns the key pointing to the file on the service that's associated with this blob. The key is in the - # standard secure-token format from Rails. So it'll look like: XTAPjJCJiuDrLk3TmwyJGpUo. This key is not intended - # to be revealed directly to the user. Always refer to blobs using the signed_id or a verified form of the key. + # Returns the key pointing to the file on the service that's associated with this blob. The key is the + # secure-token format from Rails in lower case. So it'll look like: xtapjjcjiudrlk3tmwyjgpuobabd. + # This key is not intended to be revealed directly to the user. + # Always refer to blobs using the signed_id or a verified form of the key. def key # We can't wait until the record is first saved to have a key for it self[:key] ||= self.class.generate_unique_secure_token diff --git a/activestorage/app/models/active_storage/blob/representable.rb b/activestorage/app/models/active_storage/blob/representable.rb index 03d5511481..32e8fcefdf 100644 --- a/activestorage/app/models/active_storage/blob/representable.rb +++ b/activestorage/app/models/active_storage/blob/representable.rb @@ -10,7 +10,7 @@ module ActiveStorage::Blob::Representable # Returns an ActiveStorage::Variant instance with the set of +transformations+ provided. This is only relevant for image # files, and it allows any image to be transformed for size, colors, and the like. Example: # - # avatar.variant(resize_to_fit: [100, 100]).processed.service_url + # avatar.variant(resize_to_limit: [100, 100]).processed.service_url # # This will create and process a variant of the avatar blob that's constrained to a height and width of 100px. # Then it'll upload said variant to the service according to a derivative key of the blob and the transformations. @@ -18,7 +18,7 @@ module ActiveStorage::Blob::Representable # Frequently, though, you don't actually want to transform the variant right away. But rather simply refer to a # specific variant that can be created by a controller on-demand. Like so: # - # <%= image_tag Current.user.avatar.variant(resize_to_fit: [100, 100]) %> + # <%= image_tag Current.user.avatar.variant(resize_to_limit: [100, 100]) %> # # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController # can then produce on-demand. @@ -43,13 +43,13 @@ module ActiveStorage::Blob::Representable # from a non-image blob. Active Storage comes with built-in previewers for videos and PDF documents. The video previewer # extracts the first frame from a video and the PDF previewer extracts the first page from a PDF document. # - # blob.preview(resize_to_fit: [100, 100]).processed.service_url + # blob.preview(resize_to_limit: [100, 100]).processed.service_url # # Avoid processing previews synchronously in views. Instead, link to a controller action that processes them on demand. # Active Storage provides one, but you may want to create your own (for example, if you need authentication). Here’s # how to use the built-in version: # - # <%= image_tag video.preview(resize_to_fit: [100, 100]) %> + # <%= image_tag video.preview(resize_to_limit: [100, 100]) %> # # This method raises ActiveStorage::UnpreviewableError if no previewer accepts the receiving blob. To determine # whether a blob is accepted by any previewer, call ActiveStorage::Blob#previewable?. @@ -69,7 +69,7 @@ module ActiveStorage::Blob::Representable # Returns an ActiveStorage::Preview for a previewable blob or an ActiveStorage::Variant for a variable image blob. # - # blob.representation(resize_to_fit: [100, 100]).processed.service_url + # blob.representation(resize_to_limit: [100, 100]).processed.service_url # # Raises ActiveStorage::UnrepresentableError if the receiving blob is neither variable nor previewable. Call # ActiveStorage::Blob#representable? to determine whether a blob is representable. diff --git a/activestorage/app/models/active_storage/preview.rb b/activestorage/app/models/active_storage/preview.rb index dd50494799..bb9d960443 100644 --- a/activestorage/app/models/active_storage/preview.rb +++ b/activestorage/app/models/active_storage/preview.rb @@ -38,7 +38,7 @@ class ActiveStorage::Preview # Processes the preview if it has not been processed yet. Returns the receiving Preview instance for convenience: # - # blob.preview(resize_to_fit: [100, 100]).processed.service_url + # blob.preview(resize_to_limit: [100, 100]).processed.service_url # # Processing a preview generates an image from its blob and attaches the preview image to the blob. Because the preview # image is stored with the blob, it is only generated once. diff --git a/activestorage/app/models/active_storage/variant.rb b/activestorage/app/models/active_storage/variant.rb index ea57fa5f78..bc0058967a 100644 --- a/activestorage/app/models/active_storage/variant.rb +++ b/activestorage/app/models/active_storage/variant.rb @@ -27,7 +27,7 @@ require "ostruct" # To refer to such a delayed on-demand variant, simply link to the variant through the resolved route provided # by Active Storage like so: # -# <%= image_tag Current.user.avatar.variant(resize_to_fit: [100, 100]) %> +# <%= image_tag Current.user.avatar.variant(resize_to_limit: [100, 100]) %> # # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController # can then produce on-demand. @@ -36,15 +36,15 @@ require "ostruct" # has already been processed and uploaded to the service, and, if so, just return that. Otherwise it will perform # the transformations, upload the variant to the service, and return itself again. Example: # -# avatar.variant(resize_to_fit: [100, 100]).processed.service_url +# avatar.variant(resize_to_limit: [100, 100]).processed.service_url # # This will create and process a variant of the avatar blob that's constrained to a height and width of 100. # Then it'll upload said variant to the service according to a derivative key of the blob and the transformations. # # You can combine any number of ImageMagick/libvips operations into a variant, as well as any macros provided by the -# ImageProcessing gem (such as +resize_to_fit+): +# ImageProcessing gem (such as +resize_to_limit+): # -# avatar.variant(resize_to_fit: [800, 800], monochrome: true, rotate: "-90") +# avatar.variant(resize_to_limit: [800, 800], monochrome: true, rotate: "-90") # # Visit the following links for a list of available ImageProcessing commands and ImageMagick/libvips operations: # diff --git a/activestorage/app/models/active_storage/variation.rb b/activestorage/app/models/active_storage/variation.rb index 3adc2407e5..67568772da 100644 --- a/activestorage/app/models/active_storage/variation.rb +++ b/activestorage/app/models/active_storage/variation.rb @@ -6,7 +6,7 @@ # In case you do need to use this directly, it's instantiated using a hash of transformations where # the key is the command and the value is the arguments. Example: # -# ActiveStorage::Variation.new(resize_to_fit: [100, 100], monochrome: true, trim: true, rotate: "-90") +# ActiveStorage::Variation.new(resize_to_limit: [100, 100], monochrome: true, trim: true, rotate: "-90") # # The options map directly to {ImageProcessing}[https://github.com/janko-m/image_processing] commands. class ActiveStorage::Variation diff --git a/activestorage/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb b/activestorage/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb new file mode 100644 index 0000000000..6830203cd6 --- /dev/null +++ b/activestorage/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb @@ -0,0 +1,7 @@ +class AddForeignKeyConstraintToActiveStorageAttachmentsForBlobId < ActiveRecord::Migration[6.0] + def up + unless foreign_key_exists?(:active_storage_attachments, column: :blob_id) + add_foreign_key :active_storage_attachments, :active_storage_blobs, column: :blob_id + end + end +end diff --git a/activestorage/lib/active_storage.rb b/activestorage/lib/active_storage.rb index 19c7f6be2a..e542c4b2ca 100644 --- a/activestorage/lib/active_storage.rb +++ b/activestorage/lib/active_storage.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2017-2018 David Heinemeier Hansson, Basecamp +# Copyright (c) 2017-2019 David Heinemeier Hansson, Basecamp # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -42,7 +42,7 @@ module ActiveStorage mattr_accessor :logger mattr_accessor :verifier - mattr_accessor :queue + mattr_accessor :queues, default: {} mattr_accessor :previewers, default: [] mattr_accessor :analyzers, default: [] mattr_accessor :variant_processor, default: :mini_magick diff --git a/activestorage/lib/active_storage/analyzer/video_analyzer.rb b/activestorage/lib/active_storage/analyzer/video_analyzer.rb index 18d8ff8237..e56c97f4f5 100644 --- a/activestorage/lib/active_storage/analyzer/video_analyzer.rb +++ b/activestorage/lib/active_storage/analyzer/video_analyzer.rb @@ -11,7 +11,7 @@ module ActiveStorage # # Example: # - # ActiveStorage::VideoAnalyzer.new(blob).metadata + # ActiveStorage::Analyzer::VideoAnalyzer.new(blob).metadata # # => { width: 640.0, height: 480.0, duration: 5.0, angle: 0, display_aspect_ratio: [4, 3] } # # When a video's angle is 90 or 270 degrees, its width and height are automatically swapped for convenience. diff --git a/activestorage/lib/active_storage/engine.rb b/activestorage/lib/active_storage/engine.rb index b3bcf48d6f..384e6ebfa6 100644 --- a/activestorage/lib/active_storage/engine.rb +++ b/activestorage/lib/active_storage/engine.rb @@ -20,6 +20,7 @@ module ActiveStorage config.active_storage.previewers = [ ActiveStorage::Previewer::PopplerPDFPreviewer, ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ] config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer ] config.active_storage.paths = ActiveSupport::OrderedOptions.new + config.active_storage.queues = ActiveSupport::OrderedOptions.new config.active_storage.variable_content_types = %w( image/png @@ -27,6 +28,7 @@ module ActiveStorage image/jpg image/jpeg image/pjpeg + image/tiff image/vnd.adobe.photoshop image/vnd.microsoft.icon ) @@ -49,6 +51,7 @@ module ActiveStorage image/gif image/jpg image/jpeg + image/tiff image/vnd.adobe.photoshop image/vnd.microsoft.icon application/pdf @@ -59,7 +62,6 @@ module ActiveStorage initializer "active_storage.configs" do config.after_initialize do |app| ActiveStorage.logger = app.config.active_storage.logger || Rails.logger - ActiveStorage.queue = app.config.active_storage.queue ActiveStorage.variant_processor = app.config.active_storage.variant_processor || :mini_magick ActiveStorage.previewers = app.config.active_storage.previewers || [] ActiveStorage.analyzers = app.config.active_storage.analyzers || [] @@ -115,6 +117,20 @@ module ActiveStorage end end + initializer "active_storage.queues" do + config.after_initialize do |app| + if queue = app.config.active_storage.queue + ActiveSupport::Deprecation.warn \ + "config.active_storage.queue is deprecated and will be removed in Rails 6.1. " \ + "Set config.active_storage.queues.purge and config.active_storage.queues.analysis instead." + + ActiveStorage.queues = { purge: queue, analysis: queue } + else + ActiveStorage.queues = app.config.active_storage.queues || {} + end + end + end + initializer "active_storage.reflection" do ActiveSupport.on_load(:active_record) do include Reflection::ActiveRecordExtensions diff --git a/activestorage/lib/active_storage/gem_version.rb b/activestorage/lib/active_storage/gem_version.rb index 492620731b..c4c7bf5677 100644 --- a/activestorage/lib/active_storage/gem_version.rb +++ b/activestorage/lib/active_storage/gem_version.rb @@ -10,7 +10,7 @@ module ActiveStorage MAJOR = 6 MINOR = 0 TINY = 0 - PRE = "alpha" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb b/activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb index 69eb617d7b..6bf501a607 100644 --- a/activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb +++ b/activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb @@ -28,7 +28,7 @@ module ActiveStorage private def draw_first_page_from(file, &block) - # use 72 dpi to match thumbnail dimesions of the PDF + # use 72 dpi to match thumbnail dimensions of the PDF draw self.class.pdftoppm_path, "-singlefile", "-r", "72", "-png", file.path, &block end end diff --git a/activestorage/lib/active_storage/service/azure_storage_service.rb b/activestorage/lib/active_storage/service/azure_storage_service.rb index 17cecd891c..993cc0e5f7 100644 --- a/activestorage/lib/active_storage/service/azure_storage_service.rb +++ b/activestorage/lib/active_storage/service/azure_storage_service.rb @@ -10,8 +10,8 @@ module ActiveStorage class Service::AzureStorageService < Service attr_reader :client, :blobs, :container, :signer - def initialize(storage_account_name:, storage_access_key:, container:) - @client = Azure::Storage::Client.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key) + def initialize(storage_account_name:, storage_access_key:, container:, **options) + @client = Azure::Storage::Client.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key, **options) @signer = Azure::Storage::Core::Auth::SharedAccessSignature.new(storage_account_name, storage_access_key) @blobs = client.blob_client @container = container diff --git a/activestorage/lib/active_storage/service/s3_service.rb b/activestorage/lib/active_storage/service/s3_service.rb index 382920ef61..bf94f3f49e 100644 --- a/activestorage/lib/active_storage/service/s3_service.rb +++ b/activestorage/lib/active_storage/service/s3_service.rb @@ -16,9 +16,9 @@ module ActiveStorage @upload_options = upload end - def upload(key, io, checksum: nil, **) + def upload(key, io, checksum: nil, content_type: nil, **) instrument :upload, key: key, checksum: checksum do - object_for(key).put(upload_options.merge(body: io, content_md5: checksum)) + object_for(key).put(upload_options.merge(body: io, content_md5: checksum, content_type: content_type)) rescue Aws::S3::Errors::BadDigest raise ActiveStorage::IntegrityError end diff --git a/activestorage/lib/tasks/activestorage.rake b/activestorage/lib/tasks/activestorage.rake index ac254d717f..6b0469636c 100644 --- a/activestorage/lib/tasks/activestorage.rake +++ b/activestorage/lib/tasks/activestorage.rake @@ -12,4 +12,11 @@ namespace :active_storage do Rake::Task["app:active_storage:install:migrations"].invoke end end + + # desc "Copy over the migrations needed to the application upgrading" + task update: :environment do + ENV["MIGRATIONS_PATH"] = "db/update_migrate" + + Rake::Task["active_storage:install"].invoke + end end diff --git a/activestorage/package.json b/activestorage/package.json index 00876985cf..f7b896b5ee 100644 --- a/activestorage/package.json +++ b/activestorage/package.json @@ -1,6 +1,6 @@ { - "name": "activestorage", - "version": "6.0.0-alpha", + "name": "@rails/activestorage", + "version": "6.0.0-beta1", "description": "Attach cloud and local files in Rails applications", "main": "app/assets/javascripts/activestorage.js", "files": [ diff --git a/activestorage/test/controllers/blobs_controller_test.rb b/activestorage/test/controllers/blobs_controller_test.rb index 9c811df895..9bf2641de6 100644 --- a/activestorage/test/controllers/blobs_controller_test.rb +++ b/activestorage/test/controllers/blobs_controller_test.rb @@ -20,3 +20,28 @@ class ActiveStorage::BlobsControllerTest < ActionDispatch::IntegrationTest assert_equal "max-age=300, private", @response.headers["Cache-Control"] end end + +if SERVICE_CONFIGURATIONS[:s3] && SERVICE_CONFIGURATIONS[:s3][:access_key_id].present? + class ActiveStorage::S3BlobsControllerTest < ActionDispatch::IntegrationTest + setup do + @old_service = ActiveStorage::Blob.service + ActiveStorage::Blob.service = ActiveStorage::Service.configure(:s3, SERVICE_CONFIGURATIONS) + end + + teardown do + ActiveStorage::Blob.service = @old_service + end + + test "allow redirection to the different host" do + blob = create_file_blob filename: "racecar.jpg" + + assert_nothing_raised { get rails_blob_url(blob) } + assert_response :redirect + assert_no_match @request.host, @response.headers["Location"] + ensure + blob.purge + end + end +else + puts "Skipping S3 redirection tests because no S3 configuration was supplied" +end diff --git a/activestorage/test/controllers/representations_controller_test.rb b/activestorage/test/controllers/representations_controller_test.rb index 2662cc5283..4ae0ff877e 100644 --- a/activestorage/test/controllers/representations_controller_test.rb +++ b/activestorage/test/controllers/representations_controller_test.rb @@ -59,3 +59,33 @@ class ActiveStorage::RepresentationsControllerWithPreviewsTest < ActionDispatch: assert_response :not_found end end + +if SERVICE_CONFIGURATIONS[:s3] && SERVICE_CONFIGURATIONS[:s3][:access_key_id].present? + class ActiveStorage::S3RepresentationsControllerWithVariantsTest < ActionDispatch::IntegrationTest + setup do + @old_service = ActiveStorage::Blob.service + ActiveStorage::Blob.service = ActiveStorage::Service.configure(:s3, SERVICE_CONFIGURATIONS) + end + + teardown do + ActiveStorage::Blob.service = @old_service + end + + test "allow redirection to the different host" do + blob = create_file_blob filename: "racecar.jpg" + + assert_nothing_raised do + get rails_blob_representation_url( + filename: blob.filename, + signed_blob_id: blob.signed_id, + variation_key: ActiveStorage::Variation.encode(resize: "100x100")) + end + assert_response :redirect + assert_no_match @request.host, @response.headers["Location"] + ensure + blob.purge + end + end +else + puts "Skipping S3 redirection tests because no S3 configuration was supplied" +end diff --git a/activestorage/test/dummy/config/application.rb b/activestorage/test/dummy/config/application.rb index bd14ac0b1a..151c8ade4b 100644 --- a/activestorage/test/dummy/config/application.rb +++ b/activestorage/test/dummy/config/application.rb @@ -15,7 +15,7 @@ Bundler.require(*Rails.groups) module Dummy class Application < Rails::Application - config.load_defaults 5.2 + config.load_defaults 6.0 config.active_storage.service = :local end diff --git a/activestorage/test/dummy/config/environments/development.rb b/activestorage/test/dummy/config/environments/development.rb index 47fc5bf25c..4b80d291ca 100644 --- a/activestorage/test/dummy/config/environments/development.rb +++ b/activestorage/test/dummy/config/environments/development.rb @@ -17,6 +17,7 @@ Rails.application.configure do # Enable/disable caching. By default caching is disabled. if Rails.root.join("tmp/caching-dev.txt").exist? config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true config.cache_store = :memory_store config.public_file_server.headers = { diff --git a/activestorage/test/fixtures/files/racecar.tif b/activestorage/test/fixtures/files/racecar.tif Binary files differnew file mode 100644 index 0000000000..0a11b22896 --- /dev/null +++ b/activestorage/test/fixtures/files/racecar.tif diff --git a/activestorage/test/models/blob_test.rb b/activestorage/test/models/blob_test.rb index 1503f5fc50..54cf9e2b8a 100644 --- a/activestorage/test/models/blob_test.rb +++ b/activestorage/test/models/blob_test.rb @@ -47,6 +47,10 @@ class ActiveStorage::BlobTest < ActiveSupport::TestCase assert_equal "text/csv", blob.content_type end + test "create after upload generates a 28-character base36 key" do + assert_match(/^[a-z0-9]{28}$/, create_blob.key) + end + test "image?" do blob = create_file_blob filename: "racecar.jpg" assert_predicate blob, :image? diff --git a/activestorage/test/models/variant_test.rb b/activestorage/test/models/variant_test.rb index 4f88440e54..d98935eb9f 100644 --- a/activestorage/test/models/variant_test.rb +++ b/activestorage/test/models/variant_test.rb @@ -133,6 +133,17 @@ class ActiveStorage::VariantTest < ActiveSupport::TestCase assert_equal 20, image.height end + test "resized variation of TIFF blob" do + blob = create_file_blob(filename: "racecar.tif") + variant = blob.variant(resize: "50x50").processed + assert_match(/racecar\.png/, variant.service_url) + + image = read_image(variant) + assert_equal "PNG", image.type + assert_equal 50, image.width + assert_equal 33, image.height + end + test "optimized variation of GIF blob" do blob = create_file_blob(filename: "image.gif", content_type: "image/gif") @@ -150,7 +161,7 @@ class ActiveStorage::VariantTest < ActiveSupport::TestCase test "service_url doesn't grow in length despite long variant options" do blob = create_file_blob(filename: "racecar.jpg") variant = blob.variant(font: "a" * 10_000).processed - assert_operator variant.service_url.length, :<, 726 + assert_operator variant.service_url.length, :<, 730 end test "works for vips processor" do diff --git a/activestorage/test/service/s3_service_test.rb b/activestorage/test/service/s3_service_test.rb index 0a6004267f..74c0aa0405 100644 --- a/activestorage/test/service/s3_service_test.rb +++ b/activestorage/test/service/s3_service_test.rb @@ -59,6 +59,24 @@ if SERVICE_CONFIGURATIONS[:s3] service.delete key end end + + test "upload with content type" do + key = SecureRandom.base58(24) + data = "Something else entirely!" + content_type = "text/plain" + + @service.upload( + key, + StringIO.new(data), + checksum: Digest::MD5.base64digest(data), + filename: "cool_data.txt", + content_type: content_type + ) + + assert_equal content_type, @service.bucket.object(key).content_type + ensure + @service.delete key + end end else puts "Skipping S3 Service tests because no S3 configuration was supplied" diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index e678f48244..2da774ca66 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,37 @@ +* Add `before_reset` callback to `CurrentAttributes` and define `after_reset` as an alias of `resets` for symmetry. + + *Rosa Gutierrez* + +* Remove the `` Kernel#` `` override that suppresses ENOENT and accidentally returns nil on Unix systems. + + *Akinori Musha* + +* Add `ActiveSupport::HashWithIndifferentAccess#assoc`. + + `assoc` can now be called with either a string or a symbol. + + *Stefan Schüßler* + +## Rails 6.0.0.beta1 (January 18, 2019) ## + +* Remove deprecated `Module#reachable?` method. + + *Rafael Mendonça França* + +* Remove deprecated `#acronym_regex` method from `Inflections`. + + *Rafael Mendonça França* + +* Fix `String#safe_constantize` throwing a `LoadError` for incorrectly cased constant references. + + *Keenan Brock* + +* Preserve key order passed to `ActiveSupport::CacheStore#fetch_multi`. + + `fetch_multi(*names)` now returns its results in the same order as the `*names` requested, rather than returning cache hits followed by cache misses. + + *Gannon McGibbon* + * If the same block is `included` multiple times for a Concern, an exception is no longer raised. *Mark J. Titorenko*, *Vlad Bokov* diff --git a/activesupport/MIT-LICENSE b/activesupport/MIT-LICENSE index 8f769c0767..315a4bc7c4 100644 --- a/activesupport/MIT-LICENSE +++ b/activesupport/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2005-2018 David Heinemeier Hansson +Copyright (c) 2005-2019 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index a4fb697669..5589c71281 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2005-2018 David Heinemeier Hansson +# Copyright (c) 2005-2019 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index e8518645d9..30a69c550b 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -438,18 +438,18 @@ module ActiveSupport options = merged_options(options) instrument :read_multi, names, options do |payload| - read_multi_entries(names, options).tap do |results| - payload[:hits] = results.keys - payload[:super_operation] = :fetch_multi + reads = read_multi_entries(names, options) + writes = {} + ordered = names.each_with_object({}) do |name, hash| + hash[name] = reads.fetch(name) { writes[name] = yield(name) } + end - writes = {} + payload[:hits] = reads.keys + payload[:super_operation] = :fetch_multi - (names - results.keys).each do |name| - results[name] = writes[name] = yield(name) - end + write_multi(writes, options) - write_multi writes, options - end + ordered end end diff --git a/activesupport/lib/active_support/core_ext/date/calculations.rb b/activesupport/lib/active_support/core_ext/date/calculations.rb index 1cd7acb05d..d03a8d3997 100644 --- a/activesupport/lib/active_support/core_ext/date/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date/calculations.rb @@ -110,12 +110,13 @@ class Date # Provides precise Date calculations for years, months, and days. The +options+ parameter takes a hash with # any of these keys: <tt>:years</tt>, <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>. def advance(options) - options = options.dup d = self - d = d >> options.delete(:years) * 12 if options[:years] - d = d >> options.delete(:months) if options[:months] - d = d + options.delete(:weeks) * 7 if options[:weeks] - d = d + options.delete(:days) if options[:days] + + d = d >> options[:years] * 12 if options[:years] + d = d >> options[:months] if options[:months] + d = d + options[:weeks] * 7 if options[:weeks] + d = d + options[:days] if options[:days] + d end diff --git a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb index 05abd83221..e2e11545e2 100644 --- a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb @@ -134,7 +134,7 @@ module DateAndTime # now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000 # now.beginning_of_quarter # => Wed, 01 Jul 2015 00:00:00 +0000 def beginning_of_quarter - first_quarter_month = [10, 7, 4, 1].detect { |m| m <= month } + first_quarter_month = month - (2 + month) % 3 beginning_of_month.change(month: first_quarter_month) end alias :at_beginning_of_quarter :beginning_of_quarter @@ -149,7 +149,7 @@ module DateAndTime # now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000 # now.end_of_quarter # => Wed, 30 Sep 2015 23:59:59 +0000 def end_of_quarter - last_quarter_month = [3, 6, 9, 12].detect { |m| m >= month } + last_quarter_month = month + (12 - month) % 3 beginning_of_month.change(month: last_quarter_month).end_of_month end alias :at_end_of_quarter :end_of_quarter diff --git a/activesupport/lib/active_support/core_ext/kernel.rb b/activesupport/lib/active_support/core_ext/kernel.rb index 0f4356fbdd..7708069301 100644 --- a/activesupport/lib/active_support/core_ext/kernel.rb +++ b/activesupport/lib/active_support/core_ext/kernel.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require "active_support/core_ext/kernel/agnostics" require "active_support/core_ext/kernel/concern" require "active_support/core_ext/kernel/reporting" require "active_support/core_ext/kernel/singleton_class" diff --git a/activesupport/lib/active_support/core_ext/kernel/agnostics.rb b/activesupport/lib/active_support/core_ext/kernel/agnostics.rb deleted file mode 100644 index 403b5f31f0..0000000000 --- a/activesupport/lib/active_support/core_ext/kernel/agnostics.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -class Object - # Makes backticks behave (somewhat more) similarly on all platforms. - # On win32 `nonexistent_command` raises Errno::ENOENT; on Unix, the - # spawned shell prints a message to stderr and sets $?. We emulate - # Unix on the former but not the latter. - def `(command) #:nodoc: - super - rescue Errno::ENOENT => e - STDERR.puts "#$0: #{e}" - end -end diff --git a/activesupport/lib/active_support/core_ext/module.rb b/activesupport/lib/active_support/core_ext/module.rb index d91e3fba6a..542af98c04 100644 --- a/activesupport/lib/active_support/core_ext/module.rb +++ b/activesupport/lib/active_support/core_ext/module.rb @@ -3,7 +3,6 @@ require "active_support/core_ext/module/aliasing" require "active_support/core_ext/module/introspection" require "active_support/core_ext/module/anonymous" -require "active_support/core_ext/module/reachable" require "active_support/core_ext/module/attribute_accessors" require "active_support/core_ext/module/attribute_accessors_per_thread" require "active_support/core_ext/module/attr_internal" diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index be90390ae4..5652f2d1cc 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -243,7 +243,7 @@ class Module # end # # def person - # @event.detail.person || @event.creator + # detail.person || creator # end # # private @@ -266,7 +266,7 @@ class Module # end # # def person - # @event.detail.person || @event.creator + # detail.person || creator # end # end # diff --git a/activesupport/lib/active_support/core_ext/module/reachable.rb b/activesupport/lib/active_support/core_ext/module/reachable.rb index e9cbda5245..2020f5204c 100644 --- a/activesupport/lib/active_support/core_ext/module/reachable.rb +++ b/activesupport/lib/active_support/core_ext/module/reachable.rb @@ -3,9 +3,4 @@ require "active_support/core_ext/module/anonymous" require "active_support/core_ext/string/inflections" -class Module - def reachable? #:nodoc: - !anonymous? && name.safe_constantize.equal?(self) - end - deprecate :reachable? -end +ActiveSupport::Deprecation.warn("reachable is deprecated and will be removed from the framework.") diff --git a/activesupport/lib/active_support/core_ext/securerandom.rb b/activesupport/lib/active_support/core_ext/securerandom.rb index b4a491f5fd..ef812f7e1a 100644 --- a/activesupport/lib/active_support/core_ext/securerandom.rb +++ b/activesupport/lib/active_support/core_ext/securerandom.rb @@ -4,17 +4,18 @@ require "securerandom" module SecureRandom BASE58_ALPHABET = ("0".."9").to_a + ("A".."Z").to_a + ("a".."z").to_a - ["0", "O", "I", "l"] + BASE36_ALPHABET = ("0".."9").to_a + ("a".."z").to_a + # SecureRandom.base58 generates a random base58 string. # - # The argument _n_ specifies the length, of the random string to be generated. + # The argument _n_ specifies the length of the random string to be generated. # # If _n_ is not specified or is +nil+, 16 is assumed. It may be larger in the future. # - # The result may contain alphanumeric characters except 0, O, I and l + # The result may contain alphanumeric characters except 0, O, I and l. # # p SecureRandom.base58 # => "4kUgL2pdQMSCQtjE" # p SecureRandom.base58(24) # => "77TMHrHJFvFDwodq8w7Ev2m7" - # def self.base58(n = 16) SecureRandom.random_bytes(n).unpack("C*").map do |byte| idx = byte % 64 @@ -22,4 +23,23 @@ module SecureRandom BASE58_ALPHABET[idx] end.join end + + # SecureRandom.base36 generates a random base36 string in lowercase. + # + # The argument _n_ specifies the length of the random string to be generated. + # + # If _n_ is not specified or is +nil+, 16 is assumed. It may be larger in the future. + # This method can be used over +base58+ if a deterministic case key is necessary. + # + # The result will contain alphanumeric characters in lowercase. + # + # p SecureRandom.base36 # => "4kugl2pdqmscqtje" + # p SecureRandom.base36(24) # => "77tmhrhjfvfdwodq8w7ev2m7" + def self.base36(n = 16) + SecureRandom.random_bytes(n).unpack("C*").map do |byte| + idx = byte % 64 + idx = SecureRandom.random_number(36) if idx >= 36 + BASE36_ALPHABET[idx] + end.join + end end diff --git a/activesupport/lib/active_support/current_attributes.rb b/activesupport/lib/active_support/current_attributes.rb index 3145ff87a1..67ebe102d7 100644 --- a/activesupport/lib/active_support/current_attributes.rb +++ b/activesupport/lib/active_support/current_attributes.rb @@ -119,10 +119,16 @@ module ActiveSupport end end + # Calls this block before #reset is called on the instance. Used for resetting external collaborators that depend on current values. + def before_reset(&block) + set_callback :reset, :before, &block + end + # Calls this block after #reset is called on the instance. Used for resetting external collaborators, like Time.zone. def resets(&block) set_callback :reset, :after, &block end + alias_method :after_reset, :resets delegate :set, :reset, to: :instance diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index 314c926ac0..97b4634d7b 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -214,8 +214,11 @@ module ActiveSupport end def coerce(other) #:nodoc: - if Scalar === other + case other + when Scalar [other, self] + when Duration + [Scalar.new(other.value), self] else [Scalar.new(other), self] end diff --git a/activesupport/lib/active_support/gem_version.rb b/activesupport/lib/active_support/gem_version.rb index c951ad16a3..8bf016e88c 100644 --- a/activesupport/lib/active_support/gem_version.rb +++ b/activesupport/lib/active_support/gem_version.rb @@ -10,7 +10,7 @@ module ActiveSupport MAJOR = 6 MINOR = 0 TINY = 0 - PRE = "alpha" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index f1af76019a..3a2b2652c4 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -164,6 +164,19 @@ module ActiveSupport super(convert_key(key)) end + # Same as <tt>Hash#assoc</tt> where the key passed as argument can be + # either a string or a symbol: + # + # counters = ActiveSupport::HashWithIndifferentAccess.new + # counters[:foo] = 1 + # + # counters.assoc('foo') # => ["foo", 1] + # counters.assoc(:foo) # => ["foo", 1] + # counters.assoc(:zoo) # => nil + def assoc(key) + super(convert_key(key)) + end + # Same as <tt>Hash#fetch</tt> where the key passed as argument can be # either a string or a symbol: # diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb index fa087c4dd6..88cdd99dbd 100644 --- a/activesupport/lib/active_support/inflector/inflections.rb +++ b/activesupport/lib/active_support/inflector/inflections.rb @@ -65,8 +65,7 @@ module ActiveSupport @__instance__[locale] ||= new end - attr_reader :plurals, :singulars, :uncountables, :humans, :acronyms, :acronym_regex - deprecate :acronym_regex + attr_reader :plurals, :singulars, :uncountables, :humans, :acronyms attr_reader :acronyms_camelize_regex, :acronyms_underscore_regex # :nodoc: diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index 1af9833d46..ee193add6f 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -328,6 +328,8 @@ module ActiveSupport e.name.to_s == camel_cased_word.to_s) rescue ArgumentError => e raise unless /not missing constant #{const_regexp(camel_cased_word)}!$/.match?(e.message) + rescue LoadError => e + raise unless /Unable to autoload constant #{const_regexp(camel_cased_word)}/.match?(e.message) end # Returns the suffix that should be added to a number to denote the position diff --git a/activesupport/lib/active_support/key_generator.rb b/activesupport/lib/active_support/key_generator.rb index 00edcdd05a..8b61982883 100644 --- a/activesupport/lib/active_support/key_generator.rb +++ b/activesupport/lib/active_support/key_generator.rb @@ -38,36 +38,4 @@ module ActiveSupport @cache_keys[args.join] ||= @key_generator.generate_key(*args) end end - - class LegacyKeyGenerator # :nodoc: - SECRET_MIN_LENGTH = 30 # Characters - - def initialize(secret) - ensure_secret_secure(secret) - @secret = secret - end - - def generate_key(salt) - @secret - end - - private - - # To prevent users from using something insecure like "Password" we make sure that the - # secret they've provided is at least 30 characters in length. - def ensure_secret_secure(secret) - if secret.blank? - raise ArgumentError, "A secret is required to generate an integrity hash " \ - "for cookie session data. Set a secret_key_base of at least " \ - "#{SECRET_MIN_LENGTH} characters by running `rails credentials:edit`." - end - - if secret.length < SECRET_MIN_LENGTH - raise ArgumentError, "Secret should be something secure, " \ - "like \"#{SecureRandom.hex(16)}\". The value you " \ - "provided, \"#{secret}\", is shorter than the minimum length " \ - "of #{SECRET_MIN_LENGTH} characters." - end - end - end end diff --git a/activesupport/lib/active_support/log_subscriber.rb b/activesupport/lib/active_support/log_subscriber.rb index 0f7be06c8e..938cfdb914 100644 --- a/activesupport/lib/active_support/log_subscriber.rb +++ b/activesupport/lib/active_support/log_subscriber.rb @@ -5,8 +5,8 @@ require "active_support/core_ext/class/attribute" require "active_support/subscriber" module ActiveSupport - # ActiveSupport::LogSubscriber is an object set to consume - # ActiveSupport::Notifications with the sole purpose of logging them. + # <tt>ActiveSupport::LogSubscriber</tt> is an object set to consume + # <tt>ActiveSupport::Notifications</tt> with the sole purpose of logging them. # The log subscriber dispatches notifications to a registered object based # on its given namespace. # @@ -16,7 +16,7 @@ module ActiveSupport # module ActiveRecord # class LogSubscriber < ActiveSupport::LogSubscriber # def sql(event) - # "#{event.payload[:name]} (#{event.duration}) #{event.payload[:sql]}" + # info "#{event.payload[:name]} (#{event.duration}) #{event.payload[:sql]}" # end # end # end @@ -29,13 +29,36 @@ module ActiveSupport # subscriber, the line above should be called after your # <tt>ActiveRecord::LogSubscriber</tt> definition. # - # After configured, whenever a "sql.active_record" notification is published, - # it will properly dispatch the event (ActiveSupport::Notifications::Event) to - # the sql method. + # After configured, whenever a <tt>"sql.active_record"</tt> notification is published, + # it will properly dispatch the event + # (<tt>ActiveSupport::Notifications::Event</tt>) to the sql method. + # + # Being an <tt>ActiveSupport::Notifications</tt> consumer, + # <tt>ActiveSupport::LogSubscriber</tt> exposes a simple interface to check if + # instrumented code raises an exception. It is common to log a different + # message in case of an error, and this can be achieved by extending + # the previous example: + # + # module ActiveRecord + # class LogSubscriber < ActiveSupport::LogSubscriber + # def sql(event) + # exception = event.payload[:exception] + # + # if exception + # exception_object = event.payload[:exception_object] + # + # error "[ERROR] #{event.payload[:name]}: #{exception.join(', ')} " \ + # "(#{exception_object.backtrace.first})" + # else + # # standard logger code + # end + # end + # end + # end # # Log subscriber also has some helpers to deal with logging and automatically - # flushes all logs when the request finishes (via action_dispatch.callback - # notification) in a Rails environment. + # flushes all logs when the request finishes + # (via <tt>action_dispatch.callback</tt> notification) in a Rails environment. class LogSubscriber < Subscriber # Embed in a String to clear all previous ANSI sequences. CLEAR = "\e[0m" diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb index 0ff32bd810..7ccc333463 100644 --- a/activesupport/lib/active_support/notifications.rb +++ b/activesupport/lib/active_support/notifications.rb @@ -67,9 +67,12 @@ module ActiveSupport # have a key <tt>:exception</tt> with an array of two elements as value: a string with # the name of the exception class, and the exception message. # The <tt>:exception_object</tt> key of the payload will have the exception - # itself as the value. + # itself as the value: # - # As the previous example depicts, the class <tt>ActiveSupport::Notifications::Event</tt> + # event.payload[:exception] # => ["ArgumentError", "Invalid value"] + # event.payload[:exception_object] # => #<ArgumentError: Invalid value> + # + # As the earlier example depicts, the class <tt>ActiveSupport::Notifications::Event</tt> # is able to take the arguments as they come and provide an object-oriented # interface to that data. # diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb index 4e4ca70942..11721db103 100644 --- a/activesupport/lib/active_support/notifications/fanout.rb +++ b/activesupport/lib/active_support/notifications/fanout.rb @@ -13,7 +13,8 @@ module ActiveSupport include Mutex_m def initialize - @subscribers = [] + @string_subscribers = Hash.new { |h, k| h[k] = [] } + @other_subscribers = [] @listeners_for = Concurrent::Map.new super end @@ -21,8 +22,13 @@ module ActiveSupport def subscribe(pattern = nil, block = Proc.new) subscriber = Subscribers.new pattern, block synchronize do - @subscribers << subscriber - @listeners_for.clear + if String === pattern + @string_subscribers[pattern] << subscriber + @listeners_for.delete(pattern) + else + @other_subscribers << subscriber + @listeners_for.clear + end end subscriber end @@ -31,12 +37,18 @@ module ActiveSupport synchronize do case subscriber_or_name when String - @subscribers.reject! { |s| s.matches?(subscriber_or_name) } + @string_subscribers[subscriber_or_name].clear + @listeners_for.delete(subscriber_or_name) else - @subscribers.delete(subscriber_or_name) + pattern = subscriber_or_name.try(:pattern) + if String === pattern + @string_subscribers[pattern].delete(subscriber_or_name) + @listeners_for.delete(pattern) + else + @other_subscribers.delete(subscriber_or_name) + @listeners_for.clear + end end - - @listeners_for.clear end end @@ -56,7 +68,8 @@ module ActiveSupport # this is correctly done double-checked locking (Concurrent::Map's lookups have volatile semantics) @listeners_for[name] || synchronize do # use synchronisation when accessing @subscribers - @listeners_for[name] ||= @subscribers.select { |s| s.subscribed_to?(name) } + @listeners_for[name] ||= + @string_subscribers[name] + @other_subscribers.select { |s| s.subscribed_to?(name) } end end @@ -101,6 +114,8 @@ module ActiveSupport end class Evented #:nodoc: + attr_reader :pattern + def initialize(pattern, delegate) @pattern = pattern @delegate = delegate diff --git a/activesupport/test/autoloading_fixtures/raises_load_error.rb b/activesupport/test/autoloading_fixtures/raises_load_error.rb new file mode 100644 index 0000000000..f97be29b71 --- /dev/null +++ b/activesupport/test/autoloading_fixtures/raises_load_error.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +# raises a load error typical of the dynamic code that manually raises load errors +raise LoadError, "required gem not present kind of error" diff --git a/activesupport/test/cache/behaviors/cache_store_behavior.rb b/activesupport/test/cache/behaviors/cache_store_behavior.rb index 9f54b1e7de..a696760bb2 100644 --- a/activesupport/test/cache/behaviors/cache_store_behavior.rb +++ b/activesupport/test/cache/behaviors/cache_store_behavior.rb @@ -130,7 +130,7 @@ module CacheStoreBehavior assert_equal("fufu", @cache.read("fu")) end - def test_multi_with_objects + def test_fetch_multi_with_objects cache_struct = Struct.new(:cache_key, :title) foo = cache_struct.new("foo", "FOO!") bar = cache_struct.new("bar") @@ -142,6 +142,14 @@ module CacheStoreBehavior assert_equal({ foo => "FOO!", bar => "BAM!" }, values) end + def test_fetch_multi_returns_ordered_names + @cache.write("bam", "BAM") + + values = @cache.fetch_multi("foo", "bar", "bam") { |key| key.upcase } + + assert_equal(%w(foo bar bam), values.keys) + end + def test_fetch_multi_without_block assert_raises(ArgumentError) do @cache.fetch_multi("foo") diff --git a/activesupport/test/constantize_test_cases.rb b/activesupport/test/constantize_test_cases.rb index 2c6145940b..cdb8441b81 100644 --- a/activesupport/test/constantize_test_cases.rb +++ b/activesupport/test/constantize_test_cases.rb @@ -112,6 +112,16 @@ module ConstantizeTestCases assert_nil yield("A::Object::B") assert_nil yield("A::Object::Object::Object::B") + with_autoloading_fixtures do + assert_nil yield("Em") + end + + assert_raises(LoadError) do + with_autoloading_fixtures do + yield("RaisesLoadError") + end + end + assert_raises(NameError) do with_autoloading_fixtures do yield("RaisesNameError") diff --git a/activesupport/test/core_ext/module/reachable_test.rb b/activesupport/test/core_ext/module/reachable_test.rb deleted file mode 100644 index f356d46957..0000000000 --- a/activesupport/test/core_ext/module/reachable_test.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -require "abstract_unit" -require "active_support/core_ext/module/reachable" - -class AnonymousTest < ActiveSupport::TestCase - test "an anonymous class or module is not reachable" do - assert_deprecated do - assert_not_predicate Module.new, :reachable? - assert_not_predicate Class.new, :reachable? - end - end - - test "ordinary named classes or modules are reachable" do - assert_deprecated do - assert_predicate Kernel, :reachable? - assert_predicate Object, :reachable? - end - end - - test "a named class or module whose constant has gone is not reachable" do - c = eval "class C; end; C" - m = eval "module M; end; M" - - self.class.send(:remove_const, :C) - self.class.send(:remove_const, :M) - - assert_deprecated do - assert_not_predicate c, :reachable? - assert_not_predicate m, :reachable? - end - end - - test "a named class or module whose constants store different objects are not reachable" do - c = eval "class C; end; C" - m = eval "module M; end; M" - - self.class.send(:remove_const, :C) - self.class.send(:remove_const, :M) - - eval "class C; end" - eval "module M; end" - - assert_deprecated do - assert_predicate C, :reachable? - assert_predicate M, :reachable? - assert_not_predicate c, :reachable? - assert_not_predicate m, :reachable? - end - end -end diff --git a/activesupport/test/core_ext/secure_random_test.rb b/activesupport/test/core_ext/secure_random_test.rb index 7067fb524c..4b73233971 100644 --- a/activesupport/test/core_ext/secure_random_test.rb +++ b/activesupport/test/core_ext/secure_random_test.rb @@ -19,4 +19,24 @@ class SecureRandomTest < ActiveSupport::TestCase assert_not_equal s1, s2 assert_equal 24, s1.length end + + def test_base36 + s1 = SecureRandom.base36 + s2 = SecureRandom.base36 + + assert_not_equal s1, s2 + assert_equal 16, s1.length + assert_match(/^[a-z0-9]+$/, s1) + assert_match(/^[a-z0-9]+$/, s2) + end + + def test_base36_with_length + s1 = SecureRandom.base36(24) + s2 = SecureRandom.base36(24) + + assert_not_equal s1, s2 + assert_equal 24, s1.length + assert_match(/^[a-z0-9]+$/, s1) + assert_match(/^[a-z0-9]+$/, s2) + end end diff --git a/activesupport/test/current_attributes_test.rb b/activesupport/test/current_attributes_test.rb index 1669f08f68..adbdc646bc 100644 --- a/activesupport/test/current_attributes_test.rb +++ b/activesupport/test/current_attributes_test.rb @@ -3,13 +3,18 @@ require "abstract_unit" class CurrentAttributesTest < ActiveSupport::TestCase - Person = Struct.new(:name, :time_zone) + Person = Struct.new(:id, :name, :time_zone) class Current < ActiveSupport::CurrentAttributes attribute :world, :account, :person, :request delegate :time_zone, to: :person - resets { Time.zone = "UTC" } + before_reset { Session.previous = person.try(:id) } + + resets do + Time.zone = "UTC" + Session.current = nil + end def account=(account) super @@ -19,6 +24,7 @@ class CurrentAttributesTest < ActiveSupport::TestCase def person=(person) super Time.zone = person.try(:time_zone) + Session.current = person.try(:id) end def request @@ -30,9 +36,14 @@ class CurrentAttributesTest < ActiveSupport::TestCase end end + class Session < ActiveSupport::CurrentAttributes + attribute :current, :previous + end + setup do @original_time_zone = Time.zone Current.reset + Session.reset end teardown do @@ -56,16 +67,28 @@ class CurrentAttributesTest < ActiveSupport::TestCase end test "set auxiliary class via overwritten method" do - Current.person = Person.new("David", "Central Time (US & Canada)") + Current.person = Person.new(42, "David", "Central Time (US & Canada)") assert_equal "Central Time (US & Canada)", Time.zone.name + assert_equal 42, Session.current end - test "resets auxiliary class via callback" do - Current.person = Person.new("David", "Central Time (US & Canada)") + test "resets auxiliary classes via callback" do + Current.person = Person.new(42, "David", "Central Time (US & Canada)") assert_equal "Central Time (US & Canada)", Time.zone.name Current.reset assert_equal "UTC", Time.zone.name + assert_nil Session.current + end + + test "set auxiliary class based on current attributes via before callback" do + Current.person = Person.new(42, "David", "Central Time (US & Canada)") + assert_nil Session.previous + assert_equal 42, Session.current + + Current.reset + assert_equal 42, Session.previous + assert_nil Session.current end test "set attribute only via scope" do @@ -92,13 +115,13 @@ class CurrentAttributesTest < ActiveSupport::TestCase end test "delegation" do - Current.person = Person.new("David", "Central Time (US & Canada)") + Current.person = Person.new(42, "David", "Central Time (US & Canada)") assert_equal "Central Time (US & Canada)", Current.time_zone assert_equal "Central Time (US & Canada)", Current.instance.time_zone end test "all methods forward to the instance" do - Current.person = Person.new("David", "Central Time (US & Canada)") + Current.person = Person.new(42, "David", "Central Time (US & Canada)") assert_equal "David, in Central Time (US & Canada)", Current.intro assert_equal "David, in Central Time (US & Canada)", Current.instance.intro end diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index b1b3070891..d4e709137e 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -841,7 +841,7 @@ class DependenciesTest < ActiveSupport::TestCase remove_constants(:C) end - def test_new_contants_in_without_constants + def test_new_constants_in_without_constants assert_equal [], (ActiveSupport::Dependencies.new_constants_in(Object) { }) assert ActiveSupport::Dependencies.constant_watch_stack.all? { |k, v| v.empty? } end diff --git a/activesupport/test/hash_with_indifferent_access_test.rb b/activesupport/test/hash_with_indifferent_access_test.rb index f81e0dc70f..1d6a07ec5f 100644 --- a/activesupport/test/hash_with_indifferent_access_test.rb +++ b/activesupport/test/hash_with_indifferent_access_test.rb @@ -447,6 +447,14 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings end + def test_indifferent_assoc + indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings) + key, value = indifferent_strings.assoc(:a) + + assert_equal("a", key) + assert_equal(1, value) + end + def test_indifferent_compact hash_contain_nil_value = @strings.merge("z" => nil) hash = ActiveSupport::HashWithIndifferentAccess.new(hash_contain_nil_value) @@ -478,7 +486,7 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase assert_equal @strings, roundtrip assert_equal "1234", roundtrip.default - # Ensure nested hashes are not HashWithIndiffereneAccess + # Ensure nested hashes are not HashWithIndifferentAccess new_to_hash = @nested_mixed.with_indifferent_access.to_hash assert_not new_to_hash.instance_of?(HashWithIndifferentAccess) assert_not new_to_hash["a"].instance_of?(HashWithIndifferentAccess) diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index 5e50acf5db..c3e1faff5d 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -224,12 +224,6 @@ class InflectorTest < ActiveSupport::TestCase assert_equal("json_html_api", ActiveSupport::Inflector.underscore("JSONHTMLAPI")) end - def test_acronym_regexp_is_deprecated - assert_deprecated do - ActiveSupport::Inflector.inflections.acronym_regex - end - end - def test_underscore CamelToUnderscore.each do |camel, underscore| assert_equal(underscore, ActiveSupport::Inflector.underscore(camel)) diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb index 4e0aef2cc7..b5d72d1a42 100644 --- a/activesupport/test/notifications_test.rb +++ b/activesupport/test/notifications_test.rb @@ -81,7 +81,7 @@ module Notifications assert_equal expected, events end - def test_subsribing_to_instrumentation_while_inside_it + def test_subscribing_to_instrumentation_while_inside_it # the repro requires that there are no evented subscribers for the "foo" event, # so we have to duplicate some of the setup code old_notifier = ActiveSupport::Notifications.notifier diff --git a/activesupport/test/safe_buffer_test.rb b/activesupport/test/safe_buffer_test.rb index 49a3951623..08d04e3223 100644 --- a/activesupport/test/safe_buffer_test.rb +++ b/activesupport/test/safe_buffer_test.rb @@ -151,7 +151,7 @@ class SafeBufferTest < ActiveSupport::TestCase assert_equal "", ActiveSupport::SafeBuffer.new("foo").clone_empty end - test "clone_empty keeps the original dirtyness" do + test "clone_empty keeps the original dirtiness" do assert_predicate @buffer.clone_empty, :html_safe? assert_not_predicate @buffer.gsub!("", "").clone_empty, :html_safe? end diff --git a/activesupport/test/share_lock_test.rb b/activesupport/test/share_lock_test.rb index 30a1ddad3f..a40c813fe3 100644 --- a/activesupport/test/share_lock_test.rb +++ b/activesupport/test/share_lock_test.rb @@ -38,7 +38,7 @@ class ShareLockTest < ActiveSupport::TestCase end end - def test_multiple_exlusives_are_able_to_progress + def test_multiple_exclusives_are_able_to_progress with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch| exclusive_threads = (1..2).map do Thread.new do diff --git a/activesupport/test/test_case_test.rb b/activesupport/test/test_case_test.rb index 8698c66e6d..56cd2665e0 100644 --- a/activesupport/test/test_case_test.rb +++ b/activesupport/test/test_case_test.rb @@ -104,7 +104,7 @@ class AssertionsTest < ActiveSupport::TestCase def test_expression_is_evaluated_in_the_appropriate_scope silence_warnings do local_scope = "foo" - local_scope = local_scope # to suppress unused variable warning + _ = local_scope # to suppress unused variable warning assert_difference("local_scope; @object.num") { @object.increment } end end diff --git a/activesupport/test/time_travel_test.rb b/activesupport/test/time_travel_test.rb index 9c61ab0ab5..a1f84bf69e 100644 --- a/activesupport/test/time_travel_test.rb +++ b/activesupport/test/time_travel_test.rb @@ -148,7 +148,7 @@ class TimeTravelTest < ActiveSupport::TestCase end end - def test_travel_to_will_reset_the_usec_to_avoid_mysql_rouding + def test_travel_to_will_reset_the_usec_to_avoid_mysql_rounding Time.stub(:now, Time.now) do travel_to Time.utc(2014, 10, 10, 10, 10, 50, 999999) do assert_equal 50, Time.now.sec diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index 6d45a6726d..f948c363df 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -32,7 +32,7 @@ class TimeZoneTest < ActiveSupport::TestCase end end - def test_period_for_local_with_ambigiuous_time + def test_period_for_local_with_ambiguous_time zone = ActiveSupport::TimeZone["Moscow"] period = zone.period_for_local(Time.utc(2015, 1, 1)) assert_equal period, zone.period_for_local(Time.utc(2014, 10, 26, 1, 0, 0)) diff --git a/ci/travis.rb b/ci/travis.rb index 168bb259f4..038db476e9 100755 --- a/ci/travis.rb +++ b/ci/travis.rb @@ -154,6 +154,7 @@ ENV["GEM"].split(",").each do |gem| next if gem == "actionview:ujs" && isolated next if gem == "activestorage" && isolated next if gem == "actionmailbox" && isolated + next if gem == "actiontext" && isolated build = Build.new(gem, isolated: isolated) results[build.key] = build.run! diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md index 9f95e22245..6432dbfd00 100644 --- a/guides/CHANGELOG.md +++ b/guides/CHANGELOG.md @@ -1,3 +1,13 @@ +## Rails 6.0.0.beta1 (January 18, 2019) ## + +* Add "Action Text Overview" Guide. + + *DHH* + +* Add "Action Mailbox Basics" Guide. + + *DHH* + * New section _Troubleshooting_ in the _Autoloading and Reloading Constants_ guide. *Xavier Noria* diff --git a/guides/assets/images/getting_started/template_is_missing_articles_new.png b/guides/assets/images/getting_started/template_is_missing_articles_new.png Binary files differindex a1603f5d28..830b19bd9d 100644 --- a/guides/assets/images/getting_started/template_is_missing_articles_new.png +++ b/guides/assets/images/getting_started/template_is_missing_articles_new.png diff --git a/guides/bug_report_templates/active_record_gem.rb b/guides/bug_report_templates/active_record_gem.rb index d88304a219..74b329115d 100644 --- a/guides/bug_report_templates/active_record_gem.rb +++ b/guides/bug_report_templates/active_record_gem.rb @@ -9,7 +9,7 @@ gemfile(true) do # Activate the gem you are reporting the issue against. gem "activerecord", "5.2.0" - gem "sqlite3" + gem "sqlite3", "~> 1.3.6" end require "active_record" diff --git a/guides/bug_report_templates/active_record_migrations_gem.rb b/guides/bug_report_templates/active_record_migrations_gem.rb index 5dfd49fb38..8d71863034 100644 --- a/guides/bug_report_templates/active_record_migrations_gem.rb +++ b/guides/bug_report_templates/active_record_migrations_gem.rb @@ -9,7 +9,7 @@ gemfile(true) do # Activate the gem you are reporting the issue against. gem "activerecord", "5.2.0" - gem "sqlite3" + gem "sqlite3", "~> 1.3.6" end require "active_record" diff --git a/guides/rails_guides/generator.rb b/guides/rails_guides/generator.rb index 48e90510e1..b0ddb0e7e0 100644 --- a/guides/rails_guides/generator.rb +++ b/guides/rails_guides/generator.rb @@ -63,9 +63,9 @@ module RailsGuides end def mobi - mobi = "ruby_on_rails_guides_#{@version || @edge[0, 7]}" - mobi += ".#{@language}" if @language - mobi += ".mobi" + mobi = +"ruby_on_rails_guides_#{@version || @edge[0, 7]}" + mobi << ".#{@language}" if @language + mobi << ".mobi" end def initialize_dirs @@ -150,8 +150,8 @@ module RailsGuides puts "Generating #{guide} as #{output_file}" layout = @kindle ? "kindle/layout" : "layout" - view = ActionView::Base.new( - @source_dir, + view = ActionView::Base.with_view_paths( + [@source_dir], edge: @edge, version: @version, mobi: "kindle/#{mobi}", diff --git a/guides/rails_guides/markdown.rb b/guides/rails_guides/markdown.rb index a98aa8fe66..018f49ffd0 100644 --- a/guides/rails_guides/markdown.rb +++ b/guides/rails_guides/markdown.rb @@ -3,6 +3,7 @@ require "redcarpet" require "nokogiri" require "rails_guides/markdown/renderer" +require "rails-html-sanitizer" module RailsGuides class Markdown @@ -20,6 +21,7 @@ module RailsGuides @raw_body = body extract_raw_header_and_body generate_header + generate_description generate_title generate_body generate_structure @@ -82,6 +84,11 @@ module RailsGuides @header = engine.render(@raw_header).html_safe end + def generate_description + sanitizer = Rails::Html::FullSanitizer.new + @description = sanitizer.sanitize(@header).squish + end + def generate_structure @headings_for_index = [] if @body.present? @@ -165,6 +172,7 @@ module RailsGuides def render_page @view.content_for(:header_section) { @header } + @view.content_for(:description) { @description } @view.content_for(:page_title) { @title } @view.content_for(:index_section) { @index } @view.render(layout: @layout, html: @body.html_safe) diff --git a/guides/source/6_0_release_notes.md b/guides/source/6_0_release_notes.md index f3ed21dc45..e6fb15c09c 100644 --- a/guides/source/6_0_release_notes.md +++ b/guides/source/6_0_release_notes.md @@ -5,7 +5,10 @@ Ruby on Rails 6.0 Release Notes Highlights in Rails 6.0: +* Action Mailbox +* Action Text * Parallel Testing +* Action Cable Testing These release notes cover only the major changes. To learn about various bug fixes and changes, please refer to the change logs or check out the [list of @@ -28,6 +31,29 @@ guide. Major Features -------------- +### Action Mailbox + +[Pull Request](https://github.com/rails/rails/pull/34786) + +[Action Mailbox](https://github.com/rails/rails/tree/6-0-stable/actionmailbox) allows you +to route incoming emails to controller-like mailboxes. +You can read more about Action Mailbox in the [Action Mailbox Basics](action_mailbox_basics.html) guide. + +### Action Text + +[Pull Request](https://github.com/rails/rails/pull/34873) + +[Action Text](https://github.com/rails/rails/tree/6-0-stable/actiontext) +brings rich text content and editing to Rails. It includes +the [Trix editor](https://trix-editor.org) that handles everything from formatting +to links to quotes to lists to embedded images and galleries. +The rich text content generated by the Trix editor is saved in its own +RichText model that's associated with any existing Active Record model in the application. +Any embedded images (or other attachments) are automatically stored using +Active Storage and associated with the included RichText model. + +You can read more about Action Text in the [Action Text Overview](action_text_overview.html) guide. + ### Parallel Testing [Pull Request](https://github.com/rails/rails/pull/31900) @@ -37,6 +63,13 @@ test suite. While forking processes is the default method, threading is supported as well. Running tests in parallel reduces the time it takes your entire test suite to run. +### Action Cable Testing + +[Pull Request](https://github.com/rails/rails/pull/33659) + +[Action Cable testing tools](testing.html#testing-action-cable) allow you to test your +Action Cable functionality at any level: connections, channels, broadcasts. + Railties -------- @@ -59,6 +92,22 @@ Please refer to the [Changelog][action-cable] for detailed changes. ### Notable changes +* The ActionCable javascript package has been converted from CoffeeScript + to ES2015, and we now publish the source code in the npm distribution. + + This allows ActionCable users to depend on the javascript source code + rather than the compiled code, which can produce smaller javascript bundles. + + This change includes some breaking changes to optional parts of the + ActionCable javascript API: + + - Configuration of the WebSocket adapter and logger adapter have been moved + from properties of `ActionCable` to properties of `ActionCable.adapters`. + + - The `ActionCable.startDebugging()` and `ActionCable.stopDebugging()` + methods have been removed and replaced with the property + `ActionCable.logger.enabled`. + Action Pack ----------- diff --git a/guides/source/action_cable_overview.md b/guides/source/action_cable_overview.md index 2f602c3e0a..8f5c44849a 100644 --- a/guides/source/action_cable_overview.md +++ b/guides/source/action_cable_overview.md @@ -27,6 +27,36 @@ client-side JavaScript framework and a server-side Ruby framework. You have access to your full domain model written with Active Record or your ORM of choice. +Terminology +----------- + +A single Action Cable server can handle multiple connection instances. It has one +connection instance per WebSocket connection. A single user may have multiple +WebSockets open to your application if they use multiple browser tabs or devices. +The client of a WebSocket connection is called the consumer. + +Each consumer can in turn subscribe to multiple cable channels. Each channel +encapsulates a logical unit of work, similar to what a controller does in +a regular MVC setup. For example, you could have a `ChatChannel` and +an `AppearancesChannel`, and a consumer could be subscribed to either +or to both of these channels. At the very least, a consumer should be subscribed +to one channel. + +When the consumer is subscribed to a channel, they act as a subscriber. +The connection between the subscriber and the channel is, surprise-surprise, +called a subscription. A consumer can act as a subscriber to a given channel +any number of times. For example, a consumer could subscribe to multiple chat rooms +at the same time. (And remember that a physical user may have multiple consumers, +one per tab/device open to your connection). + +Each channel can then again be streaming zero or more broadcastings. +A broadcasting is a pubsub link where anything transmitted by the broadcaster is +sent directly to the channel subscribers who are streaming that named broadcasting. + +As you can see, this is a fairly deep architectural stack. There's a lot of new +terminology to identify the new pieces, and on top of that, you're dealing +with both client and server side reflections of each unit. + What is Pub/Sub --------------- @@ -147,16 +177,13 @@ established using the following JavaScript, which is generated by default by Rai #### Connect Consumer ```js -// app/assets/javascripts/cable.js -//= require action_cable -//= require_self -//= require_tree ./channels +// app/javascript/channels/consumer.js +// Action Cable provides the framework to deal with WebSockets in Rails. +// You can generate new channels where WebSocket features live using the `rails generate channel` command. -(function() { - this.App || (this.App = {}); +import { createConsumer } from "@rails/actioncable" - App.cable = ActionCable.createConsumer(); -}).call(this); +export default createConsumer() ``` This will ready a consumer that'll connect against `/cable` on your server by default. @@ -167,12 +194,16 @@ you're interested in having. A consumer becomes a subscriber by creating a subscription to a given channel: -```coffeescript -# app/assets/javascripts/cable/subscriptions/chat.coffee -App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" } +```js +// app/javascript/channels/chat_channel.js +import consumer from "./consumer" + +consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" }) -# app/assets/javascripts/cable/subscriptions/appearance.coffee -App.cable.subscriptions.create { channel: "AppearanceChannel" } +// app/javascript/channels/appearance_channel.js +import consumer from "./consumer" + +consumer.subscriptions.create({ channel: "AppearanceChannel" }) ``` While this creates the subscription, the functionality needed to respond to @@ -181,9 +212,12 @@ received data will be described later on. A consumer can act as a subscriber to a given channel any number of times. For example, a consumer could subscribe to multiple chat rooms at the same time: -```coffeescript -App.cable.subscriptions.create { channel: "ChatChannel", room: "1st Room" } -App.cable.subscriptions.create { channel: "ChatChannel", room: "2nd Room" } +```js +// app/javascript/channels/chat_channel.js +import consumer from "./consumer" + +consumer.subscriptions.create({ channel: "ChatChannel", room: "1st Room" }) +consumer.subscriptions.create({ channel: "ChatChannel", room: "2nd Room" }) ``` ## Client-Server Interactions @@ -256,24 +290,31 @@ When a consumer is subscribed to a channel, they act as a subscriber. This connection is called a subscription. Incoming messages are then routed to these channel subscriptions based on an identifier sent by the cable consumer. -```coffeescript -# app/assets/javascripts/cable/subscriptions/chat.coffee -# Assumes you've already requested the right to send web notifications -App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }, - received: (data) -> - @appendLine(data) - - appendLine: (data) -> - html = @createLine(data) - $("[data-chat-room='Best Room']").append(html) - - createLine: (data) -> - """ - <article class="chat-line"> - <span class="speaker">#{data["sent_by"]}</span> - <span class="body">#{data["body"]}</span> - </article> - """ +```js +// app/javascript/channels/chat_channel.js +// Assumes you've already requested the right to send web notifications +import consumer from "./consumer" + +consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" }, { + received(data) { + this.appendLine(data) + }, + + appendLine(data) { + const html = this.createLine(data) + const element = document.querySelector("[data-chat-room='Best Room']") + element.insertAdjacentHTML("beforeend", html) + }, + + createLine(data) { + return ` + <article class="chat-line"> + <span class="speaker">${data["sent_by"]}</span> + <span class="body">${data["body"]}</span> + </article> + ` + } +}) ``` ### Passing Parameters to Channels @@ -293,23 +334,30 @@ end An object passed as the first argument to `subscriptions.create` becomes the params hash in the cable channel. The keyword `channel` is required: -```coffeescript -# app/assets/javascripts/cable/subscriptions/chat.coffee -App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }, - received: (data) -> - @appendLine(data) - - appendLine: (data) -> - html = @createLine(data) - $("[data-chat-room='Best Room']").append(html) - - createLine: (data) -> - """ - <article class="chat-line"> - <span class="speaker">#{data["sent_by"]}</span> - <span class="body">#{data["body"]}</span> - </article> - """ +```js +// app/javascript/channels/chat_channel.js +import consumer from "./consumer" + +consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" }, { + received(data) { + this.appendLine(data) + }, + + appendLine(data) { + const html = this.createLine(data) + const element = document.querySelector("[data-chat-room='Best Room']") + element.insertAdjacentHTML("beforeend", html) + }, + + createLine(data) { + return ` + <article class="chat-line"> + <span class="speaker">${data["sent_by"]}</span> + <span class="body">${data["body"]}</span> + </article> + ` + } +}) ``` ```ruby @@ -340,13 +388,17 @@ class ChatChannel < ApplicationCable::Channel end ``` -```coffeescript -# app/assets/javascripts/cable/subscriptions/chat.coffee -App.chatChannel = App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }, - received: (data) -> - # data => { sent_by: "Paul", body: "This is a cool chat app." } +```js +// app/javascript/channels/chat_channel.js +import consumer from "./consumer" + +const chatChannel = consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" }, { + received(data) { + // data => { sent_by: "Paul", body: "This is a cool chat app." } + } +} -App.chatChannel.send({ sent_by: "Paul", body: "This is a cool chat app." }) +chatChannel.send({ sent_by: "Paul", body: "This is a cool chat app." }) ``` The rebroadcast will be received by all connected clients, _including_ the @@ -396,46 +448,69 @@ appear/disappear API could be backed by Redis, a database, or whatever else. Create the client-side appearance channel subscription: -```coffeescript -# app/assets/javascripts/cable/subscriptions/appearance.coffee -App.cable.subscriptions.create "AppearanceChannel", - # Called when the subscription is ready for use on the server. - connected: -> - @install() - @appear() - - # Called when the WebSocket connection is closed. - disconnected: -> - @uninstall() - - # Called when the subscription is rejected by the server. - rejected: -> - @uninstall() - - appear: -> - # Calls `AppearanceChannel#appear(data)` on the server. - @perform("appear", appearing_on: $("main").data("appearing-on")) - - away: -> - # Calls `AppearanceChannel#away` on the server. - @perform("away") - - - buttonSelector = "[data-behavior~=appear_away]" - - install: -> - $(document).on "turbolinks:load.appearance", => - @appear() - - $(document).on "click.appearance", buttonSelector, => - @away() - false - - $(buttonSelector).show() - - uninstall: -> - $(document).off(".appearance") - $(buttonSelector).hide() +```js +// app/javascript/channels/appearance_channel.js +import consumer from "./consumer" + +consumer.subscriptions.create("AppearanceChannel", { + // Called once when the subscription is created. + initialized() { + this.update = this.update.bind(this) + }, + + // Called when the subscription is ready for use on the server. + connected() { + this.install() + this.update() + }, + + // Called when the WebSocket connection is closed. + disconnected() { + this.uninstall() + }, + + // Called when the subscription is rejected by the server. + rejected() { + this.uninstall() + }, + + update() { + this.documentIsActive ? this.appear() : this.away() + }, + + appear() { + // Calls `AppearanceChannel#appear(data)` on the server. + this.perform("appear", { appearing_on: this.appearingOn }) + }, + + away() { + // Calls `AppearanceChannel#away` on the server. + this.perform("away") + }, + + install() { + window.addEventListener("focus", this.update) + window.addEventListener("blur", this.update) + document.addEventListener("turbolinks:load", this.update) + document.addEventListener("visibilitychange", this.update) + }, + + uninstall() { + window.removeEventListener("focus", this.update) + window.removeEventListener("blur", this.update) + document.removeEventListener("turbolinks:load", this.update) + document.removeEventListener("visibilitychange", this.update) + }, + + get documentIsActive() { + return document.visibilityState == "visible" && document.hasFocus() + }, + + get appearingOn() { + const element = document.querySelector("[data-appearing-on]") + return element ? element.getAttribute("data-appearing-on") : null + } +}) ``` ##### Client-Server Interaction @@ -445,16 +520,16 @@ ActionCable.createConsumer("ws://cable.example.com")`. (`cable.js`). The **Server** identifies this connection by `current_user`. 2. **Client** subscribes to the appearance channel via -`App.cable.subscriptions.create(channel: "AppearanceChannel")`. (`appearance.coffee`) +`consumer.subscriptions.create({ channel: "AppearanceChannel" })`. (`appearance_channel.js`) 3. **Server** recognizes a new subscription has been initiated for the appearance channel and runs its `subscribed` callback, calling the `appear` method on `current_user`. (`appearance_channel.rb`) 4. **Client** recognizes that a subscription has been established and calls -`connected` (`appearance.coffee`) which in turn calls `@install` and `@appear`. -`@appear` calls `AppearanceChannel#appear(data)` on the server, and supplies a -data hash of `{ appearing_on: $("main").data("appearing-on") }`. This is +`connected` (`appearance_channel.js`) which in turn calls `install` and `appear`. +`appear` calls `AppearanceChannel#appear(data)` on the server, and supplies a +data hash of `{ appearing_on: this.appearingOn }`. This is possible because the server-side channel instance automatically exposes all public methods declared on the class (minus the callbacks), so that these can be reached as remote procedure calls via a subscription's `perform` method. @@ -488,13 +563,17 @@ end Create the client-side web notifications channel subscription: -```coffeescript -# app/assets/javascripts/cable/subscriptions/web_notifications.coffee -# Client-side which assumes you've already requested -# the right to send web notifications. -App.cable.subscriptions.create "WebNotificationsChannel", - received: (data) -> - new Notification data["title"], body: data["body"] +```js +// app/javascript/channels/web_notifications_channel.js +// Client-side which assumes you've already requested +// the right to send web notifications. +import consumer from "./consumer" + +consumer.subscriptions.create("WebNotificationsChannel", { + received(data) { + new Notification(data["title"], body: data["body"]) + } +}) ``` Broadcast content to a web notification channel instance from elsewhere in your @@ -629,10 +708,9 @@ class Application < Rails::Application end ``` -You can use `App.cable = ActionCable.createConsumer()` to connect to the cable -server if `action_cable_meta_tag` is invoked in the layout. A custom path is -specified as first argument to `createConsumer` (e.g. `App.cable = -ActionCable.createConsumer("/websocket")`). +You can use `ActionCable.createConsumer()` to connect to the cable +server if `action_cable_meta_tag` is invoked in the layout. Otherwise, A path is +specified as first argument to `createConsumer` (e.g. `ActionCable.createConsumer("/websocket")`). For every instance of your server you create and for every worker your server spawns, you will also have a new instance of Action Cable, but the use of Redis @@ -690,3 +768,8 @@ internally, irrespective of whether the application server is multi-threaded or Accordingly, Action Cable works with popular servers like Unicorn, Puma, and Passenger. + +## Testing + +You can find detailed instructions on how to test your Action Cable functionality in the +[testing guide](testing.html#testing-action-cable). diff --git a/guides/source/action_mailbox_basics.md b/guides/source/action_mailbox_basics.md new file mode 100644 index 0000000000..c90892d456 --- /dev/null +++ b/guides/source/action_mailbox_basics.md @@ -0,0 +1,398 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.** + +Action Mailbox Basics +===================== + +This guide provides you with all you need to get started in receiving +emails to your application. + +After reading this guide, you will know: + +* How to receive email within a Rails application. +* How to configure Action Mailbox. +* How to generate and route emails to a mailbox. +* How to test incoming emails. + +-------------------------------------------------------------------------------- + +Introduction +------------ + +Action Mailbox routes incoming emails to controller-like mailboxes for +processing in Rails. It ships with ingresses for Amazon SES, Mailgun, Mandrill, +Postmark, and SendGrid. You can also handle inbound mails directly via the +built-in Exim, Postfix, and Qmail ingresses. + +The inbound emails are turned into `InboundEmail` records using Active Record +and feature lifecycle tracking, storage of the original email on cloud storage +via Active Storage, and responsible data handling with +on-by-default incineration. + +These inbound emails are routed asynchronously using Active Job to one or +several dedicated mailboxes, which are capable of interacting directly +with the rest of your domain model. + +## Setup + +Install migrations needed for `InboundEmail` and ensure Active Storage is set up: + +```bash +$ rails action_mailbox:install +$ rails db:migrate +``` + +## Configuration + +### Amazon SES + +Install the [`aws-sdk-sns`](https://rubygems.org/gems/aws-sdk-sns) gem: + +```ruby +# Gemfile +gem "aws-sdk-sns", ">= 1.9.0", require: false +``` + +Tell Action Mailbox to accept emails from SES: + +```ruby +# config/environments/production.rb +config.action_mailbox.ingress = :amazon +``` + +[Configure SES](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-notifications.html) +to deliver emails to your application via POST requests to +`/rails/action_mailbox/amazon/inbound_emails`. If your application lived at +`https://example.com`, you would specify the fully-qualified URL +`https://example.com/rails/action_mailbox/amazon/inbound_emails`. + +### Exim + +Tell Action Mailbox to accept emails from an SMTP relay: + +```ruby +# config/environments/production.rb +config.action_mailbox.ingress = :relay +``` + +Generate a strong password that Action Mailbox can use to authenticate requests to the relay ingress. + +Use `rails credentials:edit` to add the password to your application's encrypted credentials under +`action_mailbox.ingress_password`, where Action Mailbox will automatically find it: + +```yaml +action_mailbox: + ingress_password: ... +``` + +Alternatively, provide the password in the `RAILS_INBOUND_EMAIL_PASSWORD` environment variable. + +Configure Exim to pipe inbound emails to `bin/rails action_mailbox:ingress:exim`, +providing the `URL` of the relay ingress and the `INGRESS_PASSWORD` you +previously generated. If your application lived at `https://example.com`, the +full command would look like this: + +```shell +bin/rails action_mailbox:ingress:exim URL=https://example.com/rails/action_mailbox/relay/inbound_emails INGRESS_PASSWORD=... +``` + +### Mailgun + +Give Action Mailbox your +[Mailgun API key](https://help.mailgun.com/hc/en-us/articles/203380100-Where-can-I-find-my-API-key-and-SMTP-credentials) +so it can authenticate requests to the Mailgun ingress. + +Use `rails credentials:edit` to add your API key to your application's +encrypted credentials under `action_mailbox.mailgun_api_key`, +where Action Mailbox will automatically find it: + +```yaml +action_mailbox: + mailgun_api_key: ... +``` + +Alternatively, provide your API key in the `MAILGUN_INGRESS_API_KEY` environment +variable. + +Tell Action Mailbox to accept emails from Mailgun: + +```ruby +# config/environments/production.rb +config.action_mailbox.ingress = :mailgun +``` + +[Configure Mailgun](https://documentation.mailgun.com/en/latest/user_manual.html#receiving-forwarding-and-storing-messages) +to forward inbound emails to `/rails/action_mailbox/mailgun/inbound_emails/mime`. +If your application lived at `https://example.com`, you would specify the +fully-qualified URL `https://example.com/rails/action_mailbox/mailgun/inbound_emails/mime`. + +### Mandrill + +Give Action Mailbox your Mandrill API key so it can authenticate requests to +the Mandrill ingress. + +Use `rails credentials:edit` to add your API key to your application's +encrypted credentials under `action_mailbox.mandrill_api_key`, +where Action Mailbox will automatically find it: + +```yaml +action_mailbox: + mandrill_api_key: ... +``` + +Alternatively, provide your API key in the `MANDRILL_INGRESS_API_KEY` +environment variable. + +Tell Action Mailbox to accept emails from Mandrill: + +```ruby +# config/environments/production.rb +config.action_mailbox.ingress = :mandrill +``` + +[Configure Mandrill](https://mandrill.zendesk.com/hc/en-us/articles/205583197-Inbound-Email-Processing-Overview) +to route inbound emails to `/rails/action_mailbox/mandrill/inbound_emails`. +If your application lived at `https://example.com`, you would specify +the fully-qualified URL `https://example.com/rails/action_mailbox/mandrill/inbound_emails`. + +### Postfix + +Tell Action Mailbox to accept emails from an SMTP relay: + +```ruby +# config/environments/production.rb +config.action_mailbox.ingress = :relay +``` + +Generate a strong password that Action Mailbox can use to authenticate requests to the relay ingress. + +Use `rails credentials:edit` to add the password to your application's encrypted credentials under +`action_mailbox.ingress_password`, where Action Mailbox will automatically find it: + +```yaml +action_mailbox: + ingress_password: ... +``` + +Alternatively, provide the password in the `RAILS_INBOUND_EMAIL_PASSWORD` environment variable. + +[Configure Postfix](https://serverfault.com/questions/258469/how-to-configure-postfix-to-pipe-all-incoming-email-to-a-script) +to pipe inbound emails to `bin/rails action_mailbox:ingress:postfix`, providing +the `URL` of the Postfix ingress and the `INGRESS_PASSWORD` you previously +generated. If your application lived at `https://example.com`, the full command +would look like this: + +```shell +$ bin/rails action_mailbox:ingress:postfix URL=https://example.com/rails/action_mailbox/relay/inbound_emails INGRESS_PASSWORD=... +``` + +### Postmark + +Tell Action Mailbox to accept emails from Postmark: + +```ruby +# config/environments/production.rb +config.action_mailbox.ingress = :postmark +``` + +Generate a strong password that Action Mailbox can use to authenticate +requests to the Postmark ingress. + +Use `rails credentials:edit` to add the password to your application's +encrypted credentials under `action_mailbox.ingress_password`, +where Action Mailbox will automatically find it: + +```yaml +action_mailbox: + ingress_password: ... +``` + +Alternatively, provide the password in the `RAILS_INBOUND_EMAIL_PASSWORD` +environment variable. + +[Configure Postmark inbound webhook](https://postmarkapp.com/manual#configure-your-inbound-webhook-url) +to forward inbound emails to `/rails/action_mailbox/postmark/inbound_emails` with the username `actionmailbox` +and the password you previously generated. If your application lived at `https://example.com`, you would +configure Postmark with the following fully-qualified URL: + +``` +https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/postmark/inbound_emails +``` + +NOTE: When configuring your Postmark inbound webhook, be sure to check the box labeled **"Include raw email content in JSON payload"**. +Action Mailbox needs the raw email content to work. + +### Qmail + +Tell Action Mailbox to accept emails from an SMTP relay: + +```ruby +# config/environments/production.rb +config.action_mailbox.ingress = :relay +``` + +Generate a strong password that Action Mailbox can use to authenticate requests to the relay ingress. + +Use `rails credentials:edit` to add the password to your application's encrypted credentials under +`action_mailbox.ingress_password`, where Action Mailbox will automatically find it: + +```yaml +action_mailbox: + ingress_password: ... +``` + +Alternatively, provide the password in the `RAILS_INBOUND_EMAIL_PASSWORD` environment variable. + +Configure Qmail to pipe inbound emails to `bin/rails action_mailbox:ingress:qmail`, +providing the `URL` of the relay ingress and the `INGRESS_PASSWORD` you +previously generated. If your application lived at `https://example.com`, the +full command would look like this: + +```shell +bin/rails action_mailbox:ingress:qmail URL=https://example.com/rails/action_mailbox/relay/inbound_emails INGRESS_PASSWORD=... +``` + +### SendGrid + +Tell Action Mailbox to accept emails from SendGrid: + +```ruby +# config/environments/production.rb +config.action_mailbox.ingress = :sendgrid +``` + +Generate a strong password that Action Mailbox can use to authenticate +requests to the SendGrid ingress. + +Use `rails credentials:edit` to add the password to your application's +encrypted credentials under `action_mailbox.ingress_password`, +where Action Mailbox will automatically find it: + +```yaml +action_mailbox: + ingress_password: ... +``` + +Alternatively, provide the password in the `RAILS_INBOUND_EMAIL_PASSWORD` +environment variable. + +[Configure SendGrid Inbound Parse](https://sendgrid.com/docs/for-developers/parsing-email/setting-up-the-inbound-parse-webhook/) +to forward inbound emails to +`/rails/action_mailbox/sendgrid/inbound_emails` with the username `actionmailbox` +and the password you previously generated. If your application lived at `https://example.com`, +you would configure SendGrid with the following URL: + +``` +https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/sendgrid/inbound_emails +``` + +NOTE: When configuring your SendGrid Inbound Parse webhook, be sure to check the box labeled **“Post the raw, full MIME message.”** Action Mailbox needs the raw MIME message to work. + +## Examples + +Configure basic routing: + +```ruby +# app/mailboxes/application_mailbox.rb +class ApplicationMailbox < ActionMailbox::Base + routing /^save@/i => :forwards + routing /@replies\./i => :replies +end +``` + +Then set up a mailbox: + +```ruby +# Generate new mailbox +$ bin/rails generate mailbox forwards +``` + +```ruby +# app/mailboxes/forwards_mailbox.rb +class ForwardsMailbox < ApplicationMailbox + # Callbacks specify prerequisites to processing + before_processing :require_forward + + def process + if forwarder.buckets.one? + record_forward + else + stage_forward_and_request_more_details + end + end + + private + def require_forward + unless message.forward? + # Use Action Mailers to bounce incoming emails back to sender – this halts processing + bounce_with Forwards::BounceMailer.missing_forward( + inbound_email, forwarder: forwarder + ) + end + end + + def forwarder + @forwarder ||= Person.where(email_address: mail.from) + end + + def record_forward + forwarder.buckets.first.record \ + Forward.new forwarder: forwarder, subject: message.subject, content: mail.content + end + + def stage_forward_and_request_more_details + Forwards::RoutingMailer.choose_project(mail).deliver_now + end +end +``` + +## Incineration of InboundEmails + +By default, an InboundEmail that has been successfully processed will be +incinerated after 30 days. This ensures you're not holding on to people's data +willy-nilly after they may have canceled their accounts or deleted their +content. The intention is that after you've processed an email, you should have +extracted all the data you needed and turned it into domain models and content +on your side of the application. The InboundEmail simply stays in the system +for the extra time to provide debugging and forensics options. + +The actual incineration is done via the `IncinerationJob` that's scheduled +to run after `config.action_mailbox.incinerate_after` time. This value is +by default set to `30.days`, but you can change it in your production.rb +configuration. (Note that this far-future incineration scheduling relies on +your job queue being able to hold jobs for that long.) + +## Working with Action Mailbox in development + +It's helpful to be able to test incoming emails in development without actually +sending and receiving real emails. To accomplish this, there's a conductor +controller mounted at `/rails/conductor/action_mailbox/inbound_emails`, +which gives you an index of all the InboundEmails in the system, their +state of processing, and a form to create a new InboundEmail as well. + +## Testing mailboxes + +Example: + +```ruby +class ForwardsMailboxTest < ActionMailbox::TestCase + test "directly recording a client forward for a forwarder and forwardee corresponding to one project" do + assert_difference -> { people(:david).buckets.first.recordings.count } do + receive_inbound_email_from_mail \ + to: 'save@example.com', + from: people(:david).email_address, + subject: "Fwd: Status update?", + body: <<~BODY + --- Begin forwarded message --- + From: Frank Holland <frank@microsoft.com> + + What's the status? + BODY + end + + recording = people(:david).buckets.first.recordings.last + assert_equal people(:david), recording.creator + assert_equal "Status update?", recording.forward.subject + assert_match "What's the status?", recording.forward.content.to_s + end +end +``` diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index 1acb993cad..16db433bd4 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -3,13 +3,13 @@ Action Mailer Basics ==================== -This guide provides you with all you need to get started in sending and -receiving emails from and to your application, and many internals of Action +This guide provides you with all you need to get started in sending +emails from and to your application, and many internals of Action Mailer. It also covers how to test your mailers. After reading this guide, you will know: -* How to send and receive email within a Rails application. +* How to send email within a Rails application. * How to generate and edit an Action Mailer class and mailer view. * How to configure Action Mailer for your environment. * How to test your Action Mailer classes. @@ -427,7 +427,7 @@ If you would like to render a template located outside of the default `app/views ```ruby class UserMailer < ApplicationMailer prepend_view_path "custom/path/to/mailer/view" - + # This will try to load "custom/path/to/mailer/view/welcome_email" template def welcome_email # ... @@ -651,48 +651,8 @@ class UserMailer < ApplicationMailer end ``` -Receiving Emails ----------------- - -Receiving and parsing emails with Action Mailer can be a rather complex -endeavor. Before your email reaches your Rails app, you would have had to -configure your system to somehow forward emails to your app, which needs to be -listening for that. So, to receive emails in your Rails app you'll need to: - -* Implement a `receive` method in your mailer. - -* Configure your email server to forward emails from the address(es) you would - like your app to receive to `/path/to/app/bin/rails runner - 'UserMailer.receive(STDIN.read)'`. - -Once a method called `receive` is defined in any mailer, Action Mailer will -parse the raw incoming email into an email object, decode it, instantiate a new -mailer, and pass the email object to the mailer `receive` instance -method. Here's an example: - -```ruby -class UserMailer < ApplicationMailer - def receive(email) - page = Page.find_by(address: email.to.first) - page.emails.create( - subject: email.subject, - body: email.body - ) - - if email.has_attachments? - email.attachments.each do |attachment| - page.attachments.create({ - file: attachment, - description: email.subject - }) - end - end - end -end -``` - Action Mailer Callbacks ---------------------------- +----------------------- Action Mailer allows for you to specify a `before_action`, `after_action` and `around_action`. @@ -882,7 +842,7 @@ class EmailDeliveryObserver end end ``` -Like interceptors, you need to register observers with the Action Mailer framework. You can do this in an initializer file +Like interceptors, you need to register observers with the Action Mailer framework. You can do this in an initializer file `config/initializers/email_delivery_observer.rb` ```ruby diff --git a/guides/source/action_text_overview.md b/guides/source/action_text_overview.md new file mode 100644 index 0000000000..08ec35e52a --- /dev/null +++ b/guides/source/action_text_overview.md @@ -0,0 +1,99 @@ +**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.** + +Action Text Overview +==================== + +This guide provides you with all you need to get started in handling +rich text content. + +After reading this guide, you will know: + +* How to configure Action Text. +* How to handle rich text content. +* How to style rich text content. + +-------------------------------------------------------------------------------- + +Introduction +------------ + +Action Text brings rich text content and editing to Rails. It includes +the [Trix editor](https://trix-editor.org) that handles everything from formatting +to links to quotes to lists to embedded images and galleries. +The rich text content generated by the Trix editor is saved in its own +RichText model that's associated with any existing Active Record model in the application. +Any embedded images (or other attachments) are automatically stored using +Active Storage and associated with the included RichText model. + +## Trix compared to other rich text editors + +Most WYSIWYG editors are wrappers around HTML’s `contenteditable` and `execCommand` APIs, +designed by Microsoft to support live editing of web pages in Internet Explorer 5.5, +and [eventually reverse-engineered](https://blog.whatwg.org/the-road-to-html-5-contenteditable#history) +and copied by other browsers. + +Because these APIs were never fully specified or documented, +and because WYSIWYG HTML editors are enormous in scope, each +browser's implementation has its own set of bugs and quirks, +and JavaScript developers are left to resolve the inconsistencies. + +Trix sidesteps these inconsistencies by treating contenteditable +as an I/O device: when input makes its way to the editor, Trix converts that input +into an editing operation on its internal document model, then re-renders +that document back into the editor. This gives Trix complete control over what +happens after every keystroke, and avoids the need to use execCommand at all. + +## Installation + +Run `rails action_text:install` to add the Yarn package and copy over the necessary migration. + +## Examples + +Adding a rich text field to an existing model: + +```ruby +# app/models/message.rb +class Message < ApplicationRecord + has_rich_text :content +end +``` + +Then refer to this field in the form for the model: + +```erb +<%# app/views/messages/_form.html.erb %> +<%= form_with(model: message) do |form| %> + <div class="field"> + <%= form.label :content %> + <%= form.rich_text_area :content %> + </div> +<% end %> +``` + +And finally display the sanitized rich text on a page: + +```erb +<%= @message.content %> +``` + +To accept the rich text content, all you have to do is permit the referenced attribute: + +```ruby +class MessagesController < ApplicationController + def create + message = Message.create! params.require(:message).permit(:title, :content) + redirect_to message + end +end +``` + +## Custom styling + +By default, the Action Text editor and content is styled by the Trix defaults. +If you want to change these defaults, you'll want to remove +the `app/assets/stylesheets/actiontext.css` linker and base your stylings on +the [contents of that file](https://raw.githubusercontent.com/basecamp/trix/master/dist/trix.css). + +You can also style the HTML used for embedded images and other attachments (known as blobs). +On installation, Action Text will copy over a partial to +`app/views/active_storage/blobs/_blob.html.erb`, which you can specialize. diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md index a67e2924d7..b0d4bbd2c0 100644 --- a/guides/source/active_record_basics.md +++ b/guides/source/active_record_basics.md @@ -105,9 +105,9 @@ depending on the purpose of these columns. fields that Active Record will look for when you create associations between your models. * **Primary keys** - By default, Active Record will use an integer column named - `id` as the table's primary key. When using [Active Record - Migrations](active_record_migrations.html) to create your tables, this column will be - automatically created. + `id` as the table's primary key (`bigint` for Postgres and MYSQL, `integer` + for SQLite). When using [Active Record Migrations](active_record_migrations.html) + to create your tables, this column will be automatically created. There are also some optional column names that will add additional features to Active Record instances: diff --git a/guides/source/active_storage_overview.md b/guides/source/active_storage_overview.md index 51f50e8931..e3bb41ae32 100644 --- a/guides/source/active_storage_overview.md +++ b/guides/source/active_storage_overview.md @@ -434,7 +434,7 @@ original blob into the specified format and redirect to its new service location. ```erb -<%= image_tag user.avatar.variant(resize_to_fit: [100, 100]) %> +<%= image_tag user.avatar.variant(resize_to_limit: [100, 100]) %> ``` To switch to the Vips processor, you would add the following to @@ -489,8 +489,7 @@ directly from the client to the cloud. Using the npm package: ```js - import * as ActiveStorage from "activestorage" - ActiveStorage.start() + require("@rails/activestorage").start() ``` 2. Annotate file inputs with the direct upload URL. @@ -616,7 +615,7 @@ of choice, instantiate a DirectUpload and call its create method. Create takes a callback to invoke when the upload completes. ```js -import { DirectUpload } from "activestorage" +import { DirectUpload } from "@rails/activestorage" const input = document.querySelector('input[type=file]') @@ -664,7 +663,7 @@ will call the object's `directUploadWillStoreFileWithXHR` method. You can then bind your own progress handler on the XHR. ```js -import { DirectUpload } from "activestorage" +import { DirectUpload } from "@rails/activestorage" class Uploader { constructor(file, url) { diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index 3db46bc42e..903f39994f 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -1201,6 +1201,8 @@ The `inquiry` method converts a string into a `StringInquirer` object making equ "active".inquiry.inactive? # => false ``` +NOTE: Defined in `active_support/core_ext/string/inquiry.rb`. + ### `starts_with?` and `ends_with?` Active Support defines 3rd person aliases of `String#start_with?` and `String#end_with?`: diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md index 5e68b3f400..c1c3832b79 100644 --- a/guides/source/active_support_instrumentation.md +++ b/guides/source/active_support_instrumentation.md @@ -255,13 +255,16 @@ Active Record ### sql.active_record -| Key | Value | -| ---------------- | ---------------------------------------- | -| `:sql` | SQL statement | -| `:name` | Name of the operation | -| `:connection_id` | `self.object_id` | -| `:binds` | Bind parameters | -| `:cached` | `true` is added when cached queries used | +| Key | Value | +| -------------------- | ---------------------------------------- | +| `:sql` | SQL statement | +| `:name` | Name of the operation | +| `:connection_id` | Object ID of the connection object | +| `:connection` | Connection object | +| `:binds` | Bind parameters | +| `:type_casted_binds` | Typecasted bind parameters | +| `:statement_name` | SQL Statement name | +| `:cached` | `true` is added when cached queries used | INFO. The adapters will add their own data as well. @@ -270,7 +273,10 @@ INFO. The adapters will add their own data as well. sql: "SELECT \"posts\".* FROM \"posts\" ", name: "Post Load", connection_id: 70307250813140, - binds: [] + connection: #<ActiveRecord::ConnectionAdapters::SQLite3Adapter:0x00007f9f7a838850>, + binds: [#<ActiveModel::Attribute::WithCastValue:0x00007fe19d15dc00>], + type_casted_binds: [11], + statement_name: nil } ``` @@ -291,32 +297,6 @@ INFO. The adapters will add their own data as well. Action Mailer ------------- -### receive.action_mailer - -| Key | Value | -| ------------- | -------------------------------------------- | -| `:mailer` | Name of the mailer class | -| `:message_id` | ID of the message, generated by the Mail gem | -| `:subject` | Subject of the mail | -| `:to` | To address(es) of the mail | -| `:from` | From address of the mail | -| `:bcc` | BCC addresses of the mail | -| `:cc` | CC addresses of the mail | -| `:date` | Date of the mail | -| `:mail` | The encoded form of the mail | - -```ruby -{ - mailer: "Notification", - message_id: "4f5b5491f1774_181b23fc3d4434d38138e5@mba.local.mail", - subject: "Rails Guides", - to: ["users@rails.com", "dhh@rails.com"], - from: ["me@rails.com"], - date: Sat, 10 Mar 2012 14:18:09 +0100, - mail: "..." # omitted for brevity -} -``` - ### deliver.action_mailer | Key | Value | diff --git a/guides/source/api_app.md b/guides/source/api_app.md index 85367c50e7..870f5f7b87 100644 --- a/guides/source/api_app.md +++ b/guides/source/api_app.md @@ -374,7 +374,7 @@ controller modules by default: - `ActionController::Renderers::All`: Support for `render :json` and friends. - `ActionController::ConditionalGet`: Support for `stale?`. - `ActionController::BasicImplicitRender`: Makes sure to return an empty response, if there isn't an explicit one. -- `ActionController::StrongParameters`: Support for parameters white-listing in combination with Active Model mass assignment. +- `ActionController::StrongParameters`: Support for parameters filtering in combination with Active Model mass assignment. - `ActionController::DataStreaming`: Support for `send_file` and `send_data`. - `AbstractController::Callbacks`: Support for `before_action` and similar helpers. diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 500e230ff9..e7faa5c330 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -1101,7 +1101,7 @@ Windows you have a JavaScript runtime installed in your operating system. -### Serving GZipped version of assets +### GZipping your assets By default, gzipped version of compiled assets will be generated, along with the non-gzipped version of assets. Gzipped assets help reduce the transmission @@ -1111,6 +1111,8 @@ of data over the wire. You can configure this by setting the `gzip` flag. config.assets.gzip = false # disable gzipped assets generation ``` +Refer to your web server's documentation for instructions on how to serve gzipped assets. + ### Using Your Own Compressor The compressor config settings for CSS and JavaScript also take any object. diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index 4f3e8b2cff..38e46e4bf8 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -1257,7 +1257,7 @@ Controls what happens to the associated object when its owner is destroyed: * `:destroy` causes the associated object to also be destroyed * `:delete` causes the associated object to be deleted directly from the database (so callbacks will not execute) -* `:nullify` causes the foreign key to be set to `NULL`. Callbacks are not executed. +* `:nullify` causes the foreign key to be set to `NULL`. Polymorphic type column is also nullified on polymorphic associations. Callbacks are not executed. * `:restrict_with_exception` causes an `ActiveRecord::DeleteRestrictionError` exception to be raised if there is an associated record * `:restrict_with_error` causes an error to be added to the owner if there is an associated object @@ -1658,7 +1658,7 @@ Controls what happens to the associated objects when their owner is destroyed: * `:destroy` causes all the associated objects to also be destroyed * `:delete_all` causes all the associated objects to be deleted directly from the database (so callbacks will not execute) -* `:nullify` causes the foreign keys to be set to `NULL`. Callbacks are not executed. +* `:nullify` causes the foreign key to be set to `NULL`. Polymorphic type column is also nullified on polymorphic associations. Callbacks are not executed. * `:restrict_with_exception` causes an `ActiveRecord::DeleteRestrictionError` exception to be raised if there are any associated records * `:restrict_with_error` causes an error to be added to the owner if there are any associated objects diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md index 3ac3f8fa8b..56c0ca78a0 100644 --- a/guides/source/caching_with_rails.md +++ b/guides/source/caching_with_rails.md @@ -51,10 +51,10 @@ For instance, it will not impact low-level caching, that we address ### Page Caching Page caching is a Rails mechanism which allows the request for a generated page -to be fulfilled by the webserver (i.e. Apache or NGINX) without having to go +to be fulfilled by the web server (i.e. Apache or NGINX) without having to go through the entire Rails stack. While this is super fast it can't be applied to every situation (such as pages that need authentication). Also, because the -webserver is serving a file directly from the filesystem you will need to +web server is serving a file directly from the filesystem you will need to implement cache expiration. INFO: Page Caching has been removed from Rails 4. See the [actionpack-page_caching gem](https://github.com/rails/actionpack-page_caching). diff --git a/guides/source/configuring.md b/guides/source/configuring.md index ae1de3079f..2911b1f7c3 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -387,28 +387,6 @@ The PostgreSQL adapter adds one additional configuration option: highly recommended that you do not enable this in a production environment. Defaults to `false` in all environments. -The SQLite3Adapter adapter adds one additional configuration option: - -* `ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer` -indicates whether boolean values are stored in sqlite3 databases as 1 and 0 or -'t' and 'f'. Leaving `ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer` -set to false is deprecated. SQLite databases have used 't' and 'f' to serialize -boolean values and must have old data converted to 1 and 0 (its native boolean -serialization) before setting this flag to true. Conversion can be accomplished -by setting up a Rake task which runs - - ```ruby - ExampleModel.where("boolean_column = 't'").update_all(boolean_column: 1) - ExampleModel.where("boolean_column = 'f'").update_all(boolean_column: 0) - ``` - - for all models and all boolean columns, after which the flag must be set to true -by adding the following to your `application.rb` file: - - ```ruby - Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true - ``` - The schema dumper adds two additional configuration options: * `ActiveRecord::SchemaDumper.ignore_tables` accepts an array of tables that should _not_ be included in any generated schema file. @@ -441,7 +419,7 @@ The schema dumper adds two additional configuration options: * `config.action_controller.per_form_csrf_tokens` configures whether CSRF tokens are only valid for the method/action they were generated for. -* `config.action_controller.default_protect_from_forgery` determines whether forgery protection is added on `ActionController:Base`. This is false by default, but enabled when loading defaults for Rails 5.2. +* `config.action_controller.default_protect_from_forgery` determines whether forgery protection is added on `ActionController:Base`. This is false by default. * `config.action_controller.relative_url_root` can be used to tell Rails that you are [deploying to a subdirectory](configuring.html#deploy-to-a-subdirectory-relative-url-root). The default is `ENV['RAILS_RELATIVE_URL_ROOT']`. @@ -619,6 +597,29 @@ Defaults to `'signed cookie'`. development mode, but for large test suites, disabling this option in the test environment can improve performance. This defaults to `true`. + +### Configuring Action Mailbox + +`config.action_mailbox` provides the following configuration options: + +* `config.action_mailbox.logger` contains the logger used by Action Mailbox. It accepts a logger conforming to the interface of Log4r or the default Ruby Logger class. The default is `Rails.logger`. + + ```ruby + config.action_mailbox.logger = ActiveSupport::Logger.new(STDOUT) + ``` + +* `config.action_mailbox.incinerate_after` accepts an `ActiveSupport::Duration` indicating how long after processing `ActionMailbox::InboundEmail` records should be destroyed. It defaults to `30.days`. + + ```ruby + # Incinerate inbound emails 14 days after processing. + config.action_mailbox.incinerate_after = 14.days + ``` + +* `config.action_mailbox.queues.incineration` accepts a symbol indicating the Active Job queue to use for incineration jobs. It defaults to `:action_mailbox_incineration`. + +* `config.action_mailbox.queues.routing` accepts a symbol indicating the Active Job queue to use for routing jobs. It defaults to `:action_mailbox_routing`. + + ### Configuring Action Mailer There are a number of settings available on `config.action_mailer`: @@ -698,6 +699,8 @@ There are a number of settings available on `config.action_mailer`: * `config.action_mailer.perform_caching` specifies whether the mailer templates should perform fragment caching or not. By default this is `false` in all environments. +* `config.action_mailer.delivery_job` specifies delivery job for mail. Defaults to `ActionMailer::DeliveryJob`. + ### Configuring Active Support @@ -715,7 +718,7 @@ There are a few configuration options available in Active Support: * `config.active_support.use_sha1_digests` specifies whether to use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header. Defaults to false. -* `config.active_support.use_authenticated_message_encryption` specifies whether to use AES-256-GCM authenticated encryption as the default cipher for encrypting messages instead of AES-256-CBC. This is false by default, but enabled when loading defaults for Rails 5.2. +* `config.active_support.use_authenticated_message_encryption` specifies whether to use AES-256-GCM authenticated encryption as the default cipher for encrypting messages instead of AES-256-CBC. This is false by default. * `ActiveSupport::Logger.silencer` is set to `false` to disable the ability to silence logging in a block. The default is `true`. @@ -815,15 +818,21 @@ normal Rails server. config.active_storage.paths[:ffprobe] = '/usr/local/bin/ffprobe' ``` -* `config.active_storage.variable_content_types` accepts an array of strings indicating the content types that Active Storage can transform through ImageMagick. The default is `%w(image/png image/gif image/jpg image/jpeg image/pjpeg image/vnd.adobe.photoshop image/vnd.microsoft.icon)`. +* `config.active_storage.variable_content_types` accepts an array of strings indicating the content types that Active Storage can transform through ImageMagick. The default is `%w(image/png image/gif image/jpg image/jpeg image/pjpeg image/tiff image/vnd.adobe.photoshop image/vnd.microsoft.icon)`. * `config.active_storage.content_types_to_serve_as_binary` accepts an array of strings indicating the content types that Active Storage will always serve as an attachment, rather than inline. The default is `%w(text/html text/javascript image/svg+xml application/postscript application/x-shockwave-flash text/xml application/xml application/xhtml+xml)`. -* `config.active_storage.queue` can be used to set the name of the Active Job queue used to perform jobs like analyzing the content of a blob or purging a blog. +* `config.active_storage.queues.analysis` accepts a symbol indicating the Active Job queue to use for analysis jobs. When this option is `nil`, analysis jobs are sent to the default Active Job queue (see `config.active_job.default_queue_name`). + + ```ruby + config.active_storage.queues.analysis = :low_priority + ``` + +* `config.active_storage.queues.purge` accepts a symbol indicating the Active Job queue to use for purge jobs. When this option is `nil`, purge jobs are sent to the default Active Job queue (see `config.active_job.default_queue_name`). ```ruby - config.active_storage.queue = :low_priority + config.active_storage.queues.purge = :low_priority ``` * `config.active_storage.logger` can be used to set the logger used by Active Storage. Accepts a logger conforming to the interface of Log4r or the default Ruby Logger class. @@ -847,6 +856,39 @@ text/javascript image/svg+xml application/postscript application/x-shockwave-fla The default is `/rails/active_storage` +### Results of `load_defaults` + +#### With '5.0': + +- `config.action_controller.per_form_csrf_tokens`: `true` +- `config.action_controller.forgery_protection_origin_check`: `true` +- `ActiveSupport.to_time_preserves_timezone`: `true` +- `config.active_record.belongs_to_required_by_default`: `true` +- `config.ssl_options`: `{ hsts: { subdomains: true } }` + +#### With '5.1': + +- `config.assets.unknown_asset_fallback`: `false` +- `config.action_view.form_with_generates_remote_forms`: `true` + +#### With '5.2': + +- `config.active_record.cache_versioning`: `true` +- `action_dispatch.use_authenticated_cookie_encryption`: `true` +- `config.active_support.use_authenticated_message_encryption`: `true` +- `config.active_support.use_sha1_digests`: `true` +- `config.action_controller.default_protect_from_forgery`: `true` +- `config.action_view.form_with_generates_ids`: `true` + +#### With '6.0': + +- `config.action_view.default_enforce_utf8`: `false` +- `config.action_dispatch.use_cookies_with_metadata`: `true` +- `config.action_mailer.delivery_job`: `"ActionMailer::MailDeliveryJob"` +- `config.active_job.return_false_on_aborted_enqueue`: `true` +- `config.active_storage.queues.analysis`: `:active_storage_analysis` +- `config.active_storage.queues.purge`: `:active_storage_purge` + ### Configuring a Database Just about every Rails application will interact with a database. You can connect to the database by setting an environment variable `ENV['DATABASE_URL']` or by using a configuration file called `config/database.yml`. @@ -1197,6 +1239,8 @@ Using Initializer Files After loading the framework and any gems in your application, Rails turns to loading initializers. An initializer is any Ruby file stored under `config/initializers` in your application. You can use initializers to hold configuration settings that should be made after all of the frameworks and gems are loaded, such as options to configure settings for these parts. +NOTE: There is no guarantee that your initializers will run after all the gem initializers, so any initialization code that depends on a given gem having been initialized should go into a `config.after_initialize` block. + NOTE: You can use subfolders to organize your initializers if you like, because Rails will look into the whole file hierarchy from the initializers folder on down. TIP: While Rails supports numbering of initializer file names for load ordering purposes, a better technique is to place any code that need to load in a specific order within the same file. This reduces file name churn, makes dependencies more explicit, and can help surface new concepts within your application. diff --git a/guides/source/development_dependencies_install.md b/guides/source/development_dependencies_install.md index b3baf726e3..e84e5561f2 100644 --- a/guides/source/development_dependencies_install.md +++ b/guides/source/development_dependencies_install.md @@ -227,7 +227,6 @@ If you're using another database, check the file `activerecord/test/config.yml` If you installed Yarn, you will need to install the javascript dependencies: ```bash -$ cd activestorage $ yarn install ``` diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml index 25c159d471..25e4fdb4e6 100644 --- a/guides/source/documents.yaml +++ b/guides/source/documents.yaml @@ -74,7 +74,17 @@ - name: Action Mailer Basics url: action_mailer_basics.html - description: This guide describes how to use Action Mailer to send and receive emails. + description: This guide describes how to use Action Mailer to send emails. + - + name: Action Mailbox Basics + work_in_progress: true + url: action_mailbox_basics.html + description: This guide describes how to use Action Mailbox to receive emails. + - + name: Action Text Overview + work_in_progress: true + url: action_text_overview.html + description: This guide describes how to use Action Text to handle rich text content. - name: Active Job Basics url: active_job_basics.html diff --git a/guides/source/engines.md b/guides/source/engines.md index 1e93a19c84..053e3aa16e 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -1091,16 +1091,15 @@ main Rails application. Engine model and controller classes can be extended by open classing them in the main Rails application (since model and controller classes are just Ruby classes that inherit Rails specific functionality). Open classing an Engine class -redefines it for use in the main application. This is usually implemented by -using the decorator pattern. +redefines it for use in the main application. For simple class modifications, use `Class#class_eval`. For complex class modifications, consider using `ActiveSupport::Concern`. -#### A note on Decorators and Loading Code +#### A note on Overriding and Loading Code -Because these decorators are not referenced by your Rails application itself, -Rails' autoloading system will not kick in and load your decorators. This means +Because these overrides are not referenced by your Rails application itself, +Rails' autoloading system will not kick in and load your overrides. This means that you need to require them yourself. Here is some sample code to do this: @@ -1112,7 +1111,7 @@ module Blorgh isolate_namespace Blorgh config.to_prepare do - Dir.glob(Rails.root + "app/decorators/**/*_decorator*.rb").each do |c| + Dir.glob(Rails.root + "app/overrides/**/*_override*.rb").each do |c| require_dependency(c) end end @@ -1120,15 +1119,15 @@ module Blorgh end ``` -This doesn't apply to just Decorators, but anything that you add in an engine +This doesn't apply to just overrides, but anything that you add in an engine that isn't referenced by your main application. -#### Implementing Decorator Pattern Using Class#class_eval +#### Reopening existing classes using Class#class_eval **Adding** `Article#time_since_created`: ```ruby -# MyApp/app/decorators/models/blorgh/article_decorator.rb +# MyApp/app/overrides/models/blorgh/article_override.rb Blorgh::Article.class_eval do def time_since_created @@ -1149,7 +1148,7 @@ end **Overriding** `Article#summary`: ```ruby -# MyApp/app/decorators/models/blorgh/article_decorator.rb +# MyApp/app/overrides/models/blorgh/article_override.rb Blorgh::Article.class_eval do def summary @@ -1169,7 +1168,7 @@ class Article < ApplicationRecord end ``` -#### Implementing Decorator Pattern Using ActiveSupport::Concern +#### Reopening existing classes using ActiveSupport::Concern Using `Class#class_eval` is great for simple adjustments, but for more complex class modifications, you might want to consider using [`ActiveSupport::Concern`] @@ -1498,6 +1497,8 @@ To hook into the initialization process of one of the following classes use the | Class | Available Hooks | | --------------------------------- | ------------------------------------ | | `ActionCable` | `action_cable` | +| `ActionCable::Channel::Base` | `action_cable_channel` | +| `ActionCable::Connection::Base` | `action_cable_connection` | | `ActionController::API` | `action_controller_api` | | `ActionController::API` | `action_controller` | | `ActionController::Base` | `action_controller_base` | @@ -1505,13 +1506,19 @@ To hook into the initialization process of one of the following classes use the | `ActionController::TestCase` | `action_controller_test_case` | | `ActionDispatch::IntegrationTest` | `action_dispatch_integration_test` | | `ActionDispatch::SystemTestCase` | `action_dispatch_system_test_case` | +| `ActionMailbox::Base` | `action_mailbox` | +| `ActionMailbox::InboundEmail` | `action_mailbox_inbound_email` | +| `ActionMailbox::TestCase` | `action_mailbox_test_case` | | `ActionMailer::Base` | `action_mailer` | | `ActionMailer::TestCase` | `action_mailer_test_case` | +| `ActionText::Content` | `action_text_content` | +| `ActionText::RichText` | `action_text_rich_text` | | `ActionView::Base` | `action_view` | | `ActionView::TestCase` | `action_view_test_case` | | `ActiveJob::Base` | `active_job` | | `ActiveJob::TestCase` | `active_job_test_case` | | `ActiveRecord::Base` | `active_record` | +| `ActiveStorage::Blob` | `active_storage_blob` | | `ActiveSupport::TestCase` | `active_support_test_case` | | `i18n` | `i18n` | diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index 264c94326e..be59cd0cfa 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -461,22 +461,19 @@ available, Rails will raise an exception. Let's look at the full error message again: ->ArticlesController#new is missing a template for this request format and variant. request.formats: ["text/html"] request.variant: [] NOTE! For XHR/Ajax or API requests, this action would normally respond with 204 No Content: an empty white screen. Since you're loading it in a web browser, we assume that you expected to actually render a template, not… nothing, so we're showing an error to be extra-clear. If you expect 204 No Content, carry on. That's what you'll get from an XHR or API request. Give it a shot. +>ArticlesController#new is missing a template for request formats: text/html -That's quite a lot of text! Let's quickly go through and understand what each -part of it means. +>NOTE! +>Unless told otherwise, Rails expects an action to render a template with the same name, contained in a folder named after its controller. If this controller is an API responding with 204 (No Content), which does not require a template, then this error will occur when trying to access it via browser, since we expect an HTML template to be rendered for such requests. If that's the case, carry on. -The first part identifies which template is missing. In this case, it's the +The message identifies which template is missing. In this case, it's the `articles/new` template. Rails will first look for this template. If not found, -then it will attempt to load a template called `application/new`. It looks for -one here because the `ArticlesController` inherits from `ApplicationController`. - -The next part of the message contains `request.formats` which specifies -the format of template to be served in response. It is set to `text/html` as we -requested this page via browser, so Rails is looking for an HTML template. -`request.variant` specifies what kind of physical devices would be served by -the response and helps Rails determine which template to use in the response. -It is empty because no information has been provided. +then it will attempt to load a template called `application/new`, because the +`ArticlesController` inherits from `ApplicationController`. + +Next the message contains `request.formats` which specifies the format of +template to be served in response. It is set to `text/html` as we requested +this page via browser, so Rails is looking for an HTML template. The simplest template that would work in this case would be one located at `app/views/articles/new.html.erb`. The extension of this file name is important: diff --git a/guides/source/i18n.md b/guides/source/i18n.md index 10b1a6de7e..d146685675 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -139,10 +139,12 @@ Note that appending directly to `I18n.load_paths` instead of to the application' ### Managing the Locale across Requests -The default locale is used for all translations unless `I18n.locale` is explicitly set. - A localized application will likely need to provide support for multiple locales. To accomplish this, the locale should be set at the beginning of each request so that all strings are translated using the desired locale during the lifetime of that request. +The default locale is used for all translations unless `I18n.locale=` or `I18n.with_locale` is used. + +`I18n.locale` can leak into subsequent requests served by the same thread/process if it is not consistently set in every controller. For example executing `I18n.locale = :es` in one POST requests will have effects for all later requests to controllers that don't set the locale, but only in that particular thread/process. For that reason, instead of `I18n.locale =` you can use `I18n.with_locale` which does not have this leak issue. + The locale can be set in an `around_action` in the `ApplicationController`: ```ruby @@ -299,7 +301,7 @@ A trivial implementation of using an `Accept-Language` header would be: def switch_locale(&action) logger.debug "* Accept-Language: #{request.env['HTTP_ACCEPT_LANGUAGE']}" locale = extract_locale_from_accept_language_header - logger.debug "* Locale set to '#{I18n.locale}'" + logger.debug "* Locale set to '#{locale}'" I18n.with_locale(locale, &action) end diff --git a/guides/source/index.html.erb b/guides/source/index.html.erb index 76f01fea0a..10e388774c 100644 --- a/guides/source/index.html.erb +++ b/guides/source/index.html.erb @@ -1,6 +1,5 @@ -<% content_for :page_title do %> -Ruby on Rails Guides -<% end %> +<% content_for :page_title, "Ruby on Rails Guides" %> +<% content_for :description, "Ruby on Rails Guides" %> <% content_for :header_section do %> <%= render 'welcome' %> diff --git a/guides/source/layout.html.erb b/guides/source/layout.html.erb index 1f42d72756..65a003fceb 100644 --- a/guides/source/layout.html.erb +++ b/guides/source/layout.html.erb @@ -3,7 +3,7 @@ <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> - <title><%= yield(:page_title) || 'Ruby on Rails Guides' %></title> + <title><%= yield(:page_title) %></title> <link rel="stylesheet" type="text/css" href="stylesheets/style.css" data-turbolinks-track="reload"> <link rel="stylesheet" type="text/css" href="stylesheets/print.css" media="print"> <link rel="stylesheet" type="text/css" href="stylesheets/syntaxhighlighter/shCore.css" data-turbolinks-track="reload"> @@ -14,6 +14,13 @@ <script src="javascripts/turbolinks.js" data-turbolinks-track="reload"></script> <script src="javascripts/guides.js" data-turbolinks-track="reload"></script> <script src="javascripts/responsive-tables.js" data-turbolinks-track="reload"></script> + <meta property="og:title" content="<%= yield(:page_title) %>" /> + <meta name="description" content="<%= yield(:description) %>" /> + <meta property="og:description" content="<%= yield(:description) %>" /> + <meta property="og:locale" content="en_US" /> + <meta property="og:site_name" content="Ruby on Rails Guides" /> + <meta property="og:image" content="https://avatars.githubusercontent.com/u/4223" /> + <meta property="og:type" content="website" /> </head> <body class="guide"> <% if @edge %> diff --git a/guides/source/rails_on_rack.md b/guides/source/rails_on_rack.md index c33851a0f9..69b5f254bf 100644 --- a/guides/source/rails_on_rack.md +++ b/guides/source/rails_on_rack.md @@ -35,7 +35,7 @@ application. Any Rack compliant web server should be using ### `rails server` -`rails server` does the basic job of creating a `Rack::Server` object and starting the webserver. +`rails server` does the basic job of creating a `Rack::Server` object and starting the web server. Here's how `rails server` creates an instance of `Rack::Server` diff --git a/guides/source/routing.md b/guides/source/routing.md index 0a0f1b6754..92d5b45e7d 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -260,7 +260,7 @@ In each of these cases, the named routes remain the same as if you did not use ` | PATCH/PUT | /admin/articles/:id | articles#update | article_path(:id) | | DELETE | /admin/articles/:id | articles#destroy | article_path(:id) | -TIP: _If you need to use a different controller namespace inside a `namespace` block you can specify an absolute controller path, e.g: `get '/foo' => '/foo#index'`._ +TIP: _If you need to use a different controller namespace inside a `namespace` block you can specify an absolute controller path, e.g: `get '/foo', to: '/foo#index'`._ ### Nested Resources @@ -519,7 +519,7 @@ resources :photos do end ``` -You can leave out the `:on` option, this will create the same member route except that the resource id value will be available in `params[:photo_id]` instead of `params[:id]`. +You can leave out the `:on` option, this will create the same member route except that the resource id value will be available in `params[:photo_id]` instead of `params[:id]`. Route helpers will also be renamed from `preview_photo_url` and `preview_photo_path` to `photo_preview_url` and `photo_preview_path`. #### Adding Collection Routes diff --git a/guides/source/security.md b/guides/source/security.md index dbec3cdd2d..a2fb4663cf 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -32,27 +32,17 @@ In order to develop secure web applications you have to keep up to date on all l Sessions -------- -A good place to start looking at security is with sessions, which can be vulnerable to particular attacks. +This chapter describes some particular attacks related to sessions, and security measures to protect your session data. ### What are Sessions? -NOTE: _HTTP is a stateless protocol. Sessions make it stateful._ +INFO: Sessions enable the application to maintain user-specific state, while users interact with the application. For example, sessions allow users to authenticate once and remain signed in for future requests. -Most applications need to keep track of certain state of a particular user. This could be the contents of a shopping basket or the user id of the currently logged in user. Without the idea of sessions, the user would have to identify, and probably authenticate, on every request. -Rails will create a new session automatically if a new user accesses the application. It will load an existing session if the user has already used the application. +Most applications need to keep track of state for users that interact with the application. This could be the contents of a shopping basket, or the user id of the currently logged in user. This kind of user-specific state can be stored in the session. -A session usually consists of a hash of values and a session ID, usually a 32-character string, to identify the hash. Every cookie sent to the client's browser includes the session ID. And the other way round: the browser will send it to the server on every request from the client. In Rails you can save and retrieve values using the session method: +Rails provides a session object for each user that accesses the application. If the user already has an active session, Rails uses the existing session. Otherwise a new session is created. -```ruby -session[:user_id] = @current_user.id -User.find(session[:user_id]) -``` - -### Session ID - -NOTE: _The session ID is a 32-character random hex string._ - -The session ID is generated using `SecureRandom.hex` which generates a random hex string using platform specific methods (such as OpenSSL, /dev/urandom or Win32 CryptoAPI) for generating cryptographically secure random numbers. Currently it is not feasible to brute-force Rails' session IDs. +NOTE: Read more about sessions and how to use them in [Action Controller Overview Guide](action_controller_overview.html#session). ### Session Hijacking @@ -76,35 +66,31 @@ Hence, the cookie serves as temporary authentication for the web application. An The main objective of most attackers is to make money. The underground prices for stolen bank login accounts range from 0.5%-10% of account balance, $0.5-$30 for credit card numbers ($20-$60 with full details), $0.1-$1.5 for identities (Name, SSN & DOB), $20-$50 for retailer accounts, and $6-$10 for cloud service provider accounts, according to the [Symantec Internet Security Threat Report (2017)](https://www.symantec.com/content/dam/symantec/docs/reports/istr-22-2017-en.pdf). -### Session Guidelines - -Here are some general guidelines on sessions. - -* _Do not store large objects in a session_. Instead you should store them in the database and save their id in the session. This will eliminate synchronization headaches and it won't fill up your session storage space (depending on what session storage you chose, see below). -This will also be a good idea, if you modify the structure of an object and old versions of it are still in some user's cookies. With server-side session storages you can clear out the sessions, but with client-side storages, this is hard to mitigate. - -* _Critical data should not be stored in session_. If the user clears their cookies or closes the browser, they will be lost. And with a client-side session storage, the user can read the data. +### Session Storage -### Encrypted Session Storage +NOTE: Rails uses `ActionDispatch::Session::CookieStore` as the default session storage. -NOTE: _Rails provides several storage mechanisms for the session hashes. The most important is `ActionDispatch::Session::CookieStore`._ +TIP: Learn more about other session storages in [Action Controller Overview Guide](action_controller_overview.html#session). -The `CookieStore` saves the session hash directly in a cookie on the -client-side. The server retrieves the session hash from the cookie and +Rails `CookieStore` saves the session hash in a cookie on the client-side. +The server retrieves the session hash from the cookie and eliminates the need for a session ID. That will greatly increase the speed of the application, but it is a controversial storage option and you have to think about the security implications and storage limitations of it: -* Cookies imply a strict size limit of 4kB. This is fine as you should - not store large amounts of data in a session anyway, as described - before. Storing the current user's database id in a session is common - practice. +* Cookies have a size limit of 4kB. Use cookies only for data which is relevant for the session. + +* Cookies are stored on the client-side. The client may preserve cookie contents even for expired cookies. The client may copy cookies to other machines. Avoid storing sensitive data in cookies. + +* Cookies are temporary by nature. The server can set expiration time for the cookie, but the client may delete the cookie and its contents before that. Persist all data that is of more permanent nature on the server side. * Session cookies do not invalidate themselves and can be maliciously reused. It may be a good idea to have your application invalidate old session cookies using a stored timestamp. +* Rails encrypts cookies by default. The client cannot read or edit the contents of the cookie, without breaking encryption. If you take appropriate care of your secrets, you can consider your cookies to be generally secured. + The `CookieStore` uses the [encrypted](http://api.rubyonrails.org/classes/ActionDispatch/Cookies/ChainedCookieJars.html#method-i-encrypted) cookie jar to provide a secure, encrypted location to store session @@ -114,32 +100,9 @@ verification key used for [signed](http://api.rubyonrails.org/classes/ActionDispatch/Cookies/ChainedCookieJars.html#method-i-signed) cookies, is derived from the `secret_key_base` configuration value. -As of Rails 5.2 encrypted cookies and sessions are protected using AES -GCM encryption. This form of encryption is a type of Authenticated -Encryption and couples authentication and encryption in single step -while also producing shorter ciphertexts as compared to other -algorithms previously used. The key for cookies encrypted with AES GCM -are derived using a salt value defined by the -`config.action_dispatch.authenticated_encrypted_cookie_salt` -configuration value. - -Prior to this version, encrypted cookies were secured using AES in CBC -mode with HMAC using SHA1 for authentication. The keys for this type of -encryption and for HMAC verification were derived via the salts defined -by `config.action_dispatch.encrypted_cookie_salt` and -`config.action_dispatch.encrypted_signed_cookie_salt` respectively. - -Prior to Rails version 4 in both versions 2 and 3, session cookies were -protected using only HMAC verification. As such, these session cookies -only provided integrity to their content because the actual session data -was stored in plaintext encoded as base64. This is how `signed` cookies -work in the current version of Rails. These kinds of cookies are still -useful for protecting the integrity of certain client-stored data and -information. - -__Do not use a trivial secret for the `secret_key_base`, i.e. a word -from a dictionary, or one which is shorter than 30 characters! Instead -use `rails secret` to generate secret keys!__ +TIP: Secrets must be long and random. Use `rails secret` to get new unique secrets. + +INFO: Learn more about [managing credentials later in this guide](security.html#custom-credentials) It is also important to use different salt values for encrypted and signed cookies. Using the same value for different salt configuration @@ -150,7 +113,7 @@ In test and development applications get a `secret_key_base` derived from the ap secret_key_base: 492f... -If you have received an application where the secret was exposed (e.g. an application whose source was shared), strongly consider changing the secret. +WARNING: If your application's secrets may have been exposed, strongly consider changing them. Changing `secret_key_base` will expire currently active sessions. ### Rotating Encrypted and Signed Cookies Configurations @@ -1204,23 +1167,18 @@ loaded inline `<script>` elements. Environmental Security ---------------------- -It is beyond the scope of this guide to inform you on how to secure your application code and environments. However, please secure your database configuration, e.g. `config/database.yml`, and your server-side secret, e.g. stored in `config/secrets.yml`. You may want to further restrict access, using environment-specific versions of these files and any others that may contain sensitive information. +It is beyond the scope of this guide to inform you on how to secure your application code and environments. However, please secure your database configuration, e.g. `config/database.yml`, master key for `credentials.yml`, and other unencrypted secrets. You may want to further restrict access, using environment-specific versions of these files and any others that may contain sensitive information. ### Custom credentials -Rails generates a `config/credentials.yml.enc` to store third-party credentials -within the repo. This is only viable because Rails encrypts the file with a master -key that's generated into a version control ignored `config/master.key` — Rails -will also look for that key in `ENV["RAILS_MASTER_KEY"]`. Rails also requires the -key to boot in production, so the credentials can be read. +Rails stores secrets in `config/credentials.yml.enc`, which is encrypted and hence cannot be edited directly. Rails uses `config/master.key` or alternatively looks for environment variable `ENV["RAILS_MASTER_KEY"]` to encrypt the credentials file. The credentials file can be stored in version control, as long as master key is kept safe. -To edit stored credentials use `rails credentials:edit`. +To add new secret to credentials, first run `rails secret` to get a new secret. Then run `rails credentials:edit` to edit credentials, and add the secret. Running `credentials:edit` creates new credentials file and master key, if they did not already exist. By default, this file contains the application's -`secret_key_base`, but it could also be used to store other credentials such as -access keys for external APIs. +`secret_key_base`, but it could also be used to store other credentials such as access keys for external APIs. -The credentials added to this file are accessible via `Rails.application.credentials`. +The secrets kept in credentials file are accessible via `Rails.application.credentials`. For example, with the following decrypted `config/credentials.yml.enc`: secret_key_base: 3b7cd727ee24e8444053437c36cc66c3 @@ -1235,6 +1193,11 @@ version: Rails.application.credentials.some_api_key! # => raises KeyError: :some_api_key is blank ``` + +TIP: Learn more about credentials with `rails credentials:help`. + +WARNING: Keep your master key safe. Do not commit your master key. + Dependency Management and CVEs ------------------------------ diff --git a/guides/source/testing.md b/guides/source/testing.md index f34f9d95f4..9667521f3b 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -33,11 +33,11 @@ Rails creates a `test` directory for you as soon as you create a Rails project u ```bash $ ls -F test -application_system_test_case.rb fixtures/ integration/ models/ test_helper.rb -controllers/ helpers/ mailers/ system/ +application_system_test_case.rb controllers/ helpers/ mailers/ system/ +channels/ fixtures/ integration/ models/ test_helper.rb ``` -The `helpers`, `mailers`, and `models` directories are meant to hold tests for view helpers, mailers, and models, respectively. The `controllers` directory is meant to hold tests for controllers, routes, and views. The `integration` directory is meant to hold tests for interactions between controllers. +The `helpers`, `mailers`, and `models` directories are meant to hold tests for view helpers, mailers, and models, respectively. The `channels` directory is meant to hold tests for Action Cable connection and channels. The `controllers` directory is meant to hold tests for controllers, routes, and views. The `integration` directory is meant to hold tests for interactions between controllers. The system test directory holds system tests, which are used for full browser testing of your application. System tests allow you to test your application @@ -1731,6 +1731,139 @@ class ProductTest < ActiveJob::TestCase end ``` +Testing Action Cable +-------------------- + +Since Action Cable is used at different levels inside your application, +you'll need to test both the channels, connection classes themselves, and that other +entities broadcast correct messages. + +### Connection Test Case + +By default, when you generate new Rails application with Action Cable, a test for the base connection class (`ApplicationCable::Connection`) is generated as well under `test/channels/application_cable` directory. + +Connection tests aim to check whether a connection's identifiers get assigned properly +or that any improper connection requests are rejected. Here is an example: + +```ruby +class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase + test "connects with params" do + # Simulate a connection opening by calling the `connect` method + connect params: { user_id: 42 } + + # You can access the Connection object via `connection` in tests + assert_equal connection.user_id, "42" + end + + test "rejects connection without params" do + # Use `assert_reject_connection` matcher to verify that + # connection is rejected + assert_reject_connection { connect } + end +end +``` + +You can also specify request cookies the same way you do in integration tests: + +```ruby +test "connects with cookies" do + cookies.signed[:user_id] = "42" + + connect + + assert_equal connection.user_id, "42" +end +``` + +See the API documentation for [`ActionCable::Connection::TestCase`](http://api.rubyonrails.org/classes/ActionCable/Connection/TestCase.html) for more information. + +### Channel Test Case + +By default, when you generate a channel, an associated test will be generated as well +under the `test/channels` directory. Here's an example test with a chat channel: + +```ruby +require "test_helper" + +class ChatChannelTest < ActionCable::Channel::TestCase + test "subscribes and stream for room" do + # Simulate a subscription creation by calling `subscribe` + subscribe room: "15" + + # You can access the Channel object via `subscription` in tests + assert subscription.confirmed? + assert_has_stream "chat_15" + end +end +``` + +This test is pretty simple and only asserts that the channel subscribes the connection to a particular stream. + +You can also specify the underlying connection identifiers. Here's an example test with a web notifications channel: + +```ruby +require "test_helper" + +class WebNotificationsChannelTest < ActionCable::Channel::TestCase + test "subscribes and stream for user" do + stub_connection current_user: users(:john) + + subscribe + + assert_has_stream_for users(:john) + end +end +``` + +See the API documentation for [`ActionCable::Channel::TestCase`](http://api.rubyonrails.org/classes/ActionCable/Channel/TestCase.html) for more information. + +### Custom Assertions And Testing Broadcasts Inside Other Components + +Action Cable ships with a bunch of custom assertions that can be used to lessen the verbosity of tests. For a full list of available assertions, see the API documentation for [`ActionCable::TestHelper`](http://api.rubyonrails.org/classes/ActionCable/TestHelper.html). + +It's a good practice to ensure that the correct message has been broadcasted inside other components (e.g. inside your controllers). This is precisely where +the custom assertions provided by Action Cable are pretty useful. For instance, +within a model: + +```ruby +require 'test_helper' + +class ProductTest < ActionCable::TestCase + test "broadcast status after charge" do + assert_broadcast_on("products:#{product.id}", type: "charged") do + product.charge(account) + end + end +end +``` + +If you want to test the broadcasting made with `Channel.broadcast_to`, you shoud use +`Channel.broadcasting_for` to generate an underlying stream name: + +```ruby +# app/jobs/chat_relay_job.rb +class ChatRelayJob < ApplicationJob + def perform_later(room, message) + ChatChannel.broadcast_to room, text: message + end +end + +# test/jobs/chat_relay_job_test.rb +require 'test_helper' + +class ChatRelayJobTest < ActiveJob::TestCase + include ActionCable::TestHelper + + test "broadcast message to room" do + room = rooms(:all) + + assert_broadcast_on(ChatChannel.broadcasting_for(room), text: "Hi!") do + ChatRelayJob.perform_now(room, "Hi!") + end + end +end +``` + Additional Testing Resources ---------------------------- diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 2682c6ffd7..1f30ce1971 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -97,6 +97,42 @@ If you require your cookies to be read by 5.2 and older, or you are still valida to allow you to rollback set `Rails.application.config.action_dispatch.use_cookies_with_metadata` to `false`. +### ActionCable javascript API Changes + +The ActionCable javascript package has been converted from CoffeeScript +to ES2015, and we now publish the source code in the npm distribution. + +This change includes some breaking changes to optional parts of the +ActionCable javascript API: + +- Configuration of the WebSocket adapter and logger adapter have been moved + from properties of `ActionCable` to properties of `ActionCable.adapters`. + If you are currently configuring these adapters you will need to make + these changes when upgrading: + + ```diff + - ActionCable.WebSocket = MyWebSocket + + ActionCable.adapters.WebSocket = MyWebSocket + ``` + ```diff + - ActionCable.logger = myLogger + + ActionCable.adapters.logger = myLogger + ``` + +- The `ActionCable.startDebugging()` and `ActionCable.stopDebugging()` + methods have been removed and replaced with the property + `ActionCable.logger.enabled`. If you are currently using these methods you + will need to make these changes when upgrading: + + ```diff + - ActionCable.startDebugging() + + ActionCable.logger.enabled = true + ``` + ```diff + - ActionCable.stopDebugging() + + ActionCable.logger.enabled = false + ``` + Upgrading from Rails 5.1 to Rails 5.2 ------------------------------------- diff --git a/package.json b/package.json index 88029de141..43055fa6bc 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "private": true, "workspaces": [ "actioncable", + "actiontext", "activestorage", "actionview", "tmp/templates/app_template", diff --git a/rails.gemspec b/rails.gemspec index ec3db2f77c..5f2f1dfdcc 100644 --- a/rails.gemspec +++ b/rails.gemspec @@ -30,6 +30,7 @@ Gem::Specification.new do |s| s.add_dependency "actioncable", version s.add_dependency "activestorage", version s.add_dependency "actionmailbox", version + s.add_dependency "actiontext", version s.add_dependency "railties", version s.add_dependency "bundler", ">= 1.3.0" diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 8e358b4293..19f4de8a1d 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,66 @@ +* Fix deeply nested namespace command printing. + + *Gannon McGibbon* + + +## Rails 6.0.0.beta1 (January 18, 2019) ## + +* Remove deprecated `after_bundle` helper inside plugins templates. + + *Rafael Mendonça França* + +* Remove deprecated support to old `config.ru` that use the application class as argument of `run`. + + *Rafael Mendonça França* + +* Remove deprecated `environment` argument from the rails commands. + + *Rafael Mendonça França* + +* Remove deprecated `capify!`. + + *Rafael Mendonça França* + +* Remove deprecated `config.secret_token`. + + *Rafael Mendonça França* + +* Seed database with inline ActiveJob job adapter. + + *Gannon McGibbon* + +* Add `rails db:system:change` command for changing databases. + + ``` + bin/rails db:system:change --to=postgresql + force config/database.yml + gsub Gemfile + ``` + + The change command copies a template `config/database.yml` with the target database adapter into your app, and replaces your database gem with the target database gem. + + *Gannon McGibbon* + +* Add `rails test:channels`. + + *bogdanvlviv* + +* Use original `bundler` environment variables during the process of generating a new rails project. + + *Marco Costa* + +* Send Active Storage analysis and purge jobs to dedicated queues by default. + + Analysis jobs now use the `:active_storage_analysis` queue, and purge jobs + now use the `:active_storage_purge` queue. This matches Action Mailbox, + which sends its jobs to dedicated queues by default. + + *George Claghorn* + +* Add `rails test:mailboxes`. + + *George Claghorn* + * Introduce guard against DNS rebinding attacks The `ActionDispatch::HostAuthorization` is a new middleware that prevent @@ -12,7 +75,7 @@ In other environments `Rails.application.config.hosts` is empty and no `Host` header checks will be done. If you want to guard against header - attacks on production, you have to manually whitelist the allowed hosts + attacks on production, you have to manually permit the allowed hosts with: Rails.application.config.hosts << "product.com" @@ -25,7 +88,7 @@ # `beta1.product.com`. Rails.application.config.hosts << /.*\.product\.com/ - A special case is supported that allows you to whitelist all sub-domains: + A special case is supported that allows you to permit all sub-domains: # Allow requests from subdomains like `www.product.com` and # `beta1.product.com`. @@ -59,12 +122,6 @@ *Gannon McGibbon* -* Add JSON support to rails properties route (`/rails/info/properties`). - - Now, `Rails::Info` properties may be accessed in JSON format at `/rails/info/properties.json`. - - *Yoshiyuki Hirano* - * Use Ids instead of memory addresses when displaying references in scaffold views. Fixes #29200. @@ -106,12 +163,20 @@ *Richard Schneeman* -* Support environment specific credentials file. +* Support environment specific credentials overrides. + + So any environment will look for `config/credentials/#{Rails.env}.yml.enc` and fall back + to `config/credentials.yml.enc`. + + The encryption key can be in `ENV["RAILS_MASTER_KEY"]` or `config/credentials/production.key`. + + Environment credentials overrides can be edited with `rails credentials:edit --environment production`. + If no override is setup for the passed environment, it will be created. + + Additionally, the default lookup paths can be overwritten with these configs: - For `production` environment look first for `config/credentials/production.yml.enc` file that can be decrypted by - `ENV["RAILS_MASTER_KEY"]` or `config/credentials/production.key` master key. - Edit given environment credentials file by command `rails credentials:edit --environment production`. - Default paths can be overwritten by setting `config.credentials.content_path` and `config.credentials.key_path`. + - `config.credentials.content_path` + - `config.credentials.key_path` *Wojciech Wnętrzak* diff --git a/railties/MIT-LICENSE b/railties/MIT-LICENSE index cce00cbc3a..ea8823d7ef 100644 --- a/railties/MIT-LICENSE +++ b/railties/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2018 David Heinemeier Hansson +Copyright (c) 2004-2019 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/railties/RDOC_MAIN.rdoc b/railties/RDOC_MAIN.rdoc index 89fc6bcbce..1c4252edd0 100644 --- a/railties/RDOC_MAIN.rdoc +++ b/railties/RDOC_MAIN.rdoc @@ -44,11 +44,11 @@ or to generate the body of an email. In \Rails, View generation is handled by {A {Active Record}[link:files/activerecord/README_rdoc.html], {Active Model}[link:files/activemodel/README_rdoc.html], {Action Pack}[link:files/actionpack/README_rdoc.html], and {Action View}[link:files/actionview/README_rdoc.html] can each be used independently outside \Rails. In addition to that, \Rails also comes with {Action Mailer}[link:files/actionmailer/README_rdoc.html], a library -to generate and send emails; {Active Job}[link:files/activejob/README_md.html], a -framework for declaring jobs and making them run on a variety of queueing +to generate and send emails; {Action Mailbox}[link:files/actionmailbox/README_md.html], a library to receive emails within a Rails application; +{Active Job}[link:files/activejob/README_md.html], a framework for declaring jobs and making them run on a variety of queueing backends; {Action Cable}[link:files/actioncable/README_md.html], a framework to integrate WebSockets with a \Rails application; {Active Storage}[link:files/activestorage/README_md.html], -a library to attach cloud and local files to \Rails applications; +a library to attach cloud and local files to \Rails applications; {Action Text}[link:files/actiontext/README_md.html], a library to handle rich text content; and {Active Support}[link:files/activesupport/README_rdoc.html], a collection of utility classes and standard library extensions that are useful for \Rails, and may also be used independently outside \Rails. diff --git a/railties/lib/minitest/rails_plugin.rb b/railties/lib/minitest/rails_plugin.rb index 6486fa1798..4b7df6360a 100644 --- a/railties/lib/minitest/rails_plugin.rb +++ b/railties/lib/minitest/rails_plugin.rb @@ -54,6 +54,6 @@ module Minitest end end - # Backwardscompatibility with Rails 5.0 generated plugin test scripts + # Backwards compatibility with Rails 5.0 generated plugin test scripts mattr_reader :run_via, default: {} end diff --git a/railties/lib/rails/all.rb b/railties/lib/rails/all.rb index 4d37f4b3bf..da810f1eed 100644 --- a/railties/lib/rails/all.rb +++ b/railties/lib/rails/all.rb @@ -13,6 +13,7 @@ require "rails" active_job/railtie action_cable/engine action_mailbox/engine + action_text/engine rails/test_unit/railtie sprockets/railtie ).each do |railtie| diff --git a/railties/lib/rails/api/task.rb b/railties/lib/rails/api/task.rb index aa83375363..d5312843e4 100644 --- a/railties/lib/rails/api/task.rb +++ b/railties/lib/rails/api/task.rb @@ -82,6 +82,14 @@ module Rails ) }, + "actiontext" => { + include: %w( + README.md + app/**/action_text/**/*.rb + lib/action_text/**/*.rb + ) + }, + "railties" => { include: %w( README.rdoc diff --git a/railties/lib/rails/app_loader.rb b/railties/lib/rails/app_loader.rb index aabcc5970c..cc057a407d 100644 --- a/railties/lib/rails/app_loader.rb +++ b/railties/lib/rails/app_loader.rb @@ -23,7 +23,7 @@ control: # too that you may or may not want (like yarn) If you already have Rails binstubs in source control, you might be -inadverently overwriting them during deployment by using bundle install +inadvertently overwriting them during deployment by using bundle install with the --binstubs option. If your application was created prior to Rails 4, here's how to upgrade: diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index acd97b64bf..5a924ab8e6 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -172,14 +172,9 @@ module Rails def key_generator # number of iterations selected based on consultation with the google security # team. Details at https://github.com/rails/rails/pull/6952#issuecomment-7661220 - @caching_key_generator ||= - if secret_key_base - ActiveSupport::CachingKeyGenerator.new( - ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000) - ) - else - ActiveSupport::LegacyKeyGenerator.new(secrets.secret_token) - end + @caching_key_generator ||= ActiveSupport::CachingKeyGenerator.new( + ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000) + ) end # Returns a message verifier object. @@ -254,7 +249,6 @@ module Rails super.merge( "action_dispatch.parameter_filter" => config.filter_parameters, "action_dispatch.redirect_filter" => config.filter_redirect, - "action_dispatch.secret_token" => secrets.secret_token, "action_dispatch.secret_key_base" => secret_key_base, "action_dispatch.show_exceptions" => config.action_dispatch.show_exceptions, "action_dispatch.show_detailed_exceptions" => config.consider_all_requests_local, @@ -404,14 +398,6 @@ module Rails # Fallback to config.secret_key_base if secrets.secret_key_base isn't set secrets.secret_key_base ||= config.secret_key_base - # Fallback to config.secret_token if secrets.secret_token isn't set - secrets.secret_token ||= config.secret_token - - if secrets.secret_token.present? - ActiveSupport::Deprecation.warn( - "`secrets.secret_token` is deprecated in favor of `secret_key_base` and will be removed in Rails 6.0." - ) - end secrets end @@ -587,7 +573,7 @@ module Rails secret_key_base elsif secret_key_base raise ArgumentError, "`secret_key_base` for #{Rails.env} environment must be a type of String`" - elsif secrets.secret_token.blank? + else raise ArgumentError, "Missing `secret_key_base` for '#{Rails.env}' environment, set this string with `rails credentials:edit`" end end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 22a82c051d..b7838f7e32 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -13,7 +13,7 @@ module Rails :cache_classes, :cache_store, :consider_all_requests_local, :console, :eager_load, :exceptions_app, :file_watcher, :filter_parameters, :force_ssl, :helpers_paths, :hosts, :logger, :log_formatter, :log_tags, - :railties_order, :relative_url_root, :secret_key_base, :secret_token, + :railties_order, :relative_url_root, :secret_key_base, :ssl_options, :public_file_server, :session_options, :time_zone, :reload_classes_only_on_change, :beginning_of_week, :filter_redirect, :x, :enable_dependency_loading, @@ -30,7 +30,7 @@ module Rails @filter_parameters = [] @filter_redirect = [] @helpers_paths = [] - @hosts = Array(([IPAddr.new("0.0.0.0/0"), IPAddr.new("::/0"), "localhost"] if Rails.env.development?)) + @hosts = Array(([IPAddr.new("0.0.0.0/0"), IPAddr.new("::/0"), ".localhost"] if Rails.env.development?)) @public_file_server = ActiveSupport::OrderedOptions.new @public_file_server.enabled = true @public_file_server.index_name = "index" @@ -50,7 +50,6 @@ module Rails @autoflush_log = true @log_formatter = ActiveSupport::Logger::SimpleFormatter.new @eager_load = nil - @secret_token = nil @secret_key_base = nil @api_only = false @debug_exception_response_format = nil @@ -97,10 +96,6 @@ module Rails if respond_to?(:active_record) active_record.cache_versioning = true - # Remove the temporary load hook from SQLite3Adapter when this is removed - ActiveSupport.on_load(:active_record_sqlite3adapter) do - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = true - end end if respond_to?(:action_dispatch) @@ -130,9 +125,18 @@ module Rails action_dispatch.use_cookies_with_metadata = true end + if respond_to?(:action_mailer) + action_mailer.delivery_job = "ActionMailer::MailDeliveryJob" + end + if respond_to?(:active_job) active_job.return_false_on_aborted_enqueue = true end + + if respond_to?(:active_storage) + active_storage.queues.analysis = :active_storage_analysis + active_storage.queues.purge = :active_storage_purge + end else raise "Unknown version #{target_version.to_s.inspect}" end @@ -284,25 +288,25 @@ module Rails end private - def credentials_available_for_current_env? - File.exist?("#{root}/config/credentials/#{Rails.env}.yml.enc") - end - def default_credentials_content_path if credentials_available_for_current_env? - File.join(root, "config", "credentials", "#{Rails.env}.yml.enc") + root.join("config", "credentials", "#{Rails.env}.yml.enc") else - File.join(root, "config", "credentials.yml.enc") + root.join("config", "credentials.yml.enc") end end def default_credentials_key_path if credentials_available_for_current_env? - File.join(root, "config", "credentials", "#{Rails.env}.key") + root.join("config", "credentials", "#{Rails.env}.key") else - File.join(root, "config", "master.key") + root.join("config", "master.key") end end + + def credentials_available_for_current_env? + File.exist?(root.join("config", "credentials", "#{Rails.env}.yml.enc")) + end end end end diff --git a/railties/lib/rails/code_statistics.rb b/railties/lib/rails/code_statistics.rb index 19d331ff30..09082282f3 100644 --- a/railties/lib/rails/code_statistics.rb +++ b/railties/lib/rails/code_statistics.rb @@ -95,8 +95,8 @@ class CodeStatistics #:nodoc: end def print_line(name, statistics) - m_over_c = (statistics.methods / statistics.classes) rescue m_over_c = 0 - loc_over_m = (statistics.code_lines / statistics.methods) - 2 rescue loc_over_m = 0 + m_over_c = (statistics.methods / statistics.classes) rescue 0 + loc_over_m = (statistics.code_lines / statistics.methods) - 2 rescue 0 print "| #{name.ljust(20)} " HEADERS.each_key do |k| diff --git a/railties/lib/rails/command/actions.rb b/railties/lib/rails/command/actions.rb index cbb743346b..50651ad61a 100644 --- a/railties/lib/rails/command/actions.rb +++ b/railties/lib/rails/command/actions.rb @@ -11,10 +11,20 @@ module Rails end def require_application_and_environment! + require_application! + require_environment! + end + + def require_application! require ENGINE_PATH if defined?(ENGINE_PATH) if defined?(APP_PATH) require APP_PATH + end + end + + def require_environment! + if defined?(APP_PATH) Rails.application.require_environment! end end diff --git a/railties/lib/rails/command/base.rb b/railties/lib/rails/command/base.rb index 766872de8a..a22b198c66 100644 --- a/railties/lib/rails/command/base.rb +++ b/railties/lib/rails/command/base.rb @@ -115,7 +115,7 @@ module Rails # For a Rails::Command::TestCommand placed in <tt>rails/command/test_command.rb</tt> # would return <tt>rails/test</tt>. def default_command_root - path = File.expand_path(File.join("../commands", command_root_namespace), __dir__) + path = File.expand_path(relative_command_path, __dir__) path if File.exist?(path) end @@ -135,12 +135,20 @@ module Rails end def command_root_namespace - (namespace.split(":") - %w( rails )).first + (namespace.split(":") - %w(rails)).join(":") + end + + def relative_command_path + File.join("../commands", *command_root_namespace.split(":")) end def namespaced_commands commands.keys.map do |key| - key == command_root_namespace ? key : "#{command_root_namespace}:#{key}" + if command_root_namespace.match?(/(\A|\:)#{key}\z/) + command_root_namespace + else + "#{command_root_namespace}:#{key}" + end end end end diff --git a/railties/lib/rails/command/environment_argument.rb b/railties/lib/rails/command/environment_argument.rb index 5dc98b113d..fdc5ee92d9 100644 --- a/railties/lib/rails/command/environment_argument.rb +++ b/railties/lib/rails/command/environment_argument.rb @@ -8,23 +8,13 @@ module Rails extend ActiveSupport::Concern included do - argument :environment, optional: true, banner: "environment" - class_option :environment, aliases: "-e", type: :string, desc: "Specifies the environment to run this console under (test/development/production)." end private def extract_environment_option_from_argument - if environment - self.options = options.merge(environment: acceptable_environment(environment)) - - ActiveSupport::Deprecation.warn "Passing the environment's name as a " \ - "regular argument is deprecated and " \ - "will be removed in the next Rails " \ - "version. Please, use the -e option " \ - "instead." - elsif options[:environment] + if options[:environment] self.options = options.merge(environment: acceptable_environment(options[:environment])) else self.options = options.merge(environment: Rails::Command.environment) diff --git a/railties/lib/rails/commands/credentials/USAGE b/railties/lib/rails/commands/credentials/USAGE index 6b33d1ab74..f7268a64d1 100644 --- a/railties/lib/rails/commands/credentials/USAGE +++ b/railties/lib/rails/commands/credentials/USAGE @@ -41,9 +41,18 @@ from leaking. === Environment Specific Credentials -It is possible to have credentials for each environment. If the file for current environment exists it will take -precedence over `config/credentials.yml.enc`, thus for `production` environment first look for -`config/credentials/production.yml.enc` that can be decrypted using master key taken from `ENV["RAILS_MASTER_KEY"]` -or stored in `config/credentials/production.key`. -To edit given file use command `rails credentials:edit --environment production` -Default paths can be overwritten by setting `config.credentials.content_path` and `config.credentials.key_path`. +The `credentials` command supports passing an `--environment` option to create an +environment specific override. That override will takes precedence over the +global `config/credentials.yml.enc` file when running in that environment. So: + + rails credentials:edit --environment development + +will create `config/credentials/development.yml.enc` with the corresponding +encryption key in `config/credentials/development.key` if the credentials file +doesn't exist. + +The encryption key can also be put in `ENV["RAILS_MASTER_KEY"]`, which takes +precedence over the file encryption key. + +In addition to that, the default credentials lookup paths can be overridden through +`config.credentials.content_path` and `config.credentials.key_path`. diff --git a/railties/lib/rails/commands/credentials/credentials_command.rb b/railties/lib/rails/commands/credentials/credentials_command.rb index 4b30d208e0..54ccd97506 100644 --- a/railties/lib/rails/commands/credentials/credentials_command.rb +++ b/railties/lib/rails/commands/credentials/credentials_command.rb @@ -20,17 +20,15 @@ module Rails end def edit - require_application_and_environment! + require_application! ensure_editor_available(command: "bin/rails credentials:edit") || (return) - encrypted = Rails.application.encrypted(content_path, key_path: key_path) - - ensure_encryption_key_has_been_added(key_path) if encrypted.key.nil? - ensure_encrypted_file_has_been_added(content_path, key_path) + ensure_encryption_key_has_been_added if credentials.key.nil? + ensure_credentials_have_been_added catch_editing_exceptions do - change_encrypted_file_in_system_editor(content_path, key_path) + change_credentials_in_system_editor end say "File encrypted and saved." @@ -39,38 +37,48 @@ module Rails end def show - require_application_and_environment! - - encrypted = Rails.application.encrypted(content_path, key_path: key_path) + require_application! - say encrypted.read.presence || missing_encrypted_message(key: encrypted.key, key_path: key_path, file_path: content_path) + say credentials.read.presence || missing_credentials_message end private - def content_path - options[:environment] ? "config/credentials/#{options[:environment]}.yml.enc" : "config/credentials.yml.enc" - end - - def key_path - options[:environment] ? "config/credentials/#{options[:environment]}.key" : "config/master.key" + def credentials + Rails.application.encrypted(content_path, key_path: key_path) end - - def ensure_encryption_key_has_been_added(key_path) + def ensure_encryption_key_has_been_added encryption_key_file_generator.add_key_file(key_path) encryption_key_file_generator.ignore_key_file(key_path) end - def ensure_encrypted_file_has_been_added(file_path, key_path) - encrypted_file_generator.add_encrypted_file_silently(file_path, key_path) + def ensure_credentials_have_been_added + encrypted_file_generator.add_encrypted_file_silently(content_path, key_path) end - def change_encrypted_file_in_system_editor(file_path, key_path) - Rails.application.encrypted(file_path, key_path: key_path).change do |tmp_path| + def change_credentials_in_system_editor + credentials.change do |tmp_path| system("#{ENV["EDITOR"]} #{tmp_path}") end end + def missing_credentials_message + if credentials.key.nil? + "Missing '#{key_path}' to decrypt credentials. See `rails credentials:help`" + else + "File '#{content_path}' does not exist. Use `rails credentials:edit` to change that." + end + end + + + def content_path + options[:environment] ? "config/credentials/#{options[:environment]}.yml.enc" : "config/credentials.yml.enc" + end + + def key_path + options[:environment] ? "config/credentials/#{options[:environment]}.key" : "config/master.key" + end + def encryption_key_file_generator require "rails/generators" @@ -85,14 +93,6 @@ module Rails Rails::Generators::EncryptedFileGenerator.new end - - def missing_encrypted_message(key:, key_path:, file_path:) - if key.nil? - "Missing '#{key_path}' to decrypt credentials. See `rails credentials:help`" - else - "File '#{file_path}' does not exist. Use `rails credentials:edit` to change that." - end - end end end end diff --git a/railties/lib/rails/commands/db/system/change/change_command.rb b/railties/lib/rails/commands/db/system/change/change_command.rb new file mode 100644 index 0000000000..760c229c07 --- /dev/null +++ b/railties/lib/rails/commands/db/system/change/change_command.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require "rails/generators" +require "rails/generators/rails/db/system/change/change_generator" + +module Rails + module Command + module Db + module System + class ChangeCommand < Base # :nodoc: + class_option :to, desc: "The database system to switch to." + + def perform + Rails::Generators::Db::System::ChangeGenerator.start + end + end + end + end + end +end diff --git a/railties/lib/rails/commands/server/server_command.rb b/railties/lib/rails/commands/server/server_command.rb index 70789e0303..47c3f05bb3 100644 --- a/railties/lib/rails/commands/server/server_command.rb +++ b/railties/lib/rails/commands/server/server_command.rb @@ -21,19 +21,6 @@ module Rails set_environment end - def app - @app ||= begin - app = super - if app.is_a?(Class) - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Using `Rails::Application` subclass to start the server is deprecated and will be removed in Rails 6.0. - Please change `run #{app}` to `run Rails.application` in config.ru. - MSG - end - app.respond_to?(:to_app) ? app.to_app : app - end - end - def opt_parser Options.new end @@ -302,9 +289,10 @@ module Rails MSG else suggestion = Rails::Command::Spellchecker.suggest(server, from: RACK_SERVERS) + suggestion_msg = "Maybe you meant #{suggestion.inspect}?" if suggestion <<~MSG - Could not find server "#{server}". Maybe you meant #{suggestion.inspect}? + Could not find server "#{server}". #{suggestion_msg} Run `rails server --help` for more options. MSG end diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index 6a13a84108..d6c329b581 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -473,9 +473,10 @@ module Rails # files inside eager_load paths. def eager_load! config.eager_load_paths.each do |load_path| - matcher = /\A#{Regexp.escape(load_path.to_s)}\/(.*)\.rb\Z/ + # Starts after load_path plus a slash, ends before ".rb". + relname_range = (load_path.to_s.length + 1)...-3 Dir.glob("#{load_path}/**/*.rb").sort.each do |file| - require_dependency file.sub(matcher, '\1') + require_dependency file[relname_range] end end end @@ -548,7 +549,7 @@ module Rails # Blog::Engine.load_seed def load_seed seed_file = paths["db/seeds.rb"].existent.first - load(seed_file) if seed_file + with_inline_jobs { load(seed_file) } if seed_file end # Add configured load paths to Ruby's load path, and remove duplicate entries. @@ -658,6 +659,18 @@ module Rails end end + def with_inline_jobs + queue_adapter = config.active_job.queue_adapter + ActiveSupport.on_load(:active_job) do + self.queue_adapter = :inline + end + yield + ensure + ActiveSupport.on_load(:active_job) do + self.queue_adapter = queue_adapter + end + end + def has_migrations? paths["db/migrate"].existent.any? end diff --git a/railties/lib/rails/gem_version.rb b/railties/lib/rails/gem_version.rb index 54bfbdd516..82df56559c 100644 --- a/railties/lib/rails/gem_version.rb +++ b/railties/lib/rails/gem_version.rb @@ -10,7 +10,7 @@ module Rails MAJOR = 6 MINOR = 0 TINY = 0 - PRE = "alpha" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index 5e8cebc50a..b835b3f3fd 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -23,6 +23,8 @@ module Rails autoload :ActiveModel, "rails/generators/active_model" autoload :Base, "rails/generators/base" autoload :Migration, "rails/generators/migration" + autoload :Database, "rails/generators/database" + autoload :AppName, "rails/generators/app_name" autoload :NamedBase, "rails/generators/named_base" autoload :ResourceHelpers, "rails/generators/resource_helpers" autoload :TestCase, "rails/generators/test_case" @@ -218,6 +220,7 @@ module Rails rails.delete("encryption_key_file") rails.delete("master_key") rails.delete("credentials") + rails.delete("db:system:change") hidden_namespaces.each { |n| groups.delete(n.to_s) } @@ -272,8 +275,10 @@ module Rails else options = sorted_groups.flat_map(&:last) suggestion = Rails::Command::Spellchecker.suggest(namespace.to_s, from: options) + suggestion_msg = "Maybe you meant #{suggestion.inspect}?" if suggestion + puts <<~MSG - Could not find generator '#{namespace}'. Maybe you meant #{suggestion.inspect}? + Could not find generator '#{namespace}'. #{suggestion_msg} Run `rails generate --help` for more options. MSG end diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb index 4646a55316..1a5f2ff203 100644 --- a/railties/lib/rails/generators/actions.rb +++ b/railties/lib/rails/generators/actions.rb @@ -8,7 +8,6 @@ module Rails def initialize(*) # :nodoc: super @indentation = 0 - @after_bundle_callbacks = [] end # Adds an entry into +Gemfile+ for the supplied gem. @@ -223,6 +222,7 @@ module Rails log :generate, what options = args.extract_options! + options[:without_rails_env] = true argument = args.flat_map(&:to_s).join(" ") execute_command :rails, "generate #{what} #{argument}", options @@ -248,15 +248,6 @@ module Rails execute_command :rails, command, options end - # Just run the capify command in root - # - # capify! - def capify! - ActiveSupport::Deprecation.warn("`capify!` is deprecated and will be removed in the next version of Rails.") - log :capify, "" - in_root { run("#{extify(:capify)} .", verbose: false) } - end - # Make an entry in Rails routing file <tt>config/routes.rb</tt> # # route "root 'welcome#index'" @@ -276,16 +267,6 @@ module Rails log File.read(find_in_source_paths(path)) end - # Registers a callback to be executed after bundle and spring binstubs - # have run. - # - # after_bundle do - # git add: '.' - # end - def after_bundle(&block) - @after_bundle_callbacks << block - end - private # Define log for backwards compatibility. If just one argument is sent, @@ -304,14 +285,15 @@ module Rails # based on the executor parameter provided. def execute_command(executor, command, options = {}) # :doc: log executor, command - env = options[:env] || ENV["RAILS_ENV"] || "development" + env = options[:env] || ENV["RAILS_ENV"] || "development" + rails_env = " RAILS_ENV=#{env}" unless options[:without_rails_env] sudo = options[:sudo] && !Gem.win_platform? ? "sudo " : "" config = { verbose: false } config[:capture] = options[:capture] if options[:capture] config[:abort_on_failure] = options[:abort_on_failure] if options[:abort_on_failure] - in_root { run("#{sudo}#{extify(executor)} #{command} RAILS_ENV=#{env}", config) } + in_root { run("#{sudo}#{extify(executor)} #{command}#{rails_env}", config) } end # Add an extension to the given name based on the platform. diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index f3b99ff937..0eed552042 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -11,9 +11,8 @@ require "active_support/core_ext/array/extract_options" module Rails module Generators class AppBase < Base # :nodoc: - DATABASES = %w( mysql postgresql sqlite3 oracle frontbase ibm_db sqlserver ) - JDBC_DATABASES = %w( jdbcmysql jdbcsqlite3 jdbcpostgresql jdbc ) - DATABASES.concat(JDBC_DATABASES) + include Database + include AppName attr_accessor :rails_template add_shebang_option! @@ -44,6 +43,12 @@ module Rails default: false, desc: "Skip Action Mailer files" + class_option :skip_action_mailbox, type: :boolean, default: false, + desc: "Skip Action Mailbox gem" + + class_option :skip_action_text, type: :boolean, default: false, + desc: "Skip Action Text gem" + class_option :skip_active_record, type: :boolean, aliases: "-O", default: false, desc: "Skip Active Record files" @@ -100,7 +105,6 @@ module Rails @gem_filter = lambda { |gem| true } @extra_entries = [] super - convert_database_option_for_jruby end private @@ -124,7 +128,7 @@ module Rails def gemfile_entries # :doc: [rails_gemfile_entry, database_gemfile_entry, - webserver_gemfile_entry, + web_server_gemfile_entry, assets_gemfile_entry, webpacker_gemfile_entry, javascript_gemfile_entry, @@ -185,7 +189,7 @@ module Rails "Use #{options[:database]} as the database for Active Record" end - def webserver_gemfile_entry # :doc: + def web_server_gemfile_entry # :doc: return [] if options[:skip_puma] comment = "Use Puma as the app server" GemfileEntry.new("puma", "~> 3.11", comment) @@ -200,7 +204,9 @@ module Rails :skip_sprockets, :skip_action_cable ), - skip_active_storage? + skip_active_storage?, + skip_action_mailbox?, + skip_action_text? ].flatten.none? end @@ -229,6 +235,14 @@ module Rails options[:skip_active_storage] || options[:skip_active_record] end + def skip_action_mailbox? # :doc: + options[:skip_action_mailbox] || skip_active_storage? + end + + def skip_action_text? # :doc: + options[:skip_action_text] || skip_active_storage? + end + class GemfileEntry < Struct.new(:name, :version, :comment, :options, :commented_out) def initialize(name, version, comment, options = {}, commented_out = false) super @@ -290,34 +304,6 @@ module Rails end end - def gem_for_database - # %w( mysql postgresql sqlite3 oracle frontbase ibm_db sqlserver jdbcmysql jdbcsqlite3 jdbcpostgresql ) - case options[:database] - when "mysql" then ["mysql2", [">= 0.4.4"]] - when "postgresql" then ["pg", [">= 0.18", "< 2.0"]] - when "oracle" then ["activerecord-oracle_enhanced-adapter", nil] - when "frontbase" then ["ruby-frontbase", nil] - when "sqlserver" then ["activerecord-sqlserver-adapter", nil] - when "jdbcmysql" then ["activerecord-jdbcmysql-adapter", nil] - when "jdbcsqlite3" then ["activerecord-jdbcsqlite3-adapter", nil] - when "jdbcpostgresql" then ["activerecord-jdbcpostgresql-adapter", nil] - when "jdbc" then ["activerecord-jdbc-adapter", nil] - else [options[:database], nil] - end - end - - def convert_database_option_for_jruby - if defined?(JRUBY_VERSION) - opt = options.dup - case opt[:database] - when "postgresql" then opt[:database] = "jdbcpostgresql" - when "mysql" then opt[:database] = "jdbcmysql" - when "sqlite3" then opt[:database] = "jdbcsqlite3" - end - self.options = opt.freeze - end - end - def assets_gemfile_entry return [] if options[:skip_sprockets] @@ -330,7 +316,7 @@ module Rails if options.dev? || options.edge? GemfileEntry.github "webpacker", "rails/webpacker", nil, "Use development version of Webpacker" else - GemfileEntry.new "webpacker", nil, "Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker" + GemfileEntry.version "webpacker", ">= 4.0.0.rc.3", "Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker" end end @@ -372,19 +358,21 @@ module Rails # its own vendored Thor, which could be a different version. Running both # things in the same process is a recipe for a night with paracetamol. # - # We unset temporary bundler variables to load proper bundler and Gemfile. - # # Thanks to James Tucker for the Gem tricks involved in this call. _bundle_command = Gem.bin_path("bundler", "bundle") require "bundler" - Bundler.with_clean_env do - full_command = %Q["#{Gem.ruby}" "#{_bundle_command}" #{command}] - if options[:quiet] - system(env, full_command, out: File::NULL) - else - system(env, full_command) - end + Bundler.with_original_env do + exec_bundle_command(_bundle_command, command, env) + end + end + + def exec_bundle_command(bundle_command, command, env) + full_command = %Q["#{Gem.ruby}" "#{bundle_command}" #{command}] + if options[:quiet] + system(env, full_command, out: File::NULL) + else + system(env, full_command) end end diff --git a/railties/lib/rails/generators/app_name.rb b/railties/lib/rails/generators/app_name.rb new file mode 100644 index 0000000000..c4f71694d8 --- /dev/null +++ b/railties/lib/rails/generators/app_name.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Rails + module Generators + module AppName # :nodoc: + RESERVED_NAMES = %w(application destroy plugin runner test) + + private + def app_name + @app_name ||= original_app_name.tr("-", "_") + end + + def original_app_name + @original_app_name ||= (defined_app_const_base? ? defined_app_name : File.basename(destination_root)).tr('\\', "").tr(". ", "_") + end + + def defined_app_name + defined_app_const_base.underscore + end + + def defined_app_const_base + Rails.respond_to?(:application) && defined?(Rails::Application) && + Rails.application.is_a?(Rails::Application) && Rails.application.class.name.chomp("::Application") + end + + alias :defined_app_const_base? :defined_app_const_base + + def app_const_base + @app_const_base ||= defined_app_const_base || app_name.gsub(/\W/, "_").squeeze("_").camelize + end + alias :camelized :app_const_base + + def app_const + @app_const ||= "#{app_const_base}::Application" + end + + def valid_const? + if /^\d/.match?(app_const) + raise Error, "Invalid application name #{original_app_name}. Please give a name which does not start with numbers." + elsif RESERVED_NAMES.include?(original_app_name) + raise Error, "Invalid application name #{original_app_name}. Please give a " \ + "name which does not match one of the reserved rails " \ + "words: #{RESERVED_NAMES.join(", ")}" + elsif Object.const_defined?(app_const_base) + raise Error, "Invalid application name #{original_app_name}, constant #{app_const_base} is already in use. Please choose another application name." + end + end + end + end +end diff --git a/railties/lib/rails/generators/database.rb b/railties/lib/rails/generators/database.rb new file mode 100644 index 0000000000..be3e61bde8 --- /dev/null +++ b/railties/lib/rails/generators/database.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Rails + module Generators + module Database # :nodoc: + JDBC_DATABASES = %w( jdbcmysql jdbcsqlite3 jdbcpostgresql jdbc ) + DATABASES = %w( mysql postgresql sqlite3 oracle frontbase ibm_db sqlserver ) + JDBC_DATABASES + + def initialize(*) + super + convert_database_option_for_jruby + end + + def gem_for_database(database = options[:database]) + case database + when "mysql" then ["mysql2", [">= 0.4.4"]] + when "postgresql" then ["pg", [">= 0.18", "< 2.0"]] + when "oracle" then ["activerecord-oracle_enhanced-adapter", nil] + when "frontbase" then ["ruby-frontbase", nil] + when "sqlserver" then ["activerecord-sqlserver-adapter", nil] + when "jdbcmysql" then ["activerecord-jdbcmysql-adapter", nil] + when "jdbcsqlite3" then ["activerecord-jdbcsqlite3-adapter", nil] + when "jdbcpostgresql" then ["activerecord-jdbcpostgresql-adapter", nil] + when "jdbc" then ["activerecord-jdbc-adapter", nil] + else [database, nil] + end + end + + def convert_database_option_for_jruby + if defined?(JRUBY_VERSION) + opt = options.dup + case opt[:database] + when "postgresql" then opt[:database] = "jdbcpostgresql" + when "mysql" then opt[:database] = "jdbcmysql" + when "sqlite3" then opt[:database] = "jdbcsqlite3" + end + self.options = opt.freeze + end + end + + private + def mysql_socket + @mysql_socket ||= [ + "/tmp/mysql.sock", # default + "/var/run/mysqld/mysqld.sock", # debian/gentoo + "/var/tmp/mysql.sock", # freebsd + "/var/lib/mysql/mysql.sock", # fedora + "/opt/local/lib/mysql/mysql.sock", # fedora + "/opt/local/var/run/mysqld/mysqld.sock", # mac + darwinports + mysql + "/opt/local/var/run/mysql4/mysqld.sock", # mac + darwinports + mysql4 + "/opt/local/var/run/mysql5/mysqld.sock", # mac + darwinports + mysql5 + "/opt/lampp/var/mysql/mysql.sock" # xampp for linux + ].find { |f| File.exist?(f) } unless Gem.win_platform? + end + end + end +end diff --git a/railties/lib/rails/generators/migration.rb b/railties/lib/rails/generators/migration.rb index 5081060895..b6ec0160cf 100644 --- a/railties/lib/rails/generators/migration.rb +++ b/railties/lib/rails/generators/migration.rb @@ -63,8 +63,7 @@ module Rails numbered_destination = File.join(dir, ["%migration_number%", base].join("_")) create_migration numbered_destination, nil, config do - match = ERB.version.match(/\Aerb\.rb \[(?<version>[^ ]+) /) - if match && match[:version] >= "2.2.0" # Ruby 2.6+ + if ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+ ERB.new(::File.binread(source), trim_mode: "-", eoutvar: "@output_buffer").result(context) else ERB.new(::File.binread(source), nil, "-", "@output_buffer").result(context) diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 33002790d4..f2f46d6e25 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -213,6 +213,7 @@ module Rails empty_directory_with_keep_file "test/helpers" empty_directory_with_keep_file "test/integration" + template "test/channels/application_cable/connection_test.rb" template "test/test_helper.rb" end @@ -241,7 +242,6 @@ module Rails # We need to store the RAILS_DEV_PATH in a constant, otherwise the path # can change in Ruby 1.8.7 when we FileUtils.cd. RAILS_DEV_PATH = File.expand_path("../../../../../..", __dir__) - RESERVED_NAMES = %w[application destroy plugin runner test] class AppGenerator < AppBase # :nodoc: WEBPACKS = %w( react vue angular elm stimulus ) @@ -268,7 +268,7 @@ module Rails super if !options[:skip_active_record] && !DATABASES.include?(options[:database]) - raise Error, "Invalid value for --database option. Supported for preconfiguration are: #{DATABASES.join(", ")}." + raise Error, "Invalid value for --database option. Supported preconfigurations are: #{DATABASES.join(", ")}." end # Force sprockets and yarn to be skipped when generating API only apps. @@ -276,6 +276,8 @@ module Rails if options[:api] self.options = options.merge(skip_sprockets: true, skip_javascript: true).freeze end + + @after_bundle_callbacks = [] end public_task :set_default_accessors! @@ -305,6 +307,13 @@ module Rails end remove_task :update_bin_files + def update_active_storage + unless skip_active_storage? + rails_command "active_storage:update" + end + end + remove_task :update_active_storage + def create_config_files build(:config) end @@ -440,6 +449,7 @@ module Rails if options[:skip_action_cable] remove_dir "app/javascript/channels" remove_dir "app/channels" + remove_dir "test/channels" end end @@ -489,58 +499,14 @@ module Rails create_file(*args, &block) end - def app_name - @app_name ||= original_app_name.tr("-", "_") - end - - def original_app_name - @original_app_name ||= (defined_app_const_base? ? defined_app_name : File.basename(destination_root)).tr('\\', "").tr(". ", "_") - end - - def defined_app_name - defined_app_const_base.underscore - end - - def defined_app_const_base - Rails.respond_to?(:application) && defined?(Rails::Application) && - Rails.application.is_a?(Rails::Application) && Rails.application.class.name.sub(/::Application$/, "") - end - - alias :defined_app_const_base? :defined_app_const_base - - def app_const_base - @app_const_base ||= defined_app_const_base || app_name.gsub(/\W/, "_").squeeze("_").camelize - end - alias :camelized :app_const_base - - def app_const - @app_const ||= "#{app_const_base}::Application" - end - - def valid_const? - if /^\d/.match?(app_const) - raise Error, "Invalid application name #{original_app_name}. Please give a name which does not start with numbers." - elsif RESERVED_NAMES.include?(original_app_name) - raise Error, "Invalid application name #{original_app_name}. Please give a " \ - "name which does not match one of the reserved rails " \ - "words: #{RESERVED_NAMES.join(", ")}" - elsif Object.const_defined?(app_const_base) - raise Error, "Invalid application name #{original_app_name}, constant #{app_const_base} is already in use. Please choose another application name." - end - end - - def mysql_socket - @mysql_socket ||= [ - "/tmp/mysql.sock", # default - "/var/run/mysqld/mysqld.sock", # debian/gentoo - "/var/tmp/mysql.sock", # freebsd - "/var/lib/mysql/mysql.sock", # fedora - "/opt/local/lib/mysql/mysql.sock", # fedora - "/opt/local/var/run/mysqld/mysqld.sock", # mac + darwinports + mysql - "/opt/local/var/run/mysql4/mysqld.sock", # mac + darwinports + mysql4 - "/opt/local/var/run/mysql5/mysqld.sock", # mac + darwinports + mysql5 - "/opt/lampp/var/mysql/mysql.sock" # xampp for linux - ].find { |f| File.exist?(f) } unless Gem.win_platform? + # Registers a callback to be executed after bundle and spring binstubs + # have run. + # + # after_bundle do + # git add: '.' + # end + def after_bundle(&block) # :doc: + @after_bundle_callbacks << block end def get_builder_class diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt index fb264935bd..d39b5d311f 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt @@ -18,17 +18,14 @@ ruby <%= "'#{RUBY_VERSION}'" -%> <% end -%> <% end -%> -# Use ActiveModel has_secure_password +# Use Active Model has_secure_password # gem 'bcrypt', '~> 3.1.7' <% unless skip_active_storage? -%> -# Use ActiveStorage variant +# Use Active Storage variant # gem 'image_processing', '~> 1.2' <% end -%> -# Use Capistrano for deployment -# gem 'capistrano-rails', group: :development - <% if depend_on_bootsnap? -%> # Reduces boot times through caching; required in config/boot.rb gem 'bootsnap', '>= 1.1.0', require: false diff --git a/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/consumer.js b/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/consumer.js index 76ca3d0f2f..0eceb59b18 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/consumer.js +++ b/railties/lib/rails/generators/rails/app/templates/app/javascript/channels/consumer.js @@ -1,6 +1,6 @@ // Action Cable provides the framework to deal with WebSockets in Rails. // You can generate new channels where WebSocket features live using the `rails generate channel` command. -import ActionCable from "actioncable" +import { createConsumer } from "@rails/actioncable" -export default ActionCable.createConsumer() +export default createConsumer() diff --git a/railties/lib/rails/generators/rails/app/templates/app/javascript/packs/application.js.tt b/railties/lib/rails/generators/rails/app/templates/app/javascript/packs/application.js.tt index 4d7a145cd6..908487d500 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/javascript/packs/application.js.tt +++ b/railties/lib/rails/generators/rails/app/templates/app/javascript/packs/application.js.tt @@ -3,19 +3,13 @@ // a relevant structure within app/javascript and only use these pack files to reference // that code so it'll be compiled. -import Rails from "rails-ujs" -Rails.start() +require("@rails/ujs").start() <%- unless options[:skip_turbolinks] -%> - -import Turbolinks from "turbolinks" -Turbolinks.start() +require("turbolinks").start() <%- end -%> <%- unless skip_active_storage? -%> - -import * as ActiveStorage from "activestorage" -ActiveStorage.start() +require("@rails/activestorage").start() <%- end -%> <%- unless options[:skip_action_cable] -%> - -import "channels" +require("channels") <%- end -%> diff --git a/railties/lib/rails/generators/rails/app/templates/config/application.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/application.rb.tt index 9a427113c7..1b0ee54071 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/application.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb.tt @@ -11,6 +11,8 @@ require "active_job/railtie" <%= comment_if :skip_active_storage %>require "active_storage/engine" require "action_controller/railtie" <%= comment_if :skip_action_mailer %>require "action_mailer/railtie" +<%= comment_if :skip_action_mailbox %>require "action_mailbox/engine" +<%= comment_if :skip_action_text %>require "action_text/engine" require "action_view/railtie" <%= comment_if :skip_action_cable %>require "action_cable/engine" <%= comment_if :skip_sprockets %>require "sprockets/railtie" diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml.tt index 33f422c622..6ab4a26084 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml.tt @@ -24,7 +24,7 @@ test: <<: *default database: <%= app_name %>_test -# As with config/secrets.yml, you never want to store sensitive information, +# As with config/credentials.yml, you never want to store sensitive information, # like your database password, in your source code. If your source code is # ever seen by anyone, they now have access to your database. # diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml.tt index 681c765e93..e422aa31fc 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml.tt @@ -60,7 +60,7 @@ test: <<: *default database: <%= app_name[0,4] %>_tst -# As with config/secrets.yml, you never want to store sensitive information, +# As with config/credentials.yml, you never want to store sensitive information, # like your database password, in your source code. If your source code is # ever seen by anyone, they now have access to your database. # diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml.tt index af69f12059..678455c622 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml.tt @@ -54,7 +54,7 @@ test: <<: *default url: jdbc:db://localhost/<%= app_name %>_test -# As with config/secrets.yml, you never want to store sensitive information, +# As with config/credentials.yml, you never want to store sensitive information, # like your database password, in your source code. If your source code is # ever seen by anyone, they now have access to your database. # diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml.tt index f39593372c..b5a0efef47 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml.tt @@ -27,7 +27,7 @@ test: <<: *default database: <%= app_name %>_test -# As with config/secrets.yml, you never want to store sensitive information, +# As with config/credentials.yml, you never want to store sensitive information, # like your database password, in your source code. If your source code is # ever seen by anyone, they now have access to your database. # diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml.tt index 2383fe97d3..009a81a6b8 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml.tt @@ -43,7 +43,7 @@ test: <<: *default database: <%= app_name %>_test -# As with config/secrets.yml, you never want to store sensitive information, +# As with config/credentials.yml, you never want to store sensitive information, # like your database password, in your source code. If your source code is # ever seen by anyone, they now have access to your database. # diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml.tt index b6c2e7448a..386eb511e5 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml.tt @@ -32,7 +32,7 @@ test: <<: *default database: <%= app_name %>_test -# As with config/secrets.yml, you never want to store sensitive information, +# As with config/credentials.yml, you never want to store sensitive information, # like your database password, in your source code. If your source code is # ever seen by anyone, they now have access to your database. # diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml.tt index 8d9d33ba6c..f7b6dfafab 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml.tt @@ -33,7 +33,7 @@ test: <<: *default database: <%= app_name %>_test -# As with config/secrets.yml, you never want to store sensitive information, +# As with config/credentials.yml, you never want to store sensitive information, # like your database password, in your source code. If your source code is # ever seen by anyone, they now have access to your database. # diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt index 2f51030756..44dafbd0c0 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt @@ -59,7 +59,7 @@ test: <<: *default database: <%= app_name %>_test -# As with config/secrets.yml, you never want to store sensitive information, +# As with config/credentials.yml, you never want to store sensitive information, # like your database password, in your source code. If your source code is # ever seen by anyone, they now have access to your database. # diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml.tt index 0246fb0d02..27e8f2f35d 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml.tt @@ -26,7 +26,7 @@ test: <<: *default database: <%= app_name %>_test -# As with config/secrets.yml, you never want to store sensitive information, +# As with config/credentials.yml, you never want to store sensitive information, # like your database password, in your source code. If your source code is # ever seen by anyone, they now have access to your database. # diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt index 3807c8a9aa..2887bc2d67 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt @@ -16,6 +16,7 @@ Rails.application.configure do # Run rails dev:cache to toggle caching. if Rails.root.join('tmp', 'caching-dev.txt').exist? config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true config.cache_store = :memory_store config.public_file_server.headers = { diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt index 08befd9196..94f7dd0c79 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt @@ -98,4 +98,24 @@ Rails.application.configure do # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false <%- end -%> + + # Inserts middleware to perform automatic connection switching. + # The `database_selector` hash is used to pass options to the DatabaseSelector + # middleware. The `delay` is used to determine how long to wait after a write + # to send a subsequent read to the primary. + # + # The `database_resolver` class is used by the middleware to determine which + # database is appropriate to use based on the time delay. + # + # The `database_operations` class is used by the middleware to set timestamps + # for the last write to the primary. The resolver uses the operations class + # timestamps to determine how long to wait before reading from the replica. + # + # By default Rails will store a last write timestamp in the session. The + # DatabaseSelector middleware is designed as such you can define your own + # strategy for connection switching and pass that into the middleware through + # these configuration options. + # config.active_record.database_selector = { delay: 2.seconds } + # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver + # config.active_record.database_operations = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session end diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt index fe48fc34ee..e92382f2d9 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt @@ -5,6 +5,10 @@ Rails.application.config.assets.version = '1.0' # Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path +<%- unless options[:skip_javascript] -%> +# Add Yarn node_modules folder to the asset load path. +Rails.application.config.assets.paths << Rails.root.join('node_modules') +<%- end -%> # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in the app/assets diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt index 5cca8ae570..d25552e923 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt @@ -6,7 +6,7 @@ # # Read the Guide for Upgrading Ruby on Rails for more info on each option. -# Don't force requests from old versions of IE to be UTF-8 encoded +# Don't force requests from old versions of IE to be UTF-8 encoded. # Rails.application.config.action_view.default_enforce_utf8 = false # Embed purpose and expiry metadata inside signed and encrypted @@ -16,5 +16,18 @@ # It's best enabled when your entire app is migrated and stable on 6.0. # Rails.application.config.action_dispatch.use_cookies_with_metadata = true -# Return false instead of self when #enqueue method was aborted from the callback -Rails.application.config.active_job.return_false_on_aborted_enqueue = true +# Return false instead of self when enqueuing is aborted from a callback. +# Rails.application.config.active_job.return_false_on_aborted_enqueue = true + +# Send Active Storage analysis and purge jobs to dedicated queues. +# Rails.application.config.active_storage.queues.analysis = :active_storage_analysis +# Rails.application.config.active_storage.queues.purge = :active_storage_purge + +# Use ActionMailer::MailDeliveryJob for sending parameterized and normal mail. +# +# The default delivery jobs (ActionMailer::Parameterized::DeliveryJob, ActionMailer::DeliveryJob), +# will be removed in Rails 6.1. This setting is not backwards compatible with earlier Rails versions. +# If you send mail in the background, job workers need to have a copy of +# MailDeliveryJob to ensure all delivery jobs are processed properly. +# Make sure your entire app is migrated and stable on 6.0 before using this setting. +# Rails.application.config.action_mailer.delivery_job = "ActionMailer::MailDeliveryJob" diff --git a/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt index f6146e7259..649253aeca 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt @@ -17,7 +17,7 @@ port ENV.fetch("PORT") { 3000 } environment ENV.fetch("RAILS_ENV") { "development" } # Specifies the number of `workers` to boot in clustered mode. -# Workers are forked webserver processes. If using threads and workers together +# Workers are forked web server processes. If using threads and workers together # the concurrency of the application would be max `threads` * `workers`. # Workers do not work on JRuby or Windows (both of which do not support # processes). diff --git a/railties/lib/rails/generators/rails/app/templates/package.json.tt b/railties/lib/rails/generators/rails/app/templates/package.json.tt index 7174116989..07207e1747 100644 --- a/railties/lib/rails/generators/rails/app/templates/package.json.tt +++ b/railties/lib/rails/generators/rails/app/templates/package.json.tt @@ -2,10 +2,10 @@ "name": "<%= app_name %>", "private": true, "dependencies": { - "rails-ujs": ">=5.2.1"<% unless options[:skip_turbolinks] %>, - "turbolinks": "5.1.1"<% end -%><% unless skip_active_storage? %>, - "activestorage": ">=5.2.1"<% end -%><% unless options[:skip_action_cable] %>, - "actioncable": ">=5.2.1"<% end %> + "@rails/ujs": "^6.0.0-alpha"<% unless options[:skip_turbolinks] %>, + "turbolinks": "^5.2.0"<% end -%><% unless skip_active_storage? %>, + "@rails/activestorage": "^6.0.0-alpha"<% end -%><% unless options[:skip_action_cable] %>, + "@rails/actioncable": "^6.0.0-alpha"<% end %> }, "version": "0.1.0" } diff --git a/railties/lib/rails/generators/rails/app/templates/test/channels/application_cable/connection_test.rb.tt b/railties/lib/rails/generators/rails/app/templates/test/channels/application_cable/connection_test.rb.tt new file mode 100644 index 0000000000..800405f15e --- /dev/null +++ b/railties/lib/rails/generators/rails/app/templates/test/channels/application_cable/connection_test.rb.tt @@ -0,0 +1,11 @@ +require "test_helper" + +class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase + # test "connects with cookies" do + # cookies.signed[:user_id] = 42 + # + # connect + # + # assert_equal connection.user_id, "42" + # end +end diff --git a/railties/lib/rails/generators/rails/db/system/change/change_generator.rb b/railties/lib/rails/generators/rails/db/system/change/change_generator.rb new file mode 100644 index 0000000000..63849eb18d --- /dev/null +++ b/railties/lib/rails/generators/rails/db/system/change/change_generator.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require "rails/generators/base" + +module Rails + module Generators + module Db + module System + class ChangeGenerator < Base # :nodoc: + include Database + include AppName + + class_option :to, required: true, + desc: "The database system to switch to." + + def self.default_generator_root + path = File.expand_path(File.join(base_name, "app"), base_root) + path if File.exist?(path) + end + + def initialize(*) + super + + unless DATABASES.include?(options[:to]) + raise Error, "Invalid value for --to option. Supported preconfigurations are: #{DATABASES.join(", ")}." + end + + opt = options.dup + opt[:database] ||= opt[:to] + self.options = opt.freeze + end + + def edit_database_config + template("config/databases/#{options[:database]}.yml", "config/database.yml") + end + + def edit_gemfile + database_gem_name, _ = gem_for_database + gsub_file("Gemfile", all_database_gems_regex, database_gem_name) + end + + private + def all_database_gems + DATABASES.map { |database| gem_for_database(database) } + end + + def all_database_gems_regex + all_database_gem_names = all_database_gems.map(&:first) + /(\b#{all_database_gem_names.join('\b|\b')}\b)/ + end + end + end + end + end +end diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb index 239b3a5739..79a06648b5 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -263,16 +263,6 @@ task default: :test public_task :apply_rails_template - def run_after_bundle_callbacks - unless @after_bundle_callbacks.empty? - ActiveSupport::Deprecation.warn("`after_bundle` is deprecated and will be removed in the next version of Rails. ") - end - - @after_bundle_callbacks.each do |callback| - callback.call - end - end - def name @name ||= begin # same as ActiveSupport::Inflector#underscore except not replacing '-' @@ -349,9 +339,9 @@ task default: :test def wrap_in_modules(unwrapped_code) unwrapped_code = "#{unwrapped_code}".strip.gsub(/\s$\n/, "") modules.reverse.inject(unwrapped_code) do |content, mod| - str = "module #{mod}\n" - str += content.lines.map { |line| " #{line}" }.join - str += content.present? ? "\nend" : "end" + str = +"module #{mod}\n" + str << content.lines.map { |line| " #{line}" }.join + str << (content.present? ? "\nend" : "end") end end diff --git a/railties/lib/rails/info.rb b/railties/lib/rails/info.rb index c68405619d..72b555ec19 100644 --- a/railties/lib/rails/info.rb +++ b/railties/lib/rails/info.rb @@ -54,10 +54,6 @@ module Rails table << "</table>" end end - - def to_json - Hash[properties].to_json - end end # The Rails version. diff --git a/railties/lib/rails/info_controller.rb b/railties/lib/rails/info_controller.rb index 14459623ac..f74d979721 100644 --- a/railties/lib/rails/info_controller.rb +++ b/railties/lib/rails/info_controller.rb @@ -14,16 +14,8 @@ class Rails::InfoController < Rails::ApplicationController # :nodoc: end def properties - respond_to do |format| - format.html do - @info = Rails::Info.to_html - @page_title = "Properties" - end - - format.json do - render json: Rails::Info.to_json - end - end + @info = Rails::Info.to_html + @page_title = "Properties" end def routes diff --git a/railties/lib/rails/tasks/framework.rake b/railties/lib/rails/tasks/framework.rake index 1a3711c446..2886986865 100644 --- a/railties/lib/rails/tasks/framework.rake +++ b/railties/lib/rails/tasks/framework.rake @@ -2,7 +2,7 @@ namespace :app do desc "Update configs and some other initially generated files (or use just update:configs or update:bin)" - task update: [ "update:configs", "update:bin", "update:upgrade_guide_info" ] + task update: [ "update:configs", "update:bin", "update:active_storage", "update:upgrade_guide_info" ] desc "Applies the template supplied by LOCATION=(/path/to/template) or URL" task template: :environment do @@ -51,6 +51,10 @@ namespace :app do Rails::AppUpdater.invoke_from_app_generator :update_bin_files end + task :active_storage do + Rails::AppUpdater.invoke_from_app_generator :update_active_storage + end + task :upgrade_guide_info do Rails::AppUpdater.invoke_from_app_generator :display_upgrade_guide_info end diff --git a/railties/lib/rails/tasks/statistics.rake b/railties/lib/rails/tasks/statistics.rake index 8cacf4a49f..5abba7d3b4 100644 --- a/railties/lib/rails/tasks/statistics.rake +++ b/railties/lib/rails/tasks/statistics.rake @@ -9,6 +9,7 @@ STATS_DIRECTORIES = [ %w(Jobs app/jobs), %w(Models app/models), %w(Mailers app/mailers), + %w(Mailboxes app/mailboxes), %w(Channels app/channels), %w(JavaScripts app/assets/javascripts), %w(JavaScript app/javascript), @@ -18,6 +19,8 @@ STATS_DIRECTORIES = [ %w(Helper\ tests test/helpers), %w(Model\ tests test/models), %w(Mailer\ tests test/mailers), + %w(Mailbox\ tests test/mailboxes), + %w(Channel\ tests test/channels), %w(Job\ tests test/jobs), %w(Integration\ tests test/integration), %w(System\ tests test/system), diff --git a/railties/lib/rails/tasks/yarn.rake b/railties/lib/rails/tasks/yarn.rake index 4fb8586b69..48a8d8e143 100644 --- a/railties/lib/rails/tasks/yarn.rake +++ b/railties/lib/rails/tasks/yarn.rake @@ -6,8 +6,7 @@ namespace :yarn do # Install only production deps when for not usual envs. valid_node_envs = %w[test development production] node_env = ENV.fetch("NODE_ENV") do - rails_env = ENV["RAILS_ENV"] - valid_node_envs.include?(rails_env) ? rails_env : "production" + valid_node_envs.include?(Rails.env) ? Rails.env : "production" end system({ "NODE_ENV" => node_env }, "#{Rails.root}/bin/yarn install --no-progress --frozen-lockfile") end diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake index 32ac27a135..3a1b62d9d1 100644 --- a/railties/lib/rails/test_unit/testing.rake +++ b/railties/lib/rails/test_unit/testing.rake @@ -28,7 +28,7 @@ namespace :test do desc "Run tests quickly, but also reset db" task db: %w[db:test:prepare test] - ["models", "helpers", "controllers", "mailers", "integration", "jobs"].each do |name| + ["models", "helpers", "channels", "controllers", "mailers", "integration", "jobs", "mailboxes"].each do |name| task name => "test:prepare" do $: << "test" Rails::TestUnit::Runner.rake_run(["test/#{name}"]) diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 886fb0f843..7006b0855f 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -331,7 +331,7 @@ module ApplicationTests assert_not_includes Post.instance_methods, :title end - test "eager loads attribute methods in production" do + test "does not eager load attribute methods in production when the schema cache is empty" do app_file "app/models/post.rb", <<-RUBY class Post < ActiveRecord::Base end @@ -354,9 +354,71 @@ module ApplicationTests app "production" + assert_not_includes Post.instance_methods, :title + end + + test "eager loads attribute methods in production when the schema cache is populated" do + app_file "app/models/post.rb", <<-RUBY + class Post < ActiveRecord::Base + end + RUBY + + app_file "config/initializers/active_record.rb", <<-RUBY + ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") + ActiveRecord::Migration.verbose = false + ActiveRecord::Schema.define(version: 1) do + create_table :posts do |t| + t.string :title + end + end + RUBY + + add_to_config <<-RUBY + config.eager_load = true + config.cache_classes = true + RUBY + + app_file "config/initializers/schema_cache.rb", <<-RUBY + ActiveRecord::Base.connection.schema_cache.add("posts") + RUBY + + app "production" + assert_includes Post.instance_methods, :title end + test "does not attempt to eager load attribute methods for models that aren't connected" do + app_file "app/models/post.rb", <<-RUBY + class Post < ActiveRecord::Base + end + RUBY + + app_file "config/initializers/active_record.rb", <<-RUBY + ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") + ActiveRecord::Migration.verbose = false + ActiveRecord::Schema.define(version: 1) do + create_table :posts do |t| + t.string :title + end + end + RUBY + + add_to_config <<-RUBY + config.eager_load = true + config.cache_classes = true + RUBY + + app_file "app/models/comment.rb", <<-RUBY + class Comment < ActiveRecord::Base + establish_connection(adapter: "mysql2", database: "does_not_exist") + end + RUBY + + assert_nothing_raised do + app "production" + end + end + test "initialize an eager loaded, cache classes app" do add_to_config <<-RUBY config.eager_load = true @@ -534,45 +596,6 @@ module ApplicationTests assert_equal "some_value", verifier.verify(message) end - test "application message verifier can be used when the key_generator is ActiveSupport::LegacyKeyGenerator" do - app_file "config/initializers/secret_token.rb", <<-RUBY - Rails.application.credentials.secret_key_base = nil - Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33" - RUBY - - app "production" - - assert_kind_of ActiveSupport::LegacyKeyGenerator, Rails.application.key_generator - message = app.message_verifier(:sensitive_value).generate("some_value") - assert_equal "some_value", Rails.application.message_verifier(:sensitive_value).verify(message) - end - - test "config.secret_token is deprecated" do - app_file "config/initializers/secret_token.rb", <<-RUBY - Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33" - RUBY - - app "production" - - assert_deprecated(/secret_token/) do - app.secrets - end - end - - test "secrets.secret_token is deprecated" do - app_file "config/secrets.yml", <<-YAML - production: - secret_token: "b3c631c314c0bbca50c1b2843150fe33" - YAML - - app "production" - - assert_deprecated(/secret_token/) do - app.secrets - end - end - - test "raises when secret_key_base is blank" do app_file "config/initializers/secret_token.rb", <<-RUBY Rails.application.credentials.secret_key_base = nil @@ -594,20 +617,6 @@ module ApplicationTests end end - test "prefer secrets.secret_token over config.secret_token" do - app_file "config/initializers/secret_token.rb", <<-RUBY - Rails.application.config.secret_token = "" - RUBY - app_file "config/secrets.yml", <<-YAML - development: - secret_token: 3b7cd727ee24e8444053437c36cc66c3 - YAML - - app "development" - - assert_equal "3b7cd727ee24e8444053437c36cc66c3", app.secrets.secret_token - end - test "application verifier can build different verifiers" do make_basic_app do |application| application.credentials.secret_key_base = "b3c631c314c0bbca50c1b2843150fe33" @@ -649,22 +658,6 @@ module ApplicationTests assert_equal "3b7cd727ee24e8444053437c36cc66c3", app.secrets.secret_key_base end - test "config.secret_token over-writes a blank secrets.secret_token" do - app_file "config/initializers/secret_token.rb", <<-RUBY - Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33" - RUBY - app_file "config/secrets.yml", <<-YAML - development: - secret_key_base: - secret_token: - YAML - - app "development" - - assert_equal "b3c631c314c0bbca50c1b2843150fe33", app.secrets.secret_token - assert_equal "b3c631c314c0bbca50c1b2843150fe33", app.config.secret_token - end - test "custom secrets saved in config/secrets.yml are loaded in app secrets" do app_file "config/secrets.yml", <<-YAML development: @@ -727,19 +720,6 @@ module ApplicationTests assert_equal "iaminallyoursecretkeybase", app.secrets.secret_key_base end - test "uses ActiveSupport::LegacyKeyGenerator as app.key_generator when secrets.secret_key_base is blank" do - app_file "config/initializers/secret_token.rb", <<-RUBY - Rails.application.credentials.secret_key_base = nil - Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33" - RUBY - - app "production" - - assert_equal "b3c631c314c0bbca50c1b2843150fe33", app.config.secret_token - assert_nil app.credentials.secret_key_base - assert_kind_of ActiveSupport::LegacyKeyGenerator, app.key_generator - end - test "that nested keys are symbolized the same as parents for hashes more than one level deep" do app_file "config/secrets.yml", <<-YAML development: @@ -1879,37 +1859,29 @@ module ApplicationTests assert_equal({}, Rails.application.config.my_custom_config) end - test "default SQLite3Adapter.represent_boolean_as_integer for 5.1 is false" do + test "represent_boolean_as_integer is deprecated" do remove_from_config '.*config\.load_defaults.*\n' - app_file "app/models/post.rb", <<-RUBY - class Post < ActiveRecord::Base - end + app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY + Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true RUBY - app "development" - force_lazy_load_hooks { Post } - - assert_not ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer - end - - test "default SQLite3Adapter.represent_boolean_as_integer for new installs is true" do app_file "app/models/post.rb", <<-RUBY class Post < ActiveRecord::Base end RUBY app "development" - force_lazy_load_hooks { Post } - - assert ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer + assert_deprecated do + force_lazy_load_hooks { Post } + end end - test "represent_boolean_as_integer should be able to set via config.active_record.sqlite3.represent_boolean_as_integer" do + test "represent_boolean_as_integer raises when the value is false" do remove_from_config '.*config\.load_defaults.*\n' app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY - Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true + Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = false RUBY app_file "app/models/post.rb", <<-RUBY @@ -1918,9 +1890,9 @@ module ApplicationTests RUBY app "development" - force_lazy_load_hooks { Post } - - assert ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer + assert_raises(RuntimeError) do + force_lazy_load_hooks { Post } + end end test "config_for containing ERB tags should evaluate" do @@ -2164,6 +2136,132 @@ module ApplicationTests assert_equal true, ActiveJob::Base.return_false_on_aborted_enqueue end + test "ActiveStorage.queues[:analysis] is :active_storage_analysis by default" do + app "development" + + assert_equal :active_storage_analysis, ActiveStorage.queues[:analysis] + end + + test "ActiveStorage.queues[:analysis] is nil without Rails 6 defaults" do + remove_from_config '.*config\.load_defaults.*\n' + + app "development" + + assert_nil ActiveStorage.queues[:analysis] + end + + test "ActiveStorage.queues[:purge] is :active_storage_purge by default" do + app "development" + + assert_equal :active_storage_purge, ActiveStorage.queues[:purge] + end + + test "ActiveStorage.queues[:purge] is nil without Rails 6 defaults" do + remove_from_config '.*config\.load_defaults.*\n' + + app "development" + + assert_nil ActiveStorage.queues[:purge] + end + + test "ActionMailbox.logger is Rails.logger by default" do + app "development" + + assert_equal Rails.logger, ActionMailbox.logger + end + + test "ActionMailbox.logger can be configured" do + app_file "lib/my_logger.rb", <<-RUBY + require "logger" + class MyLogger < ::Logger + end + RUBY + + add_to_config <<-RUBY + require "my_logger" + config.action_mailbox.logger = MyLogger.new(STDOUT) + RUBY + + app "development" + + assert_equal "MyLogger", ActionMailbox.logger.class.name + end + + test "ActionMailbox.incinerate_after is 30.days by default" do + app "development" + + assert_equal 30.days, ActionMailbox.incinerate_after + end + + test "ActionMailbox.incinerate_after can be configured" do + add_to_config <<-RUBY + config.action_mailbox.incinerate_after = 14.days + RUBY + + app "development" + + assert_equal 14.days, ActionMailbox.incinerate_after + end + + test "ActionMailbox.queues[:incineration] is :action_mailbox_incineration by default" do + app "development" + + assert_equal :action_mailbox_incineration, ActionMailbox.queues[:incineration] + end + + test "ActionMailbox.queues[:incineration] can be configured" do + add_to_config <<-RUBY + config.action_mailbox.queues.incineration = :another_queue + RUBY + + app "development" + + assert_equal :another_queue, ActionMailbox.queues[:incineration] + end + + test "ActionMailbox.queues[:routing] is :action_mailbox_routing by default" do + app "development" + + assert_equal :action_mailbox_routing, ActionMailbox.queues[:routing] + end + + test "ActionMailbox.queues[:routing] can be configured" do + add_to_config <<-RUBY + config.action_mailbox.queues.routing = :another_queue + RUBY + + app "development" + + assert_equal :another_queue, ActionMailbox.queues[:routing] + end + + test "ActionMailer::Base.delivery_job is ActionMailer::MailDeliveryJob by default" do + app "development" + + assert_equal ActionMailer::MailDeliveryJob, ActionMailer::Base.delivery_job + end + + test "ActionMailer::Base.delivery_job is ActionMailer::DeliveryJob in the 5.x defaults" do + remove_from_config '.*config\.load_defaults.*\n' + add_to_config 'config.load_defaults "5.2"' + + app "development" + + assert_equal ActionMailer::DeliveryJob, ActionMailer::Base.delivery_job + end + + test "ActionMailer::Base.delivery_job can be configured in the new framework defaults" do + remove_from_config '.*config\.load_defaults.*\n' + + app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY + Rails.application.config.action_mailer.delivery_job = "ActionMailer::MailDeliveryJob" + RUBY + + app "development" + + assert_equal ActionMailer::MailDeliveryJob, ActionMailer::Base.delivery_job + end + test "ActiveRecord::Base.filter_attributes should equal to filter_parameters" do app_file "config/initializers/filter_parameters_logging.rb", <<-RUBY Rails.application.config.filter_parameters += [ :password, :credit_card_number ] @@ -2191,6 +2289,11 @@ module ApplicationTests MESSAGE end + test "hosts include .localhost in development" do + app "development" + assert_includes Rails.application.config.hosts, ".localhost" + end + private def force_lazy_load_hooks yield # Tasty clarifying sugar, homie! We only need to reference a constant to load it. diff --git a/railties/test/application/console_test.rb b/railties/test/application/console_test.rb index e74daccbe7..b6270525f0 100644 --- a/railties/test/application/console_test.rb +++ b/railties/test/application/console_test.rb @@ -149,7 +149,7 @@ class FullStackConsoleTest < ActiveSupport::TestCase end def test_environment_option_and_irb_option - spawn_console("test -- --verbose") + spawn_console("-e test -- --verbose") write_prompt "a = 1", "a = 1" write_prompt "puts Rails.env", "puts Rails.env\r\ntest" diff --git a/railties/test/application/credentials_test.rb b/railties/test/application/credentials_test.rb new file mode 100644 index 0000000000..2f6b109b50 --- /dev/null +++ b/railties/test/application/credentials_test.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "env_helpers" + +class Rails::CredentialsTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation, EnvHelpers + + setup :build_app + teardown :teardown_app + + test "reads credentials from environment specific path" do + write_credentials_override(:production) + + app("production") + + assert_equal "revealed", Rails.application.credentials.mystery + end + + test "reads credentials from customized path and key" do + write_credentials_override(:staging) + add_to_env_config("production", "config.credentials.content_path = config.root.join('config/credentials/staging.yml.enc')") + add_to_env_config("production", "config.credentials.key_path = config.root.join('config/credentials/staging.key')") + + app("production") + + assert_equal "revealed", Rails.application.credentials.mystery + end + + test "reads credentials using environment variable key" do + write_credentials_override(:production, with_key: false) + + switch_env("RAILS_MASTER_KEY", credentials_key) do + app("production") + + assert_equal "revealed", Rails.application.credentials.mystery + end + end + + private + def write_credentials_override(name, with_key: true) + Dir.chdir(app_path) do + Dir.mkdir "config/credentials" + File.write "config/credentials/#{name}.key", credentials_key if with_key + + # secret_key_base: secret + # mystery: revealed + File.write "config/credentials/#{name}.yml.enc", + "vgvKu4MBepIgZ5VHQMMPwnQNsLlWD9LKmJHu3UA/8yj6x+3fNhz3DwL9brX7UA==--qLdxHP6e34xeTAiI--nrcAsleXuo9NqiEuhntAhw==" + end + end + + def credentials_key + "2117e775dc2024d4f49ddf3aeb585919" + end +end diff --git a/railties/test/application/middleware/remote_ip_test.rb b/railties/test/application/middleware/remote_ip_test.rb index 83cf8a27f7..515b32080e 100644 --- a/railties/test/application/middleware/remote_ip_test.rb +++ b/railties/test/application/middleware/remote_ip_test.rb @@ -12,7 +12,9 @@ module ApplicationTests remote_ip = nil env = Rack::MockRequest.env_for("/").merge(env).merge!( "action_dispatch.show_exceptions" => false, - "action_dispatch.key_generator" => ActiveSupport::LegacyKeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33") + "action_dispatch.key_generator" => ActiveSupport::CachingKeyGenerator.new( + ActiveSupport::KeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33", iterations: 1000) + ) ) endpoint = Proc.new do |e| diff --git a/railties/test/application/middleware/session_test.rb b/railties/test/application/middleware/session_test.rb index b25e56b625..479615c133 100644 --- a/railties/test/application/middleware/session_test.rb +++ b/railties/test/application/middleware/session_test.rb @@ -215,8 +215,6 @@ module ApplicationTests RUBY add_to_config <<-RUBY - secrets.secret_token = "3b7cd727ee24e8444053437c36cc66c4" - # Enable AEAD cookies config.action_dispatch.use_authenticated_cookie_encryption = true RUBY @@ -238,68 +236,6 @@ module ApplicationTests assert_equal 1, encryptor.decrypt_and_verify(last_response.body, purpose: "cookie._myapp_session")["foo"] end - test "session upgrading signature to encryption cookie store upgrades session to encrypted mode" do - app_file "config/routes.rb", <<-RUBY - Rails.application.routes.draw do - get ':controller(/:action)' - end - RUBY - - controller :foo, <<-RUBY - class FooController < ActionController::Base - def write_raw_session - # {"session_id"=>"1965d95720fffc123941bdfb7d2e6870", "foo"=>1} - cookies[:_myapp_session] = "BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJTE5NjVkOTU3MjBmZmZjMTIzOTQxYmRmYjdkMmU2ODcwBjsAVEkiCGZvbwY7AEZpBg==--315fb9931921a87ae7421aec96382f0294119749" - head :ok - end - - def write_session - session[:foo] = session[:foo] + 1 - head :ok - end - - def read_session - render plain: session[:foo] - end - - def read_encrypted_cookie - render plain: cookies.encrypted[:_myapp_session]['foo'] - end - - def read_raw_cookie - render plain: cookies[:_myapp_session] - end - end - RUBY - - add_to_config <<-RUBY - secrets.secret_token = "3b7cd727ee24e8444053437c36cc66c4" - - # Enable AEAD cookies - config.action_dispatch.use_authenticated_cookie_encryption = true - RUBY - - require "#{app_path}/config/environment" - - get "/foo/write_raw_session" - get "/foo/read_session" - assert_equal "1", last_response.body - - get "/foo/write_session" - get "/foo/read_session" - assert_equal "2", last_response.body - - get "/foo/read_encrypted_cookie" - assert_equal "2", last_response.body - - cipher = "aes-256-gcm" - secret = app.key_generator.generate_key("authenticated encrypted cookie") - encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher) - - get "/foo/read_raw_cookie" - assert_equal 2, encryptor.decrypt_and_verify(last_response.body, purpose: "cookie._myapp_session")["foo"] - end - test "session upgrading from AES-CBC-HMAC encryption to AES-GCM encryption" do app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do @@ -370,70 +306,6 @@ module ApplicationTests end end - test "session upgrading legacy signed cookies to new signed cookies" do - app_file "config/routes.rb", <<-RUBY - Rails.application.routes.draw do - get ':controller(/:action)' - end - RUBY - - controller :foo, <<-RUBY - class FooController < ActionController::Base - def write_raw_session - # {"session_id"=>"1965d95720fffc123941bdfb7d2e6870", "foo"=>1} - cookies[:_myapp_session] = "BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJTE5NjVkOTU3MjBmZmZjMTIzOTQxYmRmYjdkMmU2ODcwBjsAVEkiCGZvbwY7AEZpBg==--315fb9931921a87ae7421aec96382f0294119749" - head :ok - end - - def write_session - session[:foo] = session[:foo] + 1 - head :ok - end - - def read_session - render plain: session[:foo] - end - - def read_signed_cookie - render plain: cookies.signed[:_myapp_session]['foo'] - end - - def read_raw_cookie - render plain: cookies[:_myapp_session] - end - end - RUBY - - add_to_config <<-RUBY - secrets.secret_token = "3b7cd727ee24e8444053437c36cc66c4" - Rails.application.credentials.secret_key_base = nil - RUBY - - begin - old_rails_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "production" - - require "#{app_path}/config/environment" - - get "/foo/write_raw_session" - get "/foo/read_session" - assert_equal "1", last_response.body - - get "/foo/write_session" - get "/foo/read_session" - assert_equal "2", last_response.body - - get "/foo/read_signed_cookie" - assert_equal "2", last_response.body - - verifier = ActiveSupport::MessageVerifier.new(app.secrets.secret_token) - - get "/foo/read_raw_cookie" - assert_equal 2, verifier.verify(last_response.body, purpose: "cookie._myapp_session")["foo"] - ensure - ENV["RAILS_ENV"] = old_rails_env - end - end - test "calling reset_session on request does not trigger an error for API apps" do add_to_config "config.api_only = true" diff --git a/railties/test/application/multiple_applications_test.rb b/railties/test/application/multiple_applications_test.rb index d6c81c1fe2..432344bccc 100644 --- a/railties/test/application/multiple_applications_test.rb +++ b/railties/test/application/multiple_applications_test.rb @@ -165,12 +165,12 @@ module ApplicationTests app.config.some_setting = "a_different_setting" assert_equal "a_different_setting", app.config.some_setting, "The configuration's some_setting should be set." - new_config = Rails::Application::Configuration.new("root_of_application") + new_config = Rails::Application::Configuration.new(Pathname.new("root_of_application")) new_config.some_setting = "some_setting_dude" app.config = new_config assert_equal "some_setting_dude", app.config.some_setting, "The configuration's some_setting should have changed." - assert_equal "root_of_application", app.config.root, "The root should have changed to the new config's root." + assert_equal "root_of_application", app.config.root.to_s, "The root should have changed to the new config's root." assert_equal new_config, app.config, "The application's config should have changed to the new config." end end diff --git a/railties/test/application/rake/routes_test.rb b/railties/test/application/rake/routes_test.rb index 933c735078..9879d1f047 100644 --- a/railties/test/application/rake/routes_test.rb +++ b/railties/test/application/rake/routes_test.rb @@ -22,7 +22,8 @@ module ApplicationTests cart GET /cart(.:format) cart#show rails_amazon_inbound_emails POST /rails/action_mailbox/amazon/inbound_emails(.:format) action_mailbox/ingresses/amazon/inbound_emails#create rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create - rails_postfix_inbound_emails POST /rails/action_mailbox/postfix/inbound_emails(.:format) action_mailbox/ingresses/postfix/inbound_emails#create + rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create + rails_relay_inbound_emails POST /rails/action_mailbox/relay/inbound_emails(.:format) action_mailbox/ingresses/relay/inbound_emails#create rails_sendgrid_inbound_emails POST /rails/action_mailbox/sendgrid/inbound_emails(.:format) action_mailbox/ingresses/sendgrid/inbound_emails#create rails_mailgun_inbound_emails POST /rails/action_mailbox/mailgun/inbound_emails/mime(.:format) action_mailbox/ingresses/mailgun/inbound_emails#create rails_conductor_inbound_emails GET /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#index diff --git a/railties/test/application/server_test.rb b/railties/test/application/server_test.rb index ab9e910aed..f4bd09903a 100644 --- a/railties/test/application/server_test.rb +++ b/railties/test/application/server_test.rb @@ -18,20 +18,6 @@ module ApplicationTests teardown_app end - test "deprecate support of older `config.ru`" do - remove_file "config.ru" - app_file "config.ru", <<-RUBY - require_relative 'config/environment' - run AppTemplate::Application - RUBY - - server = Rails::Server.new(config: "#{app_path}/config.ru") - server.app - - log = File.read(Rails.application.config.paths["log"].first) - assert_match(/DEPRECATION WARNING: Using `Rails::Application` subclass to start the server is deprecated/, log) - end - test "restart rails server with custom pid file path" do skip "PTY unavailable" unless available_pty? diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb index 55ce72181f..0bdd2b314d 100644 --- a/railties/test/application/test_runner_test.rb +++ b/railties/test/application/test_runner_test.rb @@ -16,7 +16,7 @@ module ApplicationTests teardown_app end - def test_run_via_backwardscompatibility + def test_run_via_backwards_compatibility require "minitest/rails_plugin" assert_nothing_raised do @@ -98,6 +98,17 @@ module ApplicationTests end end + def test_run_channels + create_test_file :channels, "foo_channel" + create_test_file :channels, "bar_channel" + + rails("test:channels").tap do |output| + assert_match "FooChannelTest", output + assert_match "BarChannelTest", output + assert_match "2 runs, 2 assertions, 0 failures", output + end + end + def test_run_controllers create_test_file :controllers, "foo_controller" create_test_file :controllers, "bar_controller" @@ -131,6 +142,18 @@ module ApplicationTests end end + def test_run_mailboxes + create_test_file :mailboxes, "foo_mailbox" + create_test_file :mailboxes, "bar_mailbox" + create_test_file :models, "foo" + + rails("test:mailboxes").tap do |output| + assert_match "FooMailboxTest", output + assert_match "BarMailboxTest", output + assert_match "2 runs, 2 assertions, 0 failures", output + end + end + def test_run_functionals create_test_file :mailers, "foo_mailer" create_test_file :controllers, "bar_controller" @@ -155,11 +178,11 @@ module ApplicationTests end def test_run_all_suites - suites = [:models, :helpers, :unit, :controllers, :mailers, :functional, :integration, :jobs] + suites = [:models, :helpers, :unit, :channels, :controllers, :mailers, :functional, :integration, :jobs, :mailboxes] suites.each { |suite| create_test_file suite, "foo_#{suite}" } run_test_command("") .tap do |output| suites.each { |suite| assert_match "Foo#{suite.to_s.camelize}Test", output } - assert_match "8 runs, 8 assertions, 0 failures", output + assert_match "10 runs, 10 assertions, 0 failures", output end end @@ -719,6 +742,7 @@ module ApplicationTests def test_reset_sessions_before_rollback_on_system_tests app_file "test/system/reset_session_before_rollback_test.rb", <<-RUBY require "application_system_test_case" + require "selenium/webdriver" class ResetSessionBeforeRollbackTest < ApplicationSystemTestCase def teardown_fixtures @@ -747,6 +771,7 @@ module ApplicationTests def test_reset_sessions_on_failed_system_test_screenshot app_file "test/system/reset_sessions_on_failed_system_test_screenshot_test.rb", <<~RUBY require "application_system_test_case" + require "selenium/webdriver" class ResetSessionsOnFailedSystemTestScreenshotTest < ApplicationSystemTestCase ActionDispatch::SystemTestCase.class_eval do @@ -803,6 +828,7 @@ module ApplicationTests def test_system_tests_are_run_through_rake_test_when_given_in_TEST app_file "test/system/dummy_test.rb", <<-RUBY require "application_system_test_case" + require "selenium/webdriver" class DummyTest < ApplicationSystemTestCase test "something" do diff --git a/railties/test/command/base_test.rb b/railties/test/command/base_test.rb index a49ae8aae7..9132c8b4af 100644 --- a/railties/test/command/base_test.rb +++ b/railties/test/command/base_test.rb @@ -4,10 +4,12 @@ require "abstract_unit" require "rails/command" require "rails/commands/generate/generate_command" require "rails/commands/secrets/secrets_command" +require "rails/commands/db/system/change/change_command" class Rails::Command::BaseTest < ActiveSupport::TestCase test "printing commands" do assert_equal %w(generate), Rails::Command::GenerateCommand.printing_commands assert_equal %w(secrets:setup secrets:edit secrets:show), Rails::Command::SecretsCommand.printing_commands + assert_equal %w(db:system:change), Rails::Command::Db::System::ChangeCommand.printing_commands end end diff --git a/railties/test/commands/console_test.rb b/railties/test/commands/console_test.rb index 0b2fe204f8..1941c83d6d 100644 --- a/railties/test/commands/console_test.rb +++ b/railties/test/commands/console_test.rb @@ -94,28 +94,7 @@ class Rails::ConsoleTest < ActiveSupport::TestCase assert_match(/\sspecial-production\s/, output) end - def test_rails_env_is_production_when_first_argument_is_p - assert_deprecated do - start ["p"] - assert_match(/\sproduction\s/, output) - end - end - - def test_rails_env_is_test_when_first_argument_is_t - assert_deprecated do - start ["t"] - assert_match(/\stest\s/, output) - end - end - - def test_rails_env_is_development_when_argument_is_d - assert_deprecated do - start ["d"] - assert_match(/\sdevelopment\s/, output) - end - end - - def test_rails_env_is_dev_when_argument_is_dev_and_dev_env_is_present + def test_rails_env_is_dev_when_environment_option_is_dev_and_dev_env_is_present Rails::Command::ConsoleCommand.class_eval do alias_method :old_environments, :available_environments @@ -124,9 +103,7 @@ class Rails::ConsoleTest < ActiveSupport::TestCase end end - assert_deprecated do - assert_match("dev", parse_arguments(["dev"])[:environment]) - end + assert_match("dev", parse_arguments(["-e", "dev"])[:environment]) ensure Rails::Command::ConsoleCommand.class_eval do undef_method :available_environments diff --git a/railties/test/commands/credentials_test.rb b/railties/test/commands/credentials_test.rb index 7842b0db61..10d0ba1cae 100644 --- a/railties/test/commands/credentials_test.rb +++ b/railties/test/commands/credentials_test.rb @@ -63,6 +63,14 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase end end + test "edit command does not raise when an initializer tries to access non-existent credentials" do + app_file "config/initializers/raise_when_loaded.rb", <<-RUBY + Rails.application.credentials.missing_key! + RUBY + + assert_match(/access_key_id: 123/, run_edit_command(environment: "qa")) + end + test "show credentials" do assert_match(/access_key_id: 123/, run_show_command) end diff --git a/railties/test/commands/db_system_change_test.rb b/railties/test/commands/db_system_change_test.rb new file mode 100644 index 0000000000..2ff45a7878 --- /dev/null +++ b/railties/test/commands/db_system_change_test.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" +require "rails/command" +require "rails/commands/db/system/change/change_command" + +class Rails::Command::Db::System::ChangeCommandTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + setup { build_app } + + teardown { teardown_app } + + test "change to existing database" do + change_database(to: "sqlite3") + + output = change_database(to: "sqlite3") + + assert_match "identical config/database.yml", output + assert_match "gsub Gemfile", output + end + + test "change to invalid database" do + output = change_database(to: "invalid-db") + + assert_match <<~MSG.squish, output + Invalid value for --to option. + Supported preconfigurations are: + mysql, postgresql, sqlite3, oracle, frontbase, + ibm_db, sqlserver, jdbcmysql, jdbcsqlite3, + jdbcpostgresql, jdbc. + MSG + end + + test "change to postgresql" do + output = change_database(to: "postgresql") + + assert_match "force config/database.yml", output + assert_match "gsub Gemfile", output + end + + test "change to mysql" do + output = change_database(to: "mysql") + + assert_match "force config/database.yml", output + assert_match "gsub Gemfile", output + end + + test "change to sqlite3" do + change_database(to: "postgresql") + output = change_database(to: "sqlite3") + + assert_match "force config/database.yml", output + assert_match "gsub Gemfile", output + end + + private + def change_database(to:, **options) + args = ["--to", to] + rails "db:system:change", args, **options + end +end diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb index ce048ac527..65f6916acb 100644 --- a/railties/test/commands/dbconsole_test.rb +++ b/railties/test/commands/dbconsole_test.rb @@ -99,28 +99,12 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase ENV["RACK_ENV"] = nil end - def test_rails_env_is_development_when_argument_is_dev - assert_deprecated do - stub_available_environments([ "development", "test" ]) do - assert_match("development", parse_arguments([ "dev" ])[:environment]) - end - end - end - def test_rails_env_is_development_when_environment_option_is_dev stub_available_environments([ "development", "test" ]) do assert_match("development", parse_arguments([ "-e", "dev" ])[:environment]) end end - def test_rails_env_is_dev_when_argument_is_dev_and_dev_env_is_present - assert_deprecated do - stub_available_environments([ "dev" ]) do - assert_match("dev", parse_arguments([ "dev" ])[:environment]) - end - end - end - def test_mysql start(adapter: "mysql2", database: "db") assert_not aborted @@ -128,9 +112,9 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase end def test_mysql_full - start(adapter: "mysql2", database: "db", host: "locahost", port: 1234, socket: "socket", username: "user", password: "qwerty", encoding: "UTF-8") + start(adapter: "mysql2", database: "db", host: "localhost", port: 1234, socket: "socket", username: "user", password: "qwerty", encoding: "UTF-8") assert_not aborted - assert_equal [%w[mysql mysql5], "--host=locahost", "--port=1234", "--socket=socket", "--user=user", "--default-character-set=UTF-8", "-p", "db"], dbconsole.find_cmd_and_exec_args + assert_equal [%w[mysql mysql5], "--host=localhost", "--port=1234", "--socket=socket", "--user=user", "--default-character-set=UTF-8", "-p", "db"], dbconsole.find_cmd_and_exec_args end def test_mysql_include_password @@ -265,14 +249,14 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase stdout = capture(:stdout) do Rails::Command.invoke(:dbconsole, ["-h"]) end - assert_match(/rails dbconsole \[environment\]/, stdout) + assert_match(/rails dbconsole \[options\]/, stdout) end def test_print_help_long stdout = capture(:stdout) do Rails::Command.invoke(:dbconsole, ["--help"]) end - assert_match(/rails dbconsole \[environment\]/, stdout) + assert_match(/rails dbconsole \[options\]/, stdout) end attr_reader :aborted, :output diff --git a/railties/test/commands/routes_test.rb b/railties/test/commands/routes_test.rb index a43a6d32b9..b4f927060e 100644 --- a/railties/test/commands/routes_test.rb +++ b/railties/test/commands/routes_test.rb @@ -18,15 +18,15 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase RUBY assert_equal <<~OUTPUT, run_routes_command([ "-c", "PostController" ]) - Prefix Verb URI Pattern Controller#Action - new_post GET /post/new(.:format) posts#new - edit_post GET /post/edit(.:format) posts#edit - post GET /post(.:format) posts#show - PATCH /post(.:format) posts#update - PUT /post(.:format) posts#update - DELETE /post(.:format) posts#destroy - POST /post(.:format) posts#create - rails_postfix_inbound_emails POST /rails/action_mailbox/postfix/inbound_emails(.:format) action_mailbox/ingresses/postfix/inbound_emails#create + Prefix Verb URI Pattern Controller#Action + new_post GET /post/new(.:format) posts#new + edit_post GET /post/edit(.:format) posts#edit + post GET /post(.:format) posts#show + PATCH /post(.:format) posts#update + PUT /post(.:format) posts#update + DELETE /post(.:format) posts#destroy + POST /post(.:format) posts#create + rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create OUTPUT assert_equal <<~OUTPUT, run_routes_command([ "-c", "UserPermissionController" ]) @@ -64,7 +64,8 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase POST /cart(.:format) cart#create rails_amazon_inbound_emails POST /rails/action_mailbox/amazon/inbound_emails(.:format) action_mailbox/ingresses/amazon/inbound_emails#create rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create - rails_postfix_inbound_emails POST /rails/action_mailbox/postfix/inbound_emails(.:format) action_mailbox/ingresses/postfix/inbound_emails#create + rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create + rails_relay_inbound_emails POST /rails/action_mailbox/relay/inbound_emails(.:format) action_mailbox/ingresses/relay/inbound_emails#create rails_sendgrid_inbound_emails POST /rails/action_mailbox/sendgrid/inbound_emails(.:format) action_mailbox/ingresses/sendgrid/inbound_emails#create rails_mailgun_inbound_emails POST /rails/action_mailbox/mailgun/inbound_emails/mime(.:format) action_mailbox/ingresses/mailgun/inbound_emails#create POST /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#create @@ -131,15 +132,15 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase OUTPUT assert_equal <<~OUTPUT, run_routes_command([ "-c", "PostController" ]) - Prefix Verb URI Pattern Controller#Action - new_admin_post GET /admin/post/new(.:format) admin/posts#new - edit_admin_post GET /admin/post/edit(.:format) admin/posts#edit - admin_post GET /admin/post(.:format) admin/posts#show - PATCH /admin/post(.:format) admin/posts#update - PUT /admin/post(.:format) admin/posts#update - DELETE /admin/post(.:format) admin/posts#destroy - POST /admin/post(.:format) admin/posts#create - rails_postfix_inbound_emails POST /rails/action_mailbox/postfix/inbound_emails(.:format) action_mailbox/ingresses/postfix/inbound_emails#create + Prefix Verb URI Pattern Controller#Action + new_admin_post GET /admin/post/new(.:format) admin/posts#new + edit_admin_post GET /admin/post/edit(.:format) admin/posts#edit + admin_post GET /admin/post(.:format) admin/posts#show + PATCH /admin/post(.:format) admin/posts#update + PUT /admin/post(.:format) admin/posts#update + DELETE /admin/post(.:format) admin/posts#destroy + POST /admin/post(.:format) admin/posts#create + rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create OUTPUT expected_permission_output = <<~OUTPUT @@ -167,7 +168,8 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase Prefix Verb URI Pattern Controller#Action rails_amazon_inbound_emails POST /rails/action_mailbox/amazon/inbound_emails(.:format) action_mailbox/ingresses/amazon/inbound_emails#create rails_mandrill_inbound_emails POST /rails/action_mailbox/mandrill/inbound_emails(.:format) action_mailbox/ingresses/mandrill/inbound_emails#create - rails_postfix_inbound_emails POST /rails/action_mailbox/postfix/inbound_emails(.:format) action_mailbox/ingresses/postfix/inbound_emails#create + rails_postmark_inbound_emails POST /rails/action_mailbox/postmark/inbound_emails(.:format) action_mailbox/ingresses/postmark/inbound_emails#create + rails_relay_inbound_emails POST /rails/action_mailbox/relay/inbound_emails(.:format) action_mailbox/ingresses/relay/inbound_emails#create rails_sendgrid_inbound_emails POST /rails/action_mailbox/sendgrid/inbound_emails(.:format) action_mailbox/ingresses/sendgrid/inbound_emails#create rails_mailgun_inbound_emails POST /rails/action_mailbox/mailgun/inbound_emails/mime(.:format) action_mailbox/ingresses/mailgun/inbound_emails#create rails_conductor_inbound_emails GET /rails/conductor/action_mailbox/inbound_emails(.:format) rails/conductor/action_mailbox/inbound_emails#index @@ -215,86 +217,91 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase URI | /rails/action_mailbox/mandrill/inbound_emails(.:format) Controller#Action | action_mailbox/ingresses/mandrill/inbound_emails#create --[ Route 4 ]-------------- - Prefix | rails_postfix_inbound_emails + Prefix | rails_postmark_inbound_emails Verb | POST - URI | /rails/action_mailbox/postfix/inbound_emails(.:format) - Controller#Action | action_mailbox/ingresses/postfix/inbound_emails#create + URI | /rails/action_mailbox/postmark/inbound_emails(.:format) + Controller#Action | action_mailbox/ingresses/postmark/inbound_emails#create --[ Route 5 ]-------------- + Prefix | rails_relay_inbound_emails + Verb | POST + URI | /rails/action_mailbox/relay/inbound_emails(.:format) + Controller#Action | action_mailbox/ingresses/relay/inbound_emails#create + --[ Route 6 ]-------------- Prefix | rails_sendgrid_inbound_emails Verb | POST URI | /rails/action_mailbox/sendgrid/inbound_emails(.:format) Controller#Action | action_mailbox/ingresses/sendgrid/inbound_emails#create - --[ Route 6 ]-------------- + --[ Route 7 ]-------------- Prefix | rails_mailgun_inbound_emails Verb | POST URI | /rails/action_mailbox/mailgun/inbound_emails/mime(.:format) Controller#Action | action_mailbox/ingresses/mailgun/inbound_emails#create - --[ Route 7 ]-------------- + --[ Route 8 ]-------------- Prefix | rails_conductor_inbound_emails Verb | GET URI | /rails/conductor/action_mailbox/inbound_emails(.:format) Controller#Action | rails/conductor/action_mailbox/inbound_emails#index - --[ Route 8 ]-------------- + --[ Route 9 ]-------------- Prefix | Verb | POST URI | /rails/conductor/action_mailbox/inbound_emails(.:format) Controller#Action | rails/conductor/action_mailbox/inbound_emails#create - --[ Route 9 ]-------------- + --[ Route 10 ]------------- Prefix | new_rails_conductor_inbound_email Verb | GET URI | /rails/conductor/action_mailbox/inbound_emails/new(.:format) Controller#Action | rails/conductor/action_mailbox/inbound_emails#new - --[ Route 10 ]------------- + --[ Route 11 ]------------- Prefix | edit_rails_conductor_inbound_email Verb | GET URI | /rails/conductor/action_mailbox/inbound_emails/:id/edit(.:format) Controller#Action | rails/conductor/action_mailbox/inbound_emails#edit - --[ Route 11 ]------------- + --[ Route 12 ]------------- Prefix | rails_conductor_inbound_email Verb | GET URI | /rails/conductor/action_mailbox/inbound_emails/:id(.:format) Controller#Action | rails/conductor/action_mailbox/inbound_emails#show - --[ Route 12 ]------------- + --[ Route 13 ]------------- Prefix | Verb | PATCH URI | /rails/conductor/action_mailbox/inbound_emails/:id(.:format) Controller#Action | rails/conductor/action_mailbox/inbound_emails#update - --[ Route 13 ]------------- + --[ Route 14 ]------------- Prefix | Verb | PUT URI | /rails/conductor/action_mailbox/inbound_emails/:id(.:format) Controller#Action | rails/conductor/action_mailbox/inbound_emails#update - --[ Route 14 ]------------- + --[ Route 15 ]------------- Prefix | Verb | DELETE URI | /rails/conductor/action_mailbox/inbound_emails/:id(.:format) Controller#Action | rails/conductor/action_mailbox/inbound_emails#destroy - --[ Route 15 ]------------- + --[ Route 16 ]------------- Prefix | rails_conductor_inbound_email_reroute Verb | POST URI | /rails/conductor/action_mailbox/:inbound_email_id/reroute(.:format) Controller#Action | rails/conductor/action_mailbox/reroutes#create - --[ Route 16 ]------------- + --[ Route 17 ]------------- Prefix | rails_service_blob Verb | GET URI | /rails/active_storage/blobs/:signed_id/*filename(.:format) Controller#Action | active_storage/blobs#show - --[ Route 17 ]------------- + --[ Route 18 ]------------- Prefix | rails_blob_representation Verb | GET URI | /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) Controller#Action | active_storage/representations#show - --[ Route 18 ]------------- + --[ Route 19 ]------------- Prefix | rails_disk_service Verb | GET URI | /rails/active_storage/disk/:encoded_key/*filename(.:format) Controller#Action | active_storage/disk#show - --[ Route 19 ]------------- + --[ Route 20 ]------------- Prefix | update_rails_disk_service Verb | PUT URI | /rails/active_storage/disk/:encoded_token(.:format) Controller#Action | active_storage/disk#update - --[ Route 20 ]------------- + --[ Route 21 ]------------- Prefix | rails_direct_uploads Verb | POST URI | /rails/active_storage/direct_uploads(.:format) diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb index fbdd3f3ebb..25b89ecbd8 100644 --- a/railties/test/commands/server_test.rb +++ b/railties/test/commands/server_test.rb @@ -32,6 +32,12 @@ class Rails::Command::ServerCommandTest < ActiveSupport::TestCase assert_match(/Could not find server "tin". Maybe you meant "thin"?/, run_command("--using", "tin")) end + def test_using_server_mistype_without_suggestion + output = run_command("--using", "t") + assert_match(/Could not find server "t"/, output) + assert_no_match(/Maybe you meant/, output) + end + def test_using_positional_argument_deprecation assert_match(/DEPRECATION WARNING/, run_command("tin")) end diff --git a/railties/test/credentials_test.rb b/railties/test/credentials_test.rb deleted file mode 100644 index 11765b0de5..0000000000 --- a/railties/test/credentials_test.rb +++ /dev/null @@ -1,65 +0,0 @@ -# frozen_string_literal: true - -require "isolation/abstract_unit" -require "env_helpers" - -class Rails::CredentialsTest < ActiveSupport::TestCase - include ActiveSupport::Testing::Isolation, EnvHelpers - - setup :build_app - teardown :teardown_app - - test "reads credentials from environment specific path" do - with_credentials do |content, key| - Dir.chdir(app_path) do - Dir.mkdir("config/credentials") - File.write("config/credentials/production.yml.enc", content) - File.write("config/credentials/production.key", key) - end - - app("production") - - assert_equal "revealed", Rails.application.credentials.mystery - end - end - - test "reads credentials from customized path and key" do - with_credentials do |content, key| - Dir.chdir(app_path) do - Dir.mkdir("config/credentials") - File.write("config/credentials/staging.yml.enc", content) - File.write("config/credentials/staging.key", key) - end - - add_to_env_config("production", "config.credentials.content_path = config.root.join('config/credentials/staging.yml.enc')") - add_to_env_config("production", "config.credentials.key_path = config.root.join('config/credentials/staging.key')") - app("production") - - assert_equal "revealed", Rails.application.credentials.mystery - end - end - - test "reads credentials using environment variable key" do - with_credentials do |content, key| - Dir.chdir(app_path) do - Dir.mkdir("config/credentials") - File.write("config/credentials/production.yml.enc", content) - end - - switch_env("RAILS_MASTER_KEY", key) do - app("production") - - assert_equal "revealed", Rails.application.credentials.mystery - end - end - end - - private - def with_credentials - key = "2117e775dc2024d4f49ddf3aeb585919" - # secret_key_base: secret - # mystery: revealed - content = "vgvKu4MBepIgZ5VHQMMPwnQNsLlWD9LKmJHu3UA/8yj6x+3fNhz3DwL9brX7UA==--qLdxHP6e34xeTAiI--nrcAsleXuo9NqiEuhntAhw==" - yield(content, key) - end -end diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb index af475400a1..44d4e92256 100644 --- a/railties/test/generators/actions_test.rb +++ b/railties/test/generators/actions_test.rb @@ -320,6 +320,12 @@ class ActionsTest < Rails::Generators::TestCase assert_no_file "app/models/my_model.rb" end + def test_generate_should_run_command_without_env + assert_called_with(generator, :run, ["rails generate model MyModel name:string", verbose: false]) do + action :generate, "model", "MyModel", "name:string" + end + end + def test_rake_should_run_rake_command_with_default_env assert_called_with(generator, :run, ["rake log:clear RAILS_ENV=development", verbose: false]) do with_rails_env nil do @@ -412,15 +418,6 @@ class ActionsTest < Rails::Generators::TestCase end end - def test_capify_should_run_the_capify_command - content = capture(:stderr) do - assert_called_with(generator, :run, ["capify .", verbose: false]) do - action :capify! - end - end - assert_match(/DEPRECATION WARNING: `capify!` is deprecated/, content) - end - def test_route_should_add_data_to_the_routes_block_in_config_routes run_generator route_command = "route '/login', controller: 'sessions', action: 'new'" diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 154cd3e80c..860e2bf63e 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -81,6 +81,7 @@ DEFAULT_APP_FILES = %w( test/test_helper.rb test/fixtures test/fixtures/files + test/channels/application_cable/connection_test.rb test/controllers test/models test/helpers @@ -198,7 +199,7 @@ class AppGeneratorTest < Rails::Generators::TestCase end end - def test_new_application_use_json_serialzier + def test_new_application_use_json_serializer run_generator assert_file("config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/) @@ -363,6 +364,8 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "#{app_root}/config/environments/production.rb" do |content| assert_no_match(/config\.action_cable/, content) end + + assert_no_file "#{app_root}/test/channels/application_cable/connection_test.rb" end def test_app_update_does_not_generate_bootsnap_contents_when_skip_bootsnap_is_given @@ -435,6 +438,36 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_no_file "#{app_root}/config/storage.yml" end + def test_generator_skips_action_mailbox_when_skip_action_mailbox_is_given + run_generator [destination_root, "--skip-action-mailbox"] + assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']action_mailbox\/engine["']/ + end + + def test_generator_skips_action_mailbox_when_skip_active_record_is_given + run_generator [destination_root, "--skip-active-record"] + assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']action_mailbox\/engine["']/ + end + + def test_generator_skips_action_mailbox_when_skip_active_storage_is_given + run_generator [destination_root, "--skip-active-storage"] + assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']action_mailbox\/engine["']/ + end + + def test_generator_skips_action_text_when_skip_action_text_is_given + run_generator [destination_root, "--skip-action-text"] + assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']action_text\/engine["']/ + end + + def test_generator_skips_action_text_when_skip_active_record_is_given + run_generator [destination_root, "--skip-active-record"] + assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']action_text\/engine["']/ + end + + def test_generator_skips_action_text_when_skip_active_storage_is_given + run_generator [destination_root, "--skip-active-storage"] + assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']action_text\/engine["']/ + end + def test_app_update_does_not_change_config_target_version run_generator @@ -743,6 +776,24 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_bundler_command_called("install") end + def test_generation_use_original_bundle_environment + generator([destination_root], skip_webpack_install: true) + + mock_original_env = -> do + { "BUNDLE_RUBYONRAILS__ORG" => "user:pass" } + end + + ensure_environment_is_set = -> *_args do + assert_equal "user:pass", ENV["BUNDLE_RUBYONRAILS__ORG"] + end + + Bundler.stub :original_env, mock_original_env do + generator.stub :exec_bundle_command, ensure_environment_is_set do + quietly { generator.invoke_all } + end + end + end + def test_dev_option generator([destination_root], dev: true, skip_webpack_install: true) diff --git a/railties/test/generators/channel_generator_test.rb b/railties/test/generators/channel_generator_test.rb index 1cb8465539..1a25422c3c 100644 --- a/railties/test/generators/channel_generator_test.rb +++ b/railties/test/generators/channel_generator_test.rb @@ -67,12 +67,23 @@ class ChannelGeneratorTest < Rails::Generators::TestCase assert_file "app/javascript/channels/consumer.js" end + def test_invokes_default_test_framework + run_generator %w(chat -t=test_unit) + + assert_file "test/channels/chat_channel_test.rb" do |test| + assert_match(/class ChatChannelTest < ActionCable::Channel::TestCase/, test) + assert_match(/# test "subscribes" do/, test) + assert_match(/# assert subscription.confirmed\?/, test) + end + end + def test_channel_on_revoke run_generator ["chat"] run_generator ["chat"], behavior: :revoke assert_no_file "app/channels/chat_channel.rb" assert_no_file "app/javascript/channels/chat_channel.js" + assert_no_file "test/channels/chat_channel_test.rb" assert_file "app/channels/application_cable/channel.rb" assert_file "app/channels/application_cable/connection.rb" @@ -88,5 +99,8 @@ class ChannelGeneratorTest < Rails::Generators::TestCase assert_no_file "app/javascript/channels/chat_channel_channel.js" assert_file "app/javascript/channels/chat_channel.js" + + assert_no_file "test/channels/chat_channel_channel_test.rb" + assert_file "test/channels/chat_channel_test.rb" end end diff --git a/railties/test/generators/db_system_change_generator_test.rb b/railties/test/generators/db_system_change_generator_test.rb new file mode 100644 index 0000000000..d476bfd2dc --- /dev/null +++ b/railties/test/generators/db_system_change_generator_test.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require "generators/generators_test_helper" +require "rails/generators/rails/db/system/change/change_generator" + +module Rails + module Generators + module Db + module System + class ChangeGeneratorTest < Rails::Generators::TestCase + include GeneratorsTestHelper + + setup do + copy_gemfile( + GemfileEntry.new("sqlite3", nil, "Use sqlite3 as the database for Active Record") + ) + end + + test "change to invalid database" do + output = capture(:stderr) do + run_generator ["--to", "invalid-db"] + end + + assert_match <<~MSG.squish, output + Invalid value for --to option. + Supported preconfigurations are: + mysql, postgresql, sqlite3, oracle, frontbase, + ibm_db, sqlserver, jdbcmysql, jdbcsqlite3, + jdbcpostgresql, jdbc. + MSG + end + + test "change to postgresql" do + run_generator ["--to", "postgresql"] + + assert_file("config/database.yml") do |content| + assert_match "adapter: postgresql", content + assert_match "database: test_app", content + end + + assert_file("Gemfile") do |content| + assert_match "# Use pg as the database for Active Record", content + assert_match "gem 'pg'", content + end + end + + test "change to mysql" do + run_generator ["--to", "mysql"] + + assert_file("config/database.yml") do |content| + assert_match "adapter: mysql2", content + assert_match "database: test_app", content + end + + assert_file("Gemfile") do |content| + assert_match "# Use mysql2 as the database for Active Record", content + assert_match "gem 'mysql2'", content + end + end + + test "change to sqlite3" do + run_generator ["--to", "sqlite3"] + + assert_file("config/database.yml") do |content| + assert_match "adapter: sqlite3", content + assert_match "db/development.sqlite3", content + end + + assert_file("Gemfile") do |content| + assert_match "# Use sqlite3 as the database for Active Record", content + assert_match "gem 'sqlite3'", content + end + end + end + end + end + end +end diff --git a/railties/test/generators/generators_test_helper.rb b/railties/test/generators/generators_test_helper.rb index 25d5dba1d8..8b42cb83db 100644 --- a/railties/test/generators/generators_test_helper.rb +++ b/railties/test/generators/generators_test_helper.rb @@ -30,6 +30,12 @@ module GeneratorsTestHelper include ActiveSupport::Testing::Stream include ActiveSupport::Testing::MethodCallAssertions + GemfileEntry = Struct.new(:name, :version, :comment, :options, :commented_out) do + def initialize(name, version, comment, options = {}, commented_out = false) + super + end + end + def self.included(base) base.class_eval do destination File.join(Rails.root, "tmp") @@ -63,4 +69,38 @@ module GeneratorsTestHelper FileUtils.mkdir_p(destination) FileUtils.cp routes, File.join(destination, "routes.rb") end + + def copy_gemfile(*gemfile_entries) + locals = gemfile_locals.merge(gemfile_entries: gemfile_entries) + gemfile = File.expand_path("../../lib/rails/generators/rails/app/templates/Gemfile.tt", __dir__) + gemfile = evaluate_template(gemfile, locals) + destination = File.join(destination_root) + File.write File.join(destination, "Gemfile"), gemfile + end + + def evaluate_template(file, locals = {}) + erb = if ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+ + ERB.new(File.read(file), trim_mode: "-", eoutvar: "@output_buffer") + else + ERB.new(File.read(file), nil, "-", "@output_buffer") + end + context = Class.new do + locals.each do |local, value| + class_attribute local, default: value + end + end + erb.result(context.new.instance_eval("binding")) + end + + private + def gemfile_locals + { + skip_active_storage: true, + depend_on_bootsnap: false, + depend_on_listen: false, + spring_install: false, + depends_on_system_test: false, + options: ActiveSupport::OrderedOptions.new, + } + end end diff --git a/railties/test/generators/mailer_generator_test.rb b/railties/test/generators/mailer_generator_test.rb index ddac6e1a1e..099e06c4d3 100644 --- a/railties/test/generators/mailer_generator_test.rb +++ b/railties/test/generators/mailer_generator_test.rb @@ -119,7 +119,7 @@ class MailerGeneratorTest < Rails::Generators::TestCase assert_match(/haml \[not found\]/, content) end - def test_mailer_with_namedspaced_mailer + def test_mailer_with_namespaced_mailer run_generator ["Farm::Animal", "moos"] assert_file "app/mailers/farm/animal_mailer.rb" do |mailer| assert_match(/class Farm::AnimalMailer < ApplicationMailer/, mailer) diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb index b06db6dd8a..134659f285 100644 --- a/railties/test/generators/model_generator_test.rb +++ b/railties/test/generators/model_generator_test.rb @@ -104,7 +104,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase ActiveRecord::Base.pluralize_table_names = true end - def test_migration_with_namespaces_in_model_name_without_plurization + def test_migration_with_namespaces_in_model_name_without_pluralization ActiveRecord::Base.pluralize_table_names = false run_generator ["Gallery::Image"] assert_migration "db/migrate/create_gallery_image", /class CreateGalleryImage < ActiveRecord::Migration\[[0-9.]+\]/ diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index 66286fc554..f45464f8d0 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -442,7 +442,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase end end - def test_dummy_appplication_skip_listen_by_default + def test_dummy_application_skip_listen_by_default run_generator assert_file "test/dummy/config/environments/development.rb" do |contents| @@ -712,38 +712,6 @@ class PluginGeneratorTest < Rails::Generators::TestCase Object.send(:remove_const, "ENGINE_ROOT") end - def test_after_bundle_callback - path = "http://example.org/rails_template" - template = +%{ after_bundle { run "echo ran after_bundle" } } - template.instance_eval "def read; self; end" # Make the string respond to read - - check_open = -> *args do - assert_equal [ path, "Accept" => "application/x-thor-template" ], args - template - end - - sequence = ["echo ran after_bundle"] - @sequence_step ||= 0 - ensure_bundler_first = -> command do - assert_equal sequence[@sequence_step], command, "commands should be called in sequence #{sequence}" - @sequence_step += 1 - end - - content = nil - generator([destination_root], template: path).stub(:open, check_open, template) do - generator.stub(:bundle_command, ensure_bundler_first) do - generator.stub(:run, ensure_bundler_first) do - silence_stream($stdout) do - content = capture(:stderr) { generator.invoke_all } - end - end - end - end - - assert_equal 1, @sequence_step - assert_match(/DEPRECATION WARNING: `after_bundle` is deprecated/, content) - end - private def action(*args, &block) diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb index 7441ab0603..26ce487c5f 100644 --- a/railties/test/generators/shared_generator_tests.rb +++ b/railties/test/generators/shared_generator_tests.rb @@ -127,6 +127,8 @@ module SharedGeneratorTests "--skip-active-record", "--skip-active-storage", "--skip-action-mailer", + "--skip-action-mailbox", + "--skip-action-text", "--skip-action-cable", "--skip-sprockets" ] @@ -138,6 +140,10 @@ module SharedGeneratorTests assert_file "#{application_path}/config/application.rb", /^# require\s+["']active_storage\/engine["']/ assert_file "#{application_path}/config/application.rb", /^require\s+["']action_controller\/railtie["']/ assert_file "#{application_path}/config/application.rb", /^# require\s+["']action_mailer\/railtie["']/ + unless generator_class.name == "Rails::Generators::PluginGenerator" + assert_file "#{application_path}/config/application.rb", /^# require\s+["']action_mailbox\/engine["']/ + assert_file "#{application_path}/config/application.rb", /^# require\s+["']action_text\/engine["']/ + end assert_file "#{application_path}/config/application.rb", /^require\s+["']action_view\/railtie["']/ assert_file "#{application_path}/config/application.rb", /^# require\s+["']action_cable\/engine["']/ assert_file "#{application_path}/config/application.rb", /^# require\s+["']sprockets\/railtie["']/ @@ -200,7 +206,7 @@ module SharedGeneratorTests unless generator_class.name == "Rails::Generators::PluginGenerator" assert_file "#{application_path}/app/javascript/packs/application.js" do |content| - assert_match(/^import \* as ActiveStorage from "activestorage"\nActiveStorage.start\(\)/, content) + assert_match(/^require\("@rails\/activestorage"\)\.start\(\)/, content) end end @@ -261,7 +267,7 @@ module SharedGeneratorTests assert_file "#{application_path}/config/application.rb", /#\s+require\s+["']active_storage\/engine["']/ assert_file "#{application_path}/app/javascript/packs/application.js" do |content| - assert_no_match(/^import * as ActiveStorage from "activestorage"\nActiveStorage.start\(\)/, content) + assert_no_match(/^require\("@rails\/activestorage"\)\.start\(\)/, content) end assert_file "#{application_path}/config/environments/development.rb" do |content| @@ -340,11 +346,16 @@ module SharedGeneratorTests run_generator assert_file "#{application_path}/package.json", /dependencies/ assert_file "#{application_path}/bin/yarn" + assert_file "#{application_path}/config/initializers/assets.rb", /node_modules/ end def test_generator_for_yarn_skipped run_generator([destination_root, "--skip-javascript"]) assert_no_file "#{application_path}/package.json" assert_no_file "#{application_path}/bin/yarn" + + assert_file "#{application_path}/config/initializers/assets.rb" do |content| + assert_no_match(/node_modules/, content) + end end end diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb index f98c1f78f7..abdc04a8d3 100644 --- a/railties/test/generators_test.rb +++ b/railties/test/generators_test.rb @@ -28,6 +28,7 @@ class GeneratorsTest < Rails::Generators::TestCase output = capture(:stdout) { Rails::Generators.invoke name } assert_match "Could not find generator '#{name}'", output assert_match "`rails generate --help`", output + assert_no_match "Maybe you meant", output end def test_generator_suggestions diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 7d4a26ff9d..e9cf60a13d 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -476,14 +476,14 @@ Module.new do `yarn build` end - `#{Gem.ruby} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails new #{app_template_path} --skip-gemfile --skip-listen --no-rc` + `#{Gem.ruby} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails new #{app_template_path} --skip-bundle --skip-listen --no-rc` File.open("#{app_template_path}/config/boot.rb", "w") do |f| f.puts "require 'rails/all'" end Dir.chdir(app_template_path) { `yarn add https://github.com/rails/webpacker.git` } # Use the latest version. - # Manually install `webpack` as bin symlinks are not created for subdependencies + # Manually install `webpack` as bin symlinks are not created for sub dependencies # in workspaces. See https://github.com/yarnpkg/yarn/issues/4964 Dir.chdir(app_template_path) { `yarn add webpack@4.17.1 --tilde` } Dir.chdir(app_template_path) { `yarn add webpack-cli` } diff --git a/railties/test/path_generation_test.rb b/railties/test/path_generation_test.rb index 849b183b37..0c1ee259b0 100644 --- a/railties/test/path_generation_test.rb +++ b/railties/test/path_generation_test.rb @@ -66,7 +66,7 @@ class PathGenerationTest < ActiveSupport::TestCase super app = self @routes = TestSet.new ->(c) { app.controller = c } - secrets.secret_token = "foo" + secrets.secret_key_base = "foo" end def app; routes; end } diff --git a/railties/test/rails_info_controller_test.rb b/railties/test/rails_info_controller_test.rb index 6ab68f8333..878a238f8d 100644 --- a/railties/test/rails_info_controller_test.rb +++ b/railties/test/rails_info_controller_test.rb @@ -50,11 +50,6 @@ class InfoControllerTest < ActionController::TestCase assert_select "table" end - test "info controller renders json with properties" do - get :properties, format: :json - assert_equal Rails::Info.to_json, response.body - end - test "info controller renders with routes" do get :routes assert_response :success diff --git a/railties/test/rails_info_test.rb b/railties/test/rails_info_test.rb index d167a86e56..50522c1be6 100644 --- a/railties/test/rails_info_test.rb +++ b/railties/test/rails_info_test.rb @@ -43,18 +43,6 @@ class InfoTest < ActiveSupport::TestCase end end - def test_json_includes_middleware - Rails::Info.module_eval do - property "Middleware", ["Rack::Lock", "Rack::Static"] - end - - hash = JSON.parse(Rails::Info.to_json) - assert_includes hash.keys, "Middleware" - properties.value_for("Middleware").each do |value| - assert_includes hash["Middleware"], value - end - end - private def properties Rails::Info.properties diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index ca77c3a786..508367212b 100644 --- a/railties/test/railties/engine_test.rb +++ b/railties/test/railties/engine_test.rb @@ -90,8 +90,8 @@ module RailtiesTest boot_rails Dir.chdir(app_path) do - # Install Active Storage and Action Mailbox migration files first so as not to affect test. - `bundle exec rake active_storage:install action_mailbox:install` + # Install Active Storage, Action Mailbox, and Action Text migration files first so as not to affect test. + `bundle exec rake active_storage:install action_mailbox:install action_text:install` output = `bundle exec rake bukkits:install:migrations` ["CreateUsers", "AddLastNameToUsers", "CreateSessions"].each do |migration_name| @@ -177,8 +177,8 @@ module RailtiesTest boot_rails Dir.chdir(app_path) do - # Install Active Storage and Action Mailbox migrations first so as not to affect test. - `bundle exec rake active_storage:install action_mailbox:install` + # Install Active Storage, Action Mailbox, and Action Text migrations first so as not to affect test. + `bundle exec rake active_storage:install action_mailbox:install action_text:install` output = `bundle exec rake railties:install:migrations`.split("\n") assert_match(/Copied migration \d+_create_users\.core_engine\.rb from core_engine/, output.first) @@ -879,6 +879,18 @@ YAML assert Bukkits::Engine.config.bukkits_seeds_loaded end + test "jobs are ran inline while loading seeds" do + app_file "db/seeds.rb", <<-RUBY + Rails.application.config.seed_queue_adapter = ActiveJob::Base.queue_adapter + RUBY + + boot_rails + Rails.application.load_seed + + assert_instance_of ActiveJob::QueueAdapters::InlineAdapter, Rails.application.config.seed_queue_adapter + assert_instance_of ActiveJob::QueueAdapters::AsyncAdapter, ActiveJob::Base.queue_adapter + end + test "skips nonexistent seed data" do FileUtils.rm "#{app_path}/db/seeds.rb" boot_rails diff --git a/railties/test/railties/mounted_engine_test.rb b/railties/test/railties/mounted_engine_test.rb index 48f0fbc80f..9755823e51 100644 --- a/railties/test/railties/mounted_engine_test.rb +++ b/railties/test/railties/mounted_engine_test.rb @@ -232,7 +232,7 @@ module ApplicationTests get "/someone/blog/generate_application_route" assert_equal "/", last_response.body - get "/somone/blog/application_route_in_view" + get "/someone/blog/application_route_in_view" assert_equal "/", last_response.body # test generating engine's route from other engine @@ -258,8 +258,8 @@ module ApplicationTests assert_equal "http://example.org/anonymous/blog/posts/44", last_response.body # test that correct path is generated for the same polymorphic_path call in an engine - get "/somone/blog/engine_polymorphic_path" - assert_equal "/somone/blog/posts/44", last_response.body + get "/someone/blog/engine_polymorphic_path" + assert_equal "/someone/blog/posts/44", last_response.body # and in an application get "/application_polymorphic_path" diff --git a/tasks/release.rb b/tasks/release.rb index a13003aa27..82d1fb6a68 100644 --- a/tasks/release.rb +++ b/tasks/release.rb @@ -1,6 +1,20 @@ # frozen_string_literal: true -FRAMEWORKS = %w( activesupport activemodel activerecord actionview actionpack activejob actionmailer actioncable activestorage railties ) +# Order dependent. E.g. Action Mailbox depends on Active Record so it should be after. +FRAMEWORKS = %w( + activesupport + activemodel + activerecord + actionview + actionpack + activejob + actionmailer + actioncable + activestorage + actionmailbox + actiontext + railties +) FRAMEWORK_NAMES = Hash.new { |h, k| k.split(/(?<=active|action)/).map(&:capitalize).join(" ") } root = File.expand_path("..", __dir__) @@ -122,16 +136,20 @@ namespace :changelog do end end - task :release_summary, [:base_release] do |_, args| + task :release_summary, [:base_release, :release] do |_, args| release_regexp = args[:base_release] ? Regexp.escape(args[:base_release]) : /\d+\.\d+\.\d+/ + puts release + FRAMEWORKS.each do |fw| puts "## #{FRAMEWORK_NAMES[fw]}" fname = File.join fw, "CHANGELOG.md" contents = File.readlines fname contents.shift changes = [] - until contents.first =~ /^## Rails #{release_regexp}.*$/ + until contents.first =~ /^## Rails #{release_regexp}.*$/ || + contents.first =~ /^Please check.*for previous changes\.$/ || + contents.empty? changes << contents.shift end @@ -159,15 +177,58 @@ namespace :all do end task verify: :install do - app_name = "pkg/verify-#{version}-#{Time.now.to_i}" + require "tmpdir" + + cd Dir.tmpdir + app_name = "verify-#{version}-#{Time.now.to_i}" sh "rails _#{version}_ new #{app_name} --skip-bundle" # Generate with the right version. cd app_name + substitute = -> (file_name, regex, replacement) do + File.write(file_name, File.read(file_name).sub(regex, replacement)) + end + # Replace the generated gemfile entry with the exact version. - File.write("Gemfile", File.read("Gemfile").sub(/^gem 'rails.*/, "gem 'rails', '#{version}'")) + substitute.call("Gemfile", /^gem 'rails.*/, "gem 'rails', '#{version}'") + substitute.call("Gemfile", /^# gem 'image_processing/, "gem 'image_processing") sh "bundle" + sh "rails action_mailbox:install" + sh "rails action_text:install" + + sh "rails generate scaffold user name description:text admin:boolean" + sh "rails db:migrate" + + # Replace the generated gemfile entry with the exact version. + substitute.call("app/models/user.rb", /end\n\z/, <<~CODE) + has_one_attached :avatar + has_rich_text :description + end + CODE + + substitute.call("app/views/users/_form.html.erb", /text_area :description %>\n <\/div>/, <<~CODE) + rich_text_area :description %>\n </div> + + <div class="field"> + Avatar: <%= form.file_field :avatar %> + </div> + CODE + + substitute.call("app/views/users/show.html.erb", /description %>\n<\/p>/, <<~CODE) + description %>\n</p> - sh "rails generate scaffold user name admin:boolean && rails db:migrate" + <p> + <% if @user.avatar.attached? -%> + <%= image_tag @user.avatar.representation(resize_to_limit: [500, 500]) %> + <% end -%> + </p> + CODE + + # Permit the avatar param. + substitute.call("app/controllers/users_controller.rb", /:admin/, ":admin, :avatar") + + if ENV["EDITOR"] + `#{ENV["EDITOR"]} #{File.expand_path(app_name)}` + end puts "Booting a Rails server. Verify the release by:" puts @@ -253,8 +314,7 @@ task :announce do require "erb" template = File.read("../tasks/release_announcement_draft.erb") - match = ERB.version.match(/\Aerb\.rb \[(?<version>[^ ]+) /) - if match && match[:version] >= "2.2.0" # Ruby 2.6+ + if ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+ puts ERB.new(template, trim_mode: "<>").result(binding) else puts ERB.new(template, nil, "<>").result(binding) diff --git a/version.rb b/version.rb index 54bfbdd516..82df56559c 100644 --- a/version.rb +++ b/version.rb @@ -10,7 +10,7 @@ module Rails MAJOR = 6 MINOR = 0 TINY = 0 - PRE = "alpha" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end @@ -5692,6 +5692,11 @@ trim-right@^1.0.1: resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= +trix@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trix/-/trix-1.0.0.tgz#e9cc98cf6030c908f8d54e317b5b072f927b0c6b" + integrity sha512-feli9QVXe6gzZOCUfpPGpNDURW9jMciIRVQ5gkDudOctcA1oMtI5K/qEbsL2rFCoGl1rSoeRt+HPhIFGyQscKg== + tslib@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" |