diff options
34 files changed, 329 insertions, 104 deletions
diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index 5ab92c8cfc..a90a7b37f7 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,13 @@ *Rails 3.1.0 (unreleased)* +* Allow you to add `force_ssl` into controller to force browser to transfer data via HTTPS protocol on that particular controller. You can also specify `:only` or `:except` to specific it to particular action. [DHH and Prem Sichanugrist] + +* Allow FormHelper#form_for to specify the :method as a direct option instead of through the :html hash [DHH] + + form_for(@post, remote: true, method: :delete) instead of form_for(@post, remote: true, html: { method: :delete }) + +* Make JavaScriptHelper#j() an alias for JavaScriptHelper#escape_javascript() -- note this then supersedes the Object#j() method that the JSON gem adds within templates using the JavaScriptHelper [DHH] + * Sensitive query string parameters (specified in config.filter_parameters) will now be filtered out from the request paths in the log file. [Prem Sichanugrist, fxn] * URL parameters which return false for to_param now appear in the query string (previously they were removed) [Andrew White] diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index 5b81cd39f4..62cc18b253 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -14,6 +14,7 @@ module ActionController autoload :ConditionalGet autoload :Cookies autoload :Flash + autoload :ForceSSL autoload :Head autoload :Helpers autoload :HideActions diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 81c0698fb8..e6523e56d2 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -198,6 +198,7 @@ module ActionController Cookies, Flash, RequestForgeryProtection, + ForceSSL, Streaming, RecordIdentifier, HttpAuthentication::Basic::ControllerMethods, diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb new file mode 100644 index 0000000000..eb8ed7dfbd --- /dev/null +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -0,0 +1,35 @@ +module ActionController + # This module provides a method which will redirects browser to use HTTPS + # protocol. This will ensure that user's sensitive information will be + # transferred safely over the internet. You _should_ always force browser + # to use HTTPS when you're transferring sensitive information such as + # user authentication, account information, or credit card information. + # + # Note that if you really concern about your application safety, you might + # consider using +config.force_ssl+ in your configuration config file instead. + # That will ensure all the data transferred via HTTPS protocol and prevent + # user from getting session hijacked when accessing the site under unsecured + # HTTP protocol. + module ForceSSL + extend ActiveSupport::Concern + include AbstractController::Callbacks + + module ClassMethods + # Force the request to this particular controller or specified actions to be + # under HTTPS protocol. + # + # Note that this method will not be effective on development environment. + # + # ==== Options + # * <tt>only</tt> - The callback should be run only for this action + # * <tt>except<tt> - The callback should be run for all actions except this action + def force_ssl(options = {}) + before_filter(options) do + if !request.ssl? && !Rails.env.development? + redirect_to :protocol => 'https://', :status => :moved_permanently + end + end + end + end + end +end
\ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index 48abf119f1..9025d9e24c 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -185,7 +185,7 @@ module ActionView # # is equivalent to something like: # - # <%= form_for @post, :as => :post, :url => post_path(@post), :html => { :method => :put, :class => "edit_post", :id => "edit_post_45" } do |f| %> + # <%= form_for @post, :as => :post, :url => post_path(@post), :method => :put, :html => { :class => "edit_post", :id => "edit_post_45" } do |f| %> # ... # <% end %> # @@ -236,6 +236,16 @@ module ActionView # Where <tt>@document = Document.find(params[:id])</tt> and # <tt>@comment = Comment.new</tt>. # + # === Setting the method + # + # You can force the form to use the full array of HTTP verbs by setting + # + # :method => (:get|:post|:put|:delete) + # + # in the options hash. If the verb is not GET or POST, which are natively supported by HTML forms, the + # form will be set to POST and a hidden input called _method will carry the intended verb for the server + # to interpret. + # # === Unobtrusive JavaScript # # Specifying: @@ -298,7 +308,7 @@ module ActionView # # In this case, if you use this: # - # <%= render :partial => f %> + # <%= render f %> # # The rendered template is <tt>people/_labelling_form</tt> and the local # variable referencing the form builder is called @@ -350,6 +360,7 @@ module ActionView end options[:html][:remote] = options.delete(:remote) + options[:html][:method] = options.delete(:method) if options.has_key?(:method) options[:html][:authenticity_token] = options.delete(:authenticity_token) builder = options[:parent_builder] = instantiate_builder(object_name, object, options, &proc) diff --git a/actionpack/lib/action_view/helpers/javascript_helper.rb b/actionpack/lib/action_view/helpers/javascript_helper.rb index cd3a3eac80..a19ba7a968 100644 --- a/actionpack/lib/action_view/helpers/javascript_helper.rb +++ b/actionpack/lib/action_view/helpers/javascript_helper.rb @@ -47,6 +47,9 @@ module ActionView "'" => "\\'" } # Escape carrier returns and single and double quotes for JavaScript segments. + # Also available through the alias j(). This is particularly helpful in JavaScript responses, like: + # + # $('some_element').replaceWith('<%=j render 'some/element_template' %>'); def escape_javascript(javascript) if javascript javascript.gsub(/(\\|<\/|\r\n|[\n\r"'])/) { JS_ESCAPE_MAP[$1] } @@ -55,6 +58,8 @@ module ActionView end end + alias_method :j, :escape_javascript + # Returns a JavaScript tag with the +content+ inside. Example: # javascript_tag "alert('All is good')" # diff --git a/actionpack/lib/action_view/template/resolver.rb b/actionpack/lib/action_view/template/resolver.rb index 6c1063592f..41c6310ae2 100644 --- a/actionpack/lib/action_view/template/resolver.rb +++ b/actionpack/lib/action_view/template/resolver.rb @@ -157,8 +157,8 @@ module ActionView query.gsub!(/\:#{ext}/, "{#{variants.compact.uniq.join(',')}}") } - query.gsub!(/\.{html,/, ".{html,text.html,") - query.gsub!(/\.{text,/, ".{text,text.plain,") + query.gsub!('.{html,', '.{html,text.html,') + query.gsub!('.{text,', '.{text,text.plain,') File.expand_path(query, @path) end diff --git a/actionpack/test/controller/force_ssl_test.rb b/actionpack/test/controller/force_ssl_test.rb new file mode 100644 index 0000000000..3e723e20d9 --- /dev/null +++ b/actionpack/test/controller/force_ssl_test.rb @@ -0,0 +1,83 @@ +require 'abstract_unit' + +class ForceSSLController < ActionController::Base + def banana + render :text => "monkey" + end + + def cheeseburger + render :text => "sikachu" + end +end + +class ForceSSLControllerLevel < ForceSSLController + force_ssl +end + +class ForceSSLOnlyAction < ForceSSLController + force_ssl :only => :cheeseburger +end + +class ForceSSLExceptAction < ForceSSLController + force_ssl :except => :banana +end + +class ForceSSLControllerLevelTest < ActionController::TestCase + tests ForceSSLControllerLevel + + def test_banana_redirects_to_https + get :banana + assert_response 301 + assert_equal "https://test.host/force_ssl_controller_level/banana", redirect_to_url + end + + def test_cheeseburger_redirects_to_https + get :cheeseburger + assert_response 301 + assert_equal "https://test.host/force_ssl_controller_level/cheeseburger", redirect_to_url + end +end + +class ForceSSLOnlyActionTest < ActionController::TestCase + tests ForceSSLOnlyAction + + def test_banana_not_redirects_to_https + get :banana + assert_response 200 + end + + def test_cheeseburger_redirects_to_https + get :cheeseburger + assert_response 301 + assert_equal "https://test.host/force_ssl_only_action/cheeseburger", redirect_to_url + end +end + +class ForceSSLExceptActionTest < ActionController::TestCase + tests ForceSSLExceptAction + + def test_banana_not_redirects_to_https + get :banana + assert_response 200 + end + + def test_cheeseburger_redirects_to_https + get :cheeseburger + assert_response 301 + assert_equal "https://test.host/force_ssl_except_action/cheeseburger", redirect_to_url + end +end + +class ForceSSLExcludeDevelopmentTest < ActionController::TestCase + tests ForceSSLControllerLevel + + def setup + Rails.env.stubs(:development?).returns(false) + end + + def test_development_environment_not_redirects_to_https + Rails.env.stubs(:development?).returns(true) + get :banana + assert_response 200 + end +end
\ No newline at end of file diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb index 359b078466..ff183d097d 100644 --- a/actionpack/test/template/form_helper_test.rb +++ b/actionpack/test/template/form_helper_test.rb @@ -715,14 +715,31 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end + def test_form_for_with_method_as_part_of_html_options + form_for(@post, :url => '/', :html => { :id => 'create-post', :method => :delete }) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + end + + expected = whole_form("/", "create-post", "edit_post", "delete") do + "<input name='post[title]' size='30' type='text' id='post_title' value='Hello World' />" + + "<textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + + "<input name='post[secret]' type='hidden' value='0' />" + + "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" + end + + assert_dom_equal expected, output_buffer + end + def test_form_for_with_method - form_for(@post, :url => '/', :html => { :id => 'create-post', :method => :put }) do |f| + form_for(@post, :url => '/', :method => :delete, :html => { :id => 'create-post' }) do |f| concat f.text_field(:title) concat f.text_area(:body) concat f.check_box(:secret) end - expected = whole_form("/", "create-post", "edit_post", "put") do + expected = whole_form("/", "create-post", "edit_post", "delete") do "<input name='post[title]' size='30' type='text' id='post_title' value='Hello World' />" + "<textarea name='post[body]' id='post_body' rows='20' cols='40'>Back to the hill and over it again!</textarea>" + "<input name='post[secret]' type='hidden' value='0' />" + diff --git a/actionpack/test/template/javascript_helper_test.rb b/actionpack/test/template/javascript_helper_test.rb index 2e7484afaf..8aa2730da1 100644 --- a/actionpack/test/template/javascript_helper_test.rb +++ b/actionpack/test/template/javascript_helper_test.rb @@ -27,6 +27,7 @@ class JavaScriptHelperTest < ActionView::TestCase assert_equal %(This \\"thing\\" is really\\n netos\\'), escape_javascript(%(This "thing" is really\n netos')) assert_equal %(backslash\\\\test), escape_javascript( %(backslash\\test) ) assert_equal %(dont <\\/close> tags), escape_javascript(%(dont </close> tags)) + assert_equal %(dont <\\/close> tags), j(%(dont </close> tags)) end def test_button_to_function diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 1d2e8667e4..dfcb116392 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -34,7 +34,8 @@ module ActiveRecord when :destroy target.destroy when :nullify - target.update_attribute(reflection.foreign_key, nil) + target.send("#{reflection.foreign_key}=", nil) + target.save(:validations => false) end end end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 17a64b6e86..3377a5934b 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -104,19 +104,17 @@ module ActiveRecord became end - # Updates a single attribute and saves the record. - # This is especially useful for boolean flags on existing records. Also note that + # Updates a single attribute of an object, without calling save. # # * Validation is skipped. - # * Callbacks are invoked. - # * updated_at/updated_on column is updated if that column is available. - # * Updates all the attributes that are dirty in this object. + # * Callbacks are skipped. + # * updated_at/updated_on column is not updated in any case. # - def update_attribute(name, value) + def update_column(name, value) name = name.to_s raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name) send("#{name}=", value) - save(:validate => false) + self.class.update_all({ name => value }, self.class.primary_key => id) end # Updates the attributes of the model from the passed-in hash and saves the @@ -156,7 +154,7 @@ module ActiveRecord # Saving is not subjected to validation checks. Returns +true+ if the # record could be saved. def increment!(attribute, by = 1) - increment(attribute, by).update_attribute(attribute, self[attribute]) + increment(attribute, by).update_column(attribute, self[attribute]) end # Initializes +attribute+ to zero if +nil+ and subtracts the value passed as +by+ (default is 1). @@ -173,7 +171,7 @@ module ActiveRecord # Saving is not subjected to validation checks. Returns +true+ if the # record could be saved. def decrement!(attribute, by = 1) - decrement(attribute, by).update_attribute(attribute, self[attribute]) + decrement(attribute, by).update_column(attribute, self[attribute]) end # Assigns to +attribute+ the boolean opposite of <tt>attribute?</tt>. So @@ -190,7 +188,7 @@ module ActiveRecord # Saving is not subjected to validation checks. Returns +true+ if the # record could be saved. def toggle!(attribute) - toggle(attribute).update_attribute(attribute, self[attribute]) + toggle(attribute).update_column(attribute, self[attribute]) end # Reloads the attributes of this object from the database. diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index 73d02c9676..f4d14853d3 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -604,7 +604,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase project = SpecialProject.create("name" => "Special Project") assert developer.save developer.projects << project - developer.update_attribute("name", "Bruza") + developer.update_column("name", "Bruza") assert_equal 1, Developer.connection.select_value(<<-end_sql).to_i SELECT count(*) FROM developers_projects WHERE project_id = #{project.id} diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index ad774eb9ce..16d4877fe8 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -639,7 +639,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_deleting_updates_counter_cache_with_dependent_delete_all post = posts(:welcome) - post.update_attribute(:taggings_with_delete_all_count, post.taggings_count) + post.update_column(:taggings_with_delete_all_count, post.taggings_count) assert_difference "post.reload.taggings_with_delete_all_count", -1 do post.taggings_with_delete_all.delete(post.taggings_with_delete_all.first) @@ -648,7 +648,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_deleting_updates_counter_cache_with_dependent_destroy post = posts(:welcome) - post.update_attribute(:taggings_with_destroy_count, post.taggings_count) + post.update_column(:taggings_with_destroy_count, post.taggings_count) assert_difference "post.reload.taggings_with_destroy_count", -1 do post.taggings_with_destroy.delete(post.taggings_with_destroy.first) @@ -787,7 +787,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm = Firm.find(:first) # break the vanilla firm_id foreign key assert_equal 2, firm.clients.count - firm.clients.first.update_attribute(:firm_id, nil) + firm.clients.first.update_column(:firm_id, nil) assert_equal 1, firm.clients(true).count assert_equal 1, firm.clients_using_primary_key_with_delete_all.count old_record = firm.clients_using_primary_key_with_delete_all.first diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 9adaebe924..1efe3420a0 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -286,7 +286,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_update_counter_caches_on_delete_with_dependent_destroy post = posts(:welcome) tag = post.tags.create!(:name => 'doomed') - post.update_attribute(:tags_with_destroy_count, post.tags.count) + post.update_column(:tags_with_destroy_count, post.tags.count) assert_difference ['post.reload.taggings_count', 'post.reload.tags_with_destroy_count'], -1 do posts(:welcome).tags_with_destroy.delete(tag) @@ -296,7 +296,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_update_counter_caches_on_delete_with_dependent_nullify post = posts(:welcome) tag = post.tags.create!(:name => 'doomed') - post.update_attribute(:tags_with_nullify_count, post.tags.count) + post.update_column(:tags_with_nullify_count, post.tags.count) assert_no_difference 'post.reload.taggings_count' do assert_difference 'post.reload.tags_with_nullify_count', -1 do diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index 9ba5549277..968025ade8 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -90,12 +90,12 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase def test_has_one_through_with_conditions_eager_loading # conditions on the through table assert_equal clubs(:moustache_club), Member.find(@member.id, :include => :favourite_club).favourite_club - memberships(:membership_of_favourite_club).update_attribute(:favourite, false) + memberships(:membership_of_favourite_club).update_column(:favourite, false) assert_equal nil, Member.find(@member.id, :include => :favourite_club).reload.favourite_club # conditions on the source table assert_equal clubs(:moustache_club), Member.find(@member.id, :include => :hairy_club).hairy_club - clubs(:moustache_club).update_attribute(:name, "Association of Clean-Shaven Persons") + clubs(:moustache_club).update_column(:name, "Association of Clean-Shaven Persons") assert_equal nil, Member.find(@member.id, :include => :hairy_club).reload.hairy_club end diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 1f95b31497..5a7b139030 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -161,7 +161,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_delete_polymorphic_has_many_with_delete_all assert_equal 1, posts(:welcome).taggings.count - posts(:welcome).taggings.first.update_attribute :taggable_type, 'PostWithHasManyDeleteAll' + posts(:welcome).taggings.first.update_column :taggable_type, 'PostWithHasManyDeleteAll' post = find_post_with_dependency(1, :has_many, :taggings, :delete_all) old_count = Tagging.count @@ -172,7 +172,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_delete_polymorphic_has_many_with_destroy assert_equal 1, posts(:welcome).taggings.count - posts(:welcome).taggings.first.update_attribute :taggable_type, 'PostWithHasManyDestroy' + posts(:welcome).taggings.first.update_column :taggable_type, 'PostWithHasManyDestroy' post = find_post_with_dependency(1, :has_many, :taggings, :destroy) old_count = Tagging.count @@ -183,7 +183,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_delete_polymorphic_has_many_with_nullify assert_equal 1, posts(:welcome).taggings.count - posts(:welcome).taggings.first.update_attribute :taggable_type, 'PostWithHasManyNullify' + posts(:welcome).taggings.first.update_column :taggable_type, 'PostWithHasManyNullify' post = find_post_with_dependency(1, :has_many, :taggings, :nullify) old_count = Tagging.count @@ -194,7 +194,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_delete_polymorphic_has_one_with_destroy assert posts(:welcome).tagging - posts(:welcome).tagging.update_attribute :taggable_type, 'PostWithHasOneDestroy' + posts(:welcome).tagging.update_column :taggable_type, 'PostWithHasOneDestroy' post = find_post_with_dependency(1, :has_one, :tagging, :destroy) old_count = Tagging.count @@ -205,7 +205,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_delete_polymorphic_has_one_with_nullify assert posts(:welcome).tagging - posts(:welcome).tagging.update_attribute :taggable_type, 'PostWithHasOneNullify' + posts(:welcome).tagging.update_column :taggable_type, 'PostWithHasOneNullify' post = find_post_with_dependency(1, :has_one, :tagging, :nullify) old_count = Tagging.count @@ -707,7 +707,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase # create dynamic Post models to allow different dependency options def find_post_with_dependency(post_id, association, association_name, dependency) class_name = "PostWith#{association.to_s.classify}#{dependency.to_s.classify}" - Post.find(post_id).update_attribute :type, class_name + Post.find(post_id).update_column :type, class_name klass = Object.const_set(class_name, Class.new(ActiveRecord::Base)) klass.set_table_name 'posts' klass.send(association, association_name, :as => :taggable, :dependent => dependency) diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 47b8e48582..04f628a398 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -66,7 +66,7 @@ class AssociationsTest < ActiveRecord::TestCase ship = Ship.create!(:name => "The good ship Dollypop") part = ship.parts.create!(:name => "Mast") part.mark_for_destruction - ShipPart.find(part.id).update_attribute(:name, 'Deck') + ShipPart.find(part.id).update_column(:name, 'Deck') ship.parts.send(:load_target) assert_equal 'Deck', ship.parts[0].name end @@ -170,7 +170,7 @@ class AssociationProxyTest < ActiveRecord::TestCase david = developers(:david) assert !david.projects.loaded? - david.update_attribute(:created_at, Time.now) + david.update_column(:created_at, Time.now) assert !david.projects.loaded? end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index d03c3ee1a1..84f75cc803 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -132,6 +132,27 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end + def test_read_attributes_after_type_cast_on_datetime + tz = "Pacific Time (US & Canada)" + + in_time_zone tz do + record = @target.new + + date_string = "2011-03-24" + time = Time.zone.parse date_string + + record.written_on = date_string + assert_equal date_string, record.written_on_before_type_cast + assert_equal time, record.written_on + assert_equal ActiveSupport::TimeZone[tz], record.written_on.time_zone + + record.save + record.reload + + assert_equal time, record.written_on + end + end + def test_hash_content topic = Topic.new topic.content = { "one" => 1, "two" => 2 } diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index fba7af741d..aeb0b28bab 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -484,7 +484,7 @@ class BasicsTest < ActiveRecord::TestCase weird.reload assert_equal 'value', weird.send('a$b') - weird.update_attribute('a$b', 'value2') + weird.update_column('a$b', 'value2') weird.reload assert_equal 'value2', weird.send('a$b') end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index caf07a7357..c97f1ae634 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -311,8 +311,8 @@ class CalculationsTest < ActiveRecord::TestCase def test_should_count_scoped_select_with_options Account.update_all("credit_limit = NULL") - Account.last.update_attribute('credit_limit', 49) - Account.first.update_attribute('credit_limit', 51) + Account.last.update_column('credit_limit', 49) + Account.first.update_column('credit_limit', 51) assert_equal 1, Account.scoped(:select => "credit_limit").count(:conditions => ['credit_limit >= 50']) end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index a6738fb654..b75482a956 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -413,7 +413,7 @@ class DirtyTest < ActiveRecord::TestCase with_partial_updates(Topic) do Topic.create!(:author_name => 'Bill', :content => {:a => "a"}) topic = Topic.select('id, author_name').first - topic.update_attribute :author_name, 'John' + topic.update_column :author_name, 'John' topic = Topic.first assert_not_nil topic.content end @@ -487,7 +487,8 @@ class DirtyTest < ActiveRecord::TestCase assert !pirate.previous_changes.key?('created_on') pirate = Pirate.find_by_catchphrase("Ahoy!") - pirate.update_attribute(:catchphrase, "Ninjas suck!") + pirate.catchphrase = "Ninjas suck!" + pirate.save(:validations => false) assert_equal 2, pirate.previous_changes.size assert_equal ["Ahoy!", "Ninjas suck!"], pirate.previous_changes['catchphrase'] diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 8ca9d626d1..68d861c9c2 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -327,66 +327,62 @@ class PersistencesTest < ActiveRecord::TestCase assert_raise(ActiveSupport::FrozenObjectError) { client.name = "something else" } end - def test_update_attribute - assert !Topic.find(1).approved? - Topic.find(1).update_attribute("approved", true) - assert Topic.find(1).approved? + def test_update_column + topic = Topic.find(1) + topic.update_column("approved", true) + assert topic.approved? + topic.reload + assert topic.approved? - Topic.find(1).update_attribute(:approved, false) - assert !Topic.find(1).approved? + topic.update_column(:approved, false) + assert !topic.approved? + topic.reload + assert !topic.approved? end - def test_update_attribute_for_readonly_attribute + def test_update_column_with_model_having_primary_key_other_than_id minivan = Minivan.find('m1') - assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_attribute(:color, 'black') } - end - - # This test is correct, but it is hard to fix it since - # update_attribute trigger simply call save! that triggers - # all callbacks. - # def test_update_attribute_with_one_changed_and_one_updated - # t = Topic.order('id').limit(1).first - # title, author_name = t.title, t.author_name - # t.author_name = 'John' - # t.update_attribute(:title, 'super_title') - # assert_equal 'John', t.author_name - # assert_equal 'super_title', t.title - # assert t.changed?, "topic should have changed" - # assert t.author_name_changed?, "author_name should have changed" - # assert !t.title_changed?, "title should not have changed" - # assert_nil t.title_change, 'title change should be nil' - # assert_equal ['author_name'], t.changed - # - # t.reload - # assert_equal 'David', t.author_name - # assert_equal 'super_title', t.title - # end - - def test_update_attribute_with_one_updated - t = Topic.first - title = t.title - t.update_attribute(:title, 'super_title') - assert_equal 'super_title', t.title - assert !t.changed?, "topic should not have changed" - assert !t.title_changed?, "title should not have changed" - assert_nil t.title_change, 'title change should be nil' + new_name = 'sebavan' - t.reload - assert_equal 'super_title', t.title + minivan.update_column(:name, new_name) + assert_equal new_name, minivan.name end - def test_update_attribute_for_updated_at_on + def test_update_column_for_readonly_attribute + minivan = Minivan.find('m1') + prev_color = minivan.color + assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_column(:color, 'black') } + assert_equal prev_color, minivan.color + end + + def test_update_column_should_not_modify_updated_at developer = Developer.find(1) prev_month = Time.now.prev_month - developer.update_attribute(:updated_at, prev_month) + developer.update_column(:updated_at, prev_month) assert_equal prev_month, developer.updated_at - developer.update_attribute(:salary, 80001) - assert_not_equal prev_month, developer.updated_at + developer.update_column(:salary, 80001) + assert_equal prev_month, developer.updated_at developer.reload - assert_not_equal prev_month, developer.updated_at + assert_equal prev_month, developer.updated_at + end + + def test_update_column_with_one_changed_and_one_updated + t = Topic.order('id').limit(1).first + title, author_name = t.title, t.author_name + t.author_name = 'John' + t.update_column(:title, 'super_title') + assert_equal 'John', t.author_name + assert_equal 'super_title', t.title + assert t.changed?, "topic should have changed" + assert t.author_name_changed?, "author_name should have changed" + assert t.title_changed?, "title should have changed" + + t.reload + assert_equal author_name, t.author_name + assert_equal 'super_title', t.title end def test_update_attributes diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 1c21f0f3b6..08bbd14d38 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -113,7 +113,8 @@ class TimestampTest < ActiveRecord::TestCase pet = Pet.first owner = pet.owner - owner.update_attribute(:happy_at, 3.days.ago) + owner.happy_at = 3.days.ago + owner.save previously_owner_updated_at = owner.updated_at pet.name = "I'm a parrot" @@ -131,8 +132,9 @@ class TimestampTest < ActiveRecord::TestCase toy = Toy.first pet = toy.pet owner = pet.owner + time = 3.days.ago - owner.update_attribute(:updated_at, (time = 3.days.ago)) + owner.update_column(:updated_at, time) toy.touch owner.reload diff --git a/railties/CHANGELOG b/railties/CHANGELOG index 75f1df44e7..c1e0a214d2 100644 --- a/railties/CHANGELOG +++ b/railties/CHANGELOG @@ -1,5 +1,7 @@ *Rails 3.1.0 (unreleased)* +* Added `config.force_ssl` configuration which loads Rack::SSL middleware and force all requests to be under HTTPS protocol [DHH, Prem Sichanugrist, and Josh Peek] + * Added `rails plugin new` command which generates rails plugin with gemspec, tests and dummy application for testing [Piotr Sarnacki] * Added -j parameter with jquery/prototype as options. Now you can create your apps with jQuery using `rails new myapp -j jquery`. The default is still Prototype. [siong1987] diff --git a/railties/guides/source/action_controller_overview.textile b/railties/guides/source/action_controller_overview.textile index ecb03a48e4..178d98c2d6 100644 --- a/railties/guides/source/action_controller_overview.textile +++ b/railties/guides/source/action_controller_overview.textile @@ -816,6 +816,28 @@ end NOTE: Certain exceptions are only rescuable from the +ApplicationController+ class, as they are raised before the controller gets initialized and the action gets executed. See Pratik Naik's "article":http://m.onkey.org/2008/7/20/rescue-from-dispatching on the subject for more information. +h3. Force HTTPS protocol + +Sometime you might want to force a particular controller to only be accessible via an HTTPS protocol for security reason. Since Rails 3.1 you can now use +force_ssl+ method in your controller to enforce that: + +<ruby> +class DinnerController + force_ssl +end +</ruby> + +Just like the filter, you could also passing +:only+ and +:except+ to enforce the secure connection only to specific actions + +<ruby> +class DinnerController + force_ssl :only => :cheeseburger + # or + force_ssl :except => :cheeseburger +end +</ruby> + +Please note that if you found yourself adding +force_ssl+ to many controllers, you may found yourself wanting to force the whole application to use HTTPS instead. In that case, you can set the +config.force_ssl+ in your environment file. + h3. Changelog * February 17, 2009: Yet another proofread by Xavier Noria. diff --git a/railties/guides/source/configuring.textile b/railties/guides/source/configuring.textile index 62b846e871..298335d484 100644 --- a/railties/guides/source/configuring.textile +++ b/railties/guides/source/configuring.textile @@ -81,6 +81,8 @@ end * +config.filter_parameters+ used for filtering out the parameters that you don't want shown in the logs, such as passwords or credit card numbers. +* +config.force_ssl+ forcing all requests to be under HTTPS protocol by using +Rack::SSL+ middleware. This will secure your application from a session hijack attempt. + * +config.helper_paths+ configures where Rails can find helpers for this application. * +config.log_level+ defines the verbosity of the Rails logger. In production mode, this defaults to +:info+. In development mode, it defaults to +:debug+. @@ -147,6 +149,7 @@ h4. Configuring Middleware Every Rails application comes with a standard set of middleware which it uses in this order in the development environment: +* +Rack::SSL+ Will force every requests to be under HTTPS protocol. Will be available if +config.force_ssl+ is set to _true_. * +ActionDispatch::Static+ is used to serve static assets. Disabled if +config.serve_static_assets+ is _true_. * +Rack::Lock+ Will wrap the app in mutex so it can only be called by a single thread at a time. Only enabled if +config.action_controller.allow_concurrency+ is set to _false_, which it is by default. * +ActiveSupport::Cache::Strategy::LocalCache+ Serves as a basic memory backed cache. This cache is not thread safe and is intended only for serving as a temporary memory cache for a single thread. diff --git a/railties/guides/source/security.textile b/railties/guides/source/security.textile index 182f3631ef..893f65856c 100644 --- a/railties/guides/source/security.textile +++ b/railties/guides/source/security.textile @@ -57,7 +57,11 @@ Many web applications have an authentication system: a user provides a user name Hence, the cookie serves as temporary authentication for the web application. Everyone who seizes a cookie from someone else, may use the web application as this user – with possibly severe consequences. Here are some ways to hijack a session, and their countermeasures: -* Sniff the cookie in an insecure network. A wireless LAN can be an example of such a network. In an unencrypted wireless LAN it is especially easy to listen to the traffic of all connected clients. This is one more reason not to work from a coffee shop. For the web application builder this means to _(highlight)provide a secure connection over SSL_. +* Sniff the cookie in an insecure network. A wireless LAN can be an example of such a network. In an unencrypted wireless LAN it is especially easy to listen to the traffic of all connected clients. This is one more reason not to work from a coffee shop. For the web application builder this means to _(highlight)provide a secure connection over SSL_. In Rails 3.1 and later, this could be accomplished by always forcing SSL connection in your application config file: + +<ruby> +config.force_ssl = true +</ruby> * Most people don't clear out the cookies after working at a public terminal. So if the last user didn't log out of a web application, you would be able to use it as this user. Provide the user with a _(highlight)log-out button_ in the web application, and _(highlight)make it prominent_. diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 94819820bc..1b834275a7 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -145,15 +145,21 @@ module Rails def default_middleware_stack ActionDispatch::MiddlewareStack.new.tap do |middleware| - rack_cache = config.action_controller.perform_caching && config.action_dispatch.rack_cache + if rack_cache = config.action_controller.perform_caching && config.action_dispatch.rack_cache + require "action_dispatch/http/rack_cache" + middleware.use ::Rack::Cache, rack_cache + end - require "action_dispatch/http/rack_cache" if rack_cache - middleware.use ::Rack::Cache, rack_cache if rack_cache + if config.force_ssl + require "rack/ssl" + middleware.use ::Rack::SSL + end if config.serve_static_assets asset_paths = ActiveSupport::OrderedHash[config.static_asset_paths.to_a.reverse] middleware.use ::ActionDispatch::Static, asset_paths end + middleware.use ::Rack::Lock unless config.allow_concurrency middleware.use ::Rack::Runtime middleware.use ::Rails::Rack::Logger @@ -174,7 +180,10 @@ module Rails middleware.use ::ActionDispatch::Head middleware.use ::Rack::ConditionalGet middleware.use ::Rack::ETag, "no-cache" - middleware.use ::ActionDispatch::BestStandardsSupport, config.action_dispatch.best_standards_support if config.action_dispatch.best_standards_support + + if config.action_dispatch.best_standards_support + middleware.use ::ActionDispatch::BestStandardsSupport, config.action_dispatch.best_standards_support + end end end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index c74bcbedf2..23b0e765ae 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -9,7 +9,7 @@ module Rails :filter_parameters, :helpers_paths, :logger, :preload_frameworks, :reload_plugins, :secret_token, :serve_static_assets, :session_options, - :time_zone, :whiny_nils + :time_zone, :whiny_nils, :force_ssl attr_writer :log_level @@ -22,6 +22,7 @@ module Rails @helpers_paths = [] @dependency_loading = true @serve_static_assets = true + @force_ssl = false @session_store = :cookie_store @session_options = {} @time_zone = "UTC" diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt index 91d3133ea4..bdb897ad33 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt @@ -3,7 +3,7 @@ # In the development environment your application's code is reloaded on # every request. This slows down response time but is perfect for development - # since you don't have to restart the webserver when you make code changes. + # since you don't have to restart the web server when you make code changes. config.cache_classes = false # Log error messages when you accidentally call methods on nil. diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt index 89bb891ddd..874cc403ba 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt @@ -1,7 +1,6 @@ <%= app_const %>.configure do # Settings specified here will take precedence over those in config/application.rb - # The production environment is meant for finished, "live" apps. # Code is not reloaded between requests config.cache_classes = true @@ -9,14 +8,15 @@ config.consider_all_requests_local = false config.action_controller.perform_caching = true - # Specifies the header that your server uses for sending files - config.action_dispatch.x_sendfile_header = "X-Sendfile" + # Disable Rails's static asset server (Apache or nginx will already do this) + config.serve_static_assets = false - # For nginx: - # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' + # Specifies the header that your server uses for sending files + # (comment out if your front-end server doesn't support this) + config.action_dispatch.x_sendfile_header = "X-Sendfile" # Use 'X-Accel-Redirect' for nginx - # If you have no front-end server that supports something like X-Sendfile, - # just comment this out and Rails will serve the files + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true # See everything in the log (default is :info) # config.log_level = :debug @@ -27,10 +27,6 @@ # Use a different cache store in production # config.cache_store = :mem_cache_store - # Disable Rails's static asset server - # In production, Apache or nginx will already do this - config.serve_static_assets = false - # Enable serving of images, stylesheets, and javascripts from an asset server # config.action_controller.asset_host = "http://assets.example.com" diff --git a/railties/railties.gemspec b/railties/railties.gemspec index c3793d3ac3..c51fe856be 100644 --- a/railties/railties.gemspec +++ b/railties/railties.gemspec @@ -21,6 +21,7 @@ Gem::Specification.new do |s| s.add_dependency('rake', '>= 0.8.7') s.add_dependency('thor', '~> 0.14.4') + s.add_dependency('rack-ssl', '~> 1.3.2') s.add_dependency('activesupport', version) s.add_dependency('actionpack', version) end diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb index b314832685..01e6c49d9c 100644 --- a/railties/test/application/middleware_test.rb +++ b/railties/test/application/middleware_test.rb @@ -52,6 +52,12 @@ module ApplicationTests assert_equal "Rack::Cache", middleware.first end + test "Rack::SSL is present when force_ssl is set" do + add_to_config "config.force_ssl = true" + boot! + assert middleware.include?("Rack::SSL") + end + test "removing Active Record omits its middleware" do use_frameworks [] boot! |