aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--actionpack/CHANGELOG.md8
-rw-r--r--actionpack/lib/action_controller/metal/strong_parameters.rb49
-rw-r--r--actionpack/test/controller/parameters/parameters_permit_test.rb38
-rw-r--r--actionview/lib/action_view/view_paths.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb11
-rw-r--r--guides/source/action_controller_overview.md18
-rw-r--r--railties/lib/rails/generators/app_base.rb7
-rw-r--r--railties/test/generators/app_generator_test.rb10
8 files changed, 139 insertions, 14 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index d4b8d9b456..b539c893e3 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,3 +1,11 @@
+* Add support for arbitrary hashes in strong parameters:
+
+ ```ruby
+ params.permit(preferences: {})
+ ```
+
+ *Xavier Noria*
+
* Add `ActionController::Parameters#merge!`, which behaves the same as `Hash#merge!`.
*Yuji Yaginuma*
diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb
index 98aacd53ab..1977632c4e 100644
--- a/actionpack/lib/action_controller/metal/strong_parameters.rb
+++ b/actionpack/lib/action_controller/metal/strong_parameters.rb
@@ -334,6 +334,15 @@ module ActionController
# params = ActionController::Parameters.new(tags: ['rails', 'parameters'])
# params.permit(tags: [])
#
+ # Sometimes it is not possible or convenient to declare the valid keys of
+ # a hash parameter or its internal structure. Just map to an empty hash:
+ #
+ # params.permit(preferences: {})
+ #
+ # but be careful because this opens the door to arbitrary input. In this
+ # case, +permit+ ensures values in the returned structure are permitted
+ # scalars and filters out anything else.
+ #
# You can also use +permit+ on nested parameters, like:
#
# params = ActionController::Parameters.new({
@@ -389,7 +398,8 @@ module ActionController
unpermitted_parameters!(params) if self.class.action_on_unpermitted_parameters
- params.permit!
+ params.permitted = true
+ params
end
# Returns a parameter for the given +key+. If not found,
@@ -766,6 +776,7 @@ module ActionController
end
EMPTY_ARRAY = []
+ EMPTY_HASH = {}
def hash_filter(params, filter)
filter = filter.with_indifferent_access
@@ -779,6 +790,11 @@ module ActionController
array_of_permitted_scalars?(self[key]) do |val|
params[key] = val
end
+ elsif filter[key] == EMPTY_HASH
+ # Declaration { preferences: {} }.
+ if value.is_a?(Parameters)
+ params[key] = permit_any_in_parameters(value)
+ end
elsif non_scalar?(value)
# Declaration { user: :name } or { user: [:name, :age, { address: ... }] }.
params[key] = each_element(value) do |element|
@@ -788,6 +804,37 @@ module ActionController
end
end
+ def permit_any_in_parameters(params)
+ self.class.new.tap do |sanitized|
+ params.each do |key, value|
+ if permitted_scalar?(value)
+ sanitized[key] = value
+ elsif value.is_a?(Array)
+ sanitized[key] = permit_any_in_array(value)
+ elsif value.is_a?(Parameters)
+ sanitized[key] = permit_any_in_parameters(value)
+ else
+ # Filter this one out.
+ end
+ end
+ sanitized.permitted = true
+ end
+ end
+
+ def permit_any_in_array(array)
+ [].tap do |sanitized|
+ array.each do |element|
+ if permitted_scalar?(element)
+ sanitized << element
+ elsif element.is_a?(Parameters)
+ sanitized << permit_any_in_parameters(element)
+ else
+ # Filter this one out.
+ end
+ end
+ end
+ end
+
def initialize_copy(source)
super
@parameters = @parameters.dup
diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb
index 2b9a95d17c..60bfb66f2f 100644
--- a/actionpack/test/controller/parameters/parameters_permit_test.rb
+++ b/actionpack/test/controller/parameters/parameters_permit_test.rb
@@ -168,6 +168,44 @@ class ParametersPermitTest < ActiveSupport::TestCase
end
end
+ test "key to empty hash: arbitrary hashes are permitted" do
+ params = ActionController::Parameters.new(
+ username: "fxn",
+ preferences: {
+ scheme: "Marazul",
+ font: {
+ name: "Source Code Pro",
+ size: 12
+ },
+ tabstops: [4, 8, 12, 16],
+ suspicious: [true, Object.new, false, /yo!/],
+ dubious: [{a: :a, b: /wtf!/}, {c: :c}],
+ injected: Object.new
+ },
+ hacked: 1 # not a hash
+ )
+
+ permitted = params.permit(:username, preferences: {}, hacked: {})
+
+ assert permitted.permitted?
+ assert permitted[:preferences].permitted?
+ assert permitted[:preferences][:font].permitted?
+ assert permitted[:preferences][:dubious].all?(&:permitted?)
+
+ assert_equal "fxn", permitted[:username]
+ assert_equal "Marazul", permitted[:preferences][:scheme]
+ assert_equal "Source Code Pro", permitted[:preferences][:font][:name]
+ assert_equal 12, permitted[:preferences][:font][:size]
+ assert_equal [4, 8, 12, 16], permitted[:preferences][:tabstops]
+ assert_equal [true, false], permitted[:preferences][:suspicious]
+ assert_equal :a, permitted[:preferences][:dubious][0][:a]
+ assert_equal :c, permitted[:preferences][:dubious][1][:c]
+
+ assert_filtered_out permitted[:preferences][:dubious][0], :b
+ assert_filtered_out permitted[:preferences], :injected
+ assert_filtered_out permitted, :hacked
+ end
+
test "fetch raises ParameterMissing exception" do
e = assert_raises(ActionController::ParameterMissing) do
@params.fetch :foo
diff --git a/actionview/lib/action_view/view_paths.rb b/actionview/lib/action_view/view_paths.rb
index b5cde5b43f..a9638d1e6d 100644
--- a/actionview/lib/action_view/view_paths.rb
+++ b/actionview/lib/action_view/view_paths.rb
@@ -46,10 +46,22 @@ module ActionView
{}
end
+ # Append a path to the list of view paths for the current <tt>LookupContext</tt>.
+ #
+ # ==== Parameters
+ # * <tt>path</tt> - If a String is provided, it gets converted into
+ # the default view path. You may also provide a custom view path
+ # (see ActionView::PathSet for more information)
def append_view_path(path)
lookup_context.view_paths.push(*path)
end
+ # Prepend a path to the list of view paths for the current <tt>LookupContext</tt>.
+ #
+ # ==== Parameters
+ # * <tt>path</tt> - If a String is provided, it gets converted into
+ # the default view path. You may also provide a custom view path
+ # (see ActionView::PathSet for more information)
def prepend_view_path(path)
lookup_context.view_paths.unshift(*path)
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 6b215a7374..0493ab4e4b 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -304,14 +304,6 @@ module ActiveRecord
select_values(sql, "SCHEMA").any?
end
- # Returns an array of +Column+ objects for the table specified by +table_name+.
- def columns(table_name) # :nodoc:
- table_name = table_name.to_s
- table_structure(table_name).map do |field|
- new_column_from_field(table_name, field)
- end
- end
-
def new_column_from_field(table_name, field) # :nondoc:
case field["dflt_value"]
when /^null$/i
@@ -434,11 +426,12 @@ module ActiveRecord
protected
- def table_structure(table_name)
+ def table_structure(table_name) # :nodoc:
structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA")
raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
table_structure_with_collation(table_name, structure)
end
+ alias column_definitions table_structure
def alter_table(table_name, options = {}) #:nodoc:
altered_table_name = "a#{table_name}"
diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md
index 7b1138c7d4..40eb838d32 100644
--- a/guides/source/action_controller_overview.md
+++ b/guides/source/action_controller_overview.md
@@ -258,6 +258,17 @@ scalar values, map the key to an empty array:
params.permit(id: [])
```
+Sometimes it is not possible or convenient to declare the valid keys of
+a hash parameter or its internal structure. Just map to an empty hash:
+
+```ruby
+params.permit(preferences: {})
+```
+
+but be careful because this opens the door to arbitrary input. In this
+case, `permit` ensures values in the returned structure are permitted
+scalars and filters out anything else.
+
To whitelist an entire hash of parameters, the `permit!` method can be
used:
@@ -265,9 +276,10 @@ used:
params.require(:log_entry).permit!
```
-This will mark the `:log_entry` parameters hash and any sub-hash of it as
-permitted. Extreme care should be taken when using `permit!`, as it
-will allow all current and future model attributes to be mass-assigned.
+This marks the `:log_entry` parameters hash and any sub-hash of it as
+permitted and does not check for permitted scalars, anything is accepted.
+Extreme care should be taken when using `permit!`, as it will allow all current
+and future model attributes to be mass-assigned.
#### Nested Parameters
diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb
index 91342c592c..83e9c30548 100644
--- a/railties/lib/rails/generators/app_base.rb
+++ b/railties/lib/rails/generators/app_base.rb
@@ -67,6 +67,9 @@ module Rails
class_option :skip_listen, type: :boolean, default: false,
desc: "Don't generate configuration that depends on the listen gem"
+ class_option :skip_coffee, type: :boolean, default: false,
+ desc: "Don't use CoffeeScript"
+
class_option :skip_javascript, type: :boolean, aliases: "-J", default: false,
desc: "Skip JavaScript files"
@@ -322,7 +325,9 @@ module Rails
if options[:skip_javascript] || options[:skip_sprockets]
[]
else
- gems = [coffee_gemfile_entry, javascript_runtime_gemfile_entry]
+ gems = [javascript_runtime_gemfile_entry]
+ gems << coffee_gemfile_entry unless options[:skip_coffee]
+
gems << GemfileEntry.version("#{options[:javascript]}-rails", nil,
"Use #{options[:javascript]} as the JavaScript library")
diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb
index 6c6c5613ab..3ec99193e3 100644
--- a/railties/test/generators/app_generator_test.rb
+++ b/railties/test/generators/app_generator_test.rb
@@ -488,6 +488,16 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_coffeescript_is_skipped_if_required
+ run_generator [destination_root, "--skip-coffee"]
+
+ assert_file "Gemfile" do |content|
+ assert_no_match(/coffee-rails/, content)
+ assert_match(/jquery-rails/, content)
+ assert_match(/uglifier/, content)
+ end
+ end
+
def test_inclusion_of_jbuilder
run_generator
assert_gem "jbuilder"