aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--actioncable/CHANGELOG.md15
-rw-r--r--actioncable/lib/action_cable/channel/base.rb6
-rw-r--r--actioncable/lib/action_cable/channel/streams.rb32
-rw-r--r--actioncable/lib/action_cable/connection/base.rb38
-rw-r--r--actioncable/lib/action_cable/connection/internal_channel.rb4
-rw-r--r--actioncable/lib/action_cable/connection/message_buffer.rb4
-rw-r--r--actioncable/lib/action_cable/connection/subscriptions.rb25
-rw-r--r--actioncable/lib/action_cable/server/base.rb12
-rw-r--r--actioncable/lib/action_cable/server/broadcasting.rb19
-rw-r--r--actioncable/lib/action_cable/server/configuration.rb2
-rw-r--r--actioncable/test/channel/base_test.rb4
-rw-r--r--actioncable/test/channel/rejection_test.rb2
-rw-r--r--actioncable/test/channel/stream_test.rb160
-rw-r--r--actioncable/test/client_test.rb36
-rw-r--r--actioncable/test/connection/identifier_test.rb6
-rw-r--r--actioncable/test/connection/subscriptions_test.rb2
-rw-r--r--actioncable/test/stubs/test_connection.rb23
-rw-r--r--actioncable/test/stubs/test_server.rb12
-rw-r--r--actioncable/test/subscription_adapter/common.rb3
-rw-r--r--actioncable/test/test_helper.rb8
-rw-r--r--actionview/CHANGELOG.md6
-rw-r--r--actionview/lib/action_view/helpers/form_helper.rb4
-rw-r--r--actionview/lib/action_view/helpers/form_tag_helper.rb4
-rw-r--r--actionview/test/template/form_helper_test.rb32
-rw-r--r--actionview/test/template/form_tag_helper_test.rb4
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/quoting.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb5
-rw-r--r--activerecord/lib/active_record/core.rb7
-rw-r--r--activerecord/lib/active_record/model_schema.rb7
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb5
-rw-r--r--activerecord/lib/active_record/relation/where_clause_factory.rb1
-rw-r--r--activerecord/test/cases/base_test.rb8
-rw-r--r--activerecord/test/cases/statement_cache_test.rb12
-rw-r--r--guides/source/action_controller_overview.md2
-rw-r--r--guides/source/configuring.md2
-rw-r--r--guides/source/security.md2
42 files changed, 360 insertions, 192 deletions
diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md
index 1ea862b389..d59e48e00c 100644
--- a/actioncable/CHANGELOG.md
+++ b/actioncable/CHANGELOG.md
@@ -1,11 +1,16 @@
-* Add ActiveSupport::Notifications to ActionCable::Channel.
+* Pubsub: automatic stream decoding.
- *Matthew Wear*
+ stream_for @room, coder: ActiveSupport::JSON do |message|
+ # `message` is a Ruby hash here instead of a JSON string
-* Allow channel identifiers with no backslahes/escaping to be accepted
- by the subscription storer.
+ The `coder` must respond to `#decode`. Defaults to `coder: nil`
+ which skips decoding entirely.
- *Jon Moss*
+ *Jeremy Daer*
+
+* Add ActiveSupport::Notifications to ActionCable::Channel.
+
+ *Matthew Wear*
* Safely support autoloading and class unloading, by preventing concurrent
loads, and disconnecting all cables during reload.
diff --git a/actioncable/lib/action_cable/channel/base.rb b/actioncable/lib/action_cable/channel/base.rb
index 464d0581dd..845b747fc5 100644
--- a/actioncable/lib/action_cable/channel/base.rb
+++ b/actioncable/lib/action_cable/channel/base.rb
@@ -198,7 +198,7 @@ module ActionCable
payload = { channel_class: self.class.name, data: data, via: via }
ActiveSupport::Notifications.instrument("transmit.action_cable", payload) do
- connection.transmit ActiveSupport::JSON.encode(identifier: @identifier, message: data)
+ connection.transmit identifier: @identifier, message: data
end
end
@@ -274,7 +274,7 @@ module ActionCable
logger.info "#{self.class.name} is transmitting the subscription confirmation"
ActiveSupport::Notifications.instrument("transmit_subscription_confirmation.action_cable", channel_class: self.class.name) do
- connection.transmit ActiveSupport::JSON.encode(identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:confirmation])
+ connection.transmit identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:confirmation]
@subscription_confirmation_sent = true
end
end
@@ -289,7 +289,7 @@ module ActionCable
logger.info "#{self.class.name} is transmitting the subscription rejection"
ActiveSupport::Notifications.instrument("transmit_subscription_rejection.action_cable", channel_class: self.class.name) do
- connection.transmit ActiveSupport::JSON.encode(identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:rejection])
+ connection.transmit identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:rejection]
end
end
end
diff --git a/actioncable/lib/action_cable/channel/streams.rb b/actioncable/lib/action_cable/channel/streams.rb
index 23d7320a28..f654ce0bfa 100644
--- a/actioncable/lib/action_cable/channel/streams.rb
+++ b/actioncable/lib/action_cable/channel/streams.rb
@@ -46,9 +46,7 @@ module ActionCable
# def subscribed
# @room = Chat::Room[params[:room_number]]
#
- # stream_for @room, -> (encoded_message) do
- # message = ActiveSupport::JSON.decode(encoded_message)
- #
+ # stream_for @room, coder: ActiveSupport::JSON do |message|
# if message['originated_at'].present?
# elapsed_time = (Time.now.to_f - message['originated_at']).round(2)
#
@@ -71,16 +69,23 @@ module ActionCable
# Start streaming from the named <tt>broadcasting</tt> pubsub queue. Optionally, you can pass a <tt>callback</tt> that'll be used
# instead of the default of just transmitting the updates straight to the subscriber.
- def stream_from(broadcasting, callback = nil)
+ # Pass `coder: ActiveSupport::JSON` to decode messages as JSON before passing to the callback.
+ # Defaults to `coder: nil` which does no decoding, passes raw messages.
+ def stream_from(broadcasting, callback = nil, coder: nil, &block)
broadcasting = String(broadcasting)
# Don't send the confirmation until pubsub#subscribe is successful
defer_subscription_confirmation!
- callback ||= default_stream_callback(broadcasting)
- streams << [ broadcasting, callback ]
+ if handler = callback || block
+ handler = -> message { handler.(coder.decode(message)) } if coder
+ else
+ handler = default_stream_handler(broadcasting, coder: coder)
+ end
+
+ streams << [ broadcasting, handler ]
connection.server.event_loop.post do
- pubsub.subscribe(broadcasting, callback, lambda do
+ pubsub.subscribe(broadcasting, handler, lambda do
transmit_subscription_confirmation
logger.info "#{self.class.name} is streaming from #{broadcasting}"
end)
@@ -90,8 +95,11 @@ module ActionCable
# Start streaming the pubsub queue for the <tt>model</tt> in this channel. Optionally, you can pass a
# <tt>callback</tt> that'll be used instead of the default of just transmitting the updates straight
# to the subscriber.
- def stream_for(model, callback = nil)
- stream_from(broadcasting_for([ channel_name, model ]), callback)
+ #
+ # Pass `coder: ActiveSupport::JSON` to decode messages as JSON before passing to the callback.
+ # Defaults to `coder: nil` which does no decoding, passes raw messages.
+ def stream_for(model, callback = nil, coder: nil, &block)
+ stream_from(broadcasting_for([ channel_name, model ]), callback || block, coder: coder)
end
# Unsubscribes all streams associated with this channel from the pubsub queue.
@@ -109,9 +117,11 @@ module ActionCable
@_streams ||= []
end
- def default_stream_callback(broadcasting)
+ def default_stream_handler(broadcasting, coder:)
+ coder ||= ActiveSupport::JSON
+
-> (message) do
- transmit ActiveSupport::JSON.decode(message), via: "streamed from #{broadcasting}"
+ transmit coder.decode(message), via: "streamed from #{broadcasting}"
end
end
end
diff --git a/actioncable/lib/action_cable/connection/base.rb b/actioncable/lib/action_cable/connection/base.rb
index b4488265cb..604a889bb0 100644
--- a/actioncable/lib/action_cable/connection/base.rb
+++ b/actioncable/lib/action_cable/connection/base.rb
@@ -51,8 +51,8 @@ module ActionCable
attr_reader :server, :env, :subscriptions, :logger, :worker_pool
delegate :event_loop, :pubsub, to: :server
- def initialize(server, env)
- @server, @env = server, env
+ def initialize(server, env, coder: ActiveSupport::JSON)
+ @server, @env, @coder = server, env, coder
@worker_pool = server.worker_pool
@logger = new_tagged_logger
@@ -67,7 +67,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?
@@ -77,20 +77,22 @@ module ActionCable
end
end
- # Data received over the WebSocket connection is handled by this method. It's expected that everything inbound is JSON encoded.
- # The data is routed to the proper channel that the connection has subscribed to.
- def receive(data_in_json)
+ # Decodes WebSocket messages and dispatches them to subscribed channels.
+ # WebSocket message transfer encoding is always JSON.
+ def receive(websocket_message) #:nodoc:
+ send_async :dispatch_websocket_message, websocket_message
+ end
+
+ def dispatch_websocket_message(websocket_message) #:nodoc:
if websocket.alive?
- subscriptions.execute_command ActiveSupport::JSON.decode(data_in_json)
+ subscriptions.execute_command decode(websocket_message)
else
- logger.error "Received data without a live WebSocket (#{data_in_json.inspect})"
+ logger.error "Ignoring message processed after the WebSocket was closed: #{websocket_message.inspect})"
end
end
- # Send raw data straight back down the WebSocket. This is not intended to be called directly. Use the #transmit available on the
- # Channel instead, as that'll automatically address the correct subscriber and wrap the message in JSON.
- def transmit(data) # :nodoc:
- websocket.transmit data
+ def transmit(cable_message) # :nodoc:
+ websocket.transmit encode(cable_message)
end
# Close the WebSocket connection.
@@ -115,7 +117,7 @@ module ActionCable
end
def beat
- transmit ActiveSupport::JSON.encode(type: ActionCable::INTERNAL[:message_types][:ping], message: Time.now.to_i)
+ transmit type: ActionCable::INTERNAL[:message_types][:ping], message: Time.now.to_i
end
def on_open # :nodoc:
@@ -152,6 +154,14 @@ module ActionCable
attr_reader :message_buffer
private
+ def encode(cable_message)
+ @coder.encode cable_message
+ end
+
+ def decode(websocket_message)
+ @coder.decode websocket_message
+ end
+
def handle_open
connect if respond_to?(:connect)
subscribe_to_internal_channel
@@ -178,7 +188,7 @@ module ActionCable
# Send welcome message to the internal connection monitor channel.
# This ensures the connection monitor state is reset after a successful
# websocket connection.
- transmit ActiveSupport::JSON.encode(type: ActionCable::INTERNAL[:message_types][:welcome])
+ transmit type: ActionCable::INTERNAL[:message_types][:welcome]
end
def allow_request_origin?
diff --git a/actioncable/lib/action_cable/connection/internal_channel.rb b/actioncable/lib/action_cable/connection/internal_channel.rb
index 3c5d39f59a..f70d52f99b 100644
--- a/actioncable/lib/action_cable/connection/internal_channel.rb
+++ b/actioncable/lib/action_cable/connection/internal_channel.rb
@@ -11,7 +11,7 @@ module ActionCable
def subscribe_to_internal_channel
if connection_identifier.present?
- callback = -> (message) { process_internal_message(message) }
+ callback = -> (message) { process_internal_message decode(message) }
@_internal_subscriptions ||= []
@_internal_subscriptions << [ internal_channel, callback ]
@@ -27,8 +27,6 @@ module ActionCable
end
def process_internal_message(message)
- message = ActiveSupport::JSON.decode(message)
-
case message['type']
when 'disconnect'
logger.info "Removing connection (#{connection_identifier})"
diff --git a/actioncable/lib/action_cable/connection/message_buffer.rb b/actioncable/lib/action_cable/connection/message_buffer.rb
index 19f2e6e918..6a80770cae 100644
--- a/actioncable/lib/action_cable/connection/message_buffer.rb
+++ b/actioncable/lib/action_cable/connection/message_buffer.rb
@@ -30,7 +30,7 @@ module ActionCable
protected
attr_reader :connection
- attr_accessor :buffered_messages
+ attr_reader :buffered_messages
private
def valid?(message)
@@ -38,7 +38,7 @@ module ActionCable
end
def receive(message)
- connection.send_async :receive, message
+ connection.receive message
end
def buffer(message)
diff --git a/actioncable/lib/action_cable/connection/subscriptions.rb b/actioncable/lib/action_cable/connection/subscriptions.rb
index 5aa907c2d3..3742f248d1 100644
--- a/actioncable/lib/action_cable/connection/subscriptions.rb
+++ b/actioncable/lib/action_cable/connection/subscriptions.rb
@@ -23,13 +23,13 @@ module ActionCable
end
def add(data)
- id_options = decode_hash(data['identifier'])
- identifier = normalize_identifier(id_options)
+ id_key = data['identifier']
+ id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access
subscription_klass = connection.server.channel_classes[id_options[:channel]]
if subscription_klass
- subscriptions[identifier] ||= subscription_klass.new(connection, identifier, id_options)
+ subscriptions[id_key] ||= subscription_klass.new(connection, id_key, id_options)
else
logger.error "Subscription class not found (#{data.inspect})"
end
@@ -37,7 +37,7 @@ module ActionCable
def remove(data)
logger.info "Unsubscribing from channel: #{data['identifier']}"
- remove_subscription subscriptions[normalize_identifier(data['identifier'])]
+ remove_subscription subscriptions[data['identifier']]
end
def remove_subscription(subscription)
@@ -46,7 +46,7 @@ module ActionCable
end
def perform_action(data)
- find(data).perform_action(decode_hash(data['data']))
+ find(data).perform_action ActiveSupport::JSON.decode(data['data'])
end
def identifiers
@@ -63,21 +63,8 @@ module ActionCable
private
delegate :logger, to: :connection
- def normalize_identifier(identifier)
- identifier = ActiveSupport::JSON.encode(identifier) if identifier.is_a?(Hash)
- identifier
- end
-
- # If `data` is a Hash, this means that the original JSON
- # sent by the client had no backslashes in it, and does
- # not need to be decoded again.
- def decode_hash(data)
- data = ActiveSupport::JSON.decode(data) unless data.is_a?(Hash)
- data.with_indifferent_access
- end
-
def find(data)
- if subscription = subscriptions[normalize_identifier(data['identifier'])]
+ if subscription = subscriptions[data['identifier']]
subscription
else
raise "Unable to find subscription with identifier: #{data['identifier']}"
diff --git a/actioncable/lib/action_cable/server/base.rb b/actioncable/lib/action_cable/server/base.rb
index 778f5ffeed..b1a0e11631 100644
--- a/actioncable/lib/action_cable/server/base.rb
+++ b/actioncable/lib/action_cable/server/base.rb
@@ -52,7 +52,17 @@ module ActionCable
@event_loop || @mutex.synchronize { @event_loop ||= config.event_loop_class.new }
end
- # The thread worker pool for handling all the connection work on this server. Default size is set by config.worker_pool_size.
+ # The worker pool is where we run connection callbacks and channel actions. We do as little as possible on the server's main thread.
+ # The worker pool is an executor service that's backed by a pool of threads working from a task queue. The thread pool size maxes out
+ # at 4 worker threads by default. Tune the size yourself with config.action_cable.worker_pool_size.
+ #
+ # Using Active Record, Redis, etc within your channel actions means you'll get a separate connection from each thread in the worker pool.
+ # Plan your deployment accordingly: 5 servers each running 5 Puma workers each running an 8-thread worker pool means at least 200 database
+ # connections.
+ #
+ # Also, ensure that your database connection pool size is as least as large as your worker pool size. Otherwise, workers may oversubscribe
+ # the db connection pool and block while they wait for other workers to release their connections. Use a smaller worker pool or a larger
+ # db connection pool instead.
def worker_pool
@worker_pool || @mutex.synchronize { @worker_pool ||= ActionCable::Server::Worker.new(max_size: config.worker_pool_size) }
end
diff --git a/actioncable/lib/action_cable/server/broadcasting.rb b/actioncable/lib/action_cable/server/broadcasting.rb
index 98025f27f2..8f93564113 100644
--- a/actioncable/lib/action_cable/server/broadcasting.rb
+++ b/actioncable/lib/action_cable/server/broadcasting.rb
@@ -19,27 +19,28 @@ module ActionCable
# new Notification data['title'], body: data['body']
module Broadcasting
# Broadcast a hash directly to a named <tt>broadcasting</tt>. This will later be JSON encoded.
- def broadcast(broadcasting, message)
- broadcaster_for(broadcasting).broadcast(message)
+ def broadcast(broadcasting, message, coder: ActiveSupport::JSON)
+ broadcaster_for(broadcasting, coder: coder).broadcast(message)
end
# Returns a broadcaster for a named <tt>broadcasting</tt> that can be reused. Useful when you have an object that
# may need multiple spots to transmit to a specific broadcasting over and over.
- def broadcaster_for(broadcasting)
- Broadcaster.new(self, String(broadcasting))
+ def broadcaster_for(broadcasting, coder: ActiveSupport::JSON)
+ Broadcaster.new(self, String(broadcasting), coder: coder)
end
private
class Broadcaster
- attr_reader :server, :broadcasting
+ attr_reader :server, :broadcasting, :coder
- def initialize(server, broadcasting)
- @server, @broadcasting = server, broadcasting
+ def initialize(server, broadcasting, coder:)
+ @server, @broadcasting, @coder = server, broadcasting, coder
end
def broadcast(message)
- server.logger.info "[ActionCable] Broadcasting to #{broadcasting}: #{message}"
- server.pubsub.broadcast broadcasting, ActiveSupport::JSON.encode(message)
+ server.logger.info "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}"
+ encoded = coder ? coder.encode(message) : message
+ server.pubsub.broadcast broadcasting, encoded
end
end
end
diff --git a/actioncable/lib/action_cable/server/configuration.rb b/actioncable/lib/action_cable/server/configuration.rb
index 5fe71caed2..0bb378cf03 100644
--- a/actioncable/lib/action_cable/server/configuration.rb
+++ b/actioncable/lib/action_cable/server/configuration.rb
@@ -14,7 +14,7 @@ module ActionCable
@log_tags = []
@connection_class = ActionCable::Connection::Base
- @worker_pool_size = 100
+ @worker_pool_size = 4
@disable_request_forgery_protection = false
end
diff --git a/actioncable/test/channel/base_test.rb b/actioncable/test/channel/base_test.rb
index bed54eb6b3..daa782eeb3 100644
--- a/actioncable/test/channel/base_test.rb
+++ b/actioncable/test/channel/base_test.rb
@@ -146,12 +146,12 @@ class ActionCable::Channel::BaseTest < ActiveSupport::TestCase
test "transmitting data" do
@channel.perform_action 'action' => :get_latest
- expected = ActiveSupport::JSON.encode "identifier" => "{id: 1}", "message" => { "data" => "latest" }
+ expected = { "identifier" => "{id: 1}", "message" => { "data" => "latest" }}
assert_equal expected, @connection.last_transmission
end
test "subscription confirmation" do
- expected = ActiveSupport::JSON.encode "identifier" => "{id: 1}", "type" => "confirm_subscription"
+ expected = { "identifier" => "{id: 1}", "type" => "confirm_subscription" }
assert_equal expected, @connection.last_transmission
end
diff --git a/actioncable/test/channel/rejection_test.rb b/actioncable/test/channel/rejection_test.rb
index aa93396d44..15db57d6ba 100644
--- a/actioncable/test/channel/rejection_test.rb
+++ b/actioncable/test/channel/rejection_test.rb
@@ -18,7 +18,7 @@ class ActionCable::Channel::RejectionTest < ActiveSupport::TestCase
@connection.expects(:subscriptions).returns mock().tap { |m| m.expects(:remove_subscription).with instance_of(SecretChannel) }
@channel = SecretChannel.new @connection, "{id: 1}", { id: 1 }
- expected = ActiveSupport::JSON.encode "identifier" => "{id: 1}", "type" => "reject_subscription"
+ expected = { "identifier" => "{id: 1}", "type" => "reject_subscription" }
assert_equal expected, @connection.last_transmission
end
diff --git a/actioncable/test/channel/stream_test.rb b/actioncable/test/channel/stream_test.rb
index 526ea92e4f..f51f19eb7d 100644
--- a/actioncable/test/channel/stream_test.rb
+++ b/actioncable/test/channel/stream_test.rb
@@ -2,18 +2,43 @@ require 'test_helper'
require 'stubs/test_connection'
require 'stubs/room'
-class ActionCable::Channel::StreamTest < ActionCable::TestCase
+module ActionCable::StreamTests
+ class Connection < ActionCable::Connection::Base
+ attr_reader :websocket
+
+ def send_async(method, *args)
+ send method, *args
+ end
+ end
+
class ChatChannel < ActionCable::Channel::Base
def subscribed
if params[:id]
@room = Room.new params[:id]
- stream_from "test_room_#{@room.id}"
+ stream_from "test_room_#{@room.id}", coder: pick_coder(params[:coder])
end
end
def send_confirmation
transmit_subscription_confirmation
end
+
+ private def pick_coder(coder)
+ case coder
+ when nil, 'json'
+ ActiveSupport::JSON
+ when 'custom'
+ DummyEncoder
+ when 'none'
+ nil
+ end
+ end
+ end
+
+ module DummyEncoder
+ extend self
+ def encode(*) '{ "foo": "encoded" }' end
+ def decode(*) { foo: 'decoded' } end
end
class SymbolChannel < ActionCable::Channel::Base
@@ -22,69 +47,114 @@ class ActionCable::Channel::StreamTest < ActionCable::TestCase
end
end
- test "streaming start and stop" do
- run_in_eventmachine do
- connection = TestConnection.new
- connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("test_room_1", kind_of(Proc), kind_of(Proc)).returns stub_everything(:pubsub) }
- channel = ChatChannel.new connection, "{id: 1}", { id: 1 }
+ class StreamTest < ActionCable::TestCase
+ test "streaming start and stop" do
+ run_in_eventmachine do
+ connection = TestConnection.new
+ connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("test_room_1", kind_of(Proc), kind_of(Proc)).returns stub_everything(:pubsub) }
+ channel = ChatChannel.new connection, "{id: 1}", { id: 1 }
- connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe) }
- channel.unsubscribe_from_channel
+ connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe) }
+ channel.unsubscribe_from_channel
+ end
end
- end
- test "stream from non-string channel" do
- run_in_eventmachine do
- connection = TestConnection.new
- connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("channel", kind_of(Proc), kind_of(Proc)).returns stub_everything(:pubsub) }
- channel = SymbolChannel.new connection, ""
+ test "stream from non-string channel" do
+ run_in_eventmachine do
+ connection = TestConnection.new
+ connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("channel", kind_of(Proc), kind_of(Proc)).returns stub_everything(:pubsub) }
+ channel = SymbolChannel.new connection, ""
- connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe) }
- channel.unsubscribe_from_channel
+ connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe) }
+ channel.unsubscribe_from_channel
+ end
end
- end
- test "stream_for" do
- run_in_eventmachine do
- connection = TestConnection.new
- connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("action_cable:channel:stream_test:chat:Room#1-Campfire", kind_of(Proc), kind_of(Proc)).returns stub_everything(:pubsub) }
+ test "stream_for" do
+ run_in_eventmachine do
+ connection = TestConnection.new
+ connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("action_cable:stream_tests:chat:Room#1-Campfire", kind_of(Proc), kind_of(Proc)).returns stub_everything(:pubsub) }
- channel = ChatChannel.new connection, ""
- channel.stream_for Room.new(1)
+ channel = ChatChannel.new connection, ""
+ channel.stream_for Room.new(1)
+ end
end
- end
- test "stream_from subscription confirmation" do
- run_in_eventmachine do
- connection = TestConnection.new
+ test "stream_from subscription confirmation" do
+ run_in_eventmachine do
+ connection = TestConnection.new
- ChatChannel.new connection, "{id: 1}", { id: 1 }
- assert_nil connection.last_transmission
+ ChatChannel.new connection, "{id: 1}", { id: 1 }
+ assert_nil connection.last_transmission
- wait_for_async
+ wait_for_async
- expected = ActiveSupport::JSON.encode "identifier" => "{id: 1}", "type" => "confirm_subscription"
- connection.transmit(expected)
+ confirmation = { "identifier" => "{id: 1}", "type" => "confirm_subscription" }
+ connection.transmit(confirmation)
- assert_equal expected, connection.last_transmission, "Did not receive subscription confirmation within 0.1s"
+ assert_equal confirmation, connection.last_transmission, "Did not receive subscription confirmation within 0.1s"
+ end
end
- end
- test "subscription confirmation should only be sent out once" do
- run_in_eventmachine do
- connection = TestConnection.new
+ test "subscription confirmation should only be sent out once" do
+ run_in_eventmachine do
+ connection = TestConnection.new
- channel = ChatChannel.new connection, "test_channel"
- channel.send_confirmation
- channel.send_confirmation
+ channel = ChatChannel.new connection, "test_channel"
+ channel.send_confirmation
+ channel.send_confirmation
- wait_for_async
+ wait_for_async
- expected = ActiveSupport::JSON.encode "identifier" => "test_channel", "type" => "confirm_subscription"
- assert_equal expected, connection.last_transmission, "Did not receive subscription confirmation"
+ expected = { "identifier" => "test_channel", "type" => "confirm_subscription" }
+ assert_equal expected, connection.last_transmission, "Did not receive subscription confirmation"
- assert_equal 1, connection.transmissions.size
+ assert_equal 1, connection.transmissions.size
+ end
end
end
+ require 'action_cable/subscription_adapter/inline'
+
+ class StreamEncodingTest < ActionCable::TestCase
+ setup do
+ @server = TestServer.new(subscription_adapter: ActionCable::SubscriptionAdapter::Inline)
+ @server.config.allowed_request_origins = %w( http://rubyonrails.com )
+ @server.stubs(:channel_classes).returns(ChatChannel.name => ChatChannel)
+ end
+
+ test 'custom encoder' do
+ run_in_eventmachine do
+ connection = open_connection
+ subscribe_to connection, identifiers: { id: 1 }
+
+ connection.websocket.expects(:transmit)
+ @server.broadcast 'test_room_1', { foo: 'bar' }, coder: DummyEncoder
+ wait_for_async
+ end
+ end
+
+ private
+ def subscribe_to(connection, identifiers:)
+ receive connection, command: 'subscribe', identifiers: identifiers
+ end
+
+ def open_connection
+ env = Rack::MockRequest.env_for '/test', 'HTTP_HOST' => 'localhost', 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket', 'HTTP_ORIGIN' => 'http://rubyonrails.com'
+
+ Connection.new(@server, env).tap do |connection|
+ connection.process
+ assert connection.websocket.possible?
+
+ wait_for_async
+ assert connection.websocket.alive?
+ end
+ end
+
+ def receive(connection, command:, identifiers:)
+ identifier = JSON.generate(channel: 'ActionCable::StreamTests::ChatChannel', **identifiers)
+ connection.dispatch_websocket_message JSON.generate(command: command, identifier: identifier)
+ wait_for_async
+ end
+ end
end
diff --git a/actioncable/test/client_test.rb b/actioncable/test/client_test.rb
index 5f5c09d1a1..5ac453db35 100644
--- a/actioncable/test/client_test.rb
+++ b/actioncable/test/client_test.rb
@@ -75,11 +75,11 @@ class ClientTest < ActionCable::TestCase
end
@ws.on(:message) do |event|
- hash = JSON.parse(event.data)
- if hash['type'] == 'ping'
+ message = JSON.parse(event.data)
+ if message['type'] == 'ping'
@pings += 1
else
- @messages << hash
+ @messages << message
@has_messages.release
end
end
@@ -116,8 +116,8 @@ class ClientTest < ActionCable::TestCase
list
end
- def send_message(hash)
- @ws.send(JSON.dump(hash))
+ def send_message(message)
+ @ws.send(JSON.generate(message))
end
def close
@@ -148,9 +148,9 @@ class ClientTest < ActionCable::TestCase
with_puma_server do |port|
c = faye_client(port)
assert_equal({"type" => "welcome"}, c.read_message) # pop the first welcome message off the stack
- c.send_message command: 'subscribe', identifier: JSON.dump(channel: 'EchoChannel')
+ c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel')
assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message)
- c.send_message command: 'message', identifier: JSON.dump(channel: 'EchoChannel'), data: JSON.dump(action: 'ding', message: 'hello')
+ c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'ding', message: 'hello')
assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "message"=>{"dong"=>"hello"}}, c.read_message)
c.close
end
@@ -165,12 +165,12 @@ class ClientTest < ActionCable::TestCase
clients.map {|c| Concurrent::Future.execute {
assert_equal({"type" => "welcome"}, c.read_message) # pop the first welcome message off the stack
- c.send_message command: 'subscribe', identifier: JSON.dump(channel: 'EchoChannel')
+ c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel')
assert_equal({"identifier"=>'{"channel":"EchoChannel"}', "type"=>"confirm_subscription"}, c.read_message)
- c.send_message command: 'message', identifier: JSON.dump(channel: 'EchoChannel'), data: JSON.dump(action: 'ding', message: 'hello')
+ c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'ding', message: 'hello')
assert_equal({"identifier"=>'{"channel":"EchoChannel"}', "message"=>{"dong"=>"hello"}}, c.read_message)
barrier_1.wait WAIT_WHEN_EXPECTING_EVENT
- c.send_message command: 'message', identifier: JSON.dump(channel: 'EchoChannel'), data: JSON.dump(action: 'bulk', message: 'hello')
+ c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'bulk', message: 'hello')
barrier_2.wait WAIT_WHEN_EXPECTING_EVENT
assert_equal clients.size, c.read_messages(clients.size).size
} }.each(&:wait!)
@@ -185,9 +185,9 @@ class ClientTest < ActionCable::TestCase
clients.map {|c| Concurrent::Future.execute {
assert_equal({"type" => "welcome"}, c.read_message) # pop the first welcome message off the stack
- c.send_message command: 'subscribe', identifier: JSON.dump(channel: 'EchoChannel')
+ c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel')
assert_equal({"identifier"=>'{"channel":"EchoChannel"}', "type"=>"confirm_subscription"}, c.read_message)
- c.send_message command: 'message', identifier: JSON.dump(channel: 'EchoChannel'), data: JSON.dump(action: 'ding', message: 'hello')
+ c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'ding', message: 'hello')
assert_equal({"identifier"=>'{"channel":"EchoChannel"}', "message"=>{"dong"=>"hello"}}, c.read_message)
} }.each(&:wait!)
@@ -199,16 +199,16 @@ class ClientTest < ActionCable::TestCase
with_puma_server do |port|
c = faye_client(port)
assert_equal({"type" => "welcome"}, c.read_message) # pop the first welcome message off the stack
- c.send_message command: 'subscribe', identifier: JSON.dump(channel: 'EchoChannel')
+ c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel')
assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message)
- c.send_message command: 'message', identifier: JSON.dump(channel: 'EchoChannel'), data: JSON.dump(action: 'delay', message: 'hello')
+ c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'delay', message: 'hello')
c.close # disappear before write
c = faye_client(port)
assert_equal({"type" => "welcome"}, c.read_message) # pop the first welcome message off the stack
- c.send_message command: 'subscribe', identifier: JSON.dump(channel: 'EchoChannel')
+ c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel')
assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message)
- c.send_message command: 'message', identifier: JSON.dump(channel: 'EchoChannel'), data: JSON.dump(action: 'ding', message: 'hello')
+ c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'ding', message: 'hello')
assert_equal({"identifier"=>'{"channel":"EchoChannel"}', "message"=>{"dong"=>"hello"}}, c.read_message)
c.close # disappear before read
end
@@ -217,7 +217,7 @@ class ClientTest < ActionCable::TestCase
def test_unsubscribe_client
with_puma_server do |port|
app = ActionCable.server
- identifier = JSON.dump(channel: 'EchoChannel')
+ identifier = JSON.generate(channel: 'EchoChannel')
c = faye_client(port)
assert_equal({"type" => "welcome"}, c.read_message)
@@ -240,7 +240,7 @@ class ClientTest < ActionCable::TestCase
with_puma_server do |port|
c = faye_client(port)
assert_equal({"type" => "welcome"}, c.read_message)
- c.send_message command: 'subscribe', identifier: JSON.dump(channel: 'EchoChannel')
+ c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel')
assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message)
ActionCable.server.restart
diff --git a/actioncable/test/connection/identifier_test.rb b/actioncable/test/connection/identifier_test.rb
index c3d5f1f90b..b48d9af809 100644
--- a/actioncable/test/connection/identifier_test.rb
+++ b/actioncable/test/connection/identifier_test.rb
@@ -40,8 +40,7 @@ class ActionCable::Connection::IdentifierTest < ActionCable::TestCase
open_connection_with_stubbed_pubsub
@connection.websocket.expects(:close)
- message = ActiveSupport::JSON.encode('type' => 'disconnect')
- @connection.process_internal_message message
+ @connection.process_internal_message 'type' => 'disconnect'
end
end
@@ -50,8 +49,7 @@ class ActionCable::Connection::IdentifierTest < ActionCable::TestCase
open_connection_with_stubbed_pubsub
@connection.websocket.expects(:close).never
- message = ActiveSupport::JSON.encode('type' => 'unknown')
- @connection.process_internal_message message
+ @connection.process_internal_message 'type' => 'unknown'
end
end
diff --git a/actioncable/test/connection/subscriptions_test.rb b/actioncable/test/connection/subscriptions_test.rb
index f91597f567..53e8547245 100644
--- a/actioncable/test/connection/subscriptions_test.rb
+++ b/actioncable/test/connection/subscriptions_test.rb
@@ -88,7 +88,7 @@ class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase
channel1 = subscribe_to_chat_channel
- channel2_id = ActiveSupport::JSON.encode({ id: 2, channel: 'ActionCable::Connection::SubscriptionsTest::ChatChannel' })
+ channel2_id = ActiveSupport::JSON.encode(id: 2, channel: 'ActionCable::Connection::SubscriptionsTest::ChatChannel')
channel2 = subscribe_to_chat_channel(channel2_id)
channel1.expects(:unsubscribe_from_channel)
diff --git a/actioncable/test/stubs/test_connection.rb b/actioncable/test/stubs/test_connection.rb
index 8ba284fdc6..885450dda6 100644
--- a/actioncable/test/stubs/test_connection.rb
+++ b/actioncable/test/stubs/test_connection.rb
@@ -3,24 +3,31 @@ require 'stubs/user'
class TestConnection
attr_reader :identifiers, :logger, :current_user, :server, :transmissions
- def initialize(user = User.new("lifo"))
+ delegate :pubsub, to: :server
+
+ def initialize(user = User.new("lifo"), coder: ActiveSupport::JSON, subscription_adapter: SuccessAdapter)
+ @coder = coder
@identifiers = [ :current_user ]
@current_user = user
@logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new)
- @server = TestServer.new
+ @server = TestServer.new(subscription_adapter: subscription_adapter)
@transmissions = []
end
- def pubsub
- SuccessAdapter.new(server)
+ def transmit(cable_message)
+ @transmissions << encode(cable_message)
end
- def transmit(data)
- @transmissions << data
+ def last_transmission
+ decode @transmissions.last if @transmissions.any?
end
- def last_transmission
- @transmissions.last
+ def decode(websocket_message)
+ @coder.decode websocket_message
+ end
+
+ def encode(cable_message)
+ @coder.encode cable_message
end
end
diff --git a/actioncable/test/stubs/test_server.rb b/actioncable/test/stubs/test_server.rb
index 9e860825f3..b86f422a13 100644
--- a/actioncable/test/stubs/test_server.rb
+++ b/actioncable/test/stubs/test_server.rb
@@ -2,22 +2,26 @@ require 'ostruct'
class TestServer
include ActionCable::Server::Connections
+ include ActionCable::Server::Broadcasting
- attr_reader :logger, :config
+ attr_reader :logger, :config, :mutex
- def initialize
+ def initialize(subscription_adapter: SuccessAdapter)
@logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new)
- @config = OpenStruct.new(log_tags: [], subscription_adapter: SuccessAdapter)
+
+ @config = OpenStruct.new(log_tags: [], subscription_adapter: subscription_adapter)
@config.use_faye = ENV['FAYE'].present?
@config.client_socket_class = if @config.use_faye
ActionCable::Connection::FayeClientSocket
else
ActionCable::Connection::ClientSocket
end
+
+ @mutex = Monitor.new
end
def pubsub
- @config.subscription_adapter.new(self)
+ @pubsub ||= @config.subscription_adapter.new(self)
end
def event_loop
diff --git a/actioncable/test/subscription_adapter/common.rb b/actioncable/test/subscription_adapter/common.rb
index 82f0abbbf3..285c690df0 100644
--- a/actioncable/test/subscription_adapter/common.rb
+++ b/actioncable/test/subscription_adapter/common.rb
@@ -20,8 +20,7 @@ module CommonSubscriptionAdapterTest
end
def teardown
- @tx_adapter.shutdown if @tx_adapter && @tx_adapter != @rx_adapter
- @rx_adapter.shutdown if @rx_adapter
+ [@rx_adapter, @tx_adapter].uniq.each(&:shutdown)
end
diff --git a/actioncable/test/test_helper.rb b/actioncable/test/test_helper.rb
index 030362d512..de1ee96770 100644
--- a/actioncable/test/test_helper.rb
+++ b/actioncable/test/test_helper.rb
@@ -2,11 +2,13 @@ require 'action_cable'
require 'active_support/testing/autorun'
require 'puma'
-
require 'mocha/setup'
-
require 'rack/mock'
-require 'active_support/core_ext/hash/indifferent_access'
+
+begin
+ require 'byebug'
+rescue LoadError
+end
# Require all the stubs and models
Dir[File.dirname(__FILE__) + '/stubs/*.rb'].each {|file| require file }
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md
index ecc1e764a5..a1901e8a17 100644
--- a/actionview/CHANGELOG.md
+++ b/actionview/CHANGELOG.md
@@ -1,3 +1,9 @@
+* Deprecate `datetime_field` and `datetime_field_tag` helpers.
+ Datetime input type was removed from HTML specification.
+ One can use `datetime_local_field` and `datetime_local_field_tag` instead.
+
+ *Wojciech Wnętrzak*
+
* Added log "Rendering ...", when starting to render a template to log that
we have started rendering something. This helps to easily identify the origin
of queries in the log whether they came from controller or views.
diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb
index e91b3443c5..7ced37572e 100644
--- a/actionview/lib/action_view/helpers/form_helper.rb
+++ b/actionview/lib/action_view/helpers/form_helper.rb
@@ -1118,6 +1118,10 @@ module ActionView
# # => <input id="user_born_on" name="user[born_on]" type="datetime" min="2014-05-20T00:00:00.000+0000" />
#
def datetime_field(object_name, method, options = {})
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
+ datetime_field is deprecated and will be removed in Rails 5.1.
+ Use datetime_local_field instead.
+ MESSAGE
Tags::DatetimeField.new(object_name, method, self, options).render
end
diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb
index 82e9ace428..cfff0bef5d 100644
--- a/actionview/lib/action_view/helpers/form_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/form_tag_helper.rb
@@ -691,6 +691,10 @@ module ActionView
# * <tt>:step</tt> - The acceptable value granularity.
# * Otherwise accepts the same options as text_field_tag.
def datetime_field_tag(name, value = nil, options = {})
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
+ datetime_field_tag is deprecated and will be removed in Rails 5.1.
+ Use datetime_local_field_tag instead.
+ MESSAGE
text_field_tag(name, value, options.merge(type: :datetime))
end
diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb
index e77183e39f..310d0ce514 100644
--- a/actionview/test/template/form_helper_test.rb
+++ b/actionview/test/template/form_helper_test.rb
@@ -1139,13 +1139,17 @@ class FormHelperTest < ActionView::TestCase
def test_datetime_field
expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T00:00:00.000+0000" />}
- assert_dom_equal(expected, datetime_field("post", "written_on"))
+ assert_deprecated do
+ assert_dom_equal(expected, datetime_field("post", "written_on"))
+ end
end
def test_datetime_field_with_datetime_value
expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T01:02:03.000+0000" />}
@post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
- assert_dom_equal(expected, datetime_field("post", "written_on"))
+ assert_deprecated do
+ assert_dom_equal(expected, datetime_field("post", "written_on"))
+ end
end
def test_datetime_field_with_extra_attrs
@@ -1154,20 +1158,26 @@ class FormHelperTest < ActionView::TestCase
min_value = DateTime.new(2000, 6, 15, 20, 45, 30)
max_value = DateTime.new(2010, 8, 15, 10, 25, 00)
step = 60
- assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value, step: step))
+ assert_deprecated do
+ assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value, step: step))
+ end
end
def test_datetime_field_with_value_attr
expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2013-06-29T13:37:00+00:00" />}
value = DateTime.new(2013,6,29,13,37)
- assert_dom_equal(expected, datetime_field("post", "written_on", value: value))
+ assert_deprecated do
+ assert_dom_equal(expected, datetime_field("post", "written_on", value: value))
+ end
end
def test_datetime_field_with_timewithzone_value
previous_time_zone, Time.zone = Time.zone, 'UTC'
expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T15:30:45.000+0000" />}
@post.written_on = Time.zone.parse('2004-06-15 15:30:45')
- assert_dom_equal(expected, datetime_field("post", "written_on"))
+ assert_deprecated do
+ assert_dom_equal(expected, datetime_field("post", "written_on"))
+ end
ensure
Time.zone = previous_time_zone
end
@@ -1175,7 +1185,9 @@ class FormHelperTest < ActionView::TestCase
def test_datetime_field_with_nil_value
expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" />}
@post.written_on = nil
- assert_dom_equal(expected, datetime_field("post", "written_on"))
+ assert_deprecated do
+ assert_dom_equal(expected, datetime_field("post", "written_on"))
+ end
end
def test_datetime_field_with_string_values_for_min_and_max
@@ -1183,7 +1195,9 @@ class FormHelperTest < ActionView::TestCase
@post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
min_value = "2000-06-15T20:45:30.000+0000"
max_value = "2010-08-15T10:25:00.000+0000"
- assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value))
+ assert_deprecated do
+ assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value))
+ end
end
def test_datetime_field_with_invalid_string_values_for_min_and_max
@@ -1191,7 +1205,9 @@ class FormHelperTest < ActionView::TestCase
@post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
min_value = "foo"
max_value = "bar"
- assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value))
+ assert_deprecated do
+ assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value))
+ end
end
def test_datetime_local_field
diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb
index 07b3fba754..7b93c8dc29 100644
--- a/actionview/test/template/form_tag_helper_test.rb
+++ b/actionview/test/template/form_tag_helper_test.rb
@@ -622,7 +622,9 @@ class FormTagHelperTest < ActionView::TestCase
def test_datetime_field_tag
expected = %{<input id="appointment" name="appointment" type="datetime" />}
- assert_dom_equal(expected, datetime_field_tag("appointment"))
+ assert_deprecated do
+ assert_dom_equal(expected, datetime_field_tag("appointment"))
+ end
end
def test_datetime_local_field_tag
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index 48437a1c9e..882f1225fc 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -64,11 +64,11 @@ module ActiveRecord
foreign_key = join_keys.foreign_key
value = transform_value(owner[foreign_key])
- scope = scope.where(table.name => { key => value })
+ scope = scope.where(table.name.to_sym => { key => value })
if reflection.type
polymorphic_type = transform_value(owner.class.base_class.name)
- scope = scope.where(table.name => { reflection.type => polymorphic_type })
+ scope = scope.where(table.name.to_sym => { reflection.type => polymorphic_type })
end
scope
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index 7e3760d34b..2eeefb13d7 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -82,7 +82,7 @@ module ActiveRecord
# Quotes the column name. Defaults to no quoting.
def quote_column_name(column_name)
- column_name
+ column_name.to_s
end
# Quotes the table name. Defaults to column name quoting.
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 6704843c07..069346253a 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -106,6 +106,7 @@ module ActiveRecord
@schema_cache = SchemaCache.new self
@visitor = nil
@prepared_statements = false
+ @quoted_column_names, @quoted_table_names = {}, {}
end
class Version
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
index 4b6289ec98..8015d1ed9e 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -56,7 +56,6 @@ module ActiveRecord
def initialize(connection, logger, connection_options, config)
super(connection, logger, config)
- @quoted_column_names, @quoted_table_names = {}, {}
@visitor = Arel::Visitors::MySQL.new self
@@ -165,15 +164,9 @@ module ActiveRecord
raise NotImplementedError
end
+ #--
# QUOTING ==================================================
-
- def quote_column_name(name) #:nodoc:
- @quoted_column_names[name] ||= "`#{name.to_s.gsub('`', '``')}`"
- end
-
- def quote_table_name(name) #:nodoc:
- @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`')
- end
+ #++
def quoted_true
QUOTED_TRUE
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
index 8c1442eef7..7c5980da2a 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
@@ -2,6 +2,14 @@ module ActiveRecord
module ConnectionAdapters
module MySQL
module Quoting # :nodoc:
+ def quote_column_name(name)
+ @quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`"
+ end
+
+ def quote_table_name(name)
+ @quoted_table_names[name] ||= super.gsub('.', '`.`')
+ end
+
private
def _quote(value)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index c1c77a967e..6414459cd1 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -27,8 +27,8 @@ module ActiveRecord
# - schema_name."table.name"
# - "schema.name".table_name
# - "schema.name"."table.name"
- def quote_table_name(name)
- Utils.extract_schema_qualified_name(name.to_s).quoted
+ def quote_table_name(name) # :nodoc:
+ @quoted_table_names[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted
end
# Quotes schema names for use in SQL queries.
@@ -41,8 +41,8 @@ module ActiveRecord
end
# Quotes column names for use in SQL queries.
- def quote_column_name(name) #:nodoc:
- PGconn.quote_ident(name.to_s)
+ def quote_column_name(name) # :nodoc:
+ @quoted_column_names[name] ||= PGconn.quote_ident(super)
end
# Quote date/time values for use in SQL input.
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
index 3bfd0ba055..faf2f375dc 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
@@ -2,6 +2,10 @@ module ActiveRecord
module ConnectionAdapters
module SQLite3
module Quoting # :nodoc:
+ def quote_column_name(name)
+ @quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}")
+ end
+
private
def _quote(value)
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index e585fc10fe..5c8e428bef 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -87,7 +87,6 @@ module ActiveRecord
@statements = StatementPool.new(self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
@visitor = Arel::Visitors::SQLite.new self
- @quoted_column_names = {}
if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
@prepared_statements = true
@@ -187,10 +186,6 @@ module ActiveRecord
quote_column_name(attr)
end
- def quote_column_name(name) #:nodoc:
- @quoted_column_names[name] ||= %Q("#{name.to_s.gsub('"', '""')}")
- end
-
#--
# DATABASE STATEMENTS ======================================
#++
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index 86ec8000fb..c8343dd97f 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -136,7 +136,7 @@ module ActiveRecord
end
def initialize_find_by_cache # :nodoc:
- @find_by_statement_cache = {}.extend(Mutex_m)
+ @find_by_statement_cache = { true => {}.extend(Mutex_m), false => {}.extend(Mutex_m) }
end
def inherited(child_class) # :nodoc:
@@ -280,8 +280,9 @@ module ActiveRecord
private
def cached_find_by_statement(key, &block) # :nodoc:
- @find_by_statement_cache[key] || @find_by_statement_cache.synchronize {
- @find_by_statement_cache[key] ||= StatementCache.create(connection, &block)
+ cache = @find_by_statement_cache[connection.prepared_statements]
+ cache[key] || cache.synchronize {
+ cache[key] ||= StatementCache.create(connection, &block)
}
end
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index 52eab952e1..4c836378e0 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -144,6 +144,13 @@ module ActiveRecord
def table_name=(value)
value = value && value.to_s
+ if value && value.include?(".")
+ # When this deprecation warning is removed, revert commit 04ac5655be91f49cd4dfe2838df96213502fb274
+ ActiveSupport::Deprecation.warn(
+ 'Support for table names containing "." is deprecated and will be removed in Rails 5.1.'
+ )
+ end
+
if defined?(@table_name)
return if value == @table_name
reset_column_information if connected?
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index 953495a8b6..550416238f 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -84,6 +84,7 @@ module ActiveRecord
return ["1=0"] if attributes.empty?
attributes.flat_map do |key, value|
+ key = key.to_s
if value.is_a?(Hash)
associated_predicate_builder(key).expand_from_hash(value)
else
@@ -136,7 +137,9 @@ module ActiveRecord
end
def convert_dot_notation_to_hash(attributes)
- dot_notation = attributes.keys.select { |s| s.include?(".".freeze) }
+ dot_notation = attributes.keys.select do |s|
+ s.respond_to?(:include?) && s.include?(".".freeze)
+ end
dot_notation.each do |key|
table_name, column_name = key.split(".".freeze)
diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb
index dbf172a577..c0ccb00b6f 100644
--- a/activerecord/lib/active_record/relation/where_clause_factory.rb
+++ b/activerecord/lib/active_record/relation/where_clause_factory.rb
@@ -15,7 +15,6 @@ module ActiveRecord
when Hash
attributes = predicate_builder.resolve_column_aliases(opts)
attributes = klass.send(:expand_hash_conditions_for_aggregates, attributes)
- attributes.stringify_keys!
attributes, binds = predicate_builder.create_binds(attributes)
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index eef2d29d02..24746ea56d 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -1531,4 +1531,12 @@ class BasicsTest < ActiveRecord::TestCase
assert Developer.new.respond_to?(:last_name=)
assert Developer.new.respond_to?(:last_name?)
end
+
+ test "table names containing a dot are deprecated" do
+ assert_deprecated do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = "foo.bar"
+ end
+ end
+ end
end
diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb
index a704b861cb..104226010a 100644
--- a/activerecord/test/cases/statement_cache_test.rb
+++ b/activerecord/test/cases/statement_cache_test.rb
@@ -94,5 +94,17 @@ module ActiveRecord
additional_books = cache.execute([], Book, Book.connection)
assert first_books != additional_books
end
+
+ def test_unprepared_statements_dont_share_a_cache_with_prepared_statements
+ Book.create(name: "my book")
+ Book.create(name: "my other book")
+
+ book = Book.find_by(name: "my book")
+ other_book = Book.connection.unprepared_statement do
+ Book.find_by(name: "my other book")
+ end
+
+ refute_equal book, other_book
+ end
end
end
diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md
index 8997363fce..848c9caa59 100644
--- a/guides/source/action_controller_overview.md
+++ b/guides/source/action_controller_overview.md
@@ -1088,6 +1088,8 @@ You can filter out sensitive request parameters from your log files by appending
config.filter_parameters << :password
```
+NOTE: Provided parameters will be filtered out by partial matching regular expression. Rails adds default `:password` in the appropriate initializer (`initializers/filter_parameter_logging.rb`) and cares about typical application parameters `password` and `password_confirmation`.
+
### Redirects Filtering
Sometimes it's desirable to filter out from log files some sensitive locations your application is redirecting to.
diff --git a/guides/source/configuring.md b/guides/source/configuring.md
index e57d873719..e80f994deb 100644
--- a/guides/source/configuring.md
+++ b/guides/source/configuring.md
@@ -104,7 +104,7 @@ application. Accepts a valid week day symbol (e.g. `:monday`).
* `config.filter_parameters` used for filtering out the parameters that
you don't want shown in the logs, such as passwords or credit card
-numbers. New applications filter out passwords by adding the following `config.filter_parameters+=[:password]` in `config/initializers/filter_parameter_logging.rb`.
+numbers. New applications filter out passwords by adding the following `config.filter_parameters+=[:password]` in `config/initializers/filter_parameter_logging.rb`. Parameters filter works by partial matching regular expression.
* `config.force_ssl` forces all requests to be served over HTTPS by using the `ActionDispatch::SSL` middleware, and sets `config.action_mailer.default_url_options` to be `{ protocol: 'https' }`. This can be configured by setting `config.ssl_options` - see the [ActionDispatch::SSL documentation](http://edgeapi.rubyonrails.org/classes/ActionDispatch/SSL.html) for details.
diff --git a/guides/source/security.md b/guides/source/security.md
index 4883c1abe7..16c5291037 100644
--- a/guides/source/security.md
+++ b/guides/source/security.md
@@ -494,6 +494,8 @@ By default, Rails logs all requests being made to the web application. But log f
config.filter_parameters << :password
```
+NOTE: Provided parameters will be filtered out by partial matching regular expression. Rails adds default `:password` in the appropriate initializer (`initializers/filter_parameter_logging.rb`) and cares about typical application parameters `password` and `password_confirmation`.
+
### Good Passwords
INFO: _Do you find it hard to remember all your passwords? Don't write them down, but use the initial letters of each word in an easy to remember sentence._