diff options
Diffstat (limited to 'activemodel')
26 files changed, 275 insertions, 449 deletions
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 789cff0673..847ae7f237 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,5 +1,34 @@ ## Rails 4.0.0 (unreleased) ## +* Changed `AM::Serializers::JSON.include_root_in_json' default value to false. + Now, AM Serializers and AR objects have the same default behaviour. Fixes #6578. + + class User < ActiveRecord::Base; end + + class Person + include ActiveModel::Model + include ActiveModel::AttributeMethods + include ActiveModel::Serializers::JSON + + attr_accessor :name, :age + + def attributes + instance_values + end + end + + user.as_json + => {"id"=>1, "name"=>"Konata Izumi", "age"=>16, "awesome"=>true} + # root is not included + + person.as_json + => {"name"=>"Francesco", "age"=>22} + # root is not included + + *Francesco Rodriguez* + +* Passing false hash values to `validates` will no longer enable the corresponding validators *Steve Purcell* + * `ConfirmationValidator` error messages will attach to `:#{attribute}_confirmation` instead of `attribute` *Brian Cardarella* * Added ActiveModel::Model, a mixin to make Ruby objects work with AP out of box *Guillermo Iguaran* @@ -8,6 +37,26 @@ * Trim down Active Model API by removing `valid?` and `errors.full_messages` *José Valim* +* When `^` or `$` are used in the regular expression provided to `validates_format_of` and the :multiline option is not set to true, an exception will be raised. This is to prevent security vulnerabilities when using `validates_format_of`. The problem is described in detail in the Rails security guide. + +## Rails 3.2.6 (Jun 12, 2012) ## + +* No changes. + +## Rails 3.2.5 (Jun 1, 2012) ## + +* No changes. + + +## Rails 3.2.4 (May 31, 2012) ## + +* No changes. + + +## Rails 3.2.3 (March 30, 2012) ## + +* No changes. + ## Rails 3.2.2 (March 1, 2012) ## diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index f2d004fb0a..66f324a1a1 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -8,6 +8,7 @@ Gem::Specification.new do |s| s.description = 'A toolkit for building modeling frameworks like Active Record. Rich support for attributes, callbacks, validations, observers, serialization, internationalization, and testing.' s.required_ruby_version = '>= 1.9.3' + s.license = 'MIT' s.author = 'David Heinemeier Hansson' s.email = 'david@loudthinking.com' diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index ded1b752df..f6bacf5ec0 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -30,7 +30,6 @@ module ActiveModel autoload :AttributeMethods autoload :BlockValidator, 'active_model/validator' autoload :Callbacks - autoload :Configuration autoload :Conversion autoload :Dirty autoload :EachValidator, 'active_model/validator' diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 99918fdb96..6e93fdd625 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -61,8 +61,7 @@ module ActiveModel CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/ included do - extend ActiveModel::Configuration - config_attribute :attribute_method_matchers + class_attribute :attribute_method_matchers, instance_writer: false self.attribute_method_matchers = [ClassMethods::AttributeMethodMatcher.new] end @@ -196,7 +195,7 @@ module ActiveModel attribute_method_matchers.each do |matcher| matcher_new = matcher.method_name(new_name).to_s matcher_old = matcher.method_name(old_name).to_s - define_optimized_call self, matcher_new, matcher_old + define_proxy_call false, self, matcher_new, matcher_old end end @@ -238,7 +237,7 @@ module ActiveModel if respond_to?(generate_method, true) send(generate_method, attr_name) else - define_optimized_call generated_attribute_methods, method_name, matcher.method_missing_target, attr_name.to_s + define_proxy_call true, generated_attribute_methods, method_name, matcher.method_missing_target, attr_name.to_s end end end @@ -293,7 +292,7 @@ module ActiveModel # Define a method `name` in `mod` that dispatches to `send` # using the given `extra` args. This fallbacks `define_method` # and `send` if the given names cannot be compiled. - def define_optimized_call(mod, name, send, *extra) #:nodoc: + def define_proxy_call(include_private, mod, name, send, *extra) #:nodoc: if name =~ NAME_COMPILABLE_REGEXP defn = "def #{name}(*args)" else @@ -303,7 +302,7 @@ module ActiveModel extra = (extra.map(&:inspect) << "*args").join(", ") if send =~ CALL_COMPILABLE_REGEXP - target = "#{send}(#{extra})" + target = "#{"self." unless include_private}#{send}(#{extra})" else target = "send(:'#{send}', #{extra})" end @@ -315,7 +314,7 @@ module ActiveModel RUBY end - class AttributeMethodMatcher + class AttributeMethodMatcher #:nodoc: attr_reader :prefix, :suffix, :method_missing_target AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name) diff --git a/activemodel/lib/active_model/configuration.rb b/activemodel/lib/active_model/configuration.rb deleted file mode 100644 index ba5a6a2075..0000000000 --- a/activemodel/lib/active_model/configuration.rb +++ /dev/null @@ -1,134 +0,0 @@ -require 'active_support/concern' -require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/class/attribute_accessors' - -module ActiveModel - # This API is for Rails' internal use and is not currently considered 'public', so - # it may change in the future without warning. - # - # It creates configuration attributes that can be inherited from a module down - # to a class that includes the module. E.g. - # - # module MyModel - # extend ActiveModel::Configuration - # config_attribute :awesome - # self.awesome = true - # end - # - # class Post - # include MyModel - # end - # - # Post.awesome # => true - # - # Post.awesome = false - # Post.awesome # => false - # MyModel.awesome # => true - # - # We assume that the module will have a ClassMethods submodule containing methods - # to be transferred to the including class' singleton class. - # - # Config options can also be defined directly on a class: - # - # class Post - # extend ActiveModel::Configuration - # config_attribute :awesome - # end - # - # So this allows us to define a module that doesn't care about whether it is being - # included in a class or a module: - # - # module Awesomeness - # extend ActiveSupport::Concern - # - # included do - # extend ActiveModel::Configuration - # config_attribute :awesome - # self.awesome = true - # end - # end - # - # class Post - # include Awesomeness - # end - # - # module AwesomeModel - # include Awesomeness - # end - module Configuration #:nodoc: - def config_attribute(name, options = {}) - klass = self.is_a?(Class) ? ClassAttribute : ModuleAttribute - klass.new(self, name, options).define - end - - class Attribute - attr_reader :host, :name, :options - - def initialize(host, name, options) - @host, @name, @options = host, name, options - end - - def instance_writer? - options.fetch(:instance_writer, false) - end - end - - class ClassAttribute < Attribute - def define - if options[:global] - host.cattr_accessor name, :instance_writer => instance_writer? - else - host.class_attribute name, :instance_writer => instance_writer? - end - end - end - - class ModuleAttribute < Attribute - def class_methods - @class_methods ||= begin - if host.const_defined?(:ClassMethods, false) - host.const_get(:ClassMethods) - else - host.const_set(:ClassMethods, Module.new) - end - end - end - - def define - host.singleton_class.class_eval <<-CODE, __FILE__, __LINE__ + 1 - attr_accessor :#{name} - def #{name}?; !!#{name}; end - CODE - - name, host = self.name, self.host - - class_methods.class_eval do - define_method(name) { host.send(name) } - define_method("#{name}?") { !!send(name) } - end - - host.class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name}; defined?(@#{name}) ? @#{name} : self.class.#{name}; end - def #{name}?; !!#{name}; end - CODE - - if options[:global] - class_methods.class_eval do - define_method("#{name}=") { |val| host.send("#{name}=", val) } - end - else - class_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name}=(val) - singleton_class.class_eval do - remove_possible_method(:#{name}) - define_method(:#{name}) { val } - end - end - CODE - end - - host.send(:attr_writer, name) if instance_writer? - end - end - end -end diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 3f2fd12db7..7014d8114f 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -11,14 +11,14 @@ module ActiveModel # # The requirements for implementing ActiveModel::Dirty are: # - # * <tt>include ActiveModel::Dirty</tt> in your object + # * <tt>include ActiveModel::Dirty</tt> in your object. # * Call <tt>define_attribute_methods</tt> passing each method you want to - # track + # track. # * Call <tt>attr_name_will_change!</tt> before each change to the tracked - # attribute + # attribute. # # If you wish to also track previous changes on save or update, you need to - # add + # add: # # @previously_changed = changes # @@ -27,7 +27,6 @@ module ActiveModel # A minimal implementation could be: # # class Person - # # include ActiveModel::Dirty # # define_attribute_methods :name @@ -45,47 +44,49 @@ module ActiveModel # @previously_changed = changes # @changed_attributes.clear # end - # # end # - # == Examples: - # # A newly instantiated object is unchanged: + # # person = Person.find_by_name('Uncle Bob') # person.changed? # => false # # Change the name: + # # person.name = 'Bob' # person.changed? # => true # person.name_changed? # => true - # person.name_was # => 'Uncle Bob' - # person.name_change # => ['Uncle Bob', 'Bob'] + # person.name_was # => "Uncle Bob" + # person.name_change # => ["Uncle Bob", "Bob"] # person.name = 'Bill' - # person.name_change # => ['Uncle Bob', 'Bill'] + # person.name_change # => ["Uncle Bob", "Bill"] # # Save the changes: + # # person.save # person.changed? # => false # person.name_changed? # => false # # Assigning the same value leaves the attribute unchanged: + # # person.name = 'Bill' # person.name_changed? # => false # person.name_change # => nil # # Which attributes have changed? + # # person.name = 'Bob' - # person.changed # => ['name'] - # person.changes # => { 'name' => ['Bill', 'Bob'] } + # person.changed # => ["name"] + # person.changes # => {"name" => ["Bill", "Bob"]} # # If an attribute is modified in-place then make use of <tt>[attribute_name]_will_change!</tt> # to mark that the attribute is changing. Otherwise ActiveModel can't track changes to # in-place attributes. # # person.name_will_change! - # person.name_change # => ['Bill', 'Bill'] + # person.name_change # => ["Bill", "Bill"] # person.name << 'y' - # person.name_change # => ['Bill', 'Billy'] + # person.name_change # => ["Bill", "Billy"] module Dirty extend ActiveSupport::Concern include ActiveModel::AttributeMethods @@ -95,7 +96,8 @@ module ActiveModel attribute_method_affix :prefix => 'reset_', :suffix => '!' end - # Returns true if any attribute have unsaved changes, false otherwise. + # Returns +true+ if any attribute have unsaved changes, +false+ otherwise. + # # person.changed? # => false # person.name = 'bob' # person.changed? # => true @@ -103,32 +105,41 @@ module ActiveModel changed_attributes.present? end - # List of attributes with unsaved changes. + # Returns an array with the name of the attributes with unsaved changes. + # # person.changed # => [] # person.name = 'bob' - # person.changed # => ['name'] + # person.changed # => ["name"] def changed changed_attributes.keys end - # Map of changed attrs => [original value, new value]. + # Returns a hash of changed attributes indicating their original + # and new values like <tt>attr => [original value, new value]</tt>. + # # person.changes # => {} # person.name = 'bob' - # person.changes # => { 'name' => ['bill', 'bob'] } + # person.changes # => { "name" => ["bill", "bob"] } def changes HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }] end - # Map of attributes that were changed when the model was saved. - # person.name # => 'bob' + # Returns a hash of attributes that were changed before the model was saved. + # + # person.name # => "bob" # person.name = 'robert' # person.save - # person.previous_changes # => {'name' => ['bob, 'robert']} + # person.previous_changes # => {"name" => ["bob", "robert"]} def previous_changes @previously_changed end - # Map of change <tt>attr => original value</tt>. + # Returns a hash of the attributes with unsaved changes indicating their original + # values like <tt>attr => original value</tt>. + # + # person.name # => "bob" + # person.name = 'robert' + # person.changed_attributes # => {"name" => "bob"} def changed_attributes @changed_attributes ||= {} end diff --git a/activemodel/lib/active_model/mass_assignment_security.rb b/activemodel/lib/active_model/mass_assignment_security.rb index 893fbf92c3..8f2c0bf00a 100644 --- a/activemodel/lib/active_model/mass_assignment_security.rb +++ b/activemodel/lib/active_model/mass_assignment_security.rb @@ -9,13 +9,11 @@ module ActiveModel extend ActiveSupport::Concern included do - extend ActiveModel::Configuration + class_attribute :_accessible_attributes, instance_writer: false + class_attribute :_protected_attributes, instance_writer: false + class_attribute :_active_authorizer, instance_writer: false - config_attribute :_accessible_attributes - config_attribute :_protected_attributes - config_attribute :_active_authorizer - - config_attribute :_mass_assignment_sanitizer + class_attribute :_mass_assignment_sanitizer, instance_writer: false self.mass_assignment_sanitizer = :logger end @@ -62,7 +60,7 @@ module ActiveModel # Attributes named in this macro are protected from mass-assignment # whenever attributes are sanitized before assignment. A role for the # attributes is optional, if no role is provided then :default is used. - # A role can be defined by using the :as option. + # A role can be defined by using the :as option with a symbol or an array of symbols as the value. # # Mass-assignment to these attributes will simply be ignored, to assign # to them you can use direct writer methods. This is meant to protect @@ -128,7 +126,7 @@ module ActiveModel # # Like +attr_protected+, a role for the attributes is optional, # if no role is provided then :default is used. A role can be defined by - # using the :as option. + # using the :as option with a symbol or an array of symbols as the value. # # This is the opposite of the +attr_protected+ macro: Mass-assignment # will only set attributes in this list, to assign to the rest of diff --git a/activemodel/lib/active_model/mass_assignment_security/permission_set.rb b/activemodel/lib/active_model/mass_assignment_security/permission_set.rb index 9661349503..415ab0ad17 100644 --- a/activemodel/lib/active_model/mass_assignment_security/permission_set.rb +++ b/activemodel/lib/active_model/mass_assignment_security/permission_set.rb @@ -5,7 +5,7 @@ module ActiveModel class PermissionSet < Set def +(values) - super(values.map(&:to_s)) + super(values.compact.map(&:to_s)) end def include?(key) diff --git a/activemodel/lib/active_model/observer_array.rb b/activemodel/lib/active_model/observer_array.rb index 3d463885be..8de6918d18 100644 --- a/activemodel/lib/active_model/observer_array.rb +++ b/activemodel/lib/active_model/observer_array.rb @@ -17,6 +17,11 @@ module ActiveModel # Disables one or more observers. This supports multiple forms: # + # ORM.observers.disable :all + # # => disables all observers for all models subclassed from + # # an ORM base class that includes ActiveModel::Observing + # # e.g. ActiveRecord::Base + # # ORM.observers.disable :user_observer # # => disables the UserObserver # @@ -27,9 +32,6 @@ module ActiveModel # ORM.observers.disable :observer_1, :observer_2 # # => disables Observer1 and Observer2 for all models. # - # ORM.observers.disable :all - # # => disables all observers for all models. - # # User.observers.disable :all do # # all user observers are disabled for # # just the duration of the block @@ -40,6 +42,11 @@ module ActiveModel # Enables one or more observers. This supports multiple forms: # + # ORM.observers.enable :all + # # => enables all observers for all models subclassed from + # # an ORM base class that includes ActiveModel::Observing + # # e.g. ActiveRecord::Base + # # ORM.observers.enable :user_observer # # => enables the UserObserver # @@ -51,9 +58,6 @@ module ActiveModel # ORM.observers.enable :observer_1, :observer_2 # # => enables Observer1 and Observer2 for all models. # - # ORM.observers.enable :all - # # => enables all observers for all models. - # # User.observers.enable :all do # # all user observers are enabled for # # just the duration of the block diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index 63ab8e7edc..b4baf3a946 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -10,83 +10,82 @@ module ActiveModel included do extend ActiveModel::Naming - extend ActiveModel::Configuration - config_attribute :include_root_in_json - self.include_root_in_json = true + class_attribute :include_root_in_json + self.include_root_in_json = false end # Returns a hash representing the model. Some configuration can be # passed through +options+. # # The option <tt>include_root_in_json</tt> controls the top-level behavior - # of +as_json+. If true (the default) +as_json+ will emit a single root - # node named after the object's type. For example: + # of +as_json+. If true +as_json+ will emit a single root node named after + # the object's type. The default value for <tt>include_root_in_json</tt> + # option is +false+. # # user = User.find(1) # user.as_json - # # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true} } + # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16, + # # "created_at" => "2006/08/01", "awesome" => true} + # + # ActiveRecord::Base.include_root_in_json = true # - # ActiveRecord::Base.include_root_in_json = false # user.as_json - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true} + # # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16, + # # "created_at" => "2006/08/01", "awesome" => true } } # - # This behavior can also be achieved by setting the <tt>:root</tt> option to +false+ as in: + # This behavior can also be achieved by setting the <tt>:root</tt> option + # to +true+ as in: # # user = User.find(1) - # user.as_json(root: false) - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true} - # - # The remainder of the examples in this section assume include_root_in_json is set to - # <tt>false</tt>. + # user.as_json(root: true) + # # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16, + # # "created_at" => "2006/08/01", "awesome" => true } } # # Without any +options+, the returned Hash will include all the model's - # attributes. For example: + # attributes. # # user = User.find(1) # user.as_json - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true} + # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16, + # # "created_at" => "2006/08/01", "awesome" => true} # - # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes - # included, and work similar to the +attributes+ method. For example: + # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit + # the attributes included, and work similar to the +attributes+ method. # - # user.as_json(:only => [ :id, :name ]) - # # => {"id": 1, "name": "Konata Izumi"} + # user.as_json(only: [:id, :name]) + # # => { "id" => 1, "name" => "Konata Izumi" } # - # user.as_json(:except => [ :id, :created_at, :age ]) - # # => {"name": "Konata Izumi", "awesome": true} + # user.as_json(except: [:id, :created_at, :age]) + # # => { "name" => "Konata Izumi", "awesome" => true } # # To include the result of some method calls on the model use <tt>:methods</tt>: # - # user.as_json(:methods => :permalink) - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true, - # "permalink": "1-konata-izumi"} + # user.as_json(methods: :permalink) + # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16, + # # "created_at" => "2006/08/01", "awesome" => true, + # # "permalink" => "1-konata-izumi" } # # To include associations use <tt>:include</tt>: # - # user.as_json(:include => :posts) - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true, - # "posts": [{"id": 1, "author_id": 1, "title": "Welcome to the weblog"}, - # {"id": 2, author_id: 1, "title": "So I was thinking"}]} + # user.as_json(include: :posts) + # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16, + # # "created_at" => "2006/08/01", "awesome" => true, + # # "posts" => [ { "id" => 1, "author_id" => 1, "title" => "Welcome to the weblog" }, + # # { "id" => 2, "author_id" => 1, "title" => "So I was thinking" } ] } # # Second level and higher order associations work as well: # - # user.as_json(:include => { :posts => { - # :include => { :comments => { - # :only => :body } }, - # :only => :title } }) - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true, - # "posts": [{"comments": [{"body": "1st post!"}, {"body": "Second!"}], - # "title": "Welcome to the weblog"}, - # {"comments": [{"body": "Don't think too hard"}], - # "title": "So I was thinking"}]} + # user.as_json(include: { posts: { + # include: { comments: { + # only: :body } }, + # only: :title } }) + # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16, + # # "created_at" => "2006/08/01", "awesome" => true, + # # "posts" => [ { "comments" => [ { "body" => "1st post!" }, { "body" => "Second!" } ], + # # "title" => "Welcome to the weblog" }, + # # { "comments" => [ { "body" => "Don't think too hard" } ], + # # "title" => "So I was thinking" } ] } def as_json(options = nil) root = include_root_in_json root = options[:root] if options.try(:key?, :root) diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index b78f1ff3f3..2b3e9ce134 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -172,7 +172,7 @@ module ActiveModel # <id type="integer">1</id> # <name>David</name> # <age type="integer">16</age> - # <created-at type="datetime">2011-01-30T22:29:23Z</created-at> + # <created-at type="dateTime">2011-01-30T22:29:23Z</created-at> # </user> # # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb index 6f0ca92e2a..7a86701f73 100644 --- a/activemodel/lib/active_model/translation.rb +++ b/activemodel/lib/active_model/translation.rb @@ -42,9 +42,9 @@ module ActiveModel # Specify +options+ with additional translating options. def human_attribute_name(attribute, options = {}) options = { :count => 1 }.merge!(options) - parts = attribute.to_s.split(".", 2) + parts = attribute.to_s.split(".") attribute = parts.pop - namespace = parts.pop + namespace = parts.join("/") unless parts.empty? attributes_scope = "#{self.i18n_scope}.attributes" if namespace diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 611e9ffd55..06eebf79d9 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -52,8 +52,7 @@ module ActiveModel attr_accessor :validation_context define_callbacks :validate, :scope => :name - extend ActiveModel::Configuration - config_attribute :_validators + class_attribute :_validators self._validators = Hash.new { |h,k| h[k] = [] } end diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index 4f09679541..edd42d85f2 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -30,8 +30,7 @@ module ActiveModel # * <tt>:in</tt> - An enumerable object of items that the value shouldn't be # part of. This can be supplied as a proc or lambda which returns an # enumerable. If the enumerable is a range the test is performed with - # <tt>Range#cover?</tt> (backported in Active Support for 1.8), otherwise - # with <tt>include?</tt>. + # <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>. # * <tt>:message</tt> - Specifies a custom error message (default is: "is # reserved"). # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index dd87e312f9..ffdf842d94 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -32,11 +32,21 @@ module ActiveModel def record_error(record, attribute, name, value) record.errors.add(attribute, :invalid, options.except(name).merge!(:value => value)) end - + + def regexp_using_multiline_anchors?(regexp) + regexp.source.start_with?("^") || + (regexp.source.end_with?("$") && !regexp.source.end_with?("\\$")) + end + def check_options_validity(options, name) option = options[name] if option && !option.is_a?(Regexp) && !option.respond_to?(:call) raise ArgumentError, "A regular expression or a proc or lambda must be supplied as :#{name}" + elsif option && option.is_a?(Regexp) && + regexp_using_multiline_anchors?(option) && options[:multiline] != true + raise ArgumentError, "The provided regular expression is using multiline anchors (^ or $), " \ + "which may present a security risk. Did you mean to use \\A and \\z, or forgot to add the " \ + ":multiline => true option?" end end end @@ -47,7 +57,7 @@ module ActiveModel # attribute matches the regular expression: # # class Person < ActiveRecord::Base - # validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create + # validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :on => :create # end # # Alternatively, you can require that the specified attribute does _not_ @@ -63,12 +73,16 @@ module ActiveModel # class Person < ActiveRecord::Base # # Admin can have number as a first letter in their screen name # validates_format_of :screen_name, - # :with => lambda{ |person| person.admin? ? /\A[a-z0-9][a-z0-9_\-]*\Z/i : /\A[a-z][a-z0-9_\-]*\Z/i } + # :with => lambda{ |person| person.admin? ? /\A[a-z0-9][a-z0-9_\-]*\z/i : /\A[a-z][a-z0-9_\-]*\z/i } # end # # Note: use <tt>\A</tt> and <tt>\Z</tt> to match the start and end of the # string, <tt>^</tt> and <tt>$</tt> match the start/end of a line. # + # Due to frequent misuse of <tt>^</tt> and <tt>$</tt>, you need to pass the + # :multiline => true option in case you use any of these two anchors in the provided + # regular expression. In most cases, you should be using <tt>\A</tt> and <tt>\z</tt>. + # # You must pass either <tt>:with</tt> or <tt>:without</tt> as an option. # In addition, both must be a regular expression or a proc or lambda, or # else an exception will be raised. @@ -98,6 +112,9 @@ module ActiveModel # method, proc or string should return or evaluate to a true or false value. # * <tt>:strict</tt> - Specifies whether validation should be strict. # See <tt>ActiveModel::Validation#validates!</tt> for more information. + # * <tt>:multiline</tt> - Set to true if your regular expression contains + # anchors that match the beginning or end of lines as opposed to the + # beginning or end of the string. These anchors are <tt>^</tt> and <tt>$</tt>. def validates_format_of(*attr_names) validates_with FormatValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index ffdbed0fc1..8810f2a3c1 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -28,8 +28,8 @@ module ActiveModel # Configuration options: # * <tt>:in</tt> - An enumerable object of available items. This can be # supplied as a proc or lambda which returns an enumerable. If the enumerable - # is a range the test is performed with <tt>Range#cover?</tt> - # (backported in Active Support for 1.8), otherwise with <tt>include?</tt>. + # is a range the test is performed with <tt>Range#cover?</tt>, otherwise with + # <tt>include?</tt>. # * <tt>:message</tt> - Specifies a custom error message (default is: "is not # included in the list"). # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index d94c4e3f4f..6c13d2b4a2 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -88,6 +88,7 @@ module ActiveModel defaults.merge!(:attributes => attributes) validations.each do |key, options| + next unless options key = "#{key.to_s.camelize}Validator" begin diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb index a9db29ee21..e2f2cecc09 100644 --- a/activemodel/test/cases/attribute_methods_test.rb +++ b/activemodel/test/cases/attribute_methods_test.rb @@ -76,6 +76,19 @@ private end end +class ModelWithRubyKeywordNamedAttributes + include ActiveModel::AttributeMethods + + def attributes + { :begin => 'value of begin', :end => 'value of end' } + end + +private + def attribute(name) + attributes[name.to_sym] + end +end + class ModelWithoutAttributesMethod include ActiveModel::AttributeMethods end @@ -155,6 +168,15 @@ class AttributeMethodsTest < ActiveModel::TestCase assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.foo_bar end + test '#alias_attribute works with attributes named as a ruby keyword' do + ModelWithRubyKeywordNamedAttributes.define_attribute_methods([:begin, :end]) + ModelWithRubyKeywordNamedAttributes.alias_attribute(:from, :begin) + ModelWithRubyKeywordNamedAttributes.alias_attribute(:to, :end) + + assert_equal "value of begin", ModelWithRubyKeywordNamedAttributes.new.from + assert_equal "value of end", ModelWithRubyKeywordNamedAttributes.new.to + end + test '#undefine_attribute_methods removes attribute methods' do ModelWithAttributes.define_attribute_methods(:foo) ModelWithAttributes.undefine_attribute_methods diff --git a/activemodel/test/cases/configuration_test.rb b/activemodel/test/cases/configuration_test.rb deleted file mode 100644 index a172fa26a3..0000000000 --- a/activemodel/test/cases/configuration_test.rb +++ /dev/null @@ -1,154 +0,0 @@ -require 'cases/helper' - -class ConfigurationOnModuleTest < ActiveModel::TestCase - def setup - @mod = mod = Module.new do - extend ActiveSupport::Concern - extend ActiveModel::Configuration - - config_attribute :omg - self.omg = "default" - - config_attribute :wtf, global: true - self.wtf = "default" - - config_attribute :boolean - - config_attribute :lol, instance_writer: true - end - - @klass = Class.new do - include mod - end - - @subklass = Class.new(@klass) - end - - test "default" do - assert_equal "default", @mod.omg - assert_equal "default", @klass.omg - assert_equal "default", @klass.new.omg - end - - test "setting" do - @mod.omg = "lol" - assert_equal "lol", @mod.omg - end - - test "setting on class including the module" do - @klass.omg = "lol" - assert_equal "lol", @klass.omg - assert_equal "lol", @klass.new.omg - assert_equal "default", @mod.omg - end - - test "setting on subclass of class including the module" do - @subklass.omg = "lol" - assert_equal "lol", @subklass.omg - assert_equal "default", @klass.omg - assert_equal "default", @mod.omg - end - - test "setting on instance" do - assert !@klass.new.respond_to?(:omg=) - - @klass.lol = "lol" - obj = @klass.new - assert_equal "lol", obj.lol - obj.lol = "omg" - assert_equal "omg", obj.lol - assert_equal "lol", @klass.lol - assert_equal "lol", @klass.new.lol - obj.lol = false - assert !obj.lol? - end - - test "global attribute" do - assert_equal "default", @mod.wtf - assert_equal "default", @klass.wtf - - @mod.wtf = "wtf" - - assert_equal "wtf", @mod.wtf - assert_equal "wtf", @klass.wtf - - @klass.wtf = "lol" - - assert_equal "lol", @mod.wtf - assert_equal "lol", @klass.wtf - end - - test "boolean" do - assert_equal false, @mod.boolean? - assert_equal false, @klass.new.boolean? - @mod.boolean = true - assert_equal true, @mod.boolean? - assert_equal true, @klass.new.boolean? - end -end - -class ConfigurationOnClassTest < ActiveModel::TestCase - def setup - @klass = Class.new do - extend ActiveModel::Configuration - - config_attribute :omg - self.omg = "default" - - config_attribute :wtf, global: true - self.wtf = "default" - - config_attribute :omg2, instance_writer: true - config_attribute :wtf2, instance_writer: true, global: true - end - - @subklass = Class.new(@klass) - end - - test "defaults" do - assert_equal "default", @klass.omg - assert_equal "default", @klass.wtf - assert_equal "default", @subklass.omg - assert_equal "default", @subklass.wtf - end - - test "changing" do - @klass.omg = "lol" - assert_equal "lol", @klass.omg - assert_equal "lol", @subklass.omg - end - - test "changing in subclass" do - @subklass.omg = "lol" - assert_equal "lol", @subklass.omg - assert_equal "default", @klass.omg - end - - test "changing global" do - @klass.wtf = "wtf" - assert_equal "wtf", @klass.wtf - assert_equal "wtf", @subklass.wtf - - @subklass.wtf = "lol" - assert_equal "lol", @klass.wtf - assert_equal "lol", @subklass.wtf - end - - test "instance_writer" do - obj = @klass.new - - @klass.omg2 = "omg" - @klass.wtf2 = "wtf" - - assert_equal "omg", obj.omg2 - assert_equal "wtf", obj.wtf2 - - obj.omg2 = "lol" - obj.wtf2 = "lol" - - assert_equal "lol", obj.omg2 - assert_equal "lol", obj.wtf2 - assert_equal "omg", @klass.omg2 - assert_equal "lol", @klass.wtf2 - end -end diff --git a/activemodel/test/cases/mass_assignment_security/permission_set_test.rb b/activemodel/test/cases/mass_assignment_security/permission_set_test.rb index d005b638e4..8082c49852 100644 --- a/activemodel/test/cases/mass_assignment_security/permission_set_test.rb +++ b/activemodel/test/cases/mass_assignment_security/permission_set_test.rb @@ -13,6 +13,12 @@ class PermissionSetTest < ActiveModel::TestCase assert new_list.include?('admin'), "did not add collection to #{@permission_list.inspect}}" end + test "+ compacts added collection values" do + added_collection = [ nil ] + new_list = @permission_list + added_collection + assert_equal new_list, @permission_list, "did not add collection to #{@permission_list.inspect}}" + end + test "include? normalizes multi-parameter keys" do multi_param_key = 'admin(1)' new_list = @permission_list += [ 'admin' ] diff --git a/activemodel/test/cases/serializers/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb index 7160635eb4..e2690f1827 100644 --- a/activemodel/test/cases/serializers/json_serialization_test.rb +++ b/activemodel/test/cases/serializers/json_serialization_test.rb @@ -31,10 +31,15 @@ class JsonSerializationTest < ActiveModel::TestCase @contact.preferences = { 'shows' => 'anime' } end - test "should include root in json" do + def teardown + # set to the default value + Contact.include_root_in_json = false + end + + test "should not include root in json (class method)" do json = @contact.to_json - assert_match %r{^\{"contact":\{}, json + assert_no_match %r{^\{"contact":\{}, json assert_match %r{"name":"Konata Izumi"}, json assert_match %r{"age":16}, json assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})) @@ -42,41 +47,31 @@ class JsonSerializationTest < ActiveModel::TestCase assert_match %r{"preferences":\{"shows":"anime"\}}, json end - test "should not include root in json (class method)" do - begin - Contact.include_root_in_json = false - json = @contact.to_json - - assert_no_match %r{^\{"contact":\{}, json - assert_match %r{"name":"Konata Izumi"}, json - assert_match %r{"age":16}, json - assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})) - assert_match %r{"awesome":true}, json - assert_match %r{"preferences":\{"shows":"anime"\}}, json - ensure - Contact.include_root_in_json = true - end + test "should include root in json if include_root_in_json is true" do + Contact.include_root_in_json = true + json = @contact.to_json + + assert_match %r{^\{"contact":\{}, json + assert_match %r{"name":"Konata Izumi"}, json + assert_match %r{"age":16}, json + assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})) + assert_match %r{"awesome":true}, json + assert_match %r{"preferences":\{"shows":"anime"\}}, json end test "should include root in json (option) even if the default is set to false" do - begin - Contact.include_root_in_json = false - json = @contact.to_json(:root => true) - assert_match %r{^\{"contact":\{}, json - ensure - Contact.include_root_in_json = true - end + json = @contact.to_json(root: true) + assert_match %r{^\{"contact":\{}, json end test "should not include root in json (option)" do - - json = @contact.to_json(:root => false) + json = @contact.to_json(root: false) assert_no_match %r{^\{"contact":\{}, json end test "should include custom root in json" do - json = @contact.to_json(:root => 'json_contact') + json = @contact.to_json(root: 'json_contact') assert_match %r{^\{"json_contact":\{}, json assert_match %r{"name":"Konata Izumi"}, json @@ -107,7 +102,7 @@ class JsonSerializationTest < ActiveModel::TestCase end test "should allow attribute filtering with except" do - json = @contact.to_json(:except => [:name, :age]) + json = @contact.to_json(except: [:name, :age]) assert_no_match %r{"name":"Konata Izumi"}, json assert_no_match %r{"age":16}, json @@ -122,10 +117,10 @@ class JsonSerializationTest < ActiveModel::TestCase def @contact.favorite_quote; "Constraints are liberating"; end # Single method. - assert_match %r{"label":"Has cheezburger"}, @contact.to_json(:only => :name, :methods => :label) + assert_match %r{"label":"Has cheezburger"}, @contact.to_json(only: :name, methods: :label) # Both methods. - methods_json = @contact.to_json(:only => :name, :methods => [:label, :favorite_quote]) + methods_json = @contact.to_json(only: :name, methods: [:label, :favorite_quote]) assert_match %r{"label":"Has cheezburger"}, methods_json assert_match %r{"favorite_quote":"Constraints are liberating"}, methods_json end @@ -143,14 +138,15 @@ class JsonSerializationTest < ActiveModel::TestCase end test "serializable_hash should not modify options passed in argument" do - options = { :except => :name } + options = { except: :name } @contact.serializable_hash(options) assert_nil options[:only] assert_equal :name, options[:except] end - test "as_json should return a hash" do + test "as_json should return a hash if include_root_in_json is true" do + Contact.include_root_in_json = true json = @contact.as_json assert_kind_of Hash, json @@ -160,7 +156,7 @@ class JsonSerializationTest < ActiveModel::TestCase end end - test "from_json should set the object's attributes" do + test "from_json should work without a root (class attribute)" do json = @contact.to_json result = Contact.new.from_json(json) @@ -172,7 +168,7 @@ class JsonSerializationTest < ActiveModel::TestCase end test "from_json should work without a root (method parameter)" do - json = @contact.to_json(:root => false) + json = @contact.to_json result = Contact.new.from_json(json, false) assert_equal result.name, @contact.name @@ -182,24 +178,19 @@ class JsonSerializationTest < ActiveModel::TestCase assert_equal result.preferences, @contact.preferences end - test "from_json should work without a root (class attribute)" do - begin - Contact.include_root_in_json = false - json = @contact.to_json - result = Contact.new.from_json(json) - - assert_equal result.name, @contact.name - assert_equal result.age, @contact.age - assert_equal Time.parse(result.created_at), @contact.created_at - assert_equal result.awesome, @contact.awesome - assert_equal result.preferences, @contact.preferences - ensure - Contact.include_root_in_json = true - end + test "from_json should work with a root (method parameter)" do + json = @contact.to_json(root: :true) + result = Contact.new.from_json(json, true) + + assert_equal result.name, @contact.name + assert_equal result.age, @contact.age + assert_equal Time.parse(result.created_at), @contact.created_at + assert_equal result.awesome, @contact.awesome + assert_equal result.preferences, @contact.preferences end test "custom as_json should be honored when generating json" do - def @contact.as_json(options); { :name => name, :created_at => created_at }; end + def @contact.as_json(options); { name: name, created_at: created_at }; end json = @contact.to_json assert_match %r{"name":"Konata Izumi"}, json @@ -209,7 +200,7 @@ class JsonSerializationTest < ActiveModel::TestCase end test "custom as_json options should be extendible" do - def @contact.as_json(options = {}); super(options.merge(:only => [:name])); end + def @contact.as_json(options = {}); super(options.merge(only: [:name])); end json = @contact.to_json assert_match %r{"name":"Konata Izumi"}, json @@ -217,5 +208,4 @@ class JsonSerializationTest < ActiveModel::TestCase assert_no_match %r{"awesome":}, json assert_no_match %r{"preferences":}, json end - end diff --git a/activemodel/test/cases/serializers/xml_serialization_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb index 5fa227e0e0..7eb48abc3c 100644 --- a/activemodel/test/cases/serializers/xml_serialization_test.rb +++ b/activemodel/test/cases/serializers/xml_serialization_test.rb @@ -140,7 +140,7 @@ class XmlSerializationTest < ActiveModel::TestCase end test "should serialize datetime" do - assert_match %r{<created-at type=\"datetime\">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml + assert_match %r{<created-at type=\"dateTime\">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml end test "should serialize boolean" do diff --git a/activemodel/test/cases/translation_test.rb b/activemodel/test/cases/translation_test.rb index 4999583802..fd833cdd06 100644 --- a/activemodel/test/cases/translation_test.rb +++ b/activemodel/test/cases/translation_test.rb @@ -56,6 +56,11 @@ class ActiveModelI18nTests < ActiveModel::TestCase assert_equal 'person gender attribute', Person::Gender.human_attribute_name('attribute') end + def test_translated_deeply_nested_model_attributes + I18n.backend.store_translations 'en', :activemodel => {:attributes => {:"person/contacts/addresses" => {:street => 'Deeply Nested Address Street'}}} + assert_equal 'Deeply Nested Address Street', Person.human_attribute_name('contacts.addresses.street') + end + def test_translated_nested_model_attributes I18n.backend.store_translations 'en', :activemodel => {:attributes => {:"person/addresses" => {:street => 'Person Address Street'}}} assert_equal 'Person Address Street', Person.human_attribute_name('addresses.street') diff --git a/activemodel/test/cases/validations/format_validation_test.rb b/activemodel/test/cases/validations/format_validation_test.rb index 41a1131bcb..308a3c6cef 100644 --- a/activemodel/test/cases/validations/format_validation_test.rb +++ b/activemodel/test/cases/validations/format_validation_test.rb @@ -11,7 +11,7 @@ class PresenceValidationTest < ActiveModel::TestCase end def test_validate_format - Topic.validates_format_of(:title, :content, :with => /^Validation\smacros \w+!$/, :message => "is bad data") + Topic.validates_format_of(:title, :content, :with => /\AValidation\smacros \w+!\z/, :message => "is bad data") t = Topic.new("title" => "i'm incorrect", "content" => "Validation macros rule!") assert t.invalid?, "Shouldn't be valid" @@ -27,7 +27,7 @@ class PresenceValidationTest < ActiveModel::TestCase end def test_validate_format_with_allow_blank - Topic.validates_format_of(:title, :with => /^Validation\smacros \w+!$/, :allow_blank => true) + Topic.validates_format_of(:title, :with => /\AValidation\smacros \w+!\z/, :allow_blank => true) assert Topic.new("title" => "Shouldn't be valid").invalid? assert Topic.new("title" => "").valid? assert Topic.new("title" => nil).valid? @@ -36,7 +36,7 @@ class PresenceValidationTest < ActiveModel::TestCase # testing ticket #3142 def test_validate_format_numeric - Topic.validates_format_of(:title, :content, :with => /^[1-9][0-9]*$/, :message => "is bad data") + Topic.validates_format_of(:title, :content, :with => /\A[1-9][0-9]*\z/, :message => "is bad data") t = Topic.new("title" => "72x", "content" => "6789") assert t.invalid?, "Shouldn't be valid" @@ -63,11 +63,21 @@ class PresenceValidationTest < ActiveModel::TestCase end def test_validate_format_with_formatted_message - Topic.validates_format_of(:title, :with => /^Valid Title$/, :message => "can't be %{value}") + Topic.validates_format_of(:title, :with => /\AValid Title\z/, :message => "can't be %{value}") t = Topic.new(:title => 'Invalid title') assert t.invalid? assert_equal ["can't be Invalid title"], t.errors[:title] end + + def test_validate_format_of_with_multiline_regexp_should_raise_error + assert_raise(ArgumentError) { Topic.validates_format_of(:title, :with => /^Valid Title$/) } + end + + def test_validate_format_of_with_multiline_regexp_and_option + assert_nothing_raised(ArgumentError) do + Topic.validates_format_of(:title, :with => /^Valid Title$/, :multiline => true) + end + end def test_validate_format_with_not_option Topic.validates_format_of(:title, :without => /foo/, :message => "should not contain foo") diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb index 6b6aad3bd1..bb751cf9c5 100644 --- a/activemodel/test/cases/validations/i18n_validation_test.rb +++ b/activemodel/test/cases/validations/i18n_validation_test.rb @@ -141,7 +141,7 @@ class I18nValidationTest < ActiveModel::TestCase COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_format_of on generated message #{name}" do - Person.validates_format_of :title, validation_options.merge(:with => /^[1-9][0-9]*$/) + Person.validates_format_of :title, validation_options.merge(:with => /\A[1-9][0-9]*\z/) @person.title = '72x' @person.errors.expects(:generate_message).with(:title, :invalid, generate_message_options.merge(:value => '72x')) @person.valid? @@ -291,7 +291,7 @@ class I18nValidationTest < ActiveModel::TestCase # validates_format_of w/o mocha set_expectations_for_validation "validates_format_of", :invalid do |person, options_to_merge| - Person.validates_format_of :title, options_to_merge.merge(:with => /^[1-9][0-9]*$/) + Person.validates_format_of :title, options_to_merge.merge(:with => /\A[1-9][0-9]*\z/) end # validates_inclusion_of w/o mocha diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index 1f5023bf76..8ea9745fbf 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -330,6 +330,11 @@ class ValidationsTest < ActiveModel::TestCase end end + def test_validates_with_false_hash_value + Topic.validates :title, :presence => false + assert Topic.new.valid? + end + def test_strict_validation_error_message Topic.validates :title, :strict => true, :presence => true |