Merge branch 'main' into less-initializers

This commit is contained in:
Rafael Mendonça França 2021-09-15 17:18:49 -04:00
commit fd41ea1f2d
No known key found for this signature in database
GPG Key ID: FC23B6D0F1EEE948
909 changed files with 24175 additions and 11566 deletions

1
.gitignore vendored
View File

@ -2,7 +2,6 @@
# Check out https://help.github.com/articles/ignoring-files for how to set that up.
.Gemfile
.byebug_history
.ruby-version
/*/doc/
/*/test/tmp/

View File

@ -1,4 +1,5 @@
require:
- rubocop-minitest
- rubocop-packaging
- rubocop-performance
- rubocop-rails
@ -49,6 +50,9 @@ Layout/CaseIndentation:
Layout/ClosingHeredocIndentation:
Enabled: true
Layout/ClosingParenthesisIndentation:
Enabled: true
# Align comments with method definitions.
Layout/CommentIndentation:
Enabled: true
@ -139,6 +143,9 @@ Style/DefWithParentheses:
Style/MethodDefParentheses:
Enabled: true
Style/ExplicitBlockArgument:
Enabled: true
Style/FrozenStringLiteralComment:
Enabled: true
EnforcedStyle: always
@ -288,3 +295,6 @@ Performance/DeletePrefix:
Performance/DeleteSuffix:
Enabled: true
Minitest/UnreachableAssertion:
Enabled: true

22
Gemfile
View File

@ -13,9 +13,12 @@ gem "capybara", ">= 3.26"
gem "selenium-webdriver", ">= 4.0.0.alpha7"
gem "rack-cache", "~> 1.2"
gem "sass-rails"
gem "turbolinks", "~> 5"
gem "webpacker", "~> 5.0", require: ENV["SKIP_REQUIRE_WEBPACKER"] != "true"
gem "stimulus-rails"
gem "turbo-rails"
gem "jsbundling-rails"
gem "cssbundling-rails"
gem "importmap-rails"
gem "tailwindcss-rails"
# 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.
@ -23,13 +26,14 @@ gem "bcrypt", "~> 3.1.11", require: false
# This needs to be with require false to avoid it being automatically loaded by
# sprockets.
gem "uglifier", ">= 1.3.0", require: false
gem "terser", ">= 1.1.4", require: false
# Explicitly avoid 1.x that doesn't support Ruby 2.4+
gem "json", ">= 2.0.0"
group :rubocop do
gem "rubocop", ">= 0.90", require: false
gem "rubocop-minitest", require: false
gem "rubocop-packaging", require: false
gem "rubocop-performance", require: false
gem "rubocop-rails", require: false
@ -85,7 +89,7 @@ end
group :storage do
gem "aws-sdk-s3", require: false
gem "google-cloud-storage", "~> 1.11", require: false
gem "azure-storage-blob", require: false
gem "azure-storage-blob", "~> 2.0", require: false
gem "image_processing", "~> 1.2"
end
@ -95,7 +99,6 @@ gem "aws-sdk-sns", require: false
gem "webmock"
group :ujs do
gem "qunit-selenium"
gem "webdrivers"
end
@ -111,12 +114,12 @@ instance_eval File.read local_gemfile if File.exist? local_gemfile
group :test do
gem "minitest-bisect"
gem "minitest-ci", require: false
gem "minitest-retry"
gem "minitest-reporters"
platforms :mri do
gem "stackprof"
gem "byebug"
gem "debug", ">= 1.0.0", require: false
end
gem "benchmark-ips"
@ -177,6 +180,9 @@ if RUBY_VERSION >= "3.1"
gem "net-imap", require: false
gem "net-pop", require: false
# digest gem, which is one of the default gems has bumped to 3.0.1.pre for ruby 3.1.0dev.
gem "digest", "~> 3.0.1.pre", require: false
# matrix was removed from default gems in Ruby 3.1, but is used by the `capybara` gem.
# So we need to add it as a dependency until `capybara` is fixed: https://github.com/teamcapybara/capybara/pull/2468
gem "matrix", require: false

View File

@ -81,7 +81,6 @@ PATH
i18n (>= 1.6, < 2)
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
rails (7.0.0.alpha)
actioncable (= 7.0.0.alpha)
actionmailbox (= 7.0.0.alpha)
@ -103,6 +102,7 @@ PATH
method_source
rake (>= 0.13)
thor (~> 1.0)
zeitwerk (~> 2.5.0.beta3)
GEM
remote: https://rubygems.org/
@ -110,19 +110,18 @@ GEM
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
amq-protocol (2.3.2)
ansi (1.5.0)
ast (2.4.2)
aws-eventstream (1.1.1)
aws-partitions (1.465.0)
aws-sdk-core (3.114.1)
aws-partitions (1.469.0)
aws-sdk-core (3.114.3)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.43.0)
aws-sdk-kms (1.44.0)
aws-sdk-core (~> 3, >= 3.112.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.96.0)
aws-sdk-s3 (1.96.1)
aws-sdk-core (~> 3, >= 3.112.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
@ -165,7 +164,6 @@ GEM
bunny (2.18.0)
amq-protocol (~> 2.3, >= 2.3.1)
sorted_set (~> 1, >= 1.0.2)
byebug (11.1.3)
capybara (3.35.3)
addressable
mini_mime (>= 0.1.3)
@ -174,7 +172,7 @@ GEM
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
childprocess (4.0.0)
childprocess (4.1.0)
coffee-script (2.4.1)
coffee-script-source
execjs
@ -185,10 +183,15 @@ GEM
crack (0.4.5)
rexml
crass (1.0.6)
curses (1.4.1)
cssbundling-rails (0.1.0)
rails (>= 6.0.0)
curses (1.4.2)
daemons (1.4.0)
dalli (2.7.11)
dante (0.2.0)
debug (1.0.0)
irb
reline (>= 0.2.7)
declarative (0.0.20)
delayed_job (4.1.9)
activesupport (>= 3.0, < 6.2)
@ -237,12 +240,12 @@ GEM
faye-websocket (0.11.1)
eventmachine (>= 0.12.0)
websocket-driver (>= 0.5.1)
ffi (1.15.1)
ffi (1.15.3)
fugit (1.5.0)
et-orbi (~> 1.1, >= 1.1.8)
raabro (~> 1.4)
globalid (0.4.2)
activesupport (>= 4.2.0)
globalid (0.5.1)
activesupport (>= 5.0)
google-apis-core (0.3.0)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.14)
@ -287,14 +290,21 @@ GEM
image_processing (1.12.1)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
importmap-rails (0.5.1)
rails (>= 6.0.0)
io-console (0.5.9)
irb (1.3.7)
reline (>= 0.2.7)
jmespath (1.4.0)
jsbundling-rails (0.1.0)
rails (>= 6.0.0)
json (2.5.1)
jwt (2.2.3)
kindlerb (1.2.0)
mustache
nokogiri
libxml-ruby (3.2.1)
listen (3.5.1)
listen (3.6.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
loofah (2.10.0)
@ -307,15 +317,13 @@ GEM
method_source (1.0.0)
mini_magick (4.11.0)
mini_mime (1.1.0)
mini_portile2 (2.5.3)
minitest (5.14.4)
minitest-bisect (1.5.1)
minitest-server (~> 1.0)
path_expander (~> 1.1)
minitest-reporters (1.4.3)
ansi
builder
minitest (>= 5.0)
ruby-progressbar
minitest-ci (3.4.0)
minitest (>= 5.0.6)
minitest-retry (0.2.2)
minitest (>= 5.0)
minitest-server (1.0.6)
@ -330,13 +338,16 @@ GEM
net-http-persistent (4.0.1)
connection_pool (~> 2.2)
nio4r (2.5.7)
nokogiri (1.11.7)
mini_portile2 (~> 2.5.0)
racc (~> 1.4)
nokogiri (1.11.7-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.11.7-x86_64-linux)
racc (~> 1.4)
os (1.1.1)
parallel (1.20.1)
parser (3.0.1.1)
parser (3.0.2.0)
ast (~> 2.4.1)
path_expander (1.1.0)
pg (1.2.3)
@ -345,9 +356,6 @@ GEM
puma (5.3.2)
nio4r (~> 2.0)
que (0.14.3)
qunit-selenium (0.0.4)
selenium-webdriver
thor
raabro (1.4.0)
racc (1.5.2)
rack (2.2.3)
@ -355,8 +363,6 @@ GEM
rack (>= 0.4)
rack-protection (2.1.0)
rack
rack-proxy (0.7.0)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails-dom-testing (2.0.3)
@ -372,10 +378,12 @@ GEM
rbtree (0.4.4)
rdoc (6.3.1)
redcarpet (3.2.3)
redis (4.2.5)
redis (4.3.1)
redis-namespace (1.8.1)
redis (>= 3.0.4)
regexp_parser (2.1.1)
reline (0.2.7)
io-console (~> 0.5)
representable (3.1.1)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
@ -394,23 +402,25 @@ GEM
retriable (3.1.2)
rexml (3.2.5)
rouge (3.26.0)
rubocop (1.16.0)
rubocop (1.19.0)
parallel (~> 1.10)
parser (>= 3.0.0.0)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml
rubocop-ast (>= 1.7.0, < 2.0)
rubocop-ast (>= 1.9.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 3.0)
rubocop-ast (1.7.0)
rubocop-ast (1.9.1)
parser (>= 3.0.1.1)
rubocop-minitest (0.15.0)
rubocop (>= 0.90, < 2.0)
rubocop-packaging (0.5.1)
rubocop (>= 0.89, < 2.0)
rubocop-performance (1.11.3)
rubocop-performance (1.11.4)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
rubocop-rails (2.10.1)
rubocop-rails (2.11.3)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.7.0, < 2.0)
@ -421,23 +431,12 @@ GEM
rubyzip (2.3.0)
rufus-scheduler (3.7.0)
fugit (~> 1.1, >= 1.1.6)
sass-rails (6.0.0)
sassc-rails (~> 2.1, >= 2.1.1)
sassc (2.4.0)
ffi (~> 1.9)
sassc-rails (2.1.2)
railties (>= 4.0.0)
sassc (>= 2.0)
sprockets (> 3.0)
sprockets-rails
tilt
sdoc (2.2.0)
rdoc (>= 5.0)
selenium-webdriver (4.0.0.beta4)
childprocess (>= 0.5, < 5.0)
rexml (~> 3.2)
rubyzip (>= 1.2.2)
semantic_range (3.0.0)
sequel (5.45.0)
serverengine (2.0.7)
sigdump (~> 0.2.2)
@ -476,8 +475,14 @@ GEM
sprockets (>= 3.0.0)
sqlite3 (1.4.2)
stackprof (0.2.17)
stimulus-rails (0.4.2)
rails (>= 6.0.0)
sucker_punch (3.0.1)
concurrent-ruby (~> 1.0)
tailwindcss-rails (0.4.3)
rails (>= 6.0.0)
terser (1.1.4)
execjs (>= 0.3.0, < 3)
thin (1.8.1)
daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4)
@ -485,14 +490,11 @@ GEM
thor (1.1.0)
tilt (2.0.10)
trailblazer-option (0.1.1)
turbolinks (5.2.1)
turbolinks-source (~> 5.2)
turbolinks-source (5.2.0)
turbo-rails (0.7.11)
rails (>= 6.0.0)
tzinfo (2.0.4)
concurrent-ruby (~> 1.0)
uber (0.1.0)
uglifier (4.2.0)
execjs (>= 0.3.0, < 3)
unicode-display_width (2.0.0)
useragent (0.16.10)
vegas (0.1.11)
@ -509,21 +511,17 @@ GEM
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webpacker (5.4.0)
activesupport (>= 5.2)
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
webrick (1.7.0)
websocket (1.2.9)
websocket-driver (0.7.4)
websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.4.2)
zeitwerk (2.5.0.beta3)
PLATFORMS
ruby
x86_64-darwin-19
x86_64-darwin-20
x86_64-linux
@ -534,27 +532,30 @@ DEPENDENCIES
activerecord-jdbcsqlite3-adapter (>= 1.3.0)
aws-sdk-s3
aws-sdk-sns
azure-storage-blob
azure-storage-blob (~> 2.0)
backburner
bcrypt (~> 3.1.11)
benchmark-ips
blade
bootsnap (>= 1.4.4)
byebug
capybara (>= 3.26)
connection_pool
cssbundling-rails
dalli
debug (>= 1.0.0)
delayed_job
delayed_job_active_record
google-cloud-storage (~> 1.11)
hiredis
image_processing (~> 1.2)
importmap-rails
jsbundling-rails
json (>= 2.0.0)
kindlerb (~> 1.2.0)
libxml-ruby
listen (~> 3.3)
minitest-bisect
minitest-reporters
minitest-ci
minitest-retry
mysql2 (~> 0.5)!
nokogiri (>= 1.8.1, != 1.11.0)
@ -563,7 +564,6 @@ DEPENDENCIES
puma
que
queue_classic!
qunit-selenium
racc (>= 1.4.6)
rack-cache (~> 1.2)
rails!
@ -576,10 +576,10 @@ DEPENDENCIES
rexml
rouge
rubocop (>= 0.90)
rubocop-minitest
rubocop-packaging
rubocop-performance
rubocop-rails
sass-rails
sdoc (>= 2.2.0)
selenium-webdriver (>= 4.0.0.alpha7)
sequel
@ -588,17 +588,18 @@ DEPENDENCIES
sprockets-export
sqlite3 (~> 1.4)
stackprof
stimulus-rails
sucker_punch
turbolinks (~> 5)
tailwindcss-rails
terser (>= 1.1.4)
turbo-rails
tzinfo-data
uglifier (>= 1.3.0)
w3c_validators (~> 1.3.6)
wdm (>= 0.1.0)
webdrivers
webmock
webpacker (~> 5.0)
webrick
websocket-client-simple!
BUNDLED WITH
2.2.19
2.2.25

View File

@ -49,12 +49,12 @@ give them a heads up that Rails will be released soonish.
This is only required for major and minor releases, bugfix releases aren't a
big enough deal, and are supposed to be backward compatible.
Send an email just giving a heads up about the upcoming release to these
Send a message just giving a heads up about the upcoming release to these
lists:
* team@jruby.org
* community@rubini.us
* rubyonrails-core@googlegroups.com
* [rubyonrails-core](https://discuss.rubyonrails.org/c/rubyonrails-core)
Implementors will love you and help you.
@ -135,8 +135,8 @@ Write a release announcement that includes the version, changes, and links to
GitHub where people can find the specific commit list. Here are the mailing
lists where you should announce:
* rubyonrails-core@googlegroups.com
* rubyonrails-talk@googlegroups.com
* [rubyonrails-core](https://discuss.rubyonrails.org/c/rubyonrails-core)
* [rubyonrails-talk](https://discuss.rubyonrails.org/c/rubyonrails-talk)
* ruby-talk@ruby-lang.org
Use Markdown format for your announcement. Remember to ask people to report

View File

@ -3,7 +3,8 @@
"rules": {
"semi": ["error", "never"],
"quotes": ["error", "double"],
"no-unused-vars": ["error", { "vars": "all", "args": "none" }]
"no-unused-vars": ["error", { "vars": "all", "args": "none" }],
"no-console": "off"
},
"plugins": [
"import"

View File

@ -1,3 +1,23 @@
* Compile ESM package that can be used directly in the browser as actioncable.esm.js.
*DHH*
* Move action_cable.js to actioncable.js to match naming convention used for other Rails frameworks, and use JS console to communicate the deprecation.
*DHH*
* Stop transpiling the UMD package generated as actioncable.js and drop the IE11 testing that relied on that.
*DHH*
* Truncate broadcast logging messages.
*J Smith*
* OpenSSL constants are now used for Digest computations.
*Dirkjan Bussink*
* The Action Cable client now includes safeguards to prevent a "thundering
herd" of client reconnects after server connectivity loss:
@ -11,4 +31,5 @@
*Jonathan Hefner*
Please check [6-1-stable](https://github.com/rails/rails/blob/6-1-stable/actioncable/CHANGELOG.md) for previous changes.

View File

@ -1,153 +1,113 @@
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define([ "exports" ], factory) : factory(global.ActionCable = {});
})(this, function(exports) {
typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define([ "exports" ], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
factory(global.ActionCable = {}));
})(this, (function(exports) {
"use strict";
var adapters = {
logger: self.console,
WebSocket: self.WebSocket
};
var logger = {
log: function log() {
log(...messages) {
if (this.enabled) {
var _adapters$logger;
for (var _len = arguments.length, messages = Array(_len), _key = 0; _key < _len; _key++) {
messages[_key] = arguments[_key];
}
messages.push(Date.now());
(_adapters$logger = adapters.logger).log.apply(_adapters$logger, [ "[ActionCable]" ].concat(messages));
adapters.logger.log("[ActionCable]", ...messages);
}
}
};
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function(obj) {
return typeof obj;
} : function(obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};
var classCallCheck = function(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
};
var createClass = function() {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
return function(Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps);
if (staticProps) defineProperties(Constructor, staticProps);
return Constructor;
};
}();
var now = function now() {
return new Date().getTime();
};
var secondsSince = function secondsSince(time) {
return (now() - time) / 1e3;
};
var ConnectionMonitor = function() {
function ConnectionMonitor(connection) {
classCallCheck(this, ConnectionMonitor);
const now = () => (new Date).getTime();
const secondsSince = time => (now() - time) / 1e3;
class ConnectionMonitor {
constructor(connection) {
this.visibilityDidChange = this.visibilityDidChange.bind(this);
this.connection = connection;
this.reconnectAttempts = 0;
}
ConnectionMonitor.prototype.start = function start() {
start() {
if (!this.isRunning()) {
this.startedAt = now();
delete this.stoppedAt;
this.startPolling();
addEventListener("visibilitychange", this.visibilityDidChange);
logger.log("ConnectionMonitor started. stale threshold = " + this.constructor.staleThreshold + " s");
logger.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`);
}
};
ConnectionMonitor.prototype.stop = function stop() {
}
stop() {
if (this.isRunning()) {
this.stoppedAt = now();
this.stopPolling();
removeEventListener("visibilitychange", this.visibilityDidChange);
logger.log("ConnectionMonitor stopped");
}
};
ConnectionMonitor.prototype.isRunning = function isRunning() {
}
isRunning() {
return this.startedAt && !this.stoppedAt;
};
ConnectionMonitor.prototype.recordPing = function recordPing() {
}
recordPing() {
this.pingedAt = now();
};
ConnectionMonitor.prototype.recordConnect = function recordConnect() {
}
recordConnect() {
this.reconnectAttempts = 0;
this.recordPing();
delete this.disconnectedAt;
logger.log("ConnectionMonitor recorded connect");
};
ConnectionMonitor.prototype.recordDisconnect = function recordDisconnect() {
}
recordDisconnect() {
this.disconnectedAt = now();
logger.log("ConnectionMonitor recorded disconnect");
};
ConnectionMonitor.prototype.startPolling = function startPolling() {
}
startPolling() {
this.stopPolling();
this.poll();
};
ConnectionMonitor.prototype.stopPolling = function stopPolling() {
}
stopPolling() {
clearTimeout(this.pollTimeout);
};
ConnectionMonitor.prototype.poll = function poll() {
var _this = this;
this.pollTimeout = setTimeout(function() {
_this.reconnectIfStale();
_this.poll();
}, this.getPollInterval());
};
ConnectionMonitor.prototype.getPollInterval = function getPollInterval() {
var _constructor = this.constructor, staleThreshold = _constructor.staleThreshold, reconnectionBackoffRate = _constructor.reconnectionBackoffRate;
var backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10));
var jitterMax = this.reconnectAttempts === 0 ? 1 : reconnectionBackoffRate;
var jitter = jitterMax * Math.random();
}
poll() {
this.pollTimeout = setTimeout((() => {
this.reconnectIfStale();
this.poll();
}), this.getPollInterval());
}
getPollInterval() {
const {staleThreshold: staleThreshold, reconnectionBackoffRate: reconnectionBackoffRate} = this.constructor;
const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10));
const jitterMax = this.reconnectAttempts === 0 ? 1 : reconnectionBackoffRate;
const jitter = jitterMax * Math.random();
return staleThreshold * 1e3 * backoff * (1 + jitter);
};
ConnectionMonitor.prototype.reconnectIfStale = function reconnectIfStale() {
}
reconnectIfStale() {
if (this.connectionIsStale()) {
logger.log("ConnectionMonitor detected stale connection. reconnectAttempts = " + this.reconnectAttempts + ", time stale = " + secondsSince(this.refreshedAt) + " s, stale threshold = " + this.constructor.staleThreshold + " s");
logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`);
this.reconnectAttempts++;
if (this.disconnectedRecently()) {
logger.log("ConnectionMonitor skipping reopening recent disconnect. time disconnected = " + secondsSince(this.disconnectedAt) + " s");
logger.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`);
} else {
logger.log("ConnectionMonitor reopening");
this.connection.reopen();
}
}
};
ConnectionMonitor.prototype.connectionIsStale = function connectionIsStale() {
}
get refreshedAt() {
return this.pingedAt ? this.pingedAt : this.startedAt;
}
connectionIsStale() {
return secondsSince(this.refreshedAt) > this.constructor.staleThreshold;
};
ConnectionMonitor.prototype.disconnectedRecently = function disconnectedRecently() {
}
disconnectedRecently() {
return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold;
};
ConnectionMonitor.prototype.visibilityDidChange = function visibilityDidChange() {
var _this2 = this;
}
visibilityDidChange() {
if (document.visibilityState === "visible") {
setTimeout(function() {
if (_this2.connectionIsStale() || !_this2.connection.isOpen()) {
logger.log("ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = " + document.visibilityState);
_this2.connection.reopen();
setTimeout((() => {
if (this.connectionIsStale() || !this.connection.isOpen()) {
logger.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`);
this.connection.reopen();
}
}, 200);
}), 200);
}
};
createClass(ConnectionMonitor, [ {
key: "refreshedAt",
get: function get$$1() {
return this.pingedAt ? this.pingedAt : this.startedAt;
}
} ]);
return ConnectionMonitor;
}();
}
}
ConnectionMonitor.staleThreshold = 6;
ConnectionMonitor.reconnectionBackoffRate = .15;
var INTERNAL = {
@ -166,32 +126,31 @@
default_mount_path: "/cable",
protocols: [ "actioncable-v1-json", "actioncable-unsupported" ]
};
var message_types = INTERNAL.message_types, protocols = INTERNAL.protocols;
var supportedProtocols = protocols.slice(0, protocols.length - 1);
var indexOf = [].indexOf;
var Connection = function() {
function Connection(consumer) {
classCallCheck(this, Connection);
const {message_types: message_types, protocols: protocols} = INTERNAL;
const supportedProtocols = protocols.slice(0, protocols.length - 1);
const indexOf = [].indexOf;
class Connection {
constructor(consumer) {
this.open = this.open.bind(this);
this.consumer = consumer;
this.subscriptions = this.consumer.subscriptions;
this.monitor = new ConnectionMonitor(this);
this.disconnected = true;
}
Connection.prototype.send = function send(data) {
send(data) {
if (this.isOpen()) {
this.webSocket.send(JSON.stringify(data));
return true;
} else {
return false;
}
};
Connection.prototype.open = function open() {
}
open() {
if (this.isActive()) {
logger.log("Attempted to open WebSocket, but existing socket is " + this.getState());
logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`);
return false;
} else {
logger.log("Opening WebSocket, current state is " + this.getState() + ", subprotocols: " + protocols);
logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${protocols}`);
if (this.webSocket) {
this.uninstallEventHandlers();
}
@ -200,90 +159,85 @@
this.monitor.start();
return true;
}
};
Connection.prototype.close = function close() {
var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {
allowReconnect: true
}, allowReconnect = _ref.allowReconnect;
}
close({allowReconnect: allowReconnect} = {
allowReconnect: true
}) {
if (!allowReconnect) {
this.monitor.stop();
}
if (this.isActive()) {
return this.webSocket.close();
}
};
Connection.prototype.reopen = function reopen() {
logger.log("Reopening WebSocket, current state is " + this.getState());
}
reopen() {
logger.log(`Reopening WebSocket, current state is ${this.getState()}`);
if (this.isActive()) {
try {
return this.close();
} catch (error) {
logger.log("Failed to reopen WebSocket", error);
} finally {
logger.log("Reopening WebSocket in " + this.constructor.reopenDelay + "ms");
logger.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`);
setTimeout(this.open, this.constructor.reopenDelay);
}
} else {
return this.open();
}
};
Connection.prototype.getProtocol = function getProtocol() {
}
getProtocol() {
if (this.webSocket) {
return this.webSocket.protocol;
}
};
Connection.prototype.isOpen = function isOpen() {
}
isOpen() {
return this.isState("open");
};
Connection.prototype.isActive = function isActive() {
}
isActive() {
return this.isState("open", "connecting");
};
Connection.prototype.isProtocolSupported = function isProtocolSupported() {
}
isProtocolSupported() {
return indexOf.call(supportedProtocols, this.getProtocol()) >= 0;
};
Connection.prototype.isState = function isState() {
for (var _len = arguments.length, states = Array(_len), _key = 0; _key < _len; _key++) {
states[_key] = arguments[_key];
}
}
isState(...states) {
return indexOf.call(states, this.getState()) >= 0;
};
Connection.prototype.getState = function getState() {
}
getState() {
if (this.webSocket) {
for (var state in adapters.WebSocket) {
for (let state in adapters.WebSocket) {
if (adapters.WebSocket[state] === this.webSocket.readyState) {
return state.toLowerCase();
}
}
}
return null;
};
Connection.prototype.installEventHandlers = function installEventHandlers() {
for (var eventName in this.events) {
var handler = this.events[eventName].bind(this);
this.webSocket["on" + eventName] = handler;
}
installEventHandlers() {
for (let eventName in this.events) {
const handler = this.events[eventName].bind(this);
this.webSocket[`on${eventName}`] = handler;
}
};
Connection.prototype.uninstallEventHandlers = function uninstallEventHandlers() {
for (var eventName in this.events) {
this.webSocket["on" + eventName] = function() {};
}
uninstallEventHandlers() {
for (let eventName in this.events) {
this.webSocket[`on${eventName}`] = function() {};
}
};
return Connection;
}();
}
}
Connection.reopenDelay = 500;
Connection.prototype.events = {
message: function message(event) {
message(event) {
if (!this.isProtocolSupported()) {
return;
}
var _JSON$parse = JSON.parse(event.data), identifier = _JSON$parse.identifier, message = _JSON$parse.message, reason = _JSON$parse.reason, reconnect = _JSON$parse.reconnect, type = _JSON$parse.type;
const {identifier: identifier, message: message, reason: reason, reconnect: reconnect, type: type} = JSON.parse(event.data);
switch (type) {
case message_types.welcome:
this.monitor.recordConnect();
return this.subscriptions.reload();
case message_types.disconnect:
logger.log("Disconnecting. Reason: " + reason);
logger.log(`Disconnecting. Reason: ${reason}`);
return this.close({
allowReconnect: reconnect
});
@ -301,8 +255,8 @@
return this.subscriptions.notify(identifier, "received", message);
}
},
open: function open() {
logger.log("WebSocket onopen event, using '" + this.getProtocol() + "' subprotocol");
open() {
logger.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`);
this.disconnected = false;
if (!this.isProtocolSupported()) {
logger.log("Protocol is unsupported. Stopping monitor and disconnecting.");
@ -311,7 +265,7 @@
});
}
},
close: function close(event) {
close(event) {
logger.log("WebSocket onclose event");
if (this.disconnected) {
return;
@ -322,167 +276,136 @@
willAttemptReconnect: this.monitor.isRunning()
});
},
error: function error() {
error() {
logger.log("WebSocket onerror event");
}
};
var extend = function extend(object, properties) {
const extend = function(object, properties) {
if (properties != null) {
for (var key in properties) {
var value = properties[key];
for (let key in properties) {
const value = properties[key];
object[key] = value;
}
}
return object;
};
var Subscription = function() {
function Subscription(consumer) {
var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var mixin = arguments[2];
classCallCheck(this, Subscription);
class Subscription {
constructor(consumer, params = {}, mixin) {
this.consumer = consumer;
this.identifier = JSON.stringify(params);
extend(this, mixin);
}
Subscription.prototype.perform = function perform(action) {
var data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
perform(action, data = {}) {
data.action = action;
return this.send(data);
};
Subscription.prototype.send = function send(data) {
}
send(data) {
return this.consumer.send({
command: "message",
identifier: this.identifier,
data: JSON.stringify(data)
});
};
Subscription.prototype.unsubscribe = function unsubscribe() {
}
unsubscribe() {
return this.consumer.subscriptions.remove(this);
};
return Subscription;
}();
var Subscriptions = function() {
function Subscriptions(consumer) {
classCallCheck(this, Subscriptions);
}
}
class Subscriptions {
constructor(consumer) {
this.consumer = consumer;
this.subscriptions = [];
}
Subscriptions.prototype.create = function create(channelName, mixin) {
var channel = channelName;
var params = (typeof channel === "undefined" ? "undefined" : _typeof(channel)) === "object" ? channel : {
create(channelName, mixin) {
const channel = channelName;
const params = typeof channel === "object" ? channel : {
channel: channel
};
var subscription = new Subscription(this.consumer, params, mixin);
const subscription = new Subscription(this.consumer, params, mixin);
return this.add(subscription);
};
Subscriptions.prototype.add = function add(subscription) {
}
add(subscription) {
this.subscriptions.push(subscription);
this.consumer.ensureActiveConnection();
this.notify(subscription, "initialized");
this.sendCommand(subscription, "subscribe");
return subscription;
};
Subscriptions.prototype.remove = function remove(subscription) {
}
remove(subscription) {
this.forget(subscription);
if (!this.findAll(subscription.identifier).length) {
this.sendCommand(subscription, "unsubscribe");
}
return subscription;
};
Subscriptions.prototype.reject = function reject(identifier) {
var _this = this;
return this.findAll(identifier).map(function(subscription) {
_this.forget(subscription);
_this.notify(subscription, "rejected");
}
reject(identifier) {
return this.findAll(identifier).map((subscription => {
this.forget(subscription);
this.notify(subscription, "rejected");
return subscription;
});
};
Subscriptions.prototype.forget = function forget(subscription) {
this.subscriptions = this.subscriptions.filter(function(s) {
return s !== subscription;
});
}));
}
forget(subscription) {
this.subscriptions = this.subscriptions.filter((s => s !== subscription));
return subscription;
};
Subscriptions.prototype.findAll = function findAll(identifier) {
return this.subscriptions.filter(function(s) {
return s.identifier === identifier;
});
};
Subscriptions.prototype.reload = function reload() {
var _this2 = this;
return this.subscriptions.map(function(subscription) {
return _this2.sendCommand(subscription, "subscribe");
});
};
Subscriptions.prototype.notifyAll = function notifyAll(callbackName) {
var _this3 = this;
for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
return this.subscriptions.map(function(subscription) {
return _this3.notify.apply(_this3, [ subscription, callbackName ].concat(args));
});
};
Subscriptions.prototype.notify = function notify(subscription, callbackName) {
for (var _len2 = arguments.length, args = Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) {
args[_key2 - 2] = arguments[_key2];
}
var subscriptions = void 0;
}
findAll(identifier) {
return this.subscriptions.filter((s => s.identifier === identifier));
}
reload() {
return this.subscriptions.map((subscription => this.sendCommand(subscription, "subscribe")));
}
notifyAll(callbackName, ...args) {
return this.subscriptions.map((subscription => this.notify(subscription, callbackName, ...args)));
}
notify(subscription, callbackName, ...args) {
let subscriptions;
if (typeof subscription === "string") {
subscriptions = this.findAll(subscription);
} else {
subscriptions = [ subscription ];
}
return subscriptions.map(function(subscription) {
return typeof subscription[callbackName] === "function" ? subscription[callbackName].apply(subscription, args) : undefined;
});
};
Subscriptions.prototype.sendCommand = function sendCommand(subscription, command) {
var identifier = subscription.identifier;
return subscriptions.map((subscription => typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined));
}
sendCommand(subscription, command) {
const {identifier: identifier} = subscription;
return this.consumer.send({
command: command,
identifier: identifier
});
};
return Subscriptions;
}();
var Consumer = function() {
function Consumer(url) {
classCallCheck(this, Consumer);
}
}
class Consumer {
constructor(url) {
this._url = url;
this.subscriptions = new Subscriptions(this);
this.connection = new Connection(this);
}
Consumer.prototype.send = function send(data) {
get url() {
return createWebSocketURL(this._url);
}
send(data) {
return this.connection.send(data);
};
Consumer.prototype.connect = function connect() {
}
connect() {
return this.connection.open();
};
Consumer.prototype.disconnect = function disconnect() {
}
disconnect() {
return this.connection.close({
allowReconnect: false
});
};
Consumer.prototype.ensureActiveConnection = function ensureActiveConnection() {
}
ensureActiveConnection() {
if (!this.connection.isActive()) {
return this.connection.open();
}
};
createClass(Consumer, [ {
key: "url",
get: function get$$1() {
return createWebSocketURL(this._url);
}
} ]);
return Consumer;
}();
}
}
function createWebSocketURL(url) {
if (typeof url === "function") {
url = url();
}
if (url && !/^wss?:/i.test(url)) {
var a = document.createElement("a");
const a = document.createElement("a");
a.href = url;
a.href = a.href;
a.protocol = a.protocol.replace("http", "ws");
@ -491,16 +414,16 @@
return url;
}
}
function createConsumer() {
var url = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getConfig("url") || INTERNAL.default_mount_path;
function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) {
return new Consumer(url);
}
function getConfig(name) {
var element = document.head.querySelector("meta[name='action-cable-" + name + "']");
const element = document.head.querySelector(`meta[name='action-cable-${name}']`);
if (element) {
return element.getAttribute("content");
}
}
console.log("DEPRECATION: action_cable.js has been renamed to actioncable.js please update your reference before Rails 8");
exports.Connection = Connection;
exports.ConnectionMonitor = ConnectionMonitor;
exports.Consumer = Consumer;
@ -508,11 +431,11 @@
exports.Subscription = Subscription;
exports.Subscriptions = Subscriptions;
exports.adapters = adapters;
exports.createWebSocketURL = createWebSocketURL;
exports.logger = logger;
exports.createConsumer = createConsumer;
exports.createWebSocketURL = createWebSocketURL;
exports.getConfig = getConfig;
exports.logger = logger;
Object.defineProperty(exports, "__esModule", {
value: true
});
});
}));

View File

@ -0,0 +1,442 @@
var adapters = {
logger: self.console,
WebSocket: self.WebSocket
};
var logger = {
log(...messages) {
if (this.enabled) {
messages.push(Date.now());
adapters.logger.log("[ActionCable]", ...messages);
}
}
};
const now = () => (new Date).getTime();
const secondsSince = time => (now() - time) / 1e3;
class ConnectionMonitor {
constructor(connection) {
this.visibilityDidChange = this.visibilityDidChange.bind(this);
this.connection = connection;
this.reconnectAttempts = 0;
}
start() {
if (!this.isRunning()) {
this.startedAt = now();
delete this.stoppedAt;
this.startPolling();
addEventListener("visibilitychange", this.visibilityDidChange);
logger.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`);
}
}
stop() {
if (this.isRunning()) {
this.stoppedAt = now();
this.stopPolling();
removeEventListener("visibilitychange", this.visibilityDidChange);
logger.log("ConnectionMonitor stopped");
}
}
isRunning() {
return this.startedAt && !this.stoppedAt;
}
recordPing() {
this.pingedAt = now();
}
recordConnect() {
this.reconnectAttempts = 0;
this.recordPing();
delete this.disconnectedAt;
logger.log("ConnectionMonitor recorded connect");
}
recordDisconnect() {
this.disconnectedAt = now();
logger.log("ConnectionMonitor recorded disconnect");
}
startPolling() {
this.stopPolling();
this.poll();
}
stopPolling() {
clearTimeout(this.pollTimeout);
}
poll() {
this.pollTimeout = setTimeout((() => {
this.reconnectIfStale();
this.poll();
}), this.getPollInterval());
}
getPollInterval() {
const {staleThreshold: staleThreshold, reconnectionBackoffRate: reconnectionBackoffRate} = this.constructor;
const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10));
const jitterMax = this.reconnectAttempts === 0 ? 1 : reconnectionBackoffRate;
const jitter = jitterMax * Math.random();
return staleThreshold * 1e3 * backoff * (1 + jitter);
}
reconnectIfStale() {
if (this.connectionIsStale()) {
logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`);
this.reconnectAttempts++;
if (this.disconnectedRecently()) {
logger.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`);
} else {
logger.log("ConnectionMonitor reopening");
this.connection.reopen();
}
}
}
get refreshedAt() {
return this.pingedAt ? this.pingedAt : this.startedAt;
}
connectionIsStale() {
return secondsSince(this.refreshedAt) > this.constructor.staleThreshold;
}
disconnectedRecently() {
return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold;
}
visibilityDidChange() {
if (document.visibilityState === "visible") {
setTimeout((() => {
if (this.connectionIsStale() || !this.connection.isOpen()) {
logger.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`);
this.connection.reopen();
}
}), 200);
}
}
}
ConnectionMonitor.staleThreshold = 6;
ConnectionMonitor.reconnectionBackoffRate = .15;
var INTERNAL = {
message_types: {
welcome: "welcome",
disconnect: "disconnect",
ping: "ping",
confirmation: "confirm_subscription",
rejection: "reject_subscription"
},
disconnect_reasons: {
unauthorized: "unauthorized",
invalid_request: "invalid_request",
server_restart: "server_restart"
},
default_mount_path: "/cable",
protocols: [ "actioncable-v1-json", "actioncable-unsupported" ]
};
const {message_types: message_types, protocols: protocols} = INTERNAL;
const supportedProtocols = protocols.slice(0, protocols.length - 1);
const indexOf = [].indexOf;
class Connection {
constructor(consumer) {
this.open = this.open.bind(this);
this.consumer = consumer;
this.subscriptions = this.consumer.subscriptions;
this.monitor = new ConnectionMonitor(this);
this.disconnected = true;
}
send(data) {
if (this.isOpen()) {
this.webSocket.send(JSON.stringify(data));
return true;
} else {
return false;
}
}
open() {
if (this.isActive()) {
logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`);
return false;
} else {
logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${protocols}`);
if (this.webSocket) {
this.uninstallEventHandlers();
}
this.webSocket = new adapters.WebSocket(this.consumer.url, protocols);
this.installEventHandlers();
this.monitor.start();
return true;
}
}
close({allowReconnect: allowReconnect} = {
allowReconnect: true
}) {
if (!allowReconnect) {
this.monitor.stop();
}
if (this.isActive()) {
return this.webSocket.close();
}
}
reopen() {
logger.log(`Reopening WebSocket, current state is ${this.getState()}`);
if (this.isActive()) {
try {
return this.close();
} catch (error) {
logger.log("Failed to reopen WebSocket", error);
} finally {
logger.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`);
setTimeout(this.open, this.constructor.reopenDelay);
}
} else {
return this.open();
}
}
getProtocol() {
if (this.webSocket) {
return this.webSocket.protocol;
}
}
isOpen() {
return this.isState("open");
}
isActive() {
return this.isState("open", "connecting");
}
isProtocolSupported() {
return indexOf.call(supportedProtocols, this.getProtocol()) >= 0;
}
isState(...states) {
return indexOf.call(states, this.getState()) >= 0;
}
getState() {
if (this.webSocket) {
for (let state in adapters.WebSocket) {
if (adapters.WebSocket[state] === this.webSocket.readyState) {
return state.toLowerCase();
}
}
}
return null;
}
installEventHandlers() {
for (let eventName in this.events) {
const handler = this.events[eventName].bind(this);
this.webSocket[`on${eventName}`] = handler;
}
}
uninstallEventHandlers() {
for (let eventName in this.events) {
this.webSocket[`on${eventName}`] = function() {};
}
}
}
Connection.reopenDelay = 500;
Connection.prototype.events = {
message(event) {
if (!this.isProtocolSupported()) {
return;
}
const {identifier: identifier, message: message, reason: reason, reconnect: reconnect, type: type} = JSON.parse(event.data);
switch (type) {
case message_types.welcome:
this.monitor.recordConnect();
return this.subscriptions.reload();
case message_types.disconnect:
logger.log(`Disconnecting. Reason: ${reason}`);
return this.close({
allowReconnect: reconnect
});
case message_types.ping:
return this.monitor.recordPing();
case message_types.confirmation:
return this.subscriptions.notify(identifier, "connected");
case message_types.rejection:
return this.subscriptions.reject(identifier);
default:
return this.subscriptions.notify(identifier, "received", message);
}
},
open() {
logger.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`);
this.disconnected = false;
if (!this.isProtocolSupported()) {
logger.log("Protocol is unsupported. Stopping monitor and disconnecting.");
return this.close({
allowReconnect: false
});
}
},
close(event) {
logger.log("WebSocket onclose event");
if (this.disconnected) {
return;
}
this.disconnected = true;
this.monitor.recordDisconnect();
return this.subscriptions.notifyAll("disconnected", {
willAttemptReconnect: this.monitor.isRunning()
});
},
error() {
logger.log("WebSocket onerror event");
}
};
const extend = function(object, properties) {
if (properties != null) {
for (let key in properties) {
const value = properties[key];
object[key] = value;
}
}
return object;
};
class Subscription {
constructor(consumer, params = {}, mixin) {
this.consumer = consumer;
this.identifier = JSON.stringify(params);
extend(this, mixin);
}
perform(action, data = {}) {
data.action = action;
return this.send(data);
}
send(data) {
return this.consumer.send({
command: "message",
identifier: this.identifier,
data: JSON.stringify(data)
});
}
unsubscribe() {
return this.consumer.subscriptions.remove(this);
}
}
class Subscriptions {
constructor(consumer) {
this.consumer = consumer;
this.subscriptions = [];
}
create(channelName, mixin) {
const channel = channelName;
const params = typeof channel === "object" ? channel : {
channel: channel
};
const subscription = new Subscription(this.consumer, params, mixin);
return this.add(subscription);
}
add(subscription) {
this.subscriptions.push(subscription);
this.consumer.ensureActiveConnection();
this.notify(subscription, "initialized");
this.sendCommand(subscription, "subscribe");
return subscription;
}
remove(subscription) {
this.forget(subscription);
if (!this.findAll(subscription.identifier).length) {
this.sendCommand(subscription, "unsubscribe");
}
return subscription;
}
reject(identifier) {
return this.findAll(identifier).map((subscription => {
this.forget(subscription);
this.notify(subscription, "rejected");
return subscription;
}));
}
forget(subscription) {
this.subscriptions = this.subscriptions.filter((s => s !== subscription));
return subscription;
}
findAll(identifier) {
return this.subscriptions.filter((s => s.identifier === identifier));
}
reload() {
return this.subscriptions.map((subscription => this.sendCommand(subscription, "subscribe")));
}
notifyAll(callbackName, ...args) {
return this.subscriptions.map((subscription => this.notify(subscription, callbackName, ...args)));
}
notify(subscription, callbackName, ...args) {
let subscriptions;
if (typeof subscription === "string") {
subscriptions = this.findAll(subscription);
} else {
subscriptions = [ subscription ];
}
return subscriptions.map((subscription => typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined));
}
sendCommand(subscription, command) {
const {identifier: identifier} = subscription;
return this.consumer.send({
command: command,
identifier: identifier
});
}
}
class Consumer {
constructor(url) {
this._url = url;
this.subscriptions = new Subscriptions(this);
this.connection = new Connection(this);
}
get url() {
return createWebSocketURL(this._url);
}
send(data) {
return this.connection.send(data);
}
connect() {
return this.connection.open();
}
disconnect() {
return this.connection.close({
allowReconnect: false
});
}
ensureActiveConnection() {
if (!this.connection.isActive()) {
return this.connection.open();
}
}
}
function createWebSocketURL(url) {
if (typeof url === "function") {
url = url();
}
if (url && !/^wss?:/i.test(url)) {
const a = document.createElement("a");
a.href = url;
a.href = a.href;
a.protocol = a.protocol.replace("http", "ws");
return a.href;
} else {
return url;
}
}
function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) {
return new Consumer(url);
}
function getConfig(name) {
const element = document.head.querySelector(`meta[name='action-cable-${name}']`);
if (element) {
return element.getAttribute("content");
}
}
export { Connection, ConnectionMonitor, Consumer, INTERNAL, Subscription, Subscriptions, adapters, createConsumer, createWebSocketURL, getConfig, logger };

View File

@ -0,0 +1,440 @@
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define([ "exports" ], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
factory(global.ActionCable = {}));
})(this, (function(exports) {
"use strict";
var adapters = {
logger: self.console,
WebSocket: self.WebSocket
};
var logger = {
log(...messages) {
if (this.enabled) {
messages.push(Date.now());
adapters.logger.log("[ActionCable]", ...messages);
}
}
};
const now = () => (new Date).getTime();
const secondsSince = time => (now() - time) / 1e3;
class ConnectionMonitor {
constructor(connection) {
this.visibilityDidChange = this.visibilityDidChange.bind(this);
this.connection = connection;
this.reconnectAttempts = 0;
}
start() {
if (!this.isRunning()) {
this.startedAt = now();
delete this.stoppedAt;
this.startPolling();
addEventListener("visibilitychange", this.visibilityDidChange);
logger.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`);
}
}
stop() {
if (this.isRunning()) {
this.stoppedAt = now();
this.stopPolling();
removeEventListener("visibilitychange", this.visibilityDidChange);
logger.log("ConnectionMonitor stopped");
}
}
isRunning() {
return this.startedAt && !this.stoppedAt;
}
recordPing() {
this.pingedAt = now();
}
recordConnect() {
this.reconnectAttempts = 0;
this.recordPing();
delete this.disconnectedAt;
logger.log("ConnectionMonitor recorded connect");
}
recordDisconnect() {
this.disconnectedAt = now();
logger.log("ConnectionMonitor recorded disconnect");
}
startPolling() {
this.stopPolling();
this.poll();
}
stopPolling() {
clearTimeout(this.pollTimeout);
}
poll() {
this.pollTimeout = setTimeout((() => {
this.reconnectIfStale();
this.poll();
}), this.getPollInterval());
}
getPollInterval() {
const {staleThreshold: staleThreshold, reconnectionBackoffRate: reconnectionBackoffRate} = this.constructor;
const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10));
const jitterMax = this.reconnectAttempts === 0 ? 1 : reconnectionBackoffRate;
const jitter = jitterMax * Math.random();
return staleThreshold * 1e3 * backoff * (1 + jitter);
}
reconnectIfStale() {
if (this.connectionIsStale()) {
logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`);
this.reconnectAttempts++;
if (this.disconnectedRecently()) {
logger.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`);
} else {
logger.log("ConnectionMonitor reopening");
this.connection.reopen();
}
}
}
get refreshedAt() {
return this.pingedAt ? this.pingedAt : this.startedAt;
}
connectionIsStale() {
return secondsSince(this.refreshedAt) > this.constructor.staleThreshold;
}
disconnectedRecently() {
return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold;
}
visibilityDidChange() {
if (document.visibilityState === "visible") {
setTimeout((() => {
if (this.connectionIsStale() || !this.connection.isOpen()) {
logger.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`);
this.connection.reopen();
}
}), 200);
}
}
}
ConnectionMonitor.staleThreshold = 6;
ConnectionMonitor.reconnectionBackoffRate = .15;
var INTERNAL = {
message_types: {
welcome: "welcome",
disconnect: "disconnect",
ping: "ping",
confirmation: "confirm_subscription",
rejection: "reject_subscription"
},
disconnect_reasons: {
unauthorized: "unauthorized",
invalid_request: "invalid_request",
server_restart: "server_restart"
},
default_mount_path: "/cable",
protocols: [ "actioncable-v1-json", "actioncable-unsupported" ]
};
const {message_types: message_types, protocols: protocols} = INTERNAL;
const supportedProtocols = protocols.slice(0, protocols.length - 1);
const indexOf = [].indexOf;
class Connection {
constructor(consumer) {
this.open = this.open.bind(this);
this.consumer = consumer;
this.subscriptions = this.consumer.subscriptions;
this.monitor = new ConnectionMonitor(this);
this.disconnected = true;
}
send(data) {
if (this.isOpen()) {
this.webSocket.send(JSON.stringify(data));
return true;
} else {
return false;
}
}
open() {
if (this.isActive()) {
logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`);
return false;
} else {
logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${protocols}`);
if (this.webSocket) {
this.uninstallEventHandlers();
}
this.webSocket = new adapters.WebSocket(this.consumer.url, protocols);
this.installEventHandlers();
this.monitor.start();
return true;
}
}
close({allowReconnect: allowReconnect} = {
allowReconnect: true
}) {
if (!allowReconnect) {
this.monitor.stop();
}
if (this.isActive()) {
return this.webSocket.close();
}
}
reopen() {
logger.log(`Reopening WebSocket, current state is ${this.getState()}`);
if (this.isActive()) {
try {
return this.close();
} catch (error) {
logger.log("Failed to reopen WebSocket", error);
} finally {
logger.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`);
setTimeout(this.open, this.constructor.reopenDelay);
}
} else {
return this.open();
}
}
getProtocol() {
if (this.webSocket) {
return this.webSocket.protocol;
}
}
isOpen() {
return this.isState("open");
}
isActive() {
return this.isState("open", "connecting");
}
isProtocolSupported() {
return indexOf.call(supportedProtocols, this.getProtocol()) >= 0;
}
isState(...states) {
return indexOf.call(states, this.getState()) >= 0;
}
getState() {
if (this.webSocket) {
for (let state in adapters.WebSocket) {
if (adapters.WebSocket[state] === this.webSocket.readyState) {
return state.toLowerCase();
}
}
}
return null;
}
installEventHandlers() {
for (let eventName in this.events) {
const handler = this.events[eventName].bind(this);
this.webSocket[`on${eventName}`] = handler;
}
}
uninstallEventHandlers() {
for (let eventName in this.events) {
this.webSocket[`on${eventName}`] = function() {};
}
}
}
Connection.reopenDelay = 500;
Connection.prototype.events = {
message(event) {
if (!this.isProtocolSupported()) {
return;
}
const {identifier: identifier, message: message, reason: reason, reconnect: reconnect, type: type} = JSON.parse(event.data);
switch (type) {
case message_types.welcome:
this.monitor.recordConnect();
return this.subscriptions.reload();
case message_types.disconnect:
logger.log(`Disconnecting. Reason: ${reason}`);
return this.close({
allowReconnect: reconnect
});
case message_types.ping:
return this.monitor.recordPing();
case message_types.confirmation:
return this.subscriptions.notify(identifier, "connected");
case message_types.rejection:
return this.subscriptions.reject(identifier);
default:
return this.subscriptions.notify(identifier, "received", message);
}
},
open() {
logger.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`);
this.disconnected = false;
if (!this.isProtocolSupported()) {
logger.log("Protocol is unsupported. Stopping monitor and disconnecting.");
return this.close({
allowReconnect: false
});
}
},
close(event) {
logger.log("WebSocket onclose event");
if (this.disconnected) {
return;
}
this.disconnected = true;
this.monitor.recordDisconnect();
return this.subscriptions.notifyAll("disconnected", {
willAttemptReconnect: this.monitor.isRunning()
});
},
error() {
logger.log("WebSocket onerror event");
}
};
const extend = function(object, properties) {
if (properties != null) {
for (let key in properties) {
const value = properties[key];
object[key] = value;
}
}
return object;
};
class Subscription {
constructor(consumer, params = {}, mixin) {
this.consumer = consumer;
this.identifier = JSON.stringify(params);
extend(this, mixin);
}
perform(action, data = {}) {
data.action = action;
return this.send(data);
}
send(data) {
return this.consumer.send({
command: "message",
identifier: this.identifier,
data: JSON.stringify(data)
});
}
unsubscribe() {
return this.consumer.subscriptions.remove(this);
}
}
class Subscriptions {
constructor(consumer) {
this.consumer = consumer;
this.subscriptions = [];
}
create(channelName, mixin) {
const channel = channelName;
const params = typeof channel === "object" ? channel : {
channel: channel
};
const subscription = new Subscription(this.consumer, params, mixin);
return this.add(subscription);
}
add(subscription) {
this.subscriptions.push(subscription);
this.consumer.ensureActiveConnection();
this.notify(subscription, "initialized");
this.sendCommand(subscription, "subscribe");
return subscription;
}
remove(subscription) {
this.forget(subscription);
if (!this.findAll(subscription.identifier).length) {
this.sendCommand(subscription, "unsubscribe");
}
return subscription;
}
reject(identifier) {
return this.findAll(identifier).map((subscription => {
this.forget(subscription);
this.notify(subscription, "rejected");
return subscription;
}));
}
forget(subscription) {
this.subscriptions = this.subscriptions.filter((s => s !== subscription));
return subscription;
}
findAll(identifier) {
return this.subscriptions.filter((s => s.identifier === identifier));
}
reload() {
return this.subscriptions.map((subscription => this.sendCommand(subscription, "subscribe")));
}
notifyAll(callbackName, ...args) {
return this.subscriptions.map((subscription => this.notify(subscription, callbackName, ...args)));
}
notify(subscription, callbackName, ...args) {
let subscriptions;
if (typeof subscription === "string") {
subscriptions = this.findAll(subscription);
} else {
subscriptions = [ subscription ];
}
return subscriptions.map((subscription => typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined));
}
sendCommand(subscription, command) {
const {identifier: identifier} = subscription;
return this.consumer.send({
command: command,
identifier: identifier
});
}
}
class Consumer {
constructor(url) {
this._url = url;
this.subscriptions = new Subscriptions(this);
this.connection = new Connection(this);
}
get url() {
return createWebSocketURL(this._url);
}
send(data) {
return this.connection.send(data);
}
connect() {
return this.connection.open();
}
disconnect() {
return this.connection.close({
allowReconnect: false
});
}
ensureActiveConnection() {
if (!this.connection.isActive()) {
return this.connection.open();
}
}
}
function createWebSocketURL(url) {
if (typeof url === "function") {
url = url();
}
if (url && !/^wss?:/i.test(url)) {
const a = document.createElement("a");
a.href = url;
a.href = a.href;
a.protocol = a.protocol.replace("http", "ws");
return a.href;
} else {
return url;
}
}
function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) {
return new Consumer(url);
}
function getConfig(name) {
const element = document.head.querySelector(`meta[name='action-cable-${name}']`);
if (element) {
return element.getAttribute("content");
}
}
exports.Connection = Connection;
exports.ConnectionMonitor = ConnectionMonitor;
exports.Consumer = Consumer;
exports.INTERNAL = INTERNAL;
exports.Subscription = Subscription;
exports.Subscriptions = Subscriptions;
exports.adapters = adapters;
exports.createConsumer = createConsumer;
exports.createWebSocketURL = createWebSocketURL;
exports.getConfig = getConfig;
exports.logger = logger;
Object.defineProperty(exports, "__esModule", {
value: true
});
}));

View File

@ -0,0 +1,2 @@
export * from "./index"
console.log("DEPRECATION: action_cable.js has been renamed to actioncable.js please update your reference before Rails 8")

View File

@ -27,7 +27,6 @@ if (process.env.CI) {
sl_ff: sauce("firefox", 63),
sl_safari: sauce("safari", 12.0, "macOS 10.13"),
sl_edge: sauce("microsoftedge", 17.17134, "Windows 10"),
sl_ie_11: sauce("internet explorer", 11, "Windows 8.1"),
}
config.browsers = Object.keys(config.customLaunchers)

View File

@ -25,7 +25,7 @@ module ActionCable
serialize_broadcasting([ channel_name, model ])
end
def serialize_broadcasting(object) #:nodoc:
def serialize_broadcasting(object) # :nodoc:
case
when object.is_a?(Array)
object.map { |m| serialize_broadcasting(m) }.join(":")

View File

@ -68,7 +68,7 @@ module ActionCable
# Called by the server when a new WebSocket connection is established. This configures the callbacks intended for overwriting by the user.
# This method should not be called directly -- instead rely upon on the #connect (and #disconnect) callbacks.
def process #:nodoc:
def process # :nodoc:
logger.info started_request_message
if websocket.possible? && allow_request_origin?
@ -80,11 +80,11 @@ module ActionCable
# Decodes WebSocket messages and dispatches them to subscribed channels.
# WebSocket message transfer encoding is always JSON.
def receive(websocket_message) #:nodoc:
def receive(websocket_message) # :nodoc:
send_async :dispatch_websocket_message, websocket_message
end
def dispatch_websocket_message(websocket_message) #:nodoc:
def dispatch_websocket_message(websocket_message) # :nodoc:
if websocket.alive?
subscriptions.execute_command decode(websocket_message)
else

View File

@ -18,10 +18,10 @@ module ActionCable
@tags = @tags.uniq
end
def tag(logger)
def tag(logger, &block)
if logger.respond_to?(:tagged)
current_tags = tags - logger.formatter.current_tags
logger.tagged(*current_tags) { yield }
logger.tagged(*current_tags, &block)
else
yield
end

View File

@ -9,6 +9,7 @@ module ActionCable
class Engine < Rails::Engine # :nodoc:
config.action_cable = ActiveSupport::OrderedOptions.new
config.action_cable.mount_path = ActionCable::INTERNAL[:default_mount_path]
config.action_cable.precompile_assets = true
config.eager_load_namespaces << ActionCable
@ -22,6 +23,14 @@ module ActionCable
ActiveSupport.on_load(:action_cable) { self.logger ||= ::Rails.logger }
end
initializer "action_cable.asset" do
config.after_initialize do |app|
if Rails.application.config.respond_to?(:assets) && app.config.action_cable.precompile_assets
Rails.application.config.assets.precompile += %w( actioncable.js actioncable.esm.js )
end
end
end
initializer "action_cable.set_configs" do |app|
options = app.config.action_cable
options.allowed_request_origins ||= /https?:\/\/localhost:\d+/ if ::Rails.env.development?

View File

@ -8,14 +8,15 @@ module ActionCable
#
# <head>
# <%= action_cable_meta_tag %>
# <%= javascript_include_tag 'application', 'data-turbolinks-track' => 'reload' %>
# <%= javascript_include_tag 'application', 'data-turbo-track' => 'reload' %>
# </head>
#
# This is then used by Action Cable to determine the URL of your WebSocket server.
# Your JavaScript can then connect to the server without needing to specify the
# URL directly:
#
# window.Cable = require("@rails/actioncable")
# import Cable from "@rails/actioncable"
# window.Cable = Cable
# window.App = {}
# App.cable = Cable.createConsumer()
#

View File

@ -40,7 +40,7 @@ module ActionCable
end
def broadcast(message)
server.logger.debug { "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}" }
server.logger.debug { "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect.truncate(300)}" }
payload = { broadcasting: broadcasting, message: message, coder: coder }
ActiveSupport::Notifications.instrument("broadcast.action_cable", payload) do

View File

@ -9,6 +9,7 @@ module ActionCable
attr_accessor :connection_class, :worker_pool_size
attr_accessor :disable_request_forgery_protection, :allowed_request_origins, :allow_same_origin_as_host
attr_accessor :cable, :url, :mount_path
attr_accessor :precompile_assets
def initialize
@log_tags = []

View File

@ -36,12 +36,10 @@ module ActionCable
@executor.shuttingdown?
end
def work(connection)
def work(connection, &block)
self.connection = connection
run_callbacks :work do
yield
end
run_callbacks :work, &block
ensure
self.connection = nil
end

View File

@ -12,8 +12,8 @@ module ActionCable
end
end
def with_database_connections
connection.logger.tag(ActiveRecord::Base.logger) { yield }
def with_database_connections(&block)
connection.logger.tag(ActiveRecord::Base.logger, &block)
end
end
end

View File

@ -3,7 +3,7 @@
gem "pg", "~> 1.1"
require "pg"
require "thread"
require "digest/sha1"
require "openssl"
module ActionCable
module SubscriptionAdapter
@ -58,7 +58,7 @@ module ActionCable
private
def channel_identifier(channel)
channel.size > 63 ? Digest::SHA1.hexdigest(channel) : channel
channel.size > 63 ? OpenSSL::Digest::SHA1.hexdigest(channel) : channel
end
def listener

View File

@ -45,7 +45,7 @@ module ActionCable
def assert_broadcasts(stream, number, &block)
if block_given?
original_count = broadcasts_size(stream)
assert_nothing_raised(&block)
_assert_nothing_raised_or_warn("assert_broadcasts", &block)
new_count = broadcasts_size(stream)
actual_count = new_count - original_count
else
@ -106,7 +106,7 @@ module ActionCable
old_messages = new_messages
clear_messages(stream)
assert_nothing_raised(&block)
_assert_nothing_raised_or_warn("assert_broadcast_on", &block)
new_messages = broadcasts(stream)
clear_messages(stream)

View File

@ -10,4 +10,4 @@ Example:
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
Assets: $JAVASCRIPT_PATH/channels/chat_channel.js

View File

@ -13,39 +13,98 @@ module Rails
hook_for :test_framework
def create_channel_file
template "channel.rb", File.join("app/channels", class_path, "#{file_name}_channel.rb")
def create_channel_files
create_shared_channel_files
create_channel_file
if options[:assets]
if behavior == :invoke
template "javascript/index.js", "app/javascript/channels/index.js"
template "javascript/consumer.js", "app/javascript/channels/consumer.js"
if using_javascript?
if first_setup_required?
create_shared_channel_javascript_files
import_channels_in_javascript_entrypoint
if using_importmap?
pin_javascript_dependencies
elsif using_node?
install_javascript_dependencies
end
end
js_template "javascript/channel", File.join("app/javascript/channels", class_path, "#{file_name}_channel")
create_channel_javascript_file
import_channel_in_javascript_entrypoint
end
generate_application_cable_files
end
private
def create_shared_channel_files
return if behavior != :invoke
copy_file "#{__dir__}/templates/application_cable/channel.rb",
"app/channels/application_cable/channel.rb"
copy_file "#{__dir__}/templates/application_cable/connection.rb",
"app/channels/application_cable/connection.rb"
end
def create_channel_file
template "channel.rb",
File.join("app/channels", class_path, "#{file_name}_channel.rb")
end
def create_shared_channel_javascript_files
template "javascript/index.js", "app/javascript/channels/index.js"
template "javascript/consumer.js", "app/javascript/channels/consumer.js"
end
def create_channel_javascript_file
channel_js_path = File.join("app/javascript/channels", class_path, "#{file_name}_channel")
js_template "javascript/channel", channel_js_path
gsub_file "#{channel_js_path}.js", /\.\/consumer/, "channels/consumer" unless using_node?
end
def import_channels_in_javascript_entrypoint
append_to_file "app/javascript/application.js",
using_node? ? %(import "./channels"\n) : %(import "channels"\n)
end
def import_channel_in_javascript_entrypoint
append_to_file "app/javascript/channels/index.js",
using_node? ? %(import "./#{file_name}_channel"\n) : %(import "channels/#{file_name}_channel"\n)
end
def install_javascript_dependencies
say "Installing JavaScript dependencies", :green
run "yarn add @rails/actioncable"
end
def pin_javascript_dependencies
append_to_file "config/importmap.rb", <<-RUBY
pin "@rails/actioncable", to: "actioncable.esm.js"
pin_all_from "app/javascript/channels", under: "channels"
RUBY
end
def file_name
@_file_name ||= super.sub(/_channel\z/i, "")
end
# FIXME: Change these files to symlinks once RubyGems 2.5.0 is required.
def generate_application_cable_files
return if behavior != :invoke
def first_setup_required?
!root.join("app/javascript/channels/index.js").exist?
end
files = [
"application_cable/channel.rb",
"application_cable/connection.rb"
]
def using_javascript?
@using_javascript ||= options[:assets] && root.join("app/javascript").exist?
end
files.each do |name|
path = File.join("app/channels/", name)
template(name, path) if !File.exist?(path)
end
def using_node?
@using_node ||= root.join("package.json").exist?
end
def using_importmap?
@using_importmap ||= root.join("config/importmap.rb").exist?
end
def root
@root ||= Pathname(destination_root)
end
end
end

View File

@ -1,5 +1 @@
// Load all the channels within this directory and all subdirectories.
// Channel files must be named *_channel.js.
const channels = require.context('.', true, /_channel\.js$/)
channels.keys().forEach(channels)
// Import all the channels to be used by Action Cable

View File

@ -2,6 +2,7 @@
"name": "@rails/actioncable",
"version": "7.0.0-alpha",
"description": "WebSocket framework for Ruby on Rails.",
"module": "app/javascript/action_cable/index.js",
"main": "app/assets/javascripts/action_cable.js",
"files": [
"app/assets/javascripts/*.js",
@ -23,9 +24,8 @@
},
"homepage": "https://rubyonrails.org/",
"devDependencies": {
"babel-core": "^6.25.0",
"babel-plugin-external-helpers": "^6.22.0",
"babel-preset-env": "^1.6.0",
"@rollup/plugin-node-resolve": "^11.0.1",
"@rollup/plugin-commonjs": "^19.0.1",
"eslint": "^4.3.0",
"eslint-plugin-import": "^2.7.0",
"karma": "^3.1.1",
@ -34,11 +34,8 @@
"karma-sauce-launcher": "^1.2.0",
"mock-socket": "^2.0.0",
"qunit": "^2.8.0",
"rollup": "^0.58.2",
"rollup-plugin-babel": "^3.0.4",
"rollup-plugin-commonjs": "^9.1.0",
"rollup-plugin-node-resolve": "^3.3.0",
"rollup-plugin-uglify": "^3.0.0"
"rollup": "^2.35.1",
"rollup-plugin-terser": "^7.0.2"
},
"scripts": {
"prebuild": "yarn lint && bundle exec rake assets:codegen",

View File

@ -1,24 +1,47 @@
import babel from "rollup-plugin-babel"
import uglify from "rollup-plugin-uglify"
import resolve from "@rollup/plugin-node-resolve"
import { terser } from "rollup-plugin-terser"
const uglifyOptions = {
const terserOptions = {
mangle: false,
compress: false,
output: {
format: {
beautify: true,
indent_level: 2
}
}
export default {
input: "app/javascript/action_cable/index.js",
output: {
file: "app/assets/javascripts/action_cable.js",
format: "umd",
name: "ActionCable"
export default [
{
input: "app/javascript/action_cable/index.js",
output: [
{
file: "app/assets/javascripts/actioncable.js",
format: "umd",
name: "ActionCable"
},
{
file: "app/assets/javascripts/actioncable.esm.js",
format: "es"
}
],
plugins: [
resolve(),
terser(terserOptions)
]
},
plugins: [
babel(),
uglify(uglifyOptions)
]
}
{
input: "app/javascript/action_cable/index_with_name_deprecation.js",
output: {
file: "app/assets/javascripts/action_cable.js",
format: "umd",
name: "ActionCable"
},
breakOnWarning: false,
plugins: [
resolve(),
terser(terserOptions)
]
},
]

View File

@ -1,6 +1,5 @@
import babel from "rollup-plugin-babel"
import commonjs from "rollup-plugin-commonjs"
import resolve from "rollup-plugin-node-resolve"
import commonjs from "@rollup/plugin-commonjs"
import resolve from "@rollup/plugin-node-resolve"
export default {
input: "test/javascript/src/test.js",
@ -12,7 +11,6 @@ export default {
plugins: [
resolve(),
commonjs(),
babel()
commonjs()
]
}

View File

@ -201,7 +201,7 @@ class ClientTest < ActionCable::TestCase
end
def concurrently(enum)
enum.map { |*x| Concurrent::Future.execute { yield(*x) } }.map(&:value!)
enum.map { |*x| Concurrent::Promises.future { yield(*x) } }.map(&:value!)
end
def test_single_client

View File

@ -12,8 +12,8 @@ class RedisAdapterTest < ActionCable::TestCase
def cable_config
{ adapter: "redis", driver: "ruby" }.tap do |x|
if host = URI(ENV["REDIS_URL"] || "").hostname
x[:host] = host
if host = ENV["REDIS_URL"]
x[:url] = host
end
end
end
@ -29,7 +29,8 @@ class RedisAdapterTest::AlternateConfiguration < RedisAdapterTest
def cable_config
alt_cable_config = super.dup
alt_cable_config.delete(:url)
alt_cable_config.merge(host: URI(ENV["REDIS_URL"] || "").hostname || "127.0.0.1", port: 6379, db: 12)
url = URI(ENV["REDIS_URL"] || "")
alt_cable_config.merge(host: url.hostname || "127.0.0.1", port: url.port || 6379, db: 12)
end
end

View File

@ -7,11 +7,6 @@ require "active_support/testing/method_call_assertions"
require "puma"
require "rack/mock"
begin
require "byebug"
rescue LoadError
end
# Require all the stubs and models
Dir[File.expand_path("stubs/*.rb", __dir__)].each { |file| require file }

View File

@ -1,4 +1,38 @@
* Add `attachments` to the list of permitted parameters for inbound emails conductor.
When using the conductor to test inbound emails with attachments, this prevents an
unpermitted parameter warning in default configurations, and prevents errors for
applications that set:
```ruby
config.action_controller.action_on_unpermitted_parameters = :raise
```
*David Jones*, *Dana Henke*
* Add ability to configure ActiveStorage service
for storing email raw source.
```yml
# config/storage.yml
incoming_emails:
service: Disk
root: /secure/dir/for/emails/only
```
```ruby
config.action_mailbox.storage_service = :incoming_emails
```
*Yurii Rashkovskii*
* Add ability to incinerate an inbound message through the conductor interface.
*Santiago Bartesaghi*
* OpenSSL constants are now used for Digest computations.
*Dirkjan Bussink*
Please check [6-1-stable](https://github.com/rails/rails/blob/6-1-stable/actionmailbox/CHANGELOG.md) for previous changes.

View File

@ -20,14 +20,18 @@ module Rails
private
def new_mail
Mail.new(params.require(:mail).permit(:from, :to, :cc, :bcc, :x_original_to, :in_reply_to, :subject, :body).to_h).tap do |mail|
Mail.new(mail_params.except(:attachments).to_h).tap do |mail|
mail[:bcc]&.include_in_headers = true
params[:mail][:attachments].to_a.each do |attachment|
mail_params[:attachments].to_a.each do |attachment|
mail.add_file(filename: attachment.original_filename, content: attachment.read)
end
end
end
def mail_params
params.require(:mail).permit(:from, :to, :cc, :bcc, :x_original_to, :in_reply_to, :subject, :body, attachments: [])
end
def create_inbound_email(mail)
ActionMailbox::InboundEmail.create_and_extract_message_id!(mail.to_s)
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
module Rails
# Incinerating will destroy an email that is due and has already been processed.
class Conductor::ActionMailbox::IncineratesController < Rails::Conductor::BaseController
def create
ActionMailbox::InboundEmail.find(params[:inbound_email_id]).incinerate
redirect_to main_app.rails_conductor_inbound_emails_url
end
end
end

View File

@ -29,7 +29,7 @@ module ActionMailbox
include Incineratable, MessageId, Routable
has_one_attached :raw_email
has_one_attached :raw_email, service: ActionMailbox.storage_service
enum status: %i[ pending processing delivered failed bounced ]
def mail

View File

@ -14,7 +14,7 @@ module ActionMailbox::InboundEmail::MessageId
# 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)
message_checksum = Digest::SHA1.hexdigest(source)
message_checksum = OpenSSL::Digest::SHA1.hexdigest(source)
message_id = extract_message_id(source) || generate_missing_message_id(message_checksum)
create! raw_email: create_and_upload_raw_email!(source),
@ -35,7 +35,8 @@ module ActionMailbox::InboundEmail::MessageId
end
def create_and_upload_raw_email!(source)
ActiveStorage::Blob.create_and_upload! io: StringIO.new(source), filename: "message.eml", content_type: "message/rfc822"
ActiveStorage::Blob.create_and_upload! io: StringIO.new(source), filename: "message.eml", content_type: "message/rfc822",
service_name: ActionMailbox.storage_service
end
end
end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module ActionMailbox
class Record < ActiveRecord::Base #:nodoc:
class Record < ActiveRecord::Base # :nodoc:
self.abstract_class = true
end
end

View File

@ -4,7 +4,7 @@
<ul>
<li><%= button_to "Route again", main_app.rails_conductor_inbound_email_reroute_path(@inbound_email), method: :post %></li>
<li>Incinerate</li>
<li><%= button_to "Incinerate", main_app.rails_conductor_inbound_email_incinerate_path(@inbound_email), method: :post %></li>
</ul>
<details>

View File

@ -21,5 +21,6 @@ Rails.application.routes.draw do
post "inbound_emails/sources", to: "inbound_emails/sources#create", as: :rails_conductor_inbound_email_sources
post ":inbound_email_id/reroute" => "reroutes#create", as: :rails_conductor_inbound_email_reroute
post ":inbound_email_id/incinerate" => "incinerates#create", as: :rails_conductor_inbound_email_incinerate
end
end

View File

@ -14,4 +14,5 @@ module ActionMailbox
mattr_accessor :incinerate, default: true
mattr_accessor :incinerate_after, default: 30.days
mattr_accessor :queues, default: {}
mattr_accessor :storage_service
end

View File

@ -77,7 +77,7 @@ module ActionMailbox
@inbound_email = inbound_email
end
def perform_processing #:nodoc:
def perform_processing # :nodoc:
track_status_of_inbound_email do
run_callbacks :process do
process
@ -92,7 +92,7 @@ module ActionMailbox
# Overwrite in subclasses
end
def finished_processing? #:nodoc:
def finished_processing? # :nodoc:
inbound_email.delivered? || inbound_email.bounced?
end

View File

@ -20,6 +20,8 @@ module ActionMailbox
config.action_mailbox.queues = ActiveSupport::InheritableOptions.new \
incineration: :action_mailbox_incineration, routing: :action_mailbox_routing
config.action_mailbox.storage_service = nil
initializer "action_mailbox.config" do
config.after_initialize do |app|
ActionMailbox.logger = app.config.action_mailbox.logger || Rails.logger
@ -27,6 +29,7 @@ module ActionMailbox
ActionMailbox.incinerate_after = app.config.action_mailbox.incinerate_after || 30.days
ActionMailbox.queues = app.config.action_mailbox.queues || {}
ActionMailbox.ingress = app.config.action_mailbox.ingress
ActionMailbox.storage_service = app.config.action_mailbox.storage_service
end
end
end

View File

@ -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 6.0
config.load_defaults 7.0
# Settings in config/environments/* take precedence over those specified here.
# Application configuration can go into files in config/initializers

View File

@ -63,8 +63,4 @@ Rails.application.configure do
# Annotate rendered view with file names
# config.action_view.annotate_rendered_view_with_filenames = 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

View File

@ -23,7 +23,7 @@ Rails.application.configure do
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
# Compress JavaScripts and CSS.
config.assets.js_compressor = :uglifier
config.assets.js_compressor = :terser
# config.assets.css_compressor = :sass
# Do not fallback to assets pipeline if a precompiled asset is missed.

View File

@ -46,4 +46,7 @@ Rails.application.configure do
# Annotate rendered view with file names
# config.action_view.annotate_rendered_view_with_filenames = true
# Raise error if unpermitted parameters are sent
config.action_controller.action_on_unpermitted_parameters = :raise
end

View File

@ -6,6 +6,10 @@ local:
service: Disk
root: <%= Rails.root.join("storage") %>
test_email:
service: Disk
root: <%= Rails.root.join("tmp/storage_email") %>
# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
# amazon:
# service: S3

View File

@ -7,7 +7,6 @@ require_relative "../test/dummy/config/environment"
ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)]
require "rails/test_help"
require "byebug"
require "webmock/minitest"
require "rails/test_unit/reporter"

View File

@ -44,5 +44,41 @@ module ActionMailbox
end
end
end
test "email gets saved to the configured storage service" do
ActionMailbox.storage_service = :test_email
assert_equal(:test_email, ActionMailbox.storage_service)
email = create_inbound_email_from_fixture("welcome.eml")
storage_service = ActiveStorage::Blob.services.fetch(ActionMailbox.storage_service)
raw = email.raw_email_blob
# Not present in the main storage
assert_not(ActiveStorage::Blob.service.exist?(raw.key))
# Present in the email storage
assert(storage_service.exist?(raw.key))
ensure
ActionMailbox.storage_service = nil
end
test "email gets saved to the default storage service, even if it gets changed" do
default_service = ActiveStorage::Blob.service
ActiveStorage::Blob.service = ActiveStorage::Blob.services.fetch(:test_email)
# Doesn't change ActionMailbox.storage_service
assert_nil(ActionMailbox.storage_service)
email = create_inbound_email_from_fixture("welcome.eml")
raw = email.raw_email_blob
# Not present in the (previously) default storage
assert_not(default_service.exist?(raw.key))
# Present in the current default storage (email)
assert(ActiveStorage::Blob.service.exist?(raw.key))
ensure
ActiveStorage::Blob.service = default_service
end
end
end

View File

@ -132,11 +132,11 @@ module ActionMailbox
end
test "missing route" do
inbound_email = create_inbound_email_from_mail(to: "going-nowhere@example.com", subject: "This is a reply")
assert_raises(ActionMailbox::Router::RoutingError) do
inbound_email = create_inbound_email_from_mail(to: "going-nowhere@example.com", subject: "This is a reply")
@router.route inbound_email
assert inbound_email.bounced?
end
assert inbound_email.bounced?
end
test "invalid address" do

View File

@ -555,7 +555,7 @@ module ActionMailer
# through a callback when you call <tt>:deliver</tt> on the <tt>Mail::Message</tt>,
# calling +deliver_mail+ directly and passing a <tt>Mail::Message</tt> will do
# nothing except tell the logger you sent the email.
def deliver_mail(mail) #:nodoc:
def deliver_mail(mail) # :nodoc:
ActiveSupport::Notifications.instrument("deliver.action_mailer") do |payload|
set_payload_for_mail(payload, mail)
yield # Let Mail do the delivery actions
@ -606,7 +606,7 @@ module ActionMailer
@_message = Mail.new
end
def process(method_name, *args) #:nodoc:
def process(method_name, *args) # :nodoc:
payload = {
mailer: self.class.name,
action: method_name,
@ -619,7 +619,7 @@ module ActionMailer
end
end
class NullMail #:nodoc:
class NullMail # :nodoc:
def body; "" end
def header; {} end

View File

@ -20,7 +20,7 @@ module ActionMailer
MSG
end
def perform(mailer, mail_method, delivery_method, *args) #:nodoc:
def perform(mailer, mail_method, delivery_method, *args) # :nodoc:
mailer.constantize.public_send(mail_method, *args).send(delivery_method)
end
ruby2_keywords(:perform)

View File

@ -17,15 +17,15 @@ module ActionMailer
include Base64
def self.previewing_email(message) #:nodoc:
def self.previewing_email(message) # :nodoc:
new(message).transform!
end
def initialize(message) #:nodoc:
def initialize(message) # :nodoc:
@message = message
end
def transform! #:nodoc:
def transform! # :nodoc:
return message if html_part.blank?
html_part.body = html_part.decoded.gsub(PATTERN) do |match|

View File

@ -15,7 +15,7 @@ module ActionMailer
# Notifier.welcome(User.first).deliver_later # enqueue email delivery as a job through Active Job
# Notifier.welcome(User.first).message # a Mail::Message object
class MessageDelivery < Delegator
def initialize(mailer_class, action, *args) #:nodoc:
def initialize(mailer_class, action, *args) # :nodoc:
@mailer_class, @action, @args = mailer_class, action, args
# The mail is only processed if we try to call any methods on it.
@ -26,12 +26,12 @@ module ActionMailer
ruby2_keywords(:initialize)
# Method calls are delegated to the Mail::Message that's ready to deliver.
def __getobj__ #:nodoc:
def __getobj__ # :nodoc:
@mail_message ||= processed_mailer.message
end
# Unused except for delegator internals (dup, marshalling).
def __setobj__(mail_message) #:nodoc:
def __setobj__(mail_message) # :nodoc:
@mail_message = mail_message
end

View File

@ -3,7 +3,7 @@
require "active_support/descendants_tracker"
module ActionMailer
module Previews #:nodoc:
module Previews # :nodoc:
extend ActiveSupport::Concern
included do

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
module ActionMailer #:nodoc:
module ActionMailer # :nodoc:
# Provides +rescue_from+ for mailers. Wraps mailer action processing,
# mail job processing, and mail delivery.
module Rescuable
@ -8,12 +8,12 @@ module ActionMailer #:nodoc:
include ActiveSupport::Rescuable
class_methods do
def handle_exception(exception) #:nodoc:
def handle_exception(exception) # :nodoc:
rescue_with_handler(exception) || raise(exception)
end
end
def handle_exceptions #:nodoc:
def handle_exceptions # :nodoc:
yield
rescue => exception
rescue_with_handler(exception) || raise

View File

@ -1,6 +1,81 @@
* Drop support for the `SERVER_ADDR` header
* Use a static error message when raising `ActionDispatch::Http::Parameters::ParseError`
to avoid inadvertently logging the HTTP request body at the `fatal` level when it contains
malformed JSON.
Following up https://github.com/rack/rack/pull/1573 and https://github.com/rails/rails/pull/42349
Fixes #41145
*Aaron Lahey*
* Add `Middleware#delete!` to delete middleware or raise if not found.
`Middleware#delete!` works just like `Middleware#delete` but will
raise an error if the middleware isn't found.
*Alex Ghiculescu*, *Petrik de Heus*, *Junichi Sato*
* Raise error on unpermitted open redirects.
Add `allow_other_host` options to `redirect_to`.
Opt in to this behaviour with `ActionController::Base.raise_on_open_redirects = true`.
*Gannon McGibbon*
* Deprecate `poltergeist` and `webkit` (capybara-webkit) driver registration for system testing (they will be removed in Rails 7.1). Add `cuprite` instead.
[Poltergeist](https://github.com/teampoltergeist/poltergeist) and [capybara-webkit](https://github.com/thoughtbot/capybara-webkit) are already not maintained. These usage in Rails are removed for avoiding confusing users.
[Cuprite](https://github.com/rubycdp/cuprite) is a good alternative to Poltergeist. Some guide descriptions are replaced from Poltergeist to Cuprite.
*Yusuke Iwaki*
* Exclude additional flash types from `ActionController::Base.action_methods`.
Ensures that additional flash types defined on ActionController::Base subclasses
are not listed as actions on that controller.
class MyController < ApplicationController
add_flash_types :hype
end
MyController.action_methods.include?('hype') # => false
*Gavin Morrice*
* OpenSSL constants are now used for Digest computations.
*Dirkjan Bussink*
* Remove IE6-7-8 file download related hack/fix from ActionController::DataStreaming module.
Due to the age of those versions of IE this fix is no longer relevant, more importantly it creates an edge-case for unexpected Cache-Control headers.
*Tadas Sasnauskas*
* Configuration setting to skip logging an uncaught exception backtrace when the exception is
present in `rescued_responses`.
It may be too noisy to get all backtraces logged for applications that manage uncaught
exceptions via `rescued_responses` and `exceptions_app`.
`config.action_dispatch.log_rescued_responses` (defaults to `true`) can be set to `false` in
this case, so that only exceptions not found in `rescued_responses` will be logged.
*Alexander Azarov*, *Mike Dalessio*
* Ignore file fixtures on `db:fixtures:load`.
*Kevin Sjöberg*
* Fix ActionController::Live controller test deadlocks by removing the body buffer size limit for tests.
*Dylan Thacker-Smith*
* New `ActionController::ConditionalGet#no_store` method to set HTTP cache control `no-store` directive.
*Tadas Sasnauskas*
* Drop support for the `SERVER_ADDR` header.
Following up https://github.com/rack/rack/pull/1573 and https://github.com/rails/rails/pull/42349.
*Ricardo Díaz*
@ -8,7 +83,7 @@
*Gannon McGibbon*
* Add `cache_control: {}` option to `fresh_when` and `stale?`
* Add `cache_control: {}` option to `fresh_when` and `stale?`.
Works as a shortcut to set `response.cache_control` with the above methods.
@ -22,7 +97,7 @@
* Add support for 'require-trusted-types-for' and 'trusted-types' headers.
Fixes #42034
Fixes #42034.
*lfalcao*
@ -91,7 +166,7 @@
*Janko Marohnić*
* Allow anything with `#to_str` (like `Addressable::URI`) as a `redirect_to` location
* Allow anything with `#to_str` (like `Addressable::URI`) as a `redirect_to` location.
*ojab*
@ -103,7 +178,7 @@
as `RemoteIp` middleware behaves inconsistently depending on whether this is configured
with a single value or an enumerable.
Fixes #40772
Fixes #40772.
*Christian Sutter*

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module AbstractController
module AssetPaths #:nodoc:
module AssetPaths # :nodoc:
extend ActiveSupport::Concern
included do

View File

@ -9,34 +9,20 @@ require "active_support/core_ext/module/attr_internal"
module AbstractController
# Raised when a non-existing controller action is triggered.
class ActionNotFound < StandardError
attr_reader :controller, :action
def initialize(message = nil, controller = nil, action = nil)
attr_reader :controller, :action # :nodoc:
def initialize(message = nil, controller = nil, action = nil) # :nodoc:
@controller = controller
@action = action
super(message)
end
class Correction
def initialize(error)
@error = error
if defined?(DidYouMean::Correctable) && defined?(DidYouMean::SpellChecker)
include DidYouMean::Correctable # :nodoc:
def corrections # :nodoc:
@corrections ||= DidYouMean::SpellChecker.new(dictionary: controller.class.action_methods).correct(action)
end
def corrections
if @error.action
maybe_these = @error.controller.class.action_methods
maybe_these.sort_by { |n|
DidYouMean::Jaro.distance(@error.action.to_s, n)
}.reverse.first(4)
else
[]
end
end
end
# We may not have DYM, and DYM might not let us register error handlers
if defined?(DidYouMean) && DidYouMean.respond_to?(:correct_error)
DidYouMean.correct_error(self, Correction)
end
end

View File

@ -142,8 +142,8 @@ module AbstractController
end
end
def instrument_fragment_cache(name, key) # :nodoc:
ActiveSupport::Notifications.instrument("#{name}.#{instrument_name}", instrument_payload(key)) { yield }
def instrument_fragment_cache(name, key, &block) # :nodoc:
ActiveSupport::Notifications.instrument("#{name}.#{instrument_name}", instrument_payload(key), &block)
end
end
end

View File

@ -10,6 +10,7 @@ module AbstractController
def #{sym}(*args, &block)
custom(Mime[:#{sym}], *args, &block)
end
ruby2_keywords(:#{sym})
RUBY
end
@ -22,7 +23,7 @@ module AbstractController
end
private
def method_missing(symbol, &block)
def method_missing(symbol, *args, &block)
unless mime_constant = Mime[symbol]
raise NoMethodError, "To respond to a custom format, register it as a MIME type first: " \
"https://guides.rubyonrails.org/action_controller_overview.html#restful-downloads. " \
@ -33,10 +34,11 @@ module AbstractController
if Mime::SET.include?(mime_constant)
AbstractController::Collector.generate_method_for_mime(mime_constant)
send(symbol, &block)
public_send(symbol, *args, &block)
else
super
end
end
ruby2_keywords(:method_missing)
end
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
module AbstractController
class Error < StandardError #:nodoc:
class Error < StandardError # :nodoc:
end
end

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true
require "active_support/dependencies"
require "active_support/core_ext/name_error"
module AbstractController
module Helpers

View File

@ -3,7 +3,7 @@
require "active_support/benchmarkable"
module AbstractController
module Logger #:nodoc:
module Logger # :nodoc:
extend ActiveSupport::Concern
included do

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
require "active_support/core_ext/module/introspection"
module AbstractController
module Railties
module RoutesHelpers

View File

@ -18,10 +18,6 @@ module ActionController
end
autoload_under "metal" do
eager_autoload do
autoload :Live
end
autoload :ConditionalGet
autoload :ContentSecurityPolicy
autoload :Cookies
@ -37,9 +33,11 @@ module ActionController
autoload :BasicImplicitRender
autoload :ImplicitRender
autoload :Instrumentation
autoload :Live
autoload :Logging
autoload :MimeResponds
autoload :ParamsWrapper
autoload :QueryTags
autoload :Redirecting
autoload :Renderers
autoload :Rendering

View File

@ -37,7 +37,7 @@ module ActionController
# == Renders
#
# The default API Controller stack includes all renderers, which means you
# can use <tt>render :json</tt> and brothers freely in your controllers. Keep
# can use <tt>render :json</tt> and siblings freely in your controllers. Keep
# in mind that templates are not going to be rendered, so you need to ensure
# your controller is calling either <tt>render</tt> or <tt>redirect_to</tt> in
# all actions, otherwise it will return 204 No Content.

View File

@ -2,8 +2,6 @@
require "active_support/core_ext/array/extract_options"
require "action_dispatch/middleware/stack"
require "action_dispatch/http/request"
require "action_dispatch/http/response"
module ActionController
# Extend ActionDispatch middleware stack to make it aware of options
@ -13,8 +11,8 @@ module ActionController
# use AuthenticationMiddleware, except: [:index, :show]
# end
#
class MiddlewareStack < ActionDispatch::MiddlewareStack #:nodoc:
class Middleware < ActionDispatch::MiddlewareStack::Middleware #:nodoc:
class MiddlewareStack < ActionDispatch::MiddlewareStack # :nodoc:
class Middleware < ActionDispatch::MiddlewareStack::Middleware # :nodoc:
def initialize(klass, args, actions, strategy, block)
@actions = actions
@strategy = strategy
@ -184,7 +182,7 @@ module ActionController
response_body || response.committed?
end
def dispatch(name, request, response) #:nodoc:
def dispatch(name, request, response) # :nodoc:
set_request!(request)
set_response!(response)
process(name)
@ -196,12 +194,12 @@ module ActionController
@_response = response
end
def set_request!(request) #:nodoc:
def set_request!(request) # :nodoc:
@_request = request
@_request.controller_instance = self
end
def to_a #:nodoc:
def to_a # :nodoc:
response.to_a
end

View File

@ -115,6 +115,7 @@ module ActionController
# before_action { fresh_when @article, template: 'widgets/show' }
#
def fresh_when(object = nil, etag: nil, weak_etag: nil, strong_etag: nil, last_modified: nil, public: false, cache_control: {}, template: nil)
response.cache_control.delete(:no_store)
weak_etag ||= etag || object unless strong_etag
last_modified ||= object.try(:updated_at) || object.try(:maximum, :updated_at)
@ -273,6 +274,7 @@ module ActionController
#
# The method will also ensure an HTTP Date header for client compatibility.
def expires_in(seconds, options = {})
response.cache_control.delete(:no_store)
response.cache_control.merge!(
max_age: seconds,
public: options.delete(:public),
@ -309,6 +311,12 @@ module ActionController
public: public)
end
# Sets an HTTP 1.1 Cache-Control header of <tt>no-store</tt>. This means the
# resource may not be stored in any cache.
def no_store
response.cache_control.replace(no_store: true)
end
private
def combine_etags(validator, options)
[validator, *etaggers.map { |etagger| instance_exec(options, &etagger) }].compact

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
module ActionController #:nodoc:
module ActionController # :nodoc:
module ContentSecurityPolicy
# TODO: Documentation
extend ActiveSupport::Concern

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
module ActionController #:nodoc:
module ActionController # :nodoc:
module Cookies
extend ActiveSupport::Concern

View File

@ -3,7 +3,7 @@
require "action_controller/metal/exceptions"
require "action_dispatch/http/content_disposition"
module ActionController #:nodoc:
module ActionController # :nodoc:
# Methods for sending arbitrary data and for streaming files to the browser,
# instead of rendering.
module DataStreaming
@ -11,8 +11,8 @@ module ActionController #:nodoc:
include ActionController::Rendering
DEFAULT_SEND_FILE_TYPE = "application/octet-stream" #:nodoc:
DEFAULT_SEND_FILE_DISPOSITION = "attachment" #:nodoc:
DEFAULT_SEND_FILE_TYPE = "application/octet-stream" # :nodoc:
DEFAULT_SEND_FILE_DISPOSITION = "attachment" # :nodoc:
private
# Sends the file. This uses a server-appropriate method (such as X-Sendfile)
@ -66,7 +66,7 @@ module ActionController #:nodoc:
# https://www.mnot.net/cache_docs/ for an overview of web caching and
# https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
# for the Cache-Control header spec.
def send_file(path, options = {}) #:doc:
def send_file(path, options = {}) # :doc:
raise MissingFile, "Cannot read file #{path}" unless File.file?(path) && File.readable?(path)
options[:filename] ||= File.basename(path) unless options[:url_based_filename]
@ -106,7 +106,7 @@ module ActionController #:nodoc:
# send_data image.data, type: image.content_type, disposition: 'inline'
#
# See +send_file+ for more information on HTTP Content-* headers and caching.
def send_data(data, options = {}) #:doc:
def send_data(data, options = {}) # :doc:
send_file_headers! options
render options.slice(:status, :content_type).merge(body: data)
end
@ -138,14 +138,6 @@ module ActionController #:nodoc:
end
headers["Content-Transfer-Encoding"] = "binary"
# Fix a problem with IE 6.0 on opening downloaded files:
# If Cache-Control: no-cache is set (which Rails does by default),
# IE removes the file it just downloaded from its cache immediately
# after it displays the "open/save" dialog, which means that if you
# hit "open" the file isn't there anymore when the application that
# is called for handling the download is run, so let's workaround that
response.cache_control[:public] ||= false
end
end
end

View File

@ -44,7 +44,7 @@ module ActionController
# template digest from the ETag.
def pick_template_for_etag(options)
unless options[:template] == false
options[:template] || "#{controller_path}/#{action_name}"
options[:template] || lookup_context.find_all(action_name, _prefixes).first&.virtual_path
end
end

View File

@ -1,20 +1,20 @@
# frozen_string_literal: true
module ActionController
class ActionControllerError < StandardError #:nodoc:
class ActionControllerError < StandardError # :nodoc:
end
class BadRequest < ActionControllerError #:nodoc:
class BadRequest < ActionControllerError # :nodoc:
def initialize(msg = nil)
super(msg)
set_backtrace $!.backtrace if $!
end
end
class RenderError < ActionControllerError #:nodoc:
class RenderError < ActionControllerError # :nodoc:
end
class RoutingError < ActionControllerError #:nodoc:
class RoutingError < ActionControllerError # :nodoc:
attr_reader :failures
def initialize(message, failures = [])
super(message)
@ -22,7 +22,7 @@ module ActionController
end
end
class UrlGenerationError < ActionControllerError #:nodoc:
class UrlGenerationError < ActionControllerError # :nodoc:
attr_reader :routes, :route_name, :method_name
def initialize(message, routes = nil, route_name = nil, method_name = nil)
@ -33,44 +33,33 @@ module ActionController
super(message)
end
class Correction
def initialize(error)
@error = error
end
if defined?(DidYouMean::Correctable) && defined?(DidYouMean::SpellChecker)
include DidYouMean::Correctable
def corrections
if @error.method_name
maybe_these = @error.routes.named_routes.helper_names.grep(/#{@error.route_name}/)
maybe_these -= [@error.method_name.to_s] # remove exact match
@corrections ||= begin
maybe_these = routes&.named_routes&.helper_names&.grep(/#{route_name}/) || []
maybe_these -= [method_name.to_s] # remove exact match
maybe_these.sort_by { |n|
DidYouMean::Jaro.distance(@error.route_name, n)
}.reverse.first(4)
else
[]
DidYouMean::SpellChecker.new(dictionary: maybe_these).correct(route_name)
end
end
end
# We may not have DYM, and DYM might not let us register error handlers
if defined?(DidYouMean) && DidYouMean.respond_to?(:correct_error)
DidYouMean.correct_error(self, Correction)
end
end
class MethodNotAllowed < ActionControllerError #:nodoc:
class MethodNotAllowed < ActionControllerError # :nodoc:
def initialize(*allowed_methods)
super("Only #{allowed_methods.to_sentence} requests are allowed.")
end
end
class NotImplemented < MethodNotAllowed #:nodoc:
class NotImplemented < MethodNotAllowed # :nodoc:
end
class MissingFile < ActionControllerError #:nodoc:
class MissingFile < ActionControllerError # :nodoc:
end
class SessionOverflowError < ActionControllerError #:nodoc:
class SessionOverflowError < ActionControllerError # :nodoc:
DEFAULT_MESSAGE = "Your session data is larger than the data column in which it is to be stored. You must increase the size of your data column if you intend to store large data."
def initialize(message = nil)
@ -78,10 +67,10 @@ module ActionController
end
end
class UnknownHttpMethod < ActionControllerError #:nodoc:
class UnknownHttpMethod < ActionControllerError # :nodoc:
end
class UnknownFormat < ActionControllerError #:nodoc:
class UnknownFormat < ActionControllerError # :nodoc:
end
# Raised when a nested respond_to is triggered and the content types of each
@ -102,6 +91,6 @@ module ActionController
end
end
class MissingExactTemplate < UnknownFormat #:nodoc:
class MissingExactTemplate < UnknownFormat # :nodoc:
end
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
module ActionController #:nodoc:
module ActionController # :nodoc:
module Flash
extend ActiveSupport::Concern
@ -41,10 +41,14 @@ module ActionController #:nodoc:
self._flash_types += [type]
end
end
def action_methods # :nodoc:
@action_methods ||= super - _flash_types.map(&:to_s).to_set
end
end
private
def redirect_to(options = {}, response_options_and_flash = {}) #:doc:
def redirect_to(options = {}, response_options_and_flash = {}) # :doc:
self.class._flash_types.each do |flash_type|
if type = response_options_and_flash.delete(flash_type)
flash[flash_type] = type

View File

@ -138,11 +138,11 @@ module ActionController
#
# === Simple \Digest example
#
# require "digest/md5"
# require "openssl"
# class PostsController < ApplicationController
# REALM = "SuperSecret"
# USERS = {"dhh" => "secret", #plain text password
# "dap" => Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password
# "dap" => OpenSSL::Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password
#
# before_action :authenticate, except: [:index]
#
@ -230,12 +230,12 @@ module ActionController
# of a plain-text password.
def expected_response(http_method, uri, credentials, password, password_is_ha1 = true)
ha1 = password_is_ha1 ? password : ha1(credentials, password)
ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(":"))
::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(":"))
ha2 = OpenSSL::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(":"))
OpenSSL::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(":"))
end
def ha1(credentials, password)
::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(":"))
OpenSSL::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(":"))
end
def encode_credentials(http_method, credentials, password, password_is_ha1)
@ -309,7 +309,7 @@ module ActionController
def nonce(secret_key, time = Time.now)
t = time.to_i
hashed = [t, secret_key]
digest = ::Digest::MD5.hexdigest(hashed.join(":"))
digest = OpenSSL::Digest::MD5.hexdigest(hashed.join(":"))
::Base64.strict_encode64("#{t}:#{digest}")
end
@ -326,7 +326,7 @@ module ActionController
# Opaque based on digest of secret key
def opaque(secret_key)
::Digest::MD5.hexdigest(secret_key)
OpenSSL::Digest::MD5.hexdigest(secret_key)
end
end

View File

@ -99,7 +99,7 @@ module ActionController
# A hook which allows other frameworks to log what happened during
# controller process action. This method should return an array
# with the messages to be added.
def log_process_action(payload) #:nodoc:
def log_process_action(payload) # :nodoc:
messages, view_runtime = [], payload[:view_runtime]
messages << ("Views: %.1fms" % view_runtime.to_f) if view_runtime
messages

View File

@ -124,9 +124,14 @@ module ActionController
class ClientDisconnected < RuntimeError
end
class Buffer < ActionDispatch::Response::Buffer #:nodoc:
class Buffer < ActionDispatch::Response::Buffer # :nodoc:
include MonitorMixin
class << self
attr_accessor :queue_size
end
@queue_size = 10
# Ignore that the client has disconnected.
#
# If this value is `true`, calling `write` after the client
@ -136,7 +141,7 @@ module ActionController
attr_accessor :ignore_disconnect
def initialize(response)
super(response, SizedQueue.new(10))
super(response, build_queue(self.class.queue_size))
@error_callback = lambda { true }
@cv = new_cond
@aborted = false
@ -219,9 +224,13 @@ module ActionController
yield str
end
end
def build_queue(queue_size)
queue_size ? SizedQueue.new(queue_size) : Queue.new
end
end
class Response < ActionDispatch::Response #:nodoc: all
class Response < ActionDispatch::Response # :nodoc: all
private
def before_committed
super

View File

@ -2,7 +2,7 @@
require "abstract_controller/collector"
module ActionController #:nodoc:
module ActionController # :nodoc:
module MimeResponds
# Without web-service support, an action which collects the data for displaying a list of people
# might look something like this:
@ -289,7 +289,7 @@ module ActionController #:nodoc:
@format = request.negotiate_mime(@responses.keys)
end
class VariantCollector #:nodoc:
class VariantCollector # :nodoc:
def initialize(variant = nil)
@variant = variant
@variants = {}

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
module ActionController #:nodoc:
module ActionController # :nodoc:
# HTTP Permissions Policy is a web standard for defining a mechanism to
# allow and deny the use of browser permissions in its own context, and
# in content within any <iframe> elements in the document.

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
module ActionController
module QueryTags # :nodoc:
extend ActiveSupport::Concern
included do
around_action :expose_controller_to_query_logs
end
private
def expose_controller_to_query_logs(&block)
ActiveRecord::QueryLogs.set_context(controller: self, &block)
end
end
end

View File

@ -7,6 +7,10 @@ module ActionController
include AbstractController::Logger
include ActionController::UrlFor
included do
mattr_accessor :raise_on_open_redirects, default: false
end
# Redirects the browser to the target specified in +options+. This parameter can be any one of:
#
# * <tt>Hash</tt> - The URL will be generated by calling url_for with the +options+.
@ -57,20 +61,22 @@ module ActionController
#
# redirect_to post_url(@post) and return
#
# Passing user input directly into +redirect_to+ is considered dangerous (eg. `redirect_to(params[:location])`).
# Passing user input directly into +redirect_to+ is considered dangerous (e.g. `redirect_to(params[:location])`).
# Always use regular expressions or a permitted list when redirecting to a user specified location.
def redirect_to(options = {}, response_options = {})
response_options[:allow_other_host] ||= _allow_other_host unless response_options.key?(:allow_other_host)
raise ActionControllerError.new("Cannot redirect to nil!") unless options
raise AbstractController::DoubleRenderError if response_body
self.status = _extract_redirect_to_status(options, response_options)
self.location = _compute_redirect_to_location(request, options)
self.location = _compute_safe_redirect_to_location(request, options, response_options)
self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
end
# Soft deprecated alias for <tt>redirect_back_or_to</tt> where the fallback_location location is supplied as a keyword argument instead
# of the first positional argument.
def redirect_back(fallback_location:, allow_other_host: true, **args)
def redirect_back(fallback_location:, allow_other_host: _allow_other_host, **args)
redirect_back_or_to fallback_location, allow_other_host: allow_other_host, **args
end
@ -96,13 +102,28 @@ module ActionController
#
# All other options that can be passed to #redirect_to are accepted as
# options and the behavior is identical.
def redirect_back_or_to(fallback_location, allow_other_host: true, **args)
referer = request.headers["Referer"]
redirect_to_referer = referer && (allow_other_host || _url_host_allowed?(referer))
redirect_to redirect_to_referer ? referer : fallback_location, **args
def redirect_back_or_to(fallback_location, allow_other_host: _allow_other_host, **options)
location = request.referer || fallback_location
location = fallback_location unless allow_other_host || _url_host_allowed?(request.referer)
allow_other_host = true if _allow_other_host && !allow_other_host # if the fallback is an open redirect
redirect_to location, allow_other_host: allow_other_host, **options
end
def _compute_redirect_to_location(request, options) #:nodoc:
def _compute_safe_redirect_to_location(request, options, response_options)
location = _compute_redirect_to_location(request, options)
if response_options[:allow_other_host] || _url_host_allowed?(location)
location
else
raise(ArgumentError, <<~MSG.squish)
Unsafe redirect #{location.truncate(100).inspect},
use :allow_other_host to redirect anyway.
MSG
end
end
def _compute_redirect_to_location(request, options) # :nodoc:
case options
# The scheme name consist of a letter followed by any combination of
# letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
@ -123,6 +144,10 @@ module ActionController
public :_compute_redirect_to_location
private
def _allow_other_host
!raise_on_open_redirects
end
def _extract_redirect_to_status(options, response_options)
if options.is_a?(Hash) && options.key?(:status)
Rack::Utils.status_code(options.delete(:status))

View File

@ -25,7 +25,7 @@ module ActionController
end
# Check for double render errors and set the content_type after rendering.
def render(*args) #:nodoc:
def render(*args) # :nodoc:
raise ::AbstractController::DoubleRenderError if response_body
super
end
@ -48,7 +48,7 @@ module ActionController
private
# Before processing, set the request formats in current controller formats.
def process_action(*) #:nodoc:
def process_action(*) # :nodoc:
self.formats = request.formats.filter_map(&:ref)
super
end

View File

@ -4,11 +4,11 @@ require "rack/session/abstract/id"
require "action_controller/metal/exceptions"
require "active_support/security_utils"
module ActionController #:nodoc:
class InvalidAuthenticityToken < ActionControllerError #:nodoc:
module ActionController # :nodoc:
class InvalidAuthenticityToken < ActionControllerError # :nodoc:
end
class InvalidCrossOriginRequest < ActionControllerError #:nodoc:
class InvalidCrossOriginRequest < ActionControllerError # :nodoc:
end
# Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks
@ -186,7 +186,7 @@ module ActionController #:nodoc:
end
private
class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
class NullSessionHash < Rack::Session::Abstract::SessionHash # :nodoc:
def initialize(req)
super(nil, req)
@data = {}
@ -205,7 +205,7 @@ module ActionController #:nodoc:
end
end
class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
class NullCookieJar < ActionDispatch::Cookies::CookieJar # :nodoc:
def write(*)
# nothing
end
@ -223,12 +223,14 @@ module ActionController #:nodoc:
end
class Exception
attr_accessor :warning_message
def initialize(controller)
@controller = controller
end
def handle_unverified_request
raise ActionController::InvalidAuthenticityToken
raise ActionController::InvalidAuthenticityToken, warning_message
end
end
end
@ -248,22 +250,31 @@ module ActionController #:nodoc:
mark_for_same_origin_verification!
if !verified_request?
if logger && log_warning_on_csrf_failure
if valid_request_origin?
logger.warn "Can't verify CSRF token authenticity."
else
logger.warn "HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})"
end
end
logger.warn unverified_request_warning_message if logger && log_warning_on_csrf_failure
handle_unverified_request
end
end
def handle_unverified_request # :doc:
forgery_protection_strategy.new(self).handle_unverified_request
protection_strategy = forgery_protection_strategy.new(self)
if protection_strategy.respond_to?(:warning_message)
protection_strategy.warning_message = unverified_request_warning_message
end
protection_strategy.handle_unverified_request
end
#:nodoc:
def unverified_request_warning_message # :nodoc:
if valid_request_origin?
"Can't verify CSRF token authenticity."
else
"HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})"
end
end
# :nodoc:
CROSS_ORIGIN_JAVASCRIPT_WARNING = "Security warning: an embedded " \
"<script> tag on another site requested protected JavaScript. " \
"If you know what you're doing, go ahead and disable forgery " \

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
module ActionController #:nodoc:
module ActionController # :nodoc:
# This module is responsible for providing +rescue_from+ helpers
# to controllers and configuring when detailed exceptions must be
# shown.

View File

@ -2,7 +2,7 @@
require "rack/chunked"
module ActionController #:nodoc:
module ActionController # :nodoc:
# Allows views to be streamed back to the client as they are rendered.
#
# By default, Rails renders views by first rendering the template

View File

@ -27,27 +27,12 @@ module ActionController
super("param is missing or the value is empty: #{param}")
end
class Correction
def initialize(error)
@error = error
if defined?(DidYouMean::Correctable) && defined?(DidYouMean::SpellChecker)
include DidYouMean::Correctable # :nodoc:
def corrections # :nodoc:
@corrections ||= DidYouMean::SpellChecker.new(dictionary: keys).correct(param.to_s)
end
def corrections
if @error.param && @error.keys
maybe_these = @error.keys
maybe_these.sort_by { |n|
DidYouMean::Jaro.distance(@error.param.to_s, n)
}.reverse.first(4)
else
[]
end
end
end
# We may not have DYM, and DYM might not let us register error handlers
if defined?(DidYouMean) && DidYouMean.respond_to?(:correct_error)
DidYouMean.correct_error(self, Correction)
end
end
@ -955,7 +940,7 @@ module ActionController
def each_element(object, &block)
case object
when Array
object.grep(Parameters).filter_map { |el| yield el }
object.grep(Parameters).filter_map(&block)
when Parameters
if object.nested_attributes?
object.each_nested_attribute(&block)

View File

@ -8,8 +8,10 @@ require "action_controller/railties/helpers"
require "action_view/railtie"
module ActionController
class Railtie < Rails::Railtie #:nodoc:
class Railtie < Rails::Railtie # :nodoc:
config.action_controller = ActiveSupport::OrderedOptions.new
config.action_controller.raise_on_open_redirects = false
config.action_controller.log_query_tags_around_actions = true
config.eager_load_namespaces << ActionController
@ -25,14 +27,19 @@ module ActionController
options = app.config.action_controller
ActiveSupport.on_load(:action_controller, run_once: true) do
ActionController::Parameters.permit_all_parameters = options.delete(:permit_all_parameters) { false }
ActionController::Parameters.permit_all_parameters = options.permit_all_parameters || false
if app.config.action_controller[:always_permitted_parameters]
ActionController::Parameters.always_permitted_parameters =
app.config.action_controller.delete(:always_permitted_parameters)
app.config.action_controller.always_permitted_parameters
end
ActionController::Parameters.action_on_unpermitted_parameters = options.delete(:action_on_unpermitted_parameters) do
(Rails.env.test? || Rails.env.development?) ? :log : false
action_on_unpermitted_parameters = options.action_on_unpermitted_parameters
if action_on_unpermitted_parameters.nil?
action_on_unpermitted_parameters = (Rails.env.test? || Rails.env.development?) ? :log : false
end
ActionController::Parameters.action_on_unpermitted_parameters = action_on_unpermitted_parameters
end
end
@ -55,6 +62,14 @@ module ActionController
extend ::AbstractController::Railties::RoutesHelpers.with(app.routes)
extend ::ActionController::Railties::Helpers
# Configs used in other initializers
options = options.except(
:log_query_tags_around_actions,
:permit_all_parameters,
:action_on_unpermitted_parameters,
:always_permitted_parameters
)
options.each do |k, v|
k = "#{k}="
if respond_to?(k)
@ -85,5 +100,27 @@ module ActionController
ActionController::Metal.descendants.each(&:action_methods) if config.eager_load
end
end
initializer "action_controller.query_log_tags" do |app|
query_logs_tags_enabled = app.config.respond_to?(:active_record) &&
app.config.active_record.query_log_tags_enabled &&
app.config.action_controller.log_query_tags_around_actions
if query_logs_tags_enabled
app.config.active_record.query_log_tags += [:controller, :action]
ActiveSupport.on_load(:action_controller) do
include ActionController::QueryTags
end
ActiveSupport.on_load(:active_record) do
ActiveRecord::QueryLogs.taggings.merge!(
controller: ->(context) { context[:controller].controller_name },
action: ->(context) { context[:controller].action_name },
namespaced_controller: ->(context) { context[:controller].class.name }
)
end
end
end
end
end

View File

@ -24,11 +24,14 @@ module ActionController
def new_controller_thread # :nodoc:
yield
end
# Avoid a deadlock from the queue filling up
Buffer.queue_size = nil
end
# ActionController::TestCase will be deprecated and moved to a gem in the future.
# Please use ActionDispatch::IntegrationTest going forward.
class TestRequest < ActionDispatch::TestRequest #:nodoc:
class TestRequest < ActionDispatch::TestRequest # :nodoc:
DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup
DEFAULT_ENV.delete "PATH_INFO"
@ -176,7 +179,7 @@ module ActionController
# Methods #destroy and #load! are overridden to avoid calling methods on the
# @store object, which does not exist for the TestSession class.
class TestSession < Rack::Session::Abstract::PersistedSecure::SecureSessionHash #:nodoc:
class TestSession < Rack::Session::Abstract::PersistedSecure::SecureSessionHash # :nodoc:
DEFAULT_OPTIONS = Rack::Session::Abstract::Persisted::DEFAULT_OPTIONS
def initialize(session = {})

View File

@ -187,13 +187,20 @@ module ActionDispatch
return if control.empty? && cache_control.empty? # Let middleware handle default behavior
if extras = control.delete(:extras)
cache_control[:extras] ||= []
cache_control[:extras] += extras
cache_control[:extras].uniq!
end
if cache_control.any?
# Any caching directive coming from a controller overrides
# no-cache/no-store in the default Cache-Control header.
control.delete(:no_cache)
control.delete(:no_store)
control.merge! cache_control
if extras = control.delete(:extras)
cache_control[:extras] ||= []
cache_control[:extras] += extras
cache_control[:extras].uniq!
end
control.merge! cache_control
end
options = []

View File

@ -2,7 +2,7 @@
require "active_support/core_ext/object/deep_dup"
module ActionDispatch #:nodoc:
module ActionDispatch # :nodoc:
class ContentSecurityPolicy
class Middleware
CONTENT_TYPE = "Content-Type"

View File

@ -13,8 +13,8 @@ module Mime
@symbols = []
end
def each
@mimes.each { |x| yield x }
def each(&block)
@mimes.each(&block)
end
def <<(type)
@ -42,9 +42,9 @@ module Mime
Type.lookup_by_extension(type)
end
def fetch(type)
def fetch(type, &block)
return type if type.is_a?(Type)
EXTENSION_LOOKUP.fetch(type.to_s) { |k| yield k }
EXTENSION_LOOKUP.fetch(type.to_s, &block)
end
end
@ -67,7 +67,7 @@ module Mime
@register_callbacks = []
# A simple helper class used in parsing the accept header.
class AcceptItem #:nodoc:
class AcceptItem # :nodoc:
attr_accessor :index, :name, :q
alias :to_s :name
@ -85,7 +85,7 @@ module Mime
end
end
class AcceptList #:nodoc:
class AcceptList # :nodoc:
def self.sort!(list)
list.sort!

View File

@ -17,8 +17,8 @@ module ActionDispatch
# Raised when raw data from the request cannot be parsed by the parser
# defined for request's content MIME type.
class ParseError < StandardError
def initialize
super($!.message)
def initialize(message = $!.message)
super(message)
end
end
@ -62,7 +62,7 @@ module ActionDispatch
end
alias :params :parameters
def path_parameters=(parameters) #:nodoc:
def path_parameters=(parameters) # :nodoc:
delete_header("action_dispatch.request.parameters")
parameters = Request::Utils.set_binary_encoding(self, parameters, parameters[:controller], parameters[:action])
@ -93,7 +93,7 @@ module ActionDispatch
strategy.call(raw_post)
rescue # JSON or Ruby code block errors.
log_parse_error_once
raise ParseError
raise ParseError, "Error occurred while parsing request parameters"
end
end

View File

@ -2,7 +2,7 @@
require "active_support/core_ext/object/deep_dup"
module ActionDispatch #:nodoc:
module ActionDispatch # :nodoc:
class PermissionsPolicy
class Middleware
CONTENT_TYPE = "Content-Type"

View File

@ -87,7 +87,7 @@ module ActionDispatch
controller_param = name.underscore
const_name = controller_param.camelize << "Controller"
begin
ActiveSupport::Dependencies.constantize(const_name)
const_name.constantize
rescue NameError => error
if error.missing_name == const_name || const_name.start_with?("#{error.missing_name}::")
raise MissingController.new(error.message, error.name)
@ -162,7 +162,7 @@ module ActionDispatch
set_header(routes.env_key, name.dup)
end
def request_method=(request_method) #:nodoc:
def request_method=(request_method) # :nodoc:
if check_method(request_method)
@request_method = set_header("REQUEST_METHOD", request_method)
end
@ -352,7 +352,7 @@ module ActionDispatch
FORM_DATA_MEDIA_TYPES.include?(media_type)
end
def body_stream #:nodoc:
def body_stream # :nodoc:
get_header("rack.input")
end
@ -360,7 +360,7 @@ module ActionDispatch
session.destroy
end
def session=(session) #:nodoc:
def session=(session) # :nodoc:
Session.set self, session
end

View File

@ -336,7 +336,7 @@ module ActionDispatch # :nodoc:
# Avoid having to pass an open file handle as the response body.
# Rack::Sendfile will usually intercept the response and uses
# the path directly, so there is no reason to open the file.
class FileBody #:nodoc:
class FileBody # :nodoc:
attr_reader :to_path
def initialize(path)

View File

@ -270,9 +270,10 @@ module ActionDispatch
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
# req.standard_port # => 80
def standard_port
case protocol
when "https://" then 443
else 80
if "https://" == protocol
443
else
80
end
end

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