searchlogic 1.5.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (125) hide show
  1. data/CHANGELOG.rdoc +228 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Manifest +123 -0
  4. data/README.rdoc +383 -0
  5. data/Rakefile +15 -0
  6. data/TODO.rdoc +6 -0
  7. data/examples/README.rdoc +4 -0
  8. data/init.rb +1 -0
  9. data/lib/searchlogic.rb +89 -0
  10. data/lib/searchlogic/active_record/associations.rb +52 -0
  11. data/lib/searchlogic/active_record/base.rb +218 -0
  12. data/lib/searchlogic/active_record/connection_adapters/mysql_adapter.rb +172 -0
  13. data/lib/searchlogic/active_record/connection_adapters/postgresql_adapter.rb +168 -0
  14. data/lib/searchlogic/active_record/connection_adapters/sqlite_adapter.rb +75 -0
  15. data/lib/searchlogic/condition/base.rb +159 -0
  16. data/lib/searchlogic/condition/begins_with.rb +17 -0
  17. data/lib/searchlogic/condition/blank.rb +21 -0
  18. data/lib/searchlogic/condition/child_of.rb +11 -0
  19. data/lib/searchlogic/condition/descendant_of.rb +24 -0
  20. data/lib/searchlogic/condition/ends_with.rb +17 -0
  21. data/lib/searchlogic/condition/equals.rb +27 -0
  22. data/lib/searchlogic/condition/greater_than.rb +15 -0
  23. data/lib/searchlogic/condition/greater_than_or_equal_to.rb +15 -0
  24. data/lib/searchlogic/condition/inclusive_descendant_of.rb +11 -0
  25. data/lib/searchlogic/condition/keywords.rb +47 -0
  26. data/lib/searchlogic/condition/less_than.rb +15 -0
  27. data/lib/searchlogic/condition/less_than_or_equal_to.rb +15 -0
  28. data/lib/searchlogic/condition/like.rb +15 -0
  29. data/lib/searchlogic/condition/nil.rb +21 -0
  30. data/lib/searchlogic/condition/not_begin_with.rb +20 -0
  31. data/lib/searchlogic/condition/not_blank.rb +19 -0
  32. data/lib/searchlogic/condition/not_end_with.rb +20 -0
  33. data/lib/searchlogic/condition/not_equal.rb +26 -0
  34. data/lib/searchlogic/condition/not_have_keywords.rb +20 -0
  35. data/lib/searchlogic/condition/not_like.rb +20 -0
  36. data/lib/searchlogic/condition/not_nil.rb +19 -0
  37. data/lib/searchlogic/condition/sibling_of.rb +14 -0
  38. data/lib/searchlogic/condition/tree.rb +17 -0
  39. data/lib/searchlogic/conditions/base.rb +484 -0
  40. data/lib/searchlogic/conditions/protection.rb +36 -0
  41. data/lib/searchlogic/config.rb +31 -0
  42. data/lib/searchlogic/config/helpers.rb +289 -0
  43. data/lib/searchlogic/config/search.rb +53 -0
  44. data/lib/searchlogic/core_ext/hash.rb +75 -0
  45. data/lib/searchlogic/helpers/control_types/link.rb +310 -0
  46. data/lib/searchlogic/helpers/control_types/links.rb +241 -0
  47. data/lib/searchlogic/helpers/control_types/remote_link.rb +87 -0
  48. data/lib/searchlogic/helpers/control_types/remote_links.rb +72 -0
  49. data/lib/searchlogic/helpers/control_types/remote_select.rb +36 -0
  50. data/lib/searchlogic/helpers/control_types/select.rb +82 -0
  51. data/lib/searchlogic/helpers/form.rb +208 -0
  52. data/lib/searchlogic/helpers/utilities.rb +197 -0
  53. data/lib/searchlogic/modifiers/absolute.rb +15 -0
  54. data/lib/searchlogic/modifiers/acos.rb +11 -0
  55. data/lib/searchlogic/modifiers/asin.rb +11 -0
  56. data/lib/searchlogic/modifiers/atan.rb +11 -0
  57. data/lib/searchlogic/modifiers/base.rb +27 -0
  58. data/lib/searchlogic/modifiers/ceil.rb +15 -0
  59. data/lib/searchlogic/modifiers/char_length.rb +15 -0
  60. data/lib/searchlogic/modifiers/cos.rb +15 -0
  61. data/lib/searchlogic/modifiers/cot.rb +15 -0
  62. data/lib/searchlogic/modifiers/day_of_month.rb +15 -0
  63. data/lib/searchlogic/modifiers/day_of_week.rb +15 -0
  64. data/lib/searchlogic/modifiers/day_of_year.rb +15 -0
  65. data/lib/searchlogic/modifiers/degrees.rb +11 -0
  66. data/lib/searchlogic/modifiers/exp.rb +15 -0
  67. data/lib/searchlogic/modifiers/floor.rb +15 -0
  68. data/lib/searchlogic/modifiers/hex.rb +11 -0
  69. data/lib/searchlogic/modifiers/hour.rb +11 -0
  70. data/lib/searchlogic/modifiers/log.rb +15 -0
  71. data/lib/searchlogic/modifiers/log10.rb +11 -0
  72. data/lib/searchlogic/modifiers/log2.rb +11 -0
  73. data/lib/searchlogic/modifiers/lower.rb +15 -0
  74. data/lib/searchlogic/modifiers/ltrim.rb +15 -0
  75. data/lib/searchlogic/modifiers/md5.rb +11 -0
  76. data/lib/searchlogic/modifiers/microseconds.rb +11 -0
  77. data/lib/searchlogic/modifiers/milliseconds.rb +11 -0
  78. data/lib/searchlogic/modifiers/minute.rb +15 -0
  79. data/lib/searchlogic/modifiers/month.rb +15 -0
  80. data/lib/searchlogic/modifiers/octal.rb +15 -0
  81. data/lib/searchlogic/modifiers/radians.rb +11 -0
  82. data/lib/searchlogic/modifiers/round.rb +11 -0
  83. data/lib/searchlogic/modifiers/rtrim.rb +15 -0
  84. data/lib/searchlogic/modifiers/second.rb +15 -0
  85. data/lib/searchlogic/modifiers/sign.rb +11 -0
  86. data/lib/searchlogic/modifiers/sin.rb +11 -0
  87. data/lib/searchlogic/modifiers/square_root.rb +15 -0
  88. data/lib/searchlogic/modifiers/tan.rb +15 -0
  89. data/lib/searchlogic/modifiers/trim.rb +15 -0
  90. data/lib/searchlogic/modifiers/upper.rb +15 -0
  91. data/lib/searchlogic/modifiers/week.rb +11 -0
  92. data/lib/searchlogic/modifiers/year.rb +11 -0
  93. data/lib/searchlogic/search/base.rb +148 -0
  94. data/lib/searchlogic/search/conditions.rb +53 -0
  95. data/lib/searchlogic/search/ordering.rb +244 -0
  96. data/lib/searchlogic/search/pagination.rb +121 -0
  97. data/lib/searchlogic/search/protection.rb +89 -0
  98. data/lib/searchlogic/search/searching.rb +31 -0
  99. data/lib/searchlogic/shared/utilities.rb +50 -0
  100. data/lib/searchlogic/shared/virtual_classes.rb +39 -0
  101. data/lib/searchlogic/version.rb +79 -0
  102. data/searchlogic.gemspec +39 -0
  103. data/test/fixtures/accounts.yml +15 -0
  104. data/test/fixtures/cats.yml +3 -0
  105. data/test/fixtures/dogs.yml +3 -0
  106. data/test/fixtures/orders.yml +14 -0
  107. data/test/fixtures/user_groups.yml +13 -0
  108. data/test/fixtures/users.yml +36 -0
  109. data/test/test_active_record_associations.rb +81 -0
  110. data/test/test_active_record_base.rb +93 -0
  111. data/test/test_condition_base.rb +52 -0
  112. data/test/test_condition_types.rb +143 -0
  113. data/test/test_conditions_base.rb +242 -0
  114. data/test/test_conditions_protection.rb +16 -0
  115. data/test/test_config.rb +23 -0
  116. data/test/test_helper.rb +134 -0
  117. data/test/test_search_base.rb +227 -0
  118. data/test/test_search_conditions.rb +19 -0
  119. data/test/test_search_ordering.rb +165 -0
  120. data/test/test_search_pagination.rb +72 -0
  121. data/test/test_search_protection.rb +24 -0
  122. data/test_libs/acts_as_tree.rb +98 -0
  123. data/test_libs/ordered_hash.rb +9 -0
  124. data/test_libs/rexml_fix.rb +14 -0
  125. metadata +317 -0
@@ -0,0 +1,53 @@
1
+ module Searchlogic
2
+ module Search
3
+ # = Searchlogic Conditions
4
+ #
5
+ # Implements all of the conditions functionality into a searchlogic search. All of this functonality is extracted out into its own class Searchlogic::Conditions::Base. This is a separate module to help keep the code
6
+ # clean and organized.
7
+ module Conditions
8
+ def self.included(klass)
9
+ klass.class_eval do
10
+ alias_method_chain :initialize, :conditions
11
+ alias_method_chain :conditions=, :conditions
12
+ alias_method_chain :sanitize, :conditions
13
+ end
14
+ end
15
+
16
+ def initialize_with_conditions(init_options = {})
17
+ self.conditions = Searchlogic::Conditions::Base.create_virtual_class(klass).new
18
+ initialize_without_conditions(init_options)
19
+ end
20
+
21
+ # Sets conditions on the search. Accepts a hash or a Searchlogic::Conditions::Base object.
22
+ #
23
+ # === Examples
24
+ #
25
+ # search.conditions = {:first_name_like => "Ben"}
26
+ # search.conditions = User.new_conditions
27
+ #
28
+ # or to set a scope
29
+ #
30
+ # search.conditions = "user_group_id = 6"
31
+ #
32
+ # now you can create the rest of your search and your "scope" will get merged into your final SQL.
33
+ # What this does is determine if the value a hash or a conditions object, if not it sets it up as a scope.
34
+ def conditions_with_conditions=(values)
35
+ case values
36
+ when Searchlogic::Conditions::Base
37
+ @conditions = values
38
+ else
39
+ @conditions.conditions = values
40
+ end
41
+ end
42
+
43
+ def sanitize_with_conditions(searching = true) # :nodoc:
44
+ find_options = sanitize_without_conditions(searching)
45
+ if conditions_obj = find_options.delete(:conditions)
46
+ new_conditions = conditions_obj.sanitize
47
+ find_options[:conditions] = new_conditions unless new_conditions.blank?
48
+ end
49
+ find_options
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,244 @@
1
+ module Searchlogic
2
+ module Search
3
+ # = Search Ordering
4
+ #
5
+ # The purpose of this module is to provide easy ordering for your searches. All that these options do is
6
+ # build :order for you. This plays a huge part in ordering your data on the interface. See the options and examples below. The readme also touches on ordering. It's pretty simple thought:
7
+ #
8
+ # === Examples
9
+ #
10
+ # search.order_by = :id
11
+ # search.order_by = [:id, :first_name]
12
+ # search.order_by = {:user_group => :name}
13
+ # search.order_by = [:id, {:user_group => :name}]
14
+ # search.order_by = {:user_group => {:account => :name}} # you can traverse through all of your relationships
15
+ #
16
+ # search.order_as = "DESC"
17
+ # search.order_as = "ASC"
18
+ module Ordering
19
+ def self.included(klass)
20
+ klass.class_eval do
21
+ alias_method_chain :order=, :ordering
22
+ alias_method_chain :sanitize, :ordering
23
+ attr_reader :priority_order
24
+ end
25
+ end
26
+
27
+ def order_with_ordering=(value) # :nodoc
28
+ @order_by = nil
29
+ @order_as = nil
30
+ @order_by_auto_joins = nil
31
+ self.order_without_ordering = value
32
+ end
33
+
34
+ # Convenience method for determining if the ordering is ascending
35
+ def asc?
36
+ !desc?
37
+ end
38
+
39
+ # Convenience method for determining if the ordering is descending
40
+ def desc?
41
+ return false if order_as.nil?
42
+ order_as == "DESC"
43
+ end
44
+
45
+ # Determines how the search is being ordered: as DESC or ASC
46
+ def order_as
47
+ return if order.blank?
48
+ return @order_as if @order_as
49
+
50
+ case order
51
+ when /ASC$/i
52
+ @order_as = "ASC"
53
+ when /DESC$/i
54
+ @order_as = "DESC"
55
+ else
56
+ nil
57
+ end
58
+ end
59
+
60
+ # Sets how the results will be ordered: ASC or DESC
61
+ def order_as=(value)
62
+ value = value.blank? ? nil : value.to_s.upcase
63
+ raise(ArgumentError, "order_as only accepts a blank string / nil or a string as 'ASC' or 'DESC'") if !value.blank? && !["ASC", "DESC"].include?(value)
64
+ if @order_by
65
+ @order = order_by_to_order(@order_by, value)
66
+ elsif order
67
+ @order.gsub!(/(ASC|DESC)/i, value)
68
+ end
69
+ @order_as = value
70
+ end
71
+
72
+ # Determines by what columns the search is being ordered. This is nifty in that is reverse engineers the order SQL to determine this, only
73
+ # if you haven't explicitly set the order_by option yourself.
74
+ def order_by
75
+ return if order.blank?
76
+ @order_by ||= order_to_order_by(order)
77
+ end
78
+
79
+ # Lets you set how to order the data
80
+ #
81
+ # === Examples
82
+ #
83
+ # In these examples "ASC" is determined by the value of order_as
84
+ #
85
+ # order_by = :id # => users.id ASC
86
+ # order_by = [:id, name] # => users.id ASC, user.name ASC
87
+ # order_by = [:id, {:user_group => :name}] # => users.id ASC, user_groups.name ASC
88
+ def order_by=(value)
89
+ @order_by_auto_joins = nil
90
+ @order_by = get_order_by_value(value)
91
+ @order = order_by_to_order(@order_by, @order_as)
92
+ @order_by
93
+ end
94
+
95
+ # Returns the joins neccessary for the "order" statement so that we don't get an SQL error
96
+ def order_by_auto_joins
97
+ @order_by_auto_joins ||= build_order_by_auto_joins(order_by)
98
+ end
99
+
100
+ # Let's you set a priority order. Meaning this will get ordered first before anything else, but is unnoticeable and abstracted out from your regular order. For example, lets say you have a model called Product
101
+ # that had a "featured" boolean column. You want to order the products by the price, quantity, etc., but you want the featured products to always be first.
102
+ #
103
+ # Without a priority order your controller would get cluttered and your code would be much more complicated. All of your order_by_link methods would have to be order_by_link [:featured, :price], :text => "Price"
104
+ # Your order_by_link methods alternate between ASC and DESC, so the featured products would jump from the top the bottom. It presents a lot of "work arounds". So priority_order solves this.
105
+ def priority_order=(value)
106
+ @priority_order = value
107
+ end
108
+
109
+ # Same as order_by but for your priority order. See priority_order= for more informaton on priority_order.
110
+ def priority_order_by
111
+ return if priority_order.blank?
112
+ @priority_order_by ||= order_to_order_by(priority_order)
113
+ end
114
+
115
+ # Same as order_by= but for your priority order. See priority_order= for more informaton on priority_order.
116
+ def priority_order_by=(value)
117
+ @priority_order_by_auto_joins = nil
118
+ @priority_order_by = get_order_by_value(value)
119
+ @priority_order = order_by_to_order(@priority_order_by, @priority_order_as)
120
+ @priority_order_by
121
+ end
122
+
123
+ # Same as order_as but for your priority order. See priority_order= for more informaton on priority_order.
124
+ def priority_order_as
125
+ return if priority_order.blank?
126
+ return @priority_order_as if @priority_order_as
127
+
128
+ case priority_order
129
+ when /ASC$/i
130
+ @priority_order_as = "ASC"
131
+ when /DESC$/i
132
+ @priority_order_as = "DESC"
133
+ else
134
+ nil
135
+ end
136
+ end
137
+
138
+ # Same as order_as= but for your priority order. See priority_order= for more informaton on priority_order.
139
+ def priority_order_as=(value)
140
+ value = value.blank? ? nil : value.to_s.upcase
141
+ raise(ArgumentError, "priority_order_as only accepts a blank string / nil or a string as 'ASC' or 'DESC'") if !value.blank? && !["ASC", "DESC"].include?(value)
142
+ if @priority_order_by
143
+ @priority_order = order_by_to_order(@priority_order_by, value)
144
+ elsif priority_order
145
+ @priority_order.gsub!(/(ASC|DESC)/i, value)
146
+ end
147
+ @priority_order_as = value
148
+ end
149
+
150
+ def priority_order_by_auto_joins
151
+ @priority_order_by_auto_joins ||= build_order_by_auto_joins(priority_order_by)
152
+ end
153
+
154
+ def sanitize_with_ordering(searching = true)
155
+ find_options = sanitize_without_ordering(searching)
156
+ unless priority_order.blank?
157
+ order_parts = [priority_order, find_options[:order]].compact
158
+ find_options[:order] = order_parts.join(", ")
159
+ end
160
+ find_options
161
+ end
162
+
163
+ private
164
+ def order_by_to_order(order_by, order_as, alt_klass = nil)
165
+ return if order_by.blank?
166
+
167
+ k = alt_klass || klass
168
+ table_name = k.table_name
169
+ sql_parts = []
170
+
171
+ case order_by
172
+ when Array
173
+ order_by.each { |part| sql_parts << order_by_to_order(part, order_as, alt_klass) }
174
+ when Hash
175
+ raise(ArgumentError, "when passing a hash to order_by you must only have 1 key: {:user_group => :name} not {:user_group => :name, :user_group => :id}. The latter should be [{:user_group => :name}, {:user_group => :id}]") if order_by.keys.size != 1
176
+ key = order_by.keys.first
177
+ reflection = k.reflect_on_association(key.to_sym)
178
+ value = order_by.values.first
179
+ sql_parts << order_by_to_order(value, order_as, reflection.klass)
180
+ when Symbol, String
181
+ part = "#{quote_table_name(table_name)}.#{quote_column_name(order_by)}"
182
+ part += " #{order_as}" unless order_as.blank?
183
+ sql_parts << part
184
+ end
185
+
186
+ sql_parts.join(", ")
187
+ end
188
+
189
+ def order_to_order_by(order)
190
+ # Reversege engineer order, only go 1 level deep with relationships, anything beyond that is probably excessive and not good for performance
191
+ order_parts = order.split(",").collect do |part|
192
+ part.strip!
193
+ part.gsub!(/ (ASC|DESC)$/i, "")
194
+ part.gsub!(/(.*)\./, "")
195
+ table_name = ($1 ? $1.gsub(/[^a-z0-9_]/i, "") : nil)
196
+ part.gsub!(/[^a-z0-9_]/i, "")
197
+ reflection = nil
198
+ if table_name && table_name != klass.table_name
199
+ reflection = klass.reflect_on_association(table_name.to_sym) || klass.reflect_on_association(table_name.singularize.to_sym)
200
+ next unless reflection
201
+ {reflection.name.to_s => part}
202
+ else
203
+ part
204
+ end
205
+ end.compact
206
+ order_parts.size <= 1 ? order_parts.first : order_parts
207
+ end
208
+
209
+ def build_order_by_auto_joins(order_by_value)
210
+ case order_by_value
211
+ when Array
212
+ order_by_value.collect { |value| build_order_by_auto_joins(value) }.uniq.compact
213
+ when Hash
214
+ key = order_by_value.keys.first
215
+ value = order_by_value.values.first
216
+ case value
217
+ when Hash
218
+ {key.to_sym => build_order_by_auto_joins(value)}
219
+ else
220
+ key.to_sym
221
+ end
222
+ else
223
+ nil
224
+ end
225
+ end
226
+
227
+ def get_order_by_value(value)
228
+ Marshal.load(value.unpack("m").first) rescue value
229
+ end
230
+
231
+ def quote_column_name(column_name)
232
+ klass_connection.quote_column_name(column_name)
233
+ end
234
+
235
+ def quote_table_name(table_name)
236
+ klass_connection.quote_table_name(table_name)
237
+ end
238
+
239
+ def klass_connection
240
+ @connection ||= klass.connection
241
+ end
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,121 @@
1
+ module Searchlogic
2
+ module Search
3
+ # = Searchlogic Pagination
4
+ #
5
+ # Adds in pagination functionality to searchlogic
6
+ module Pagination
7
+ def self.included(klass)
8
+ klass.class_eval do
9
+ alias_method_chain :limit=, :pagination
10
+ alias_method_chain :offset=, :pagination
11
+ alias_method :per_page, :limit
12
+ alias_method :per_page=, :limit=
13
+ end
14
+ end
15
+
16
+ def limit_with_pagination=(value) # :nodoc:
17
+ r_value = self.limit_without_pagination = value
18
+ @page_count = nil
19
+ if @set_page
20
+ self.page = (@queued_page || @page) # retry setting page
21
+ else
22
+ @page = nil # the memoized page is invalid, so reset it
23
+ end
24
+ r_value
25
+ end
26
+
27
+ def offset_with_pagination=(value) #:nodoc
28
+ r_value = self.offset_without_pagination = value
29
+ @set_page = @queued_page = @page = nil
30
+ r_value
31
+ end
32
+
33
+ # The current page that the search is on
34
+ def page
35
+ @page ||= (offset.blank? || limit.blank?) ? 1 : (offset.to_f / limit).floor + 1
36
+ end
37
+ alias_method :current_page, :page
38
+
39
+ # Lets you change the page for the next search
40
+ def page=(value)
41
+ @set_page = true
42
+
43
+ if value.blank?
44
+ value = nil
45
+ @page = value
46
+ return @offset = @page
47
+ end
48
+
49
+ v = value.to_i
50
+
51
+ if limit.blank?
52
+ @queued_page = v
53
+ @page = 1
54
+ @offset = nil
55
+ else
56
+ @queued_page = nil
57
+ @page = v
58
+ v -= 1 unless v == 0
59
+ @offset = v * limit
60
+ end
61
+ value
62
+ end
63
+
64
+ # The total number of pages in your next search
65
+ def page_count
66
+ @page_count ||= (per_page.blank? || per_page <= 0) ? 1 : (count / per_page.to_f).ceil
67
+ end
68
+ alias_method :page_total, :page_count
69
+
70
+ # Always returns 1, this is a convenience method
71
+ def first_page
72
+ 1
73
+ end
74
+
75
+ # Changes the page to 1 and then runs the "all" search. What's different about this method is that it does not raise an exception if you are on the first page. Unlike prev_page! and next_page!
76
+ # I don't think an exception raised is warranted, because you are expecting the same results each time it is ran.
77
+ def first_page!
78
+ self.page = first_page
79
+ all
80
+ end
81
+
82
+ # Changes the page to the page - 1
83
+ def prev_page
84
+ self.page - 1
85
+ end
86
+
87
+ # Changes the page to page - 1 and runs the "all" search. Be careful with this method because if you are on the first page an exception is raised telling you that you are on the first page.
88
+ # I thought about just running the first page search again, but that seems confusing and unexpected.
89
+ def prev_page!
90
+ raise("You are on the first page") if page == first_page
91
+ self.page = prev_page
92
+ all
93
+ end
94
+
95
+ # Change the page to page + 1
96
+ def next_page
97
+ self.page + 1
98
+ end
99
+
100
+ # Changes the page to page + 1 and calls the "all" method. Be careful with this method because if you are on the last page an exception is raised telling you that you are on the last page.
101
+ # I thought about just running the lat page search again, but that seems confusing and unexpected.
102
+ def next_page!
103
+ raise("You are on the last page") if page == last_page
104
+ self.page = next_page
105
+ all
106
+ end
107
+
108
+ # Always returns the page_count, this is a convenience method
109
+ def last_page
110
+ page_count
111
+ end
112
+
113
+ # Changes the page to the last page and runs the "all" search. What's different about this method is that it does not raise an exception if you are on the last page. Unlike prev_page! and next_page!
114
+ # I don't think an exception raised is warranted, because you are expecting the same results each time it is ran.
115
+ def last_page!
116
+ self.page = last_page
117
+ all
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,89 @@
1
+ module Searchlogic
2
+ module Search
3
+ # = Searchlogic Protection
4
+ #
5
+ # This adds protection during mass asignments *only*. This allows you to pass a params object when doing mass assignments and not have to worry about Billy 13 year old adding in SQL injections.
6
+ # There is a section in the readme that covers protection but to reiterate:
7
+ #
8
+ # === Protected
9
+ #
10
+ # User.new_search(params[:search])
11
+ # User.new_conditions(params[:search])
12
+ #
13
+ # search.options = params[:search]
14
+ # conditions.conditions = params[:conditions]
15
+ #
16
+ # === NOT Protected
17
+ #
18
+ # User.new_search!(params[:search])
19
+ # User.new_conditions!(params[:search])
20
+ # User.find(:all, params[:search])
21
+ # User.first(params[:search])
22
+ # User.all(params[:search])
23
+ module Protection
24
+ # Options that are allowed when protecting against SQL injections (still checked though)
25
+ SAFE_OPTIONS = Base::SPECIAL_FIND_OPTIONS + [:conditions, :limit, :offset] - [:priority_order]
26
+
27
+ VULNERABLE_FIND_OPTIONS = Base::AR_FIND_OPTIONS - SAFE_OPTIONS + [:priority_order]
28
+
29
+ VULNERABLE_CALCULATIONS_OPTIONS = Base::AR_CALCULATIONS_OPTIONS - SAFE_OPTIONS + [:priority_order]
30
+
31
+ # Options that are not allowed, at all, when protecting against SQL injections
32
+ VULNERABLE_OPTIONS = Base::OPTIONS - SAFE_OPTIONS
33
+
34
+ def self.included(klass)
35
+ klass.class_eval do
36
+ attr_reader :protect
37
+ alias_method_chain :options=, :protection
38
+ end
39
+ end
40
+
41
+ def options_with_protection=(values) # :nodoc:
42
+ return unless values.is_a?(Hash)
43
+ self.protect = values.delete(:protect) if values.has_key?(:protect) # make sure we do this first
44
+ frisk!(values) if protect?
45
+ self.options_without_protection = values
46
+ end
47
+
48
+ # Accepts a boolean. Will protect mass assignemnts if set to true, and unprotect mass assignments if set to false
49
+ def protect=(value)
50
+ conditions.protect = value
51
+ @protect = value
52
+ end
53
+
54
+ # Convenience methof for determing if the search is protected or not.
55
+ def protect?
56
+ protect == true
57
+ end
58
+ alias_method :protected?, :protect?
59
+
60
+ private
61
+ def order_by_safe?(order_by, alt_klass = nil)
62
+ return true if order_by.blank?
63
+
64
+ k = alt_klass || klass
65
+ column_names = k.column_names
66
+
67
+ [order_by].flatten.each do |column|
68
+ case column
69
+ when Hash
70
+ reflection = k.reflect_on_association(column.keys.first.to_sym)
71
+ return false unless reflection
72
+ return false unless order_by_safe?(column.values.first, reflection.klass)
73
+ when Array
74
+ return false unless order_by_safe?(column)
75
+ else
76
+ return false unless column_names.include?(column.to_s)
77
+ end
78
+ end
79
+
80
+ true
81
+ end
82
+
83
+ def frisk!(options)
84
+ options.symbolize_keys.fast_assert_valid_keys(SAFE_OPTIONS)
85
+ raise(ArgumentError, ":order_by can only contain colum names in the string, hash, or array format") unless order_by_safe?(get_order_by_value(options[:order_by]))
86
+ end
87
+ end
88
+ end
89
+ end