From c5ec16e5833a4284a5ef07c7b09306df01e964f2 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Sat, 3 Jun 2006 22:15:06 +0000 Subject: Added simple hash conditions to find that'll just convert hash to an AND-based condition string (closes #5143) [hcatlin@gmail.com] git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4425 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- activerecord/CHANGELOG | 11 ++++++++ activerecord/lib/active_record/base.rb | 48 +++++++++++++++++++++++++++------- activerecord/test/finder_test.rb | 33 +++++++++++++++++++++-- 3 files changed, 81 insertions(+), 11 deletions(-) (limited to 'activerecord') diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index a04b81144e..71f3252f51 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,16 @@ *SVN* +* Added simple hash conditions to find that'll just convert hash to an AND-based condition string #5143 [hcatlin@gmail.com]. Example: + + Person.find(:all, :conditions => { :last_name => "Catlin", :status => 1 }, :limit => 2) + +...is the same as: + + Person.find(:all, :conditions => [ "last_name = ? and status = ?", "Catlin", 1 ], :limit => 2) + + This makes it easier to pass in the options from a form or otherwise outside. + + * Fixed issues with BLOB limits, charsets, and booleans for Firebird #5194, #5191, #5189 [kennethkunz@gmail.com] * Fixed usage of :limit and with_scope when the association in scope is a 1:m #5208 [alex@purefiction.net] diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 1583c28efe..6cd4b506da 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -81,9 +81,10 @@ module ActiveRecord #:nodoc: # # == Conditions # - # Conditions can either be specified as a string or an array representing the WHERE-part of an SQL statement. + # Conditions can either be specified as a string, array, or hash representing the WHERE-part of an SQL statement. # The array form is to be used when the condition input is tainted and requires sanitization. The string form can - # be used for statements that don't involve tainted data. Examples: + # be used for statements that don't involve tainted data. The hash form works much like the array form, except + # only equality is possible. Examples: # # class User < ActiveRecord::Base # def self.authenticate_unsafely(user_name, password) @@ -93,12 +94,16 @@ module ActiveRecord #:nodoc: # def self.authenticate_safely(user_name, password) # find(:first, :conditions => [ "user_name = ? AND password = ?", user_name, password ]) # end + # + # def self.authenticate_safely_simply(user_name, password) + # find(:first, :conditions => { :user_name => user_name, :password => password }) + # end # end # # The authenticate_unsafely method inserts the parameters directly into the query and is thus susceptible to SQL-injection - # attacks if the user_name and +password+ parameters come directly from a HTTP request. The authenticate_safely method, - # on the other hand, will sanitize the user_name and +password+ before inserting them in the query, which will ensure that - # an attacker can't escape the query and fake the login (or worse). + # attacks if the user_name and +password+ parameters come directly from a HTTP request. The authenticate_safely and + # authenticate_safely_simply both will sanitize the user_name and +password+ before inserting them in the query, + # which will ensure that an attacker can't escape the query and fake the login (or worse). # # When using multiple parameters in the conditions, it can easily become hard to read exactly what the fourth or fifth # question mark is supposed to represent. In those cases, you can resort to named bind variables instead. That's done by replacing @@ -109,6 +114,13 @@ module ActiveRecord #:nodoc: # { :id => 3, :name => "37signals", :division => "First", :accounting_date => '2005-01-01' } # ]) # + # Similarly, a simple hash without a statement will generate conditions based on equality with the SQL AND + # operator. For instance: + # + # Student.find(:all, :conditions => { :first_name => "Harvey", :status => 1 }) + # Student.find(:all, :conditions => params[:student]) + # + # # == Overwriting default accessors # # All column values are automatically available through basic accessors on the Active Record object, but some times you @@ -1273,12 +1285,30 @@ module ActiveRecord #:nodoc: klass.base_class.name end - # Accepts an array or string. The string is returned untouched, but the array has each value + #Accepts an array, hash, or string of sql conditions and + #deals with them accordingly + # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'" + # { :name => "foo'bar", :group_id => 4 } returns "name='foo''bar' and group_id='4'" + # "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'" + def sanitize_sql(condition) + return sanitize_sql_array(condition) if condition.is_a?(Array) + return sanitize_sql_hash(condition) if condition.is_a?(Hash) + condition + end + + # Accepts a hash of conditions. The hash has each key/value or attribute/value pair + # sanitized and interpolated into the sql statement. + # { :name => "foo'bar", :group_id => 4 } returns "name='foo''bar' and group_id= 4" + def sanitize_sql_hash(hash) + hash.collect { |attrib, value| + "#{table_name}.#{connection.quote_column_name(attrib)} = #{quote(value)}" + }.join(" AND ") + end + + # Accepts an array of conditions. The array has each value # sanitized and interpolated into the sql statement. # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'" - def sanitize_sql(ary) - return ary unless ary.is_a?(Array) - + def sanitize_sql_array(ary) statement, *values = ary if values.first.is_a?(Hash) and statement =~ /:\w+/ replace_named_bind_variables(statement, values.first) diff --git a/activerecord/test/finder_test.rb b/activerecord/test/finder_test.rb index 1cd16c7cd8..20898237d1 100644 --- a/activerecord/test/finder_test.rb +++ b/activerecord/test/finder_test.rb @@ -117,17 +117,46 @@ class FinderTest < Test::Unit::TestCase assert topic.respond_to?("author_name") end - def test_find_on_conditions + def test_find_on_array_conditions assert Topic.find(1, :conditions => ["approved = ?", false]) assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => ["approved = ?", true]) } end - def test_condition_interpolation + def test_find_on_hash_conditions + assert Topic.find(1, :conditions => { :approved => false }) + assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :approved => true }) } + end + + def test_find_on_multiple_hash_conditions + assert Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => false }) + assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) } + assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "HHC", :replies_count => 1, :approved => false }) } + assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) } + end + + def test_condition_array_interpolation assert_kind_of Firm, Company.find(:first, :conditions => ["name = '%s'", "37signals"]) assert_nil Company.find(:first, :conditions => ["name = '%s'", "37signals!"]) assert_nil Company.find(:first, :conditions => ["name = '%s'", "37signals!' OR 1=1"]) assert_kind_of Time, Topic.find(:first, :conditions => ["id = %d", 1]).written_on end + + def test_condition_hash_interpolation + assert_kind_of Firm, Company.find(:first, :conditions => { :name => "37signals"}) + assert_nil Company.find(:first, :conditions => { :name => "37signals!"}) + assert_kind_of Time, Topic.find(:first, :conditions => {:id => 1}).written_on + end + + def test_hash_condition_find_malformed + assert_raises(ActiveRecord::StatementInvalid) { + Company.find(:first, :conditions => { :id => 2, :dhh => true }) + } + end + + def test_hash_condition_find_with_escaped_characters + Company.create("name" => "Ain't noth'n like' \#stuff") + assert Company.find(:first, :conditions => { :name => "Ain't noth'n like' \#stuff"}) + end def test_bind_variables assert_kind_of Firm, Company.find(:first, :conditions => ["name = ?", "37signals"]) -- cgit v1.2.3