aboutsummaryrefslogblamecommitdiffstats
path: root/activerecord/lib/active_record/attribute_assignment.rb
blob: b030e64562de365d7eafea26c3a979845b26bd94 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11

                   
                                                 
                                                                       
                                                                       

     



                                               
               
                                          

       
                       
                                    
                                                                        


             







                                                                                                        




                                                                                                           

















































                                                                                                   
                                     
 

                                                                 
                                      

                                                            







                                                                                   

                                                 
            
                                 


           
                                                                                                               
                                                                                                           

                                                 













                                              









                                                              




                                                                                    
                                                                                                                 
                                                                                                               










                                                                                                                 





                                                                                         
                                                                                                                                                           


                          

                                                                                                                                                      


         





















                                                                                                    
                                                               

                                                            

                                                                                           
                      
                                                                       






                                                                       
                                                                       

                                                                                          
                             
                                                                                                              
                                                         



                                                                              
                                                                           



                                                                                        
 

                                                                                               
                                                     
                                                                                  














                                                                                                                                                                      



                                                                           


                        






                                                                                        



                                                                                                






                                                                                

     
module ActiveRecord
  ActiveSupport.on_load(:active_record_config) do
    mattr_accessor :whitelist_attributes,      instance_accessor: false
    mattr_accessor :mass_assignment_sanitizer, instance_accessor: false
  end

  module AttributeAssignment
    extend ActiveSupport::Concern
    include ActiveModel::MassAssignmentSecurity

    included do
      initialize_mass_assignment_sanitizer
    end

    module ClassMethods
      def inherited(child) # :nodoc:
        child.send :initialize_mass_assignment_sanitizer if self == Base
        super
      end

      private

      # The primary key and inheritance column can never be set by mass-assignment for security reasons.
      def attributes_protected_by_default
        default = [ primary_key, inheritance_column ]
        default << 'id' unless primary_key.eql? 'id'
        default
      end

      def initialize_mass_assignment_sanitizer
        attr_accessible(nil) if Model.whitelist_attributes
        self.mass_assignment_sanitizer = Model.mass_assignment_sanitizer if Model.mass_assignment_sanitizer
      end
    end

    # Allows you to set all the attributes at once by passing in a hash with keys
    # matching the attribute names (which again matches the column names).
    #
    # If any attributes are protected by either +attr_protected+ or
    # +attr_accessible+ then only settable attributes will be assigned.
    #
    #   class User < ActiveRecord::Base
    #     attr_protected :is_admin
    #   end
    #
    #   user = User.new
    #   user.attributes = { :username => 'Phusion', :is_admin => true }
    #   user.username   # => "Phusion"
    #   user.is_admin?  # => false
    def attributes=(new_attributes)
      return unless new_attributes.is_a?(Hash)

      assign_attributes(new_attributes)
    end

    # Allows you to set all the attributes for a particular mass-assignment
    # security role by passing in a hash of attributes with keys matching
    # the attribute names (which again matches the column names) and the role
    # name using the :as option.
    #
    # To bypass mass-assignment security you can use the :without_protection => true
    # option.
    #
    #   class User < ActiveRecord::Base
    #     attr_accessible :name
    #     attr_accessible :name, :is_admin, :as => :admin
    #   end
    #
    #   user = User.new
    #   user.assign_attributes({ :name => 'Josh', :is_admin => true })
    #   user.name       # => "Josh"
    #   user.is_admin?  # => false
    #
    #   user = User.new
    #   user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin)
    #   user.name       # => "Josh"
    #   user.is_admin?  # => true
    #
    #   user = User.new
    #   user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true)
    #   user.name       # => "Josh"
    #   user.is_admin?  # => true
    def assign_attributes(new_attributes, options = {})
      return if new_attributes.blank?

      attributes                  = new_attributes.stringify_keys
      multi_parameter_attributes  = []
      nested_parameter_attributes = []
      previous_options            = @mass_assignment_options
      @mass_assignment_options    = options

      unless options[:without_protection]
        attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role)
      end

      attributes.each do |k, v|
        if k.include?("(")
          multi_parameter_attributes << [ k, v ]
        elsif v.is_a?(Hash)
          nested_parameter_attributes << [ k, v ]
        else
          _assign_attribute(k, v)
        end
      end

      assign_nested_parameter_attributes(nested_parameter_attributes) unless nested_parameter_attributes.empty?
      assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty?
    ensure
      @mass_assignment_options = previous_options
    end

    protected

    def mass_assignment_options
      @mass_assignment_options ||= {}
    end

    def mass_assignment_role
      mass_assignment_options[:as] || :default
    end

    private

    def _assign_attribute(k, v)
      public_send("#{k}=", v)
    rescue NoMethodError
      if respond_to?("#{k}=")
        raise
      else
        raise UnknownAttributeError, "unknown attribute: #{k}"
      end
    end

    # Assign any deferred nested attributes after the base attributes have been set.
    def assign_nested_parameter_attributes(pairs)
      pairs.each { |k, v| _assign_attribute(k, v) }
    end

    # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
    # by calling new on the column type or aggregation type (through composed_of) object with these parameters.
    # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
    # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
    # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum,
    # f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the
    # attribute will be set to nil.
    def assign_multiparameter_attributes(pairs)
      execute_callstack_for_multiparameter_attributes(
        extract_callstack_for_multiparameter_attributes(pairs)
      )
    end

    def execute_callstack_for_multiparameter_attributes(callstack)
      errors = []
      callstack.each do |name, values_with_empty_parameters|
        begin
          send(name + "=", read_value_from_parameter(name, values_with_empty_parameters))
        rescue => ex
          errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name)
        end
      end
      unless errors.empty?
        error_descriptions = errors.map { |ex| ex.message }.join(",")
        raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]"
      end
    end

    def extract_callstack_for_multiparameter_attributes(pairs)
      attributes = { }

      pairs.each do |(multiparameter_name, value)|
        attribute_name = multiparameter_name.split("(").first
        attributes[attribute_name] ||= {}

        parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
        attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
      end

      attributes
    end

    def instantiate_time_object(name, values)
      if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name))
        Time.zone.local(*values)
      else
        Time.time_with_datetime_fallback(self.class.default_timezone, *values)
      end
    end

    def read_value_from_parameter(name, values_hash_from_param)
      return if values_hash_from_param.values.compact.empty?

      column = self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)
      klass  = column.klass
      if klass == Time
        read_time_parameter_value(column, name, values_hash_from_param)
      elsif klass == Date
        read_date_parameter_value(name, values_hash_from_param)
      else
        read_other_parameter_value(klass, name, values_hash_from_param)
      end
    end

    def read_time_parameter_value(column, name, values_hash_from_param)
      # If column is a :time (and not :date or :timestamp) there is no need to validate if
      # there are year/month/day fields
      if column.type == :time
        # if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil
        { 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value|
          values_hash_from_param[key] ||= value
        end
      else
        # else column is a timestamp, so if Date bits were not provided, error
        validate_missing_parameters!(name, [1,2,3], values_hash_from_param)

        # If Date bits were provided but blank, then return nil
        return nil if (1..3).any? { |position| values_hash_from_param[position].blank? }
      end

      max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6)
      set_values = (1..max_position).collect{ |position| values_hash_from_param[position] }
      # If Time bits are not there, then default to 0
      (3..5).each { |i| set_values[i] = set_values[i].blank? ? 0 : set_values[i] }
      instantiate_time_object(name, set_values)
    end

    def read_date_parameter_value(name, values_hash_from_param)
      return nil if (1..3).any? {|position| values_hash_from_param[position].blank?}
      set_values = [values_hash_from_param[1], values_hash_from_param[2], values_hash_from_param[3]]
      begin
        Date.new(*set_values)
      rescue ArgumentError # if Date.new raises an exception on an invalid date
        instantiate_time_object(name, set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
      end
    end

    def read_other_parameter_value(klass, name, values_hash_from_param)
      max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param)
      positions    = (1..max_position)
      validate_missing_parameters!(name, positions, values_hash_from_param)

      values = values_hash_from_param.values_at(*positions)
      klass.new(*values)
    end

    # If some position is not provided, it errors out a missing parameter exception.
    def validate_missing_parameters!(name, positions, values_hash)
      if missing_parameter = positions.detect { |position| !values_hash.key?(position) }
        raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter})")
      end
    end

    def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100)
      [values_hash_from_param.keys.max,upper_cap].min
    end

    def type_cast_attribute_value(multiparameter_name, value)
      multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value
    end

    def find_parameter_position(multiparameter_name)
      multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
    end
  end
end