Skip to content
This repository
Browse code

Don't use mass-assignment protection when applying the scoped.scope_f…

…or_create. Fixes #481.
  • Loading branch information...
commit 9a7dbe2c0570e11b9033df735c937d5f5416e0ca 1 parent 8f999a3
Jon Leighton authored May 10, 2011
6  activerecord/lib/active_record/associations/collection_association.rb
@@ -99,7 +99,6 @@ def build(attributes = {}, options = {}, &block)
99 99
         else
100 100
           add_to_target(build_record(attributes, options)) do |record|
101 101
             yield(record) if block_given?
102  
-            set_owner_attributes(record)
103 102
           end
104 103
         end
105 104
       end
@@ -423,7 +422,10 @@ def insert_record(record, validate = true)
423 422
         end
424 423
 
425 424
         def build_record(attributes, options)
426  
-          reflection.build_association(scoped.scope_for_create.merge(attributes || {}), options)
  425
+          record = reflection.build_association
  426
+          record.assign_attributes(scoped.scope_for_create, :without_protection => true)
  427
+          record.assign_attributes(attributes || {}, options)
  428
+          record
427 429
         end
428 430
 
429 431
         def delete_or_destroy(records, method)
4  activerecord/lib/active_record/associations/through_association.rb
@@ -14,7 +14,9 @@ module ThroughAssociation #:nodoc:
14 14
         def target_scope
15 15
           scope = super
16 16
           chain[1..-1].each do |reflection|
17  
-            scope = scope.merge(reflection.klass.scoped)
  17
+            # Discard the create with value, as we don't want that the affect the objects we
  18
+            # create on the association
  19
+            scope = scope.merge(reflection.klass.scoped.create_with(nil))
18 20
           end
19 21
           scope
20 22
         end
9  activerecord/test/cases/associations/has_many_associations_test.rb
@@ -86,11 +86,20 @@ def test_create_from_association_set_owner_attributes_by_passing_protection
86 86
     bulb = car.bulbs.new
87 87
     assert_equal car.id, bulb.car_id
88 88
 
  89
+    bulb = car.bulbs.new :car_id => car.id + 1
  90
+    assert_equal car.id, bulb.car_id
  91
+
89 92
     bulb = car.bulbs.build
90 93
     assert_equal car.id, bulb.car_id
91 94
 
  95
+    bulb = car.bulbs.build :car_id => car.id + 1
  96
+    assert_equal car.id, bulb.car_id
  97
+
92 98
     bulb = car.bulbs.create
93 99
     assert_equal car.id, bulb.car_id
  100
+
  101
+    bulb = car.bulbs.create :car_id => car.id + 1
  102
+    assert_equal car.id, bulb.car_id
94 103
   ensure
95 104
     Bulb.attr_protected :id
96 105
   end

1 note on commit 9a7dbe2

José Valim
Owner

Sweeeeeeet!

Jon Leighton

@josevalim: could you please sanity-check this for me? I'm pretty sure it's okay to do this without protection (as 'scoped' shouldn't contain user input), but I would appreciate another pair of eyes on this so it doesn't come back to bite me later ;)

Jon Leighton

The main difference between this and the previous version is that this will incorporate conditions which are defined on the association (and they won't be protected when assigned).

So one question is: should those conditions defined on the association be assigned with protection or not? I would say not (as it isn't user input), but let me hear your thoughts. Once we decide I should add a test case.

José Valim

Isn't this something we would like to cache or it is cached on super?

José Valim

It looks fine and I agree scope conditions should be without protection. You just don't need (attributes || {}) here, assign_attributes will automatically abort if attributes is nil. :)

Jon Leighton

We don't want to cache it because the result of super could be different depending on whether or not we are within a scoping { ... } block.

We cache association_scope and then merge it into target_scope when scoped is called.

Andrew White

I've some STI models that override new to return a subclass based upon a key in the attributes hash - any chance we can pass the attributes to build_association or is that going to run into problems with attribute protection?

Jon Leighton

@pixeltrix I am not sure about this.

We need to assign scoped.scope_for_create without protection, so it cannot be through new.

And we need attributes to be assigned, with protection, after scoped.scope_for_create so that the attributes can override any defaults if necessary.

So the only way I can think to support what you're saying is to assign attributes once via new and then again after the scoped.scope_for_create has been applied. This seems sub-optimal and could lead to bugs where the user is not expecting something to be assigned twice.

So my suggestion would be that you create a different class method which does the subclass disambiguation before passing the attributes on to new/build/etc.

@josevalim, your thoughts?

José Valim

You can pass :without_protection => true to new. It should work fine.

Jon Leighton

Ok, but @pixeltrix said that his code works based on the attributes hash, not based on scoped.scoped_for_create... @pixeltrix: c/d?

José Valim

Oh, touchez. There is nothing we can do then.

Andrew White

This is basically what I do at the moment:

class Menu < ActiveRecord::Base
  has_many :links, :dependent => :destroy
  accept_nested_attributes_for :links
end

class Link < ActiveRecord::Base
  include Rails.application.routes.url_helpers

  class << self
    def new(attributes = {}, &block)
      link_type = attributes.delete(:link_type)
      return super if link_type.blank?

      begin
        link_class(link_type).create_with(scoped.scope_for_create).new(attributes, &block)
      rescue NameError
        raise RuntimeError, "Unknown link type #{link_type}"
      end
    end

    def link_class(link)
      "#{link.to_s.camelize}Link".constantize
    end
  end
end

class IndexLink < Link
  def url(options = {})
    root_path(options)
  end
end

page.links.build(:link_type => :index) 

The only workaround I've found so far is to override the build method in the association definition, e.g:

class Menu < ActiveRecord::Base
  has_many :links do
    def build(attributes = {}, options = {}, &block)
      # do stuff
    end
  end
end

This works but is kinda sucky - what about adding support for specifying :type in the attributes hash and the association code using the appropriate subclass?

Jon Leighton

what about adding support for specifying :type in the attributes hash

That would open up the possibility for user input to specify what class an object is (without the author necessarily have intended that to be the case). :type => 'AdminUser', anyone?

I think the only proper way to support this would be to add to the API, and I am not sure that we should...

Andrew White

Well I was thinking of limiting to subclasses of the association class. :-)

It would be disabled by default - you'd have to use attr_accessible to enable it and it would also honour the :as option.

Jon Leighton

@pixeltrix that looks good to me, though I would prefer to call stringify_keys on attributes than convert the scope to a HashWithIndifferentAccess.

Please sign in to comment.
Something went wrong with that request. Please try again.