diff options
Diffstat (limited to 'activemodel/lib')
-rw-r--r-- | activemodel/lib/active_model/mass_assignment_security.rb | 117 | ||||
-rw-r--r-- | activemodel/lib/active_model/observer_array.rb | 104 | ||||
-rw-r--r-- | activemodel/lib/active_model/observing.rb | 28 | ||||
-rw-r--r-- | activemodel/lib/active_model/validations.rb | 2 | ||||
-rw-r--r-- | activemodel/lib/active_model/validations/callbacks.rb | 4 |
5 files changed, 215 insertions, 40 deletions
diff --git a/activemodel/lib/active_model/mass_assignment_security.rb b/activemodel/lib/active_model/mass_assignment_security.rb index be48415739..01eef762fd 100644 --- a/activemodel/lib/active_model/mass_assignment_security.rb +++ b/activemodel/lib/active_model/mass_assignment_security.rb @@ -24,10 +24,7 @@ module ActiveModel # include ActiveModel::MassAssignmentSecurity # # attr_accessible :first_name, :last_name - # - # def self.admin_accessible_attributes - # accessible_attributes + [ :plan_id ] - # end + # attr_accessible :first_name, :last_name, :plan_id, :as => :admin # # def update # ... @@ -38,18 +35,17 @@ module ActiveModel # protected # # def account_params - # sanitize_for_mass_assignment(params[:account]) - # end - # - # def mass_assignment_authorizer - # admin ? admin_accessible_attributes : super + # scope = admin ? :admin : :default + # sanitize_for_mass_assignment(params[:account], scope) # end # # end # module ClassMethods # Attributes named in this macro are protected from mass-assignment - # whenever attributes are sanitized before assignment. + # whenever attributes are sanitized before assignment. A scope for the + # attributes is optional, if no scope is provided then :default is used. + # A scope can be defined by using the :as option. # # Mass-assignment to these attributes will simply be ignored, to assign # to them you can use direct writer methods. This is meant to protect @@ -60,36 +56,58 @@ module ActiveModel # include ActiveModel::MassAssignmentSecurity # # attr_accessor :name, :credit_rating - # attr_protected :credit_rating # - # def attributes=(values) - # sanitize_for_mass_assignment(values).each do |k, v| + # attr_protected :credit_rating, :last_login + # attr_protected :last_login, :as => :admin + # + # def assign_attributes(values, options = {}) + # sanitize_for_mass_assignment(values, options[:as]).each do |k, v| # send("#{k}=", v) # end # end # end # + # When using a :default scope : + # # customer = Customer.new - # customer.attributes = { "name" => "David", "credit_rating" => "Excellent" } + # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :default) # customer.name # => "David" # customer.credit_rating # => nil + # customer.last_login # => nil # # customer.credit_rating = "Average" # customer.credit_rating # => "Average" # + # And using the :admin scope : + # + # customer = Customer.new + # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :admin) + # customer.name # => "David" + # customer.credit_rating # => "Excellent" + # customer.last_login # => nil + # # To start from an all-closed default and enable attributes as needed, # have a look at +attr_accessible+. # # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_protected+ # to sanitize attributes won't provide sufficient protection. - def attr_protected(*names) - self._protected_attributes = self.protected_attributes + names + def attr_protected(*args) + options = args.extract_options! + scope = options[:as] || :default + + self._protected_attributes = protected_attributes_configs.dup + self._protected_attributes[scope] = self.protected_attributes(scope) + args + self._active_authorizer = self._protected_attributes end # Specifies a white list of model attributes that can be set via # mass-assignment. # + # Like +attr_protected+, a scope for the attributes is optional, + # if no scope is provided then :default is used. A scope can be defined by + # using the :as option. + # # This is the opposite of the +attr_protected+ macro: Mass-assignment # will only set attributes in this list, to assign to the rest of # attributes you can use direct writer methods. This is meant to protect @@ -102,57 +120,90 @@ module ActiveModel # include ActiveModel::MassAssignmentSecurity # # attr_accessor :name, :credit_rating + # # attr_accessible :name + # attr_accessible :name, :credit_rating, :as => :admin # - # def attributes=(values) - # sanitize_for_mass_assignment(values).each do |k, v| + # def assign_attributes(values, options = {}) + # sanitize_for_mass_assignment(values, options[:as]).each do |k, v| # send("#{k}=", v) # end # end # end # + # When using a :default scope : + # # customer = Customer.new - # customer.attributes = { :name => "David", :credit_rating => "Excellent" } + # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :default) # customer.name # => "David" # customer.credit_rating # => nil # # customer.credit_rating = "Average" # customer.credit_rating # => "Average" # + # And using the :admin scope : + # + # customer = Customer.new + # customer.assign_attributes({ "name" => "David", "credit_rating" => "Excellent", :last_login => 1.day.ago }, :as => :admin) + # customer.name # => "David" + # customer.credit_rating # => "Excellent" + # # Note that using <tt>Hash#except</tt> or <tt>Hash#slice</tt> in place of +attr_accessible+ # to sanitize attributes won't provide sufficient protection. - def attr_accessible(*names) - self._accessible_attributes = self.accessible_attributes + names + def attr_accessible(*args) + options = args.extract_options! + scope = options[:as] || :default + + self._accessible_attributes = accessible_attributes_configs.dup + self._accessible_attributes[scope] = self.accessible_attributes(scope) + args + self._active_authorizer = self._accessible_attributes end - def protected_attributes - self._protected_attributes ||= BlackList.new(attributes_protected_by_default).tap do |w| - w.logger = self.logger if self.respond_to?(:logger) - end + def protected_attributes(scope = :default) + protected_attributes_configs[scope] end - def accessible_attributes - self._accessible_attributes ||= WhiteList.new.tap { |w| w.logger = self.logger if self.respond_to?(:logger) } + def accessible_attributes(scope = :default) + accessible_attributes_configs[scope] end - def active_authorizer - self._active_authorizer ||= protected_attributes + def active_authorizers + self._active_authorizer ||= protected_attributes_configs end + alias active_authorizer active_authorizers def attributes_protected_by_default [] end + + private + + def protected_attributes_configs + self._protected_attributes ||= begin + default_black_list = BlackList.new(attributes_protected_by_default).tap do |w| + w.logger = self.logger if self.respond_to?(:logger) + end + Hash.new(default_black_list) + end + end + + def accessible_attributes_configs + self._accessible_attributes ||= begin + default_white_list = WhiteList.new.tap { |w| w.logger = self.logger if self.respond_to?(:logger) } + Hash.new(default_white_list) + end + end end protected - def sanitize_for_mass_assignment(attributes) - mass_assignment_authorizer.sanitize(attributes) + def sanitize_for_mass_assignment(attributes, scope = :default) + mass_assignment_authorizer(scope).sanitize(attributes) end - def mass_assignment_authorizer - self.class.active_authorizer + def mass_assignment_authorizer(scope = :default) + self.class.active_authorizer[scope] end end end diff --git a/activemodel/lib/active_model/observer_array.rb b/activemodel/lib/active_model/observer_array.rb new file mode 100644 index 0000000000..ab7f86007f --- /dev/null +++ b/activemodel/lib/active_model/observer_array.rb @@ -0,0 +1,104 @@ +require 'set' + +module ActiveModel + # Stores the enabled/disabled state of individual observers for + # a particular model classes. + class ObserverArray < Array + attr_reader :model_class + def initialize(model_class, *args) + @model_class = model_class + super(*args) + end + + def disabled_for?(observer) + disabled_observers.include?(observer.class) + end + + def disable(*observers, &block) + set_enablement(false, observers, &block) + end + + def enable(*observers, &block) + set_enablement(true, observers, &block) + end + + protected + + def disabled_observers + @disabled_observers ||= Set.new + end + + def observer_class_for(observer) + return observer if observer.is_a?(Class) + + if observer.respond_to?(:to_sym) # string/symbol + observer.to_s.camelize.constantize + else + raise ArgumentError, "#{observer} was not a class or a " + + "lowercase, underscored class name as expected." + end + end + + def start_transaction + disabled_observer_stack.push(disabled_observers.dup) + each_subclass_array do |array| + array.start_transaction + end + end + + def disabled_observer_stack + @disabled_observer_stack ||= [] + end + + def end_transaction + @disabled_observers = disabled_observer_stack.pop + each_subclass_array do |array| + array.end_transaction + end + end + + def transaction + start_transaction + + begin + yield + ensure + end_transaction + end + end + + def each_subclass_array + model_class.descendants.each do |subclass| + yield subclass.observers + end + end + + def set_enablement(enabled, observers) + if block_given? + transaction do + set_enablement(enabled, observers) + yield + end + else + observers = ActiveModel::Observer.descendants if observers == [:all] + observers.each do |obs| + klass = observer_class_for(obs) + + unless klass < ActiveModel::Observer + raise ArgumentError.new("#{obs} does not refer to a valid observer") + end + + if enabled + disabled_observers.delete(klass) + else + disabled_observers << klass + end + end + + each_subclass_array do |array| + array.set_enablement(enabled, observers) + end + end + end + end +end diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb index 3c80d584fe..c1ac4eb4af 100644 --- a/activemodel/lib/active_model/observing.rb +++ b/activemodel/lib/active_model/observing.rb @@ -1,13 +1,20 @@ require 'singleton' +require 'active_model/observer_array' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/module/aliasing' require 'active_support/core_ext/module/remove_method' require 'active_support/core_ext/string/inflections' +require 'active_support/core_ext/enumerable' +require 'active_support/descendants_tracker' module ActiveModel module Observing extend ActiveSupport::Concern + included do + extend ActiveSupport::DescendantsTracker + end + module ClassMethods # == Active Model Observers Activation # @@ -30,12 +37,12 @@ module ActiveModel # +instantiate_observers+ is called during startup, and before # each development request. def observers=(*values) - @observers = values.flatten + observers.replace(values.flatten) end # Gets the current observers. def observers - @observers ||= [] + @observers ||= ObserverArray.new(self) end # Gets the current observer instances. @@ -76,7 +83,11 @@ module ActiveModel elsif observer.respond_to?(:instance) observer.instance else - raise ArgumentError, "#{observer} must be a lowercase, underscored class name (or an instance of the class itself) responding to the instance method. Example: Person.observers = :big_brother # calls BigBrother.instance" + raise ArgumentError, + "#{observer} must be a lowercase, underscored class name (or an " + + "instance of the class itself) responding to the instance " + + "method. Example: Person.observers = :big_brother # calls " + + "BigBrother.instance" end end @@ -165,6 +176,7 @@ module ActiveModel # class Observer include Singleton + extend ActiveSupport::DescendantsTracker class << self # Attaches the observer to the supplied model classes. @@ -210,7 +222,9 @@ module ActiveModel # Send observed_method(object) if the method exists. def update(observed_method, object) #:nodoc: - send(observed_method, object) if respond_to?(observed_method) + return unless respond_to?(observed_method) + return if disabled_for?(object) + send(observed_method, object) end # Special method sent by the observed class when it is inherited. @@ -224,5 +238,11 @@ module ActiveModel def add_observer!(klass) #:nodoc: klass.add_observer(self) end + + def disabled_for?(object) + klass = object.class + return false unless klass.respond_to?(:observers) + klass.observers.disabled_for?(self) + end end end diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index d968609e67..5e567307f3 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -133,7 +133,7 @@ module ActiveModel if options.key?(:on) options = options.dup options[:if] = Array.wrap(options[:if]) - options[:if] << "validation_context == :#{options[:on]}" + options[:if].unshift("validation_context == :#{options[:on]}") end args << options set_callback(:validate, *args, &block) diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb index adc2867ad0..22a77320dc 100644 --- a/activemodel/lib/active_model/validations/callbacks.rb +++ b/activemodel/lib/active_model/validations/callbacks.rb @@ -31,7 +31,7 @@ module ActiveModel options = args.last if options.is_a?(Hash) && options[:on] options[:if] = Array.wrap(options[:if]) - options[:if] << "self.validation_context == :#{options[:on]}" + options[:if].unshift("self.validation_context == :#{options[:on]}") end set_callback(:validation, :before, *args, &block) end @@ -41,7 +41,7 @@ module ActiveModel options[:prepend] = true options[:if] = Array.wrap(options[:if]) options[:if] << "!halted" - options[:if] << "self.validation_context == :#{options[:on]}" if options[:on] + options[:if].unshift("self.validation_context == :#{options[:on]}") if options[:on] set_callback(:validation, :after, *(args << options), &block) end end |