diff --git a/.gitignore b/.gitignore index 2dfdf96e7f..8daa1e4dcd 100644 --- a/.gitignore +++ b/.gitignore @@ -10,12 +10,15 @@ activerecord/doc actionpack/doc actionmailer/doc activesupport/doc +activesupport/test/tmp activemodel/test/fixtures/fixture_database.sqlite3 actionpack/test/tmp activesupport/test/fixtures/isolation_test +dist railties/test/500.html railties/test/fixtures/tmp railties/test/initializer/root/log railties/doc railties/guides/output railties/tmp +.rvmrc diff --git a/.yardopts b/.yardopts new file mode 100644 index 0000000000..25ec38658f --- /dev/null +++ b/.yardopts @@ -0,0 +1,4 @@ +--exclude /templates/ +--quiet +act*/lib/**/*.rb +railties/lib/**/*.rb diff --git a/Gemfile b/Gemfile index d594d20fac..879598c6db 100644 --- a/Gemfile +++ b/Gemfile @@ -1,41 +1,55 @@ source 'http://rubygems.org' +gemspec + if ENV['AREL'] gem "arel", :path => ENV['AREL'] else gem "arel", :git => "git://github.com/rails/arel.git" end -gem "rails", :path => File.dirname(__FILE__) +gem "rack", :git => "git://github.com/rack/rack.git" +gem "rack-test", :git => "git://github.com/brynary/rack-test.git" gem "rake", ">= 0.8.7" gem "mocha", ">= 0.9.8" -gem "rdoc", ">= 2.5.9" -gem "horo", ">= 1.0.1" + +group :doc do + gem "rdoc", "~> 3.4" + gem "horo", "= 1.0.3" + gem "RedCloth", "~> 4.2" if RUBY_VERSION < "1.9.3" +end # AS gem "memcache-client", ">= 1.8.5" - -# AM -gem "text-format", "~> 1.0.0" +gem "fssm", "~> 0.2.5" platforms :mri_18 do gem "system_timer" gem "ruby-debug", ">= 0.10.3" + gem 'ruby-prof' +end + +platforms :mri_19 do + # TODO: Remove the conditional when ruby-debug19 supports Ruby >= 1.9.3 + gem "ruby-debug19", :require => 'ruby-debug' if RUBY_VERSION < "1.9.3" end platforms :ruby do + if ENV["RB_FSEVENT"] + gem 'rb-fsevent' + end gem 'json' gem 'yajl-ruby' - gem "nokogiri", ">= 1.4.3.1" + gem "nokogiri", ">= 1.4.4" # AR - gem "sqlite3-ruby", "~> 1.3.1", :require => 'sqlite3' + gem "sqlite3", "~> 1.3.3" group :db do gem "pg", ">= 0.9.0" gem "mysql", ">= 2.8.1" - gem "mysql2", :git => 'git://github.com/brianmario/mysql2.git' + gem "mysql2", :git => "git://github.com/brianmario/mysql2.git" end end @@ -44,17 +58,25 @@ platforms :jruby do gem "activerecord-jdbcsqlite3-adapter" + # This is needed by now to let tests work on JRuby + # TODO: When the JRuby guys merge jruby-openssl in + # jruby this will be removed + gem "jruby-openssl" + group :db do gem "activerecord-jdbcmysql-adapter" gem "activerecord-jdbcpostgresql-adapter" end end -env 'CI' do - gem "nokogiri", ">= 1.4.3.1" - - platforms :ruby_18 do - # fcgi gem doesn't compile on 1.9 - gem "fcgi", ">= 0.8.8" +# gems that are necessary for ActiveRecord tests with Oracle database +if ENV['ORACLE_ENHANCED_PATH'] || ENV['ORACLE_ENHANCED'] + platforms :ruby do + gem 'ruby-oci8', ">= 2.0.4" + end + if ENV['ORACLE_ENHANCED_PATH'] + gem 'activerecord-oracle_enhanced-adapter', :path => ENV['ORACLE_ENHANCED_PATH'] + else + gem "activerecord-oracle_enhanced-adapter", :git => "git://github.com/rsim/oracle-enhanced.git" end end diff --git a/RAILS_VERSION b/RAILS_VERSION index 7706a9b5ac..30afce968e 100644 --- a/RAILS_VERSION +++ b/RAILS_VERSION @@ -1 +1 @@ -3.0.0.rc +3.1.0.beta diff --git a/README.rdoc b/README.rdoc index 090a6bb68c..0b209cf56f 100644 --- a/README.rdoc +++ b/README.rdoc @@ -1,6 +1,6 @@ -== Welcome to Rails +== Welcome to \Rails -Rails is a web-application framework that includes everything needed to create +\Rails is a web-application framework that includes everything needed to create database-backed web applications according to the Model-View-Control pattern. This pattern splits the view (also called the presentation) into "dumb" @@ -11,30 +11,30 @@ persist themselves to a database. The controller handles the incoming requests (such as Save New Account, Update Product, Show Post) by manipulating the model and directing data to the view. -In Rails, the model is handled by what's called an object-relational mapping +In \Rails, the model is handled by what's called an object-relational mapping layer entitled Active Record. This layer allows you to present the data from database rows as objects and embellish these data objects with business logic -methods. You can read more about Active Record in -link:files/vendor/rails/activerecord/README.html. +methods. You can read more about Active Record in its +{README}[link:files/activerecord/README_rdoc.html]. The controller and view are handled by the Action Pack, which handles both layers by its two parts: Action View and Action Controller. These two layers are bundled in a single package due to their heavy interdependence. This is unlike the relationship between the Active Record and Action Pack that is much more separate. Each of these packages can be used independently outside of -Rails. You can read more about Action Pack in -link:files/vendor/rails/actionpack/README.html. +\Rails. You can read more about Action Pack in its +{README}[link:files/actionpack/README_rdoc.html]. == Getting Started -1. Install Rails at the command prompt if you haven't yet: +1. Install \Rails at the command prompt if you haven't yet: gem install rails -2. At the command prompt, create a new Rails application: +2. At the command prompt, create a new \Rails application: - rails new myapp + rails new myapp where "myapp" is the application name. @@ -48,20 +48,21 @@ link:files/vendor/rails/actionpack/README.html. "Welcome aboard: You're riding Ruby on Rails!" -5. Follow the guidelines to start developing your application. You can find -the following resources handy: +5. Follow the guidelines to start developing your application. You can find the following resources handy: * The README file created within your application. -* The {Getting Started Guide}[http://guides.rubyonrails.org/getting_started.html]. -* The {Ruby on Rails Tutorial Book}[http://railstutorial.org/book]. +* The {Getting Started with Rails}[http://guides.rubyonrails.org/getting_started.html]. +* The {Ruby on Rails Tutorial}[http://railstutorial.org/book]. +* The {Ruby on Rails Guides}[http://guides.rubyonrails.org]. +* The {API Documentation}[http://api.rubyonrails.org]. == Contributing -We encourage you to contribute to Ruby on Rails! Please check out the {Contributing to Rails +We encourage you to contribute to Ruby on \Rails! Please check out the {Contributing to Rails guide}[http://edgeguides.rubyonrails.org/contributing_to_rails.html] for guidelines about how to proceed. {Join us}[http://contributors.rubyonrails.org]! == License -Ruby on Rails is released under the MIT license. +Ruby on \Rails is released under the MIT license. diff --git a/Rakefile b/Rakefile old mode 100644 new mode 100755 index ceb0e832b3..607ce01fd0 --- a/Rakefile +++ b/Rakefile @@ -1,34 +1,16 @@ -gem 'rdoc', '>= 2.5.9' -require 'rdoc' +#!/usr/bin/env rake -require 'rake' require 'rdoc/task' -require 'rake/gempackagetask' +require 'net/http' -# RDoc skips some files in the Rails tree due to its binary? predicate. This is a quick -# hack for edge docs, until we decide which is the correct way to address this issue. -# If not fixed in RDoc itself, via an option or something, we should probably move this -# to railties and use it also in doc:rails. -def hijack_rdoc! - require "rdoc/parser" - class << RDoc::Parser - def binary?(file) - s = File.read(file, 1024) or return false +$:.unshift File.expand_path('..', __FILE__) +require "tasks/release" - if s[0, 2] == Marshal.dump('')[0, 2] then - true - elsif file =~ /erb\.rb$/ then - false - elsif s.index("\x00") then # ORIGINAL is s.scan(/<%|%>/).length >= 4 || s.index("\x00") - true - elsif 0.respond_to? :fdiv then - s.count("^ -~\t\r\n").fdiv(s.size) > 0.3 - else # HACK 1.8.6 - (s.count("^ -~\t\r\n").to_f / s.size) > 0.3 - end - end - end -end +desc "Build gem files for all projects" +task :build => "all:build" + +desc "Release all gems to gemcutter and create a tag" +task :release => "all:release" PROJECTS = %w(activesupport activemodel actionpack actionmailer activeresource activerecord railties) @@ -54,27 +36,6 @@ task :smoke do system %(cd activerecord && #{$0} sqlite3:isolated_test) end -spec = eval(File.read('rails.gemspec')) -Rake::GemPackageTask.new(spec) do |pkg| - pkg.gem_spec = spec -end - -desc "Release all gems to gemcutter. Package rails, package & push components, then push rails" -task :release => :release_projects do - require 'rake/gemcutter' - Rake::Gemcutter::Tasks.new(spec).define - Rake::Task['gem:push'].invoke -end - -desc "Release all components to gemcutter." -task :release_projects => :package do - errors = [] - PROJECTS.each do |project| - system(%(cd #{project} && #{$0} release)) || errors << project - end - fail("Errors in #{errors.join(', ')}") unless errors.empty? -end - desc "Install gems for all projects." task :install => :gem do version = File.read("RAILS_VERSION").strip @@ -88,8 +49,6 @@ end desc "Generate documentation for the Rails framework" RDoc::Task.new do |rdoc| - hijack_rdoc! - rdoc.rdoc_dir = 'doc/rdoc' rdoc.title = "Ruby on Rails Documentation" @@ -126,6 +85,7 @@ RDoc::Task.new do |rdoc| rdoc.rdoc_files.include('actionmailer/README.rdoc') rdoc.rdoc_files.include('actionmailer/CHANGELOG') rdoc.rdoc_files.include('actionmailer/lib/action_mailer/base.rb') + rdoc.rdoc_files.include('actionmailer/lib/action_mailer/mail_helper.rb') rdoc.rdoc_files.exclude('actionmailer/lib/action_mailer/vendor/*') rdoc.rdoc_files.include('activesupport/README.rdoc') @@ -144,12 +104,7 @@ task :rdoc do FileUtils.copy "activerecord/examples/associations.png", "doc/rdoc/files/examples/associations.png" end -desc "Publish API docs for Rails as a whole and for each component" -task :pdoc => :rdoc do - require 'rake/contrib/sshpublisher' - Rake::SshDirPublisher.new("rails@api.rubyonrails.org", "public_html/api", "doc/rdoc").upload -end - +desc 'Bump all versions to match version.rb' task :update_versions do require File.dirname(__FILE__) + "/version" @@ -177,3 +132,27 @@ task :update_versions do end end end + +# +# We have a webhook configured in Github that gets invoked after pushes. +# This hook triggers the following tasks: +# +# * updates the local checkout +# * updates Rails Contributors +# * generates and publishes edge docs +# * if there's a new stable tag, generates and publishes stable docs +# +# Everything is automated and you do NOT need to run this task normally. +# +# We publish a new version by tagging, and pushing a tag does not trigger +# that webhook. Stable docs would be updated by any subsequent regular +# push, but if you want that to happen right away just run this. +# +desc 'Publishes docs, run this AFTER a new stable tag has been pushed' +task :publish_docs do + Net::HTTP.new('rails-hooks.hashref.com').start do |http| + request = Net::HTTP::Post.new('/rails-master-hook') + response = http.request(request) + puts response.body + end +end diff --git a/actionmailer/CHANGELOG b/actionmailer/CHANGELOG index 76eab935e5..167c1da9c5 100644 --- a/actionmailer/CHANGELOG +++ b/actionmailer/CHANGELOG @@ -1,9 +1,16 @@ -*Rails 3.0.0 [release candidate] (July 26th, 2010)* +*Rails 3.1.0 (unreleased)* -* No material changes +* No changes +*Rails 3.0.2 (unreleased)* -*Rails 3.0.0 [beta 4] (June 8th, 2010)* +* No changes + +*Rails 3.0.1 (October 15, 2010)* + +* No Changes, just a version bump. + +*Rails 3.0.0 (August 29, 2010)* * subject is automatically looked up on I18n using mailer_name and action_name as scope as in t(".subject") [JK] @@ -11,16 +18,10 @@ * Added ability to pass Proc objects to the defaults hash [ML] - -*Rails 3.0.0 [beta 3] (April 13th, 2010)* - * Removed all quoting.rb type files from ActionMailer and put Mail 2.2.0 in instead [ML] * Lot of updates to various test cases that now work better with the new Mail and so have different expectations - -*Rails 3.0.0 [beta 2] (April 1st, 2010)* - * Added interceptors and observers from Mail [ML] ActionMailer::Base.register_interceptor calls Mail.register_interceptor @@ -38,9 +39,6 @@ * Whole new API added with tests. See base.rb for full details. Old API is deprecated. - -*Rails 3.0.0 [beta 1] (February 4, 2010)* - * The Mail::Message class has helped methods for all the field types that return 'common' defaults for the common use case, so to get the subject, mail.subject will give you a string, mail.date will give you a DateTime object, mail.from will give you an array of address specs (mikel@test.lindsaar.net) etc. If you want to access the field object itself, call mail[:field_name] which will return the field object you want, which you can then chain, like mail[:from].formatted * Mail#content_type now returns the content_type field as a string. If you want the mime type of a mail, then you call Mail#mime_type (eg, text/plain), if you want the parameters of the content type field, you call Mail#content_type_parameters which gives you a hash, eg {'format' => 'flowed', 'charset' => 'utf-8'} @@ -181,7 +179,7 @@ * ActionMailer::Base documentation rewrite. Closes #4991 [Kevin Clark, Marcel Molina Jr.] -* Replace alias method chaining with Module#alias_method_chain. [Marcel Molina Jr.] +* Replace alias method chaining with Module#alias_method_chain. [Marcel Molina Jr.] * Replace Ruby's deprecated append_features in favor of included. [Marcel Molina Jr.] @@ -327,7 +325,7 @@ * Added that deliver_* will now return the email that was sent -* Added that quoting to UTF-8 only happens if the characters used are in that range #955 [Jamis Buck] +* Added that quoting to UTF-8 only happens if the characters used are in that range #955 [Jamis Buck] * Fixed quoting for all address headers, not just to #955 [Jamis Buck] @@ -366,7 +364,7 @@ @body = "Nothing to see here." @charset = "iso-8859-1" end - + def unencoded_subject(recipient) @recipients = recipient @subject = "testing unencoded subject" @@ -375,7 +373,7 @@ @encode_subject = false @charset = "iso-8859-1" end - + *0.6.1* (January 18th, 2005) diff --git a/actionmailer/MIT-LICENSE b/actionmailer/MIT-LICENSE index a345a2419d..7ad1051066 100644 --- a/actionmailer/MIT-LICENSE +++ b/actionmailer/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2010 David Heinemeier Hansson +Copyright (c) 2004-2011 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionmailer/README.rdoc b/actionmailer/README.rdoc index b52c993f56..0fa18d751b 100644 --- a/actionmailer/README.rdoc +++ b/actionmailer/README.rdoc @@ -5,7 +5,7 @@ are used to consolidate code for sending out forgotten passwords, welcome wishes on signup, invoices for billing, and any other use case that requires a written notification to either a person or another system. -Action Mailer is in essence a wrapper around Action Controller and the +Action Mailer is in essence a wrapper around Action Controller and the Mail gem. It provides a way to make emails using templates in the same way that Action Controller renders views using templates. @@ -23,7 +23,7 @@ This can be as simple as: class Notifier < ActionMailer::Base delivers_from 'system@loudthinking.com' - + def welcome(recipient) @recipient = recipient mail(:to => recipient, @@ -36,13 +36,13 @@ ERb) that has the instance variables that are declared in the mailer action. So the corresponding body template for the method above could look like this: - Hello there, + Hello there, Mr. <%= @recipient %> Thank you for signing up! - -And if the recipient was given as "david@loudthinking.com", the email + +And if the recipient was given as "david@loudthinking.com", the email generated would look like this: Date: Mon, 25 Jan 2010 22:48:09 +1100 @@ -55,18 +55,18 @@ generated would look like this: charset="US-ASCII"; Content-Transfer-Encoding: 7bit - Hello there, + Hello there, Mr. david@loudthinking.com -In previous version of rails you would call create_method_name and +In previous version of Rails you would call create_method_name and deliver_method_name. Rails 3.0 has a much simpler interface, you simply call the method and optionally call +deliver+ on the return value. Calling the method returns a Mail Message object: - message = Notifier.welcome #=> Returns a Mail::Message object - message.deliver #=> delivers the email + message = Notifier.welcome # => Returns a Mail::Message object + message.deliver # => delivers the email Or you can just chain the methods together like: @@ -74,10 +74,10 @@ Or you can just chain the methods together like: == Receiving emails -To receive emails, you need to implement a public instance method called receive that takes a -tmail object as its single parameter. The Action Mailer framework has a corresponding class method, +To receive emails, you need to implement a public instance method called receive that takes an +email object as its single parameter. The Action Mailer framework has a corresponding class method, which is also called receive, that accepts a raw, unprocessed email as a string, which it then turns -into the tmail object and calls the receive instance method. +into the email object and calls the receive instance method. Example: @@ -90,7 +90,7 @@ Example: if email.has_attachments? for attachment in email.attachments - page.attachments.create({ + page.attachments.create({ :file => attachment, :description => email.subject }) end @@ -98,13 +98,13 @@ Example: end end -This Mailman can be the target for Postfix or other MTAs. In Rails, you would use the runner in the +This Mailman can be the target for Postfix or other MTAs. In Rails, you would use the runner in the trivial case like this: rails runner 'Mailman.receive(STDIN.read)' -However, invoking Rails in the runner for each mail to be received is very resource intensive. A single -instance of Rails should be run within a daemon if it is going to be utilized to process more than just +However, invoking Rails in the runner for each mail to be received is very resource intensive. A single +instance of Rails should be run within a daemon, if it is going to be utilized to process more than just a limited number of email. == Configuration @@ -128,7 +128,7 @@ The latest version of Action Mailer can be installed with Rubygems: Source code can be downloaded as part of the Rails project on GitHub -* http://github.com/rails/rails/tree/master/actionmailer/ +* https://github.com/rails/rails/tree/master/actionmailer/ == License @@ -145,3 +145,4 @@ API documentation is at Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here: * https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets + diff --git a/actionmailer/Rakefile b/actionmailer/Rakefile old mode 100644 new mode 100755 index a47426bd07..df996acbc2 --- a/actionmailer/Rakefile +++ b/actionmailer/Rakefile @@ -1,8 +1,5 @@ -gem 'rdoc', '>= 2.5.9' -require 'rdoc' -require 'rake' +#!/usr/bin/env rake require 'rake/testtask' -require 'rdoc/task' require 'rake/packagetask' require 'rake/gempackagetask' @@ -20,24 +17,11 @@ namespace :test do task :isolated do ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME')) Dir.glob("test/**/*_test.rb").all? do |file| - system(ruby, '-Ilib:test', file) + sh(ruby, '-Ilib:test', file) end or raise "Failures" end end -# Generate the RDoc documentation -RDoc::Task.new { |rdoc| - rdoc.rdoc_dir = 'doc' - rdoc.title = "Action Mailer -- Easy email delivery and testing" - rdoc.options << '--charset' << 'utf-8' - rdoc.options << '-f' << 'horo' - rdoc.options << '--main' << 'README.rdoc' - rdoc.rdoc_files.include('README.rdoc', 'CHANGELOG') - rdoc.rdoc_files.include('lib/action_mailer.rb') - rdoc.rdoc_files.include('lib/action_mailer/*.rb') - rdoc.rdoc_files.include('lib/action_mailer/delivery_method/*.rb') -} - spec = eval(File.read('actionmailer.gemspec')) Rake::GemPackageTask.new(spec) do |p| diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec index daf30e434a..2ae85f8b57 100644 --- a/actionmailer/actionmailer.gemspec +++ b/actionmailer/actionmailer.gemspec @@ -20,5 +20,5 @@ Gem::Specification.new do |s| s.has_rdoc = true s.add_dependency('actionpack', version) - s.add_dependency('mail', '~> 2.2.5') + s.add_dependency('mail', '~> 2.2.15') end diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb index 6e2d288082..b9e682b711 100644 --- a/actionmailer/lib/action_mailer.rb +++ b/actionmailer/lib/action_mailer.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2010 David Heinemeier Hansson +# Copyright (c) 2004-2011 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -26,6 +26,7 @@ $:.unshift(actionpack_path) if File.directory?(actionpack_path) && !$:.include?( require 'abstract_controller' require 'action_view' +require 'action_mailer/version' # Common Active Support usage in Action Mailer require 'active_support/core_ext/class' @@ -43,7 +44,6 @@ module ActionMailer autoload :Collector autoload :Base autoload :DeliveryMethods - autoload :DeprecatedApi autoload :MailHelper autoload :OldApi autoload :TestCase diff --git a/actionmailer/lib/action_mailer/adv_attr_accessor.rb b/actionmailer/lib/action_mailer/adv_attr_accessor.rb index be6b1feca9..c1aa8021ce 100644 --- a/actionmailer/lib/action_mailer/adv_attr_accessor.rb +++ b/actionmailer/lib/action_mailer/adv_attr_accessor.rb @@ -1,26 +1,28 @@ module ActionMailer module AdvAttrAccessor #:nodoc: - def adv_attr_accessor(*names) - names.each do |name| - ivar = "@#{name}" + def adv_attr_accessor(name, deprecation=nil) + ivar = "@#{name}" + deprecation ||= "Please pass :#{name} as hash key to mail() instead" - class_eval <<-ACCESSORS, __FILE__, __LINE__ + 1 - def #{name}=(value) - #{ivar} = value + class_eval <<-ACCESSORS, __FILE__, __LINE__ + 1 + def #{name}=(value) + ActiveSupport::Deprecation.warn "#{name}= is deprecated. #{deprecation}" + #{ivar} = value + end + + def #{name}(*args) + raise ArgumentError, "expected 0 or 1 parameters" unless args.length <= 1 + if args.empty? + ActiveSupport::Deprecation.warn "#{name}() is deprecated and will be removed in future versions." + #{ivar} if instance_variable_names.include?(#{ivar.inspect}) + else + ActiveSupport::Deprecation.warn "#{name}(value) is deprecated. #{deprecation}" + #{ivar} = args.first end + end + ACCESSORS - def #{name}(*args) - raise ArgumentError, "expected 0 or 1 parameters" unless args.length <= 1 - if args.empty? - #{ivar} if instance_variable_names.include?(#{ivar.inspect}) - else - #{ivar} = args.first - end - end - ACCESSORS - - self.protected_instance_variables << ivar if self.respond_to?(:protected_instance_variables) - end + self.protected_instance_variables << ivar if self.respond_to?(:protected_instance_variables) end end end diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index f742f982f2..16fcf112b7 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -187,31 +187,31 @@ module ActionMailer #:nodoc: # with the filename +free_book.pdf+. # # = Inline Attachments - # - # You can also specify that a file should be displayed inline with other HTML. This is useful + # + # You can also specify that a file should be displayed inline with other HTML. This is useful # if you want to display a corporate logo or a photo. - # + # # class ApplicationMailer < ActionMailer::Base # def welcome(recipient) # attachments.inline['photo.png'] = File.read('path/to/photo.png') # mail(:to => recipient, :subject => "Here is what we look like") # end # end - # + # # And then to reference the image in the view, you create a welcome.html.erb file and - # make a call to +image_tag+ passing in the attachment you want to display and then call + # make a call to +image_tag+ passing in the attachment you want to display and then call # +url+ on the attachment to get the relative content id path for the image source: - # + # #

Please Don't Cringe

- # + # # <%= image_tag attachments['photo.png'].url -%> - # + # # As we are using Action View's +image_tag+ method, you can pass in any other options you want: - # + # #

Please Don't Cringe

- # + # # <%= image_tag attachments['photo.png'].url, :alt => 'Our Photo', :class => 'photo' -%> - # + # # = Observing and Intercepting Mails # # Action Mailer provides hooks into the Mail observer and interceptor methods. These allow you to @@ -222,7 +222,7 @@ module ActionMailer #:nodoc: # # An interceptor object must implement the :delivering_email(message) method which will be # called before the email is sent, allowing you to make modifications to the email before it hits - # the delivery agents. Your object should make and needed modifications directly to the passed + # the delivery agents. Your object should make any needed modifications directly to the passed # in Mail::Message instance. # # = Default Hash @@ -234,8 +234,8 @@ module ActionMailer #:nodoc: # default :sender => 'system@example.com' # end # - # You can pass in any header value that a Mail::Message, out of the box, ActionMailer::Base - # sets the following: + # You can pass in any header value that a Mail::Message accepts. Out of the box, + # ActionMailer::Base sets the following: # # * :mime_version => "1.0" # * :charset => "UTF-8", @@ -246,7 +246,7 @@ module ActionMailer #:nodoc: # but Action Mailer translates them appropriately and sets the correct values. # # As you can pass in any header, you need to either quote the header as a string, or pass it in as - # an underscorised symbol, so the following will work: + # an underscored symbol, so the following will work: # # class Notifier < ActionMailer::Base # default 'Content-Transfer-Encoding' => '7bit', @@ -273,7 +273,7 @@ module ActionMailer #:nodoc: # = Configuration options # # These options are specified on the class level, like - # ActionMailer::Base.template_root = "/my/templates" + # ActionMailer::Base.raise_delivery_errors = true # # * default - You can pass this in at a class level as well as within the class itself as # per the above section. @@ -290,13 +290,15 @@ module ActionMailer #:nodoc: # * :password - If your mail server requires authentication, set the password in this setting. # * :authentication - If your mail server requires authentication, you need to specify the # authentication type here. - # This is a symbol and one of :plain, :login, :cram_md5. + # This is a symbol and one of :plain (will send the password in the clear), :login (will + # send password Base64 encoded) or :cram_md5 (combines a Challenge/Response mechanism to exchange + # information and a cryptographic Message Digest 5 algorithm to hash important information) # * :enable_starttls_auto - When set to true, detects if STARTTLS is enabled in your SMTP server # and starts to use it. # # * sendmail_settings - Allows you to override options for the :sendmail delivery method. # * :location - The location of the sendmail executable. Defaults to /usr/sbin/sendmail. - # * :arguments - The command line arguments. Defaults to -i -t with -f sender@addres + # * :arguments - The command line arguments. Defaults to -i -t with -f sender@address # added automatically before the message is sent. # # * file_settings - Allows you to override options for the :file delivery method. @@ -341,10 +343,11 @@ module ActionMailer #:nodoc: include AbstractController::Translation include AbstractController::AssetPaths - helper ActionMailer::MailHelper + cattr_reader :protected_instance_variables + @@protected_instance_variables = [] + helper ActionMailer::MailHelper include ActionMailer::OldApi - include ActionMailer::DeprecatedApi delegate :register_observer, :to => Mail delegate :register_interceptor, :to => Mail @@ -360,7 +363,6 @@ module ActionMailer #:nodoc: }.freeze class << self - def mailer_name @mailer_name ||= name.underscore end @@ -402,14 +404,14 @@ module ActionMailer #:nodoc: end end - def respond_to?(method, *args) #:nodoc: + def respond_to?(method, include_private = false) #:nodoc: super || action_methods.include?(method.to_s) end protected def set_payload_for_mail(payload, mail) #:nodoc: - payload[:mailer] = self.name + payload[:mailer] = name payload[:message_id] = mail.message_id payload[:subject] = mail.subject payload[:to] = mail.to @@ -421,11 +423,8 @@ module ActionMailer #:nodoc: end def method_missing(method, *args) #:nodoc: - if action_methods.include?(method.to_s) - new(method, *args).message - else - super - end + return super unless respond_to?(method) + new(method, *args).message end end @@ -446,6 +445,10 @@ module ActionMailer #:nodoc: super end + def mailer_name + self.class.mailer_name + end + # Allows you to pass random and unusual headers to the new +Mail::Message+ object # which will add them to itself. # @@ -690,15 +693,8 @@ module ActionMailer #:nodoc: end def each_template(paths, name, &block) #:nodoc: - Array.wrap(paths).each do |path| - templates = lookup_context.find_all(name, path) - templates = templates.uniq_by { |t| t.formats } - - unless templates.empty? - templates.each(&block) - return - end - end + templates = lookup_context.find_all(name, Array.wrap(paths)) + templates.uniq_by { |t| t.formats }.each(&block) end def create_parts_from_responses(m, responses) #:nodoc: @@ -720,28 +716,6 @@ module ActionMailer #:nodoc: container.add_part(part) end - module DeprecatedUrlOptions - def default_url_options - deprecated_url_options - end - - def default_url_options=(val) - deprecated_url_options - end - - def deprecated_url_options - raise "You can no longer call ActionMailer::Base.default_url_options " \ - "directly. You need to set config.action_mailer.default_url_options. " \ - "If you are using ActionMailer standalone, you need to include the " \ - "routing url_helpers directly." - end - end - - # This module will complain if the user tries to set default_url_options - # directly instead of through the config object. In Action Mailer's Railtie, - # we include the router's url_helpers, which will override this module. - extend DeprecatedUrlOptions - ActiveSupport.run_load_hooks(:action_mailer, self) end end diff --git a/actionmailer/lib/action_mailer/delivery_methods.rb b/actionmailer/lib/action_mailer/delivery_methods.rb index 043794bb12..b324ba790d 100644 --- a/actionmailer/lib/action_mailer/delivery_methods.rb +++ b/actionmailer/lib/action_mailer/delivery_methods.rb @@ -46,11 +46,11 @@ module ActionMailer # as alias and the default options supplied: # # Example: - # + # # add_delivery_method :sendmail, Mail::Sendmail, # :location => '/usr/sbin/sendmail', # :arguments => '-i -t' - # + # def add_delivery_method(symbol, klass, default_options={}) class_attribute(:"#{symbol}_settings") unless respond_to?(:"#{symbol}_settings") send(:"#{symbol}_settings=", default_options) diff --git a/actionmailer/lib/action_mailer/deprecated_api.rb b/actionmailer/lib/action_mailer/deprecated_api.rb deleted file mode 100644 index 7d57feba04..0000000000 --- a/actionmailer/lib/action_mailer/deprecated_api.rb +++ /dev/null @@ -1,141 +0,0 @@ -require 'active_support/core_ext/object/try' - -module ActionMailer - # This is the API which is deprecated and is going to be removed on Rails 3.1 release. - # Part of the old API will be deprecated after 3.1, for a smoother deprecation process. - # Check those in OldApi instead. - module DeprecatedApi #:nodoc: - extend ActiveSupport::Concern - - included do - [:charset, :content_type, :mime_version, :implicit_parts_order].each do |method| - class_eval <<-FILE, __FILE__, __LINE__ + 1 - def self.default_#{method} - @@default_#{method} - end - - def self.default_#{method}=(value) - ActiveSupport::Deprecation.warn "ActionMailer::Base.default_#{method}=value is deprecated, " << - "use default :#{method} => value instead" - @@default_#{method} = value - end - - @@default_#{method} = nil - FILE - end - end - - module ClassMethods - # Deliver the given mail object directly. This can be used to deliver - # a preconstructed mail object, like: - # - # email = MyMailer.create_some_mail(parameters) - # email.set_some_obscure_header "frobnicate" - # MyMailer.deliver(email) - def deliver(mail, show_warning=true) - if show_warning - ActiveSupport::Deprecation.warn "#{self}.deliver is deprecated, call " << - "deliver in the mailer instance instead", caller[0,2] - end - - raise "no mail object available for delivery!" unless mail - wrap_delivery_behavior(mail) - mail.deliver - mail - end - - def template_root - self.view_paths && self.view_paths.first - end - - def template_root=(root) - ActiveSupport::Deprecation.warn "template_root= is deprecated, use prepend_view_path instead", caller[0,2] - self.view_paths = ActionView::Base.process_view_paths(root) - end - - def respond_to?(method_symbol, include_private = false) - matches_dynamic_method?(method_symbol) || super - end - - def method_missing(method_symbol, *parameters) - if match = matches_dynamic_method?(method_symbol) - case match[1] - when 'create' - ActiveSupport::Deprecation.warn "#{self}.create_#{match[2]} is deprecated, " << - "use #{self}.#{match[2]} instead", caller[0,2] - new(match[2], *parameters).message - when 'deliver' - ActiveSupport::Deprecation.warn "#{self}.deliver_#{match[2]} is deprecated, " << - "use #{self}.#{match[2]}.deliver instead", caller[0,2] - new(match[2], *parameters).message.deliver - else super - end - else - super - end - end - - private - - def matches_dynamic_method?(method_name) - method_name = method_name.to_s - /^(create|deliver)_([_a-z]\w*)/.match(method_name) || /^(new)$/.match(method_name) - end - end - - # Delivers a Mail object. By default, it delivers the cached mail - # object (from the create! method). If no cached mail object exists, and - # no alternate has been given as the parameter, this will fail. - def deliver!(mail = @_message) - ActiveSupport::Deprecation.warn "Calling deliver in the AM::Base object is deprecated, " << - "please call deliver in the Mail instance", caller[0,2] - self.class.deliver(mail, false) - end - alias :deliver :deliver! - - def render(*args) - options = args.last.is_a?(Hash) ? args.last : {} - - if options[:body].is_a?(Hash) - ActiveSupport::Deprecation.warn(':body in render deprecated. Please use instance ' << - 'variables as assigns instead', caller[0,1]) - - options[:body].each { |k,v| instance_variable_set(:"@#{k}", v) } - end - super - end - - # Render a message but does not set it as mail body. Useful for rendering - # data for part and attachments. - # - # Examples: - # - # render_message "special_message" - # render_message :template => "special_message" - # render_message :inline => "<%= 'Hi!' %>" - # - def render_message(*args) - ActiveSupport::Deprecation.warn "render_message is deprecated, use render instead", caller[0,2] - render(*args) - end - - private - - def initialize_defaults(*) - @charset ||= self.class.default_charset.try(:dup) - @content_type ||= self.class.default_content_type.try(:dup) - @implicit_parts_order ||= self.class.default_implicit_parts_order.try(:dup) - @mime_version ||= self.class.default_mime_version.try(:dup) - super - end - - def create_parts - if @body.is_a?(Hash) && !@body.empty? - ActiveSupport::Deprecation.warn "Giving a hash to body is deprecated, please use instance variables instead", caller[0,2] - @body.each { |k, v| instance_variable_set(:"@#{k}", v) } - end - super - end - - end -end diff --git a/actionmailer/lib/action_mailer/mail_helper.rb b/actionmailer/lib/action_mailer/mail_helper.rb index b708881edf..6f22adc479 100644 --- a/actionmailer/lib/action_mailer/mail_helper.rb +++ b/actionmailer/lib/action_mailer/mail_helper.rb @@ -3,23 +3,14 @@ module ActionMailer # Uses Text::Format to take the text and format it, indented two spaces for # each line, and wrapped at 72 columns. def block_format(text) - begin - require 'text/format' - rescue LoadError => e - $stderr.puts "You don't have text-format installed in your application. Please add it to your Gemfile and run bundle install" - raise e - end unless defined?(Text::Format) - formatted = text.split(/\n\r\n/).collect { |paragraph| - Text::Format.new( - :columns => 72, :first_indent => 2, :body_indent => 2, :text => paragraph - ).format + format_paragraph(paragraph) }.join("\n") - + # Make list points stand on their own line formatted.gsub!(/[ ]*([*]+) ([^*]*)/) { |s| " #{$1} #{$2.strip}\n" } formatted.gsub!(/[ ]*([#]+) ([^#]*)/) { |s| " #{$1} #{$2.strip}\n" } - + formatted end @@ -37,5 +28,29 @@ module ActionMailer def attachments @_message.attachments end + + # Returns +text+ wrapped at +len+ columns and indented +indent+ spaces. + # + # === Examples + # + # my_text = "Here is a sample text with more than 40 characters" + # + # format_paragraph(my_text, 25, 4) + # # => " Here is a sample text with\n more than 40 characters" + def format_paragraph(text, len = 72, indent = 2) + sentences = [[]] + + text.split.each do |word| + if (sentences.last + [word]).join(' ').length > len + sentences << [word] + else + sentences.last << word + end + end + + sentences.map { |sentence| + "#{" " * indent}#{sentence.join(' ')}" + }.join "\n" + end end end diff --git a/actionmailer/lib/action_mailer/old_api.rb b/actionmailer/lib/action_mailer/old_api.rb index 79d024a350..04728cafb0 100644 --- a/actionmailer/lib/action_mailer/old_api.rb +++ b/actionmailer/lib/action_mailer/old_api.rb @@ -8,9 +8,7 @@ module ActionMailer included do extend ActionMailer::AdvAttrAccessor - - @@protected_instance_variables = %w(@parts) - cattr_reader :protected_instance_variables + self.protected_instance_variables.concat %w(@parts @mail_was_called) # Specify the BCC addresses for the message adv_attr_accessor :bcc @@ -42,11 +40,11 @@ module ActionMailer # The recipient addresses for the message, either as a string (for a single # address) or an array (for multiple addresses). - adv_attr_accessor :recipients + adv_attr_accessor :recipients, "Please pass :to as hash key to mail() instead" # The date on which the message was sent. If not set (the default), the # header will be set by the delivery agent. - adv_attr_accessor :sent_on + adv_attr_accessor :sent_on, "Please pass :date as hash key to mail() instead" # Specify the subject of the message. adv_attr_accessor :subject @@ -54,20 +52,12 @@ module ActionMailer # Specify the template name to use for current message. This is the "base" # template name, without the extension or directory, and may be used to # have multiple mailer methods share the same template. - adv_attr_accessor :template - - # Override the mailer name, which defaults to an inflected version of the - # mailer's class name. If you want to use a template in a non-standard - # location, you can use this to specify that location. - adv_attr_accessor :mailer_name + adv_attr_accessor :template, "Please pass :template_name or :template_path as hash key to mail() instead" # Define the body of the message. This is either a Hash (in which case it # specifies the variables to pass to the template when it is rendered), # or a string, in which case it specifies the actual text of the message. adv_attr_accessor :body - - # Alias controller_path to mailer_name so render :partial in views work. - alias :controller_path :mailer_name end def process(method_name, *args) @@ -84,6 +74,8 @@ module ActionMailer # part itself is yielded to the block so that other properties (charset, # body, headers, etc.) can be set on it. def part(params) + ActiveSupport::Deprecation.warn "part() is deprecated and will be removed in future versions. " << + "Please pass a block to mail() instead." params = {:content_type => params} if String === params if custom_headers = params.delete(:headers) @@ -99,6 +91,8 @@ module ActionMailer # Add an attachment to a multipart message. This is simply a part with the # content-disposition set to "attachment". def attachment(params, &block) + ActiveSupport::Deprecation.warn "attachment() is deprecated and will be removed in future versions. " << + "Please use the attachments[] API instead." params = { :content_type => params } if String === params params[:content] ||= params.delete(:data) || params.delete(:body) @@ -116,43 +110,43 @@ module ActionMailer def normalize_nonfile_hash(params) content_disposition = "attachment;" - + mime_type = params.delete(:mime_type) - + if content_type = params.delete(:content_type) content_type = "#{mime_type || content_type};" end params[:body] = params.delete(:data) if params[:data] - + { :content_type => content_type, :content_disposition => content_disposition }.merge(params) end - + def normalize_file_hash(params) filename = File.basename(params.delete(:filename)) content_disposition = "attachment; filename=\"#{File.basename(filename)}\"" - + mime_type = params.delete(:mime_type) - + if (content_type = params.delete(:content_type)) && (content_type !~ /filename=/) content_type = "#{mime_type || content_type}; filename=\"#{filename}\"" end - + params[:body] = params.delete(:data) if params[:data] - + { :content_type => content_type, :content_disposition => content_disposition }.merge(params) end - def create_mail + def create_mail m = @_message - set_fields!({:subject => subject, :to => recipients, :from => from, - :bcc => bcc, :cc => cc, :reply_to => reply_to}, charset) + set_fields!({:subject => @subject, :to => @recipients, :from => @from, + :bcc => @bcc, :cc => @cc, :reply_to => @reply_to}, @charset) - m.mime_version = mime_version unless mime_version.nil? - m.date = sent_on.to_time rescue sent_on if sent_on + m.mime_version = @mime_version if @mime_version + m.date = @sent_on.to_time rescue @sent_on if @sent_on @headers.each { |k, v| m[k] = v } @@ -178,19 +172,21 @@ module ActionMailer wrap_delivery_behavior! m.content_transfer_encoding = '8bit' unless m.body.only_us_ascii? - + @_message end - + # Set up the default values for the various instance variables of this # mailer. Subclasses may override this method to provide different # defaults. - def initialize_defaults(method_name) + def initialize_defaults(method_name) @charset ||= self.class.default[:charset].try(:dup) @content_type ||= self.class.default[:content_type].try(:dup) @implicit_parts_order ||= self.class.default[:parts_order].try(:dup) @mime_version ||= self.class.default[:mime_version].try(:dup) + @cc, @bcc, @reply_to, @subject, @from, @recipients = nil, nil, nil, nil, nil, nil + @mailer_name ||= self.class.mailer_name.dup @template ||= method_name @mail_was_called = false @@ -201,11 +197,11 @@ module ActionMailer @body ||= {} end - def create_parts + def create_parts if String === @body @parts.unshift create_inline_part(@body) elsif @parts.empty? || @parts.all? { |p| p.content_disposition =~ /^attachment/ } - lookup_context.find_all(@template, @mailer_name).each do |template| + lookup_context.find_all(@template, [@mailer_name]).each do |template| self.formats = template.formats @parts << create_inline_part(render(:template => template), template.mime_type) end @@ -216,11 +212,11 @@ module ActionMailer # If this is a multipart e-mail add the mime_version if it is not # already set. - @mime_version ||= "1.0" if !@parts.empty? + @mime_version ||= "1.0" unless @parts.empty? end end - def create_inline_part(body, mime_type=nil) + def create_inline_part(body, mime_type=nil) ct = mime_type || "text/plain" main_type, sub_type = split_content_type(ct.to_s) @@ -242,17 +238,17 @@ module ActionMailer m.reply_to ||= headers.delete(:reply_to) if headers[:reply_to] end - def split_content_type(ct) + def split_content_type(ct) ct.to_s.split("/") end - def parse_content_type(defaults=nil) + def parse_content_type if @content_type.blank? [ nil, {} ] else ctype, *attrs = @content_type.split(/;\s*/) - attrs = attrs.inject({}) { |h,s| k,v = s.split(/\=/, 2); h[k] = v; h } - [ctype, {"charset" => @charset}.merge(attrs)] + attrs = Hash[attrs.map { |attr| attr.split(/=/, 2) }] + [ctype, {"charset" => @charset}.merge!(attrs)] end end end diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb index ce6d8cc5b5..4ec478067f 100644 --- a/actionmailer/lib/action_mailer/railtie.rb +++ b/actionmailer/lib/action_mailer/railtie.rb @@ -1,5 +1,6 @@ require "action_mailer" require "rails" +require "abstract_controller/railties/routes_helpers" module ActionMailer class Railtie < Rails::Railtie @@ -13,14 +14,26 @@ module ActionMailer paths = app.config.paths options = app.config.action_mailer - options.assets_dir ||= paths.public.to_a.first - options.javascripts_dir ||= paths.public.javascripts.to_a.first - options.stylesheets_dir ||= paths.public.stylesheets.to_a.first + options.assets_dir ||= paths["public"].first + options.javascripts_dir ||= paths["public/javascripts"].first + options.stylesheets_dir ||= paths["public/stylesheets"].first + + # make sure readers methods get compiled + options.asset_path ||= app.config.asset_path + options.asset_host ||= app.config.asset_host ActiveSupport.on_load(:action_mailer) do - include app.routes.url_helpers + include AbstractController::UrlFor + extend ::AbstractController::Railties::RoutesHelpers.with(app.routes) + include app.routes.mounted_helpers options.each { |k,v| send("#{k}=", v) } end end + + initializer "action_mailer.compile_config_methods" do + ActiveSupport.on_load(:action_mailer) do + config.compile_methods! if config.respond_to?(:compile_methods!) + end + end end -end \ No newline at end of file +end diff --git a/actionmailer/lib/action_mailer/test_case.rb b/actionmailer/lib/action_mailer/test_case.rb index f4d1bb59f1..63e18147f6 100644 --- a/actionmailer/lib/action_mailer/test_case.rb +++ b/actionmailer/lib/action_mailer/test_case.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/class/attribute' + module ActionMailer class NonInferrableMailerError < ::StandardError def initialize(name) @@ -15,11 +17,11 @@ module ActionMailer module ClassMethods def tests(mailer) - write_inheritable_attribute(:mailer_class, mailer) + self._mailer_class = mailer end def mailer_class - if mailer = read_inheritable_attribute(:mailer_class) + if mailer = self._mailer_class mailer else tests determine_default_mailer(name) @@ -65,6 +67,7 @@ module ActionMailer end included do + class_attribute :_mailer_class setup :initialize_test_deliveries setup :set_expected_mail end diff --git a/actionmailer/lib/action_mailer/tmail_compat.rb b/actionmailer/lib/action_mailer/tmail_compat.rb index 26962f972f..1b2cdcfb27 100644 --- a/actionmailer/lib/action_mailer/tmail_compat.rb +++ b/actionmailer/lib/action_mailer/tmail_compat.rb @@ -1,17 +1,19 @@ module Mail class Message - + def set_content_type(*args) - ActiveSupport::Deprecation.warn('Message#set_content_type is deprecated, please just call ' << - 'Message#content_type with the same arguments', caller[0,2]) + message = 'Message#set_content_type is deprecated, please just call ' << + 'Message#content_type with the same arguments' + ActiveSupport::Deprecation.warn(message, caller[0,2]) content_type(*args) end - + alias :old_transfer_encoding :transfer_encoding def transfer_encoding(value = nil) if value - ActiveSupport::Deprecation.warn('Message#transfer_encoding is deprecated, please call ' << - 'Message#content_transfer_encoding with the same arguments', caller[0,2]) + message = 'Message#transfer_encoding is deprecated, ' << + 'please call Message#content_transfer_encoding with the same arguments' + ActiveSupport::Deprecation.warn(message, caller[0,2]) content_transfer_encoding(value) else old_transfer_encoding @@ -19,16 +21,17 @@ module Mail end def transfer_encoding=(value) - ActiveSupport::Deprecation.warn('Message#transfer_encoding= is deprecated, please call ' << - 'Message#content_transfer_encoding= with the same arguments', caller[0,2]) + message = 'Message#transfer_encoding= is deprecated, ' << + 'please call Message#content_transfer_encoding= with the same arguments' + ActiveSupport::Deprecation.warn(message, caller[0,2]) self.content_transfer_encoding = value end def original_filename - ActiveSupport::Deprecation.warn('Message#original_filename is deprecated, ' << - 'please call Message#filename', caller[0,2]) + message = 'Message#original_filename is deprecated, please call Message#filename' + ActiveSupport::Deprecation.warn(message, caller[0,2]) filename end - + end -end \ No newline at end of file +end diff --git a/actionmailer/lib/action_mailer/version.rb b/actionmailer/lib/action_mailer/version.rb index 805c89be1d..27eb6c2b9b 100644 --- a/actionmailer/lib/action_mailer/version.rb +++ b/actionmailer/lib/action_mailer/version.rb @@ -1,10 +1,10 @@ module ActionMailer module VERSION #:nodoc: MAJOR = 3 - MINOR = 0 + MINOR = 1 TINY = 0 - BUILD = "rc" + PRE = "beta" - STRING = [MAJOR, MINOR, TINY, BUILD].join('.') + STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end end diff --git a/actionmailer/lib/rails/generators/mailer/USAGE b/actionmailer/lib/rails/generators/mailer/USAGE index a08d459739..448ddbd5df 100644 --- a/actionmailer/lib/rails/generators/mailer/USAGE +++ b/actionmailer/lib/rails/generators/mailer/USAGE @@ -1,4 +1,5 @@ Description: +============ Stubs out a new mailer and its views. Pass the mailer name, either CamelCased or under_scored, and an optional list of emails as arguments. @@ -6,10 +7,12 @@ Description: engine and test framework generators. Example: - `rails generate mailer Notifications signup forgot_password invoice` +======== + rails generate mailer Notifications signup forgot_password invoice creates a Notifications mailer class, views, test, and fixtures: Mailer: app/mailers/notifications.rb Views: app/views/notifications/signup.erb [...] Test: test/functional/notifications_test.rb Fixtures: test/fixtures/notifications/signup [...] + diff --git a/actionmailer/lib/rails/generators/mailer/templates/mailer.rb b/actionmailer/lib/rails/generators/mailer/templates/mailer.rb index 21e5918ecb..370a508cad 100644 --- a/actionmailer/lib/rails/generators/mailer/templates/mailer.rb +++ b/actionmailer/lib/rails/generators/mailer/templates/mailer.rb @@ -1,3 +1,4 @@ +<% module_namespacing do -%> class <%= class_name %> < ActionMailer::Base default :from => "from@example.com" <% for action in actions -%> @@ -5,7 +6,7 @@ class <%= class_name %> < ActionMailer::Base # Subject can be set in your I18n file at config/locales/en.yml # with the following lookup: # - # en.<%= file_name %>.<%= action %>.subject + # en.<%= file_path.gsub("/",".") %>.<%= action %>.subject # def <%= action %> @greeting = "Hi" @@ -14,3 +15,4 @@ class <%= class_name %> < ActionMailer::Base end <% end -%> end +<% end -%> diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb index ea15709b45..ce664bf301 100644 --- a/actionmailer/test/abstract_unit.rb +++ b/actionmailer/test/abstract_unit.rb @@ -11,11 +11,20 @@ ensure $VERBOSE = old end - require 'active_support/core_ext/kernel/reporting' + +require 'active_support/core_ext/string/encoding' +if "ruby".encoding_aware? + # These are the normal settings that will be set up by Railties + # TODO: Have these tests support other combinations of these values + silence_warnings do + Encoding.default_internal = "UTF-8" + Encoding.default_external = "UTF-8" + end +end + silence_warnings do # These external dependencies have warnings :/ - require 'text/format' require 'mail' end @@ -67,4 +76,6 @@ end def restore_delivery_method ActionMailer::Base.delivery_method = @old_delivery_method -end \ No newline at end of file +end + +ActiveSupport::Deprecation.silenced = true diff --git a/actionmailer/test/old_base/asset_host_test.rb b/actionmailer/test/asset_host_test.rb similarity index 85% rename from actionmailer/test/old_base/asset_host_test.rb rename to actionmailer/test/asset_host_test.rb index cc13c8a4d7..b24eca5fbb 100644 --- a/actionmailer/test/old_base/asset_host_test.rb +++ b/actionmailer/test/asset_host_test.rb @@ -3,9 +3,9 @@ require 'action_controller' class AssetHostMailer < ActionMailer::Base def email_with_asset - recipients 'test@localhost' - subject "testing email containing asset path while asset_host is set" - from "tester@example.com" + mail :to => 'test@localhost', + :subject => 'testing email containing asset path while asset_host is set', + :from => 'tester@example.com' end end @@ -29,7 +29,7 @@ class AssetHostTest < Test::Unit::TestCase assert_equal %Q{Somelogo}, mail.body.to_s.strip end - def test_asset_host_as_one_arguement_proc + def test_asset_host_as_one_argument_proc AssetHostMailer.config.asset_host = Proc.new { |source| if source.starts_with?('/images') "http://images.example.com" @@ -41,7 +41,7 @@ class AssetHostTest < Test::Unit::TestCase assert_equal %Q{Somelogo}, mail.body.to_s.strip end - def test_asset_host_as_two_arguement_proc + def test_asset_host_as_two_argument_proc ActionController::Base.config.asset_host = Proc.new {|source,request| if request && request.ssl? "https://www.example.com" diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index fec0ecf477..6a7931da8c 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -7,9 +7,6 @@ require 'mailers/proc_mailer' require 'mailers/asset_mailer' class BaseTest < ActiveSupport::TestCase - # TODO Add some tests for implicity layout render and url helpers - # so we can get rid of old base tests altogether with old base. - def teardown ActionMailer::Base.asset_host = nil ActionMailer::Base.assets_dir = nil @@ -35,21 +32,21 @@ class BaseTest < ActiveSupport::TestCase end test "mail() with bcc, cc, content_type, charset, mime_version, reply_to and date" do - @time = Time.now.beginning_of_day.to_datetime + time = Time.now.beginning_of_day.to_datetime email = BaseMailer.welcome(:bcc => 'bcc@test.lindsaar.net', :cc => 'cc@test.lindsaar.net', :content_type => 'multipart/mixed', :charset => 'iso-8559-1', :mime_version => '2.0', :reply_to => 'reply-to@test.lindsaar.net', - :date => @time) + :date => time) assert_equal(['bcc@test.lindsaar.net'], email.bcc) assert_equal(['cc@test.lindsaar.net'], email.cc) assert_equal('multipart/mixed; charset=iso-8559-1', email.content_type) assert_equal('iso-8559-1', email.charset) assert_equal('2.0', email.mime_version) assert_equal(['reply-to@test.lindsaar.net'], email.reply_to) - assert_equal(@time, email.date) + assert_equal(time, email.date) end test "mail() renders the template using the method being processed" do @@ -148,7 +145,7 @@ class BaseTest < ActiveSupport::TestCase assert_equal("application/pdf", email.parts[1].mime_type) assert_equal("VGhpcyBpcyB0ZXN0IEZpbGUgY29udGVudA==\r\n", email.parts[1].body.encoded) end - + test "can embed an inline attachment" do email = BaseMailer.inline_attachment # Need to call #encoded to force the JIT sort on parts @@ -156,8 +153,8 @@ class BaseTest < ActiveSupport::TestCase assert_equal(2, email.parts.length) assert_equal("multipart/related", email.mime_type) assert_equal("multipart/alternative", email.parts[0].mime_type) - assert_equal("text/plain", email.parts[0].parts[0].mime_type) - assert_equal("text/html", email.parts[0].parts[1].mime_type) + assert_equal("text/plain", email.parts[0].parts[0].mime_type) + assert_equal("text/html", email.parts[0].parts[1].mime_type) assert_equal("logo.png", email.parts[1].filename) end @@ -209,6 +206,12 @@ class BaseTest < ActiveSupport::TestCase assert_equal "New Subject!", email.subject end + test "translations are scoped properly" do + I18n.backend.store_translations('en', :base_mailer => {:email_with_translations => {:greet_user => "Hello %{name}!"}}) + email = BaseMailer.email_with_translations + assert_equal 'Hello lifo!', email.body.encoded + end + # Implicit multipart test "implicit multipart" do email = BaseMailer.implicit_multipart @@ -413,7 +416,7 @@ class BaseTest < ActiveSupport::TestCase BaseMailer.welcome.deliver assert_equal(1, BaseMailer.deliveries.length) end - + test "calling deliver, ActionMailer should yield back to mail to let it call :do_delivery on itself" do mail = Mail::Message.new mail.expects(:do_delivery).once @@ -447,7 +450,7 @@ class BaseTest < ActiveSupport::TestCase mail = BaseMailer.welcome_from_another_path(['unknown/invalid', 'another.path/base_mailer']).deliver assert_equal("Welcome from another path", mail.body.encoded) end - + test "assets tags should use ActionMailer's asset_host settings" do ActionMailer::Base.config.asset_host = "http://global.com" ActionMailer::Base.config.assets_dir = "global/" @@ -456,7 +459,7 @@ class BaseTest < ActiveSupport::TestCase assert_equal(%{Dummy}, mail.body.to_s.strip) end - + test "assets tags should use a Mailer's asset_host settings when available" do ActionMailer::Base.config.asset_host = "global.com" ActionMailer::Base.config.assets_dir = "global/" @@ -469,12 +472,12 @@ class BaseTest < ActiveSupport::TestCase end # Before and After hooks - + class MyObserver def self.delivered_email(mail) end end - + test "you can register an observer to the mail object that gets informed on email delivery" do ActionMailer::Base.register_observer(MyObserver) mail = BaseMailer.welcome @@ -493,7 +496,7 @@ class BaseTest < ActiveSupport::TestCase MyInterceptor.expects(:delivering_email).with(mail) mail.deliver end - + test "being able to put proc's into the defaults hash and they get evaluated on mail sending" do mail1 = ProcMailer.welcome yesterday = 1.day.ago @@ -501,7 +504,7 @@ class BaseTest < ActiveSupport::TestCase mail2 = ProcMailer.welcome assert(mail1['X-Proc-Method'].to_s.to_i > mail2['X-Proc-Method'].to_s.to_i) end - + test "we can call other defined methods on the class as needed" do mail = ProcMailer.welcome assert_equal("Thanks for signing up this afternoon", mail.subject) diff --git a/actionmailer/test/delivery_methods_test.rb b/actionmailer/test/delivery_methods_test.rb index 22a7d19bc2..08f84dbf3b 100644 --- a/actionmailer/test/delivery_methods_test.rb +++ b/actionmailer/test/delivery_methods_test.rb @@ -128,7 +128,7 @@ class MailDeliveryTest < ActiveSupport::TestCase Mail::Message.any_instance.expects(:deliver!).never DeliveryMailer.welcome.deliver end - + test "does not append the deliveries collection if told not to perform the delivery" do DeliveryMailer.perform_deliveries = false DeliveryMailer.deliveries.clear @@ -160,7 +160,7 @@ class MailDeliveryTest < ActiveSupport::TestCase DeliveryMailer.welcome.deliver end end - + test "does not increment the deliveries collection on bogus deliveries" do DeliveryMailer.delivery_method = BogusDelivery DeliveryMailer.raise_delivery_errors = false @@ -168,5 +168,5 @@ class MailDeliveryTest < ActiveSupport::TestCase DeliveryMailer.welcome.deliver assert_equal(0, DeliveryMailer.deliveries.length) end - + end diff --git a/actionmailer/test/fixtures/base_mailer/email_with_translations.html.erb b/actionmailer/test/fixtures/base_mailer/email_with_translations.html.erb new file mode 100644 index 0000000000..30466dd005 --- /dev/null +++ b/actionmailer/test/fixtures/base_mailer/email_with_translations.html.erb @@ -0,0 +1 @@ +<%= t('.greet_user', :name => 'lifo') %> \ No newline at end of file diff --git a/actionmailer/test/fixtures/i18n_test_mailer/mail_with_i18n_subject.erb b/actionmailer/test/fixtures/i18n_test_mailer/mail_with_i18n_subject.erb new file mode 100644 index 0000000000..f5340283f1 --- /dev/null +++ b/actionmailer/test/fixtures/i18n_test_mailer/mail_with_i18n_subject.erb @@ -0,0 +1,4 @@ +Hello there, + +Mr. <%= @recipient %>. Be greeted, new member! + diff --git a/actionmailer/test/fixtures/raw_email10 b/actionmailer/test/fixtures/raw_email10 index b1fc2b2617..edad5ccff1 100644 --- a/actionmailer/test/fixtures/raw_email10 +++ b/actionmailer/test/fixtures/raw_email10 @@ -15,6 +15,6 @@ Content-Type: text/plain; charset=X-UNKNOWN Test test. Hi. Waving. m ---------------------------------------------------------------- -Sent via Bell Mobility's Text Messaging service. +Sent via Bell Mobility's Text Messaging service. Envoyé par le service de messagerie texte de Bell Mobilité. ---------------------------------------------------------------- diff --git a/actionmailer/test/fixtures/raw_email2 b/actionmailer/test/fixtures/raw_email2 index 3999fcc877..9f87bb2a98 100644 --- a/actionmailer/test/fixtures/raw_email2 +++ b/actionmailer/test/fixtures/raw_email2 @@ -32,7 +32,7 @@ To: xxxxx xxxx Subject: Fwd: Signed email causes file attachments In-Reply-To: Mime-Version: 1.0 -Content-Type: multipart/mixed; +Content-Type: multipart/mixed; boundary="----=_Part_5028_7368284.1115579351471" References: diff --git a/actionmailer/test/fixtures/raw_email3 b/actionmailer/test/fixtures/raw_email3 index 771a96350d..3a0927490a 100644 --- a/actionmailer/test/fixtures/raw_email3 +++ b/actionmailer/test/fixtures/raw_email3 @@ -31,7 +31,7 @@ Reply-To: Test Tester To: xxxx@xxxx.com, xxxx@xxxx.com Subject: Another PDF Mime-Version: 1.0 -Content-Type: multipart/mixed; +Content-Type: multipart/mixed; boundary="----=_Part_2192_32400445.1115745999735" X-Virus-Scanned: amavisd-new at textdrive.com diff --git a/actionmailer/test/fixtures/raw_email5 b/actionmailer/test/fixtures/raw_email5 index 151c631471..bbe31bcdc5 100644 --- a/actionmailer/test/fixtures/raw_email5 +++ b/actionmailer/test/fixtures/raw_email5 @@ -14,6 +14,6 @@ Importance: normal Test test. Hi. Waving. m ---------------------------------------------------------------- -Sent via Bell Mobility's Text Messaging service. +Sent via Bell Mobility's Text Messaging service. Envoyé par le service de messagerie texte de Bell Mobilité. ---------------------------------------------------------------- diff --git a/actionmailer/test/fixtures/raw_email6 b/actionmailer/test/fixtures/raw_email6 index 93289c4f92..8e37bd7392 100644 --- a/actionmailer/test/fixtures/raw_email6 +++ b/actionmailer/test/fixtures/raw_email6 @@ -15,6 +15,6 @@ Content-Type: text/plain; charset=us-ascii Test test. Hi. Waving. m ---------------------------------------------------------------- -Sent via Bell Mobility's Text Messaging service. +Sent via Bell Mobility's Text Messaging service. Envoyé par le service de messagerie texte de Bell Mobilité. ---------------------------------------------------------------- diff --git a/actionmailer/test/fixtures/raw_email8 b/actionmailer/test/fixtures/raw_email8 index 2382dfdf34..79996365b3 100644 --- a/actionmailer/test/fixtures/raw_email8 +++ b/actionmailer/test/fixtures/raw_email8 @@ -8,7 +8,7 @@ To: xxxxx xxxx Subject: Fwd: Signed email causes file attachments In-Reply-To: Mime-Version: 1.0 -Content-Type: multipart/mixed; +Content-Type: multipart/mixed; boundary="----=_Part_5028_7368284.1115579351471" References: diff --git a/actionmailer/test/fixtures/raw_email9 b/actionmailer/test/fixtures/raw_email9 index 8b9b1eaa04..02ea0b05c5 100644 --- a/actionmailer/test/fixtures/raw_email9 +++ b/actionmailer/test/fixtures/raw_email9 @@ -10,19 +10,19 @@ Date: Wed, 23 Feb 2005 18:20:17 -0400 From: "xxx xxx" Message-ID: <4D6AA7EB.6490534@xxx.xxx> To: xxx@xxx.com -Subject: Stop adware/spyware once and for all. +Subject: Stop adware/spyware once and for all. X-Scanned-By: MIMEDefang 2.11 (www dot roaringpenguin dot com slash mimedefang) -You are infected with: +You are infected with: Ad Ware and Spy Ware -Get your free scan and removal download now, -before it gets any worse. +Get your free scan and removal download now, +before it gets any worse. http://xxx.xxx.info?aid=3D13&?stat=3D4327kdzt -no more? (you will still be infected) +no more? (you will still be infected) http://xxx.xxx.info/discon/?xxx@xxx.com diff --git a/actionmailer/test/fixtures/templates/signed_up.erb b/actionmailer/test/fixtures/templates/signed_up.erb index a85d5fa442..7afe1f651c 100644 --- a/actionmailer/test/fixtures/templates/signed_up.erb +++ b/actionmailer/test/fixtures/templates/signed_up.erb @@ -1,3 +1,3 @@ -Hello there, +Hello there, Mr. <%= @recipient %> \ No newline at end of file diff --git a/actionmailer/test/fixtures/test_mailer/custom_templating_extension.html.haml b/actionmailer/test/fixtures/test_mailer/custom_templating_extension.html.haml index 847d065c37..8dcf9746cc 100644 --- a/actionmailer/test/fixtures/test_mailer/custom_templating_extension.html.haml +++ b/actionmailer/test/fixtures/test_mailer/custom_templating_extension.html.haml @@ -1,6 +1,6 @@ -%p Hello there, +%p Hello there, -%p +%p Mr. = @recipient from haml \ No newline at end of file diff --git a/actionmailer/test/fixtures/test_mailer/custom_templating_extension.text.haml b/actionmailer/test/fixtures/test_mailer/custom_templating_extension.text.haml index 847d065c37..8dcf9746cc 100644 --- a/actionmailer/test/fixtures/test_mailer/custom_templating_extension.text.haml +++ b/actionmailer/test/fixtures/test_mailer/custom_templating_extension.text.haml @@ -1,6 +1,6 @@ -%p Hello there, +%p Hello there, -%p +%p Mr. = @recipient from haml \ No newline at end of file diff --git a/actionmailer/test/fixtures/test_mailer/signed_up.html.erb b/actionmailer/test/fixtures/test_mailer/signed_up.html.erb index a85d5fa442..7afe1f651c 100644 --- a/actionmailer/test/fixtures/test_mailer/signed_up.html.erb +++ b/actionmailer/test/fixtures/test_mailer/signed_up.html.erb @@ -1,3 +1,3 @@ -Hello there, +Hello there, Mr. <%= @recipient %> \ No newline at end of file diff --git a/actionmailer/test/fixtures/url_test_mailer/signed_up_with_url.erb b/actionmailer/test/fixtures/url_test_mailer/signed_up_with_url.erb index 4c5806d30d..6e7875cff5 100644 --- a/actionmailer/test/fixtures/url_test_mailer/signed_up_with_url.erb +++ b/actionmailer/test/fixtures/url_test_mailer/signed_up_with_url.erb @@ -1,4 +1,4 @@ -Hello there, +Hello there, Mr. <%= @recipient %>. Please see our greeting at <%= @welcome_url %> <%= welcome_url %> diff --git a/actionmailer/test/i18n_with_controller_test.rb b/actionmailer/test/i18n_with_controller_test.rb new file mode 100644 index 0000000000..7040ae6f8d --- /dev/null +++ b/actionmailer/test/i18n_with_controller_test.rb @@ -0,0 +1,46 @@ +require 'abstract_unit' +require 'action_controller' + +class I18nTestMailer < ActionMailer::Base + configure do |c| + c.assets_dir = '' + end + + def mail_with_i18n_subject(recipient) + @recipient = recipient + I18n.locale = :de + mail(:to => recipient, :subject => "#{I18n.t :email_subject} #{recipient}", + :from => "system@loudthinking.com", :date => Time.local(2004, 12, 12)) + end +end + +class TestController < ActionController::Base + def send_mail + I18nTestMailer.mail_with_i18n_subject("test@localhost").deliver + render :text => 'Mail sent' + end +end + +class ActionMailerI18nWithControllerTest < ActionDispatch::IntegrationTest + Routes = ActionDispatch::Routing::RouteSet.new + Routes.draw do + match ':controller(/:action(/:id))' + end + + def app + Routes + end + + def setup + I18n.backend.store_translations('de', :email_subject => '[Signed up] Welcome') + end + + def teardown + I18n.locale = :en + end + + def test_send_mail + get '/test/send_mail' + assert_equal "Mail sent", @response.body + end +end diff --git a/actionmailer/test/mail_helper_test.rb b/actionmailer/test/mail_helper_test.rb index 7cc0804323..17e9c82045 100644 --- a/actionmailer/test/mail_helper_test.rb +++ b/actionmailer/test/mail_helper_test.rb @@ -14,6 +14,14 @@ class HelperMailer < ActionMailer::Base end end + def use_format_paragraph + @text = "But soft! What light through yonder window breaks?" + + mail_with_defaults do |format| + format.html { render(:inline => "<%= format_paragraph @text, 15, 1 %>") } + end + end + def use_mailer mail_with_defaults do |format| format.html { render(:inline => "<%= mailer.message.subject %>") } @@ -50,5 +58,10 @@ class MailerHelperTest < ActionMailer::TestCase mail = HelperMailer.use_message assert_match "using helpers", mail.body.encoded end + + def test_use_format_paragraph + mail = HelperMailer.use_format_paragraph + assert_match " But soft! What\r\n light through\r\n yonder window\r\n breaks?", mail.body.encoded + end end diff --git a/actionmailer/test/old_base/mail_layout_test.rb b/actionmailer/test/mail_layout_test.rb similarity index 64% rename from actionmailer/test/old_base/mail_layout_test.rb rename to actionmailer/test/mail_layout_test.rb index 2c2daa0f28..def8da81b8 100644 --- a/actionmailer/test/old_base/mail_layout_test.rb +++ b/actionmailer/test/mail_layout_test.rb @@ -1,53 +1,45 @@ require 'abstract_unit' class AutoLayoutMailer < ActionMailer::Base + default :to => 'test@localhost', + :subject => "You have a mail", + :from => "tester@example.com" def hello - recipients 'test@localhost' - subject "You have a mail" - from "tester@example.com" + mail() end def spam - recipients 'test@localhost' - subject "You have a mail" - from "tester@example.com" - @world = "Earth" - body render(:inline => "Hello, <%= @world %>", :layout => 'spam') + mail(:body => render(:inline => "Hello, <%= @world %>", :layout => 'spam')) end def nolayout - recipients 'test@localhost' - subject "You have a mail" - from "tester@example.com" - @world = "Earth" - body render(:inline => "Hello, <%= @world %>", :layout => false) + mail(:body => render(:inline => "Hello, <%= @world %>", :layout => false)) end def multipart(type = nil) - recipients 'test@localhost' - subject "You have a mail" - from "tester@example.com" - - content_type(type) if type + mail(:content_type => type) do |format| + format.text { render } + format.html { render } + end end end class ExplicitLayoutMailer < ActionMailer::Base layout 'spam', :except => [:logout] + default :to => 'test@localhost', + :subject => "You have a mail", + :from => "tester@example.com" + def signup - recipients 'test@localhost' - subject "You have a mail" - from "tester@example.com" + mail() end def logout - recipients 'test@localhost' - subject "You have a mail" - from "tester@example.com" + mail() end end @@ -91,19 +83,6 @@ class LayoutMailerTest < Test::Unit::TestCase assert_equal "Hello from layout text/html multipart", mail.parts.last.body.to_s end - def test_should_fix_multipart_layout - mail = AutoLayoutMailer.multipart("text/plain") - assert_equal "multipart/alternative", mail.mime_type - assert_equal 2, mail.parts.size - - assert_equal 'text/plain', mail.parts.first.mime_type - assert_equal "text/plain layout - text/plain multipart", mail.parts.first.body.to_s - - assert_equal 'text/html', mail.parts.last.mime_type - assert_equal "Hello from layout text/html multipart", mail.parts.last.body.to_s - end - - def test_should_pickup_layout_given_to_render mail = AutoLayoutMailer.spam assert_equal "Spammer layout Hello, Earth", mail.body.to_s.strip diff --git a/actionmailer/test/mailers/base_mailer.rb b/actionmailer/test/mailers/base_mailer.rb index 2c6de36ccf..9416bc718e 100644 --- a/actionmailer/test/mailers/base_mailer.rb +++ b/actionmailer/test/mailers/base_mailer.rb @@ -111,4 +111,8 @@ class BaseMailer < ActionMailer::Base format.html { render :layout => layout_name } end end + + def email_with_translations + mail :body => render("email_with_translations.html") + end end diff --git a/actionmailer/test/mailers/proc_mailer.rb b/actionmailer/test/mailers/proc_mailer.rb index 6a79cd71fc..43916e1421 100644 --- a/actionmailer/test/mailers/proc_mailer.rb +++ b/actionmailer/test/mailers/proc_mailer.rb @@ -6,11 +6,11 @@ class ProcMailer < ActionMailer::Base def welcome mail end - + private - + def give_a_greeting "Thanks for signing up this afternoon" end - + end diff --git a/actionmailer/test/old_base/adv_attr_test.rb b/actionmailer/test/old_base/adv_attr_test.rb index f22d733bc5..c5a6b6d88b 100644 --- a/actionmailer/test/old_base/adv_attr_test.rb +++ b/actionmailer/test/old_base/adv_attr_test.rb @@ -11,9 +11,14 @@ class AdvAttrTest < ActiveSupport::TestCase end def setup + ActiveSupport::Deprecation.silenced = true @person = Person.new end + def teardown + ActiveSupport::Deprecation.silenced = false + end + def test_adv_attr assert_nil @person.name @person.name 'Bob' diff --git a/actionmailer/test/old_base/mail_render_test.rb b/actionmailer/test/old_base/mail_render_test.rb index 08951833a5..3a1d3184f4 100644 --- a/actionmailer/test/old_base/mail_render_test.rb +++ b/actionmailer/test/old_base/mail_render_test.rb @@ -19,18 +19,6 @@ class RenderMailer < ActionMailer::Base body render(:file => "templates/signed_up") end - def rxml_template - recipients 'test@localhost' - subject "rendering rxml template" - from "tester@example.com" - end - - def included_subtemplate - recipients 'test@localhost' - subject "Including another template in the one being rendered" - from "tester@example.com" - end - def no_instance_variable recipients 'test@localhost' subject "No Instance Variable" @@ -41,11 +29,6 @@ class RenderMailer < ActionMailer::Base end end - def initialize_defaults(method_name) - super - mailer_name "test_mailer" - end - def multipart_alternative recipients 'test@localhost' subject 'multipart/alternative' @@ -97,11 +80,13 @@ class RenderHelperTest < Test::Unit::TestCase set_delivery_method :test ActionMailer::Base.perform_deliveries = true ActionMailer::Base.deliveries.clear + ActiveSupport::Deprecation.silenced = true @recipient = 'test@localhost' end def teardown + ActiveSupport::Deprecation.silenced = false restore_delivery_method end @@ -112,38 +97,19 @@ class RenderHelperTest < Test::Unit::TestCase def test_file_template mail = RenderMailer.file_template - assert_equal "Hello there, \n\nMr. test@localhost", mail.body.to_s.strip - end - - def test_rxml_template - mail = RenderMailer.rxml_template.deliver - assert_equal %(\n), mail.body.to_s.strip - end - - def test_included_subtemplate - mail = RenderMailer.included_subtemplate.deliver - assert_equal "Hey Ho, let's go!", mail.body.to_s.strip + assert_equal "Hello there,\n\nMr. test@localhost", mail.body.to_s.strip end def test_no_instance_variable mail = RenderMailer.no_instance_variable.deliver assert_equal "Look, subject.nil? is true!", mail.body.to_s.strip end - - def test_legacy_multipart_alternative - mail = RenderMailer.multipart_alternative.deliver - assert_equal(2, mail.parts.size) - assert_equal("multipart/alternative", mail.mime_type) - assert_equal("text/plain", mail.parts[0].mime_type) - assert_equal("foo: bar", mail.parts[0].body.encoded) - assert_equal("text/html", mail.parts[1].mime_type) - assert_equal("foo bar", mail.parts[1].body.encoded) - end end class FirstSecondHelperTest < Test::Unit::TestCase def setup set_delivery_method :test + ActiveSupport::Deprecation.silenced = true ActionMailer::Base.perform_deliveries = true ActionMailer::Base.deliveries.clear @@ -151,6 +117,7 @@ class FirstSecondHelperTest < Test::Unit::TestCase end def teardown + ActiveSupport::Deprecation.silenced = false restore_delivery_method end diff --git a/actionmailer/test/old_base/mail_service_test.rb b/actionmailer/test/old_base/mail_service_test.rb index 831adf3e32..0b5b0b2da3 100644 --- a/actionmailer/test/old_base/mail_service_test.rb +++ b/actionmailer/test/old_base/mail_service_test.rb @@ -106,7 +106,7 @@ class TestMailer < ActionMailer::Base cc "Foo áëô îü " bcc "Foo áëô îü " charset "UTF-8" - + body "åœö blah" end @@ -328,21 +328,20 @@ class ActionMailerTest < Test::Unit::TestCase mail end - # Replacing logger work around for mocha bug. Should be fixed in mocha 0.3.3 def setup set_delivery_method :test ActionMailer::Base.perform_deliveries = true ActionMailer::Base.raise_delivery_errors = true ActionMailer::Base.deliveries.clear + ActiveSupport::Deprecation.silenced = true - @original_logger = TestMailer.logger @recipient = 'test@localhost' TestMailer.delivery_method = :test end def teardown - TestMailer.logger = @original_logger + ActiveSupport::Deprecation.silenced = false restore_delivery_method end @@ -359,7 +358,7 @@ class ActionMailerTest < Test::Unit::TestCase assert_equal "text/plain", created.parts[0].parts[0].mime_type assert_equal "text/html", created.parts[0].parts[1].mime_type assert_equal "application/octet-stream", created.parts[1].mime_type - + end def test_nested_parts_with_body @@ -392,14 +391,14 @@ class ActionMailerTest < Test::Unit::TestCase expected = new_mail expected.to = @recipient expected.subject = "[Signed up] Welcome #{@recipient}" - expected.body = "Hello there, \n\nMr. #{@recipient}" + expected.body = "Hello there,\n\nMr. #{@recipient}" expected.from = "system@loudthinking.com" expected.date = Time.now created = nil assert_nothing_raised { created = TestMailer.signed_up(@recipient) } assert_not_nil created - + expected.message_id = '<123@456>' created.message_id = '<123@456>' @@ -420,7 +419,7 @@ class ActionMailerTest < Test::Unit::TestCase expected = new_mail expected.to = @recipient expected.subject = "[Signed up] Welcome #{@recipient}" - expected.body = "Hello there, \n\nMr. #{@recipient}" + expected.body = "Hello there,\n\nMr. #{@recipient}" expected.from = "system@loudthinking.com" expected.date = Time.local(2004, 12, 12) @@ -503,7 +502,7 @@ class ActionMailerTest < Test::Unit::TestCase delivered = ActionMailer::Base.deliveries.first expected.message_id = '<123@456>' delivered.message_id = '<123@456>' - + assert_equal expected.encoded, delivered.encoded end @@ -546,10 +545,10 @@ class ActionMailerTest < Test::Unit::TestCase created = TestMailer.different_reply_to @recipient end assert_not_nil created - + expected.message_id = '<123@456>' created.message_id = '<123@456>' - + assert_equal expected.encoded, created.encoded assert_nothing_raised do @@ -558,10 +557,10 @@ class ActionMailerTest < Test::Unit::TestCase delivered = ActionMailer::Base.deliveries.first assert_not_nil delivered - + expected.message_id = '<123@456>' delivered.message_id = '<123@456>' - + assert_equal expected.encoded, delivered.encoded end @@ -581,7 +580,7 @@ class ActionMailerTest < Test::Unit::TestCase created = TestMailer.iso_charset @recipient end assert_not_nil created - + expected.message_id = '<123@456>' created.message_id = '<123@456>' @@ -596,7 +595,7 @@ class ActionMailerTest < Test::Unit::TestCase expected.message_id = '<123@456>' delivered.message_id = '<123@456>' - + assert_equal expected.encoded, delivered.encoded end @@ -631,7 +630,7 @@ class ActionMailerTest < Test::Unit::TestCase expected.message_id = '<123@456>' delivered.message_id = '<123@456>' - + assert_equal expected.encoded, delivered.encoded end @@ -761,10 +760,10 @@ EOF delivered = ActionMailer::Base.deliveries.first assert_not_nil delivered - + expected.message_id = '<123@456>' delivered.message_id = '<123@456>' - + assert_equal expected.encoded, delivered.encoded end @@ -887,7 +886,7 @@ EOF assert_equal "iso-8859-1", mail.parts[1].charset assert_equal "image/jpeg", mail.parts[2].mime_type - + assert_equal "attachment", mail.parts[2][:content_disposition].disposition_type assert_equal "foo.jpg", mail.parts[2][:content_disposition].filename assert_equal "foo.jpg", mail.parts[2][:content_type].filename @@ -1005,7 +1004,7 @@ EOF attachment = mail.attachments.last expected = "01 Quien Te Dij\212at. Pitbull.mp3" - + if expected.respond_to?(:force_encoding) result = attachment.filename.dup expected.force_encoding(Encoding::ASCII_8BIT) @@ -1096,111 +1095,3 @@ EOF TestMailer.smtp_settings.merge!(:enable_starttls_auto => true) end end - -class InheritableTemplateRootTest < ActiveSupport::TestCase - def test_attr - expected = File.expand_path("#{File.dirname(__FILE__)}/../fixtures/path.with.dots") - assert_equal expected, FunkyPathMailer.template_root.to_s - - sub = Class.new(FunkyPathMailer) - assert_deprecated do - sub.template_root = 'test/path' - end - - assert_equal File.expand_path('test/path'), sub.template_root.to_s - assert_equal expected, FunkyPathMailer.template_root.to_s - end -end - -class MethodNamingTest < ActiveSupport::TestCase - include ActionMailer::TestHelper - - class TestMailer < ActionMailer::Base - def send - body 'foo' - end - end - - def setup - set_delivery_method :test - ActionMailer::Base.perform_deliveries = true - ActionMailer::Base.deliveries.clear - end - - def teardown - restore_delivery_method - end - - def test_send_method - assert_nothing_raised do - assert_emails 1 do - assert_deprecated do - TestMailer.deliver_send - end - end - end - end -end -class RespondToTest < Test::Unit::TestCase - class RespondToMailer < ActionMailer::Base; end - - def setup - set_delivery_method :test - end - - def teardown - restore_delivery_method - end - - def test_should_respond_to_new - assert_respond_to RespondToMailer, :new - end - - def test_should_respond_to_create_with_template_suffix - assert_respond_to RespondToMailer, :create_any_old_template - end - - def test_should_respond_to_deliver_with_template_suffix - assert_respond_to RespondToMailer, :deliver_any_old_template - end - - def test_should_not_respond_to_new_with_template_suffix - assert !RespondToMailer.respond_to?(:new_any_old_template) - end - - def test_should_not_respond_to_create_with_template_suffix_unless_it_is_separated_by_an_underscore - assert !RespondToMailer.respond_to?(:createany_old_template) - end - - def test_should_not_respond_to_deliver_with_template_suffix_unless_it_is_separated_by_an_underscore - assert !RespondToMailer.respond_to?(:deliverany_old_template) - end - - def test_should_not_respond_to_create_with_template_suffix_if_it_begins_with_a_uppercase_letter - assert !RespondToMailer.respond_to?(:create_Any_old_template) - end - - def test_should_not_respond_to_deliver_with_template_suffix_if_it_begins_with_a_uppercase_letter - assert !RespondToMailer.respond_to?(:deliver_Any_old_template) - end - - def test_should_not_respond_to_create_with_template_suffix_if_it_begins_with_a_digit - assert !RespondToMailer.respond_to?(:create_1_template) - end - - def test_should_not_respond_to_deliver_with_template_suffix_if_it_begins_with_a_digit - assert !RespondToMailer.respond_to?(:deliver_1_template) - end - - def test_should_not_respond_to_method_where_deliver_is_not_a_suffix - assert !RespondToMailer.respond_to?(:foo_deliver_template) - end - - def test_should_still_raise_exception_with_expected_message_when_calling_an_undefined_method - error = assert_raise NoMethodError do - RespondToMailer.not_a_method - end - - assert_match(/method.*not_a_method/, error.message) - end -end diff --git a/actionmailer/test/old_base/tmail_compat_test.rb b/actionmailer/test/old_base/tmail_compat_test.rb index 255205de84..51558c2bfa 100644 --- a/actionmailer/test/old_base/tmail_compat_test.rb +++ b/actionmailer/test/old_base/tmail_compat_test.rb @@ -1,6 +1,14 @@ require 'abstract_unit' class TmailCompatTest < ActiveSupport::TestCase + def setup + @silence = ActiveSupport::Deprecation.silenced + ActiveSupport::Deprecation.silenced = false + end + + def teardown + ActiveSupport::Deprecation.silenced = @silence + end def test_set_content_type_raises_deprecation_warning mail = Mail.new @@ -31,5 +39,4 @@ class TmailCompatTest < ActiveSupport::TestCase end assert_equal mail.content_transfer_encoding, "base64" end - end diff --git a/actionmailer/test/test_helper_test.rb b/actionmailer/test/test_helper_test.rb index 8ff604c2c7..dd62164176 100644 --- a/actionmailer/test/test_helper_test.rb +++ b/actionmailer/test/test_helper_test.rb @@ -2,11 +2,10 @@ require 'abstract_unit' class TestHelperMailer < ActionMailer::Base def test - recipients "test@example.com" - from "tester@example.com" - @world = "Earth" - render(:inline => "Hello, <%= @world %>") + mail :body => render(:inline => "Hello, <%= @world %>"), + :to => "test@example.com", + :from => "tester@example.com" end end @@ -32,7 +31,7 @@ class TestHelperMailerTest < ActionMailer::TestCase self.class.determine_default_mailer("NotAMailerTest") end end - + def test_charset_is_utf_8 assert_equal "UTF-8", charset end @@ -44,14 +43,14 @@ class TestHelperMailerTest < ActionMailer::TestCase end end end - + def test_repeated_assert_emails_calls assert_nothing_raised do assert_emails 1 do TestHelperMailer.test.deliver end end - + assert_nothing_raised do assert_emails 2 do TestHelperMailer.test.deliver @@ -59,20 +58,20 @@ class TestHelperMailerTest < ActionMailer::TestCase end end end - + def test_assert_emails_with_no_block assert_nothing_raised do TestHelperMailer.test.deliver assert_emails 1 end - + assert_nothing_raised do TestHelperMailer.test.deliver TestHelperMailer.test.deliver assert_emails 3 end end - + def test_assert_no_emails assert_nothing_raised do assert_no_emails do @@ -80,17 +79,17 @@ class TestHelperMailerTest < ActionMailer::TestCase end end end - + def test_assert_emails_too_few_sent error = assert_raise ActiveSupport::TestCase::Assertion do assert_emails 2 do TestHelperMailer.test.deliver end end - + assert_match(/2 .* but 1/, error.message) end - + def test_assert_emails_too_many_sent error = assert_raise ActiveSupport::TestCase::Assertion do assert_emails 1 do @@ -98,17 +97,17 @@ class TestHelperMailerTest < ActionMailer::TestCase TestHelperMailer.test.deliver end end - + assert_match(/1 .* but 2/, error.message) end - + def test_assert_no_emails_failure error = assert_raise ActiveSupport::TestCase::Assertion do assert_no_emails do TestHelperMailer.test.deliver end end - + assert_match(/0 .* but 1/, error.message) end end diff --git a/actionmailer/test/old_base/url_test.rb b/actionmailer/test/url_test.rb similarity index 77% rename from actionmailer/test/old_base/url_test.rb rename to actionmailer/test/url_test.rb index b6496bfe1b..0536e83098 100644 --- a/actionmailer/test/old_base/url_test.rb +++ b/actionmailer/test/url_test.rb @@ -18,13 +18,10 @@ class UrlTestMailer < ActionMailer::Base end def signed_up_with_url(recipient) - @recipients = recipient - @subject = "[Signed up] Welcome #{recipient}" - @from = "system@loudthinking.com" - @sent_on = Time.local(2004, 12, 12) - @recipient = recipient @welcome_url = url_for :host => "example.com", :controller => "welcome", :action => "greeting" + mail(:to => recipient, :subject => "[Signed up] Welcome #{recipient}", + :from => "system@loudthinking.com", :date => Time.local(2004, 12, 12)) end end @@ -47,6 +44,7 @@ class ActionMailerUrlTest < ActionMailer::TestCase set_delivery_method :test ActionMailer::Base.perform_deliveries = true ActionMailer::Base.deliveries.clear + ActiveSupport::Deprecation.silenced = false @recipient = 'test@localhost' end @@ -58,19 +56,18 @@ class ActionMailerUrlTest < ActionMailer::TestCase def test_signed_up_with_url UrlTestMailer.delivery_method = :test - assert_deprecated do - AppRoutes.draw do |map| - map.connect ':controller/:action/:id' - map.welcome 'welcome', :controller=>"foo", :action=>"bar" - end + AppRoutes.draw do + match ':controller(/:action(/:id))' + match '/welcome' => "foo#bar", :as => "welcome" end expected = new_mail expected.to = @recipient expected.subject = "[Signed up] Welcome #{@recipient}" - expected.body = "Hello there, \n\nMr. #{@recipient}. Please see our greeting at http://example.com/welcome/greeting http://www.basecamphq.com/welcome\n\n\"Somelogo\"" + expected.body = "Hello there,\n\nMr. #{@recipient}. Please see our greeting at http://example.com/welcome/greeting http://www.basecamphq.com/welcome\n\n\"Somelogo\"" expected.from = "system@loudthinking.com" expected.date = Time.local(2004, 12, 12) + expected.content_type = "text/html" created = nil assert_nothing_raised { created = UrlTestMailer.signed_up_with_url(@recipient) } @@ -83,7 +80,7 @@ class ActionMailerUrlTest < ActionMailer::TestCase assert_nothing_raised { UrlTestMailer.signed_up_with_url(@recipient).deliver } assert_not_nil ActionMailer::Base.deliveries.first delivered = ActionMailer::Base.deliveries.first - + delivered.message_id = '<123@456>' assert_equal expected.encoded, delivered.encoded end diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index 81abb8b5f1..5ab92c8cfc 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,4 +1,74 @@ -*Rails 3.0.0 [release candidate] (July 26th, 2010)* +*Rails 3.1.0 (unreleased)* + +* 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] + +* URL parameters which return nil for to_param are now removed from the query string [Andrew White] + +* ActionDispatch::MiddlewareStack now uses composition over inheritance. It is +no longer an array which means there may be methods missing that were not +tested. + +* Add an :authenticity_token option to form_tag for custom handling or to omit the token (pass :authenticity_token => false). [Jakub Kuźma, Igor Wiedler] + +* HTML5 button_tag helper. [Rizwan Reza] + +* Template lookup now searches further up in the inheritance chain. [Artemave] + +* Brought back config.action_view.cache_template_loading, which allows to decide whether templates should be cached or not. [Piotr Sarnacki] + +* url_for and named url helpers now accept :subdomain and :domain as options, [Josh Kalderimis] + +* The redirect route method now also accepts a hash of options which will only change the parts of the url in question, or an object which responds to call, allowing for redirects to be reused (check the documentation for examples). [Josh Kalderimis] + +* Added config.action_controller.include_all_helpers. By default 'helper :all' is done in ActionController::Base, which includes all the helpers by default. Setting include_all_helpers to false will result in including only application_helper and helper corresponding to controller (like foo_helper for foo_controller). [Piotr Sarnacki] + +* Added a convenience idiom to generate HTML5 data-* attributes in tag helpers from a :data hash of options: + + tag("div", :data => {:name => 'Stephen', :city_state => %w(Chicago IL)}) + # =>
+ + Keys are dasherized. Values are JSON-encoded, except for strings and symbols. [Stephen Celis] + +* Added render :once. You can pass either a string or an array of strings and Rails will ensure they each of them are rendered just once. [José Valim] + +* Deprecate old template handler API. The new API simply requires a template handler to respond to call. [José Valim] + +* :rhtml and :rxml were finally removed as template handlers. [José Valim] + +* Moved etag responsibility from ActionDispatch::Response to the middleware stack. [José Valim] + +* Rely on Rack::Session stores API for more compatibility across the Ruby world. This is backwards incompatible since Rack::Session expects #get_session to accept 4 arguments and requires #destroy_session instead of simply #destroy. [José Valim] + +* file_field automatically adds :multipart => true to the enclosing form. [Santiago Pastorino] + +* Renames csrf_meta_tag -> csrf_meta_tags, and aliases csrf_meta_tag for backwards compatibility. [fxn] + +* Add Rack::Cache to the default stack. Create a Rails store that delegates to the Rails cache, so by default, whatever caching layer you are using will be used for HTTP caching. Note that Rack::Cache will be used if you use #expires_in, #fresh_when or #stale with :public => true. Otherwise, the caching rules will apply to the browser only. [Yehuda Katz, Carl Lerche] + + +*Rails 3.0.2 (unreleased)* + +* The helper number_to_currency accepts a new :negative_format option to be able to configure how to render negative amounts. [Don Wilson] + + +*Rails 3.0.1 (October 15, 2010)* + +* No Changes, just a version bump. + + +*Rails 3.0.0 (August 29, 2010)* + +* password_field renders with nil value by default making the use of passwords secure by default, if you want to render you should do for instance f.password_field(:password, :value => @user.password) [Santiago Pastorino] + +* Symbols and strings in routes should yield the same behavior. Note this may break existing apps that were using symbols with the new routes API. [José Valim] + +* Add clear_helpers as a way to clean up all helpers added to this controller, maintaining just the helper with the same name as the controller. [José Valim] + +* Support routing constraints in functional tests. [Andrew White] + +* Add a header that tells Internet Explorer (all versions) to use the best available standards support. [Yehuda Katz] * Allow stylesheet/javascript extensions to be changed through railties. [Josh Kalderimis] @@ -26,16 +96,13 @@ resources :comments end end - + You can now use comment_path for /comments/1 instead of post_comment_path for /posts/1/comments/1. * Add support for multi-subdomain session by setting cookie host in session cookie so you can share session between www.example.com, example.com and user.example.com. #4818 [Guillermo Álvarez] * Removed textilize, textilize_without_paragraph and markdown helpers. [Santiago Pastorino] - -*Rails 3.0.0 [beta 4] (June 8th, 2010)* - * Remove middleware laziness [José Valim] * Make session stores rely on request.cookie_jar and change set_session semantics to return the cookie value instead of a boolean. [José Valim] @@ -52,9 +119,6 @@ * Changed translate helper so that it doesn’t mark every translation as safe HTML. Only keys with a "_html" suffix and keys named "html" are considered to be safe HTML. All other translations are left untouched. [Craig Davey] - -*Rails 3.0.0 [beta 3] (April 13th, 2010)* - * New option :as added to form_for allows to change the object name. The old <% form_for :client, @post %> becomes <% form_for @post, :as => :client %> [spastorino] * Removed verify method in controllers. [JV] @@ -89,9 +153,6 @@ "HEAD" and #request_method returns "GET" in HEAD requests). This is for compatibility with Rack::Request [YK] - -*Rails 3.0.0 [beta 2] (April 1st, 2010)* - * #concat is now deprecated in favor of using <%= %> helpers [YK] * Block helpers now return Strings, so you can use <%= form_for @foo do |f| %>. @@ -110,7 +171,7 @@ * ActionDispatch::Request#content_type returns a String to be compatible with Rack::Request. Use #content_mime_type for the Mime::Type instance [YK] -* Updated Prototype to 1.6.1 and Scriptaculous to 1.8.3 [ML] +* Updated Prototype to 1.6.1 and Scriptaculous to 1.8.3 [ML] * Change the preferred way that URL helpers are included into a class[YK & CL] @@ -120,9 +181,6 @@ # for just url_for include Rails.application.router.url_for - -*Rails 3.0.0 [beta 1] (February 4, 2010)* - * Fixed that PrototypeHelper#update_page should return html_safe [DHH] * Fixed that much of DateHelper wouldn't return html_safe? strings [DHH] diff --git a/actionpack/MIT-LICENSE b/actionpack/MIT-LICENSE index a345a2419d..7ad1051066 100644 --- a/actionpack/MIT-LICENSE +++ b/actionpack/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2010 David Heinemeier Hansson +Copyright (c) 2004-2011 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionpack/README.rdoc b/actionpack/README.rdoc index b297ceb0e2..3661d27d51 100644 --- a/actionpack/README.rdoc +++ b/actionpack/README.rdoc @@ -102,10 +102,10 @@ A short rundown of some of the major features: class WeblogController < ActionController::Base # filters as methods before_filter :authenticate, :cache, :audit - + # filter as a proc after_filter { |c| c.response.body = Gzip::compress(c.response.body) } - + # class filter after_filter LocalizeFilter @@ -262,7 +262,7 @@ methods: layout "weblog/layout" def index - @posts = Post.find(:all) + @posts = Post.all end def show @@ -323,7 +323,7 @@ The latest version of Action Pack can be installed with Rubygems: Source code can be downloaded as part of the Rails project on GitHub -* http://github.com/rails/rails/tree/master/actionpack/ +* https://github.com/rails/rails/tree/master/actionpack/ == License diff --git a/actionpack/RUNNING_UNIT_TESTS b/actionpack/RUNNING_UNIT_TESTS index 95a8bc7497..1e3ba7abe7 100644 --- a/actionpack/RUNNING_UNIT_TESTS +++ b/actionpack/RUNNING_UNIT_TESTS @@ -1,22 +1,25 @@ == Running with Rake The easiest way to run the unit tests is through Rake. The default task runs -the entire test suite for all classes. For more information, checkout the +the entire test suite for all classes. For more information, checkout the full array of rake tasks with "rake -T" Rake can be found at http://rake.rubyforge.org == Running by hand -If you only want to run a single test suite, or don't want to bother with Rake, -you can do so with something like: +To run a single test suite - ruby -Itest test/controller/base_tests.rb + rake test TEST=path/to/test.rb -== Dependency on ActiveRecord and database setup +which can be further narrowed down to one test: + + rake test TEST=path/to/test.rb TESTOPTS="--name=test_something" + +== Dependency on Active Record and database setup Test cases in the test/controller/active_record/ directory depend on having -activerecord and sqlite installed. If ActiveRecord is not in +activerecord and sqlite installed. If Active Record is not in actionpack/../activerecord directory, or the sqlite rubygem is not installed, these tests are skipped. diff --git a/actionpack/Rakefile b/actionpack/Rakefile old mode 100644 new mode 100755 index 4af8ea167a..9030db9f7a --- a/actionpack/Rakefile +++ b/actionpack/Rakefile @@ -1,8 +1,5 @@ -gem 'rdoc', '>= 2.5.9' -require 'rdoc' -require 'rake' +#!/usr/bin/env rake require 'rake/testtask' -require 'rdoc/task' require 'rake/packagetask' require 'rake/gempackagetask' @@ -21,7 +18,8 @@ Rake::TestTask.new(:test_action_pack) do |t| # this will not happen automatically and the tests (as a whole) will error t.test_files = Dir.glob('test/{abstract,controller,dispatch,template}/**/*_test.rb').sort - # t.warning = true + t.warning = true + t.verbose = true end namespace :test do @@ -36,24 +34,6 @@ Rake::TestTask.new(:test_active_record_integration) do |t| t.test_files = Dir.glob("test/activerecord/*_test.rb") end -# Genereate the RDoc documentation - -RDoc::Task.new { |rdoc| - rdoc.rdoc_dir = 'doc' - rdoc.title = "Action Pack -- On rails from request to response" - rdoc.options << '--charset' << 'utf-8' - rdoc.options << '-f' << 'horo' - rdoc.options << '--main' << 'README.rdoc' - if ENV['DOC_FILES'] - rdoc.rdoc_files.include(ENV['DOC_FILES'].split(/,\s*/)) - else - rdoc.rdoc_files.include('README.rdoc', 'RUNNING_UNIT_TESTS', 'CHANGELOG') - rdoc.rdoc_files.include(Dir['lib/**/*.rb'] - - Dir['lib/*/vendor/**/*.rb']) - rdoc.rdoc_files.exclude('lib/actionpack.rb') - end -} - spec = eval(File.read('actionpack.gemspec')) Rake::GemPackageTask.new(spec) do |p| diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index 99deff234c..f6bc5e0d37 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -19,13 +19,14 @@ Gem::Specification.new do |s| s.has_rdoc = true - s.add_dependency('activesupport', version) - s.add_dependency('activemodel', version) - s.add_dependency('builder', '~> 2.1.2') - s.add_dependency('i18n', '~> 0.4.1') - s.add_dependency('rack', '~> 1.2.1') - s.add_dependency('rack-test', '~> 0.5.4') - s.add_dependency('rack-mount', '~> 0.6.9') - s.add_dependency('tzinfo', '~> 0.3.22') - s.add_dependency('erubis', '~> 2.6.6') + s.add_dependency('activesupport', version) + s.add_dependency('activemodel', version) + s.add_dependency('rack-cache', '~> 1.0.0') + s.add_dependency('builder', '~> 3.0.0') + s.add_dependency('i18n', '~> 0.5.0') + s.add_dependency('rack', '~> 1.2.1') + s.add_dependency('rack-test', '~> 0.5.7') + s.add_dependency('rack-mount', '~> 0.6.13') + s.add_dependency('tzinfo', '~> 0.3.23') + s.add_dependency('erubis', '~> 2.6.6') end diff --git a/actionpack/lib/abstract_controller.rb b/actionpack/lib/abstract_controller.rb index c565c940a1..cc5878c88e 100644 --- a/actionpack/lib/abstract_controller.rb +++ b/actionpack/lib/abstract_controller.rb @@ -2,6 +2,7 @@ activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__) $:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path) require 'action_pack' +require 'active_support/concern' require 'active_support/ruby/shim' require 'active_support/dependencies/autoload' require 'active_support/core_ext/class/attribute' @@ -23,4 +24,5 @@ module AbstractController autoload :Translation autoload :AssetPaths autoload :ViewPaths + autoload :UrlFor end diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb index db0a6736e0..c384fd0978 100644 --- a/actionpack/lib/abstract_controller/base.rb +++ b/actionpack/lib/abstract_controller/base.rb @@ -6,9 +6,14 @@ module AbstractController class Error < StandardError; end class ActionNotFound < StandardError; end + # AbstractController::Base is a low-level API. Nobody should be + # using it directly, and subclasses (like ActionController::Base) are + # expected to provide their own +render+ method, since rendering means + # different things depending on the context. class Base attr_internal :response_body attr_internal :action_name + attr_internal :formats include ActiveSupport::Configurable extend ActiveSupport::DescendantsTracker @@ -26,23 +31,21 @@ module AbstractController # A list of all internal methods for a controller. This finds the first # abstract superclass of a controller, and gets a list of all public # instance methods on that abstract class. Public instance methods of - # a controller would normally be considered action methods, so we - # are removing those methods on classes declared as abstract - # (ActionController::Metal and ActionController::Base are defined - # as abstract) + # a controller would normally be considered action methods, so methods + # declared on abstract classes are being removed. + # (ActionController::Metal and ActionController::Base are defined as abstract) def internal_methods controller = self controller = controller.superclass until controller.abstract? controller.public_instance_methods(true) end - # The list of hidden actions to an empty Array. Defaults to an - # empty Array. This can be modified by other modules or subclasses + # The list of hidden actions to an empty array. Defaults to an + # empty array. This can be modified by other modules or subclasses # to specify particular actions as hidden. # # ==== Returns - # Array[String]:: An array of method names that should not be - # considered actions. + # * array - An array of method names that should not be considered actions. def hidden_actions [] end @@ -54,18 +57,17 @@ module AbstractController # itself. Finally, #hidden_actions are removed. # # ==== Returns - # Array[String]:: A list of all methods that should be considered - # actions. + # * array - A list of all methods that should be considered actions. def action_methods @action_methods ||= begin # All public instance methods of this class, including ancestors - methods = public_instance_methods(true).map { |m| m.to_s }.to_set - + methods = (public_instance_methods(true) - # Except for public instance methods of Base and its ancestors - internal_methods.map { |m| m.to_s } + + internal_methods + # Be sure to include shadowed public instance methods of this class - public_instance_methods(false).map { |m| m.to_s } - + public_instance_methods(false)).uniq.map { |x| x.to_s } - # And always exclude explicitly hidden actions - hidden_actions + hidden_actions.to_a # Clear out AS callback method pollution methods.reject { |method| method =~ /_one_time_conditions/ } @@ -84,7 +86,7 @@ module AbstractController # controller_name. # # ==== Returns - # String + # * string def controller_path @controller_path ||= name.sub(/Controller$/, '').underscore unless anonymous? end @@ -104,12 +106,12 @@ module AbstractController # ActionNotFound error is raised. # # ==== Returns - # self + # * self def process(action, *args) @_action_name = action_name = action.to_s unless action_name = method_for_action(action_name) - raise ActionNotFound, "The action '#{action}' could not be found for #{self.class.name}" + raise ActionNotFound, "The action '#{action}' could not be found for #{self.class.name}" end @_response_body = nil @@ -133,10 +135,10 @@ module AbstractController # can be considered an action. # # ==== Parameters - # name:: The name of an action to be tested + # * name - The name of an action to be tested # # ==== Returns - # TrueClass, FalseClass + # * TrueClass, FalseClass def action_method?(name) self.class.action_methods.include?(name) end @@ -180,11 +182,11 @@ module AbstractController # returns nil, an ActionNotFound exception will be raised. # # ==== Parameters - # action_name:: An action name to find a method name for + # * action_name - An action name to find a method name for # # ==== Returns - # String:: The name of the method that handles the action - # nil:: No method name could be found. Raise ActionNotFound. + # * string - The name of the method that handles the action + # * nil - No method name could be found. Raise ActionNotFound. def method_for_action(action_name) if action_method?(action_name) then action_name elsif respond_to?(:action_missing, true) then "_handle_action_missing" diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index 67efeb7063..1943ca4436 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -13,8 +13,8 @@ module AbstractController # Override AbstractController::Base's process_action to run the # process_action callbacks around the normal behavior. - def process_action(method_name) - run_callbacks(:process_action, method_name) do + def process_action(method_name, *args) + run_callbacks(:process_action, action_name) do super end end @@ -28,9 +28,8 @@ module AbstractController # a Rails process. # # ==== Options - # :only<#to_s>:: The callback should be run only for this action - # :except<#to_s>:: The callback should be run for all actions - # except this action + # * only - The callback should be run only for this action + # * except - The callback should be run for all actions except this action def _normalize_callback_options(options) if only = options[:only] only = Array(only).map {|o| "action_name == '#{o}'"}.join(" || ") @@ -45,7 +44,7 @@ module AbstractController # Skip before, after, and around filters matching any of the names # # ==== Parameters - # *names:: A list of valid names that could be used for + # * names - A list of valid names that could be used for # callbacks. Note that skipping uses Ruby equality, so it's # impossible to skip a callback defined using an anonymous proc # using #skip_filter @@ -60,13 +59,13 @@ module AbstractController # the normalization across several methods that use it. # # ==== Parameters - # callbacks:: A list of callbacks, with an optional + # * callbacks - An array of callbacks, with an optional # options hash as the last parameter. - # block:: A proc that should be added to the callbacks. + # * block - A proc that should be added to the callbacks. # # ==== Block Parameters - # name:: The callback to be added - # options:: A list of options to be used when adding the callback + # * name - The callback to be added + # * options - A hash of options to be used when adding the callback def _insert_callbacks(callbacks, block) options = callbacks.last.is_a?(Hash) ? callbacks.pop : {} _normalize_callback_options(options) @@ -84,6 +83,7 @@ module AbstractController # for details on the allowed parameters. def #{filter}_filter(*names, &blk) _insert_callbacks(names, blk) do |name, options| + options[:if] = (Array.wrap(options[:if]) << "!halted") if #{filter == :after} set_callback(:process_action, :#{filter}, name, options) end end @@ -92,6 +92,7 @@ module AbstractController # for details on the allowed parameters. def prepend_#{filter}_filter(*names, &blk) _insert_callbacks(names, blk) do |name, options| + options[:if] = (Array.wrap(options[:if]) << "!halted") if #{filter == :after} set_callback(:process_action, :#{filter}, name, options.merge(:prepend => true)) end end diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb index 4374b439d0..20f8601a8e 100644 --- a/actionpack/lib/abstract_controller/helpers.rb +++ b/actionpack/lib/abstract_controller/helpers.rb @@ -9,6 +9,9 @@ module AbstractController included do class_attribute :_helpers self._helpers = Module.new + + class_attribute :_helper_methods + self._helper_methods = Array.new end module ClassMethods @@ -40,10 +43,13 @@ module AbstractController # <% if logged_in? -%>Welcome, <%= current_user.name %><% end -%> # # ==== Parameters - # meths:: The name of a method on the controller + # * method[, method] - A name or names of a method on the controller # to be made available on the view. def helper_method(*meths) - meths.flatten.each do |meth| + meths.flatten! + self._helper_methods += meths + + meths.each do |meth| _helpers.class_eval <<-ruby_eval, __FILE__, __LINE__ + 1 def #{meth}(*args, &blk) controller.send(%(#{meth}), *args, &blk) @@ -55,8 +61,8 @@ module AbstractController # The +helper+ class method can take a series of helper module names, a block, or both. # # ==== Parameters - # *args - # block:: A block defining helper methods + # * *args - Module, Symbol, String, :all + # * block - A block defining helper methods # # ==== Examples # When the argument is a module it will be included directly in the template class. @@ -95,12 +101,23 @@ module AbstractController _helpers.module_eval(&block) if block_given? end + # Clears up all existing helpers in this class, only keeping the helper + # with the same name as this class. + def clear_helpers + inherited_helper_methods = _helper_methods + self._helpers = Module.new + self._helper_methods = Array.new + + inherited_helper_methods.each { |meth| helper_method meth } + default_helper_module! unless anonymous? + end + private # Makes all the (instance) methods in the helper module available to templates # rendered through this controller. # # ==== Parameters - # mod:: The module to include into the current helper module + # * module - The module to include into the current helper module # for the class def add_template_helper(mod) _helpers.module_eval { include mod } @@ -118,10 +135,10 @@ module AbstractController # are returned. # # ==== Parameters - # args:: A list of helpers + # * args - An array of helpers # # ==== Returns - # Array[Module]:: A normalized list of modules for the list of + # * Array - A normalized list of modules for the list of # helpers provided. def modules_for_helpers(args) args.flatten.map! do |arg| diff --git a/actionpack/lib/abstract_controller/layouts.rb b/actionpack/lib/abstract_controller/layouts.rb index 5cd7a90ab5..4ee54474cc 100644 --- a/actionpack/lib/abstract_controller/layouts.rb +++ b/actionpack/lib/abstract_controller/layouts.rb @@ -114,11 +114,13 @@ module AbstractController # # class WeblogController < ActionController::Base # layout proc{ |controller| controller.logged_in? ? "writer_layout" : "reader_layout" } + # end # # Of course, the most common way of specifying a layout is still just as a plain template name: # # class WeblogController < ActionController::Base # layout "weblog_standard" + # end # # If no directory is specified for the template name, the template will by default be looked for in app/views/layouts/. # Otherwise, it will be looked up relative to the template root. @@ -183,7 +185,7 @@ module AbstractController # layout. # # ==== Returns - # Boolean:: True if the action has a layout, false otherwise. + # * Boolean - True if the action has a layout, false otherwise. def action_has_layout? return unless super @@ -209,11 +211,11 @@ module AbstractController # true:: raise an ArgumentError # # ==== Parameters - # layout:: The layout to use. + # * String, Symbol, false - The layout to use. # # ==== Options (conditions) - # :only<#to_s, Array[#to_s]>:: A list of actions to apply this layout to. - # :except<#to_s, Array[#to_s]>:: Apply this layout to all actions but this one + # * :only - A list of actions to apply this layout to. + # * :except - Apply this layout to all actions but this one. def layout(layout, conditions = {}) include LayoutConditions unless conditions.empty? @@ -228,18 +230,15 @@ module AbstractController # value of this method. # # ==== Returns - # String:: A template name + # * String - A template name def _implied_layout_name controller_path end - # Takes the specified layout and creates a _layout method to be called - # by _default_layout + # Creates a _layout method to be called by _default_layout . # - # If there is no explicit layout specified: - # If a layout is found in the view paths with the controller's - # name, return that string. Otherwise, use the superclass' - # layout (which might also be implied) + # If a layout is not explicitly mentioned then look for a layout with the controller's name. + # if nothing is found then try same procedure to find super class's layout. def _write_layout_method remove_possible_method(:_layout) @@ -266,11 +265,11 @@ module AbstractController raise ArgumentError, "Layouts must be specified as a String, Symbol, false, or nil" when nil if name - _prefix = "layouts" unless _implied_layout_name =~ /\blayouts/ + _prefixes = _implied_layout_name =~ /\blayouts/ ? [] : ["layouts"] self.class_eval <<-RUBY, __FILE__, __LINE__ + 1 def _layout - if template_exists?("#{_implied_layout_name}", #{_prefix.inspect}) + if template_exists?("#{_implied_layout_name}", #{_prefixes.inspect}) "#{_implied_layout_name}" else super @@ -313,8 +312,8 @@ module AbstractController # the name type. # # ==== Parameters - # name:: The name of the template - # details Object}>:: A list of details to restrict + # * name - The name of the template + # * details - A list of details to restrict # the lookup to. By default, layout lookup is limited to the # formats specified for the current request. def _layout_for_option(name) @@ -333,20 +332,19 @@ module AbstractController # Optionally raises an exception if the layout could not be found. # # ==== Parameters - # details:: A list of details to restrict the search by. This + # * details - A list of details to restrict the search by. This # might include details like the format or locale of the template. - # require_layout:: If this is true, raise an ArgumentError + # * require_logout - If this is true, raise an ArgumentError # with details about the fact that the exception could not be # found (defaults to false) # # ==== Returns - # Template:: The template object for the default layout (or nil) + # * template - The template object for the default layout (or nil) def _default_layout(require_layout = false) begin layout_name = _layout if action_has_layout? rescue NameError => e - raise NoMethodError, - "You specified #{@_layout.inspect} as the layout, but no such method was found" + raise e, "Could not render layout: #{e.message}" end if require_layout && action_has_layout? && !layout_name diff --git a/actionpack/lib/abstract_controller/railties/routes_helpers.rb b/actionpack/lib/abstract_controller/railties/routes_helpers.rb new file mode 100644 index 0000000000..dec1e9d6d9 --- /dev/null +++ b/actionpack/lib/abstract_controller/railties/routes_helpers.rb @@ -0,0 +1,18 @@ +module AbstractController + module Railties + module RoutesHelpers + def self.with(routes) + Module.new do + define_method(:inherited) do |klass| + super(klass) + if namespace = klass.parents.detect {|m| m.respond_to?(:_railtie) } + klass.send(:include, namespace._railtie.routes.url_helpers) + else + klass.send(:include, routes.url_helpers) + end + end + end + end + end + end +end diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index b81d5954eb..691310d5d2 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -12,16 +12,16 @@ module AbstractController # This is a class to fix I18n global state. Whenever you provide I18n.locale during a request, # it will trigger the lookup_context and consequently expire the cache. - # TODO Add some deprecation warnings to remove I18n.locale from controllers class I18nProxy < ::I18n::Config #:nodoc: - attr_reader :i18n_config, :lookup_context + attr_reader :original_config, :lookup_context - def initialize(i18n_config, lookup_context) - @i18n_config, @lookup_context = i18n_config, lookup_context + def initialize(original_config, lookup_context) + original_config = original_config.original_config if original_config.respond_to?(:original_config) + @original_config, @lookup_context = original_config, lookup_context end def locale - @i18n_config.locale + @original_config.locale end def locale=(value) @@ -47,19 +47,34 @@ module AbstractController @view_context_class ||= begin controller = self Class.new(ActionView::Base) do + if controller.respond_to?(:_routes) && controller._routes + include controller._routes.url_helpers + include controller._routes.mounted_helpers + end + if controller.respond_to?(:_helpers) include controller._helpers - if controller.respond_to?(:_routes) - include controller._routes.url_helpers - end - # TODO: Fix RJS to not require this self.helpers = controller._helpers end end end end + + def parent_prefixes + @parent_prefixes ||= begin + parent_controller = superclass + prefixes = [] + + until parent_controller.abstract? + prefixes << parent_controller.controller_path + parent_controller = parent_controller.superclass + end + + prefixes + end + end end attr_writer :view_context_class @@ -98,7 +113,7 @@ module AbstractController def render_to_string(*args, &block) options = _normalize_args(*args, &block) _normalize_options(options) - render_to_body(options) + render_to_body(options).tap { self.response_body = nil } end # Raw rendering of a template to a Rack-compatible body. @@ -114,12 +129,15 @@ module AbstractController view_context.render(options) end - # The prefix used in render "foo" shortcuts. - def _prefix - controller_path + # The prefixes used in render "foo" shortcuts. + def _prefixes + @_prefixes ||= begin + parent_prefixes = self.class.parent_prefixes + parent_prefixes.dup.unshift(controller_path) + end end - private + private # This method should return a hash with assigns. # You can overwrite this configuration per controller. @@ -128,7 +146,7 @@ module AbstractController hash = {} variables = instance_variable_names variables -= protected_instance_variables if respond_to?(:protected_instance_variables) - variables.each { |name| hash[name.to_s[1..-1]] = instance_variable_get(name) } + variables.each { |name| hash[name.to_s[1, name.length]] = instance_variable_get(name) } hash end @@ -138,13 +156,13 @@ module AbstractController case action when NilClass when Hash - options, action = action, nil + options = action when String, Symbol action = action.to_s key = action.include?(?/) ? :file : :action options[key] = action else - options.merge!(:partial => action) + options[:partial] = action end options @@ -155,8 +173,8 @@ module AbstractController options[:partial] = action_name end - if (options.keys & [:partial, :file, :template]).empty? - options[:prefix] ||= _prefix + if (options.keys & [:partial, :file, :template, :once]).empty? + options[:prefixes] ||= _prefixes end options[:template] ||= (options[:action] || action_name).to_s diff --git a/actionpack/lib/abstract_controller/url_for.rb b/actionpack/lib/abstract_controller/url_for.rb new file mode 100644 index 0000000000..e5d5bef6b4 --- /dev/null +++ b/actionpack/lib/abstract_controller/url_for.rb @@ -0,0 +1,27 @@ +module AbstractController + module UrlFor + extend ActiveSupport::Concern + include ActionDispatch::Routing::UrlFor + + def _routes + raise "In order to use #url_for, you must include routing helpers explicitly. " \ + "For instance, `include Rails.application.routes.url_helpers" + end + + module ClassMethods + def _routes + nil + end + + def action_methods + @action_methods ||= begin + if _routes + super - _routes.named_routes.helper_names + else + super + end + end + end + end + end +end diff --git a/actionpack/lib/abstract_controller/view_paths.rb b/actionpack/lib/abstract_controller/view_paths.rb index b552a649d1..cea0f5ad1e 100644 --- a/actionpack/lib/abstract_controller/view_paths.rb +++ b/actionpack/lib/abstract_controller/view_paths.rb @@ -34,9 +34,9 @@ module AbstractController # Append a path to the list of view paths for this controller. # # ==== Parameters - # path:: If a String is provided, it gets converted into - # the default view path. You may also provide a custom view path - # (see ActionView::ViewPathSet for more information) + # * path - If a String is provided, it gets converted into + # the default view path. You may also provide a custom view path + # (see ActionView::PathSet for more information) def append_view_path(path) self.view_paths = view_paths.dup + Array(path) end @@ -44,9 +44,9 @@ module AbstractController # Prepend a path to the list of view paths for this controller. # # ==== Parameters - # path:: If a String is provided, it gets converted into - # the default view path. You may also provide a custom view path - # (see ActionView::ViewPathSet for more information) + # * path - If a String is provided, it gets converted into + # the default view path. You may also provide a custom view path + # (see ActionView::PathSet for more information) def prepend_view_path(path) self.view_paths = Array(path) + view_paths.dup end @@ -59,8 +59,8 @@ module AbstractController # Set the view paths. # # ==== Parameters - # paths:: If a ViewPathSet is provided, use that; - # otherwise, process the parameter into a ViewPathSet. + # * paths - If a PathSet is provided, use that; + # otherwise, process the parameter into a PathSet. def view_paths=(paths) self._view_paths = ActionView::Base.process_view_paths(paths) self._view_paths.freeze diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index ca0e5d6ff6..5b81cd39f4 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -34,7 +34,6 @@ module ActionController autoload :UrlFor end - autoload :Dispatcher, 'action_controller/deprecated/dispatcher' autoload :Integration, 'action_controller/deprecated/integration_test' autoload :IntegrationTest, 'action_controller/deprecated/integration_test' autoload :PerformanceTest, 'action_controller/deprecated/performance_test' @@ -73,4 +72,5 @@ require 'active_support/core_ext/load_error' require 'active_support/core_ext/module/attr_internal' require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/name_error' +require 'active_support/core_ext/uri' require 'active_support/inflector' diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 9dfffced75..81c0698fb8 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -1,6 +1,171 @@ require "action_controller/log_subscriber" module ActionController + # Action Controllers are the core of a web request in \Rails. They are made up of one or more actions that are executed + # on request and then either render a template or redirect to another action. An action is defined as a public method + # on the controller, which will automatically be made accessible to the web-server through \Rails Routes. + # + # By default, only the ApplicationController in a \Rails application inherits from ActionController::Base. All other + # controllers in turn inherit from ApplicationController. This gives you one class to configure things such as + # request forgery protection and filtering of sensitive request parameters. + # + # A sample controller could look like this: + # + # class PostsController < ApplicationController + # def index + # @posts = Post.all + # end + # + # def create + # @post = Post.create params[:post] + # redirect_to posts_path + # end + # end + # + # Actions, by default, render a template in the app/views directory corresponding to the name of the controller and action + # after executing code in the action. For example, the +index+ action of the PostsController would render the + # template app/views/posts/index.html.erb by default after populating the @posts instance variable. + # + # Unlike index, the create action will not render a template. After performing its main purpose (creating a + # new post), it initiates a redirect instead. This redirect works by returning an external + # "302 Moved" HTTP response that takes the user to the index action. + # + # These two methods represent the two basic action archetypes used in Action Controllers. Get-and-show and do-and-redirect. + # Most actions are variations of these themes. + # + # == Requests + # + # For every request, the router determines the value of the +controller+ and +action+ keys. These determine which controller + # and action are called. The remaining request parameters, the session (if one is available), and the full request with + # all the HTTP headers are made available to the action through accessor methods. Then the action is performed. + # + # The full request object is available via the request accessor and is primarily used to query for HTTP headers: + # + # def server_ip + # location = request.env["SERVER_ADDR"] + # render :text => "This server hosted at #{location}" + # end + # + # == Parameters + # + # All request parameters, whether they come from a GET or POST request, or from the URL, are available through the params method + # which returns a hash. For example, an action that was performed through /posts?category=All&limit=5 will include + # { "category" => "All", "limit" => 5 } in params. + # + # It's also possible to construct multi-dimensional parameter hashes by specifying keys using brackets, such as: + # + # + # + # + # A request stemming from a form holding these inputs will include { "post" => { "name" => "david", "address" => "hyacintvej" } }. + # If the address input had been named "post[address][street]", the params would have included + # { "post" => { "address" => { "street" => "hyacintvej" } } }. There's no limit to the depth of the nesting. + # + # == Sessions + # + # Sessions allows you to store objects in between requests. This is useful for objects that are not yet ready to be persisted, + # such as a Signup object constructed in a multi-paged process, or objects that don't change much and are needed all the time, such + # as a User object for a system that requires login. The session should not be used, however, as a cache for objects where it's likely + # they could be changed unknowingly. It's usually too much work to keep it all synchronized -- something databases already excel at. + # + # You can place objects in the session by using the session method, which accesses a hash: + # + # session[:person] = Person.authenticate(user_name, password) + # + # And retrieved again through the same hash: + # + # Hello #{session[:person]} + # + # For removing objects from the session, you can either assign a single key to +nil+: + # + # # removes :person from session + # session[:person] = nil + # + # or you can remove the entire session with +reset_session+. + # + # Sessions are stored by default in a browser cookie that's cryptographically signed, but unencrypted. + # This prevents the user from tampering with the session but also allows him to see its contents. + # + # Do not put secret information in cookie-based sessions! + # + # Other options for session storage: + # + # * ActiveRecord::SessionStore - Sessions are stored in your database, which works better than PStore with multiple app servers and, + # unlike CookieStore, hides your session contents from the user. To use ActiveRecord::SessionStore, set + # + # MyApplication::Application.config.session_store :active_record_store + # + # in your config/initializers/session_store.rb and run script/rails g session_migration. + # + # == Responses + # + # Each action results in a response, which holds the headers and document to be sent to the user's browser. The actual response + # object is generated automatically through the use of renders and redirects and requires no user intervention. + # + # == Renders + # + # Action Controller sends content to the user by using one of five rendering methods. The most versatile and common is the rendering + # of a template. Included in the Action Pack is the Action View, which enables rendering of ERb templates. It's automatically configured. + # The controller passes objects to the view by assigning instance variables: + # + # def show + # @post = Post.find(params[:id]) + # end + # + # Which are then automatically available to the view: + # + # Title: <%= @post.title %> + # + # You don't have to rely on the automated rendering. Especially actions that could result in the rendering of different templates will use + # the manual rendering methods: + # + # def search + # @results = Search.find(params[:query]) + # case @results + # when 0 then render :action => "no_results" + # when 1 then render :action => "show" + # when 2..10 then render :action => "show_many" + # end + # end + # + # Read more about writing ERb and Builder templates in ActionView::Base. + # + # == Redirects + # + # Redirects are used to move from one action to another. For example, after a create action, which stores a blog entry to a database, + # we might like to show the user the new entry. Because we're following good DRY principles (Don't Repeat Yourself), we're going to reuse (and redirect to) + # a show action that we'll assume has already been created. The code might look like this: + # + # def create + # @entry = Entry.new(params[:entry]) + # if @entry.save + # # The entry was saved correctly, redirect to show + # redirect_to :action => 'show', :id => @entry.id + # else + # # things didn't go so well, do something else + # end + # end + # + # In this case, after saving our new entry to the database, the user is redirected to the show method which is then executed. + # + # Learn more about redirect_to and what options you have in ActionController::Redirecting. + # + # == Calling multiple redirects or renders + # + # An action may contain only a single render or a single redirect. Attempting to try to do either again will result in a DoubleRenderError: + # + # def do_something + # redirect_to :action => "elsewhere" + # render :action => "overthere" # raises DoubleRenderError + # end + # + # If you need to redirect on the condition of something, then be sure to add "and return" to halt execution. + # + # def do_something + # redirect_to(:action => "elsewhere") and return if monkeys.nil? + # render :action => "overthere" # won't be called if monkeys is nil + # end + # class Base < Metal abstract! @@ -58,13 +223,6 @@ module ActionController # Rails 2.x compatibility include ActionController::Compatibility - def self.inherited(klass) - super - klass.helper :all - end - ActiveSupport.run_load_hooks(:action_controller, self) end end - -require "action_controller/deprecated/base" diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb index 4105f9e14f..14137f2886 100644 --- a/actionpack/lib/action_controller/caching.rb +++ b/actionpack/lib/action_controller/caching.rb @@ -3,7 +3,7 @@ require 'uri' require 'set' module ActionController #:nodoc: - # Caching is a cheap way of speeding up slow applications by keeping the result of + # \Caching is a cheap way of speeding up slow applications by keeping the result of # calculations, renderings, and database calls around for subsequent requests. # Action Controller affords you three approaches in varying levels of granularity: # Page, Action, Fragment. @@ -14,7 +14,7 @@ module ActionController #:nodoc: # Note: To turn off all caching and sweeping, set # config.action_controller.perform_caching = false. # - # == Caching stores + # == \Caching stores # # All the caching stores from ActiveSupport::Cache are available to be used as backends # for Action Controller caching. This setting only affects action and fragment caching diff --git a/actionpack/lib/action_controller/caching/actions.rb b/actionpack/lib/action_controller/caching/actions.rb index 546f043c58..2c8a6e4d4d 100644 --- a/actionpack/lib/action_controller/caching/actions.rb +++ b/actionpack/lib/action_controller/caching/actions.rb @@ -4,53 +4,58 @@ module ActionController #:nodoc: module Caching # Action caching is similar to page caching by the fact that the entire # output of the response is cached, but unlike page caching, every - # request still goes through the Action Pack. The key benefit - # of this is that filters are run before the cache is served, which - # allows for authentication and other restrictions on whether someone - # is allowed to see the cache. Example: + # request still goes through Action Pack. The key benefit of this is + # that filters run before the cache is served, which allows for + # authentication and other restrictions on whether someone is allowed + # to execute such action. Example: # # class ListsController < ApplicationController # before_filter :authenticate, :except => :public + # # caches_page :public - # caches_action :index, :show, :feed + # caches_action :index, :show # end # - # In this example, the public action doesn't require authentication, - # so it's possible to use the faster page caching method. But both - # the show and feed action are to be shielded behind the authenticate - # filter, so we need to implement those as action caches. + # In this example, the +public+ action doesn't require authentication + # so it's possible to use the faster page caching. On the other hand + # +index+ and +show+ require authentication. They can still be cached, + # but we need action caching for them. # - # Action caching internally uses the fragment caching and an around - # filter to do the job. The fragment cache is named according to both - # the current host and the path. So a page that is accessed at - # http://david.somewhere.com/lists/show/1 will result in a fragment named - # "david.somewhere.com/lists/show/1". This allows the cacher to - # differentiate between "david.somewhere.com/lists/" and - # "jamis.somewhere.com/lists/" -- which is a helpful way of assisting + # Action caching uses fragment caching internally and an around + # filter to do the job. The fragment cache is named according to + # the host and path of the request. A page that is accessed at + # http://david.example.com/lists/show/1 will result in a fragment named + # david.example.com/lists/show/1. This allows the cacher to + # differentiate between david.example.com/lists/ and + # jamis.example.com/lists/ -- which is a helpful way of assisting # the subdomain-as-account-key pattern. # # Different representations of the same resource, e.g. - # http://david.somewhere.com/lists and - # http://david.somewhere.com/lists.xml + # http://david.example.com/lists and + # http://david.example.com/lists.xml # are treated like separate requests and so are cached separately. # Keep in mind when expiring an action cache that # :action => 'lists' is not the same as # :action => 'list', :format => :xml. # # You can set modify the default action cache path by passing a - # :cache_path option. This will be passed directly to - # ActionCachePath.path_for. This is handy for actions with multiple - # possible routes that should be cached differently. If a block is - # given, it is called with the current controller instance. + # :cache_path option. This will be passed directly to + # ActionCachePath.path_for. This is handy for actions with + # multiple possible routes that should be cached differently. If a + # block is given, it is called with the current controller instance. # - # And you can also use :if (or :unless) to pass a Proc that - # specifies when the action should be cached. + # And you can also use :if (or :unless) to pass a + # proc that specifies when the action should be cached. # - # Finally, if you are using memcached, you can also pass :expires_in. + # Finally, if you are using memcached, you can also pass :expires_in. + # + # The following example depicts some of the points made above: # # class ListsController < ApplicationController # before_filter :authenticate, :except => :public - # caches_page :public + # + # caches_page :public + # # caches_action :index, :if => proc do |c| # !c.request.format.json? # cache if is not a JSON request # end @@ -58,19 +63,28 @@ module ActionController #:nodoc: # caches_action :show, :cache_path => { :project => 1 }, # :expires_in => 1.hour # - # caches_action :feed, :cache_path => proc do |controller| - # if controller.params[:user_id] - # controller.send(:user_list_url, - # controller.params[:user_id], controller.params[:id]) + # caches_action :feed, :cache_path => proc do |c| + # if c.params[:user_id] + # c.send(:user_list_url, + # c.params[:user_id], c.params[:id]) # else - # controller.send(:list_url, controller.params[:id]) + # c.send(:list_url, c.params[:id]) # end # end # end # - # If you pass :layout => false, it will only cache your action - # content. It is useful when your layout has dynamic information. + # If you pass :layout => false, it will only cache your action + # content. That's useful when your layout has dynamic information. # + # Warning: If the format of the request is determined by the Accept HTTP + # header the Content-Type of the cached response could be wrong because + # no information about the MIME type is stored in the cache key. So, if + # you first ask for MIME type M in the Accept header, a cache entry is + # created, and then perform a second request to the same resource asking + # for a different MIME type, you'd get the content cached for M. + # + # The :format parameter is taken into account though. The safest + # way to cache by MIME type is to pass the format in the route. module Actions extend ActiveSupport::Concern @@ -89,12 +103,14 @@ module ActionController #:nodoc: end def _save_fragment(name, options) - return unless caching_allowed? - content = response_body content = content.join if content.is_a?(Array) - write_fragment(name, content, options) + if caching_allowed? + write_fragment(name, content, options) + else + content + end end protected @@ -144,7 +160,7 @@ module ActionController #:nodoc: attr_reader :path, :extension # If +infer_extension+ is true, the cache path extension is looked up from the request's - # path & format. This is desirable when reading and writing the cache, but not when + # path and format. This is desirable when reading and writing the cache, but not when # expiring the cache - expire_action should expire the same files regardless of the # request format. def initialize(controller, options = {}, infer_extension = true) @@ -161,7 +177,7 @@ module ActionController #:nodoc: def normalize!(path) path << 'index' if path[-1] == ?/ path << ".#{extension}" if extension and !path.ends_with?(extension) - URI.unescape(path) + URI.parser.unescape(path) end end end diff --git a/actionpack/lib/action_controller/caching/fragments.rb b/actionpack/lib/action_controller/caching/fragments.rb index 460273dac1..0be04b70a1 100644 --- a/actionpack/lib/action_controller/caching/fragments.rb +++ b/actionpack/lib/action_controller/caching/fragments.rb @@ -1,52 +1,72 @@ module ActionController #:nodoc: module Caching - # Fragment caching is used for caching various blocks within templates without caching the entire action as a whole. This is useful when - # certain elements of an action change frequently or depend on complicated state while other parts rarely change or can be shared amongst multiple - # parties. The caching is done using the cache helper available in the Action View. A template with caching might look something like: + # Fragment caching is used for caching various blocks within + # views without caching the entire action as a whole. This is + # useful when certain elements of an action change frequently or + # depend on complicated state while other parts rarely change or + # can be shared amongst multiple parties. The caching is done using + # the cache helper available in the Action View. A + # template with fragment caching might look like: # # Hello <%= @name %> + # # <% cache do %> # All the topics in the system: # <%= render :partial => "topic", :collection => Topic.find(:all) %> # <% end %> # - # This cache will bind to the name of the action that called it, so if this code was part of the view for the topics/list action, you would - # be able to invalidate it using expire_fragment(:controller => "topics", :action => "list"). + # This cache will bind the name of the action that called it, so if + # this code was part of the view for the topics/list action, you + # would be able to invalidate it using: + # + # expire_fragment(:controller => "topics", :action => "list") # - # This default behavior is of limited use if you need to cache multiple fragments per action or if the action itself is cached using - # caches_action, so we also have the option to qualify the name of the cached fragment with something like: + # This default behavior is limited if you need to cache multiple + # fragments per action or if the action itself is cached using + # caches_action. To remedy this, there is an option to + # qualify the name of the cached fragment by using the + # :action_suffix option: # # <% cache(:action => "list", :action_suffix => "all_topics") do %> # - # That would result in a name such as "/topics/list/all_topics", avoiding conflicts with the action cache and with any fragments that use a - # different suffix. Note that the URL doesn't have to really exist or be callable - the url_for system is just used to generate unique - # cache names that we can refer to when we need to expire the cache. + # That would result in a name such as + # /topics/list/all_topics, avoiding conflicts with the + # action cache and with any fragments that use a different suffix. + # Note that the URL doesn't have to really exist or be callable + # - the url_for system is just used to generate unique cache names + # that we can refer to when we need to expire the cache. # # The expiration call for this example is: # - # expire_fragment(:controller => "topics", :action => "list", :action_suffix => "all_topics") + # expire_fragment(:controller => "topics", + # :action => "list", + # :action_suffix => "all_topics") module Fragments - # Given a key (as described in expire_fragment), returns a key suitable for use in reading, - # writing, or expiring a cached fragment. If the key is a hash, the generated key is the return - # value of url_for on that hash (without the protocol). All keys are prefixed with "views/" and uses + # Given a key (as described in expire_fragment), returns + # a key suitable for use in reading, writing, or expiring a + # cached fragment. If the key is a hash, the generated key is the + # return value of url_for on that hash (without the protocol). + # All keys are prefixed with views/ and uses # ActiveSupport::Cache.expand_cache_key for the expansion. def fragment_cache_key(key) ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views) end - # Writes content to the location signified by key (see expire_fragment for acceptable formats) + # Writes content to the location signified by + # key (see expire_fragment for acceptable formats). def write_fragment(key, content, options = nil) return content unless cache_configured? key = fragment_cache_key(key) instrument_fragment_cache :write_fragment, key do - content = content.html_safe.to_str if content.respond_to?(:html_safe) + content = content.to_str cache_store.write(key, content, options) end content end - # Reads a cached fragment from the location signified by key (see expire_fragment for acceptable formats) + # Reads a cached fragment from the location signified by key + # (see expire_fragment for acceptable formats). def read_fragment(key, options = nil) return unless cache_configured? @@ -57,7 +77,8 @@ module ActionController #:nodoc: end end - # Check if a cached fragment from the location signified by key exists (see expire_fragment for acceptable formats) + # Check if a cached fragment from the location signified by + # key exists (see expire_fragment for acceptable formats) def fragment_exist?(key, options = nil) return unless cache_configured? key = fragment_cache_key(key) @@ -70,8 +91,9 @@ module ActionController #:nodoc: # Removes fragments from the cache. # # +key+ can take one of three forms: + # # * String - This would normally take the form of a path, like - # "pages/45/notes". + # pages/45/notes. # * Hash - Treated as an implicit call to +url_for+, like # {:controller => "pages", :action => "notes", :id => 45} # * Regexp - Will remove any fragment that matches, so diff --git a/actionpack/lib/action_controller/caching/pages.rb b/actionpack/lib/action_controller/caching/pages.rb index 4f7a5d3f55..8c583c7ce0 100644 --- a/actionpack/lib/action_controller/caching/pages.rb +++ b/actionpack/lib/action_controller/caching/pages.rb @@ -1,5 +1,4 @@ require 'fileutils' -require 'uri' require 'active_support/core_ext/class/attribute_accessors' module ActionController #:nodoc: @@ -71,9 +70,9 @@ module ActionController #:nodoc: # Manually cache the +content+ in the key determined by +path+. Example: # cache_page "I'm the cached content", "/lists/show" - def cache_page(content, path) + def cache_page(content, path, extension = nil) return unless perform_caching - path = page_cache_path(path) + path = page_cache_path(path, extension) instrument_page_cache :write_page, path do FileUtils.makedirs(File.dirname(path)) @@ -98,14 +97,16 @@ module ActionController #:nodoc: end private - def page_cache_file(path) - name = (path.empty? || path == "/") ? "/index" : URI.unescape(path.chomp('/')) - name << page_cache_extension unless (name.split('/').last || name).include? '.' + def page_cache_file(path, extension) + name = (path.empty? || path == "/") ? "/index" : URI.parser.unescape(path.chomp('/')) + unless (name.split('/').last || name).include? '.' + name << (extension || self.page_cache_extension) + end return name end - def page_cache_path(path) - page_cache_directory + page_cache_file(path) + def page_cache_path(path, extension = nil) + page_cache_directory.to_s + page_cache_file(path, extension) end def instrument_page_cache(name, path) @@ -135,7 +136,7 @@ module ActionController #:nodoc: # If no options are provided, the requested url is used. Example: # cache_page "I'm the cached content", :controller => "lists", :action => "show" def cache_page(content = nil, options = nil) - return unless self.class.perform_caching && caching_allowed + return unless self.class.perform_caching && caching_allowed? path = case options when Hash @@ -146,13 +147,13 @@ module ActionController #:nodoc: request.path end - self.class.cache_page(content || response.body, path) + if (type = Mime::LOOKUP[self.content_type]) && (type_symbol = type.symbol).present? + extension = ".#{type_symbol}" + end + + self.class.cache_page(content || response.body, path, extension) end - private - def caching_allowed - request.get? && response.status.to_i == 200 - end end end end diff --git a/actionpack/lib/action_controller/deprecated.rb b/actionpack/lib/action_controller/deprecated.rb index 9f2de57033..aa0cfc9395 100644 --- a/actionpack/lib/action_controller/deprecated.rb +++ b/actionpack/lib/action_controller/deprecated.rb @@ -1,3 +1,3 @@ ActionController::AbstractRequest = ActionController::Request = ActionDispatch::Request ActionController::AbstractResponse = ActionController::Response = ActionDispatch::Response -ActionController::Routing = ActionDispatch::Routing +ActionController::Routing = ActionDispatch::Routing \ No newline at end of file diff --git a/actionpack/lib/action_controller/deprecated/base.rb b/actionpack/lib/action_controller/deprecated/base.rb deleted file mode 100644 index 3975afcaf0..0000000000 --- a/actionpack/lib/action_controller/deprecated/base.rb +++ /dev/null @@ -1,133 +0,0 @@ -module ActionController - class Base - # Deprecated methods. Wrap them in a module so they can be overwritten by plugins - # (like the verify method.) - module DeprecatedBehavior #:nodoc: - def relative_url_root - ActiveSupport::Deprecation.warn "ActionController::Base.relative_url_root is ineffective. " << - "Please stop using it.", caller - end - - def relative_url_root= - ActiveSupport::Deprecation.warn "ActionController::Base.relative_url_root= is ineffective. " << - "Please stop using it.", caller - end - - def consider_all_requests_local - ActiveSupport::Deprecation.warn "ActionController::Base.consider_all_requests_local is deprecated, " << - "use Rails.application.config.consider_all_requests_local instead", caller - Rails.application.config.consider_all_requests_local - end - - def consider_all_requests_local=(value) - ActiveSupport::Deprecation.warn "ActionController::Base.consider_all_requests_local= is deprecated. " << - "Please configure it on your application with config.consider_all_requests_local=", caller - Rails.application.config.consider_all_requests_local = value - end - - def allow_concurrency - ActiveSupport::Deprecation.warn "ActionController::Base.allow_concurrency is deprecated, " << - "use Rails.application.config.allow_concurrency instead", caller - Rails.application.config.allow_concurrency - end - - def allow_concurrency=(value) - ActiveSupport::Deprecation.warn "ActionController::Base.allow_concurrency= is deprecated. " << - "Please configure it on your application with config.allow_concurrency=", caller - Rails.application.config.allow_concurrency = value - end - - def ip_spoofing_check=(value) - ActiveSupport::Deprecation.warn "ActionController::Base.ip_spoofing_check= is deprecated. " << - "Please configure it on your application with config.action_dispatch.ip_spoofing_check=", caller - Rails.application.config.action_dispatch.ip_spoofing_check = value - end - - def ip_spoofing_check - ActiveSupport::Deprecation.warn "ActionController::Base.ip_spoofing_check is deprecated. " << - "Configuring ip_spoofing_check on the application configures a middleware.", caller - Rails.application.config.action_dispatch.ip_spoofing_check - end - - def cookie_verifier_secret=(value) - ActiveSupport::Deprecation.warn "ActionController::Base.cookie_verifier_secret= is deprecated. " << - "Please configure it on your application with config.secret_token=", caller - end - - def cookie_verifier_secret - ActiveSupport::Deprecation.warn "ActionController::Base.cookie_verifier_secret is deprecated.", caller - end - - def trusted_proxies=(value) - ActiveSupport::Deprecation.warn "ActionController::Base.trusted_proxies= is deprecated. " << - "Please configure it on your application with config.action_dispatch.trusted_proxies=", caller - Rails.application.config.action_dispatch.ip_spoofing_check = value - end - - def trusted_proxies - ActiveSupport::Deprecation.warn "ActionController::Base.trusted_proxies is deprecated. " << - "Configuring trusted_proxies on the application configures a middleware.", caller - Rails.application.config.action_dispatch.ip_spoofing_check = value - end - - def session(*args) - ActiveSupport::Deprecation.warn( - "Disabling sessions for a single controller has been deprecated. " + - "Sessions are now lazy loaded. So if you don't access them, " + - "consider them off. You can still modify the session cookie " + - "options with request.session_options.", caller) - end - - def session=(value) - ActiveSupport::Deprecation.warn "ActionController::Base.session= is deprecated. " << - "Please configure it on your application with config.session_store :cookie_store, :key => '....'", caller - if value.delete(:disabled) - Rails.application.config.session_store :disabled - else - store = Rails.application.config.session_store - Rails.application.config.session_store store, value - end - end - - # Controls the resource action separator - def resource_action_separator - @resource_action_separator ||= "/" - end - - def resource_action_separator=(val) - ActiveSupport::Deprecation.warn "ActionController::Base.resource_action_separator is deprecated and only " \ - "works with the deprecated router DSL." - @resource_action_separator = val - end - - def use_accept_header - ActiveSupport::Deprecation.warn "ActionController::Base.use_accept_header doesn't do anything anymore. " \ - "The accept header is always taken into account." - end - - def use_accept_header=(val) - use_accept_header - end - - # This method has been moved to ActionDispatch::Request.filter_parameters - def filter_parameter_logging(*args, &block) - ActiveSupport::Deprecation.warn("Setting filter_parameter_logging in ActionController is deprecated and has no longer effect, please set 'config.filter_parameters' in config/application.rb instead", caller) - filter = Rails.application.config.filter_parameters - filter.concat(args) - filter << block if block - filter - end - - # This was moved to a plugin - def verify(*args) - ActiveSupport::Deprecation.warn "verify was removed from Rails and is now available as a plugin. " << - "Please install it with `rails plugin install git://github.com/rails/verification.git`.", caller - end - end - - extend DeprecatedBehavior - - delegate :consider_all_requests_local, :consider_all_requests_local=, - :allow_concurrency, :allow_concurrency=, :to => :"self.class" - end -end diff --git a/actionpack/lib/action_controller/deprecated/dispatcher.rb b/actionpack/lib/action_controller/deprecated/dispatcher.rb deleted file mode 100644 index 8c21e375dd..0000000000 --- a/actionpack/lib/action_controller/deprecated/dispatcher.rb +++ /dev/null @@ -1,28 +0,0 @@ -module ActionController - class Dispatcher - class << self - def before_dispatch(*args, &block) - ActiveSupport::Deprecation.warn "ActionController::Dispatcher.before_dispatch is deprecated. " << - "Please use ActionDispatch::Callbacks.before instead.", caller - ActionDispatch::Callbacks.before(*args, &block) - end - - def after_dispatch(*args, &block) - ActiveSupport::Deprecation.warn "ActionController::Dispatcher.after_dispatch is deprecated. " << - "Please use ActionDispatch::Callbacks.after instead.", caller - ActionDispatch::Callbacks.after(*args, &block) - end - - def to_prepare(*args, &block) - ActiveSupport::Deprecation.warn "ActionController::Dispatcher.to_prepare is deprecated. " << - "Please use config.to_prepare instead", caller - ActionDispatch::Callbacks.after(*args, &block) - end - - def new - ActiveSupport::Deprecation.warn "ActionController::Dispatcher.new is deprecated, use Rails.application instead." - Rails.application - end - end - end -end diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb index ece270b3ce..3fae697cc3 100644 --- a/actionpack/lib/action_controller/log_subscriber.rb +++ b/actionpack/lib/action_controller/log_subscriber.rb @@ -16,7 +16,11 @@ module ActionController payload = event.payload additions = ActionController::Base.log_process_action(payload) - message = "Completed #{payload[:status]} #{Rack::Utils::HTTP_STATUS_CODES[payload[:status]]} in %.0fms" % event.duration + status = payload[:status] + if status.nil? && payload[:exception].present? + status = Rack::Utils.status_code(ActionDispatch::ShowExceptions.rescue_responses[payload[:exception].first]) rescue nil + end + message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in %.0fms" % event.duration message << " (#{additions.join(" | ")})" unless additions.blank? info(message) @@ -42,7 +46,7 @@ module ActionController def #{method}(event) key_or_path = event.payload[:key] || event.payload[:path] human_name = #{method.to_s.humanize.inspect} - info("\#{human_name} \#{key_or_path} (%.1fms)" % event.duration) + info("\#{human_name} \#{key_or_path} \#{"(%.1fms)" % event.duration}") end METHOD end diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 2281c500c5..e5db31061b 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -12,7 +12,7 @@ module ActionController # class MiddlewareStack < ActionDispatch::MiddlewareStack #:nodoc: class Middleware < ActionDispatch::MiddlewareStack::Middleware #:nodoc: - def initialize(klass, *args) + def initialize(klass, *args, &block) options = args.extract_options! @only = Array(options.delete(:only)).map(&:to_s) @except = Array(options.delete(:except)).map(&:to_s) @@ -36,35 +36,88 @@ module ActionController action = action.to_s raise "MiddlewareStack#build requires an app" unless app - reverse.inject(app) do |a, middleware| + middlewares.reverse.inject(app) do |a, middleware| middleware.valid?(action) ? middleware.build(a) : a end end end - # ActionController::Metal provides a way to get a valid Rack application from a controller. + # ActionController::Metal is the simplest possible controller, providing a + # valid Rack interface without the additional niceties provided by + # ActionController::Base. + # + # A sample metal controller might look like this: + # + # class HelloController < ActionController::Metal + # def index + # self.response_body = "Hello World!" + # end + # end + # + # And then to route requests to your metal controller, you would add + # something like this to config/routes.rb: + # + # match 'hello', :to => HelloController.action(:index) + # + # The +action+ method returns a valid Rack application for the \Rails + # router to dispatch to. + # + # == Rendering Helpers + # + # ActionController::Metal by default provides no utilities for rendering + # views, partials, or other responses aside from explicitly calling of + # response_body=, content_type=, and status=. To + # add the render helpers you're used to having in a normal controller, you + # can do the following: + # + # class HelloController < ActionController::Metal + # include ActionController::Rendering + # append_view_path "#{Rails.root}/app/views" + # + # def index + # render "hello/index" + # end + # end + # + # == Redirection Helpers + # + # To add redirection helpers to your metal controller, do the following: + # + # class HelloController < ActionController::Metal + # include ActionController::Redirecting + # include Rails.application.routes.url_helpers + # + # def index + # redirect_to root_url + # end + # end + # + # == Other Helpers + # + # You can refer to the modules included in ActionController::Base to see + # other features you can bring into your metal controller. # - # In AbstractController, dispatching is triggered directly by calling #process on a new controller. - # ActionController::Metal provides an #action method that returns a valid Rack application for a - # given action. Other rack builders, such as Rack::Builder, Rack::URLMap, and the Rails router, - # can dispatch directly to the action returned by FooController.action(:index). class Metal < AbstractController::Base abstract! - attr_internal :env + attr_internal_writer :env + + def env + @_env ||= {} + end # Returns the last part of the controller's name, underscored, without the ending - # "Controller". For instance, MyApp::MyPostsController would return "my_posts" for - # controller_name + # Controller. For instance, PostsController returns posts. + # Namespaces are left out, so Admin::PostsController returns posts as well. # # ==== Returns - # String + # * string def self.controller_name @controller_name ||= self.name.demodulize.sub(/Controller$/, '').underscore end - # Delegates to the class' #controller_name + # Delegates to the class' controller_name def controller_name self.class.controller_name end @@ -81,6 +134,9 @@ module ActionController def initialize(*) @_headers = {"Content-Type" => "text/html"} @_status = 200 + @_request = nil + @_response = nil + @_routes = nil super end @@ -112,6 +168,11 @@ module ActionController headers["Location"] = url end + # basic url_for that can be overridden for more robust functionality + def url_for(string) + string + end + def status @_status end @@ -121,12 +182,11 @@ module ActionController end def response_body=(val) - body = val.respond_to?(:each) ? val : [val] + body = val.nil? ? nil : (val.respond_to?(:each) ? val : [val]) super body end - # :api: private - def dispatch(name, request) + def dispatch(name, request) #:nodoc: @_request = request @_env = request.env @_env['action_controller.instance'] = self @@ -134,8 +194,7 @@ module ActionController to_a end - # :api: private - def to_a + def to_a #:nodoc: response ? response.to_a : [status, headers, response_body] end @@ -147,8 +206,8 @@ module ActionController super end - def self.use(*args) - middleware_stack.use(*args) + def self.use(*args, &block) + middleware_stack.use(*args, &block) end def self.middleware @@ -164,10 +223,10 @@ module ActionController # for the same action. # # ==== Parameters - # action<#to_s>:: An action name + # * action - An action name # # ==== Returns - # Proc:: A rack application + # * proc - A rack application def self.action(name, klass = ActionDispatch::Request) middleware_stack.build(name.to_s) do |env| new.dispatch(name, klass.new(env)) diff --git a/actionpack/lib/action_controller/metal/compatibility.rb b/actionpack/lib/action_controller/metal/compatibility.rb index d49465fa0b..006b9fd456 100644 --- a/actionpack/lib/action_controller/metal/compatibility.rb +++ b/actionpack/lib/action_controller/metal/compatibility.rb @@ -5,9 +5,6 @@ module ActionController class ::ActionController::ActionControllerError < StandardError #:nodoc: end - module ClassMethods - end - # Temporary hax included do ::ActionController::UnknownAction = ::AbstractController::ActionNotFound @@ -39,12 +36,6 @@ module ActionController def assign_shortcuts(*) end def _normalize_options(options) - if options[:action] && options[:action].to_s.include?(?/) - ActiveSupport::Deprecation.warn "Giving a path to render :action is deprecated. " << - "Please use render :template instead", caller - options[:template] = options.delete(:action) - end - options[:text] = nil if options.delete(:nothing) == true options[:text] = " " if options.key?(:text) && options[:text].nil? super diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index 61e7ece90d..a5e37172c9 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -6,7 +6,7 @@ module ActionController include Head # Sets the etag, last_modified, or both on the response and renders a - # "304 Not Modified" response if the request is already fresh. + # 304 Not Modified response if the request is already fresh. # # Parameters: # * :etag @@ -17,11 +17,11 @@ module ActionController # # def show # @article = Article.find(params[:id]) - # fresh_when(:etag => @article, :last_modified => @article.created_at.utc, :public => true) + # fresh_when(:etag => @article, :last_modified => @article.created_at, :public => true) # end # # This will render the show template if the request isn't sending a matching etag or - # If-Modified-Since header and just a "304 Not Modified" response if there's a match. + # If-Modified-Since header and just a 304 Not Modified response if there's a match. # def fresh_when(options) options.assert_valid_keys(:etag, :last_modified, :public) @@ -36,7 +36,7 @@ module ActionController # Sets the etag and/or last_modified on the response and checks it against # the client request. If the request doesn't match the options provided, the # request is considered stale and should be generated from scratch. Otherwise, - # it's fresh and we don't need to generate anything and a reply of "304 Not Modified" is sent. + # it's fresh and we don't need to generate anything and a reply of 304 Not Modified is sent. # # Parameters: # * :etag @@ -48,7 +48,7 @@ module ActionController # def show # @article = Article.find(params[:id]) # - # if stale?(:etag => @article, :last_modified => @article.created_at.utc) + # if stale?(:etag => @article, :last_modified => @article.created_at) # @statistics = @article.really_expensive_call # respond_to do |format| # # all the supported formats @@ -60,13 +60,13 @@ module ActionController !request.fresh?(response) end - # Sets a HTTP 1.1 Cache-Control header. Defaults to issuing a "private" instruction, so that - # intermediate caches shouldn't cache the response. + # Sets a HTTP 1.1 Cache-Control header. Defaults to issuing a private instruction, so that + # intermediate caches must not cache the response. # # Examples: # expires_in 20.minutes # expires_in 3.hours, :public => true - # expires in 3.hours, 'max-stale' => 5.hours, :public => true + # expires_in 3.hours, 'max-stale' => 5.hours, :public => true # # This method will overwrite an existing Cache-Control header. # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities. @@ -77,7 +77,7 @@ module ActionController response.cache_control[:extras] = options.map {|k,v| "#{k}=#{v}"} end - # Sets a HTTP 1.1 Cache-Control header of "no-cache" so no caching should occur by the browser or + # Sets a HTTP 1.1 Cache-Control header of no-cache so no caching should occur by the browser or # intermediate caches (like caching proxy servers). def expires_now #:doc: response.cache_control.replace(:no_cache => true) diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb index a5c9910d68..8abcad55a2 100644 --- a/actionpack/lib/action_controller/metal/head.rb +++ b/actionpack/lib/action_controller/metal/head.rb @@ -2,8 +2,6 @@ module ActionController module Head extend ActiveSupport::Concern - include ActionController::UrlFor - # Return a response that has no content (merely headers). The options # argument is interpreted to be a hash of header names and values. # This allows you to easily return a response that consists only of @@ -22,13 +20,13 @@ module ActionController location = options.delete(:location) options.each do |key, value| - headers[key.to_s.dasherize.split(/-/).map { |v| v.capitalize }.join("-")] = value.to_s + headers[key.to_s.dasherize.split('-').each { |v| v[0] = v[0].chr.upcase }.join('-')] = value.to_s end self.status = status self.location = url_for(location) if location - self.content_type = Mime[formats.first] + self.content_type = Mime[formats.first] if formats self.response_body = " " end end -end \ No newline at end of file +end diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index e0bc47318a..91a88ab68a 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -2,21 +2,21 @@ require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/class/attribute' module ActionController - # The Rails framework provides a large number of helpers for working with +assets+, +dates+, +forms+, - # +numbers+ and model objects, to name a few. These helpers are available to all templates + # The \Rails framework provides a large number of helpers for working with assets, dates, forms, + # numbers and model objects, to name a few. These helpers are available to all templates # by default. # - # In addition to using the standard template helpers provided in the Rails framework, creating custom helpers to + # In addition to using the standard template helpers provided, creating custom helpers to # extract complicated logic or reusable functionality is strongly encouraged. By default, the controller will # include a helper whose name matches that of the controller, e.g., MyController will automatically # include MyHelper. # - # Additional helpers can be specified using the +helper+ class method in ActionController::Base or any + # Additional helpers can be specified using the +helper+ class method in ActionController::Base or any # controller which inherits from it. # # ==== Examples - # The +to_s+ method from the Time class can be wrapped in a helper method to display a custom message if - # the Time object is blank: + # The +to_s+ method from the \Time class can be wrapped in a helper method to display a custom message if + # a \Time object is blank: # # module FormattedTimeHelper # def format_time(time, format=:long, blank_message=" ") @@ -53,30 +53,20 @@ module ActionController include AbstractController::Helpers included do - config_accessor :helpers_path + config_accessor :helpers_path, :include_all_helpers self.helpers_path ||= [] + self.include_all_helpers = true end module ClassMethods - def helpers_dir - ActiveSupport::Deprecation.warn "helpers_dir is deprecated, use helpers_path instead", caller - self.helpers_path - end - - def helpers_dir=(value) - ActiveSupport::Deprecation.warn "helpers_dir= is deprecated, use helpers_path= instead", caller - self.helpers_path = Array.wrap(value) - end - # Declares helper accessors for controller attributes. For example, the # following adds new +name+ and name= instance methods to a # controller and makes them available to the view: - # helper_attr :name # attr_accessor :name + # helper_attr :name # # ==== Parameters - # *attrs:: Names of attributes to be converted - # into helpers. + # * attrs - Names of attributes to be converted into helpers. def helper_attr(*attrs) attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") } end @@ -88,25 +78,28 @@ module ActionController private # Overwrite modules_for_helpers to accept :all as argument, which loads - # all helpers in helpers_dir. + # all helpers in helpers_path. # # ==== Parameters - # args:: A list of helpers + # * args - A list of helpers # # ==== Returns - # Array[Module]:: A normalized list of modules for the list of - # helpers provided. + # * array - A normalized list of modules for the list of helpers provided. def modules_for_helpers(args) args += all_application_helpers if args.delete(:all) super(args) end - # Extract helper names from files in app/helpers/**/*_helper.rb + # Extract helper names from files in app/helpers/**/*_helper.rb def all_application_helpers + all_helpers_from_path(helpers_path) + end + + def all_helpers_from_path(path) helpers = [] - Array.wrap(helpers_path).each do |path| - extract = /^#{Regexp.quote(path.to_s)}\/?(.*)_helper.rb$/ - helpers += Dir["#{path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') } + Array.wrap(path).each do |_path| + extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/ + helpers += Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') } end helpers.sort! helpers.uniq! diff --git a/actionpack/lib/action_controller/metal/hide_actions.rb b/actionpack/lib/action_controller/metal/hide_actions.rb index 32d7a96701..b55c4643be 100644 --- a/actionpack/lib/action_controller/metal/hide_actions.rb +++ b/actionpack/lib/action_controller/metal/hide_actions.rb @@ -1,8 +1,7 @@ require 'active_support/core_ext/class/attribute' module ActionController - # ActionController::HideActions adds the ability to prevent public methods on a controller - # to be called as actions. + # Adds the ability to prevent public methods on a controller to be called as actions. module HideActions extend ActiveSupport::Concern @@ -23,7 +22,7 @@ module ActionController # Sets all of the actions passed in as hidden actions. # # ==== Parameters - # *args<#to_s>:: A list of actions + # * args - A list of actions def hide_action(*args) self.hidden_actions = hidden_actions.dup.merge(args.map(&:to_s)).freeze end diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index b0eb24a4a8..39c804d707 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -3,9 +3,9 @@ require 'active_support/core_ext/object/blank' module ActionController module HttpAuthentication - # Makes it dead easy to do HTTP Basic authentication. + # Makes it dead easy to do HTTP \Basic and \Digest authentication. # - # Simple Basic example: + # === Simple \Basic example # # class PostsController < ApplicationController # USER_NAME, PASSWORD = "dhh", "secret" @@ -29,7 +29,9 @@ module ActionController # end # # - # Here is a more advanced Basic example where only Atom feeds and the XML API is protected by HTTP authentication, + # === Advanced \Basic example + # + # Here is a more advanced \Basic example where only Atom feeds and the XML API is protected by HTTP authentication, # the regular HTML interface is protected by a session approach: # # class ApplicationController < ActionController::Base @@ -69,7 +71,7 @@ module ActionController # assert_equal 200, status # end # - # Simple Digest example: + # === Simple \Digest example # # require 'digest/md5' # class PostsController < ApplicationController @@ -95,18 +97,20 @@ module ActionController # end # end # - # NOTE: The +authenticate_or_request_with_http_digest+ block must return the user's password or the ha1 digest hash so the framework can appropriately - # hash to check the user's credentials. Returning +nil+ will cause authentication to fail. - # Storing the ha1 hash: MD5(username:realm:password), is better than storing a plain password. If - # the password file or database is compromised, the attacker would be able to use the ha1 hash to - # authenticate as the user at this +realm+, but would not have the user's password to try using at - # other sites. + # === Notes # - # On shared hosts, Apache sometimes doesn't pass authentication headers to - # FCGI instances. If your environment matches this description and you cannot - # authenticate, try this rule in your Apache setup: + # The +authenticate_or_request_with_http_digest+ block must return the user's password + # or the ha1 digest hash so the framework can appropriately hash to check the user's + # credentials. Returning +nil+ will cause authentication to fail. # - # RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L] + # Storing the ha1 hash: MD5(username:realm:password), is better than storing a plain password. If + # the password file or database is compromised, the attacker would be able to use the ha1 hash to + # authenticate as the user at this +realm+, but would not have the user's password to try using at + # other sites. + # + # In rare instances, web servers or front proxies strip authorization headers before + # they reach your application. You can debug this situation by logging all environment + # variables, and check for HTTP_AUTHORIZATION, amongst others. module Basic extend self @@ -210,7 +214,7 @@ module ActionController def encode_credentials(http_method, credentials, password, password_is_ha1) credentials[:response] = expected_response(http_method, credentials[:uri], credentials, password, password_is_ha1) - "Digest " + credentials.sort_by {|x| x[0].to_s }.inject([]) {|a, v| a << "#{v[0]}='#{v[1]}'" }.join(', ') + "Digest " + credentials.sort_by {|x| x[0].to_s }.map {|v| "#{v[0]}='#{v[1]}'" }.join(', ') end def decode_credentials_header(request) @@ -218,11 +222,10 @@ module ActionController end def decode_credentials(header) - header.to_s.gsub(/^Digest\s+/,'').split(',').inject({}) do |hash, pair| + Hash[header.to_s.gsub(/^Digest\s+/,'').split(',').map do |pair| key, value = pair.split('=', 2) - hash[key.strip.to_sym] = value.to_s.gsub(/^"|"$/,'').gsub(/'/, '') - hash - end + [key.strip.to_sym, value.to_s.gsub(/^"|"$/,'').gsub(/'/, '')] + end] end def authentication_header(controller, realm) @@ -392,11 +395,11 @@ module ActionController end end - # If token Authorization header is present, call the login procedure with + # If token Authorization header is present, call the login procedure with # the present token and options. # # controller - ActionController::Base instance for the current request. - # login_procedure - Proc to call if a token is present. The Proc should + # login_procedure - Proc to call if a token is present. The Proc should # take 2 arguments: # authenticate(controller) { |token, options| ... } # @@ -404,7 +407,7 @@ module ActionController # Returns nil if no token is found. def authenticate(controller, &login_procedure) token, options = token_and_options(controller.request) - if !token.blank? + unless token.blank? login_procedure.call(token, options) end end @@ -414,20 +417,19 @@ module ActionController # Authorization: Token token="abc", nonce="def" # Then the returned token is "abc", and the options is {:nonce => "def"} # - # request - ActionController::Request instance with the current headers. + # request - ActionDispatch::Request instance with the current headers. # # Returns an Array of [String, Hash] if a token is present. # Returns nil if no token is found. def token_and_options(request) if header = request.authorization.to_s[/^Token (.*)/] - values = $1.split(','). - inject({}) do |memo, value| - value.strip! # remove any spaces between commas and values - key, value = value.split(/\=\"?/) # split key=value pairs - value.chomp!('"') # chomp trailing " in value - value.gsub!(/\\\"/, '"') # unescape remaining quotes - memo.update(key => value) - end + values = Hash[$1.split(',').map do |value| + value.strip! # remove any spaces between commas and values + key, value = value.split(/\=\"?/) # split key=value pairs + value.chomp!('"') # chomp trailing " in value + value.gsub!(/\\\"/, '"') # unescape remaining quotes + [key, value] + end] [values.delete("token"), values.with_indifferent_access] end end @@ -439,9 +441,8 @@ module ActionController # # Returns String. def encode_credentials(token, options = {}) - values = ["token=#{token.to_s.inspect}"] - options.each do |key, value| - values << "#{key}=#{value.to_s.inspect}" + values = ["token=#{token.to_s.inspect}"] + options.map do |key, value| + "#{key}=#{value.to_s.inspect}" end "Token #{values * ", "}" end diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb index 282dcf66b3..cfa7004048 100644 --- a/actionpack/lib/action_controller/metal/implicit_render.rb +++ b/actionpack/lib/action_controller/metal/implicit_render.rb @@ -12,10 +12,10 @@ module ActionController def method_for_action(action_name) super || begin - if template_exists?(action_name.to_s, _prefix) + if template_exists?(action_name.to_s, _prefixes) "default_render" end end end end -end \ No newline at end of file +end diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb index b08d9a8434..dc3ea939e6 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -78,7 +78,7 @@ module ActionController yield end - # Everytime after an action is processed, this method is invoked + # Every time after an action is processed, this method is invoked # with the payload, so you can add more information. # :api: plugin def append_info_to_payload(payload) #:nodoc: diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index c6d4c6d936..a2e06fe0a6 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -63,13 +63,13 @@ module ActionController #:nodoc: # might look something like this: # # def index - # @people = Person.find(:all) + # @people = Person.all # end # # Here's the same action, with web-service support baked in: # # def index - # @people = Person.find(:all) + # @people = Person.all # # respond_to do |format| # format.html @@ -155,7 +155,7 @@ module ActionController #:nodoc: # Respond to also allows you to specify a common block for different formats by using any: # # def index - # @people = Person.find(:all) + # @people = Person.all # # respond_to do |format| # format.html @@ -178,7 +178,7 @@ module ActionController #:nodoc: # respond_to :html, :xml, :json # # def index - # @people = Person.find(:all) + # @people = Person.all # respond_with(@person) # end # end @@ -208,8 +208,8 @@ module ActionController #:nodoc: # It also accepts a block to be given. It's used to overwrite a default # response: # - # def destroy - # @user = User.find(params[:id]) + # def create + # @user = User.new(params[:user]) # flash[:notice] = "User was successfully created." if @user.save # # respond_with(@user) do |format| @@ -227,7 +227,7 @@ module ActionController #:nodoc: "controller responds to in the class level" if self.class.mimes_for_respond_to.empty? if response = retrieve_response_from_mimes(&block) - options = resources.extract_options! + options = resources.size == 1 ? {} : resources.extract_options! options.merge!(:default_response => response) (options.delete(:responder) || self.class.responder).call(self, resources, options) end @@ -258,9 +258,8 @@ module ActionController #:nodoc: # nil if :not_acceptable was sent to the client. # def retrieve_response_from_mimes(mimes=nil, &block) - collector = Collector.new { default_render } mimes ||= collect_mimes_from_class_level - mimes.each { |mime| collector.send(mime) } + collector = Collector.new(mimes) { default_render } block.call(collector) if block_given? if format = request.negotiate_mime(collector.order) @@ -277,8 +276,9 @@ module ActionController #:nodoc: include AbstractController::Collector attr_accessor :order - def initialize(&block) + def initialize(mimes, &block) @order, @responses, @default_response = [], {}, block + mimes.each { |mime| send(mime) } end def any(*args, &block) @@ -291,7 +291,7 @@ module ActionController #:nodoc: alias :all :any def custom(mime_type, &block) - mime_type = mime_type.is_a?(Mime::Type) ? mime_type : Mime::Type.lookup(mime_type.to_s) + mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type) @order << mime_type @responses[mime_type] ||= block end diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index b5f1d23ef0..55c650df6c 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -20,6 +20,7 @@ module ActionController # * Record - The URL will be generated by calling url_for with the +options+, which will reference a named URL for that record. # * String starting with protocol:// (like http://) - Is passed straight through as the target for redirection. # * String not containing a protocol - The current protocol and host is prepended to the string. + # * Proc - A block that will be executed in the controller's context. Should return any option accepted by +redirect_to+. # * :back - Back to the page that issued the request. Useful for forms that are triggered from multiple places. # Short-hand for redirect_to(request.env["HTTP_REFERER"]) # @@ -30,6 +31,7 @@ module ActionController # redirect_to "/images/screenshot.jpg" # redirect_to articles_url # redirect_to :back + # redirect_to proc { edit_post_url(@post) } # # The redirection happens as a "302 Moved" header unless otherwise specified. # @@ -39,6 +41,9 @@ module ActionController # redirect_to post_url(@post), :status => 301 # redirect_to :action=>'atom', :status => 302 # + # The status code can either be a standard {HTTP Status code}[http://www.iana.org/assignments/http-status-codes] as an + # integer, or a symbol representing the downcased, underscored and symbolized description. + # # It is also possible to assign a flash message as part of the redirection. There are two special accessors for commonly used the flash names # +alert+ and +notice+ as well as a general purpose +flash+ bucket. # @@ -48,8 +53,7 @@ module ActionController # redirect_to post_url(@post), :status => 301, :flash => { :updated_post_id => @post.id } # redirect_to { :action=>'atom' }, :alert => "Something serious happened" # - # When using redirect_to :back, if there is no referrer, - # RedirectBackError will be raised. You may specify some fallback + # When using redirect_to :back, if there is no referrer, RedirectBackError will be raised. You may specify some fallback # behavior for this case by rescuing RedirectBackError. def redirect_to(options = {}, response_status = {}) #:doc: raise ActionControllerError.new("Cannot redirect to nil!") if options.nil? @@ -83,6 +87,8 @@ module ActionController when :back raise RedirectBackError unless refer = request.headers["Referer"] refer + when Proc + _compute_redirect_to_location options.call else url_for(options) end.gsub(/[\r\n]/, '') diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 0be07cd1fc..38711c8462 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -2,6 +2,7 @@ require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/object/blank' module ActionController + # See Renderers.add def self.add_renderer(key, &block) Renderers.add(key, &block) end @@ -15,30 +16,12 @@ module ActionController end module ClassMethods - def _write_render_options - renderers = _renderers.map do |name, value| - <<-RUBY_EVAL - if options.key?(:#{name}) - _process_options(options) - return _render_option_#{name}(options.delete(:#{name}), options) - end - RUBY_EVAL - end - - class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 - def _handle_render_options(options) - #{renderers.join} - end - RUBY_EVAL - end - def use_renderers(*args) new = _renderers.dup args.each do |key| new[key] = RENDERERS[key] end self._renderers = new.freeze - _write_render_options end alias use_renderer use_renderers end @@ -47,31 +30,69 @@ module ActionController _handle_render_options(options) || super end + def _handle_render_options(options) + _renderers.each do |name, value| + if options.key?(name.to_sym) + _process_options(options) + return send("_render_option_#{name}", options.delete(name.to_sym), options) + end + end + nil + end + + # Hash of available renderers, mapping a renderer name to its proc. + # Default keys are :json, :js, :xml and :update. RENDERERS = {} + + # Adds a new renderer to call within controller actions. + # A renderer is invoked by passing its name as an option to + # AbstractController::Rendering#render. To create a renderer + # pass it a name and a block. The block takes two arguments, the first + # is the value paired with its key and the second is the remaining + # hash of options passed to +render+. + # + # === Example + # Create a csv renderer: + # + # ActionController::Renderers.add :csv do |obj, options| + # filename = options[:filename] || 'data' + # str = obj.respond_to?(:to_csv) ? obj.to_csv : obj.to_s + # send_data str, :type => Mime::CSV, + # :disposition => "attachment; filename=#{filename}.csv" + # end + # + # Note that we used Mime::CSV for the csv mime type as it comes with Rails. + # For a custom renderer, you'll need to register a mime type with + # Mime::Type.register. + # + # To use the csv renderer in a controller action: + # + # def show + # @csvable = Csvable.find(params[:id]) + # respond_to do |format| + # format.html + # format.csv { render :csv => @csvable, :filename => @csvable.name } + # } + # end + # To use renderers and their mime types in more concise ways, see + # ActionController::MimeResponds::ClassMethods.respond_to and + # ActionController::MimeResponds#respond_with def self.add(key, &block) define_method("_render_option_#{key}", &block) RENDERERS[key] = block - All._write_render_options end module All extend ActiveSupport::Concern include Renderers - INCLUDED = [] included do self._renderers = RENDERERS - _write_render_options - INCLUDED << self - end - - def self._write_render_options - INCLUDED.each(&:_write_render_options) end end add :json do |json, options| - json = ActiveSupport::JSON.encode(json, options) unless json.respond_to?(:to_str) + json = json.to_json(options) unless json.kind_of?(String) json = "#{options[:callback]}(#{json})" unless options[:callback].blank? self.content_type ||= Mime::JSON self.response_body = json diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index 86bb810947..32d52c84c4 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -2,12 +2,11 @@ module ActionController module Rendering extend ActiveSupport::Concern - include ActionController::RackDelegation include AbstractController::Rendering # Before processing, set the request formats in current controller formats. def process_action(*) #:nodoc: - self.formats = request.formats.map { |x| x.to_sym } + self.formats = request.formats.map { |x| x.ref } super end @@ -21,36 +20,35 @@ module ActionController private - # Normalize arguments by catching blocks and setting them on :update. - def _normalize_args(action=nil, options={}, &blk) #:nodoc: - options = super - options[:update] = blk if block_given? - options + # Normalize arguments by catching blocks and setting them on :update. + def _normalize_args(action=nil, options={}, &blk) #:nodoc: + options = super + options[:update] = blk if block_given? + options + end + + # Normalize both text and status options. + def _normalize_options(options) #:nodoc: + if options.key?(:text) && options[:text].respond_to?(:to_text) + options[:text] = options[:text].to_text end - # Normalize both text and status options. - def _normalize_options(options) #:nodoc: - if options.key?(:text) && options[:text].respond_to?(:to_text) - options[:text] = options[:text].to_text - end - - if options[:status] - options[:status] = Rack::Utils.status_code(options[:status]) - end - - super + if options[:status] + options[:status] = Rack::Utils.status_code(options[:status]) end - # Process controller specific options, as status, content-type and location. - def _process_options(options) #:nodoc: - status, content_type, location = options.values_at(:status, :content_type, :location) + super + end - self.status = status if status - self.content_type = content_type if content_type - self.headers["Location"] = url_for(location) if location + # Process controller specific options, as status, content-type and location. + def _process_options(options) #:nodoc: + status, content_type, location = options.values_at(:status, :content_type, :location) - super - end + self.status = status if status + self.content_type = content_type if content_type + self.headers["Location"] = url_for(location) if location + super + end end end diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index b632e7aab6..1cd93a188c 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -4,45 +4,27 @@ module ActionController #:nodoc: class InvalidAuthenticityToken < ActionControllerError #:nodoc: end - # Protecting controller actions from CSRF attacks by ensuring that all forms are coming from the current - # web application, not a forged link from another site, is done by embedding a token based on a random - # string stored in the session (which an attacker wouldn't know) in all forms and Ajax requests generated - # by Rails and then verifying the authenticity of that token in the controller. Only HTML/JavaScript - # requests are checked, so this will not protect your XML API (presumably you'll have a different - # authentication scheme there anyway). Also, GET requests are not protected as these should be - # idempotent anyway. + # Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks + # by including a token in the rendered html for your application. This token is + # stored as a random string in the session, to which an attacker does not have + # access. When a request reaches your application, \Rails then verifies the received + # token with the token in the session. Only HTML and javascript requests are checked, + # so this will not protect your XML API (presumably you'll have a different + # authentication scheme there anyway). Also, GET requests are not protected as these + # should be idempotent. # - # This is turned on with the protect_from_forgery method, which will check the token and raise an - # ActionController::InvalidAuthenticityToken if it doesn't match what was expected. You can customize the - # error message in production by editing public/422.html. A call to this method in ApplicationController is - # generated by default in post-Rails 2.0 applications. + # CSRF protection is turned on with the protect_from_forgery method, + # which will check the token and raise an ActionController::InvalidAuthenticityToken + # if it doesn't match what was expected. A call to this method is generated for new + # \Rails applications by default. You can customize the error message by editing + # public/422.html. # - # The token parameter is named authenticity_token by default. If you are generating an HTML form - # manually (without the use of Rails' form_for, form_tag or other helpers), you have to - # include a hidden field named like that and set its value to what is returned by - # form_authenticity_token. - # - # Request forgery protection is disabled by default in test environment. If you are upgrading from Rails - # 1.x, add this to config/environments/test.rb: - # - # # Disable request forgery protection in test environment - # config.action_controller.allow_forgery_protection = false - # - # == Learn more about CSRF (Cross-Site Request Forgery) attacks - # - # Here are some resources: - # * http://isc.sans.org/diary.html?storyid=1750 - # * http://en.wikipedia.org/wiki/Cross-site_request_forgery - # - # Keep in mind, this is NOT a silver-bullet, plug 'n' play, warm security blanket for your rails application. - # There are a few guidelines you should follow: - # - # * Keep your GET requests safe and idempotent. More reading material: - # * http://www.xml.com/pub/a/2002/04/24/deviant.html - # * http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.1.1 - # * Make sure the session cookies that Rails creates are non-persistent. Check in Firefox and look - # for "Expires: at end of session" + # The token parameter is named authenticity_token by default. The name and + # value of this token must be added to every layout that renders forms by including + # csrf_meta_tags in the html +head+. # + # Learn more about CSRF attacks and securing your application in the + # {Ruby on Rails Security Guide}[http://guides.rubyonrails.org/security.html]. module RequestForgeryProtection extend ActiveSupport::Concern @@ -71,39 +53,42 @@ module ActionController #:nodoc: # class FooController < ApplicationController # protect_from_forgery :except => :index # - # # you can disable csrf protection on controller-by-controller basis: - # skip_before_filter :verify_authenticity_token - # end + # You can disable csrf protection on controller-by-controller basis: + # + # skip_before_filter :verify_authenticity_token + # + # It can also be disabled for specific controller actions: + # + # skip_before_filter :verify_authenticity_token, :except => [:create] # # Valid Options: # # * :only/:except - Passed to the before_filter call. Set which actions are verified. def protect_from_forgery(options = {}) self.request_forgery_protection_token ||= :authenticity_token - before_filter :verify_authenticity_token, options + prepend_before_filter :verify_authenticity_token, options end end protected - - def protect_from_forgery(options = {}) - self.request_forgery_protection_token ||= :authenticity_token - before_filter :verify_authenticity_token, options - end - # The actual before_filter that is used. Modify this to change how you handle unverified requests. def verify_authenticity_token - verified_request? || raise(ActionController::InvalidAuthenticityToken) + verified_request? || handle_unverified_request + end + + def handle_unverified_request + reset_session end # Returns true or false if a request is verified. Checks: # - # * is the format restricted? By default, only HTML requests are checked. # * is it a GET request? Gets should be safe and idempotent # * Does the form_authenticity_token match the given token value from the params? + # * Does the X-CSRF-Token header match the form_authenticity_token def verified_request? - !protect_against_forgery? || request.forgery_whitelisted? || - form_authenticity_token == params[request_forgery_protection_token] + !protect_against_forgery? || request.get? || + form_authenticity_token == params[request_forgery_protection_token] || + form_authenticity_token == request.headers['X-CSRF-Token'] end # Sets the token value for the current session. diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index cb644dfd16..4b45413cf8 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -1,7 +1,7 @@ require 'active_support/json' module ActionController #:nodoc: - # Responder is responsible for exposing a resource to different mime requests, + # Responsible for exposing a resource to different mime requests, # usually depending on the HTTP verb. The responder is triggered when # respond_with is called. The simplest case to study is a GET request: # @@ -24,10 +24,10 @@ module ActionController #:nodoc: # # === Builtin HTTP verb semantics # - # The default Rails responder holds semantics for each HTTP verb. Depending on the + # The default \Rails responder holds semantics for each HTTP verb. Depending on the # content type, verb and the resource status, it will behave differently. # - # Using Rails default responder, a POST request for creating an object could + # Using \Rails default responder, a POST request for creating an object could # be written as: # # def create @@ -77,8 +77,6 @@ module ActionController #:nodoc: # # respond_with(@project, :manager, @task) # - # Check polymorphic_url documentation for more examples. - # class Responder attr_reader :controller, :request, :format, :resource, :resources, :options @@ -89,6 +87,8 @@ module ActionController #:nodoc: def initialize(controller, resources, options={}) @controller = controller + @request = @controller.request + @format = @controller.formats.first @resource = resources.last @resources = resources @options = options @@ -99,14 +99,6 @@ module ActionController #:nodoc: delegate :head, :render, :redirect_to, :to => :controller delegate :get?, :post?, :put?, :delete?, :to => :request - def request - @request ||= @controller.request - end - - def format - @format ||= @controller.formats.first - end - # Undefine :to_json and :to_yaml since it's defined on Object undef_method(:to_json) if method_defined?(:to_json) undef_method(:to_yaml) if method_defined?(:to_yaml) @@ -121,7 +113,7 @@ module ActionController #:nodoc: # Main entry point for responder responsible to dispatch to the proper format. # def respond - method = :"to_#{format}" + method = "to_#{format}" respond_to?(method) ? send(method) : to_format end @@ -146,7 +138,7 @@ module ActionController #:nodoc: protected - # This is the common behavior for "navigation" requests, like :html, :iphone and so forth. + # This is the common behavior for formats associated with browsing, like :html, :iphone and so forth. def navigation_behavior(error) if get? raise error @@ -157,7 +149,7 @@ module ActionController #:nodoc: end end - # This is the common behavior for "API" requests, like :xml and :json. + # This is the common behavior for formats associated with APIs, such as :xml and :json. def api_behavior(error) raise error unless resourceful? @@ -167,6 +159,8 @@ module ActionController #:nodoc: display resource.errors, :status => :unprocessable_entity elsif post? display resource, :status => :created, :location => api_location + elsif has_empty_resource_definition? + display empty_resource, :status => :ok else head :ok end @@ -175,7 +169,7 @@ module ActionController #:nodoc: # Checks whether the resource responds to the current format or not. # def resourceful? - resource.respond_to?(:"to_#{format}") + resource.respond_to?("to_#{format}") end # Returns the resource location by retrieving it from the options or @@ -227,5 +221,23 @@ module ActionController #:nodoc: def default_action @action ||= ACTIONS_FOR_VERBS[request.request_method_symbol] end + + # Check whether resource needs a specific definition of empty resource to be valid + # + def has_empty_resource_definition? + respond_to?("empty_#{format}_resource") + end + + # Delegate to proper empty resource method + # + def empty_resource + send("empty_#{format}_resource") + end + + # Return a valid empty JSON resource + # + def empty_json_resource + "{}" + end end end diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index d75b46dace..312dc8eb3e 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -69,10 +69,6 @@ module ActionController #:nodoc: options[:filename] ||= File.basename(path) unless options[:url_based_filename] send_file_headers! options - if options[:x_sendfile] - ActiveSupport::Deprecation.warn(":x_sendfile is no longer needed in send_file", caller) - end - self.status = options[:status] || 200 self.content_type = options[:content_type] if options.key?(:content_type) self.response_body = File.open(path, "rb") @@ -105,10 +101,6 @@ module ActionController #:nodoc: # send_data image.data, :type => image.content_type, :disposition => 'inline' # # See +send_file+ for more information on HTTP Content-* headers and caching. - # - # Tip: if you want to stream large amounts of on-the-fly generated - # data to the browser, then use render :text => proc { ... } - # instead. See ActionController::Base#render for more information. def send_data(data, options = {}) #:doc: send_file_headers! options.dup render options.slice(:status, :content_type).merge(:text => data) @@ -121,10 +113,6 @@ module ActionController #:nodoc: raise ArgumentError, ":#{arg} option required" if options[arg].nil? end - if options.key?(:length) - ActiveSupport::Deprecation.warn("You do not need to provide the file's length", caller) - end - disposition = options[:disposition] disposition += %(; filename="#{options[:filename]}") if options[:filename] diff --git a/actionpack/lib/action_controller/metal/testing.rb b/actionpack/lib/action_controller/metal/testing.rb index 4b8c452d50..f4efeb33ba 100644 --- a/actionpack/lib/action_controller/metal/testing.rb +++ b/actionpack/lib/action_controller/metal/testing.rb @@ -14,18 +14,9 @@ module ActionController cookies.write(@_response) end @_response.prepare! - set_test_assigns ret end - def set_test_assigns - @assigns = {} - (instance_variable_names - self.class.protected_instance_variables).each do |var| - name, value = var[1..-1], instance_variable_get(var) - @assigns[name] = value - end - end - # TODO : Rewrite tests using controller.headers= to use Rack env def headers=(new_headers) @_response ||= ActionDispatch::Response.new diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index a51fc5b8e4..6fc0cf1fb8 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -2,27 +2,25 @@ module ActionController module UrlFor extend ActiveSupport::Concern - include ActionDispatch::Routing::UrlFor + include AbstractController::UrlFor def url_options - super.reverse_merge( - :host => request.host_with_port, + @_url_options ||= super.reverse_merge( + :host => request.host, + :port => request.optional_port, :protocol => request.protocol, :_path_segments => request.symbolized_path_parameters - ).merge(:script_name => request.script_name) - end + ).freeze - def _routes - raise "In order to use #url_for, you must include routing helpers explicitly. " \ - "For instance, `include Rails.application.routes.url_helpers" - end - - module ClassMethods - def action_methods - @action_methods ||= begin - super - _routes.named_routes.helper_names + if _routes.equal?(env["action_dispatch.routes"]) + @_url_options.dup.tap do |options| + options[:script_name] = request.script_name.dup + options.freeze end + else + @_url_options end end + end end diff --git a/actionpack/lib/action_controller/middleware.rb b/actionpack/lib/action_controller/middleware.rb index 2115b07b3e..437fec3dc6 100644 --- a/actionpack/lib/action_controller/middleware.rb +++ b/actionpack/lib/action_controller/middleware.rb @@ -31,7 +31,7 @@ module ActionController super() @_app = app end - + def index call(env) end diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb index cd2dfafbe6..f0c29825ba 100644 --- a/actionpack/lib/action_controller/railtie.rb +++ b/actionpack/lib/action_controller/railtie.rb @@ -2,35 +2,13 @@ require "rails" require "action_controller" require "action_dispatch/railtie" require "action_view/railtie" -require "active_support/deprecation/proxy_wrappers" -require "active_support/deprecation" +require "abstract_controller/railties/routes_helpers" +require "action_controller/railties/paths" module ActionController class Railtie < Rails::Railtie config.action_controller = ActiveSupport::OrderedOptions.new - config.action_controller.singleton_class.tap do |d| - d.send(:define_method, :session) do - ActiveSupport::Deprecation.warn "config.action_controller.session has been deprecated. " << - "Please use Rails.application.config.session_store instead.", caller - end - - d.send(:define_method, :session=) do |val| - ActiveSupport::Deprecation.warn "config.action_controller.session= has been deprecated. " << - "Please use config.session_store(name, options) instead.", caller - end - - d.send(:define_method, :session_store) do - ActiveSupport::Deprecation.warn "config.action_controller.session_store has been deprecated. " << - "Please use Rails.application.config.session_store instead.", caller - end - - d.send(:define_method, :session_store=) do |val| - ActiveSupport::Deprecation.warn "config.action_controller.session_store= has been deprecated. " << - "Please use config.session_store(name, options) instead.", caller - end - end - initializer "action_controller.logger" do ActiveSupport.on_load(:action_controller) { self.logger ||= Rails.logger } end @@ -43,24 +21,27 @@ module ActionController paths = app.config.paths options = app.config.action_controller - options.assets_dir ||= paths.public.to_a.first - options.javascripts_dir ||= paths.public.javascripts.to_a.first - options.stylesheets_dir ||= paths.public.stylesheets.to_a.first - options.page_cache_directory ||= paths.public.to_a.first - options.helpers_path ||= paths.app.helpers.to_a + options.assets_dir ||= paths["public"].first + options.javascripts_dir ||= paths["public/javascripts"].first + options.stylesheets_dir ||= paths["public/stylesheets"].first + options.page_cache_directory ||= paths["public"].first + + # make sure readers methods get compiled + options.asset_path ||= app.config.asset_path + options.asset_host ||= app.config.asset_host ActiveSupport.on_load(:action_controller) do - include app.routes.url_helpers + include app.routes.mounted_helpers + extend ::AbstractController::Railties::RoutesHelpers.with(app.routes) + extend ::ActionController::Railties::Paths.with(app) options.each { |k,v| send("#{k}=", v) } end end - initializer "action_controller.deprecated_routes" do |app| - message = "ActionController::Routing::Routes is deprecated. " \ - "Instead, use Rails.application.routes" - - proxy = ActiveSupport::Deprecation::DeprecatedObjectProxy.new(app.routes, message) - ActionController::Routing::Routes = proxy + initializer "action_controller.compile_config_methods" do + ActiveSupport.on_load(:action_controller) do + config.compile_methods! if config.respond_to?(:compile_methods!) + end end end -end \ No newline at end of file +end diff --git a/actionpack/lib/action_controller/railties/paths.rb b/actionpack/lib/action_controller/railties/paths.rb new file mode 100644 index 0000000000..dce3c2fe88 --- /dev/null +++ b/actionpack/lib/action_controller/railties/paths.rb @@ -0,0 +1,32 @@ +module ActionController + module Railties + module Paths + def self.with(app) + Module.new do + define_method(:inherited) do |klass| + super(klass) + + if namespace = klass.parents.detect {|m| m.respond_to?(:_railtie) } + paths = namespace._railtie.paths["app/helpers"].existent + else + paths = app.config.helpers_paths + end + + klass.helpers_path = paths + if klass.superclass == ActionController::Base && ActionController::Base.include_all_helpers + klass.helper :all + end + + if app.config.serve_static_assets && namespace + paths = namespace._railtie.config.paths + + klass.config.assets_dir = paths["public"].first + klass.config.javascripts_dir = paths["public/javascripts"].first + klass.config.stylesheets_dir = paths["public/stylesheets"].first + end + end + end + end + end + end +end diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index e306697f4b..bc4f8bb9ce 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -1,6 +1,7 @@ require 'rack/session/abstract/id' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/to_query' +require 'active_support/core_ext/class/attribute' module ActionController module TemplateAssertions @@ -40,6 +41,13 @@ module ActionController ActiveSupport::Notifications.unsubscribe("!render_template.action_view") end + def process(*args) + @partials = Hash.new(0) + @templates = Hash.new(0) + @layouts = Hash.new(0) + super + end + # Asserts that the request was rendered with the appropriate template file or partials. # # ==== Examples @@ -127,7 +135,7 @@ module ActionController class Result < ::Array #:nodoc: def to_s() join '/' end def self.new_escaped(strings) - new strings.collect {|str| URI.unescape str} + new strings.collect {|str| uri_parser.unescape str} end end @@ -164,9 +172,14 @@ module ActionController end def recycle! + write_cookies! + @env.delete('HTTP_COOKIE') if @cookies.blank? + @env.delete('action_dispatch.cookies') + @cookies = nil @formats = nil @env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ } @env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ } + @symbolized_path_params = nil @method = @request_method = nil @fullpath = @ip = @remote_ip = nil @env['action_dispatch.request.query_parameters'] = {} @@ -186,20 +199,23 @@ module ActionController end end - class TestSession < ActionDispatch::Session::AbstractStore::SessionHash #:nodoc: - DEFAULT_OPTIONS = ActionDispatch::Session::AbstractStore::DEFAULT_OPTIONS + class TestSession < Rack::Session::Abstract::SessionHash #:nodoc: + DEFAULT_OPTIONS = Rack::Session::Abstract::ID::DEFAULT_OPTIONS def initialize(session = {}) + @env, @by = nil, nil replace(session.stringify_keys) @loaded = true end - def exists?; true; end + def exists? + true + end end # Superclass for ActionController functional tests. Functional tests allow you to # test a single controller action per test method. This should not be confused with - # integration tests (see ActionController::IntegrationTest), which are more like + # integration tests (see ActionDispatch::IntegrationTest), which are more like # "stories" that can involve multiple controllers and multiple actions (i.e. multiple # different HTTP requests). # @@ -244,7 +260,7 @@ module ActionController # after calling +post+. If the various assert methods are not sufficient, then you # may use this object to inspect the HTTP response in detail. # - # (Earlier versions of Rails required each functional test to subclass + # (Earlier versions of \Rails required each functional test to subclass # Test::Unit::TestCase and define @controller, @request, @response in +setup+.) # # == Controller is automatically inferred @@ -257,7 +273,7 @@ module ActionController # tests WidgetController # end # - # == Testing controller internals + # == \Testing controller internals # # In addition to these specific assertions, you also have easy access to various collections that the regular test/unit assertions # can be used against. These collections are: @@ -265,7 +281,7 @@ module ActionController # * assigns: Instance variables assigned in the action that are available for the view. # * session: Objects being saved in the session. # * flash: The flash objects currently in the session. - # * cookies: Cookies being sent to the user on this request. + # * cookies: \Cookies being sent to the user on this request. # # These collections can be used just like any other hash: # @@ -289,9 +305,13 @@ module ActionController # and cookies, though. For sessions, you just do: # # @request.session[:key] = "value" - # @request.cookies["key"] = "value" + # @request.cookies[:key] = "value" # - # == Testing named routes + # To clear the cookies for a test just clear the request's cookies hash: + # + # @request.cookies.clear + # + # == \Testing named routes # # If you're using named routes, they can be easily tested using the original named routes' methods straight in the test case. # Example: @@ -311,14 +331,14 @@ module ActionController def tests(controller_class) self.controller_class = controller_class end - + def controller_class=(new_class) prepare_controller_class(new_class) if new_class - write_inheritable_attribute(:controller_class, new_class) + self._controller_class = new_class end def controller_class - if current_controller_class = read_inheritable_attribute(:controller_class) + if current_controller_class = self._controller_class current_controller_class else self.controller_class = determine_default_controller_class(name) @@ -393,16 +413,18 @@ module ActionController parameters ||= {} @request.assign_parameters(@routes, @controller.class.name.underscore.sub(/_controller$/, ''), action.to_s, parameters) - @request.session = ActionController::TestSession.new(session) unless session.nil? + @request.session = ActionController::TestSession.new(session) if session @request.session["flash"] = @request.flash.update(flash || {}) @request.session["flash"].sweep @controller.request = @request @controller.params.merge!(parameters) build_request_uri(action, parameters) - Base.class_eval { include Testing } + @controller.class.class_eval { include Testing } @controller.process_with_new_base_test(@request, @response) + @assigns = @controller.respond_to?(:view_assigns) ? @controller.view_assigns : {} @request.session.delete('flash') if @request.session['flash'].blank? + @request.cookies.merge!(@response.cookies) @response end @@ -416,7 +438,7 @@ module ActionController @request.env.delete('PATH_INFO') - if @controller + if defined?(@controller) && @controller @controller.request = @request @controller.params = {} end @@ -430,6 +452,7 @@ module ActionController included do include ActionController::TemplateAssertions include ActionDispatch::Assertions + class_attribute :_controller_class setup :setup_controller_request_and_response end @@ -437,7 +460,7 @@ module ActionController def build_request_uri(action, parameters) unless @request.env["PATH_INFO"] - options = @controller.__send__(:url_options).merge(parameters) + options = @controller.respond_to?(:url_options) ? @controller.__send__(:url_options).merge(parameters) : parameters options.update( :only_path => true, :action => action, @@ -461,9 +484,11 @@ module ActionController # The exception is stored in the exception accessor for further inspection. module RaiseActionExceptions def self.included(base) - base.class_eval do - attr_accessor :exception - protected :exception, :exception= + unless base.method_defined?(:exception) && base.method_defined?(:exception=) + base.class_eval do + attr_accessor :exception + protected :exception, :exception= + end end end diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb index b8d73c350d..7fa3aead82 100644 --- a/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb +++ b/actionpack/lib/action_controller/vendor/html-scanner/html/document.rb @@ -48,7 +48,7 @@ EOF end end end - + # Search the tree for (and return) the first node that matches the given # conditions. The conditions are interpreted differently for different node # types, see HTML::Text#find and HTML::Tag#find. @@ -62,7 +62,7 @@ EOF def find_all(conditions) @root.find_all(conditions) end - + end end diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb index a874519978..22b3243104 100644 --- a/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb +++ b/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb @@ -1,7 +1,7 @@ require 'strscan' module HTML #:nodoc: - + class Conditions < Hash #:nodoc: def initialize(hash) super() @@ -18,14 +18,14 @@ module HTML #:nodoc: hash[k] = Conditions.new(v) when :children hash[k] = v = keys_to_symbols(v) - v.each do |k,v2| - case k + v.each do |key,value| + case key when :count, :greater_than, :less_than # keys are valid, and require no further processing when :only - v[k] = Conditions.new(v2) + v[key] = Conditions.new(value) else - raise "illegal key #{k.inspect} => #{v2.inspect}" + raise "illegal key #{key.inspect} => #{value.inspect}" end end else @@ -38,18 +38,14 @@ module HTML #:nodoc: private def keys_to_strings(hash) - hash.keys.inject({}) do |h,k| - h[k.to_s] = hash[k] - h - end + Hash[hash.keys.map {|k| [k.to_s, hash[k]]}] end def keys_to_symbols(hash) - hash.keys.inject({}) do |h,k| + Hash[hash.keys.map do |k| raise "illegal key #{k.inspect}" unless k.respond_to?(:to_sym) - h[k.to_sym] = hash[k] - h - end + [k.to_sym, hash[k]] + end] end end @@ -57,17 +53,17 @@ module HTML #:nodoc: class Node #:nodoc: # The array of children of this node. Not all nodes have children. attr_reader :children - + # The parent node of this node. All nodes have a parent, except for the # root node. attr_reader :parent - + # The line number of the input where this node was begun attr_reader :line - + # The byte position in the input where this node was begun attr_reader :position - + # Create a new node as a child of the given parent. def initialize(parent, line=0, pos=0) @parent = parent @@ -77,9 +73,7 @@ module HTML #:nodoc: # Return a textual representation of the node. def to_s - s = "" - @children.each { |child| s << child.to_s } - s + @children.join() end # Return false (subclasses must override this to provide specific matching @@ -92,7 +86,7 @@ module HTML #:nodoc: # returns non +nil+. Returns the result of the #find call that succeeded. def find(conditions) conditions = validate_conditions(conditions) - @children.each do |child| + @children.each do |child| node = child.find(conditions) return node if node end @@ -133,7 +127,7 @@ module HTML #:nodoc: equivalent end - + class </) if strict - raise "expected > (got #{scanner.rest.inspect} for #{content}, #{attributes.inspect})" + raise "expected > (got #{scanner.rest.inspect} for #{content}, #{attributes.inspect})" else # throw away all text until we find what we're looking for scanner.skip_until(/>/) or scanner.terminate @@ -212,9 +206,9 @@ module HTML #:nodoc: # A node that represents text, rather than markup. class Text < Node #:nodoc: - + attr_reader :content - + # Creates a new text node as a child of the given parent, with the given # content. def initialize(parent, line, pos, content) @@ -240,7 +234,7 @@ module HTML #:nodoc: def find(conditions) match(conditions) && self end - + # Returns non-+nil+ if this node meets the given conditions, or +nil+ # otherwise. See the discussion of #find for the valid conditions. def match(conditions) @@ -268,7 +262,7 @@ module HTML #:nodoc: content == node.content end end - + # A CDATA node is simply a text node with a specialized way of displaying # itself. class CDATA < Text #:nodoc: @@ -281,16 +275,16 @@ module HTML #:nodoc: # closing tag, or a self-closing tag. It has a name, and may have a hash of # attributes. class Tag < Node #:nodoc: - + # Either +nil+, :close, or :self attr_reader :closing - + # Either +nil+, or a hash of attributes for this node. attr_reader :attributes # The name of this tag. attr_reader :name - + # Create a new node as a child of the given parent, using the given content # to describe the node. It will be parsed and the node name, attributes and # closing status extracted. @@ -344,7 +338,7 @@ module HTML #:nodoc: def tag? true end - + # Returns +true+ if the node meets any of the given conditions. The # +conditions+ parameter must be a hash of any of the following keys # (all are optional): @@ -404,7 +398,7 @@ module HTML #:nodoc: # node.match :descendant => { :tag => "strong" } # # # test if the node has between 2 and 4 span tags as immediate children - # node.match :children => { :count => 2..4, :only => { :tag => "span" } } + # node.match :children => { :count => 2..4, :only => { :tag => "span" } } # # # get funky: test to see if the node is a "div", has a "ul" ancestor # # and an "li" parent (with "class" = "enum"), and whether or not it has @@ -439,7 +433,7 @@ module HTML #:nodoc: # test children return false unless children.find { |child| child.match(conditions[:child]) } if conditions[:child] - + # test ancestors if conditions[:ancestor] return false unless catch :found do @@ -457,13 +451,13 @@ module HTML #:nodoc: child.match(:descendant => conditions[:descendant]) end end - + # count children if opts = conditions[:children] matches = children.select do |c| (c.kind_of?(HTML::Tag) and (c.closing == :self or ! c.childless?)) end - + matches = matches.select { |c| c.match(opts[:only]) } if opts[:only] opts.each do |key, value| next if key == :only @@ -489,24 +483,24 @@ module HTML #:nodoc: self_index = siblings.index(self) if conditions[:sibling] - return false unless siblings.detect do |s| + return false unless siblings.detect do |s| s != self && s.match(conditions[:sibling]) end end if conditions[:before] - return false unless siblings[self_index+1..-1].detect do |s| + return false unless siblings[self_index+1..-1].detect do |s| s != self && s.match(conditions[:before]) end end if conditions[:after] - return false unless siblings[0,self_index].detect do |s| + return false unless siblings[0,self_index].detect do |s| s != self && s.match(conditions[:after]) end end end - + true end @@ -515,7 +509,7 @@ module HTML #:nodoc: return false unless closing == node.closing && self.name == node.name attributes == node.attributes end - + private # Match the given value to the given condition. def match_condition(value, condition) diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb index 51e0868995..09dd08898c 100644 --- a/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb +++ b/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb @@ -1,5 +1,5 @@ require 'set' -require 'active_support/core_ext/class/inheritable_attributes' +require 'active_support/core_ext/class/attribute' module HTML class Sanitizer @@ -7,11 +7,11 @@ module HTML return text unless sanitizeable?(text) tokenize(text, options).join end - + def sanitizeable?(text) !(text.nil? || text.empty? || !text.index("<")) end - + protected def tokenize(text, options) tokenizer = HTML::Tokenizer.new(text) @@ -22,12 +22,12 @@ module HTML end result end - + def process_node(node, result, options) result << node.to_s end end - + class FullSanitizer < Sanitizer def sanitize(text, options = {}) result = super @@ -37,12 +37,12 @@ module HTML # Recurse - handle all dirty nested tags result == text ? result : sanitize(result, options) end - + def process_node(node, result, options) result << node.to_s if node.class == HTML::Text end end - + class LinkSanitizer < FullSanitizer cattr_accessor :included_tags, :instance_writer => false self.included_tags = Set.new(%w(a href)) @@ -50,51 +50,51 @@ module HTML def sanitizeable?(text) !(text.nil? || text.empty? || !((text.index(""))) end - + protected def process_node(node, result, options) - result << node.to_s unless node.is_a?(HTML::Tag) && included_tags.include?(node.name) + result << node.to_s unless node.is_a?(HTML::Tag) && included_tags.include?(node.name) end end - + class WhiteListSanitizer < Sanitizer [:protocol_separator, :uri_attributes, :allowed_attributes, :allowed_tags, :allowed_protocols, :bad_tags, :allowed_css_properties, :allowed_css_keywords, :shorthand_css_properties].each do |attr| - class_inheritable_accessor attr, :instance_writer => false + class_attribute attr, :instance_writer => false end # A regular expression of the valid characters used to separate protocols like # the ':' in 'http://foo.com' self.protocol_separator = /:|(�*58)|(p)|(%|%)3A/ - + # Specifies a Set of HTML attributes that can have URIs. self.uri_attributes = Set.new(%w(href src cite action longdesc xlink:href lowsrc)) # Specifies a Set of 'bad' tags that the #sanitize helper will remove completely, as opposed # to just escaping harmless tags like <font> self.bad_tags = Set.new(%w(script)) - + # Specifies the default Set of tags that the #sanitize helper will allow unscathed. - self.allowed_tags = Set.new(%w(strong em b i p code pre tt samp kbd var sub - sup dfn cite big small address hr br div span h1 h2 h3 h4 h5 h6 ul ol li dl dt dd abbr + self.allowed_tags = Set.new(%w(strong em b i p code pre tt samp kbd var sub + sup dfn cite big small address hr br div span h1 h2 h3 h4 h5 h6 ul ol li dl dt dd abbr acronym a img blockquote del ins)) - # Specifies the default Set of html attributes that the #sanitize helper will leave + # Specifies the default Set of html attributes that the #sanitize helper will leave # in the allowed tag. self.allowed_attributes = Set.new(%w(href src width height alt cite datetime title class name xml:lang abbr)) - + # Specifies the default Set of acceptable css properties that #sanitize and #sanitize_css will accept. - self.allowed_protocols = Set.new(%w(ed2k ftp http https irc mailto news gopher nntp telnet webcal xmpp callto + self.allowed_protocols = Set.new(%w(ed2k ftp http https irc mailto news gopher nntp telnet webcal xmpp callto feed svn urn aim rsync tag ssh sftp rtsp afs)) - + # Specifies the default Set of acceptable css keywords that #sanitize and #sanitize_css will accept. - self.allowed_css_properties = Set.new(%w(azimuth background-color border-bottom-color border-collapse - border-color border-left-color border-right-color border-top-color clear color cursor direction display + self.allowed_css_properties = Set.new(%w(azimuth background-color border-bottom-color border-collapse + border-color border-left-color border-right-color border-top-color clear color cursor direction display elevation float font font-family font-size font-style font-variant font-weight height letter-spacing line-height overflow pause pause-after pause-before pitch pitch-range richness speak speak-header speak-numeral speak-punctuation speech-rate stress text-align text-decoration text-indent unicode-bidi vertical-align voice-family volume white-space width)) - + # Specifies the default Set of acceptable css keywords that #sanitize and #sanitize_css will accept. self.allowed_css_keywords = Set.new(%w(auto aqua black block blue bold both bottom brown center collapse dashed dotted fuchsia gray green !important italic left lime maroon medium none navy normal @@ -118,9 +118,9 @@ module HTML style.scan(/([-\w]+)\s*:\s*([^:;]*)/) do |prop,val| if allowed_css_properties.include?(prop.downcase) clean << prop + ': ' + val + ';' - elsif shorthand_css_properties.include?(prop.split('-')[0].downcase) + elsif shorthand_css_properties.include?(prop.split('-')[0].downcase) unless val.split().any? do |keyword| - !allowed_css_keywords.include?(keyword) && + !allowed_css_keywords.include?(keyword) && keyword !~ /^(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$/ end clean << prop + ': ' + val + ';' @@ -146,7 +146,7 @@ module HTML else options[:parent].unshift node.name end - + process_attributes_for node, options options[:tags].include?(node.name) ? node : nil @@ -154,7 +154,7 @@ module HTML bad_tags.include?(options[:parent].first) ? nil : node.to_s.gsub(/:not. For example: # p:not(.post) # Matches all paragraphs that do not have the class .post. - # + # # === Substitution Values # # You can use substitution with identifiers, class names and element values. diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb index 240dc1890f..c252e01cf5 100644 --- a/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb +++ b/actionpack/lib/action_controller/vendor/html-scanner/html/tokenizer.rb @@ -1,7 +1,7 @@ require 'strscan' module HTML #:nodoc: - + # A simple HTML tokenizer. It simply breaks a stream of text into tokens, where each # token is a string. Each string represents either "text", or an HTML element. # @@ -14,13 +14,13 @@ module HTML #:nodoc: # p token # end class Tokenizer #:nodoc: - + # The current (byte) position in the text attr_reader :position - + # The current line number attr_reader :line - + # Create a new Tokenizer for the given text. def initialize(text) text.encode! if text.encoding_aware? @@ -42,7 +42,7 @@ module HTML #:nodoc: update_current_line(scan_text) end end - + private # Treat the text at the current position as a tag, and scan it. Supports @@ -69,13 +69,13 @@ module HTML #:nodoc: def scan_text "#{@scanner.getch}#{@scanner.scan(/[^<]*/)}" end - + # Counts the number of newlines in the text and updates the current line # accordingly. def update_current_line(text) text.scan(/\r?\n/) { @current_line += 1 } end - + # Skips over quoted strings, so that less-than and greater-than characters # within the strings are ignored. def consume_quoted_regions @@ -103,5 +103,5 @@ module HTML #:nodoc: text end end - + end diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index aeec934be8..49971fc9f8 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2010 David Heinemeier Hansson +# Copyright (c) 2004-2011 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -53,6 +53,7 @@ module ActionDispatch autoload :Flash autoload :Head autoload :ParamsParser + autoload :Reloader autoload :RemoteIp autoload :Rescue autoload :ShowExceptions @@ -85,6 +86,7 @@ module ActionDispatch autoload_under 'testing' do autoload :Assertions autoload :Integration + autoload :IntegrationTest, 'action_dispatch/testing/integration' autoload :PerformanceTest autoload :TestProcess autoload :TestRequest diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index e9fdf75cc8..4f4cb96a74 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -39,10 +39,11 @@ module ActionDispatch end module Response - attr_reader :cache_control + attr_reader :cache_control, :etag + alias :etag? :etag def initialize(*) - status, header, body = super + super @cache_control = {} @etag = self["ETag"] @@ -50,8 +51,7 @@ module ActionDispatch if cache_control = self["Cache-Control"] cache_control.split(/,\s*/).each do |segment| first, last = segment.split("=") - last ||= true - @cache_control[first.to_sym] = last + @cache_control[first.to_sym] = last || true end end end @@ -70,14 +70,6 @@ module ActionDispatch headers['Last-Modified'] = utc_time.httpdate end - def etag - @etag - end - - def etag? - @etag - end - def etag=(etag) key = ActiveSupport::Cache.expand_cache_key(etag) @etag = self["ETag"] = %("#{Digest::MD5.hexdigest(key)}") @@ -88,38 +80,19 @@ module ActionDispatch def handle_conditional_get! if etag? || last_modified? || !@cache_control.empty? set_conditional_cache_control! - elsif nonempty_ok_response? - self.etag = body - - if request && request.etag_matches?(etag) - self.status = 304 - self.body = [] - end - - set_conditional_cache_control! - else - headers["Cache-Control"] = "no-cache" end end - def nonempty_ok_response? - @status == 200 && string_body? - end - - def string_body? - !@blank && @body.respond_to?(:all?) && @body.all? { |part| part.is_a?(String) } - end - DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate" def set_conditional_cache_control! - control = @cache_control - return if self["Cache-Control"].present? + control = @cache_control + if control.empty? headers["Cache-Control"] = DEFAULT_CACHE_CONTROL - elsif @cache_control[:no_cache] + elsif control[:no_cache] headers["Cache-Control"] = "no-cache" else extras = control[:extras] diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index 1ab48ae04d..8dd1af7f3d 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -5,10 +5,10 @@ require 'active_support/core_ext/object/duplicable' module ActionDispatch module Http # Allows you to specify sensitive parameters which will be replaced from - # the request log by looking in all subhashes of the param hash for keys - # to filter. If a block is given, each key and value of the parameter - # hash and all subhashes is passed to it, the value or key can be replaced - # using String#replace or similar method. + # the request log by looking in the query string of the request and all + # subhashes of the params hash to filter. If a block is given, each key and + # value of the params hash and all subhashes is passed to it, the value + # or key can be replaced using String#replace or similar method. # # Examples: # @@ -38,6 +38,11 @@ module ActionDispatch @filtered_env ||= env_filter.filter(@env) end + # Reconstructed a path with all sensitive GET parameters replaced. + def filtered_path + @filtered_path ||= query_string.empty? ? path : "#{path}?#{filtered_query_string}" + end + protected def parameter_filter @@ -52,6 +57,14 @@ module ActionDispatch @@parameter_filter_for[filters] ||= ParameterFilter.new(filters) end + KV_RE = '[^&;=]+' + PAIR_RE = %r{(#{KV_RE})=(#{KV_RE})} + def filtered_query_string + query_string.gsub(PAIR_RE) do |_| + parameter_filter.filter([[$1, $2]]).first.join("=") + end + end + end end end diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 4082770b85..68ba1a81b5 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -32,23 +32,25 @@ module ActionDispatch end end - # Returns the Mime type for the \format used in the request. + # Returns the MIME type for the \format used in the request. # # GET /posts/5.xml | request.format => Mime::XML # GET /posts/5.xhtml | request.format => Mime::HTML - # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first depending on the value of ActionController::Base.use_accept_header + # GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first # def format(view_path = []) formats.first end + BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/ + def formats accept = @env['HTTP_ACCEPT'] @env["action_dispatch.request.formats"] ||= if parameters[:format] Array(Mime[parameters[:format]]) - elsif xhr? || (accept && accept !~ /,\s*\*\/\*/) + elsif xhr? || (accept && accept !~ BROWSER_LIKE_ACCEPTS) accepts else [Mime::HTML] diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index c6fc582851..7c9ebe7c7b 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -80,6 +80,9 @@ module Mime end class << self + + TRAILING_STAR_REGEXP = /(text|application)\/\*/ + def lookup(string) LOOKUP[string] end @@ -105,15 +108,28 @@ module Mime def parse(accept_header) if accept_header !~ /,/ - [Mime::Type.lookup(accept_header)] + if accept_header =~ TRAILING_STAR_REGEXP + parse_data_with_trailing_star($1) + else + [Mime::Type.lookup(accept_header)] + end else # keep track of creation order to keep the subsequent sort stable - list = [] - accept_header.split(/,/).each_with_index do |header, index| - params, q = header.split(/;\s*q=/) - if params - params.strip! - list << AcceptItem.new(index, params, q) unless params.empty? + list, index = [], 0 + accept_header.split(/,/).each do |header| + params, q = header.split(/;\s*q=/) + if params.present? + params.strip! + + if params =~ TRAILING_STAR_REGEXP + parse_data_with_trailing_star($1).each do |m| + list << AcceptItem.new(index, m.to_s, q) + index += 1 + end + else + list << AcceptItem.new(index, params, q) + index += 1 + end end end list.sort! @@ -160,23 +176,51 @@ module Mime list end end + + # input: 'text' + # returned value: [Mime::JSON, Mime::XML, Mime::ICS, Mime::HTML, Mime::CSS, Mime::CSV, Mime::JS, Mime::YAML, Mime::TEXT] + # + # input: 'application' + # returned value: [Mime::HTML, Mime::JS, Mime::XML, Mime::YAML, Mime::ATOM, Mime::JSON, Mime::RSS, Mime::URL_ENCODED_FORM] + def parse_data_with_trailing_star(input) + Mime::SET.select { |m| m =~ input } + end + + # This method is opposite of register method. + # + # Usage: + # + # Mime::Type.unregister(:mobile) + def unregister(symbol) + symbol = symbol.to_s.upcase + mime = Mime.const_get(symbol) + Mime.instance_eval { remove_const(symbol) } + + SET.delete_if { |v| v.eql?(mime) } + LOOKUP.delete_if { |k,v| v.eql?(mime) } + EXTENSION_LOOKUP.delete_if { |k,v| v.eql?(mime) } + end end - + def initialize(string, symbol = nil, synonyms = []) @symbol, @synonyms = symbol, synonyms @string = string end - + def to_s @string end - + def to_str to_s end - + def to_sym - @symbol || @string.to_sym + @symbol + end + + def ref + to_sym || to_s end def ===(list) @@ -186,11 +230,11 @@ module Mime super end end - + def ==(mime_type) return false if mime_type.blank? - (@synonyms + [ self ]).any? do |synonym| - synonym.to_s == mime_type.to_s || synonym.to_sym == mime_type.to_sym + (@synonyms + [ self ]).any? do |synonym| + synonym.to_s == mime_type.to_s || synonym.to_sym == mime_type.to_sym end end diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb index add8cab2ab..ef5d207b26 100644 --- a/actionpack/lib/action_dispatch/http/parameters.rb +++ b/actionpack/lib/action_dispatch/http/parameters.rb @@ -15,14 +15,14 @@ module ActionDispatch alias :params :parameters def path_parameters=(parameters) #:nodoc: - @env.delete("action_dispatch.request.symbolized_path_parameters") + @symbolized_path_params = nil @env.delete("action_dispatch.request.parameters") @env["action_dispatch.request.path_parameters"] = parameters end # The same as path_parameters with explicitly symbolized keys. def symbolized_path_parameters - @env["action_dispatch.request.symbolized_path_parameters"] ||= path_parameters.symbolize_keys + @symbolized_path_params ||= path_parameters.symbolize_keys end # Returns a hash with the \parameters used to form the \path of the request. diff --git a/actionpack/lib/action_dispatch/http/rack_cache.rb b/actionpack/lib/action_dispatch/http/rack_cache.rb new file mode 100644 index 0000000000..b5c1435903 --- /dev/null +++ b/actionpack/lib/action_dispatch/http/rack_cache.rb @@ -0,0 +1,58 @@ +require "rack/cache" +require "rack/cache/context" +require "active_support/cache" + +module ActionDispatch + class RailsMetaStore < Rack::Cache::MetaStore + def self.resolve(uri) + new + end + + # TODO: Finally deal with the RAILS_CACHE global + def initialize(store = RAILS_CACHE) + @store = store + end + + def read(key) + @store.read(key) || [] + end + + def write(key, value) + @store.write(key, value) + end + + ::Rack::Cache::MetaStore::RAILS = self + end + + class RailsEntityStore < Rack::Cache::EntityStore + def self.resolve(uri) + new + end + + def initialize(store = RAILS_CACHE) + @store = store + end + + def exist?(key) + @store.exist?(key) + end + + def open(key) + @store.read(key) + end + + def read(key) + body = open(key) + body.join if body + end + + def write(body) + buf = [] + key, size = slurp(body) { |part| buf << part } + @store.write(key, buf) + [key, size] + end + + ::Rack::Cache::EntityStore::RAILS = self + end +end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index fd23b1df79..f07ac44f7a 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -2,8 +2,10 @@ require 'tempfile' require 'stringio' require 'strscan' +require 'active_support/core_ext/module/deprecation' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/string/access' +require 'active_support/inflector' require 'action_dispatch/http/headers' module ActionDispatch @@ -15,6 +17,8 @@ module ActionDispatch include ActionDispatch::Http::Upload include ActionDispatch::Http::URL + LOCALHOST = [/^127\.0\.0\.\d{1,3}$/, "::1", /^0:0:0:0:0:0:0:1(%.*)?$/].freeze + %w[ AUTH_TYPE GATEWAY_INTERFACE PATH_TRANSLATED REMOTE_HOST REMOTE_IDENT REMOTE_USER REMOTE_ADDR @@ -42,8 +46,24 @@ module ActionDispatch @env.key?(key) end - HTTP_METHODS = %w(get head put post delete options) - HTTP_METHOD_LOOKUP = HTTP_METHODS.inject({}) { |h, m| h[m] = h[m.upcase] = m.to_sym; h } + # List of HTTP request methods from the following RFCs: + # Hypertext Transfer Protocol -- HTTP/1.1 (http://www.ietf.org/rfc/rfc2616.txt) + # HTTP Extensions for Distributed Authoring -- WEBDAV (http://www.ietf.org/rfc/rfc2518.txt) + # Versioning Extensions to WebDAV (http://www.ietf.org/rfc/rfc3253.txt) + # Ordered Collections Protocol (WebDAV) (http://www.ietf.org/rfc/rfc3648.txt) + # Web Distributed Authoring and Versioning (WebDAV) Access Control Protocol (http://www.ietf.org/rfc/rfc3744.txt) + # Web Distributed Authoring and Versioning (WebDAV) SEARCH (http://www.ietf.org/rfc/rfc5323.txt) + # PATCH Method for HTTP (http://www.ietf.org/rfc/rfc5789.txt) + RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT) + RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK) + RFC3253 = %w(VERSION-CONTROL REPORT CHECKOUT CHECKIN UNCHECKOUT MKWORKSPACE UPDATE LABEL MERGE BASELINE-CONTROL MKACTIVITY) + RFC3648 = %w(ORDERPATCH) + RFC3744 = %w(ACL) + RFC5323 = %w(SEARCH) + RFC5789 = %w(PATCH) + + HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC5789 + HTTP_METHOD_LOOKUP = Hash.new { |h, m| h[m] = m.underscore.to_sym if HTTP_METHODS.include?(m) } # Returns the HTTP \method that the application should see. # In the case where the \method was overridden by a middleware @@ -52,11 +72,7 @@ module ActionDispatch # the application should use), this \method returns the overridden # value, not the original. def request_method - @request_method ||= begin - method = env["REQUEST_METHOD"] - HTTP_METHOD_LOOKUP[method] || raise(ActionController::UnknownHttpMethod, "#{method}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}") - method - end + @request_method ||= check_method(env["REQUEST_METHOD"]) end # Returns a symbol form of the #request_method @@ -68,11 +84,7 @@ module ActionDispatch # even if it was overridden by middleware. See #request_method for # more information. def method - @method ||= begin - method = env["rack.methodoverride.original_method"] || env['REQUEST_METHOD'] - HTTP_METHOD_LOOKUP[method] || raise(ActionController::UnknownHttpMethod, "#{method}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}") - method - end + @method ||= check_method(env["rack.methodoverride.original_method"] || env['REQUEST_METHOD']) end # Returns a symbol form of the #method @@ -122,8 +134,9 @@ module ActionDispatch end def forgery_whitelisted? - get? || xhr? || content_mime_type.nil? || !content_mime_type.verify_request? + get? end + deprecate :forgery_whitelisted? => "it is just an alias for 'get?' now, update your code" def media_type content_mime_type.to_s @@ -134,11 +147,11 @@ module ActionDispatch super.to_i end - # Returns true if the request's "X-Requested-With" header contains - # "XMLHttpRequest". (The Prototype Javascript library sends this header with - # every Ajax request.) + # Returns true if the "X-Requested-With" header contains "XMLHttpRequest" + # (case-insensitive). All major JavaScript libraries send this header with + # every Ajax request. def xml_http_request? - !(@env['HTTP_X_REQUESTED_WITH'] !~ /XMLHttpRequest/i) + @env['HTTP_X_REQUESTED_WITH'] =~ /XMLHttpRequest/i end alias :xhr? :xml_http_request? @@ -147,8 +160,16 @@ module ActionDispatch end # Which IP addresses are "trusted proxies" that can be stripped from - # the right-hand-side of X-Forwarded-For - TRUSTED_PROXIES = /^127\.0\.0\.1$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\./i + # the right-hand-side of X-Forwarded-For. + # + # http://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces. + TRUSTED_PROXIES = %r{ + ^127\.0\.0\.1$ | # localhost + ^(10 | # private IP 10.x.x.x + 172\.(1[6-9]|2[0-9]|3[0-1]) | # private IP in the range 172.16.0.0 .. 172.31.255.255 + 192\.168 # private IP 192.168.x.x + )\. + }x # Determines originating IP address. REMOTE_ADDR is the standard # but will fail if the user is behind a proxy. HTTP_CLIENT_IP and/or @@ -197,7 +218,7 @@ module ActionDispatch # TODO This should be broken apart into AD::Request::Session and probably # be included by the session middleware. def reset_session - session.destroy if session + session.destroy if session && session.respond_to?(:destroy) self.session = {} @env['action_dispatch.request.flash_hash'] = nil end @@ -212,13 +233,13 @@ module ActionDispatch # Override Rack's GET method to support indifferent access def GET - @env["action_dispatch.request.query_parameters"] ||= normalize_parameters(super) + @env["action_dispatch.request.query_parameters"] ||= (normalize_parameters(super) || {}) end alias :query_parameters :GET # Override Rack's POST method to support indifferent access def POST - @env["action_dispatch.request.request_parameters"] ||= normalize_parameters(super) + @env["action_dispatch.request.request_parameters"] ||= (normalize_parameters(super) || {}) end alias :request_parameters :POST @@ -231,5 +252,17 @@ module ActionDispatch @env['X_HTTP_AUTHORIZATION'] || @env['REDIRECT_X_HTTP_AUTHORIZATION'] end + + # True if the request came from localhost, 127.0.0.1. + def local? + LOCALHOST.any? { |local_ip| local_ip === remote_addr && local_ip === remote_ip } + end + + private + + def check_method(name) + HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}") + name + end end end diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb index 3b85a98576..8e03a7879f 100644 --- a/actionpack/lib/action_dispatch/http/response.rb +++ b/actionpack/lib/action_dispatch/http/response.rb @@ -4,27 +4,26 @@ require 'active_support/core_ext/object/blank' require 'active_support/core_ext/class/attribute_accessors' module ActionDispatch # :nodoc: - # Represents an HTTP response generated by a controller action. One can use - # an ActionDispatch::Response object to retrieve the current state - # of the response, or customize the response. An Response object can - # either represent a "real" HTTP response (i.e. one that is meant to be sent - # back to the web browser) or a test response (i.e. one that is generated - # from integration tests). See CgiResponse and TestResponse, respectively. + # Represents an HTTP response generated by a controller action. Use it to + # retrieve the current state of the response, or customize the response. It can + # either represent a real HTTP response (i.e. one that is meant to be sent + # back to the web browser) or a TestResponse (i.e. one that is generated + # from integration tests). # - # Response is mostly a Ruby on Rails framework implement detail, and + # \Response is mostly a Ruby on \Rails framework implementation detail, and # should never be used directly in controllers. Controllers should use the # methods defined in ActionController::Base instead. For example, if you want # to set the HTTP response's content MIME type, then use # ActionControllerBase#headers instead of Response#headers. # # Nevertheless, integration tests may want to inspect controller responses in - # more detail, and that's when Response can be useful for application + # more detail, and that's when \Response can be useful for application # developers. Integration test methods such as # ActionDispatch::Integration::Session#get and # ActionDispatch::Integration::Session#post return objects of type - # TestResponse (which are of course also of type Response). + # TestResponse (which are of course also of type \Response). # - # For example, the following demo integration "test" prints the body of the + # For example, the following demo integration test prints the body of the # controller response to the console: # # class DemoControllerTest < ActionDispatch::IntegrationTest @@ -45,8 +44,8 @@ module ActionDispatch # :nodoc: @block = nil @length = 0 - @status, @header = status, header - self.body = body + @header = header + self.body, self.status = body, status @cookie = [] @sending_file = false @@ -133,7 +132,7 @@ module ActionDispatch # :nodoc: # information. attr_accessor :charset, :content_type - CONTENT_TYPE = "Content-Type" + CONTENT_TYPE = "Content-Type" cattr_accessor(:default_charset) { "utf-8" } @@ -141,7 +140,6 @@ module ActionDispatch # :nodoc: assign_default_content_type_and_charset! handle_conditional_get! self["Set-Cookie"] = self["Set-Cookie"].join("\n") if self["Set-Cookie"].respond_to?(:join) - self["ETag"] = @_etag if @_etag super end diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index 8ee4b81cdd..37effade4f 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -1,32 +1,34 @@ -require 'active_support/core_ext/object/blank' - module ActionDispatch module Http - module UploadedFile - def self.extended(object) - object.class_eval do - attr_accessor :original_path, :content_type - alias_method :local_path, :path if method_defined?(:path) - end + class UploadedFile + attr_accessor :original_filename, :content_type, :tempfile, :headers + + def initialize(hash) + @original_filename = hash[:filename] + @content_type = hash[:type] + @headers = hash[:head] + @tempfile = hash[:tempfile] + raise(ArgumentError, ':tempfile is required') unless @tempfile end - # Take the basename of the upload's original filename. - # This handles the full Windows paths given by Internet Explorer - # (and perhaps other broken user agents) without affecting - # those which give the lone filename. - # The Windows regexp is adapted from Perl's File::Basename. - def original_filename - unless defined? @original_filename - @original_filename = - unless original_path.blank? - if original_path =~ /^(?:.*[:\\\/])?(.*)/m - $1 - else - File.basename original_path - end - end - end - @original_filename + def open + @tempfile.open + end + + def path + @tempfile.path + end + + def read(*args) + @tempfile.read(*args) + end + + def rewind + @tempfile.rewind + end + + def size + @tempfile.size end end @@ -35,11 +37,7 @@ module ActionDispatch # file upload hash with UploadedFile objects def normalize_parameters(value) if Hash === value && value.has_key?(:tempfile) - upload = value[:tempfile] - upload.extend(UploadedFile) - upload.original_path = value[:filename] - upload.content_type = value[:type] - upload + UploadedFile.new(value) else super end @@ -47,4 +45,4 @@ module ActionDispatch private :normalize_parameters end end -end \ No newline at end of file +end diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index b64a83c62e..ac0fd9607d 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -1,6 +1,81 @@ module ActionDispatch module Http module URL + mattr_accessor :tld_length + self.tld_length = 1 + + class << self + def extract_domain(host, tld_length = @@tld_length) + return nil unless named_host?(host) + host.split('.').last(1 + tld_length).join('.') + end + + def extract_subdomains(host, tld_length = @@tld_length) + return [] unless named_host?(host) + parts = host.split('.') + parts[0..-(tld_length+2)] + end + + def extract_subdomain(host, tld_length = @@tld_length) + extract_subdomains(host, tld_length).join('.') + end + + def url_for(options = {}) + unless options[:host].present? || options[:only_path].present? + raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true' + end + + rewritten_url = "" + + unless options[:only_path] + unless options[:protocol] == false + rewritten_url << (options[:protocol] || "http") + rewritten_url << ":" unless rewritten_url.match(%r{:|//}) + end + rewritten_url << "//" unless rewritten_url.match("//") + rewritten_url << rewrite_authentication(options) + rewritten_url << host_or_subdomain_and_domain(options) + rewritten_url << ":#{options.delete(:port)}" if options[:port] + end + + path = options.delete(:path) || '' + + params = options[:params] || {} + params.reject! {|k,v| v.to_param.nil? } + + rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path) + rewritten_url << "?#{params.to_query}" unless params.empty? + rewritten_url << "##{Rack::Mount::Utils.escape_uri(options[:anchor].to_param.to_s)}" if options[:anchor] + rewritten_url + end + + private + + def named_host?(host) + !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host)) + end + + def rewrite_authentication(options) + if options[:user] && options[:password] + "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@" + else + "" + end + end + + def host_or_subdomain_and_domain(options) + return options[:host] unless options[:subdomain] || options[:domain] + + tld_length = options[:tld_length] || @@tld_length + + host = "" + host << (options[:subdomain] || extract_subdomain(options[:host], tld_length)) + host << "." + host << (options[:domain] || extract_domain(options[:host], tld_length)) + host + end + end + # Returns the complete URL used for this request. def url protocol + host_with_port + fullpath @@ -8,12 +83,7 @@ module ActionDispatch # Returns 'https://' if this is an SSL request and 'http://' otherwise. def protocol - ssl? ? 'https://' : 'http://' - end - - # Is this an SSL request? - def ssl? - @env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https' + @protocol ||= ssl? ? 'https://' : 'http://' end # Returns the \host for this request, such as "example.com". @@ -38,10 +108,12 @@ module ActionDispatch # Returns the port number of this request as an integer. def port - if raw_host_with_port =~ /:(\d+)$/ - $1.to_i - else - standard_port + @port ||= begin + if raw_host_with_port =~ /:(\d+)$/ + $1.to_i + else + standard_port + end end end @@ -53,10 +125,21 @@ module ActionDispatch end end - # Returns a \port suffix like ":8080" if the \port number of this request + # Returns whether this request is using the standard port + def standard_port? + port == standard_port + end + + # Returns a number \port suffix like 8080 if the \port number of this request # is not the default HTTP \port 80 or HTTPS \port 443. + def optional_port + standard_port? ? nil : port + end + + # Returns a string \port suffix, including colon, like ":8080" if the \port + # number of this request is not the default HTTP \port 80 or HTTPS \port 443. def port_string - port == standard_port ? '' : ":#{port}" + standard_port? ? '' : ":#{port}" end def server_port @@ -65,38 +148,25 @@ module ActionDispatch # Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify # a different tld_length, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk". - def domain(tld_length = 1) - return nil unless named_host?(host) - - host.split('.').last(1 + tld_length).join('.') + def domain(tld_length = @@tld_length) + ActionDispatch::Http::URL.extract_domain(host, tld_length) end # Returns all the \subdomains as an array, so ["dev", "www"] would be # returned for "dev.www.rubyonrails.org". You can specify a different tld_length, # such as 2 to catch ["www"] instead of ["www", "rubyonrails"] # in "www.rubyonrails.co.uk". - def subdomains(tld_length = 1) - return [] unless named_host?(host) - parts = host.split('.') - parts[0..-(tld_length+2)] + def subdomains(tld_length = @@tld_length) + ActionDispatch::Http::URL.extract_subdomains(host, tld_length) end - def subdomain(tld_length = 1) - subdomains(tld_length).join('.') - end - - # Returns the request URI, accounting for server idiosyncrasies. - # WEBrick includes the full URL. IIS leaves REQUEST_URI blank. - def request_uri - ActiveSupport::Deprecation.warn "Using #request_uri is deprecated. Use fullpath instead.", caller - fullpath - end - - private - - def named_host?(host) - !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host)) + # Returns all the \subdomains as a string, so "dev.www" would be + # returned for "dev.www.rubyonrails.org". You can specify a different tld_length, + # such as 2 to catch ["www"] instead of "www.rubyonrails" + # in "www.rubyonrails.co.uk". + def subdomain(tld_length = @@tld_length) + subdomains(tld_length) end end end -end \ No newline at end of file +end diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb index e4ae480bfb..1bb2ad7f67 100644 --- a/actionpack/lib/action_dispatch/middleware/callbacks.rb +++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb @@ -1,30 +1,14 @@ +require 'active_support/core_ext/module/delegation' + module ActionDispatch # Provide callbacks to be executed before and after the request dispatch. - # - # It also provides a to_prepare callback, which is performed in all requests - # in development by only once in production and notification callback for async - # operations. - # class Callbacks include ActiveSupport::Callbacks define_callbacks :call, :rescuable => true - define_callbacks :prepare, :scope => :name - # Add a preparation callback. Preparation callbacks are run before every - # request in development mode, and before the first request in production mode. - # - # If a symbol with a block is given, the symbol is used as an identifier. - # That allows to_prepare to be called again with the same identifier to - # replace the existing callback. Passing an identifier is a suggested - # practice if the code adding a preparation block may be reloaded. - def self.to_prepare(*args, &block) - if args.first.is_a?(Symbol) && block_given? - define_method :"__#{args.first}", &block - set_callback(:prepare, :"__#{args.first}") - else - set_callback(:prepare, *args, &block) - end + class << self + delegate :to_prepare, :to_cleanup, :to => "ActionDispatch::Reloader" end def self.before(*args, &block) @@ -35,14 +19,13 @@ module ActionDispatch set_callback(:call, :after, *args, &block) end - def initialize(app, prepare_each_request = false) - @app, @prepare_each_request = app, prepare_each_request - _run_prepare_callbacks + def initialize(app, unused = nil) + ActiveSupport::Deprecation.warn "Passing a second argument to ActionDispatch::Callbacks.new is deprecated." unless unused.nil? + @app = app end def call(env) - _run_call_callbacks do - _run_prepare_callbacks if @prepare_each_request + run_callbacks :call do @app.call(env) end end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 4d33cd3b0c..7ac608f0a8 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -7,7 +7,7 @@ module ActionDispatch end end - # Cookies are read and written through ActionController#cookies. + # \Cookies are read and written through ActionController#cookies. # # The cookies being read are the ones received along with the request, the cookies # being written will be sent out with the response. Reading a cookie does not get @@ -16,15 +16,31 @@ module ActionDispatch # Examples for writing: # # # Sets a simple session cookie. + # # This cookie will be deleted when the user's browser is closed. # cookies[:user_name] = "david" # + # # Assign an array of values to a cookie. + # cookies[:lat_lon] = [47.68, -122.37] + # # # Sets a cookie that expires in 1 hour. # cookies[:login] = { :value => "XJ-122", :expires => 1.hour.from_now } # + # # Sets a signed cookie, which prevents a user from tampering with its value. + # # The cookie is signed by your app's config.secret_token value. + # # Rails generates this value by default when you create a new Rails app. + # cookies.signed[:user_id] = current_user.id + # + # # Sets a "permanent" cookie (which expires in 20 years from now). + # cookies.permanent[:login] = "XJ-122" + # + # # You can also chain these methods: + # cookies.permanent.signed[:login] = "XJ-122" + # # Examples for reading: # # cookies[:user_name] # => "david" # cookies.size # => 2 + # cookies[:lat_lon] # => [47.68, -122.37] # # Example for deleting: # @@ -55,7 +71,7 @@ module ActionDispatch # :domain => :all # Allow the cookie for the top most level # domain and subdomains. # - # * :expires - The time at which this cookie expires, as a Time object. + # * :expires - The time at which this cookie expires, as a \Time object. # * :secure - Whether this cookie is a only transmitted to HTTPS servers. # Default is +false+. # * :httponly - Whether this cookie is accessible via scripting or @@ -69,27 +85,36 @@ module ActionDispatch class CookieJar < Hash #:nodoc: - # This regular expression is used to split the levels of a domain - # So www.example.co.uk gives: - # $1 => www. - # $2 => example - # $3 => co.uk - DOMAIN_REGEXP = /^(.*\.)*(.*)\.(...|...\...|....|..\...|..)$/ + # This regular expression is used to split the levels of a domain. + # The top level domain can be any string without a period or + # **.**, ***.** style TLDs like co.uk or com.au + # + # www.example.co.uk gives: + # $& => example.co.uk + # + # example.com gives: + # $& => example.com + # + # lots.of.subdomains.example.local gives: + # $& => example.local + DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/ def self.build(request) secret = request.env[TOKEN_KEY] - host = request.env["HTTP_HOST"] + host = request.host + secure = request.ssl? - new(secret, host).tap do |hash| + new(secret, host, secure).tap do |hash| hash.update(request.cookies) end end - def initialize(secret = nil, host = nil) + def initialize(secret = nil, host = nil, secure = false) @secret = secret @set_cookies = {} @delete_cookies = {} @host = host + @secure = secure super() end @@ -103,8 +128,17 @@ module ActionDispatch options[:path] ||= "/" if options[:domain] == :all - @host =~ DOMAIN_REGEXP - options[:domain] = ".#{$2}.#{$3}" + # if there is a provided tld length then we use it otherwise default domain regexp + domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP + + # if host is not ip and matches domain regexp + # (ip confirms to domain regexp so we explicitly check for ip) + options[:domain] = if (@host !~ /^[\d.]+$/) && (@host =~ domain_regexp) + ".#{$&}" + end + elsif options[:domain].is_a? Array + # if host matches one of the supplied domains without a dot in front of it + options[:domain] = options[:domain].find {|domain| @host.include? domain[/^\.?(.*)$/, 1] } end end @@ -174,9 +208,15 @@ module ActionDispatch end def write(headers) - @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) } + @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) if write_cookie?(v) } @delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) } end + + private + + def write_cookie?(cookie) + @secure || !cookie[:secure] || defined?(Rails.env) && Rails.env.development? + end end class PermanentCookieJar < CookieJar #:nodoc: @@ -248,7 +288,7 @@ module ActionDispatch "integrity hash for cookie session data. Use " + "config.secret_token = \"some secret phrase of at " + "least #{SECRET_MIN_LENGTH} characters\"" + - "in config/application.rb" + "in config/initializers/secret_token.rb" end if secret.length < SECRET_MIN_LENGTH diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index bfa30cf5af..21aeeb217a 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -10,13 +10,13 @@ module ActionDispatch # The flash provides a way to pass temporary objects between actions. Anything you place in the flash will be exposed # to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create - # action that sets flash[:notice] = "Successfully created" before redirecting to a display action that can + # action that sets flash[:notice] = "Post successfully created" before redirecting to a display action that can # then expose the flash to its template. Actually, that exposure is automatically done. Example: # # class PostsController < ActionController::Base # def create # # save post - # flash[:notice] = "Successfully created post" + # flash[:notice] = "Post successfully created" # redirect_to posts_path(@post) # end # @@ -30,6 +30,11 @@ module ActionDispatch #
<%= flash[:notice] %>
# <% end %> # + # Since the +notice+ and +alert+ keys are a common idiom, convenience accessors are available: + # + # flash.alert = "You must be logged in" + # flash.notice = "Post successfully created" + # # This example just places a string in the flash, but you can put any object in there. And of course, you can put as # many as you like at a time too. Just remember: They'll be gone by the time the next action has been performed. # diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb new file mode 100644 index 0000000000..29289a76b4 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/reloader.rb @@ -0,0 +1,76 @@ +module ActionDispatch + # ActionDispatch::Reloader provides prepare and cleanup callbacks, + # intended to assist with code reloading during development. + # + # Prepare callbacks are run before each request, and cleanup callbacks + # after each request. In this respect they are analogs of ActionDispatch::Callback's + # before and after callbacks. However, cleanup callbacks are not called until the + # request is fully complete -- that is, after #close has been called on + # the response body. This is important for streaming responses such as the + # following: + # + # self.response_body = lambda { |response, output| + # # code here which refers to application models + # } + # + # Cleanup callbacks will not be called until after the response_body lambda + # is evaluated, ensuring that it can refer to application models and other + # classes before they are unloaded. + # + # By default, ActionDispatch::Reloader is included in the middleware stack + # only in the development environment; specifically, when config.cache_classes + # is false. Callbacks may be registered even when it is not included in the + # middleware stack, but are executed only when +ActionDispatch::Reloader.prepare!+ + # or +ActionDispatch::Reloader.cleanup!+ are called manually. + # + class Reloader + include ActiveSupport::Callbacks + + define_callbacks :prepare, :scope => :name + define_callbacks :cleanup, :scope => :name + + # Add a prepare callback. Prepare callbacks are run before each request, prior + # to ActionDispatch::Callback's before callbacks. + def self.to_prepare(*args, &block) + set_callback(:prepare, *args, &block) + end + + # Add a cleanup callback. Cleanup callbacks are run after each request is + # complete (after #close is called on the response body). + def self.to_cleanup(*args, &block) + set_callback(:cleanup, *args, &block) + end + + # Execute all prepare callbacks. + def self.prepare! + new(nil).run_callbacks :prepare + end + + # Execute all cleanup callbacks. + def self.cleanup! + new(nil).run_callbacks :cleanup + end + + def initialize(app) + @app = app + end + + module CleanupOnClose + def close + super if defined?(super) + ensure + ActionDispatch::Reloader.cleanup! + end + end + + def call(env) + run_callbacks :prepare + response = @app.call(env) + response[2].extend(CleanupOnClose) + response + rescue Exception + run_callbacks :cleanup + raise + end + end +end diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index dd82294644..64d3a87fd0 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -1,5 +1,6 @@ require 'rack/utils' require 'rack/request' +require 'rack/session/abstract/id' require 'action_dispatch/middleware/cookies' require 'active_support/core_ext/object/blank' @@ -8,249 +9,76 @@ module ActionDispatch class SessionRestoreError < StandardError #:nodoc: end - class AbstractStore - ENV_SESSION_KEY = 'rack.session'.freeze - ENV_SESSION_OPTIONS_KEY = 'rack.session.options'.freeze - - # thin wrapper around Hash that allows us to lazily - # load session id into session_options - class OptionsHash < Hash - def initialize(by, env, default_options) - @by = by - @env = env - @session_id_loaded = false - merge!(default_options) - end - - def [](key) - if key == :id - load_session_id! unless key?(:id) || has_session_id? - end - super - end - - private - - def has_session_id? - @session_id_loaded - end - - def load_session_id! - self[:id] = @by.send(:extract_session_id, @env) - @session_id_loaded = true - end + module DestroyableSession + def destroy + clear + options = @env[Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY] if @env + options ||= {} + @by.send(:destroy_session, @env, options[:id], options) if @by + options[:id] = nil + @loaded = false end + end - class SessionHash < Hash - def initialize(by, env) - super() - @by = by - @env = env - @loaded = false - end - - def [](key) - load_for_read! - super(key.to_s) - end - - def has_key?(key) - load_for_read! - super(key.to_s) - end - - def []=(key, value) - load_for_write! - super(key.to_s, value) - end - - def clear - load_for_write! - super - end - - def to_hash - load_for_read! - h = {}.replace(self) - h.delete_if { |k,v| v.nil? } - h - end - - def update(hash) - load_for_write! - super(hash.stringify_keys) - end - - def delete(key) - load_for_write! - super(key.to_s) - end - - def inspect - load_for_read! - super - end - - def exists? - return @exists if instance_variable_defined?(:@exists) - @exists = @by.send(:exists?, @env) - end - - def loaded? - @loaded - end - - def destroy - clear - @by.send(:destroy, @env) if @by - @env[ENV_SESSION_OPTIONS_KEY][:id] = nil if @env && @env[ENV_SESSION_OPTIONS_KEY] - @loaded = false - end - - private - - def load_for_read! - load! if !loaded? && exists? - end - - def load_for_write! - load! unless loaded? - end - - def load! - id, session = @by.send(:load_session, @env) - @env[ENV_SESSION_OPTIONS_KEY][:id] = id - replace(session.stringify_keys) - @loaded = true - end - - end - - DEFAULT_OPTIONS = { - :key => '_session_id', - :path => '/', - :domain => nil, - :expire_after => nil, - :secure => false, - :httponly => true, - :cookie_only => true - } + ::Rack::Session::Abstract::SessionHash.send :include, DestroyableSession + module Compatibility def initialize(app, options = {}) - @app = app - @default_options = DEFAULT_OPTIONS.merge(options) - @key = @default_options.delete(:key).freeze - @cookie_only = @default_options.delete(:cookie_only) - ensure_session_key! + options[:key] ||= '_session_id' + super end - def call(env) - prepare!(env) - response = @app.call(env) - - session_data = env[ENV_SESSION_KEY] - options = env[ENV_SESSION_OPTIONS_KEY] - - if !session_data.is_a?(AbstractStore::SessionHash) || session_data.loaded? || options[:expire_after] - session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.loaded? - - sid = options[:id] || generate_sid - session_data = session_data.to_hash - - value = set_session(env, sid, session_data) - return response unless value - - cookie = { :value => value } - unless options[:expire_after].nil? - cookie[:expires] = Time.now + options.delete(:expire_after) - end - - request = ActionDispatch::Request.new(env) - set_cookie(request, cookie.merge!(options)) - end - - response + def generate_sid + ActiveSupport::SecureRandom.hex(16) end - private + protected - def prepare!(env) - env[ENV_SESSION_KEY] = SessionHash.new(self, env) - env[ENV_SESSION_OPTIONS_KEY] = OptionsHash.new(self, env, @default_options) - end + def initialize_sid + @default_options.delete(:sidbits) + @default_options.delete(:secure_random) + end + end - def generate_sid - ActiveSupport::SecureRandom.hex(16) - end + module StaleSessionCheck + def load_session(env) + stale_session_check! { super } + end - def set_cookie(request, options) - if request.cookie_jar[@key] != options[:value] || !options[:expires].nil? - request.cookie_jar[@key] = options + def extract_session_id(env) + stale_session_check! { super } + end + + def stale_session_check! + yield + rescue ArgumentError => argument_error + if argument_error.message =~ %r{undefined class/module ([\w:]*\w)} + begin + # Note that the regexp does not allow $1 to end with a ':' + $1.constantize + rescue LoadError, NameError => const_error + raise ActionDispatch::Session::SessionRestoreError, "Session contains objects whose class definition isn't available.\nRemember to require the classes for all objects kept in the session.\n(Original exception: #{const_error.message} [#{const_error.class}])\n" end + retry + else + raise end + end + end - def load_session(env) - stale_session_check! do - sid = current_session_id(env) - sid, session = get_session(env, sid) - [sid, session] - end - end + class AbstractStore < Rack::Session::Abstract::ID + include Compatibility + include StaleSessionCheck - def extract_session_id(env) - stale_session_check! do - request = ActionDispatch::Request.new(env) - sid = request.cookies[@key] - sid ||= request.params[@key] unless @cookie_only - sid - end - end + def destroy_session(env, sid, options) + ActiveSupport::Deprecation.warn "Implementing #destroy in session stores is deprecated. " << + "Please implement destroy_session(env, session_id, options) instead." + destroy(env) + end - def current_session_id(env) - env[ENV_SESSION_OPTIONS_KEY][:id] - end - - def ensure_session_key! - if @key.blank? - raise ArgumentError, 'A key is required to write a ' + - 'cookie containing the session data. Use ' + - 'config.session_store SESSION_STORE, { :key => ' + - '"_myapp_session" } in config/application.rb' - end - end - - def stale_session_check! - yield - rescue ArgumentError => argument_error - if argument_error.message =~ %r{undefined class/module ([\w:]*\w)} - begin - # Note that the regexp does not allow $1 to end with a ':' - $1.constantize - rescue LoadError, NameError => const_error - raise ActionDispatch::Session::SessionRestoreError, "Session contains objects whose class definition isn't available.\nRemember to require the classes for all objects kept in the session.\n(Original exception: #{const_error.message} [#{const_error.class}])\n" - end - retry - else - raise - end - end - - def exists?(env) - current_session_id(env).present? - end - - def get_session(env, sid) - raise '#get_session needs to be implemented.' - end - - def set_session(env, sid, session_data) - raise '#set_session needs to be implemented and should return ' << - 'the value to be stored in the cookie (usually the sid)' - end - - def destroy(env) - raise '#destroy needs to be implemented.' - end + def destroy(env) + raise '#destroy needs to be implemented.' + end end end end diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb index ca1494425f..9c9ccc62f5 100644 --- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb @@ -1,5 +1,7 @@ require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/object/blank' +require 'action_dispatch/middleware/session/abstract_store' +require 'rack/session/cookie' module ActionDispatch module Session @@ -38,58 +40,32 @@ module ActionDispatch # "rake secret" and set the key in config/initializers/secret_token.rb. # # Note that changing digest or secret invalidates all existing sessions! - class CookieStore < AbstractStore - - def initialize(app, options = {}) - super(app, options.merge!(:cookie_only => true)) - freeze - end + class CookieStore < Rack::Session::Cookie + include Compatibility + include StaleSessionCheck private - def load_session(env) - data = unpacked_cookie_data(env) - data = persistent_session_id!(data) - [data["session_id"], data] - end - - def extract_session_id(env) - if data = unpacked_cookie_data(env) - data["session_id"] - else - nil - end - end - - def unpacked_cookie_data(env) - env["action_dispatch.request.unsigned_session_cookie"] ||= begin - stale_session_check! do - request = ActionDispatch::Request.new(env) - if data = request.cookie_jar.signed[@key] - data.stringify_keys! - end - data || {} + def unpacked_cookie_data(env) + env["action_dispatch.request.unsigned_session_cookie"] ||= begin + stale_session_check! do + request = ActionDispatch::Request.new(env) + if data = request.cookie_jar.signed[@key] + data.stringify_keys! end + data || {} end end + end - def set_cookie(request, options) - request.cookie_jar.signed[@key] = options - end + def set_session(env, sid, session_data, options) + persistent_session_id!(session_data, sid) + end - def set_session(env, sid, session_data) - persistent_session_id!(session_data, sid) - end - - def destroy(env) - # session data is stored on client; nothing to do here - end - - def persistent_session_id!(data, sid=nil) - data ||= {} - data["session_id"] ||= sid || generate_sid - data - end + def set_cookie(env, session_id, cookie) + request = ActionDispatch::Request.new(env) + request.cookie_jar.signed[@key] = cookie + end end end end diff --git a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb index 28e3dbd732..4dd9a946c2 100644 --- a/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/mem_cache_store.rb @@ -1,56 +1,17 @@ +require 'action_dispatch/middleware/session/abstract_store' +require 'rack/session/memcache' + module ActionDispatch module Session - class MemCacheStore < AbstractStore + class MemCacheStore < Rack::Session::Memcache + include Compatibility + include StaleSessionCheck + def initialize(app, options = {}) require 'memcache' - - # Support old :expires option options[:expire_after] ||= options[:expires] - - super - - @default_options = { - :namespace => 'rack:session', - :memcache_server => 'localhost:11211' - }.merge(@default_options) - - @pool = options[:cache] || MemCache.new(@default_options[:memcache_server], @default_options) - unless @pool.servers.any? { |s| s.alive? } - raise "#{self} unable to find server during initialization." - end - @mutex = Mutex.new - super end - - private - def get_session(env, sid) - sid ||= generate_sid - begin - session = @pool.get(sid) || {} - rescue MemCache::MemCacheError, Errno::ECONNREFUSED - session = {} - end - [sid, session] - end - - def set_session(env, sid, session_data) - options = env['rack.session.options'] - expiry = options[:expire_after] || 0 - @pool.set(sid, session_data, expiry) - sid - rescue MemCache::MemCacheError, Errno::ECONNREFUSED - false - end - - def destroy(env) - if sid = current_session_id(env) - @pool.delete(sid) - end - rescue MemCache::MemCacheError, Errno::ECONNREFUSED - false - end - end end end diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index e095b51342..dbe3206808 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -6,8 +6,6 @@ module ActionDispatch # This middleware rescues any exception returned by the application and renders # nice exception pages if it's being rescued locally. class ShowExceptions - LOCALHOST = [/^127\.0\.0\.\d{1,3}$/, "::1", /^0:0:0:0:0:0:0:1(%.*)?$/].freeze - RESCUES_TEMPLATE_PATH = File.join(File.dirname(__FILE__), 'templates') cattr_accessor :rescue_responses @@ -45,28 +43,29 @@ module ActionDispatch end def call(env) - status, headers, body = @app.call(env) + begin + status, headers, body = @app.call(env) + exception = nil - # Only this middleware cares about RoutingError. So, let's just raise - # it here. - # TODO: refactor this middleware to handle the X-Cascade scenario without - # having to raise an exception. - if headers['X-Cascade'] == 'pass' - raise ActionController::RoutingError, "No route matches #{env['PATH_INFO'].inspect}" + # Only this middleware cares about RoutingError. So, let's just raise + # it here. + if headers['X-Cascade'] == 'pass' + raise ActionController::RoutingError, "No route matches #{env['PATH_INFO'].inspect}" + end + rescue Exception => exception + raise exception if env['action_dispatch.show_exceptions'] == false end - [status, headers, body] - rescue Exception => exception - raise exception if env['action_dispatch.show_exceptions'] == false - render_exception(env, exception) + exception ? render_exception(env, exception) : [status, headers, body] end private def render_exception(env, exception) log_error(exception) + exception = original_exception(exception) request = Request.new(env) - if @consider_all_requests_local || local_request?(request) + if @consider_all_requests_local || request.local? rescue_action_locally(request, exception) else rescue_action_in_public(exception) @@ -112,11 +111,6 @@ module ActionDispatch end end - # True if the request came from localhost, 127.0.0.1. - def local_request?(request) - LOCALHOST.any? { |local_ip| local_ip === request.remote_addr && local_ip === request.remote_ip } - end - def status_code(exception) Rack::Utils.status_code(@@rescue_responses[exception.class.name]) end @@ -134,7 +128,7 @@ module ActionDispatch ActiveSupport::Deprecation.silence do message = "\n#{exception.class} (#{exception.message}):\n" - message << exception.annoted_source_code if exception.respond_to?(:annoted_source_code) + message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) message << " " << application_trace(exception).join("\n ") logger.fatal("#{message}\n\n") end @@ -161,5 +155,17 @@ module ActionDispatch def logger defined?(Rails.logger) ? Rails.logger : Logger.new($stderr) end + + def original_exception(exception) + if registered_original_exception?(exception) + exception.original_exception + else + exception + end + end + + def registered_original_exception?(exception) + exception.respond_to?(:original_exception) && @@rescue_responses.has_key?(exception.original_exception.class.name) + end end end diff --git a/actionpack/lib/action_dispatch/middleware/stack.rb b/actionpack/lib/action_dispatch/middleware/stack.rb index 41078eced7..a4308f528c 100644 --- a/actionpack/lib/action_dispatch/middleware/stack.rb +++ b/actionpack/lib/action_dispatch/middleware/stack.rb @@ -1,17 +1,27 @@ require "active_support/inflector/methods" +require "active_support/dependencies" module ActionDispatch - class MiddlewareStack < Array + class MiddlewareStack class Middleware - attr_reader :args, :block + attr_reader :args, :block, :name, :classcache def initialize(klass_or_name, *args, &block) - @ref = ActiveSupport::Dependencies::Reference.new(klass_or_name) + @klass = nil + + if klass_or_name.respond_to?(:name) + @klass = klass_or_name + @name = @klass.name + else + @name = klass_or_name.to_s + end + + @classcache = ActiveSupport::Dependencies::Reference @args, @block = args, block end def klass - @ref.get + @klass || classcache[@name] end def ==(middleware) @@ -21,7 +31,7 @@ module ActionDispatch when Class klass == middleware else - normalize(@ref.name) == normalize(middleware) + normalize(@name) == normalize(middleware) end end @@ -40,15 +50,39 @@ module ActionDispatch end end - def initialize(*args, &block) - super(*args) - block.call(self) if block_given? + include Enumerable + + attr_accessor :middlewares + + def initialize(*args) + @middlewares = [] + yield(self) if block_given? + end + + def each + @middlewares.each { |x| yield x } + end + + def size + middlewares.size + end + + def last + middlewares.last + end + + def [](i) + middlewares[i] + end + + def initialize_copy(other) + self.middlewares = other.middlewares.dup end def insert(index, *args, &block) index = assert_index(index, :before) middleware = self.class::Middleware.new(*args, &block) - super(index, middleware) + middlewares.insert(index, middleware) end alias_method :insert_before, :insert @@ -63,26 +97,25 @@ module ActionDispatch delete(target) end - def use(*args, &block) - middleware = self.class::Middleware.new(*args, &block) - push(middleware) + def delete(target) + middlewares.delete target end - def active - ActiveSupport::Deprecation.warn "All middlewares in the chain are active since the laziness " << - "was removed from the middleware stack", caller + def use(*args, &block) + middleware = self.class::Middleware.new(*args, &block) + middlewares.push(middleware) end def build(app = nil, &block) app ||= block raise "MiddlewareStack#build requires an app" unless app - reverse.inject(app) { |a, e| e.build(a) } + middlewares.reverse.inject(app) { |a, e| e.build(a) } end protected def assert_index(index, where) - i = index.is_a?(Integer) ? index : self.index(index) + i = index.is_a?(Integer) ? index : middlewares.index(index) raise "No such middleware to insert #{where}: #{index.inspect}" unless i i end diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index d7e88a54e4..c57f694c4d 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -1,12 +1,47 @@ require 'rack/utils' module ActionDispatch + class FileHandler + def initialize(at, root) + @at, @root = at.chomp('/'), root.chomp('/') + @compiled_at = @at.blank? ? nil : /^#{Regexp.escape(at)}/ + @compiled_root = /^#{Regexp.escape(root)}/ + @file_server = ::Rack::File.new(@root) + end + + def match?(path) + path = path.dup + if !@compiled_at || path.sub!(@compiled_at, '') + full_path = path.empty? ? @root : File.join(@root, ::Rack::Utils.unescape(path)) + paths = "#{full_path}#{ext}" + + matches = Dir[paths] + match = matches.detect { |m| File.file?(m) } + if match + match.sub!(@compiled_root, '') + match + end + end + end + + def call(env) + @file_server.call(env) + end + + def ext + @ext ||= begin + ext = ::ActionController::Base.page_cache_extension + "{,#{ext},/index#{ext}}" + end + end + end + class Static FILE_METHODS = %w(GET HEAD).freeze - def initialize(app, root) + def initialize(app, roots) @app = app - @file_server = ::Rack::File.new(root) + @file_handlers = create_file_handlers(roots) end def call(env) @@ -14,15 +49,10 @@ module ActionDispatch method = env['REQUEST_METHOD'] if FILE_METHODS.include?(method) - if file_exist?(path) - return @file_server.call(env) - else - cached_path = directory_exist?(path) ? "#{path}/index" : path - cached_path += ::ActionController::Base.page_cache_extension - - if file_exist?(cached_path) - env['PATH_INFO'] = cached_path - return @file_server.call(env) + @file_handlers.each do |file_handler| + if match = file_handler.match?(path) + env["PATH_INFO"] = match + return file_handler.call(env) end end end @@ -31,14 +61,12 @@ module ActionDispatch end private - def file_exist?(path) - full_path = File.join(@file_server.root, ::Rack::Utils.unescape(path)) - File.file?(full_path) && File.readable?(full_path) - end + def create_file_handlers(roots) + roots = { '' => roots } unless roots.is_a?(Hash) - def directory_exist?(path) - full_path = File.join(@file_server.root, ::Rack::Utils.unescape(path)) - File.directory?(full_path) && File.readable?(full_path) + roots.map do |at, root| + FileHandler.new(at, root) if File.exist?(root) + end.compact end end end diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb index e963b04524..97f7cf0bbe 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb @@ -14,7 +14,7 @@ def debug_hash(hash) hash.sort_by { |k, v| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n") - end + end unless self.class.method_defined?(:debug_hash) %>

Request

@@ -28,4 +28,4 @@

Response

-

Headers:

<%=h @response ? @response.headers.inspect.gsub(',', ",\n") : 'None' %>

+

Headers:

<%=h defined?(@response) ? @response.headers.inspect.gsub(',', ",\n") : 'None' %>

diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb index d18b162a93..8771b5fd6d 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb @@ -12,14 +12,14 @@
<% names.each do |name| %> <% - show = "document.getElementById('#{name.gsub /\s/, '-'}').style.display='block';" - hide = (names - [name]).collect {|hide_name| "document.getElementById('#{hide_name.gsub /\s/, '-'}').style.display='none';"} + show = "document.getElementById('#{name.gsub(/\s/, '-')}').style.display='block';" + hide = (names - [name]).collect {|hide_name| "document.getElementById('#{hide_name.gsub(/\s/, '-')}').style.display='none';"} %> <%= name %> <%= '|' unless names.last == name %> <% end %> <% traces.each do |name, trace| %> -
;"> +
;">
<%=h trace.join "\n" %>
<% end %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb index bd6ffbab5d..50d8ca9484 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb @@ -6,5 +6,5 @@
<%=h @exception.message %>
-<%= render :file => "rescues/_trace.erb" %> -<%= render :file => "rescues/_request_and_response.erb" %> +<%= render :template => "rescues/_trace" %> +<%= render :template => "rescues/_request_and_response" %> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb index 02fa18211d..c658559be9 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb @@ -13,9 +13,5 @@

<%=h @exception.sub_template_message %>

-<% @real_exception = @exception - @exception = @exception.original_exception || @exception %> -<%= render :file => "rescues/_trace.erb" %> -<% @exception = @real_exception %> - -<%= render :file => "rescues/_request_and_response.erb" %> +<%= render :template => "rescues/_trace" %> +<%= render :template => "rescues/_request_and_response" %> diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index a3af37947a..0a3bd5fe40 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -8,10 +8,11 @@ module ActionDispatch config.action_dispatch.ip_spoofing_check = true config.action_dispatch.show_exceptions = true config.action_dispatch.best_standards_support = true + config.action_dispatch.tld_length = 1 + config.action_dispatch.rack_cache = {:metastore => "rails:/", :entitystore => "rails:/", :verbose => true} - # Prepare dispatcher callbacks and run 'prepare' callbacks - initializer "action_dispatch.prepare_dispatcher" do |app| - ActionDispatch::Callbacks.to_prepare { app.routes_reloader.execute_if_updated } + initializer "action_dispatch.configure" do |app| + ActionDispatch::Http::URL.tld_length = app.config.action_dispatch.tld_length end end end diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 683dd72555..43fd93adf6 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -2,31 +2,11 @@ require 'active_support/core_ext/object/to_param' require 'active_support/core_ext/regexp' module ActionDispatch - # = Routing - # # The routing module provides URL rewriting in native Ruby. It's a way to # redirect incoming requests to controllers and actions. This replaces - # mod_rewrite rules. Best of all, Rails' Routing works with any web server. + # mod_rewrite rules. Best of all, Rails' \Routing works with any web server. # Routes are defined in config/routes.rb. # - # Consider the following route, which you will find commented out at the - # bottom of your generated config/routes.rb: - # - # match ':controller(/:action(/:id(.:format)))' - # - # This route states that it expects requests to consist of a - # :controller followed optionally by an :action that in - # turn is followed optionally by an :id, which in turn is followed - # optionally by a :format - # - # Suppose you get an incoming request for /blog/edit/22, you'll end - # up with: - # - # params = { :controller => 'blog', - # :action => 'edit', - # :id => '22' - # } - # # Think of creating routes as drawing a map for your requests. The map tells # them where to go based on some predefined pattern: # @@ -43,6 +23,51 @@ module ActionDispatch # # Other names simply map to a parameter as in the case of :id. # + # == Resources + # + # Resource routing allows you to quickly declare all of the common routes + # for a given resourceful controller. Instead of declaring separate routes + # for your +index+, +show+, +new+, +edit+, +create+, +update+ and +destroy+ + # actions, a resourceful route declares them in a single line of code: + # + # resources :photos + # + # Sometimes, you have a resource that clients always look up without + # referencing an ID. A common example, /profile always shows the profile of + # the currently logged in user. In this case, you can use a singular resource + # to map /profile (rather than /profile/:id) to the show action. + # + # resource :profile + # + # It's common to have resources that are logically children of other + # resources: + # + # resources :magazines do + # resources :ads + # end + # + # You may wish to organize groups of controllers under a namespace. Most + # commonly, you might group a number of administrative controllers under + # an +admin+ namespace. You would place these controllers under the + # app/controllers/admin directory, and you can group them together in your + # router: + # + # namespace "admin" do + # resources :posts, :comments + # end + # + # Alternately, you can add prefixes to your path without using a separate + # directory by using +scope+. +scope+ takes additional options which + # apply to all enclosed routes. + # + # scope :path => "/cpanel", :as => 'admin' do + # resources :posts, :comments + # end + # + # For more, see Routing::Mapper::Resources#resources, + # Routing::Mapper::Scoping#namespace, and + # Routing::Mapper::Scoping#scope. + # # == Named routes # # Routes can be named by passing an :as option, @@ -131,11 +156,35 @@ module ActionDispatch # Encoding regular expression modifiers are silently ignored. The # match will always use the default encoding or ASCII. # + # == Default route + # + # Consider the following route, which you will find commented out at the + # bottom of your generated config/routes.rb: + # + # match ':controller(/:action(/:id(.:format)))' + # + # This route states that it expects requests to consist of a + # :controller followed optionally by an :action that in + # turn is followed optionally by an :id, which in turn is followed + # optionally by a :format. + # + # Suppose you get an incoming request for /blog/edit/22, you'll end + # up with: + # + # params = { :controller => 'blog', + # :action => 'edit', + # :id => '22' + # } + # + # By not relying on default routes, you improve the security of your + # application since not all controller actions, which includes actions you + # might add at a later time, are exposed by default. + # # == HTTP Methods # # Using the :via option when specifying a route allows you to restrict it to a specific HTTP method. - # Possible values are :post, :get, :put, :delete and :any. - # If your route needs to respond to more than one method you can use an array, e.g. [ :get, :post ]. + # Possible values are :post, :get, :put, :delete and :any. + # If your route needs to respond to more than one method you can use an array, e.g. [ :get, :post ]. # The default value is :any which means that the route will respond to any of the HTTP methods. # # Examples: @@ -144,7 +193,7 @@ module ActionDispatch # match 'post/:id' => "posts#create_comment', :via => :post # # Now, if you POST to /posts/:id, it will route to the create_comment action. A GET on the same - # URL will route to the show action. + # URL will route to the show action. # # === HTTP helper methods # @@ -160,6 +209,20 @@ module ActionDispatch # however if your route needs to respond to more than one HTTP method (or all methods) then using the # :via option on match is preferable. # + # == External redirects + # + # You can redirect any path to another path using the redirect helper in your router: + # + # match "/stories" => redirect("/posts") + # + # == Routing to Rack Applications + # + # Instead of a String, like posts#index, which corresponds to the + # index action in the PostsController, you can specify any Rack application + # as the endpoint for a matcher: + # + # match "/application.js" => Sprockets + # # == Reloading routes # # You can reload routes if you feel you must: @@ -208,13 +271,15 @@ module ActionDispatch # # == View a list of all your routes # - # Run rake routes. + # rake routes + # + # Target specific controllers by prefixing the command with CONTROLLER=x. # module Routing - autoload :DeprecatedMapper, 'action_dispatch/routing/deprecated_mapper' autoload :Mapper, 'action_dispatch/routing/mapper' autoload :Route, 'action_dispatch/routing/route' autoload :RouteSet, 'action_dispatch/routing/route_set' + autoload :RoutesProxy, 'action_dispatch/routing/routes_proxy' autoload :UrlFor, 'action_dispatch/routing/url_for' autoload :PolymorphicRoutes, 'action_dispatch/routing/polymorphic_routes' diff --git a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb b/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb deleted file mode 100644 index e04062ce8b..0000000000 --- a/actionpack/lib/action_dispatch/routing/deprecated_mapper.rb +++ /dev/null @@ -1,525 +0,0 @@ -require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/object/with_options' -require 'active_support/core_ext/object/try' - -module ActionDispatch - module Routing - class RouteSet - attr_accessor :controller_namespaces - - CONTROLLER_REGEXP = /[_a-zA-Z0-9]+/ - - def controller_constraints - @controller_constraints ||= begin - namespaces = controller_namespaces + in_memory_controller_namespaces - source = namespaces.map { |ns| "#{Regexp.escape(ns)}/#{CONTROLLER_REGEXP.source}" } - source << CONTROLLER_REGEXP.source - Regexp.compile(source.sort.reverse.join('|')) - end - end - - def in_memory_controller_namespaces - namespaces = Set.new - ActionController::Base.descendants.each do |klass| - next if klass.anonymous? - namespaces << klass.name.underscore.split('/')[0...-1].join('/') - end - namespaces.delete('') - namespaces - end - end - - class DeprecatedMapper #:nodoc: - def initialize(set) #:nodoc: - ActiveSupport::Deprecation.warn "You are using the old router DSL which will be removed in Rails 3.1. " << - "Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/" - @set = set - end - - def connect(path, options = {}) - options = options.dup - - if conditions = options.delete(:conditions) - conditions = conditions.dup - method = [conditions.delete(:method)].flatten.compact - method.map! { |m| - m = m.to_s.upcase - - if m == "HEAD" - raise ArgumentError, "HTTP method HEAD is invalid in route conditions. Rails processes HEAD requests the same as GETs, returning just the response headers" - end - - unless HTTP_METHODS.include?(m.downcase.to_sym) - raise ArgumentError, "Invalid HTTP method specified in route conditions" - end - - m - } - - if method.length > 1 - method = Regexp.union(*method) - elsif method.length == 1 - method = method.first - else - method = nil - end - end - - path_prefix = options.delete(:path_prefix) - name_prefix = options.delete(:name_prefix) - namespace = options.delete(:namespace) - - name = options.delete(:_name) - name = "#{name_prefix}#{name}" if name_prefix - - requirements = options.delete(:requirements) || {} - defaults = options.delete(:defaults) || {} - options.each do |k, v| - if v.is_a?(Regexp) - if value = options.delete(k) - requirements[k.to_sym] = value - end - else - value = options.delete(k) - defaults[k.to_sym] = value.is_a?(Symbol) ? value : value.to_param - end - end - - requirements.each do |_, requirement| - if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} - raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" - end - if requirement.multiline? - raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}" - end - end - - requirements[:controller] ||= @set.controller_constraints - - if defaults[:controller] - defaults[:action] ||= 'index' - defaults[:controller] = defaults[:controller].to_s - defaults[:controller] = "#{namespace}#{defaults[:controller]}" if namespace - end - - if defaults[:action] - defaults[:action] = defaults[:action].to_s - end - - if path.is_a?(String) - path = "#{path_prefix}/#{path}" if path_prefix - path = path.gsub('.:format', '(.:format)') - path = optionalize_trailing_dynamic_segments(path, requirements, defaults) - glob = $1.to_sym if path =~ /\/\*(\w+)$/ - path = ::Rack::Mount::Utils.normalize_path(path) - - if glob && !defaults[glob].blank? - raise ActionController::RoutingError, "paths cannot have non-empty default values" - end - end - - app = Routing::RouteSet::Dispatcher.new(:defaults => defaults, :glob => glob) - - conditions = {} - conditions[:request_method] = method if method - conditions[:path_info] = path if path - - @set.add_route(app, conditions, requirements, defaults, name) - end - - def optionalize_trailing_dynamic_segments(path, requirements, defaults) #:nodoc: - path = (path =~ /^\//) ? path.dup : "/#{path}" - optional, segments = true, [] - - required_segments = requirements.keys - required_segments -= defaults.keys.compact - - old_segments = path.split('/') - old_segments.shift - length = old_segments.length - - old_segments.reverse.each_with_index do |segment, index| - required_segments.each do |required| - if segment =~ /#{required}/ - optional = false - break - end - end - - if optional - if segment == ":id" && segments.include?(":action") - optional = false - elsif segment == ":controller" || segment == ":action" || segment == ":id" - # Ignore - elsif !(segment =~ /^:\w+$/) && - !(segment =~ /^:\w+\(\.:format\)$/) - optional = false - elsif segment =~ /^:(\w+)$/ - if defaults.has_key?($1.to_sym) - defaults.delete($1.to_sym) if defaults[$1.to_sym].nil? - else - optional = false - end - end - end - - if optional && index < length - 1 - segments.unshift('(/', segment) - segments.push(')') - elsif optional - segments.unshift('/(', segment) - segments.push(')') - else - segments.unshift('/', segment) - end - end - - segments.join - end - private :optionalize_trailing_dynamic_segments - - # Creates a named route called "root" for matching the root level request. - def root(options = {}) - if options.is_a?(Symbol) - if source_route = @set.named_routes.routes[options] - options = source_route.defaults.merge({ :conditions => source_route.conditions }) - end - end - named_route("root", '', options) - end - - def named_route(name, path, options = {}) #:nodoc: - options[:_name] = name - connect(path, options) - end - - def namespace(name, options = {}, &block) - if options[:namespace] - with_options({:path_prefix => "#{options.delete(:path_prefix)}/#{name}", :name_prefix => "#{options.delete(:name_prefix)}#{name}_", :namespace => "#{options.delete(:namespace)}#{name}/" }.merge(options), &block) - else - with_options({:path_prefix => name, :name_prefix => "#{name}_", :namespace => "#{name}/" }.merge(options), &block) - end - end - - def method_missing(route_name, *args, &proc) #:nodoc: - super unless args.length >= 1 && proc.nil? - named_route(route_name, *args) - end - - INHERITABLE_OPTIONS = :namespace, :shallow - - class Resource #:nodoc: - DEFAULT_ACTIONS = :index, :create, :new, :edit, :show, :update, :destroy - - attr_reader :collection_methods, :member_methods, :new_methods - attr_reader :path_prefix, :name_prefix, :path_segment - attr_reader :plural, :singular - attr_reader :options, :defaults - - def initialize(entities, options, defaults) - @plural ||= entities - @singular ||= options[:singular] || plural.to_s.singularize - @path_segment = options.delete(:as) || @plural - - @options = options - @defaults = defaults - - arrange_actions - add_default_actions - set_allowed_actions - set_prefixes - end - - def controller - @controller ||= "#{options[:namespace]}#{(options[:controller] || plural).to_s}" - end - - def requirements(with_id = false) - @requirements ||= @options[:requirements] || {} - @id_requirement ||= { :id => @requirements.delete(:id) || /[^#{Routing::SEPARATORS.join}]+/ } - - with_id ? @requirements.merge(@id_requirement) : @requirements - end - - def conditions - @conditions ||= @options[:conditions] || {} - end - - def path - @path ||= "#{path_prefix}/#{path_segment}" - end - - def new_path - new_action = self.options[:path_names][:new] if self.options[:path_names] - new_action ||= self.defaults[:path_names][:new] - @new_path ||= "#{path}/#{new_action}" - end - - def shallow_path_prefix - @shallow_path_prefix ||= @options[:shallow] ? @options[:namespace].try(:sub, /\/$/, '') : path_prefix - end - - def member_path - @member_path ||= "#{shallow_path_prefix}/#{path_segment}/:id" - end - - def nesting_path_prefix - @nesting_path_prefix ||= "#{shallow_path_prefix}/#{path_segment}/:#{singular}_id" - end - - def shallow_name_prefix - @shallow_name_prefix ||= @options[:shallow] ? @options[:namespace].try(:gsub, /\//, '_') : name_prefix - end - - def nesting_name_prefix - "#{shallow_name_prefix}#{singular}_" - end - - def action_separator - @action_separator ||= ActionController::Base.resource_action_separator - end - - def uncountable? - @singular.to_s == @plural.to_s - end - - def has_action?(action) - !DEFAULT_ACTIONS.include?(action) || action_allowed?(action) - end - - protected - def arrange_actions - @collection_methods = arrange_actions_by_methods(options.delete(:collection)) - @member_methods = arrange_actions_by_methods(options.delete(:member)) - @new_methods = arrange_actions_by_methods(options.delete(:new)) - end - - def add_default_actions - add_default_action(member_methods, :get, :edit) - add_default_action(new_methods, :get, :new) - end - - def set_allowed_actions - only, except = @options.values_at(:only, :except) - @allowed_actions ||= {} - - if only == :all || except == :none - only = nil - except = [] - elsif only == :none || except == :all - only = [] - except = nil - end - - if only - @allowed_actions[:only] = Array(only).map {|a| a.to_sym } - elsif except - @allowed_actions[:except] = Array(except).map {|a| a.to_sym } - end - end - - def action_allowed?(action) - only, except = @allowed_actions.values_at(:only, :except) - (!only || only.include?(action)) && (!except || !except.include?(action)) - end - - def set_prefixes - @path_prefix = options.delete(:path_prefix) - @name_prefix = options.delete(:name_prefix) - end - - def arrange_actions_by_methods(actions) - (actions || {}).inject({}) do |flipped_hash, (key, value)| - (flipped_hash[value] ||= []) << key - flipped_hash - end - end - - def add_default_action(collection, method, action) - (collection[method] ||= []).unshift(action) - end - end - - class SingletonResource < Resource #:nodoc: - def initialize(entity, options, defaults) - @singular = @plural = entity - options[:controller] ||= @singular.to_s.pluralize - super - end - - alias_method :shallow_path_prefix, :path_prefix - alias_method :shallow_name_prefix, :name_prefix - alias_method :member_path, :path - alias_method :nesting_path_prefix, :path - end - - def resources(*entities, &block) - options = entities.extract_options! - entities.each { |entity| map_resource(entity, options.dup, &block) } - end - - def resource(*entities, &block) - options = entities.extract_options! - entities.each { |entity| map_singleton_resource(entity, options.dup, &block) } - end - - private - def map_resource(entities, options = {}, &block) - resource = Resource.new(entities, options, :path_names => @set.resources_path_names) - - with_options :controller => resource.controller do |map| - map_associations(resource, options) - - if block_given? - with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block) - end - - map_collection_actions(map, resource) - map_default_collection_actions(map, resource) - map_new_actions(map, resource) - map_member_actions(map, resource) - end - end - - def map_singleton_resource(entities, options = {}, &block) - resource = SingletonResource.new(entities, options, :path_names => @set.resources_path_names) - - with_options :controller => resource.controller do |map| - map_associations(resource, options) - - if block_given? - with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block) - end - - map_collection_actions(map, resource) - map_new_actions(map, resource) - map_member_actions(map, resource) - map_default_singleton_actions(map, resource) - end - end - - def map_associations(resource, options) - map_has_many_associations(resource, options.delete(:has_many), options) if options[:has_many] - - path_prefix = "#{options.delete(:path_prefix)}#{resource.nesting_path_prefix}" - name_prefix = "#{options.delete(:name_prefix)}#{resource.nesting_name_prefix}" - - Array(options[:has_one]).each do |association| - resource(association, options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => path_prefix, :name_prefix => name_prefix)) - end - end - - def map_has_many_associations(resource, associations, options) - case associations - when Hash - associations.each do |association,has_many| - map_has_many_associations(resource, association, options.merge(:has_many => has_many)) - end - when Array - associations.each do |association| - map_has_many_associations(resource, association, options) - end - when Symbol, String - resources(associations, options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix, :has_many => options[:has_many])) - else - end - end - - def map_collection_actions(map, resource) - resource.collection_methods.each do |method, actions| - actions.each do |action| - [method].flatten.each do |m| - action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash) - action_path ||= action - - map_resource_routes(map, resource, action, "#{resource.path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.name_prefix}#{resource.plural}", m) - end - end - end - end - - def map_default_collection_actions(map, resource) - index_route_name = "#{resource.name_prefix}#{resource.plural}" - - if resource.uncountable? - index_route_name << "_index" - end - - map_resource_routes(map, resource, :index, resource.path, index_route_name) - map_resource_routes(map, resource, :create, resource.path, index_route_name) - end - - def map_default_singleton_actions(map, resource) - map_resource_routes(map, resource, :create, resource.path, "#{resource.shallow_name_prefix}#{resource.singular}") - end - - def map_new_actions(map, resource) - resource.new_methods.each do |method, actions| - actions.each do |action| - route_path = resource.new_path - route_name = "new_#{resource.name_prefix}#{resource.singular}" - - unless action == :new - route_path = "#{route_path}#{resource.action_separator}#{action}" - route_name = "#{action}_#{route_name}" - end - - map_resource_routes(map, resource, action, route_path, route_name, method) - end - end - end - - def map_member_actions(map, resource) - resource.member_methods.each do |method, actions| - actions.each do |action| - [method].flatten.each do |m| - action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash) - action_path ||= @set.resources_path_names[action] || action - - map_resource_routes(map, resource, action, "#{resource.member_path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.shallow_name_prefix}#{resource.singular}", m, { :force_id => true }) - end - end - end - - route_path = "#{resource.shallow_name_prefix}#{resource.singular}" - map_resource_routes(map, resource, :show, resource.member_path, route_path) - map_resource_routes(map, resource, :update, resource.member_path, route_path) - map_resource_routes(map, resource, :destroy, resource.member_path, route_path) - end - - def map_resource_routes(map, resource, action, route_path, route_name = nil, method = nil, resource_options = {} ) - if resource.has_action?(action) - action_options = action_options_for(action, resource, method, resource_options) - formatted_route_path = "#{route_path}.:format" - - if route_name && @set.named_routes[route_name.to_sym].nil? - map.named_route(route_name, formatted_route_path, action_options) - else - map.connect(formatted_route_path, action_options) - end - end - end - - def add_conditions_for(conditions, method) - {:conditions => conditions.dup}.tap do |options| - options[:conditions][:method] = method unless method == :any - end - end - - def action_options_for(action, resource, method = nil, resource_options = {}) - default_options = { :action => action.to_s } - require_id = !resource.kind_of?(SingletonResource) - force_id = resource_options[:force_id] && !resource.kind_of?(SingletonResource) - - case default_options[:action] - when "index", "new"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements) - when "create"; default_options.merge(add_conditions_for(resource.conditions, method || :post)).merge(resource.requirements) - when "show", "edit"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements(require_id)) - when "update"; default_options.merge(add_conditions_for(resource.conditions, method || :put)).merge(resource.requirements(require_id)) - when "destroy"; default_options.merge(add_conditions_for(resource.conditions, method || :delete)).merge(resource.requirements(require_id)) - else default_options.merge(add_conditions_for(resource.conditions, method)).merge(resource.requirements(force_id)) - end - end - end - end -end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index c118c72440..14c424f24b 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1,6 +1,8 @@ require 'erb' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/object/blank' +require 'active_support/inflector' +require 'action_dispatch/routing/redirection' module ActionDispatch module Routing @@ -20,27 +22,37 @@ module ActionDispatch @app, @constraints, @request = app, constraints, request end - def call(env) + def matches?(env) req = @request.new(env) @constraints.each { |constraint| if constraint.respond_to?(:matches?) && !constraint.matches?(req) - return [ 404, {'X-Cascade' => 'pass'}, [] ] - elsif constraint.respond_to?(:call) && !constraint.call(req) - return [ 404, {'X-Cascade' => 'pass'}, [] ] + return false + elsif constraint.respond_to?(:call) && !constraint.call(*constraint_args(constraint, req)) + return false end } - @app.call(env) + return true end + + def call(env) + matches?(env) ? @app.call(env) : [ 404, {'X-Cascade' => 'pass'}, [] ] + end + + private + def constraint_args(constraint, request) + constraint.arity == 1 ? [request] : [request.symbolized_path_parameters, request] + end end class Mapping #:nodoc: IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix] - def initialize(set, scope, args) - @set, @scope = set, scope - @path, @options = extract_path_and_options(args) + def initialize(set, scope, path, options) + @set, @scope = set, scope + @options = (@scope[:options] || {}).merge(options) + @path = normalize_path(path) normalize_options! end @@ -49,28 +61,6 @@ module ActionDispatch end private - def extract_path_and_options(args) - options = args.extract_options! - - if using_to_shorthand?(args, options) - path, to = options.find { |name, value| name.is_a?(String) } - options.merge!(:to => to).delete(path) if path - else - path = args.first - end - - if path.match(':controller') - raise ArgumentError, ":controller segment is not allowed within a namespace block" if @scope[:module] - - # Add a default constraint for :controller path segments that matches namespaced - # controllers with default routes like :controller/:action/:id(.:format), e.g: - # GET /admin/products/show/1 - # => { :controller => 'admin/products', :action => 'show', :id => '1' } - options.reverse_merge!(:controller => /.+?/) - end - - [ normalize_path(path), options ] - end def normalize_options! path_without_format = @path.sub(/\(\.:format\)$/, '') @@ -78,15 +68,21 @@ module ActionDispatch if using_match_shorthand?(path_without_format, @options) to_shorthand = @options[:to].blank? @options[:to] ||= path_without_format[1..-1].sub(%r{/([^/]*)$}, '#\1') - @options[:as] ||= path_without_format[1..-1].gsub("/", "_") end @options.merge!(default_controller_and_action(to_shorthand)) - end - # match "account" => "account#index" - def using_to_shorthand?(args, options) - args.empty? && options.present? + requirements.each do |name, requirement| + # segment_keys.include?(k.to_s) || k == :controller + next unless Regexp === requirement && !constraints[name] + + if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} + raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}" + end + if requirement.multiline? + raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}" + end + end end # match "account/overview" @@ -95,8 +91,27 @@ module ActionDispatch end def normalize_path(path) - raise ArgumentError, "path is required" if @scope[:path].blank? && path.blank? - Mapper.normalize_path("#{@scope[:path]}/#{path}") + raise ArgumentError, "path is required" if path.blank? + path = Mapper.normalize_path(path) + + if path.match(':controller') + raise ArgumentError, ":controller segment is not allowed within a namespace block" if @scope[:module] + + # Add a default constraint for :controller path segments that matches namespaced + # controllers with default routes like :controller/:action/:id(.:format), e.g: + # GET /admin/products/show/1 + # => { :controller => 'admin/products', :action => 'show', :id => '1' } + @options.reverse_merge!(:controller => /.+?/) + end + + if @options[:format] == false + @options.delete(:format) + path + elsif path.include?(":format") || path.end_with?('/') || path.match(/^\/?\*/) + path + else + "#{path}(.:format)" + end end def app @@ -142,6 +157,10 @@ module ActionDispatch controller = [@scope[:module], controller].compact.join("/").presence end + if controller.is_a?(String) && controller =~ %r{\A/} + raise ArgumentError, "controller name should not start with a slash" + end + controller = controller.to_s unless controller.is_a?(Regexp) action = action.to_s unless action.is_a?(Regexp) @@ -153,21 +172,21 @@ module ActionDispatch raise ArgumentError, "missing :action" end - { :controller => controller, :action => action }.tap do |hash| - hash.delete(:controller) if hash[:controller].blank? - hash.delete(:action) if hash[:action].blank? - end + hash = {} + hash[:controller] = controller unless controller.blank? + hash[:action] = action unless action.blank? + hash end end def blocks + block = @scope[:blocks] || [] + if @options[:constraints].present? && !@options[:constraints].is_a?(Hash) - block = @options[:constraints] - else - block = nil + block << @options[:constraints] end - ((@scope[:blocks] || []) + [ block ]).compact + block end def constraints @@ -176,8 +195,8 @@ module ActionDispatch def request_method_condition if via = @options[:via] - via = Array(via).map { |m| m.to_s.upcase } - { :request_method => Regexp.union(*via) } + list = Array(via).map { |m| m.to_s.dasherize.upcase } + { :request_method => list } else { } end @@ -219,21 +238,165 @@ module ActionDispatch path end - module Base - def initialize(set) #:nodoc: - @set = set - end + def self.normalize_name(name) + normalize_path(name)[1..-1].gsub("/", "_") + end + module Base + # You can specify what Rails should route "/" to with the root method: + # + # root :to => 'pages#main' + # + # For options, see +match+, as +root+ uses it internally. + # + # You should put the root route at the top of config/routes.rb, + # because this means it will be matched first. As this is the most popular route + # of most Rails applications, this is beneficial. def root(options = {}) match '/', options.reverse_merge(:as => :root) end - def match(*args) - mapping = Mapping.new(@set, @scope, args).to_route - @set.add_route(*mapping) + # Matches a url pattern to one or more routes. Any symbols in a pattern + # are interpreted as url query parameters and thus available as +params+ + # in an action: + # + # # sets :controller, :action and :id in params + # match ':controller/:action/:id' + # + # Two of these symbols are special, +:controller+ maps to the controller + # and +:action+ to the controller's action. A pattern can also map + # wildcard segments (globs) to params: + # + # match 'songs/*category/:title' => 'songs#show' + # + # # 'songs/rock/classic/stairway-to-heaven' sets + # # params[:category] = 'rock/classic' + # # params[:title] = 'stairway-to-heaven' + # + # When a pattern points to an internal route, the route's +:action+ and + # +:controller+ should be set in options or hash shorthand. Examples: + # + # match 'photos/:id' => 'photos#show' + # match 'photos/:id', :to => 'photos#show' + # match 'photos/:id', :controller => 'photos', :action => 'show' + # + # A pattern can also point to a +Rack+ endpoint i.e. anything that + # responds to +call+: + # + # match 'photos/:id' => lambda {|hash| [200, {}, "Coming soon" } + # match 'photos/:id' => PhotoRackApp + # # Yes, controller actions are just rack endpoints + # match 'photos/:id' => PhotosController.action(:show) + # + # === Options + # + # Any options not seen here are passed on as params with the url. + # + # [:controller] + # The route's controller. + # + # [:action] + # The route's action. + # + # [:path] + # The path prefix for the routes. + # + # [:module] + # The namespace for :controller. + # + # match 'path' => 'c#a', :module => 'sekret', :controller => 'posts' + # #=> Sekret::PostsController + # + # See Scoping#namespace for its scope equivalent. + # + # [:as] + # The name used to generate routing helpers. + # + # [:via] + # Allowed HTTP verb(s) for route. + # + # match 'path' => 'c#a', :via => :get + # match 'path' => 'c#a', :via => [:get, :post] + # + # [:to] + # Points to a +Rack+ endpoint. Can be an object that responds to + # +call+ or a string representing a controller's action. + # + # match 'path', :to => 'controller#action' + # match 'path', :to => lambda { [200, {}, "Success!"] } + # match 'path', :to => RackApp + # + # [:on] + # Shorthand for wrapping routes in a specific RESTful context. Valid + # values are :member, :collection, and :new. Only use within + # resource(s) block. For example: + # + # resource :bar do + # match 'foo' => 'c#a', :on => :member, :via => [:get, :post] + # end + # + # Is equivalent to: + # + # resource :bar do + # member do + # match 'foo' => 'c#a', :via => [:get, :post] + # end + # end + # + # [:constraints] + # Constrains parameters with a hash of regular expressions or an + # object that responds to #matches? + # + # match 'path/:id', :constraints => { :id => /[A-Z]\d{5}/ } + # + # class Blacklist + # def matches?(request) request.remote_ip == '1.2.3.4' end + # end + # match 'path' => 'c#a', :constraints => Blacklist.new + # + # See Scoping#constraints for more examples with its scope + # equivalent. + # + # [:defaults] + # Sets defaults for parameters + # + # # Sets params[:format] to 'jpg' by default + # match 'path' => 'c#a', :defaults => { :format => 'jpg' } + # + # See Scoping#defaults for its scope equivalent. + # + # [:anchor] + # Boolean to anchor a #match pattern. Default is true. When set to + # false, the pattern matches any request prefixed with the given path. + # + # # Matches any request starting with 'path' + # match 'path' => 'c#a', :anchor => false + def match(path, options=nil) + mapping = Mapping.new(@set, @scope, path, options || {}) + app, conditions, requirements, defaults, as, anchor = mapping.to_route + @set.add_route(app, conditions, requirements, defaults, as, anchor) self end + # Mount a Rack-based application to be used within the application. + # + # mount SomeRackApp, :at => "some_route" + # + # Alternatively: + # + # mount(SomeRackApp => "some_route") + # + # For options, see +match+, as +mount+ uses it internally. + # + # All mounted applications come with routing helpers to access them. + # These are named after the class specified, so for the above example + # the helper is either +some_rack_app_path+ or +some_rack_app_url+. + # To customize this helper's name, use the +:as+ option: + # + # mount(SomeRackApp => "some_route", :as => "exciting") + # + # This will generate the +exciting_path+ and +exciting_url+ helpers + # which can be used to navigate to this mounted app. def mount(app, options = nil) if options path = options.delete(:at) @@ -245,7 +408,11 @@ module ActionDispatch raise "A rack application must be specified" unless path - match(path, options.merge(:to => app, :anchor => false)) + options[:as] ||= app_name(app) + + match(path, options.merge(:to => app, :anchor => false, :format => false)) + + define_generate_prefix(app, options[:as]) self end @@ -253,55 +420,83 @@ module ActionDispatch @set.default_url_options = options end alias_method :default_url_options, :default_url_options= + + def with_default_scope(scope, &block) + scope(scope) do + instance_exec(&block) + end + end + + private + def app_name(app) + return unless app.respond_to?(:routes) + + if app.respond_to?(:railtie_name) + app.railtie_name + else + class_name = app.class.is_a?(Class) ? app.name : app.class.name + ActiveSupport::Inflector.underscore(class_name).gsub("/", "_") + end + end + + def define_generate_prefix(app, name) + return unless app.respond_to?(:routes) && app.routes.respond_to?(:define_mounted_helper) + + _route = @set.named_routes.routes[name.to_sym] + _routes = @set + app.routes.define_mounted_helper(name) + app.routes.class_eval do + define_method :_generate_prefix do |options| + prefix_options = options.slice(*_route.segment_keys) + # we must actually delete prefix segment keys to avoid passing them to next url_for + _route.segment_keys.each { |k| options.delete(k) } + _routes.url_helpers.send("#{name}_path", prefix_options) + end + end + end end module HttpHelpers + # Define a route that only recognizes HTTP GET. + # For supported arguments, see Base#match. + # + # Example: + # + # get 'bacon', :to => 'food#bacon' def get(*args, &block) map_method(:get, *args, &block) end + # Define a route that only recognizes HTTP POST. + # For supported arguments, see Base#match. + # + # Example: + # + # post 'bacon', :to => 'food#bacon' def post(*args, &block) map_method(:post, *args, &block) end + # Define a route that only recognizes HTTP PUT. + # For supported arguments, see Base#match. + # + # Example: + # + # put 'bacon', :to => 'food#bacon' def put(*args, &block) map_method(:put, *args, &block) end + # Define a route that only recognizes HTTP PUT. + # For supported arguments, see Base#match. + # + # Example: + # + # delete 'broccoli', :to => 'food#broccoli' def delete(*args, &block) map_method(:delete, *args, &block) end - def redirect(*args, &block) - options = args.last.is_a?(Hash) ? args.pop : {} - - path = args.shift || block - path_proc = path.is_a?(Proc) ? path : proc { |params| path % params } - status = options[:status] || 301 - - lambda do |env| - req = Request.new(env) - - params = [req.symbolized_path_parameters] - params << req if path_proc.arity > 1 - - uri = URI.parse(path_proc.call(*params)) - uri.scheme ||= req.scheme - uri.host ||= req.host - uri.port ||= req.port unless req.port == 80 - - body = %(You are being redirected.) - - headers = { - 'Location' => uri.to_s, - 'Content-Type' => 'text/html', - 'Content-Length' => body.length.to_s - } - - [ status, headers, [body] ] - end - end - private def map_method(method, *args, &block) options = args.extract_options! @@ -312,28 +507,98 @@ module ActionDispatch end end + # You may wish to organize groups of controllers under a namespace. + # Most commonly, you might group a number of administrative controllers + # under an +admin+ namespace. You would place these controllers under + # the app/controllers/admin directory, and you can group them together + # in your router: + # + # namespace "admin" do + # resources :posts, :comments + # end + # + # This will create a number of routes for each of the posts and comments + # controller. For Admin::PostsController, Rails will create: + # + # GET /admin/posts + # GET /admin/posts/new + # POST /admin/posts + # GET /admin/posts/1 + # GET /admin/posts/1/edit + # PUT /admin/posts/1 + # DELETE /admin/posts/1 + # + # If you want to route /posts (without the prefix /admin) to + # Admin::PostsController, you could use + # + # scope :module => "admin" do + # resources :posts + # end + # + # or, for a single case + # + # resources :posts, :module => "admin" + # + # If you want to route /admin/posts to PostsController + # (without the Admin:: module prefix), you could use + # + # scope "/admin" do + # resources :posts + # end + # + # or, for a single case + # + # resources :posts, :path => "/admin" + # + # In each of these cases, the named routes remain the same as if you did + # not use scope. In the last case, the following paths map to + # PostsController: + # + # GET /admin/posts + # GET /admin/posts/new + # POST /admin/posts + # GET /admin/posts/1 + # GET /admin/posts/1/edit + # PUT /admin/posts/1 + # DELETE /admin/posts/1 module Scoping - def initialize(*args) #:nodoc: - @scope = {} - super - end - + # Scopes a set of routes to the given default options. + # + # Take the following route definition as an example: + # + # scope :path => ":account_id", :as => "account" do + # resources :projects + # end + # + # This generates helpers such as +account_projects_path+, just like +resources+ does. + # The difference here being that the routes generated are like /rails/projects/2, + # rather than /accounts/rails/projects/2. + # + # === Options + # + # Takes same options as Base#match and Resources#resources. + # + # === Examples + # + # # route /posts (without the prefix /admin) to Admin::PostsController + # scope :module => "admin" do + # resources :posts + # end + # + # # prefix the posts resource's requests with '/admin' + # scope :path => "/admin" do + # resources :posts + # end + # + # # prefix the routing helper name: sekret_posts_path instead of posts_path + # scope :as => "sekret" do + # resources :posts + # end def scope(*args) options = args.extract_options! options = options.dup - if name_prefix = options.delete(:name_prefix) - options[:as] ||= name_prefix - ActiveSupport::Deprecation.warn ":name_prefix was deprecated in the new router syntax. Use :as instead.", caller - end - - case args.first - when String - options[:path] = args.first - when Symbol - options[:controller] = args.first - end - + options[:path] = args.first if args.first.is_a?(String) recover = {} options[:constraints] ||= {} @@ -365,10 +630,57 @@ module ActionDispatch @scope[:blocks] = recover[:block] end - def controller(controller) - scope(controller.to_sym) { yield } + # Scopes routes to a specific controller + # + # Example: + # controller "food" do + # match "bacon", :action => "bacon" + # end + def controller(controller, options={}) + options[:controller] = controller + scope(options) { yield } end + # Scopes routes to a specific namespace. For example: + # + # namespace :admin do + # resources :posts + # end + # + # This generates the following routes: + # + # admin_posts GET /admin/posts(.:format) {:action=>"index", :controller=>"admin/posts"} + # admin_posts POST /admin/posts(.:format) {:action=>"create", :controller=>"admin/posts"} + # new_admin_post GET /admin/posts/new(.:format) {:action=>"new", :controller=>"admin/posts"} + # edit_admin_post GET /admin/posts/:id/edit(.:format) {:action=>"edit", :controller=>"admin/posts"} + # admin_post GET /admin/posts/:id(.:format) {:action=>"show", :controller=>"admin/posts"} + # admin_post PUT /admin/posts/:id(.:format) {:action=>"update", :controller=>"admin/posts"} + # admin_post DELETE /admin/posts/:id(.:format) {:action=>"destroy", :controller=>"admin/posts"} + # + # === Options + # + # The +:path+, +:as+, +:module+, +:shallow_path+ and +:shallow_prefix+ + # options all default to the name of the namespace. + # + # For options, see Base#match. For +:shallow_path+ option, see + # Resources#resources. + # + # === Examples + # + # # accessible through /sekret/posts rather than /admin/posts + # namespace :admin, :path => "sekret" do + # resources :posts + # end + # + # # maps to Sekret::PostsController rather than Admin::PostsController + # namespace :admin, :module => "sekret" do + # resources :posts + # end + # + # # generates sekret_posts_path rather than admin_posts_path + # namespace :admin, :as => "sekret" do + # resources :posts + # end def namespace(path, options = {}) path = path.to_s options = { :path => path, :as => path, :module => path, @@ -376,95 +688,178 @@ module ActionDispatch scope(options) { yield } end + # === Parameter Restriction + # Allows you to constrain the nested routes based on a set of rules. + # For instance, in order to change the routes to allow for a dot character in the +id+ parameter: + # + # constraints(:id => /\d+\.\d+) do + # resources :posts + # end + # + # Now routes such as +/posts/1+ will no longer be valid, but +/posts/1.1+ will be. + # The +id+ parameter must match the constraint passed in for this example. + # + # You may use this to also restrict other parameters: + # + # resources :posts do + # constraints(:post_id => /\d+\.\d+) do + # resources :comments + # end + # + # === Restricting based on IP + # + # Routes can also be constrained to an IP or a certain range of IP addresses: + # + # constraints(:ip => /192.168.\d+.\d+/) do + # resources :posts + # end + # + # Any user connecting from the 192.168.* range will be able to see this resource, + # where as any user connecting outside of this range will be told there is no such route. + # + # === Dynamic request matching + # + # Requests to routes can be constrained based on specific criteria: + # + # constraints(lambda { |req| req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do + # resources :iphones + # end + # + # You are able to move this logic out into a class if it is too complex for routes. + # This class must have a +matches?+ method defined on it which either returns +true+ + # if the user should be given access to that route, or +false+ if the user should not. + # + # class Iphone + # def self.matches(request) + # request.env["HTTP_USER_AGENT"] =~ /iPhone/ + # end + # end + # + # An expected place for this code would be +lib/constraints+. + # + # This class is then used like this: + # + # constraints(Iphone) do + # resources :iphones + # end def constraints(constraints = {}) scope(:constraints => constraints) { yield } end + # Allows you to set default parameters for a route, such as this: + # defaults :id => 'home' do + # match 'scoped_pages/(:id)', :to => 'pages#show' + # end + # Using this, the +:id+ parameter here will default to 'home'. def defaults(defaults = {}) scope(:defaults => defaults) { yield } end - def match(*args) - options = args.extract_options! - - options = (@scope[:options] || {}).merge(options) - - if @scope[:as] && !options[:as].blank? - options[:as] = "#{@scope[:as]}_#{options[:as]}" - elsif @scope[:as] && options[:as] == "" - options[:as] = @scope[:as].to_s - end - - args.push(options) - super(*args) - end - private - def scope_options + def scope_options #:nodoc: @scope_options ||= private_methods.grep(/^merge_(.+)_scope$/) { $1.to_sym } end - def merge_path_scope(parent, child) + def merge_path_scope(parent, child) #:nodoc: Mapper.normalize_path("#{parent}/#{child}") end - def merge_shallow_path_scope(parent, child) + def merge_shallow_path_scope(parent, child) #:nodoc: Mapper.normalize_path("#{parent}/#{child}") end - def merge_as_scope(parent, child) + def merge_as_scope(parent, child) #:nodoc: parent ? "#{parent}_#{child}" : child end - def merge_shallow_prefix_scope(parent, child) + def merge_shallow_prefix_scope(parent, child) #:nodoc: parent ? "#{parent}_#{child}" : child end - def merge_module_scope(parent, child) + def merge_module_scope(parent, child) #:nodoc: parent ? "#{parent}/#{child}" : child end - def merge_controller_scope(parent, child) + def merge_controller_scope(parent, child) #:nodoc: child end - def merge_path_names_scope(parent, child) + def merge_path_names_scope(parent, child) #:nodoc: merge_options_scope(parent, child) end - def merge_constraints_scope(parent, child) + def merge_constraints_scope(parent, child) #:nodoc: merge_options_scope(parent, child) end - def merge_defaults_scope(parent, child) + def merge_defaults_scope(parent, child) #:nodoc: merge_options_scope(parent, child) end - def merge_blocks_scope(parent, child) + def merge_blocks_scope(parent, child) #:nodoc: merged = parent ? parent.dup : [] merged << child if child merged end - def merge_options_scope(parent, child) + def merge_options_scope(parent, child) #:nodoc: (parent || {}).except(*override_keys(child)).merge(child) end - def merge_shallow_scope(parent, child) + def merge_shallow_scope(parent, child) #:nodoc: child ? true : false end - def override_keys(child) + def override_keys(child) #:nodoc: child.key?(:only) || child.key?(:except) ? [:only, :except] : [] end end + # Resource routing allows you to quickly declare all of the common routes + # for a given resourceful controller. Instead of declaring separate routes + # for your +index+, +show+, +new+, +edit+, +create+, +update+ and +destroy+ + # actions, a resourceful route declares them in a single line of code: + # + # resources :photos + # + # Sometimes, you have a resource that clients always look up without + # referencing an ID. A common example, /profile always shows the profile of + # the currently logged in user. In this case, you can use a singular resource + # to map /profile (rather than /profile/:id) to the show action. + # + # resource :profile + # + # It's common to have resources that are logically children of other + # resources: + # + # resources :magazines do + # resources :ads + # end + # + # You may wish to organize groups of controllers under a namespace. Most + # commonly, you might group a number of administrative controllers under + # an +admin+ namespace. You would place these controllers under the + # app/controllers/admin directory, and you can group them together in your + # router: + # + # namespace "admin" do + # resources :posts, :comments + # end + # + # By default the :id parameter doesn't accept dots. If you need to + # use dots as part of the :id parameter add a constraint which + # overrides this restriction, e.g: + # + # resources :articles, :id => /[^\/]+/ + # + # This allows any character other than a slash as part of your :id. + # module Resources # CANONICAL_ACTIONS holds all actions that does not need a prefix or # a path appended since they fit properly in their scope level. - VALID_ON_OPTIONS = [:new, :collection, :member] - CANONICAL_ACTIONS = [:index, :create, :new, :show, :update, :destroy] - RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except] + VALID_ON_OPTIONS = [:new, :collection, :member] + RESOURCE_OPTIONS = [:as, :controller, :path, :only, :except] + CANONICAL_ACTIONS = %w(index create new show update destroy) class Resource #:nodoc: DEFAULT_ACTIONS = [:index, :create, :new, :show, :update, :destroy, :edit] @@ -473,7 +868,7 @@ module ActionDispatch def initialize(entities, options = {}) @name = entities.to_s - @path = options.delete(:path) || @name + @path = (options.delete(:path) || @name).to_s @controller = (options.delete(:controller) || @name).to_s @as = options.delete(:as) @options = options @@ -498,18 +893,17 @@ module ActionDispatch end def plural - name.to_s.pluralize + @plural ||= name.to_s end def singular - name.to_s.singularize + @singular ||= name.to_s.singularize end - def member_name - singular - end + alias :member_name :singular - # Checks for uncountable plurals, and appends "_index" if they're. + # Checks for uncountable plurals, and appends "_index" if the plural + # and singular form are the same. def collection_name singular == plural ? "#{plural}_index" : plural end @@ -518,9 +912,7 @@ module ActionDispatch { :controller => controller } end - def collection_scope - path - end + alias :collection_scope :path def member_scope "#{path}/:id" @@ -540,33 +932,54 @@ module ActionDispatch DEFAULT_ACTIONS = [:show, :create, :update, :destroy, :new, :edit] def initialize(entities, options) + @as = nil @name = entities.to_s - @path = options.delete(:path) || @name + @path = (options.delete(:path) || @name).to_s @controller = (options.delete(:controller) || plural).to_s @as = options.delete(:as) @options = options end - def member_name - name + def plural + @plural ||= name.to_s.pluralize end - alias :collection_name :member_name - def member_scope - path + def singular + @singular ||= name.to_s end - alias :nested_scope :member_scope - end - def initialize(*args) #:nodoc: - super - @scope[:path_names] = @set.resources_path_names + alias :member_name :singular + alias :collection_name :singular + + alias :member_scope :path + alias :nested_scope :path end def resources_path_names(options) @scope[:path_names].merge!(options) end + # Sometimes, you have a resource that clients always look up without + # referencing an ID. A common example, /profile always shows the + # profile of the currently logged in user. In this case, you can use + # a singular resource to map /profile (rather than /profile/:id) to + # the show action: + # + # resource :geocoder + # + # creates six different routes in your application, all mapping to + # the GeoCoders controller (note that the controller is named after + # the plural): + # + # GET /geocoder/new + # POST /geocoder + # GET /geocoder + # GET /geocoder/edit + # PUT /geocoder + # DELETE /geocoder + # + # === Options + # Takes same options as +resources+. def resource(*resources, &block) options = resources.extract_options! @@ -577,25 +990,121 @@ module ActionDispatch resource_scope(SingletonResource.new(resources.pop, options)) do yield if block_given? - collection_scope do + collection do post :create end if parent_resource.actions.include?(:create) - new_scope do + new do get :new end if parent_resource.actions.include?(:new) - member_scope do + member do + get :edit if parent_resource.actions.include?(:edit) get :show if parent_resource.actions.include?(:show) put :update if parent_resource.actions.include?(:update) delete :destroy if parent_resource.actions.include?(:destroy) - get :edit if parent_resource.actions.include?(:edit) end end self end + # In Rails, a resourceful route provides a mapping between HTTP verbs + # and URLs and controller actions. By convention, each action also maps + # to particular CRUD operations in a database. A single entry in the + # routing file, such as + # + # resources :photos + # + # creates seven different routes in your application, all mapping to + # the Photos controller: + # + # GET /photos/new + # POST /photos + # GET /photos/:id + # GET /photos/:id/edit + # PUT /photos/:id + # DELETE /photos/:id + # + # Resources can also be nested infinitely by using this block syntax: + # + # resources :photos do + # resources :comments + # end + # + # This generates the following comments routes: + # + # GET /photos/:id/comments/new + # POST /photos/:id/comments + # GET /photos/:id/comments/:id + # GET /photos/:id/comments/:id/edit + # PUT /photos/:id/comments/:id + # DELETE /photos/:id/comments/:id + # + # === Options + # Takes same options as Base#match as well as: + # + # [:path_names] + # Allows you to change the paths of the seven default actions. + # Paths not specified are not changed. + # + # resources :posts, :path_names => { :new => "brand_new" } + # + # The above example will now change /posts/new to /posts/brand_new + # + # [:only] + # Only generate routes for the given actions. + # + # resources :cows, :only => :show + # resources :cows, :only => [:show, :index] + # + # [:except] + # Generate all routes except for the given actions. + # + # resources :cows, :except => :show + # resources :cows, :except => [:show, :index] + # + # [:shallow] + # Generates shallow routes for nested resource(s). When placed on a parent resource, + # generates shallow routes for all nested resources. + # + # resources :posts, :shallow => true do + # resources :comments + # end + # + # Is the same as: + # + # resources :posts do + # resources :comments + # end + # resources :comments + # + # [:shallow_path] + # Prefixes nested shallow routes with the specified path. + # + # scope :shallow_path => "sekret" do + # resources :posts do + # resources :comments, :shallow => true + # end + # end + # + # The +comments+ resource here will have the following routes generated for it: + # + # post_comments GET /sekret/posts/:post_id/comments(.:format) + # post_comments POST /sekret/posts/:post_id/comments(.:format) + # new_post_comment GET /sekret/posts/:post_id/comments/new(.:format) + # edit_comment GET /sekret/comments/:id/edit(.:format) + # comment GET /sekret/comments/:id(.:format) + # comment PUT /sekret/comments/:id(.:format) + # comment DELETE /sekret/comments/:id(.:format) + # + # === Examples + # + # # routes call Admin::PostsController + # resources :posts, :module => "admin" + # + # # resource actions are at /admin/posts. + # resources :posts, :path => "admin" def resources(*resources, &block) options = resources.extract_options! @@ -606,43 +1115,70 @@ module ActionDispatch resource_scope(Resource.new(resources.pop, options)) do yield if block_given? - collection_scope do + collection do get :index if parent_resource.actions.include?(:index) post :create if parent_resource.actions.include?(:create) end - new_scope do + new do get :new end if parent_resource.actions.include?(:new) - member_scope do + member do + get :edit if parent_resource.actions.include?(:edit) get :show if parent_resource.actions.include?(:show) put :update if parent_resource.actions.include?(:update) delete :destroy if parent_resource.actions.include?(:destroy) - get :edit if parent_resource.actions.include?(:edit) end end self end + # To add a route to the collection: + # + # resources :photos do + # collection do + # get 'search' + # end + # end + # + # This will enable Rails to recognize paths such as /photos/search + # with GET, and route to the search action of PhotosController. It will also + # create the search_photos_url and search_photos_path + # route helpers. def collection - unless @scope[:scope_level] == :resources - raise ArgumentError, "can't use collection outside resources scope" + unless resource_scope? + raise ArgumentError, "can't use collection outside resource(s) scope" end - collection_scope do - yield + with_scope_level(:collection) do + scope(parent_resource.collection_scope) do + yield + end end end + # To add a member route, add a member block into the resource block: + # + # resources :photos do + # member do + # get 'preview' + # end + # end + # + # This will recognize /photos/1/preview with GET, and route to the + # preview action of PhotosController. It will also create the + # preview_photo_url and preview_photo_path helpers. def member unless resource_scope? raise ArgumentError, "can't use member outside resource(s) scope" end - member_scope do - yield + with_scope_level(:member) do + scope(parent_resource.member_scope) do + yield + end end end @@ -651,8 +1187,10 @@ module ActionDispatch raise ArgumentError, "can't use new outside resource(s) scope" end - new_scope do - yield + with_scope_level(:new) do + scope(parent_resource.new_scope(action_path(:new))) do + yield + end end end @@ -678,6 +1216,7 @@ module ActionDispatch end end + # See ActionDispatch::Routing::Mapper::Scoping#namespace def namespace(path, options = {}) if resource_scope? nested { super } @@ -687,7 +1226,7 @@ module ActionDispatch end def shallow - scope(:shallow => true) do + scope(:shallow => true, :shallow_path => @scope[:path]) do yield end end @@ -713,46 +1252,36 @@ module ActionDispatch raise ArgumentError, "Unknown scope #{on.inspect} given to :on" end - if @scope[:scope_level] == :resource + if @scope[:scope_level] == :resources + args.push(options) + return nested { match(*args) } + elsif @scope[:scope_level] == :resource args.push(options) return member { match(*args) } end - path = options.delete(:path) action = args.first + path = path_for_action(action, options.delete(:path)) - if action.is_a?(Symbol) - path = path_for_action(action, path) - options[:to] ||= action - options[:as] = name_for_action(action, options[:as]) - - with_exclusive_scope do - return super(path, options) - end - elsif resource_method_scope? - path = path_for_custom_action - options[:as] = name_for_action(options[:as]) if options[:as] - args.push(options) - - with_exclusive_scope do - scope(path) do - return super - end - end + if action.to_s =~ /^[\w\/]+$/ + options[:action] ||= action unless action.to_s.include?("/") + else + action = nil end - if resource_scope? - raise ArgumentError, "can't define route directly in resource(s) scope" + if options.key?(:as) && !options[:as] + options.delete(:as) + else + options[:as] = name_for_action(options[:as], action) end - args.push(options) - super + super(path, options) end def root(options={}) if @scope[:scope_level] == :resources - with_scope_level(:nested) do - scope(parent_resource.path, :as => parent_resource.collection_name) do + with_scope_level(:root) do + scope(parent_resource.path) do super(options) end end @@ -767,12 +1296,21 @@ module ActionDispatch @scope[:scope_level_resource] end - def apply_common_behavior_for(method, resources, options, &block) + def apply_common_behavior_for(method, resources, options, &block) #:nodoc: if resources.length > 1 resources.each { |r| send(method, r, options, &block) } return true end + if resource_scope? + nested { send(method, resources.pop, options, &block) } + return true + end + + options.keys.each do |k| + (options[:constraints] ||= {})[k] = options.delete(k) if options[k].is_a?(Regexp) + end + scope_options = options.slice!(*RESOURCE_OPTIONS) unless scope_options.empty? scope(scope_options) do @@ -785,33 +1323,26 @@ module ActionDispatch options.merge!(scope_action_options) if scope_action_options? end - if resource_scope? - nested do - send(method, resources.pop, options, &block) - end - return true - end - false end - def action_options?(options) + def action_options?(options) #:nodoc: options[:only] || options[:except] end - def scope_action_options? + def scope_action_options? #:nodoc: @scope[:options].is_a?(Hash) && (@scope[:options][:only] || @scope[:options][:except]) end - def scope_action_options + def scope_action_options #:nodoc: @scope[:options].slice(:only, :except) end - def resource_scope? + def resource_scope? #:nodoc: [:resource, :resources].include?(@scope[:scope_level]) end - def resource_method_scope? + def resource_method_scope? #:nodoc: [:collection, :member, :new].include?(@scope[:scope_level]) end @@ -837,7 +1368,7 @@ module ActionDispatch @scope[:scope_level_resource] = old_resource end - def resource_scope(resource) + def resource_scope(resource) #:nodoc: with_scope_level(resource.is_a?(SingletonResource) ? :resource : :resources, resource) do scope(parent_resource.resource_scope) do yield @@ -845,116 +1376,108 @@ module ActionDispatch end end - def new_scope - with_scope_level(:new) do - scope(parent_resource.new_scope(action_path(:new))) do - yield - end - end - end - - def collection_scope - with_scope_level(:collection) do - scope(parent_resource.collection_scope) do - yield - end - end - end - - def member_scope - with_scope_level(:member) do - scope(parent_resource.member_scope) do - yield - end - end - end - - def nested_options + def nested_options #:nodoc: {}.tap do |options| options[:as] = parent_resource.member_name options[:constraints] = { "#{parent_resource.singular}_id".to_sym => id_constraint } if id_constraint? end end - def id_constraint? - @scope[:id].is_a?(Regexp) || (@scope[:constraints] && @scope[:constraints][:id].is_a?(Regexp)) + def id_constraint? #:nodoc: + @scope[:constraints] && @scope[:constraints][:id].is_a?(Regexp) end - def id_constraint - @scope[:id] || @scope[:constraints][:id] + def id_constraint #:nodoc: + @scope[:constraints][:id] end - def canonical_action?(action, flag) - flag && CANONICAL_ACTIONS.include?(action) + def canonical_action?(action, flag) #:nodoc: + flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s) end - def shallow_scoping? + def shallow_scoping? #:nodoc: shallow? && @scope[:scope_level] == :member end - def path_for_action(action, path) + def path_for_action(action, path) #:nodoc: prefix = shallow_scoping? ? "#{@scope[:shallow_path]}/#{parent_resource.path}/:id" : @scope[:path] - if canonical_action?(action, path.blank?) - "#{prefix}(.:format)" + path = if canonical_action?(action, path.blank?) + prefix.to_s else - "#{prefix}/#{action_path(action, path)}(.:format)" + "#{prefix}/#{action_path(action, path)}" end end - def path_for_custom_action - if shallow_scoping? - "#{@scope[:shallow_path]}/#{parent_resource.path}/:id" - else - @scope[:path] - end - end - - def action_path(name, path = nil) + def action_path(name, path = nil) #:nodoc: path || @scope[:path_names][name.to_sym] || name.to_s end - def prefix_name_for_action(action, as) - if as.present? - "#{as}_" - elsif as - "" + def prefix_name_for_action(as, action) #:nodoc: + if as + as.to_s elsif !canonical_action?(action, @scope[:scope_level]) - "#{action}_" + action.to_s end end - def name_for_action(action, as=nil) - prefix = prefix_name_for_action(action, as) + def name_for_action(as, action) #:nodoc: + prefix = prefix_name_for_action(as, action) + prefix = Mapper.normalize_name(prefix) if prefix name_prefix = @scope[:as] if parent_resource + return nil if as.nil? && action.nil? + collection_name = parent_resource.collection_name member_name = parent_resource.member_name - name_prefix = "#{name_prefix}_" if name_prefix.present? end - case @scope[:scope_level] + name = case @scope[:scope_level] + when :nested + [name_prefix, prefix] when :collection - "#{prefix}#{name_prefix}#{collection_name}" + [prefix, name_prefix, collection_name] when :new - "#{prefix}new_#{name_prefix}#{member_name}" + [prefix, :new, name_prefix, member_name] + when :member + [prefix, shallow_scoping? ? @scope[:shallow_prefix] : name_prefix, member_name] + when :root + [name_prefix, collection_name, prefix] else - if shallow_scoping? - shallow_prefix = "#{@scope[:shallow_prefix]}_" if @scope[:shallow_prefix].present? - "#{prefix}#{shallow_prefix}#{member_name}" - else - "#{prefix}#{name_prefix}#{member_name}" - end + [name_prefix, member_name, prefix] end + + candidate = name.select(&:present?).join("_").presence + candidate unless as.nil? && @set.routes.find { |r| r.name == candidate } end end + module Shorthand #:nodoc: + def match(*args) + if args.size == 1 && args.last.is_a?(Hash) + options = args.pop + path, to = options.find { |name, value| name.is_a?(String) } + options.merge!(:to => to).delete(path) + super(path, options) + else + super + end + end + end + + def initialize(set) #:nodoc: + @set = set + @scope = { :path_names => @set.resources_path_names } + end + include Base include HttpHelpers + include Redirection include Scoping include Resources + include Shorthand end end end diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index 31dba835ac..82c4fadb50 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -17,7 +17,7 @@ module ActionDispatch # # == Usage within the framework # - # Polymorphic URL helpers are used in a number of places throughout the Rails framework: + # Polymorphic URL helpers are used in a number of places throughout the \Rails framework: # # * url_for, so you can use it with a record as the argument, e.g. # url_for(@article); @@ -42,6 +42,18 @@ module ActionDispatch # # edit_polymorphic_path(@post) # => "/posts/1/edit" # polymorphic_path(@post, :format => :pdf) # => "/posts/1.pdf" + # + # == Using with mounted engines + # + # If you use mounted engine, there is a possibility that you will need to use + # polymorphic_url pointing at engine's routes. To do that, just pass proxy used + # to reach engine's routes as a first argument: + # + # For example: + # + # polymorphic_url([blog, @post]) # it will call blog.post_path(@post) + # form_for([blog, @post]) # => "/blog/posts/1 + # module PolymorphicRoutes # Constructs a call to a named RESTful route for the given record and returns the # resulting URL string. For example: @@ -78,19 +90,20 @@ module ActionDispatch def polymorphic_url(record_or_hash_or_array, options = {}) if record_or_hash_or_array.kind_of?(Array) record_or_hash_or_array = record_or_hash_or_array.compact + if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy) + proxy = record_or_hash_or_array.shift + end record_or_hash_or_array = record_or_hash_or_array[0] if record_or_hash_or_array.size == 1 end record = extract_record(record_or_hash_or_array) record = record.to_model if record.respond_to?(:to_model) - args = case record_or_hash_or_array - when Hash; [ record_or_hash_or_array ] - when Array; record_or_hash_or_array.dup - else [ record_or_hash_or_array ] - end + args = Array === record_or_hash_or_array ? + record_or_hash_or_array.dup : + [ record_or_hash_or_array ] - inflection = if options[:action].to_s == "new" + inflection = if options[:action] && options[:action].to_s == "new" args.pop :singular elsif (record.respond_to?(:persisted?) && !record.persisted?) @@ -111,7 +124,14 @@ module ActionDispatch args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options end - send(named_route, *args) + if proxy + proxy.send(named_route, *args) + else + # we need to use url_for, because polymorphic_url can be used in context of other than + # current routes (e.g. engine's routes). As named routes from engine are not included + # calling engine's named route directly would fail. + url_for _routes.url_helpers.__send__("hash_for_#{named_route}", *args) + end end # Returns the path component of a URL for the given record. It uses @@ -146,31 +166,31 @@ module ActionDispatch end def build_named_route_call(records, inflection, options = {}) - unless records.is_a?(Array) - record = extract_record(records) - route = '' - else + if records.is_a?(Array) record = records.pop - route = records.inject("") do |string, parent| + route = records.map do |parent| if parent.is_a?(Symbol) || parent.is_a?(String) - string << "#{parent}_" + parent else - string << ActiveModel::Naming.plural(parent).singularize - string << "_" + ActiveModel::Naming.route_key(parent).singularize end end + else + record = extract_record(records) + route = [] end if record.is_a?(Symbol) || record.is_a?(String) - route << "#{record}_" + route << record else - route << ActiveModel::Naming.plural(record) - route = route.singularize if inflection == :singular - route << "_" - route << "index_" if ActiveModel::Naming.uncountable?(record) && inflection == :plural + route << ActiveModel::Naming.route_key(record) + route = [route.join("_").singularize] if inflection == :singular + route << "index" if ActiveModel::Naming.uncountable?(record) && inflection == :plural end - action_prefix(options) + route + routing_type(options).to_s + route << routing_type(options) + + action_prefix(options) + route.join("_") end def extract_record(record_or_hash_or_array) diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb new file mode 100644 index 0000000000..804991ad5f --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/redirection.rb @@ -0,0 +1,110 @@ +require 'action_dispatch/http/request' + +module ActionDispatch + module Routing + module Redirection + + # Redirect any path to another path: + # + # match "/stories" => redirect("/posts") + # + # You can also use interpolation in the supplied redirect argument: + # + # match 'docs/:article', :to => redirect('/wiki/%{article}') + # + # Alternatively you can use one of the other syntaxes: + # + # The block version of redirect allows for the easy encapsulation of any logic associated with + # the redirect in question. Either the params and request are supplied as arguments, or just + # params, depending of how many arguments your block accepts. A string is required as a + # return value. + # + # match 'jokes/:number', :to => redirect do |params, request| + # path = (params[:number].to_i.even? ? "/wheres-the-beef" : "/i-love-lamp") + # "http://#{request.host_with_port}/#{path}" + # end + # + # The options version of redirect allows you to supply only the parts of the url which need + # to change, it also supports interpolation of the path similar to the first example. + # + # match 'stores/:name', :to => redirect(:subdomain => 'stores', :path => '/%{name}') + # match 'stores/:name(*all)', :to => redirect(:subdomain => 'stores', :path => '/%{name}%{all}') + # + # Finally, an object which responds to call can be supplied to redirect, allowing you to reuse + # common redirect routes. The call method must accept two arguments, params and request, and return + # a string. + # + # match 'accounts/:name' => redirect(SubdomainRedirector.new('api')) + # + def redirect(*args, &block) + options = args.last.is_a?(Hash) ? args.pop : {} + status = options.delete(:status) || 301 + + path = args.shift + + path_proc = if path.is_a?(String) + proc { |params| (params.empty? || !path.match(/%\{\w*\}/)) ? path : (path % params) } + elsif options.any? + options_proc(options) + elsif path.respond_to?(:call) + proc { |params, request| path.call(params, request) } + elsif block + block + else + raise ArgumentError, "redirection argument not supported" + end + + redirection_proc(status, path_proc) + end + + private + + def options_proc(options) + proc do |params, request| + path = if options[:path].nil? + request.path + elsif params.empty? || !options[:path].match(/%\{\w*\}/) + options.delete(:path) + else + (options.delete(:path) % params) + end + + default_options = { + :protocol => request.protocol, + :host => request.host, + :port => request.optional_port, + :path => path, + :params => request.query_parameters + } + + ActionDispatch::Http::URL.url_for(options.reverse_merge(default_options)) + end + end + + def redirection_proc(status, path_proc) + lambda do |env| + req = Request.new(env) + + params = [req.symbolized_path_parameters] + params << req if path_proc.arity > 1 + + uri = URI.parse(path_proc.call(*params)) + uri.scheme ||= req.scheme + uri.host ||= req.host + uri.port ||= req.port unless req.standard_port? + + body = %(You are being redirected.) + + headers = { + 'Location' => uri.to_s, + 'Content-Type' => 'text/html', + 'Content-Length' => body.length.to_s + } + + [ status, headers, [body] ] + end + end + + end + end +end \ No newline at end of file diff --git a/actionpack/lib/action_dispatch/routing/route.rb b/actionpack/lib/action_dispatch/routing/route.rb index aefebf8f80..a049510182 100644 --- a/actionpack/lib/action_dispatch/routing/route.rb +++ b/actionpack/lib/action_dispatch/routing/route.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/module/deprecation' + module ActionDispatch module Routing class Route #:nodoc: @@ -10,6 +12,8 @@ module ActionDispatch @defaults = defaults @name = name + # FIXME: we should not be doing this much work in a constructor. + @requirements = requirements.merge(defaults) @requirements.delete(:controller) if @requirements[:controller].is_a?(Regexp) @requirements.delete_if { |k, v| @@ -21,24 +25,22 @@ module ActionDispatch conditions[:path_info] = ::Rack::Mount::Strexp.compile(path, requirements, SEPARATORS, anchor) end - @conditions = conditions.inject({}) { |h, (k, v)| - h[k] = Rack::Mount::RegexpWithNamedGroups.new(v) - h - } + @verbs = conditions[:request_method] || [] + @conditions = conditions.dup + + # Rack-Mount requires that :request_method be a regular expression. + # :request_method represents the HTTP verb that matches this route. + # + # Here we munge values before they get sent on to rack-mount. + @conditions[:request_method] = %r[^#{verb}$] unless @verbs.empty? + @conditions[:path_info] = Rack::Mount::RegexpWithNamedGroups.new(@conditions[:path_info]) if @conditions[:path_info] @conditions.delete_if{ |k,v| k != :path_info && !valid_condition?(k) } @requirements.delete_if{ |k,v| !valid_condition?(k) } end def verb - if method = conditions[:request_method] - case method - when Regexp - method.source.upcase - else - method.to_s.upcase - end - end + @verbs.join '|' end def segment_keys @@ -48,6 +50,7 @@ module ActionDispatch def to_a [@app, @conditions, @defaults, @name] end + deprecate :to_a def to_s @to_s ||= begin diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index d23b580d97..b28f6c2297 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -1,7 +1,8 @@ require 'rack/mount' require 'forwardable' +require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/to_query' -require 'action_dispatch/routing/deprecated_mapper' +require 'active_support/core_ext/hash/slice' module ActionDispatch module Routing @@ -50,12 +51,13 @@ module ActionDispatch private def controller_reference(controller_param) + controller_name = "#{controller_param.camelize}Controller" + unless controller = @controllers[controller_param] - controller_name = "#{controller_param.camelize}Controller" controller = @controllers[controller_param] = - ActiveSupport::Dependencies.ref(controller_name) + ActiveSupport::Dependencies.reference(controller_name) end - controller.get + controller.get(controller_name) end def dispatch(controller, action, env) @@ -67,7 +69,7 @@ module ActionDispatch end def split_glob_param!(params) - params[@glob_param] = params[@glob_param].split('/').map { |v| URI.unescape(v) } + params[@glob_param] = params[@glob_param].split('/').map { |v| URI.parser.unescape(v) } end end @@ -158,10 +160,18 @@ module ActionDispatch # We use module_eval to avoid leaks @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1 - def #{selector}(options = nil) # def hash_for_users_url(options = nil) - options ? #{options.inspect}.merge(options) : #{options.inspect} # options ? {:only_path=>false}.merge(options) : {:only_path=>false} - end # end - protected :#{selector} # protected :hash_for_users_url + remove_method :#{selector} if method_defined?(:#{selector}) + def #{selector}(*args) + options = args.extract_options! + + if args.any? + options[:_positional_args] = args + options[:_positional_keys] = #{route.segment_keys.inspect} + end + + options ? #{options.inspect}.merge(options) : #{options.inspect} + end + protected :#{selector} END_EVAL helpers << selector end @@ -184,22 +194,16 @@ module ActionDispatch hash_access_method = hash_access_name(name, kind) @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1 + remove_method :#{selector} if method_defined?(:#{selector}) def #{selector}(*args) - options = #{hash_access_method}(args.extract_options!) - - if args.any? - options[:_positional_args] = args - options[:_positional_keys] = #{route.segment_keys.inspect} - end - - url_for(options) + url_for(#{hash_access_method}(*args)) end END_EVAL helpers << selector end end - attr_accessor :set, :routes, :named_routes + attr_accessor :set, :routes, :named_routes, :default_scope attr_accessor :disable_clear_and_finalize, :resources_path_names attr_accessor :default_url_options, :request_class, :valid_conditions @@ -211,7 +215,6 @@ module ActionDispatch self.routes = [] self.named_routes = NamedRouteCollection.new self.resources_path_names = self.class.default_resources_path_names.dup - self.controller_namespaces = Set.new self.default_url_options = {} self.request_class = request_class @@ -219,27 +222,35 @@ module ActionDispatch self.valid_conditions.delete(:id) self.valid_conditions.push(:controller, :action) + @append = [] @disable_clear_and_finalize = false clear! end def draw(&block) clear! unless @disable_clear_and_finalize - - mapper = Mapper.new(self) - if block.arity == 1 - mapper.instance_exec(DeprecatedMapper.new(self), &block) - else - mapper.instance_exec(&block) - end - + eval_block(block) finalize! unless @disable_clear_and_finalize nil end + def append(&block) + @append << block + end + + def eval_block(block) + mapper = Mapper.new(self) + if default_scope + mapper.with_default_scope(default_scope, &block) + else + mapper.instance_exec(&block) + end + end + def finalize! return if @finalized + @append.each { |blk| eval_block(blk) } @finalized = true @set.freeze end @@ -261,6 +272,31 @@ module ActionDispatch named_routes.install(destinations, regenerate_code) end + module MountedHelpers + end + + def mounted_helpers(name = :main_app) + define_mounted_helper(name) if name + MountedHelpers + end + + def define_mounted_helper(name) + return if MountedHelpers.method_defined?(name) + + routes = self + MountedHelpers.class_eval do + define_method "_#{name}" do + RoutesProxy.new(routes, self._routes_context) + end + end + + MountedHelpers.class_eval <<-RUBY + def #{name} + @#{name} ||= _#{name} + end + RUBY + end + def url_helpers @url_helpers ||= begin routes = self @@ -269,9 +305,9 @@ module ActionDispatch extend ActiveSupport::Concern include UrlFor - @routes = routes + @_routes = routes class << self - delegate :url_for, :to => '@routes' + delegate :url_for, :to => '@_routes' end extend routes.named_routes.module @@ -280,10 +316,10 @@ module ActionDispatch # Yes plz - JP included do routes.install_helpers(self) - singleton_class.send(:define_method, :_routes) { routes } + singleton_class.send(:redefine_method, :_routes) { routes } end - define_method(:_routes) { routes } + define_method(:_routes) { @_routes || routes } end helpers @@ -295,18 +331,31 @@ module ActionDispatch end def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true) + raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i) route = Route.new(self, app, conditions, requirements, defaults, name, anchor) - @set.add_route(*route) + @set.add_route(route.app, route.conditions, route.defaults, route.name) named_routes[name] = route if name routes << route route end class Generator #:nodoc: - attr_reader :options, :recall, :set, :script_name, :named_route + PARAMETERIZE = { + :parameterize => lambda do |name, value| + if name == :controller + value + elsif value.is_a?(Array) + value.map { |v| Rack::Mount::Utils.escape_uri(v.to_param) }.join('/') + else + return nil unless param = value.to_param + param.split('/').map { |v| Rack::Mount::Utils.escape_uri(v) }.join("/") + end + end + } + + attr_reader :options, :recall, :set, :named_route def initialize(options, recall, set, extras = false) - @script_name = options.delete(:script_name) @named_route = options.delete(:use_route) @options = options.dup @recall = recall.dup @@ -392,36 +441,19 @@ module ActionDispatch end def generate - path, params = @set.set.generate(:path_info, named_route, options, recall, opts) + path, params = @set.set.generate(:path_info, named_route, options, recall, PARAMETERIZE) raise_routing_error unless path - params.reject! {|k,v| !v } - return [path, params.keys] if @extras - path << "?#{params.to_query}" if params.any? - "#{script_name}#{path}" + [path, params] rescue Rack::Mount::RoutingError raise_routing_error end - def opts - parameterize = lambda do |name, value| - if name == :controller - value - elsif value.is_a?(Array) - value.map { |v| Rack::Mount::Utils.escape_uri(v.to_param) }.join('/') - else - return nil unless param = value.to_param - param.split('/').map { |v| Rack::Mount::Utils.escape_uri(v) }.join("/") - end - end - {:parameterize => parameterize} - end - def raise_routing_error - raise ActionController::RoutingError.new("No route matches #{options.inspect}") + raise ActionController::RoutingError, "No route matches #{options.inspect}" end def different_controller? @@ -453,7 +485,12 @@ module ActionDispatch Generator.new(options, recall, self, extras).generate end - RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :port, :trailing_slash] + RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length, + :trailing_slash, :anchor, :params, :only_path, :script_name] + + def _generate_prefix(options = {}) + nil + end def url_for(options) finalize! @@ -461,30 +498,24 @@ module ActionDispatch handle_positional_args(options) - rewritten_url = "" + user, password = extract_authentication(options) + path_segments = options.delete(:_path_segments) + script_name = options.delete(:script_name) - path_segments = options.delete(:_path_segments) - - unless options[:only_path] - rewritten_url << (options[:protocol] || "http") - rewritten_url << "://" unless rewritten_url.match("://") - rewritten_url << rewrite_authentication(options) - - raise "Missing host to link to! Please provide :host parameter or set default_url_options[:host]" unless options[:host] - - rewritten_url << options[:host] - rewritten_url << ":#{options.delete(:port)}" if options.key?(:port) - end + path = (script_name.blank? ? _generate_prefix(options) : script_name.chomp('/')).to_s path_options = options.except(*RESERVED_OPTIONS) path_options = yield(path_options) if block_given? - path = generate(path_options, path_segments || {}) - # ROUTES TODO: This can be called directly, so script_name should probably be set in the routes - rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path) - rewritten_url << "##{Rack::Mount::Utils.escape_uri(options[:anchor].to_param.to_s)}" if options[:anchor] + path_addition, params = generate(path_options, path_segments || {}) + path << path_addition - rewritten_url + ActionDispatch::Http::URL.url_for(options.merge({ + :path => path, + :params => params, + :user => user, + :password => password + })) end def call(env) @@ -494,7 +525,7 @@ module ActionDispatch def recognize_path(path, environment = {}) method = (environment[:method] || "GET").to_s.upcase - path = Rack::Mount::Utils.normalize_path(path) + path = Rack::Mount::Utils.normalize_path(path) unless path =~ %r{://} begin env = Rack::MockRequest.env_for(path, {:method => method}) @@ -502,17 +533,19 @@ module ActionDispatch raise ActionController::RoutingError, e.message end - req = Rack::Request.new(env) + req = @request_class.new(env) @set.recognize(req) do |route, matches, params| params.each do |key, value| if value.is_a?(String) value = value.dup.force_encoding(Encoding::BINARY) if value.encoding_aware? - params[key] = URI.unescape(value) + params[key] = URI.parser.unescape(value) end end dispatcher = route.app - dispatcher = dispatcher.app while dispatcher.is_a?(Mapper::Constraints) + while dispatcher.is_a?(Mapper::Constraints) && dispatcher.matches?(env) do + dispatcher = dispatcher.app + end if dispatcher.is_a?(Dispatcher) && dispatcher.controller(params, false) dispatcher.prepare_params!(params) @@ -524,28 +557,25 @@ module ActionDispatch end private + + def extract_authentication(options) + if options[:user] && options[:password] + [options.delete(:user), options.delete(:password)] + else + nil + end + end + def handle_positional_args(options) return unless args = options.delete(:_positional_args) keys = options.delete(:_positional_keys) keys -= options.keys if args.size < keys.size - 1 # take format into account - args = args.zip(keys).inject({}) do |h, (v, k)| - h[k] = v - h - end - # Tell url_for to skip default_url_options - options.merge!(args) + options.merge!(Hash[args.zip(keys).map { |v, k| [k, v] }]) end - def rewrite_authentication(options) - if options[:user] && options[:password] - "#{Rack::Utils.escape(options.delete(:user))}:#{Rack::Utils.escape(options.delete(:password))}@" - else - "" - end - end end end end diff --git a/actionpack/lib/action_dispatch/routing/routes_proxy.rb b/actionpack/lib/action_dispatch/routing/routes_proxy.rb new file mode 100644 index 0000000000..f7d5f6397d --- /dev/null +++ b/actionpack/lib/action_dispatch/routing/routes_proxy.rb @@ -0,0 +1,35 @@ +module ActionDispatch + module Routing + class RoutesProxy #:nodoc: + include ActionDispatch::Routing::UrlFor + + attr_accessor :scope, :routes + alias :_routes :routes + + def initialize(routes, scope) + @routes, @scope = routes, scope + end + + def url_options + scope.send(:_with_routes, routes) do + scope.url_options + end + end + + def method_missing(method, *args) + if routes.url_helpers.respond_to?(method) + self.class.class_eval <<-RUBY, __FILE__, __LINE__ + 1 + def #{method}(*args) + options = args.extract_options! + args << url_options.merge((options || {}).symbolize_keys) + routes.url_helpers.#{method}(*args) + end + RUBY + send(method, *args) + else + super + end + end + end + end +end diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index ba93ff8630..d4db78a25a 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -1,6 +1,6 @@ module ActionDispatch module Routing - # In routes.rb one defines URL-to-controller mappings, but the reverse + # In config/routes.rb you define URL-to-controller mappings, but the reverse # is also possible: an URL can be generated from one of your routing definitions. # URL generation functionality is centralized in this module. # @@ -12,15 +12,14 @@ module ActionDispatch # # == URL generation from parameters # - # As you may know, some functions - such as ActionController::Base#url_for + # As you may know, some functions, such as ActionController::Base#url_for # and ActionView::Helpers::UrlHelper#link_to, can generate URLs given a set # of parameters. For example, you've probably had the chance to write code # like this in one of your views: # # <%= link_to('Click here', :controller => 'users', # :action => 'new', :message => 'Welcome!') %> - # - # # Generates a link to /users/new?message=Welcome%21 + # # => "/users/new?message=Welcome%21" # # link_to, and all other functions that require URL generation functionality, # actually use ActionController::UrlFor under the hood. And in particular, @@ -61,7 +60,7 @@ module ActionDispatch # # UrlFor also allows one to access methods that have been auto-generated from # named routes. For example, suppose that you have a 'users' resource in your - # routes.rb: + # config/routes.rb: # # resources :users # @@ -99,6 +98,11 @@ module ActionDispatch end end + def initialize(*) + @_routes = nil + super + end + def url_options default_url_options end @@ -111,6 +115,13 @@ module ActionDispatch # * :host - Specifies the host the link should be targeted at. # If :only_path is false, this option must be # provided either explicitly, or via +default_url_options+. + # * :subdomain - Specifies the subdomain of the link, using the +tld_length+ + # to split the domain from the host. + # * :domain - Specifies the domain of the link, using the +tld_length+ + # to split the subdomain from the host. + # * :tld_length - Number of labels the TLD id composed of, only used if + # :subdomain or :domain are supplied. Defaults to + # ActionDispatch::Http::URL.tld_length, which in turn defaults to 1. # * :port - Optionally specify the port to connect to. # * :anchor - An anchor name to be appended to the path. # * :trailing_slash - If true, adds a trailing slash, as in "/archive/2009/" @@ -134,6 +145,18 @@ module ActionDispatch polymorphic_url(options) end end + + protected + def _with_routes(routes) + old_routes, @_routes = @_routes, routes + yield + ensure + @_routes = old_routes + end + + def _routes_context + self + end end end end diff --git a/actionpack/lib/action_dispatch/testing/assertions/dom.rb b/actionpack/lib/action_dispatch/testing/assertions/dom.rb index 9c215de743..47c84742aa 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/dom.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/dom.rb @@ -3,7 +3,7 @@ require 'action_controller/vendor/html-scanner' module ActionDispatch module Assertions module DomAssertions - # Test two HTML strings for equivalency (e.g., identical up to reordering of attributes) + # \Test two HTML strings for equivalency (e.g., identical up to reordering of attributes) # # ==== Examples # diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index ec5e9efe44..77a15f3e97 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -1,6 +1,6 @@ module ActionDispatch module Assertions - # A small suite of assertions that test responses from Rails applications. + # A small suite of assertions that test responses from \Rails applications. module ResponseAssertions extend ActiveSupport::Concern @@ -18,9 +18,9 @@ module ActionDispatch # * :missing - Status code was 404 # * :error - Status code was in the 500-599 range # - # You can also pass an explicit status number like assert_response(501) - # or its symbolic equivalent assert_response(:not_implemented). - # See ActionDispatch::StatusCodes for a full list. + # You can also pass an explicit status number like assert_response(501) + # or its symbolic equivalent assert_response(:not_implemented). + # See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list. # # ==== Examples # @@ -45,8 +45,8 @@ module ActionDispatch end # Assert that the redirection options passed in match those of the redirect called in the latest action. - # This match can be partial, such that assert_redirected_to(:controller => "weblog") will also - # match the redirection of redirect_to(:controller => "weblog", :action => "show") and so on. + # This match can be partial, such that assert_redirected_to(:controller => "weblog") will also + # match the redirection of redirect_to(:controller => "weblog", :action => "show") and so on. # # ==== Examples # @@ -81,14 +81,10 @@ module ActionDispatch def normalize_argument_to_redirection(fragment) case fragment - when %r{^\w[\w\d+.-]*:.*} + when %r{^\w[A-Za-z\d+.-]*:.*} fragment when String - if fragment =~ %r{^\w[\w\d+.-]*:.*} - fragment - else - @request.protocol + @request.host_with_port + fragment - end + @request.protocol + @request.host_with_port + fragment when :back raise RedirectBackError unless refer = @request.headers["Referer"] refer diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb index 9338fa9e70..11e8c63fa0 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb @@ -1,12 +1,13 @@ +require 'uri' require 'active_support/core_ext/hash/diff' require 'active_support/core_ext/hash/indifferent_access' module ActionDispatch module Assertions - # Suite of assertions to test routes generated by Rails and the handling of requests made to them. + # Suite of assertions to test routes generated by \Rails and the handling of requests made to them. module RoutingAssertions # Asserts that the routing of the given +path+ was handled correctly and that the parsed options (given in the +expected_options+ hash) - # match +path+. Basically, it asserts that Rails recognizes the route given by +expected_options+. + # match +path+. Basically, it asserts that \Rails recognizes the route given by +expected_options+. # # Pass a hash in the second argument (+path+) to specify the request method. This is useful for routes # requiring a specific HTTP method. The hash should contain a :path with the incoming request path @@ -36,18 +37,8 @@ module ActionDispatch # # # Test a custom route # assert_recognizes({:controller => 'items', :action => 'show', :id => '1'}, 'view/item1') - # - # # Check a Simply RESTful generated route - # assert_recognizes list_items_url, 'items/list' def assert_recognizes(expected_options, path, extras={}, message=nil) - if path.is_a? Hash - request_method = path[:method] - path = path[:path] - else - request_method = nil - end - - request = recognized_request_for(path, request_method) + request = recognized_request_for(path) expected_options = expected_options.clone extras.each_key { |key| expected_options.delete key } unless extras.nil? @@ -77,7 +68,16 @@ module ActionDispatch # # Asserts that the generated route gives us our custom route # assert_generates "changesets/12", { :controller => 'scm', :action => 'show_diff', :revision => "12" } def assert_generates(expected_path, options, defaults={}, extras = {}, message=nil) - expected_path = "/#{expected_path}" unless expected_path[0] == ?/ + if expected_path =~ %r{://} + begin + uri = URI.parse(expected_path) + expected_path = uri.path.to_s.empty? ? "/" : uri.path + rescue URI::InvalidURIError => e + raise ActionController::RoutingError, e.message + end + else + expected_path = "/#{expected_path}" unless expected_path.first == '/' + end # Load routes.rb if it hasn't been loaded. generated_path, extra_keys = @routes.generate_extras(options, defaults) @@ -121,7 +121,8 @@ module ActionDispatch options[:controller] = "/#{controller}" end - assert_generates(path.is_a?(Hash) ? path[:path] : path, options, defaults, extras, message) + generate_options = options.dup.delete_if{ |k,v| defaults.key?(k) } + assert_generates(path.is_a?(Hash) ? path[:path] : path, generate_options, defaults, extras, message) end # A helper to make it easier to test different route configurations. @@ -143,16 +144,16 @@ module ActionDispatch # def with_routing old_routes, @routes = @routes, ActionDispatch::Routing::RouteSet.new - old_controller, @controller = @controller, @controller.clone if @controller - _routes = @routes + if defined?(@controller) && @controller + old_controller, @controller = @controller, @controller.clone + _routes = @routes - # Unfortunately, there is currently an abstraction leak between AC::Base - # and AV::Base which requires having the URL helpers in both AC and AV. - # To do this safely at runtime for tests, we need to bump up the helper serial - # to that the old AV subclass isn't cached. - # - # TODO: Make this unnecessary - if @controller + # Unfortunately, there is currently an abstraction leak between AC::Base + # and AV::Base which requires having the URL helpers in both AC and AV. + # To do this safely at runtime for tests, we need to bump up the helper serial + # to that the old AV subclass isn't cached. + # + # TODO: Make this unnecessary @controller.singleton_class.send(:include, _routes.url_helpers) @controller.view_context_class = Class.new(@controller.view_context_class) do include _routes.url_helpers @@ -161,14 +162,14 @@ module ActionDispatch yield @routes ensure @routes = old_routes - if @controller + if defined?(@controller) && @controller @controller = old_controller end end # ROUTES TODO: These assertions should really work in an integration context def method_missing(selector, *args, &block) - if @controller && @routes && @routes.named_routes.helpers.include?(selector) + if defined?(@controller) && @controller && @routes && @routes.named_routes.helpers.include?(selector) @controller.send(selector, *args, &block) else super @@ -177,15 +178,35 @@ module ActionDispatch private # Recognizes the route for a given path. - def recognized_request_for(path, request_method = nil) - path = "/#{path}" unless path.first == '/' + def recognized_request_for(path) + if path.is_a?(Hash) + method = path[:method] + path = path[:path] + else + method = :get + end # Assume given controller request = ActionController::TestRequest.new - request.env["REQUEST_METHOD"] = request_method.to_s.upcase if request_method - request.path = path - params = @routes.recognize_path(path, { :method => request.method }) + if path =~ %r{://} + begin + uri = URI.parse(path) + request.env["rack.url_scheme"] = uri.scheme || "http" + request.host = uri.host if uri.host + request.port = uri.port if uri.port + request.path = uri.path.to_s.empty? ? "/" : uri.path + rescue URI::InvalidURIError => e + raise ActionController::RoutingError, e.message + end + else + path = "/#{path}" unless path.first == "/" + request.path = path + end + + request.request_method = method if method + + params = @routes.recognize_path(path, { :method => method }) request.path_parameters = params.with_indifferent_access request diff --git a/actionpack/lib/action_dispatch/testing/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb index 2fc9e2b7d6..2b862fb7d6 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/selector.rb @@ -24,10 +24,6 @@ module ActionDispatch # # Also see HTML::Selector to learn how to use selectors. module SelectorAssertions - # :call-seq: - # css_select(selector) => array - # css_select(element, selector) => array - # # Select and return all matching elements. # # If called with a single argument, uses that argument as a selector @@ -71,7 +67,7 @@ module ActionDispatch arg = args.shift elsif arg == nil raise ArgumentError, "First argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?" - elsif @selected + elsif defined?(@selected) && @selected matches = [] @selected.each do |selected| @@ -99,10 +95,6 @@ module ActionDispatch selector.select(root) end - # :call-seq: - # assert_select(selector, equality?, message?) - # assert_select(element, selector, equality?, message?) - # # An assertion that selects elements and makes one or more equality tests. # # If the first argument is an element, selects all matching elements @@ -195,6 +187,7 @@ module ActionDispatch def assert_select(*args, &block) # Start with optional element followed by mandatory selector. arg = args.shift + @selected ||= nil if arg.is_a?(HTML::Node) # First argument is a node (tag or text, but also HTML root), @@ -332,11 +325,6 @@ module ActionDispatch end end - # :call-seq: - # assert_select_rjs(id?) { |elements| ... } - # assert_select_rjs(statement, id?) { |elements| ... } - # assert_select_rjs(:insert, position, id?) { |elements| ... } - # # Selects content from the RJS response. # # === Narrowing down @@ -455,6 +443,7 @@ module ActionDispatch assert_block("") { true } # to count the assertion if block_given? && !([:remove, :show, :hide, :toggle].include? rjs_type) begin + @selected ||= nil in_scope, @selected = @selected, matches yield matches ensure @@ -474,9 +463,6 @@ module ActionDispatch end end - # :call-seq: - # assert_select_encoded(element?) { |elements| ... } - # # Extracts the content of an element, treats it as encoded HTML and runs # nested assertion on it. # @@ -529,8 +515,8 @@ module ActionDispatch node.content.gsub(/)?/m) { Rack::Utils.escapeHTML($1) } end - selected = elements.map do |element| - text = element.children.select{ |c| not c.tag? }.map{ |c| fix_content[c] }.join + selected = elements.map do |_element| + text = _element.children.select{ |c| not c.tag? }.map{ |c| fix_content[c] }.join root = HTML::Document.new(CGI.unescapeHTML("#{text}")).root css_select(root, "encoded:root", &block)[0] end @@ -543,9 +529,6 @@ module ActionDispatch end end - # :call-seq: - # assert_select_email { } - # # Extracts the body of an email and runs nested assertions on it. # # You must enable deliveries for this assertion to work, use: diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index b52795c575..5c6416a19e 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -115,8 +115,8 @@ module ActionDispatch end end - # An integration Session instance represents a set of requests and responses - # performed sequentially by some virtual user. Because you can instantiate + # An instance of this class represents a set of requests and responses + # performed sequentially by a test process. Because you can instantiate # multiple sessions and run them side-by-side, you can also mimic (to some # limited extent) multiple simultaneous users interacting with your system. # @@ -171,6 +171,7 @@ module ActionDispatch # Create and initialize a new Session instance. def initialize(app) + super() @app = app # If the app is a Rails app, make url_helpers available on the session @@ -182,6 +183,7 @@ module ActionDispatch reset! end + remove_method :default_url_options def default_url_options { :host => host, :protocol => https? ? "https" : "http" } end @@ -233,9 +235,7 @@ module ActionDispatch # Set the host name to use in the next request. # # session.host! "www.example.com" - def host!(name) - @host = name - end + alias :host! :host= private def _mock_session @@ -257,12 +257,14 @@ module ActionDispatch end end + hostname, port = host.split(':') + env = { :method => method, :params => parameters, - "SERVER_NAME" => host.split(':')[0], - "SERVER_PORT" => (https? ? "443" : "80"), + "SERVER_NAME" => hostname, + "SERVER_PORT" => port || (https? ? "443" : "80"), "HTTPS" => https? ? "on" : "off", "rack.url_scheme" => https? ? "https" : "http", @@ -305,7 +307,7 @@ module ActionDispatch include ActionDispatch::Assertions def app - @app + @app ||= nil end # Reset the current session. This is useful for testing multiple sessions @@ -317,10 +319,10 @@ module ActionDispatch %w(get post put head delete cookies assigns xml_http_request xhr get_via_redirect post_via_redirect).each do |method| define_method(method) do |*args| - reset! unless @integration_session + reset! unless integration_session # reset the html_document variable, but only for new get/post calls @html_document = nil unless %w(cookies assigns).include?(method) - @integration_session.__send__(method, *args).tap do + integration_session.__send__(method, *args).tap do copy_session_variables! end end @@ -345,7 +347,7 @@ module ActionDispatch # Copy the instance variables from the current session instance into the # test instance. def copy_session_variables! #:nodoc: - return unless @integration_session + return unless integration_session %w(controller response request).each do |var| instance_variable_set("@#{var}", @integration_session.__send__(var)) end @@ -355,35 +357,44 @@ module ActionDispatch include ActionDispatch::Routing::UrlFor def url_options - reset! unless @integration_session - @integration_session.url_options + reset! unless integration_session + integration_session.url_options + end + + def respond_to?(method, include_private = false) + integration_session.respond_to?(method, include_private) || super end # Delegate unhandled messages to the current session instance. def method_missing(sym, *args, &block) - reset! unless @integration_session - if @integration_session.respond_to?(sym) - @integration_session.__send__(sym, *args, &block).tap do + reset! unless integration_session + if integration_session.respond_to?(sym) + integration_session.__send__(sym, *args, &block).tap do copy_session_variables! end else super end end + + private + def integration_session + @integration_session ||= nil + end end end - # An IntegrationTest is one that spans multiple controllers and actions, + # An test that spans multiple controllers and actions, # tying them all together to ensure they work together as expected. It tests # more completely than either unit or functional tests do, exercising the # entire stack, from the dispatcher to the database. # - # At its simplest, you simply extend IntegrationTest and write your tests + # At its simplest, you simply extend IntegrationTest and write your tests # using the get/post methods: # # require "test_helper" # - # class ExampleTest < ActionController::IntegrationTest + # class ExampleTest < ActionDispatch::IntegrationTest # fixtures :people # # def test_login @@ -403,11 +414,11 @@ module ActionDispatch # However, you can also have multiple session instances open per test, and # even extend those instances with assertions and methods to create a very # powerful testing DSL that is specific for your application. You can even - # reference any named routes you happen to have defined! + # reference any named routes you happen to have defined. # # require "test_helper" # - # class AdvancedTest < ActionController::IntegrationTest + # class AdvancedTest < ActionDispatch::IntegrationTest # fixtures :people, :rooms # # def test_login_and_speak diff --git a/actionpack/lib/action_dispatch/testing/performance_test.rb b/actionpack/lib/action_dispatch/testing/performance_test.rb index 33a5c68b9d..e7aeb45fb3 100644 --- a/actionpack/lib/action_dispatch/testing/performance_test.rb +++ b/actionpack/lib/action_dispatch/testing/performance_test.rb @@ -1,5 +1,4 @@ require 'active_support/testing/performance' -require 'active_support/testing/default' begin module ActionDispatch @@ -11,9 +10,8 @@ begin # formats are written, so you'll have two output files per test method. class PerformanceTest < ActionDispatch::IntegrationTest include ActiveSupport::Testing::Performance - include ActiveSupport::Testing::Default end end rescue NameError $stderr.puts "Specify ruby-prof as application's dependency in Gemfile to run benchmarks." -end \ No newline at end of file +end diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb index c56ebc6438..d430691429 100644 --- a/actionpack/lib/action_dispatch/testing/test_process.rb +++ b/actionpack/lib/action_dispatch/testing/test_process.rb @@ -22,7 +22,7 @@ module ActionDispatch end def cookies - @request.cookies.merge(@response.cookies) + @request.cookies.merge(@response.cookies).with_indifferent_access end def redirect_to_url diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb index b3e67f6e36..822adb6a47 100644 --- a/actionpack/lib/action_dispatch/testing/test_request.rb +++ b/actionpack/lib/action_dispatch/testing/test_request.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/reverse_merge' +require 'rack/utils' module ActionDispatch class TestRequest < Request @@ -10,9 +11,10 @@ module ActionDispatch end def initialize(env = {}) - env = Rails.application.env_defaults.merge(env) if defined?(Rails.application) + env = Rails.application.env_config.merge(env) if defined?(Rails.application) super(DEFAULT_ENV.merge(env)) + @cookies = nil self.host = 'test.host' self.remote_addr = '0.0.0.0' self.user_agent = 'Rails Testing' @@ -66,7 +68,7 @@ module ActionDispatch def accept=(mime_types) @env.delete('action_dispatch.request.accepts') - @env['HTTP_ACCEPT'] = Array(mime_types).collect { |mime_types| mime_types.to_s }.join(",") + @env['HTTP_ACCEPT'] = Array(mime_types).collect { |mime_type| mime_type.to_s }.join(",") end def cookies @@ -76,10 +78,14 @@ module ActionDispatch private def write_cookies! unless @cookies.blank? - @env['HTTP_COOKIE'] = @cookies.map { |name, value| "#{name}=#{value};" }.join(' ') + @env['HTTP_COOKIE'] = @cookies.map { |name, value| escape_cookie(name, value) }.join('; ') end end + def escape_cookie(name, value) + "#{Rack::Utils.escape(name)}=#{Rack::Utils.escape(value)}" + end + def delete_nil_values! @env.delete_if { |k, v| v.nil? } end diff --git a/actionpack/lib/action_dispatch/testing/test_response.rb b/actionpack/lib/action_dispatch/testing/test_response.rb index 44fb1bde99..82039e72e7 100644 --- a/actionpack/lib/action_dispatch/testing/test_response.rb +++ b/actionpack/lib/action_dispatch/testing/test_response.rb @@ -14,123 +14,16 @@ module ActionDispatch end end - module DeprecatedHelpers - def template - ActiveSupport::Deprecation.warn("response.template has been deprecated. Use controller.template instead", caller) - @template - end - attr_writer :template - - def session - ActiveSupport::Deprecation.warn("response.session has been deprecated. Use request.session instead", caller) - @request.session - end - - def assigns - ActiveSupport::Deprecation.warn("response.assigns has been deprecated. Use controller.assigns instead", caller) - @template.controller.assigns - end - - def layout - ActiveSupport::Deprecation.warn("response.layout has been deprecated. Use template.layout instead", caller) - @template.layout - end - - def redirected_to - ::ActiveSupport::Deprecation.warn("response.redirected_to is deprecated. Use response.redirect_url instead", caller) - redirect_url - end - - def redirect_url_match?(pattern) - ::ActiveSupport::Deprecation.warn("response.redirect_url_match? is deprecated. Use assert_match(/foo/, response.redirect_url) instead", caller) - return false if redirect_url.nil? - p = Regexp.new(pattern) if pattern.class == String - p = pattern if pattern.class == Regexp - return false if p.nil? - p.match(redirect_url) != nil - end - - # Returns the template of the file which was used to - # render this response (or nil) - def rendered - ActiveSupport::Deprecation.warn("response.rendered has been deprecated. Use template.rendered instead", caller) - @template.instance_variable_get(:@_rendered) - end - - # A shortcut to the flash. Returns an empty hash if no session flash exists. - def flash - ActiveSupport::Deprecation.warn("response.flash has been deprecated. Use request.flash instead", caller) - request.session['flash'] || {} - end - - # Do we have a flash? - def has_flash? - ActiveSupport::Deprecation.warn("response.has_flash? has been deprecated. Use flash.any? instead", caller) - !flash.empty? - end - - # Do we have a flash that has contents? - def has_flash_with_contents? - ActiveSupport::Deprecation.warn("response.has_flash_with_contents? has been deprecated. Use flash.any? instead", caller) - !flash.empty? - end - - # Does the specified flash object exist? - def has_flash_object?(name=nil) - ActiveSupport::Deprecation.warn("response.has_flash_object? has been deprecated. Use flash[name] instead", caller) - !flash[name].nil? - end - - # Does the specified object exist in the session? - def has_session_object?(name=nil) - ActiveSupport::Deprecation.warn("response.has_session_object? has been deprecated. Use session[name] instead", caller) - !session[name].nil? - end - - # A shortcut to the template.assigns - def template_objects - ActiveSupport::Deprecation.warn("response.template_objects has been deprecated. Use template.assigns instead", caller) - @template.assigns || {} - end - - # Does the specified template object exist? - def has_template_object?(name=nil) - ActiveSupport::Deprecation.warn("response.has_template_object? has been deprecated. Use tempate.assigns[name].nil? instead", caller) - !template_objects[name].nil? - end - - # Returns binary content (downloadable file), converted to a String - def binary_content - ActiveSupport::Deprecation.warn("response.binary_content has been deprecated. Use response.body instead", caller) - body - end - end - include DeprecatedHelpers - # Was the response successful? - def success? - (200..299).include?(response_code) - end + alias_method :success?, :successful? # Was the URL not found? - def missing? - response_code == 404 - end + alias_method :missing?, :not_found? # Were we redirected? - def redirect? - (300..399).include?(response_code) - end + alias_method :redirect?, :redirection? # Was there a server-side error? - def error? - (500..599).include?(response_code) - end - alias_method :server_error?, :error? - - # Was there a client client? - def client_error? - (400..499).include?(response_code) - end + alias_method :error?, :server_error? end end diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb index 1a1497385a..914b13dbfb 100644 --- a/actionpack/lib/action_pack.rb +++ b/actionpack/lib/action_pack.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2010 David Heinemeier Hansson +# Copyright (c) 2004-2011 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionpack/lib/action_pack/version.rb b/actionpack/lib/action_pack/version.rb index 7eaf7d0534..170ceb299a 100644 --- a/actionpack/lib/action_pack/version.rb +++ b/actionpack/lib/action_pack/version.rb @@ -1,10 +1,10 @@ module ActionPack module VERSION #:nodoc: MAJOR = 3 - MINOR = 0 + MINOR = 1 TINY = 0 - BUILD = "rc" + PRE = "beta" - STRING = [MAJOR, MINOR, TINY, BUILD].join('.') + STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end end diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb index c0d7423682..60665387b6 100644 --- a/actionpack/lib/action_view.rb +++ b/actionpack/lib/action_view.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2010 David Heinemeier Hansson +# Copyright (c) 2004-2011 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -21,9 +21,6 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ -activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__) -$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path) - require 'active_support/ruby/shim' require 'active_support/core_ext/class/attribute_accessors' @@ -33,35 +30,43 @@ module ActionView extend ActiveSupport::Autoload eager_autoload do + autoload :Base autoload :Context - autoload :Template autoload :Helpers + autoload :LookupContext + autoload :Partials + autoload :PathSet + autoload :Rendering + autoload :Template + autoload :TestCase - autoload_under "render" do - autoload :Layouts - autoload :Partials - autoload :Rendering + autoload_under "renderer" do + autoload :AbstractRenderer + autoload :PartialRenderer + autoload :TemplateRenderer end - autoload :Base - autoload :LookupContext - autoload :Resolver, 'action_view/template/resolver' - autoload :PathResolver, 'action_view/template/resolver' - autoload :FileSystemResolver, 'action_view/template/resolver' - autoload :PathSet, 'action_view/paths' + autoload_at "action_view/template/resolver" do + autoload :Resolver + autoload :PathResolver + autoload :FileSystemResolver + autoload :FallbackFileSystemResolver + end - autoload :MissingTemplate, 'action_view/template/error' - autoload :ActionViewError, 'action_view/template/error' - autoload :EncodingError, 'action_view/template/error' - autoload :TemplateError, 'action_view/template/error' - autoload :WrongEncodingError, 'action_view/template/error' + autoload_at "action_view/template/error" do + autoload :MissingTemplate + autoload :ActionViewError + autoload :EncodingError + autoload :TemplateError + autoload :WrongEncodingError + end - autoload :TemplateHandler, 'action_view/template' - autoload :TemplateHandlers, 'action_view/template' + autoload_at "action_view/template" do + autoload :TemplateHandler + autoload :TemplateHandlers + end end - autoload :TestCase, 'action_view/test_case' - ENCODING_FLAG = '#.*coding[:=]\s*(\S+)[ \t]*' end diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index 20a2e7c1f0..ab8c6259c5 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -6,9 +6,6 @@ require 'active_support/ordered_options' require 'action_view/log_subscriber' module ActionView #:nodoc: - class NonConcattingString < ActiveSupport::SafeBuffer - end - # = Action View Base # # Action View templates can be written in three ways. If the template file has a .erb (or .rhtml) extension then it uses a mixture of ERb @@ -21,7 +18,7 @@ module ActionView #:nodoc: # following loop for names: # # Names of all the people - # <% for person in @people %> + # <% @people.each do |person| %> # Name: <%= person.name %>
# <% end %> # @@ -79,8 +76,8 @@ module ActionView #:nodoc: # # === Template caching # - # By default, Rails will compile each template to a method in order to render it. When you alter a template, Rails will - # check the file's modification time and recompile it. + # By default, Rails will compile each template to a method in order to render it. When you alter a template, + # Rails will check the file's modification time and recompile it in development mode. # # == Builder # @@ -156,12 +153,9 @@ module ActionView #:nodoc: # # This refreshes the sidebar, removes a person element and highlights the user list. # - # See the ActionView::Helpers::PrototypeHelper::GeneratorMethods documentation for more details. + # See the ActionView::Helpers::PrototypeHelper::JavaScriptGenerator::GeneratorMethods documentation for more details. class Base - module Subclasses - end - - include Helpers, Rendering, Partials, Layouts, ::ERB::Util, Context + include Helpers, Rendering, Partials, ::ERB::Util, Context # Specify whether RJS responses should be wrapped in a try/catch block # that alert()s the caught exception (and then re-raises it). @@ -178,24 +172,26 @@ module ActionView #:nodoc: class << self delegate :erb_trim_mode=, :to => 'ActionView::Template::Handlers::ERB' delegate :logger, :to => 'ActionController::Base', :allow_nil => true + + def cache_template_loading + ActionView::Resolver.caching? + end + + def cache_template_loading=(value) + ActionView::Resolver.caching = value + end end - attr_accessor :base_path, :assigns, :template_extension, :lookup_context - attr_internal :captures, :request, :controller, :template, :config + attr_accessor :_template + attr_internal :request, :controller, :config, :assigns, :lookup_context - delegate :find_template, :template_exists?, :formats, :formats=, :locale, :locale=, - :view_paths, :view_paths=, :with_fallbacks, :update_details, :with_layout_format, :to => :lookup_context + delegate :formats, :formats=, :locale, :locale=, :view_paths, :view_paths=, :to => :lookup_context - delegate :request_forgery_protection_token, :template, :params, :session, :cookies, :response, :headers, + delegate :request_forgery_protection_token, :params, :session, :cookies, :response, :headers, :flash, :action_name, :controller_name, :to => :controller delegate :logger, :to => :controller, :allow_nil => true - # TODO: HACK FOR RJS - def view_context - self - end - def self.xss_safe? #:nodoc: true end @@ -206,33 +202,40 @@ module ActionView #:nodoc: end def assign(new_assigns) # :nodoc: - self.assigns = new_assigns.each { |key, value| instance_variable_set("@#{key}", value) } + @_assigns = new_assigns.each { |key, value| instance_variable_set("@#{key}", value) } end def initialize(lookup_context = nil, assigns_for_first_render = {}, controller = nil, formats = nil) #:nodoc: assign(assigns_for_first_render) - self.helpers = self.class.helpers || Module.new - - if @_controller = controller - @_request = controller.request if controller.respond_to?(:request) - end - - config = controller && controller.respond_to?(:config) ? controller.config : {} - @_config = ActiveSupport::InheritableOptions.new(config) + self.helpers = Module.new unless self.class.helpers + @_config = {} @_content_for = Hash.new { |h,k| h[k] = ActiveSupport::SafeBuffer.new } @_virtual_path = nil @output_buffer = nil - @lookup_context = lookup_context.is_a?(ActionView::LookupContext) ? + if @_controller = controller + @_request = controller.request if controller.respond_to?(:request) + @_config = controller.config.inheritable_copy if controller.respond_to?(:config) + end + + @_lookup_context = lookup_context.is_a?(ActionView::LookupContext) ? lookup_context : ActionView::LookupContext.new(lookup_context) - @lookup_context.formats = formats if formats + @_lookup_context.formats = formats if formats + end + + def store_content_for(key, value) + @_content_for[key] = value end def controller_path @controller_path ||= controller && controller.controller_path end + def controller_prefixes + @controller_prefixes ||= controller && controller._prefixes + end + ActiveSupport.run_load_hooks(:action_view, self) end end diff --git a/actionpack/lib/action_view/helpers.rb b/actionpack/lib/action_view/helpers.rb index b7ffa345cc..d338ce616a 100644 --- a/actionpack/lib/action_view/helpers.rb +++ b/actionpack/lib/action_view/helpers.rb @@ -12,14 +12,13 @@ module ActionView #:nodoc: autoload :CsrfHelper autoload :DateHelper autoload :DebugHelper - autoload :DeprecatedBlockHelpers autoload :FormHelper autoload :FormOptionsHelper autoload :FormTagHelper autoload :JavaScriptHelper, "action_view/helpers/javascript_helper" autoload :NumberHelper autoload :PrototypeHelper - autoload :RawOutputHelper + autoload :OutputSafetyHelper autoload :RecordTagHelper autoload :SanitizeHelper autoload :ScriptaculousHelper @@ -49,7 +48,7 @@ module ActionView #:nodoc: include JavaScriptHelper include NumberHelper include PrototypeHelper - include RawOutputHelper + include OutputSafetyHelper include RecordTagHelper include SanitizeHelper include ScriptaculousHelper diff --git a/actionpack/lib/action_view/helpers/active_model_helper.rb b/actionpack/lib/action_view/helpers/active_model_helper.rb index 6bb0875bc3..96c3eec337 100644 --- a/actionpack/lib/action_view/helpers/active_model_helper.rb +++ b/actionpack/lib/action_view/helpers/active_model_helper.rb @@ -7,25 +7,6 @@ module ActionView # = Active Model Helpers module Helpers module ActiveModelHelper - %w(input form error_messages_for error_message_on).each do |method| - class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def #{method}(*args) - ActiveSupport::Deprecation.warn "#{method} was removed from Rails and is now available as a plugin. " << - "Please install it with `rails plugin install git://github.com/rails/dynamic_form.git`.", caller - end - RUBY - end - end - - module ActiveModelFormBuilder - %w(error_messages error_message_on).each do |method| - class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def #{method}(*args) - ActiveSupport::Deprecation.warn "f.#{method} was removed from Rails and is now available as a plugin. " << - "Please install it with `rails plugin install git://github.com/rails/dynamic_form.git`.", caller - end - RUBY - end end module ActiveModelInstanceTag @@ -67,10 +48,6 @@ module ActionView end end - class FormBuilder - include ActiveModelFormBuilder - end - class InstanceTag include ActiveModelInstanceTag end diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index a3c43d3e93..f6b2d4f3f4 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -1,9 +1,6 @@ -require 'thread' -require 'cgi' -require 'action_view/helpers/url_helper' -require 'action_view/helpers/tag_helper' -require 'active_support/core_ext/file' -require 'active_support/core_ext/object/blank' +require 'action_view/helpers/asset_tag_helpers/javascript_tag_helpers' +require 'action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers' +require 'action_view/helpers/asset_tag_helpers/asset_paths' module ActionView # = Action View Asset Tag Helpers @@ -152,7 +149,7 @@ module ActionView # # # Normally you'd calculate RELEASE_NUMBER at startup. # RELEASE_NUMBER = 12345 - # config.action_controller.asset_path_template = proc { |asset_path| + # config.action_controller.asset_path = proc { |asset_path| # "/release-#{RELEASE_NUMBER}#{asset_path}" # } # @@ -194,20 +191,8 @@ module ActionView # RewriteEngine On # RewriteRule ^/release-\d+/(images|javascripts|stylesheets)/(.*)$ /$1/$2 [L] module AssetTagHelper - mattr_reader :javascript_expansions - @@javascript_expansions = { } - - mattr_reader :stylesheet_expansions - @@stylesheet_expansions = {} - - # You can enable or disable the asset tag timestamps cache. - # With the cache enabled, the asset tag helper methods will make fewer - # expensive file system calls. However this prevents you from modifying - # any asset files while the server is running. - # - # ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = false - mattr_accessor :cache_asset_timestamps - + include JavascriptTagHelpers + include StylesheetTagHelpers # Returns a link tag that browsers and news readers can use to auto-detect # an RSS or ATOM feed. The +type+ can either be :rss (default) or # :atom. Control the link options in url_for format using the @@ -241,263 +226,6 @@ module ActionView ) end - # Computes the path to a javascript asset in the public javascripts directory. - # If the +source+ filename has no extension, .js will be appended (except for explicit URIs) - # Full paths from the document root will be passed through. - # Used internally by javascript_include_tag to build the script path. - # - # ==== Examples - # javascript_path "xmlhr" # => /javascripts/xmlhr.js - # javascript_path "dir/xmlhr.js" # => /javascripts/dir/xmlhr.js - # javascript_path "/dir/xmlhr" # => /dir/xmlhr.js - # javascript_path "http://www.railsapplication.com/js/xmlhr" # => http://www.railsapplication.com/js/xmlhr - # javascript_path "http://www.railsapplication.com/js/xmlhr.js" # => http://www.railsapplication.com/js/xmlhr.js - def javascript_path(source) - compute_public_path(source, 'javascripts', 'js') - end - alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route - - # Returns an html script tag for each of the +sources+ provided. You - # can pass in the filename (.js extension is optional) of javascript files - # that exist in your public/javascripts directory for inclusion into the - # current page or you can pass the full path relative to your document - # root. To include the Prototype and Scriptaculous javascript libraries in - # your application, pass :defaults as the source. When using - # :defaults, if an application.js file exists in your public - # javascripts directory, it will be included as well. You can modify the - # html attributes of the script tag by passing a hash as the last argument. - # - # ==== Examples - # javascript_include_tag "xmlhr" # => - # - # - # javascript_include_tag "xmlhr.js" # => - # - # - # javascript_include_tag "common.javascript", "/elsewhere/cools" # => - # - # - # - # javascript_include_tag "http://www.railsapplication.com/xmlhr" # => - # - # - # javascript_include_tag "http://www.railsapplication.com/xmlhr.js" # => - # - # - # javascript_include_tag :defaults # => - # - # - # ... - # - # - # * = The application.js file is only referenced if it exists - # - # Though it's not really recommended practice, if you need to extend the default JavaScript set for any reason - # (e.g., you're going to be using a certain .js file in every action), then take a look at the register_javascript_include_default method. - # - # You can also include all javascripts in the javascripts directory using :all as the source: - # - # javascript_include_tag :all # => - # - # - # ... - # - # - # - # - # Note that the default javascript files will be included first. So Prototype and Scriptaculous are available to - # all subsequently included files. - # - # If you want Rails to search in all the subdirectories under javascripts, you should explicitly set :recursive: - # - # javascript_include_tag :all, :recursive => true - # - # == Caching multiple javascripts into one - # - # You can also cache multiple javascripts into one file, which requires less HTTP connections to download and can better be - # compressed by gzip (leading to faster transfers). Caching will only happen if config.perform_caching - # is set to true (which is the case by default for the Rails production environment, but not for the development - # environment). - # - # ==== Examples - # javascript_include_tag :all, :cache => true # when config.perform_caching is false => - # - # - # ... - # - # - # - # - # javascript_include_tag :all, :cache => true # when config.perform_caching is true => - # - # - # javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when config.perform_caching is false => - # - # - # - # - # javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when config.perform_caching is true => - # - # - # The :recursive option is also available for caching: - # - # javascript_include_tag :all, :cache => true, :recursive => true - def javascript_include_tag(*sources) - options = sources.extract_options!.stringify_keys - concat = options.delete("concat") - cache = concat || options.delete("cache") - recursive = options.delete("recursive") - - if concat || (config.perform_caching && cache) - joined_javascript_name = (cache == true ? "all" : cache) + ".js" - joined_javascript_path = File.join(joined_javascript_name[/^#{File::SEPARATOR}/] ? config.assets_dir : config.javascripts_dir, joined_javascript_name) - - unless config.perform_caching && File.exists?(joined_javascript_path) - write_asset_file_contents(joined_javascript_path, compute_javascript_paths(sources, recursive)) - end - javascript_src_tag(joined_javascript_name, options) - else - sources = expand_javascript_sources(sources, recursive) - ensure_javascript_sources!(sources) if cache - sources.collect { |source| javascript_src_tag(source, options) }.join("\n").html_safe - end - end - - # Register one or more javascript files to be included when symbol - # is passed to javascript_include_tag. This method is typically intended - # to be called from plugin initialization to register javascript files - # that the plugin installed in public/javascripts. - # - # ActionView::Helpers::AssetTagHelper.register_javascript_expansion :monkey => ["head", "body", "tail"] - # - # javascript_include_tag :monkey # => - # - # - # - def self.register_javascript_expansion(expansions) - @@javascript_expansions.merge!(expansions) - end - - # Register one or more stylesheet files to be included when symbol - # is passed to stylesheet_link_tag. This method is typically intended - # to be called from plugin initialization to register stylesheet files - # that the plugin installed in public/stylesheets. - # - # ActionView::Helpers::AssetTagHelper.register_stylesheet_expansion :monkey => ["head", "body", "tail"] - # - # stylesheet_link_tag :monkey # => - # - # - # - def self.register_stylesheet_expansion(expansions) - @@stylesheet_expansions.merge!(expansions) - end - - # Computes the path to a stylesheet asset in the public stylesheets directory. - # If the +source+ filename has no extension, .css will be appended (except for explicit URIs). - # Full paths from the document root will be passed through. - # Used internally by +stylesheet_link_tag+ to build the stylesheet path. - # - # ==== Examples - # stylesheet_path "style" # => /stylesheets/style.css - # stylesheet_path "dir/style.css" # => /stylesheets/dir/style.css - # stylesheet_path "/dir/style.css" # => /dir/style.css - # stylesheet_path "http://www.railsapplication.com/css/style" # => http://www.railsapplication.com/css/style - # stylesheet_path "http://www.railsapplication.com/css/style.css" # => http://www.railsapplication.com/css/style.css - def stylesheet_path(source) - compute_public_path(source, 'stylesheets', 'css') - end - alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route - - # Returns a stylesheet link tag for the sources specified as arguments. If - # you don't specify an extension, .css will be appended automatically. - # You can modify the link attributes by passing a hash as the last argument. - # - # ==== Examples - # stylesheet_link_tag "style" # => - # - # - # stylesheet_link_tag "style.css" # => - # - # - # stylesheet_link_tag "http://www.railsapplication.com/style.css" # => - # - # - # stylesheet_link_tag "style", :media => "all" # => - # - # - # stylesheet_link_tag "style", :media => "print" # => - # - # - # stylesheet_link_tag "random.styles", "/css/stylish" # => - # - # - # - # You can also include all styles in the stylesheets directory using :all as the source: - # - # stylesheet_link_tag :all # => - # - # - # - # - # If you want Rails to search in all the subdirectories under stylesheets, you should explicitly set :recursive: - # - # stylesheet_link_tag :all, :recursive => true - # - # == Caching multiple stylesheets into one - # - # You can also cache multiple stylesheets into one file, which requires less HTTP connections and can better be - # compressed by gzip (leading to faster transfers). Caching will only happen if config.perform_caching - # is set to true (which is the case by default for the Rails production environment, but not for the development - # environment). Examples: - # - # ==== Examples - # stylesheet_link_tag :all, :cache => true # when config.perform_caching is false => - # - # - # - # - # stylesheet_link_tag :all, :cache => true # when config.perform_caching is true => - # - # - # stylesheet_link_tag "shop", "cart", "checkout", :cache => "payment" # when config.perform_caching is false => - # - # - # - # - # stylesheet_link_tag "shop", "cart", "checkout", :cache => "payment" # when config.perform_caching is true => - # - # - # The :recursive option is also available for caching: - # - # stylesheet_link_tag :all, :cache => true, :recursive => true - # - # To force concatenation (even in development mode) set :concat to true. This is useful if - # you have too many stylesheets for IE to load. - # - # stylesheet_link_tag :all, :concat => true - # - def stylesheet_link_tag(*sources) - options = sources.extract_options!.stringify_keys - concat = options.delete("concat") - cache = concat || options.delete("cache") - recursive = options.delete("recursive") - - if concat || (config.perform_caching && cache) - joined_stylesheet_name = (cache == true ? "all" : cache) + ".css" - joined_stylesheet_path = File.join(joined_stylesheet_name[/^#{File::SEPARATOR}/] ? config.assets_dir : config.stylesheets_dir, joined_stylesheet_name) - - unless config.perform_caching && File.exists?(joined_stylesheet_path) - write_asset_file_contents(joined_stylesheet_path, compute_stylesheet_paths(sources, recursive)) - end - stylesheet_tag(joined_stylesheet_name, options) - else - sources = expand_stylesheet_sources(sources, recursive) - ensure_stylesheet_sources!(sources) if cache - sources.collect { |source| stylesheet_tag(source, options) }.join("\n").html_safe - end - end - # Web browsers cache favicons. If you just throw a favicon.ico into the document # root of your application and it changes later, clients that have it in their cache # won't see the update. Using this helper prevents that because it appends an asset ID: @@ -546,7 +274,7 @@ module ActionView # The alias +path_to_image+ is provided to avoid that. Rails uses the alias internally, and # plugin authors are encouraged to do so. def image_path(source) - compute_public_path(source, 'images') + asset_paths.compute_public_path(source, 'images') end alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route @@ -561,7 +289,7 @@ module ActionView # video_path("/trailers/hd.avi") # => /trailers/hd.avi # video_path("http://www.railsapplication.com/vid/hd.avi") # => http://www.railsapplication.com/vid/hd.avi def video_path(source) - compute_public_path(source, 'videos') + asset_paths.compute_public_path(source, 'videos') end alias_method :path_to_video, :video_path # aliased to avoid conflicts with a video_path named route @@ -571,12 +299,12 @@ module ActionView # # ==== Examples # audio_path("horse") # => /audios/horse - # audio_path("horse.wav") # => /audios/horse.avi - # audio_path("sounds/horse.wav") # => /audios/sounds/horse.avi - # audio_path("/sounds/horse.wav") # => /sounds/horse.avi + # audio_path("horse.wav") # => /audios/horse.wav + # audio_path("sounds/horse.wav") # => /audios/sounds/horse.wav + # audio_path("/sounds/horse.wav") # => /sounds/horse.wav # audio_path("http://www.railsapplication.com/sounds/horse.wav") # => http://www.railsapplication.com/sounds/horse.wav def audio_path(source) - compute_public_path(source, 'audios') + asset_paths.compute_public_path(source, 'audios') end alias_method :path_to_audio, :audio_path # aliased to avoid conflicts with an audio_path named route @@ -705,202 +433,8 @@ module ActionView private - def rewrite_extension?(source, dir, ext) - source_ext = File.extname(source)[1..-1] - ext && (source_ext.blank? || (ext != source_ext && File.exist?(File.join(config.assets_dir, dir, "#{source}.#{ext}")))) - end - - def rewrite_host_and_protocol(source, has_request) - host = compute_asset_host(source) - if has_request && host.present? && !is_uri?(host) - host = "#{controller.request.protocol}#{host}" - end - "#{host}#{source}" - end - - # Add the the extension +ext+ if not present. Return full URLs otherwise untouched. - # Prefix with /dir/ if lacking a leading +/+. Account for relative URL - # roots. Rewrite the asset path for cache-busting asset ids. Include - # asset host, if configured, with the correct request protocol. - def compute_public_path(source, dir, ext = nil, include_host = true) - return source if is_uri?(source) - - source += ".#{ext}" if rewrite_extension?(source, dir, ext) - source = "/#{dir}/#{source}" unless source[0] == ?/ - source = rewrite_asset_path(source, config.asset_path) - - has_request = controller.respond_to?(:request) - if has_request && include_host && source !~ %r{^#{controller.config.relative_url_root}/} - source = "#{controller.config.relative_url_root}#{source}" - end - source = rewrite_host_and_protocol(source, has_request) if include_host - - source - end - - def is_uri?(path) - path =~ %r{^[-a-z]+://|^cid:} - end - - # Pick an asset host for this source. Returns +nil+ if no host is set, - # the host if no wildcard is set, the host interpolated with the - # numbers 0-3 if it contains %d (the number is the source hash mod 4), - # or the value returned from invoking the proc if it's a proc or the value from - # invoking call if it's an object responding to call. - def compute_asset_host(source) - if host = config.asset_host - if host.is_a?(Proc) || host.respond_to?(:call) - case host.is_a?(Proc) ? host.arity : host.method(:call).arity - when 2 - request = controller.respond_to?(:request) && controller.request - host.call(source, request) - else - host.call(source) - end - else - (host =~ /%d/) ? host % (source.hash % 4) : host - end - end - end - - @@asset_timestamps_cache = {} - @@asset_timestamps_cache_guard = Mutex.new - - # Use the RAILS_ASSET_ID environment variable or the source's - # modification time as its cache-busting asset id. - def rails_asset_id(source) - if asset_id = ENV["RAILS_ASSET_ID"] - asset_id - else - if @@cache_asset_timestamps && (asset_id = @@asset_timestamps_cache[source]) - asset_id - else - path = File.join(config.assets_dir, source) - asset_id = File.exist?(path) ? File.mtime(path).to_i.to_s : '' - - if @@cache_asset_timestamps - @@asset_timestamps_cache_guard.synchronize do - @@asset_timestamps_cache[source] = asset_id - end - end - - asset_id - end - end - end - - # Break out the asset path rewrite in case plugins wish to put the asset id - # someplace other than the query string. - def rewrite_asset_path(source, path = nil) - if path && path.respond_to?(:call) - return path.call(source) - elsif path && path.is_a?(String) - return path % [source] - end - - asset_id = rails_asset_id(source) - if asset_id.blank? - source - else - source + "?#{asset_id}" - end - end - - def javascript_src_tag(source, options) - content_tag("script", "", { "type" => Mime::JS, "src" => path_to_javascript(source) }.merge(options)) - end - - def stylesheet_tag(source, options) - tag("link", { "rel" => "stylesheet", "type" => Mime::CSS, "media" => "screen", "href" => html_escape(path_to_stylesheet(source)) }.merge(options), false, false) - end - - def compute_javascript_paths(*args) - expand_javascript_sources(*args).collect { |source| compute_public_path(source, 'javascripts', 'js', false) } - end - - def compute_stylesheet_paths(*args) - expand_stylesheet_sources(*args).collect { |source| compute_public_path(source, 'stylesheets', 'css', false) } - end - - def expand_javascript_sources(sources, recursive = false) - if sources.include?(:all) - all_javascript_files = collect_asset_files(config.javascripts_dir, ('**' if recursive), '*.js') - ((determine_source(:defaults, @@javascript_expansions).dup & all_javascript_files) + all_javascript_files).uniq - else - expanded_sources = sources.collect do |source| - determine_source(source, @@javascript_expansions) - end.flatten - expanded_sources << "application" if sources.include?(:defaults) && File.exist?(File.join(config.javascripts_dir, "application.js")) - expanded_sources - end - end - - def expand_stylesheet_sources(sources, recursive) - if sources.first == :all - collect_asset_files(config.stylesheets_dir, ('**' if recursive), '*.css') - else - sources.collect do |source| - determine_source(source, @@stylesheet_expansions) - end.flatten - end - end - - def determine_source(source, collection) - case source - when Symbol - collection[source] || raise(ArgumentError, "No expansion found for #{source.inspect}") - else - source - end - end - - def ensure_stylesheet_sources!(sources) - sources.each do |source| - asset_file_path!(path_to_stylesheet(source)) - end - return sources - end - - def ensure_javascript_sources!(sources) - sources.each do |source| - asset_file_path!(path_to_javascript(source)) - end - return sources - end - - def join_asset_file_contents(paths) - paths.collect { |path| File.read(asset_file_path!(path)) }.join("\n\n") - end - - def write_asset_file_contents(joined_asset_path, asset_paths) - - FileUtils.mkdir_p(File.dirname(joined_asset_path)) - File.atomic_write(joined_asset_path) { |cache| cache.write(join_asset_file_contents(asset_paths)) } - - # Set mtime to the latest of the combined files to allow for - # consistent ETag without a shared filesystem. - mt = asset_paths.map { |p| File.mtime(asset_file_path(p)) }.max - File.utime(mt, mt, joined_asset_path) - end - - def asset_file_path(path) - File.join(config.assets_dir, path.split('?').first) - end - - def asset_file_path!(path) - unless is_uri?(path) - absolute_path = asset_file_path(path) - raise(Errno::ENOENT, "Asset file not found at '#{absolute_path}'" ) unless File.exist?(absolute_path) - return absolute_path - end - end - - def collect_asset_files(*path) - dir = path.first - - Dir[File.join(*path.compact)].collect do |file| - file[-(file.size - dir.size - 1)..-1].sub(/\.\w+$/, '') - end.sort + def asset_paths + @asset_paths ||= AssetPaths.new(config, controller) end end end diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb new file mode 100644 index 0000000000..52eb43a1cd --- /dev/null +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb @@ -0,0 +1,146 @@ +require 'active_support/core_ext/class/attribute' +require 'active_support/core_ext/string/inflections' +require 'active_support/core_ext/file' +require 'action_view/helpers/tag_helper' + +module ActionView + module Helpers + module AssetTagHelper + + class AssetIncludeTag + attr_reader :config, :asset_paths + + class_attribute :expansions + def self.inherited(base) + base.expansions = { } + end + + def initialize(config, asset_paths) + @config = config + @asset_paths = asset_paths + end + + def asset_name + raise NotImplementedError + end + + def extension + raise NotImplementedError + end + + def custom_dir + raise NotImplementedError + end + + def asset_tag(source, options) + raise NotImplementedError + end + + def include_tag(*sources) + options = sources.extract_options!.stringify_keys + concat = options.delete("concat") + cache = concat || options.delete("cache") + recursive = options.delete("recursive") + + if concat || (config.perform_caching && cache) + joined_name = (cache == true ? "all" : cache) + ".#{extension}" + joined_path = File.join((joined_name[/^#{File::SEPARATOR}/] ? config.assets_dir : custom_dir), joined_name) + unless config.perform_caching && File.exists?(joined_path) + write_asset_file_contents(joined_path, compute_paths(sources, recursive)) + end + asset_tag(joined_name, options) + else + sources = expand_sources(sources, recursive) + ensure_sources!(sources) if cache + sources.collect { |source| asset_tag(source, options) }.join("\n").html_safe + end + end + + + private + + def path_to_asset(source, include_host = true) + asset_paths.compute_public_path(source, asset_name.to_s.pluralize, extension, include_host) + end + + def compute_paths(*args) + expand_sources(*args).collect { |source| asset_paths.compute_public_path(source, asset_name.pluralize, extension, false) } + end + + def expand_sources(sources, recursive) + if sources.first == :all + collect_asset_files(custom_dir, ('**' if recursive), "*.#{extension}") + else + sources.inject([]) do |list, source| + determined_source = determine_source(source, expansions) + update_source_list(list, determined_source) + end + end + end + + def update_source_list(list, source) + case source + when String + list.delete(source) + list << source + when Array + updated_sources = source - list + list.concat(updated_sources) + end + end + + def ensure_sources!(sources) + sources.each do |source| + asset_file_path!(path_to_asset(source, false)) + end + end + + def collect_asset_files(*path) + dir = path.first + + Dir[File.join(*path.compact)].collect do |file| + file[-(file.size - dir.size - 1)..-1].sub(/\.\w+$/, '') + end.sort + end + + def determine_source(source, collection) + case source + when Symbol + collection[source] || raise(ArgumentError, "No expansion found for #{source.inspect}") + else + source + end + end + + def join_asset_file_contents(paths) + paths.collect { |path| File.read(asset_file_path!(path, true)) }.join("\n\n") + end + + def write_asset_file_contents(joined_asset_path, asset_paths) + FileUtils.mkdir_p(File.dirname(joined_asset_path)) + File.atomic_write(joined_asset_path) { |cache| cache.write(join_asset_file_contents(asset_paths)) } + + # Set mtime to the latest of the combined files to allow for + # consistent ETag without a shared filesystem. + mt = asset_paths.map { |p| File.mtime(asset_file_path(p)) }.max + File.utime(mt, mt, joined_asset_path) + end + + def asset_file_path(path) + File.join(config.assets_dir, path.split('?').first) + end + + def asset_file_path!(path, error_if_file_is_uri = false) + if asset_paths.is_uri?(path) + raise(Errno::ENOENT, "Asset file #{path} is uri and cannot be merged into single file") if error_if_file_is_uri + else + absolute_path = asset_file_path(path) + raise(Errno::ENOENT, "Asset file not found at '#{absolute_path}'" ) unless File.exist?(absolute_path) + return absolute_path + end + end + end + + end + end +end diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb new file mode 100644 index 0000000000..014a03c54d --- /dev/null +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb @@ -0,0 +1,153 @@ +require 'active_support/core_ext/file' + +module ActionView + module Helpers + module AssetTagHelper + + class AssetPaths + # You can enable or disable the asset tag ids cache. + # With the cache enabled, the asset tag helper methods will make fewer + # expensive file system calls (the default implementation checks the file + # system timestamp). However this prevents you from modifying any asset + # files while the server is running. + # + # ActionView::Helpers::AssetTagHelper::AssetPaths.cache_asset_ids = false + mattr_accessor :cache_asset_ids + + attr_reader :config, :controller + + def initialize(config, controller) + @config = config + @controller = controller + end + + # Add the extension +ext+ if not present. Return full URLs otherwise untouched. + # Prefix with /dir/ if lacking a leading +/+. Account for relative URL + # roots. Rewrite the asset path for cache-busting asset ids. Include + # asset host, if configured, with the correct request protocol. + def compute_public_path(source, dir, ext = nil, include_host = true) + return source if is_uri?(source) + + source = rewrite_extension(source, dir, ext) if ext + source = "/#{dir}/#{source}" unless source[0] == ?/ + if controller.respond_to?(:env) && controller.env["action_dispatch.asset_path"] + source = rewrite_asset_path(source, controller.env["action_dispatch.asset_path"]) + end + source = rewrite_asset_path(source, config.asset_path) + + has_request = controller.respond_to?(:request) + source = rewrite_relative_url_root(source, controller.config.relative_url_root) if has_request && include_host + source = rewrite_host_and_protocol(source, has_request) if include_host + + source + end + + # Add or change an asset id in the asset id cache. This can be used + # for SASS on Heroku. + # :api: public + def add_to_asset_ids_cache(source, asset_id) + self.asset_ids_cache_guard.synchronize do + self.asset_ids_cache[source] = asset_id + end + end + + def is_uri?(path) + path =~ %r{^[-a-z]+://|^cid:} + end + + private + + def rewrite_extension(source, dir, ext) + source_ext = File.extname(source) + + source_with_ext = if source_ext.empty? + "#{source}.#{ext}" + elsif ext != source_ext[1..-1] + with_ext = "#{source}.#{ext}" + with_ext if File.exist?(File.join(config.assets_dir, dir, with_ext)) + end + + source_with_ext || source + end + + # Break out the asset path rewrite in case plugins wish to put the asset id + # someplace other than the query string. + def rewrite_asset_path(source, path = nil) + if path && path.respond_to?(:call) + return path.call(source) + elsif path && path.is_a?(String) + return path % [source] + end + + asset_id = rails_asset_id(source) + if asset_id.empty? + source + else + "#{source}?#{asset_id}" + end + end + + mattr_accessor :asset_ids_cache + self.asset_ids_cache = {} + + mattr_accessor :asset_ids_cache_guard + self.asset_ids_cache_guard = Mutex.new + + # Use the RAILS_ASSET_ID environment variable or the source's + # modification time as its cache-busting asset id. + def rails_asset_id(source) + if asset_id = ENV["RAILS_ASSET_ID"] + asset_id + else + if self.cache_asset_ids && (asset_id = self.asset_ids_cache[source]) + asset_id + else + path = File.join(config.assets_dir, source) + asset_id = File.exist?(path) ? File.mtime(path).to_i.to_s : '' + + if self.cache_asset_ids + add_to_asset_ids_cache(source, asset_id) + end + + asset_id + end + end + end + + def rewrite_relative_url_root(source, relative_url_root) + relative_url_root && !source.starts_with?("#{relative_url_root}/") ? "#{relative_url_root}#{source}" : source + end + + def rewrite_host_and_protocol(source, has_request) + host = compute_asset_host(source) + if has_request && host && !is_uri?(host) + host = "#{controller.request.protocol}#{host}" + end + "#{host}#{source}" + end + + # Pick an asset host for this source. Returns +nil+ if no host is set, + # the host if no wildcard is set, the host interpolated with the + # numbers 0-3 if it contains %d (the number is the source hash mod 4), + # or the value returned from invoking the proc if it's a proc or the value from + # invoking call if it's an object responding to call. + def compute_asset_host(source) + if host = config.asset_host + if host.is_a?(Proc) || host.respond_to?(:call) + case host.is_a?(Proc) ? host.arity : host.method(:call).arity + when 2 + request = controller.respond_to?(:request) && controller.request + host.call(source, request) + else + host.call(source) + end + else + (host =~ /%d/) ? host % (source.hash % 4) : host + end + end + end + end + + end + end +end \ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb new file mode 100644 index 0000000000..82bbfcc7d2 --- /dev/null +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb @@ -0,0 +1,184 @@ +require 'active_support/concern' +require 'active_support/core_ext/file' +require 'action_view/helpers/tag_helper' +require 'action_view/helpers/asset_tag_helpers/asset_include_tag' + +module ActionView + module Helpers + module AssetTagHelper + + class JavascriptIncludeTag < AssetIncludeTag + include TagHelper + + def asset_name + 'javascript' + end + + def extension + 'js' + end + + def asset_tag(source, options) + content_tag("script", "", { "type" => Mime::JS, "src" => path_to_asset(source) }.merge(options)) + end + + def custom_dir + config.javascripts_dir + end + + private + + def expand_sources(sources, recursive = false) + if sources.include?(:all) + all_asset_files = (collect_asset_files(custom_dir, ('**' if recursive), "*.#{extension}") - ['application']) << 'application' + ((determine_source(:defaults, expansions).dup & all_asset_files) + all_asset_files).uniq + else + expanded_sources = sources.inject([]) do |list, source| + determined_source = determine_source(source, expansions) + update_source_list(list, determined_source) + end + add_application_js(expanded_sources, sources) + expanded_sources + end + end + + def add_application_js(expanded_sources, sources) + if sources.include?(:defaults) && File.exist?(File.join(custom_dir, "application.#{extension}")) + expanded_sources.delete('application') + expanded_sources << "application" + end + end + end + + + module JavascriptTagHelpers + extend ActiveSupport::Concern + + module ClassMethods + # Register one or more javascript files to be included when symbol + # is passed to javascript_include_tag. This method is typically intended + # to be called from plugin initialization to register javascript files + # that the plugin installed in public/javascripts. + # + # ActionView::Helpers::AssetTagHelper.register_javascript_expansion :monkey => ["head", "body", "tail"] + # + # javascript_include_tag :monkey # => + # + # + # + def register_javascript_expansion(expansions) + js_expansions = JavascriptIncludeTag.expansions + expansions.each do |key, values| + js_expansions[key] = (js_expansions[key] || []) | Array(values) + end + end + end + + # Computes the path to a javascript asset in the public javascripts directory. + # If the +source+ filename has no extension, .js will be appended (except for explicit URIs) + # Full paths from the document root will be passed through. + # Used internally by javascript_include_tag to build the script path. + # + # ==== Examples + # javascript_path "xmlhr" # => /javascripts/xmlhr.js + # javascript_path "dir/xmlhr.js" # => /javascripts/dir/xmlhr.js + # javascript_path "/dir/xmlhr" # => /dir/xmlhr.js + # javascript_path "http://www.railsapplication.com/js/xmlhr" # => http://www.railsapplication.com/js/xmlhr + # javascript_path "http://www.railsapplication.com/js/xmlhr.js" # => http://www.railsapplication.com/js/xmlhr.js + def javascript_path(source) + asset_paths.compute_public_path(source, 'javascripts', 'js') + end + alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route + + # Returns an HTML script tag for each of the +sources+ provided. You + # can pass in the filename (.js extension is optional) of JavaScript files + # that exist in your public/javascripts directory for inclusion into the + # current page or you can pass the full path relative to your document + # root. To include the Prototype and Scriptaculous JavaScript libraries in + # your application, pass :defaults as the source. When using + # :defaults, if an application.js file exists in + # public/javascripts it will be included as well. You can modify the + # HTML attributes of the script tag by passing a hash as the last argument. + # + # ==== Examples + # javascript_include_tag "xmlhr" # => + # + # + # javascript_include_tag "xmlhr.js" # => + # + # + # javascript_include_tag "common.javascript", "/elsewhere/cools" # => + # + # + # + # javascript_include_tag "http://www.railsapplication.com/xmlhr" # => + # + # + # javascript_include_tag "http://www.railsapplication.com/xmlhr.js" # => + # + # + # javascript_include_tag :defaults # => + # + # + # ... + # + # + # * = The application.js file is only referenced if it exists + # + # You can also include all javascripts in the +javascripts+ directory using :all as the source: + # + # javascript_include_tag :all # => + # + # + # ... + # + # + # + # + # Note that the default javascript files will be included first. So Prototype and Scriptaculous are available to + # all subsequently included files. + # + # If you want Rails to search in all the subdirectories under javascripts, you should explicitly set :recursive: + # + # javascript_include_tag :all, :recursive => true + # + # == Caching multiple javascripts into one + # + # You can also cache multiple javascripts into one file, which requires less HTTP connections to download and can better be + # compressed by gzip (leading to faster transfers). Caching will only happen if config.perform_caching + # is set to true (which is the case by default for the Rails production environment, but not for the development + # environment). + # + # ==== Examples + # javascript_include_tag :all, :cache => true # when config.perform_caching is false => + # + # + # ... + # + # + # + # + # javascript_include_tag :all, :cache => true # when config.perform_caching is true => + # + # + # javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when config.perform_caching is false => + # + # + # + # + # javascript_include_tag "prototype", "cart", "checkout", :cache => "shop" # when config.perform_caching is true => + # + # + # The :recursive option is also available for caching: + # + # javascript_include_tag :all, :cache => true, :recursive => true + def javascript_include_tag(*sources) + @javascript_include ||= JavascriptIncludeTag.new(config, asset_paths) + @javascript_include.include_tag(*sources) + end + + end + + end + end +end diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb new file mode 100644 index 0000000000..a48c87b49a --- /dev/null +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb @@ -0,0 +1,147 @@ +require 'active_support/concern' +require 'active_support/core_ext/file' +require 'action_view/helpers/tag_helper' +require 'action_view/helpers/asset_tag_helpers/asset_include_tag' + +module ActionView + module Helpers + module AssetTagHelper + + class StylesheetIncludeTag < AssetIncludeTag + include TagHelper + + def asset_name + 'stylesheet' + end + + def extension + 'css' + end + + def asset_tag(source, options) + tag("link", { "rel" => "stylesheet", "type" => Mime::CSS, "media" => "screen", "href" => ERB::Util.html_escape(path_to_asset(source)) }.merge(options), false, false) + end + + def custom_dir + config.stylesheets_dir + end + end + + + module StylesheetTagHelpers + extend ActiveSupport::Concern + + module ClassMethods + # Register one or more stylesheet files to be included when symbol + # is passed to stylesheet_link_tag. This method is typically intended + # to be called from plugin initialization to register stylesheet files + # that the plugin installed in public/stylesheets. + # + # ActionView::Helpers::AssetTagHelper.register_stylesheet_expansion :monkey => ["head", "body", "tail"] + # + # stylesheet_link_tag :monkey # => + # + # + # + def register_stylesheet_expansion(expansions) + style_expansions = StylesheetIncludeTag.expansions + expansions.each do |key, values| + style_expansions[key] = (style_expansions[key] || []) | Array(values) + end + end + end + + # Computes the path to a stylesheet asset in the public stylesheets directory. + # If the +source+ filename has no extension, .css will be appended (except for explicit URIs). + # Full paths from the document root will be passed through. + # Used internally by +stylesheet_link_tag+ to build the stylesheet path. + # + # ==== Examples + # stylesheet_path "style" # => /stylesheets/style.css + # stylesheet_path "dir/style.css" # => /stylesheets/dir/style.css + # stylesheet_path "/dir/style.css" # => /dir/style.css + # stylesheet_path "http://www.railsapplication.com/css/style" # => http://www.railsapplication.com/css/style + # stylesheet_path "http://www.railsapplication.com/css/style.css" # => http://www.railsapplication.com/css/style.css + def stylesheet_path(source) + asset_paths.compute_public_path(source, 'stylesheets', 'css') + end + alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route + + # Returns a stylesheet link tag for the sources specified as arguments. If + # you don't specify an extension, .css will be appended automatically. + # You can modify the link attributes by passing a hash as the last argument. + # + # ==== Examples + # stylesheet_link_tag "style" # => + # + # + # stylesheet_link_tag "style.css" # => + # + # + # stylesheet_link_tag "http://www.railsapplication.com/style.css" # => + # + # + # stylesheet_link_tag "style", :media => "all" # => + # + # + # stylesheet_link_tag "style", :media => "print" # => + # + # + # stylesheet_link_tag "random.styles", "/css/stylish" # => + # + # + # + # You can also include all styles in the stylesheets directory using :all as the source: + # + # stylesheet_link_tag :all # => + # + # + # + # + # If you want Rails to search in all the subdirectories under stylesheets, you should explicitly set :recursive: + # + # stylesheet_link_tag :all, :recursive => true + # + # == Caching multiple stylesheets into one + # + # You can also cache multiple stylesheets into one file, which requires less HTTP connections and can better be + # compressed by gzip (leading to faster transfers). Caching will only happen if config.perform_caching + # is set to true (which is the case by default for the Rails production environment, but not for the development + # environment). Examples: + # + # ==== Examples + # stylesheet_link_tag :all, :cache => true # when config.perform_caching is false => + # + # + # + # + # stylesheet_link_tag :all, :cache => true # when config.perform_caching is true => + # + # + # stylesheet_link_tag "shop", "cart", "checkout", :cache => "payment" # when config.perform_caching is false => + # + # + # + # + # stylesheet_link_tag "shop", "cart", "checkout", :cache => "payment" # when config.perform_caching is true => + # + # + # The :recursive option is also available for caching: + # + # stylesheet_link_tag :all, :cache => true, :recursive => true + # + # To force concatenation (even in development mode) set :concat to true. This is useful if + # you have too many stylesheets for IE to load. + # + # stylesheet_link_tag :all, :concat => true + # + def stylesheet_link_tag(*sources) + @stylesheet_include ||= StylesheetIncludeTag.new(config, asset_paths) + @stylesheet_include.include_tag(*sources) + end + + end + + end + end +end diff --git a/actionpack/lib/action_view/helpers/atom_feed_helper.rb b/actionpack/lib/action_view/helpers/atom_feed_helper.rb index 8e7cf2e701..db9d7a08ff 100644 --- a/actionpack/lib/action_view/helpers/atom_feed_helper.rb +++ b/actionpack/lib/action_view/helpers/atom_feed_helper.rb @@ -51,7 +51,7 @@ module ActionView # * :language: Defaults to "en-US". # * :root_url: The HTML alternative that this feed is doubling for. Defaults to / on the current host. # * :url: The URL for this feed. Defaults to the current URL. - # * :id: The id for this feed. Defaults to "tag:#{request.host},#{options[:schema_date]}:#{request.request_uri.split(".")[0]}" + # * :id: The id for this feed. Defaults to "tag:#{request.host},#{options[:schema_date]}:#{request.fullpath.split(".")[0]}" # * :schema_date: The date at which the tag scheme for the feed was first used. A good default is the year you # created the feed. See http://feedvalidator.org/docs/error/InvalidTAG.html for more information. If not specified, # 2005 is used (as an "I don't care" value). diff --git a/actionpack/lib/action_view/helpers/cache_helper.rb b/actionpack/lib/action_view/helpers/cache_helper.rb index f544a9d147..385378ea29 100644 --- a/actionpack/lib/action_view/helpers/cache_helper.rb +++ b/actionpack/lib/action_view/helpers/cache_helper.rb @@ -2,31 +2,29 @@ module ActionView # = Action View Cache Helper module Helpers module CacheHelper - # This helper to exposes a method for caching of view fragments. + # This helper exposes a method for caching fragments of a view + # rather than an entire action or page. This technique is useful + # caching pieces like menus, lists of newstopics, static HTML + # fragments, and so on. This method takes a block that contains + # the content you wish to cache. + # # See ActionController::Caching::Fragments for usage instructions. # - # A method for caching fragments of a view rather than an entire - # action or page. This technique is useful caching pieces like - # menus, lists of news topics, static HTML fragments, and so on. - # This method takes a block that contains the content you wish - # to cache. See ActionController::Caching::Fragments for more - # information. - # # ==== Examples - # If you wanted to cache a navigation menu, you could do the - # following. + # If you want to cache a navigation menu, you can do following: # # <% cache do %> # <%= render :partial => "menu" %> # <% end %> # - # You can also cache static content... + # You can also cache static content: # # <% cache do %> #

Hello users! Welcome to our website!

# <% end %> # - # ...and static content mixed with RHTML content. + # Static content with embedded ruby content can be cached as + # well: # # <% cache do %> # Topics: @@ -46,8 +44,8 @@ module ActionView private # TODO: Create an object that has caching read/write on it def fragment_for(name = {}, options = nil, &block) #:nodoc: - if controller.fragment_exist?(name, options) - controller.read_fragment(name, options) + if fragment = controller.read_fragment(name, options) + fragment else # VIEW TODO: Make #capture usable outside of ERB # This dance is needed because Builder can't use capture diff --git a/actionpack/lib/action_view/helpers/capture_helper.rb b/actionpack/lib/action_view/helpers/capture_helper.rb index 89e95e8694..c88bd1efd5 100644 --- a/actionpack/lib/action_view/helpers/capture_helper.rb +++ b/actionpack/lib/action_view/helpers/capture_helper.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/string/output_safety' module ActionView # = Action View Capture Helper @@ -38,7 +39,7 @@ module ActionView value = nil buffer = with_output_buffer { value = yield(*args) } if string = buffer.presence || value and string.is_a?(String) - NonConcattingString.new(string) + ERB::Util.html_escape string end end @@ -106,7 +107,7 @@ module ActionView # <%= javascript_include_tag :defaults %> # <% end %> # - # That will place # - # +html_options+ may be a hash of attributes for the # diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb index f11027bc93..05a9c5b4f1 100644 --- a/actionpack/lib/action_view/helpers/number_helper.rb +++ b/actionpack/lib/action_view/helpers/number_helper.rb @@ -1,6 +1,7 @@ require 'active_support/core_ext/big_decimal/conversions' require 'active_support/core_ext/float/rounding' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/string/output_safety' module ActionView # = Action View Number Helpers @@ -14,7 +15,7 @@ module ActionView # unchanged if can't be converted into a valid number. module NumberHelper - DEFAULT_CURRENCY_VALUES = { :format => "%u%n", :unit => "$", :separator => ".", :delimiter => ",", + DEFAULT_CURRENCY_VALUES = { :format => "%u%n", :negative_format => "-%u%n", :unit => "$", :separator => ".", :delimiter => ",", :precision => 2, :significant => false, :strip_insignificant_zeros => false } # Raised when argument +number+ param given to the helpers is invalid and @@ -47,51 +48,51 @@ module ActionView # number_to_phone(1235551234, :country_code => 1, :extension => 1343, :delimiter => ".") # => +1.123.555.1234 x 1343 def number_to_phone(number, options = {}) - return nil if number.nil? + return unless number begin Float(number) - is_number_html_safe = true rescue ArgumentError, TypeError - if options[:raise] - raise InvalidNumberError, number - else - is_number_html_safe = number.to_s.html_safe? - end - end + raise InvalidNumberError, number + end if options[:raise] number = number.to_s.strip options = options.symbolize_keys - area_code = options[:area_code] || nil + area_code = options[:area_code] delimiter = options[:delimiter] || "-" - extension = options[:extension].to_s.strip || nil - country_code = options[:country_code] || nil + extension = options[:extension] + country_code = options[:country_code] - str = "" - str << "+#{country_code}#{delimiter}" unless country_code.blank? - str << if area_code - number.gsub!(/([0-9]{1,3})([0-9]{3})([0-9]{4}$)/,"(\\1) \\2#{delimiter}\\3") + if area_code + number.gsub!(/(\d{1,3})(\d{3})(\d{4}$)/,"(\\1) \\2#{delimiter}\\3") else - number.gsub!(/([0-9]{0,3})([0-9]{3})([0-9]{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3") - number.starts_with?('-') ? number.slice!(1..-1) : number + number.gsub!(/(\d{0,3})(\d{3})(\d{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3") + number.slice!(0, 1) if number.starts_with?('-') end + + str = [] + str << "+#{country_code}#{delimiter}" unless country_code.blank? + str << number str << " x #{extension}" unless extension.blank? - is_number_html_safe ? str.html_safe : str + ERB::Util.html_escape(str.join) end # Formats a +number+ into a currency string (e.g., $13.65). You can customize the format # in the +options+ hash. # # ==== Options - # * :locale - Sets the locale to be used for formatting (defaults to current locale). - # * :precision - Sets the level of precision (defaults to 2). - # * :unit - Sets the denomination of the currency (defaults to "$"). - # * :separator - Sets the separator between the units (defaults to "."). - # * :delimiter - Sets the thousands delimiter (defaults to ","). - # * :format - Sets the format of the output string (defaults to "%u%n"). The field types are: - # - # %u The currency unit - # %n The number + # * :locale - Sets the locale to be used for formatting (defaults to current locale). + # * :precision - Sets the level of precision (defaults to 2). + # * :unit - Sets the denomination of the currency (defaults to "$"). + # * :separator - Sets the separator between the units (defaults to "."). + # * :delimiter - Sets the thousands delimiter (defaults to ","). + # * :format - Sets the format for non-negative numbers (defaults to "%u%n"). + # Fields are %u for the currency, and %n + # for the number. + # * :negative_format - Sets the format for negative numbers (defaults to prepending + # an hyphen to the formatted number given by :format). + # Accepts the same fields than :format, except + # %n is here the absolute value of the number. # # ==== Examples # number_to_currency(1234567890.50) # => $1,234,567,890.50 @@ -99,12 +100,14 @@ module ActionView # number_to_currency(1234567890.506, :precision => 3) # => $1,234,567,890.506 # number_to_currency(1234567890.506, :locale => :fr) # => 1 234 567 890,506 € # + # number_to_currency(1234567890.50, :negative_format => "(%u%n)") + # # => ($1,234,567,890.51) # number_to_currency(1234567890.50, :unit => "£", :separator => ",", :delimiter => "") # # => £1234567890,50 # number_to_currency(1234567890.50, :unit => "£", :separator => ",", :delimiter => "", :format => "%n %u") # # => 1234567890,50 £ def number_to_currency(number, options = {}) - return nil if number.nil? + return unless number options.symbolize_keys! @@ -112,11 +115,17 @@ module ActionView currency = I18n.translate(:'number.currency.format', :locale => options[:locale], :default => {}) defaults = DEFAULT_CURRENCY_VALUES.merge(defaults).merge!(currency) + defaults[:negative_format] = "-" + options[:format] if options[:format] options = defaults.merge!(options) unit = options.delete(:unit) format = options.delete(:format) + if number.to_f < 0 + format = options.delete(:negative_format) + number = number.respond_to?("abs") ? number.abs : number.sub(/^-/, '') + end + begin value = number_with_precision(number, options.merge(:raise => true)) format.gsub(/%n/, value).gsub(/%u/, unit).html_safe @@ -149,7 +158,7 @@ module ActionView # number_to_percentage(302.24398923423, :precision => 5) # => 302.24399% # number_to_percentage(1000, :locale => :fr) # => 1 000,000% def number_to_percentage(number, options = {}) - return nil if number.nil? + return unless number options.symbolize_keys! @@ -260,13 +269,14 @@ module ActionView if number == 0 digits, rounded_number = 1, 0 else - digits = (Math.log10(number) + 1).floor - rounded_number = BigDecimal.new((number / 10 ** (digits - precision)).to_s).round.to_f * 10 ** (digits - precision) + digits = (Math.log10(number.abs) + 1).floor + rounded_number = (BigDecimal.new(number.to_s) / BigDecimal.new((10 ** (digits - precision)).to_f.to_s)).round.to_f * 10 ** (digits - precision) + digits = (Math.log10(rounded_number.abs) + 1).floor # After rounding, the number of digits may have changed end - precision = precision - digits + precision -= digits precision = precision > 0 ? precision : 0 #don't let it be negative else - rounded_number = BigDecimal.new((number * (10 ** precision)).to_s).round.to_f / 10 ** precision + rounded_number = BigDecimal.new(number.to_s).round(precision).to_f end formatted_number = number_with_delimiter("%01.#{precision}f" % rounded_number, options) if strip_insignificant_zeros @@ -325,7 +335,7 @@ module ActionView defaults = I18n.translate(:'number.format', :locale => options[:locale], :default => {}) human = I18n.translate(:'number.human.format', :locale => options[:locale], :default => {}) defaults = defaults.merge(human) - + options = options.reverse_merge(defaults) #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros) @@ -359,7 +369,7 @@ module ActionView # See number_to_human_size if you want to print a file size. # # You can also define you own unit-quantifier names if you want to use other decimal units - # (eg.: 1500 becomes "1.5 kilometers", 0.150 becomes "150 mililiters", etc). You may define + # (eg.: 1500 becomes "1.5 kilometers", 0.150 becomes "150 milliliters", etc). You may define # a wide range of unit quantifiers, even fractional ones (centi, deci, mili, etc). # # ==== Options @@ -415,13 +425,13 @@ module ActionView # thousand: # one: "kilometer" # other: "kilometers" - # billion: "gazilion-distance" + # billion: "gazillion-distance" # # Then you could do: # # number_to_human(543934, :units => :distance) # => "544 kilometers" # number_to_human(54393498, :units => :distance) # => "54400 kilometers" - # number_to_human(54393498000, :units => :distance) # => "54.4 gazilion-distance" + # number_to_human(54393498000, :units => :distance) # => "54.4 gazillion-distance" # number_to_human(343, :units => :distance, :precision => 1) # => "300 meters" # number_to_human(1, :units => :distance) # => "1 meter" # number_to_human(0.34, :units => :distance) # => "34 centimeters" @@ -447,6 +457,8 @@ module ActionView #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros) + inverted_du = DECIMAL_UNITS.invert + units = options.delete :units unit_exponents = case units when Hash @@ -457,10 +469,10 @@ module ActionView I18n.translate(:"number.human.decimal_units.units", :locale => options[:locale], :raise => true) else raise ArgumentError, ":units must be a Hash or String translation scope." - end.keys.map{|e_name| DECIMAL_UNITS.invert[e_name] }.sort_by{|e| -e} + end.keys.map{|e_name| inverted_du[e_name] }.sort_by{|e| -e} - number_exponent = Math.log10(number).floor - display_exponent = unit_exponents.find{|e| number_exponent >= e } + number_exponent = number != 0 ? Math.log10(number.abs).floor : 0 + display_exponent = unit_exponents.find{ |e| number_exponent >= e } || 0 number /= 10 ** display_exponent unit = case units diff --git a/actionpack/lib/action_view/helpers/output_safety_helper.rb b/actionpack/lib/action_view/helpers/output_safety_helper.rb new file mode 100644 index 0000000000..a035dd70ad --- /dev/null +++ b/actionpack/lib/action_view/helpers/output_safety_helper.rb @@ -0,0 +1,38 @@ +require 'active_support/core_ext/string/output_safety' + +module ActionView #:nodoc: + # = Action View Raw Output Helper + module Helpers #:nodoc: + module OutputSafetyHelper + # This method outputs without escaping a string. Since escaping tags is + # now default, this can be used when you don't want Rails to automatically + # escape tags. This is not recommended if the data is coming from the user's + # input. + # + # For example: + # + # <%=raw @user.name %> + def raw(stringish) + stringish.to_s.html_safe + end + + # This method returns a html safe string similar to what Array#join + # would return. All items in the array, including the supplied separator, are + # html escaped unless they are html safe, and the returned string is marked + # as html safe. + # + # safe_join(["

foo

".html_safe, "

bar

"], "
") + # # => "

foo

<br /><p>bar</p>" + # + # safe_join(["

foo

".html_safe, "

bar

".html_safe], "
".html_safe) + # # => "

foo


bar

" + # + def safe_join(array, sep=$,) + sep ||= "".html_safe + sep = ERB::Util.html_escape(sep) + + array.map { |i| ERB::Util.html_escape(i) }.join(sep).html_safe + end + end + end +end \ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/prototype_helper.rb b/actionpack/lib/action_view/helpers/prototype_helper.rb index 99f9363a9a..18e303778c 100644 --- a/actionpack/lib/action_view/helpers/prototype_helper.rb +++ b/actionpack/lib/action_view/helpers/prototype_helper.rb @@ -1,6 +1,7 @@ require 'set' require 'active_support/json' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/string/output_safety' module ActionView # = Action View Prototype Helpers @@ -102,7 +103,7 @@ module ActionView :form, :with, :update, :script, :type ]).merge(CALLBACKS) # Returns the JavaScript needed for a remote function. - # See the link_to_remote documentation at http://github.com/rails/prototype_legacy_helper as it takes the same arguments. + # See the link_to_remote documentation at https://github.com/rails/prototype_legacy_helper as it takes the same arguments. # # Example: # # Generates: \n) expected << %(\n) expected << %(\n) - + assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :order => [:day]) end @@ -897,7 +897,7 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :discard_day => true, :start_year => 2003, :end_year => 2005, :prefix => "date[first]"}) end - + def test_select_date_with_separator_discard_month_and_day expected = %(\n" - expected << " — " + expected << " — " expected << %(\n" - expected << " — " + expected << " — " expected << %(\n) expected << %(\n) expected << %(\n) - + expected << %(\n" @@ -1158,7 +1158,7 @@ class DateHelperTest < ActionView::TestCase expected = %(\n) expected << %(\n) expected << %(\n) - + expected << %(\n" @@ -1182,7 +1182,7 @@ class DateHelperTest < ActionView::TestCase expected = %(\n) expected << %(\n) expected << %(\n) - + expected << %(\n" @@ -1206,7 +1206,7 @@ class DateHelperTest < ActionView::TestCase expected = %(\n) expected << %(\n) expected << %(\n) - + expected << %(\n" @@ -1229,11 +1229,11 @@ class DateHelperTest < ActionView::TestCase expected = %(\n) expected << %(\n) expected << %(\n) - + expected << %(\n" - + expected << " : " expected << %(\n) expected << %(\n) expected << %(\n) - + expected << %(\n" @@ -1497,26 +1497,6 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, date_select("post", "written_on", :order=>[:year, :month], :include_blank=>true) end - def test_date_select_with_nil_and_blank_and_order - @post = Post.new - - start_year = Time.now.year-5 - end_year = Time.now.year+5 - - expected = '' + "\n" - expected << %{\n" - - expected << %{\n" - - assert_dom_equal expected, date_select("post", "written_on", :order=>[:year, :month], :include_blank=>true) - end - def test_date_select_cant_override_discard_hour @post = Post.new @post.written_on = Date.new(2004, 6, 15) @@ -1604,6 +1584,47 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, date_select("post", "written_on", { :date_separator => " / " }) end + def test_date_select_with_separator_and_order + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = %{\n" + + expected << " / " + + expected << %{\n" + + expected << " / " + + expected << %{\n" + + assert_dom_equal expected, date_select("post", "written_on", { :order => [:day, :month, :year], :date_separator => " / " }) + end + + def test_date_select_with_separator_and_order_and_year_discarded + @post = Post.new + @post.written_on = Date.new(2004, 6, 15) + + expected = %{\n" + + expected << " / " + + expected << %{\n" + expected << %{\n} + + assert_dom_equal expected, date_select("post", "written_on", { :order => [:day, :month, :year], :discard_year => true, :date_separator => " / " }) + end + def test_date_select_with_default_prompt @post = Post.new @post.written_on = Date.new(2004, 6, 15) @@ -1861,10 +1882,17 @@ class DateHelperTest < ActionView::TestCase end def test_datetime_select_defaults_to_time_zone_now_when_config_time_zone_is_set - time = stub(:year => 2004, :month => 6, :day => 15, :hour => 16, :min => 35, :sec => 0) - time_zone = mock() - time_zone.expects(:now).returns time - Time.zone_default = time_zone + # The love zone is UTC+0 + mytz = Class.new(ActiveSupport::TimeZone) { + attr_accessor :now + }.create('tenderlove', 0) + + now = Time.mktime(2004, 6, 15, 16, 35, 0) + mytz.now = now + Time.zone = mytz + + assert_equal mytz, Time.zone + @post = Post.new expected = %{', text_field("post", "title") ) assert_dom_equal( - '', password_field("post", "title") + '', password_field("post", "title") + ) + assert_dom_equal( + '', password_field("post", "title", :value => @post.title) ) assert_dom_equal( '', password_field("person", "name") @@ -240,7 +256,7 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, hidden_field("post", "title", :value => nil) end - def test_text_field_with_options + def test_hidden_field_with_options assert_dom_equal '', hidden_field("post", "title", :value => "Something Else") end @@ -250,13 +266,6 @@ class FormHelperTest < ActionView::TestCase text_field("user", "email", :type => "email") end - def test_text_field_from_a_user_defined_method - @developer = Developer.new - assert_dom_equal( - '', text_field("developer", "name") - ) - end - def test_check_box assert_dom_equal( '', @@ -600,31 +609,91 @@ class FormHelperTest < ActionView::TestCase ) end + def test_form_for_requires_block + assert_raises(ArgumentError) do + form_for(:post, @post, :html => { :id => 'create-post' }) + end + end + def test_form_for - assert_deprecated do - form_for(:post, @post, :html => { :id => 'create-post' }) do |f| - concat f.label(:title) { "The Title" } - concat f.text_field(:title) - concat f.text_area(:body) - concat f.check_box(:secret) - concat f.submit('Create post') - end + form_for(@post, :html => { :id => 'create-post' }) do |f| + concat f.label(:title) { "The Title" } + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) + concat f.submit('Create post') end - expected = - "
" + - snowman + + expected = whole_form("/posts/123", "create-post" , "edit_post", :method => "put") do "" + "" + "" + "" + "" + - "" + - "
" + "" + end assert_dom_equal expected, output_buffer end + def test_form_for_with_file_field_generate_multipart + Post.send :attr_accessor, :file + + form_for(@post, :html => { :id => 'create-post' }) do |f| + concat f.file_field(:file) + end + + expected = whole_form("/posts/123", "create-post" , "edit_post", :method => "put", :multipart => true) do + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_fields_for_with_file_field_generate_multipart + Comment.send :attr_accessor, :file + + form_for(@post) do |f| + concat f.fields_for(:comment, @post) { |c| + concat c.file_field(:file) + } + end + + expected = whole_form("/posts/123", "edit_post_123" , "edit_post", :method => "put", :multipart => true) do + "" + end + + assert_dom_equal expected, output_buffer + end + + + def test_form_for_with_format + form_for(@post, :format => :json, :html => { :id => "edit_post_123", :class => "edit_post" }) do |f| + concat f.label(:title) + end + + expected = whole_form("/posts/123.json", "edit_post_123" , "edit_post", :method => "put") do + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_for_with_isolated_namespaced_model + form_for(@blog_post) do |f| + concat f.text_field :title + concat f.submit('Edit post') + end + + expected = + "
" + + snowman + + "" + + "" + + "" + + "
" + end + def test_form_for_with_symbol_object_name form_for(@post, :as => "other_name", :html => { :id => 'create-post' }) do |f| concat f.label(:title, :class => 'post_title') @@ -640,22 +709,20 @@ class FormHelperTest < ActionView::TestCase "" + "" + "" + - "" + "" end assert_dom_equal expected, output_buffer end def test_form_for_with_method - assert_deprecated do - form_for(:post, @post, :html => { :id => 'create-post', :method => :put }) do |f| - concat f.text_field(:title) - concat f.text_area(:body) - concat f.check_box(:secret) - end + form_for(@post, :url => '/', :html => { :id => 'create-post', :method => :put }) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) end - expected = whole_form("http://www.example.com", "create-post", nil, "put") do + expected = whole_form("/", "create-post", "edit_post", "put") do "" + "" + "" + @@ -665,16 +732,28 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end - def test_form_for_with_remote - assert_deprecated do - form_for(:post, @post, :remote => true, :html => { :id => 'create-post', :method => :put }) do |f| - concat f.text_field(:title) - concat f.text_area(:body) - concat f.check_box(:secret) - end + def test_form_for_with_search_field + # Test case for bug which would emit an "object" attribute + # when used with form_for using a search_field form helper + form_for(Post.new, :url => "/search", :html => { :id => 'search-post', :method => :get}) do |f| + concat f.search_field(:title) end - expected = whole_form("http://www.example.com", "create-post", nil, :method => "put", :remote => true) do + expected = whole_form("/search", "search-post", "new_post", "get") do + "" + end + + assert_dom_equal expected, output_buffer + end + + def test_form_for_with_remote + form_for(@post, :url => '/', :remote => true, :html => { :id => 'create-post', :method => :put }) 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", :method => "put", :remote => true) do "" + "" + "" + @@ -685,15 +764,14 @@ class FormHelperTest < ActionView::TestCase end def test_form_for_with_remote_without_html - assert_deprecated do - form_for(:post, @post, :remote => true) do |f| - concat f.text_field(:title) - concat f.text_area(:body) - concat f.check_box(:secret) - end + @post.persisted = false + form_for(@post, :remote => true) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) end - expected = whole_form("http://www.example.com", nil, nil, :remote => true) do + expected = whole_form("/posts", 'new_post', 'new_post', :remote => true) do "" + "" + "" + @@ -710,7 +788,7 @@ class FormHelperTest < ActionView::TestCase concat f.check_box(:secret) end - expected = whole_form("http://www.example.com", "create-post") do + expected = whole_form("/", "create-post") do "" + "" + "" + @@ -721,16 +799,14 @@ class FormHelperTest < ActionView::TestCase end def test_form_for_with_index - assert_deprecated do - form_for("post[]", @post) do |f| - concat f.label(:title) - concat f.text_field(:title) - concat f.text_area(:body) - concat f.check_box(:secret) - end + form_for(@post, :as => "post[]") do |f| + concat f.label(:title) + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) end - expected = whole_form do + expected = whole_form('/posts/123', 'post[]_edit', 'post[]_edit', 'put') do "" + "" + "" + @@ -742,15 +818,13 @@ class FormHelperTest < ActionView::TestCase end def test_form_for_with_nil_index_option_override - assert_deprecated do - form_for("post[]", @post, :index => nil) do |f| - concat f.text_field(:title) - concat f.text_area(:body) - concat f.check_box(:secret) - end + form_for(@post, :as => "post[]", :index => nil) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) end - expected = whole_form do + expected = whole_form('/posts/123', 'post[]_edit', 'post[]_edit', 'put') do "" + "" + "" + @@ -764,15 +838,13 @@ class FormHelperTest < ActionView::TestCase old_locale, I18n.locale = I18n.locale, :submit @post.persisted = false - assert_deprecated do - form_for(:post, @post) do |f| - concat f.submit - end + form_for(@post) do |f| + concat f.submit end - expected = whole_form do - "" - end + expected = whole_form('/posts', 'new_post', 'new_post') do + "" + end assert_dom_equal expected, output_buffer ensure @@ -782,15 +854,13 @@ class FormHelperTest < ActionView::TestCase def test_submit_with_object_as_existing_record_and_locale_strings old_locale, I18n.locale = I18n.locale, :submit - assert_deprecated do - form_for(:post, @post) do |f| - concat f.submit - end + form_for(@post) do |f| + concat f.submit end - expected = whole_form do - "" - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + "" + end assert_dom_equal expected, output_buffer ensure @@ -804,9 +874,9 @@ class FormHelperTest < ActionView::TestCase concat f.submit :class => "extra" end - expected = whole_form do - "" - end + expected = whole_form do + "" + end assert_dom_equal expected, output_buffer ensure @@ -816,15 +886,13 @@ class FormHelperTest < ActionView::TestCase def test_submit_with_object_and_nested_lookup old_locale, I18n.locale = I18n.locale, :submit - assert_deprecated do - form_for(:another_post, @post) do |f| - concat f.submit - end + form_for(@post, :as => :another_post) do |f| + concat f.submit end - expected = whole_form do - "" - end + expected = whole_form('/posts/123', 'another_post_edit', 'another_post_edit', :method => 'put') do + "" + end assert_dom_equal expected, output_buffer ensure @@ -832,188 +900,167 @@ class FormHelperTest < ActionView::TestCase end def test_nested_fields_for - assert_deprecated do - form_for(:post, @post) do |f| - concat f.fields_for(:comment, @post) { |c| - concat c.text_field(:title) - } - end + @comment.body = 'Hello World' + form_for(@post) do |f| + concat f.fields_for(@comment) { |c| + concat c.text_field(:body) + } end - expected = whole_form do - "" - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + "" + end assert_dom_equal expected, output_buffer end def test_nested_fields_for_with_nested_collections - assert_deprecated do - form_for('post[]', @post) do |f| - concat f.text_field(:title) - concat f.fields_for('comment[]', @comment) { |c| - concat c.text_field(:name) - } - end + form_for(@post, :as => 'post[]') do |f| + concat f.text_field(:title) + concat f.fields_for('comment[]', @comment) { |c| + concat c.text_field(:name) + } end - expected = whole_form do - "" + - "" - end + expected = whole_form('/posts/123', 'post[]_edit', 'post[]_edit', 'put') do + "" + + "" + end assert_dom_equal expected, output_buffer end def test_nested_fields_for_with_index_and_parent_fields - assert_deprecated do - form_for('post', @post, :index => 1) do |c| - concat c.text_field(:title) - concat c.fields_for('comment', @comment, :index => 1) { |r| - concat r.text_field(:name) - } - end + form_for(@post, :index => 1) do |c| + concat c.text_field(:title) + concat c.fields_for('comment', @comment, :index => 1) { |r| + concat r.text_field(:name) + } end - expected = whole_form do - "" + - "" - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', 'put') do + "" + + "" + end assert_dom_equal expected, output_buffer end def test_form_for_with_index_and_nested_fields_for - assert_deprecated do - output_buffer = form_for(:post, @post, :index => 1) do |f| - concat f.fields_for(:comment, @post) { |c| - concat c.text_field(:title) - } - end + output_buffer = form_for(@post, :index => 1) do |f| + concat f.fields_for(:comment, @post) { |c| + concat c.text_field(:title) + } end - expected = whole_form do - "" - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', 'put') do + "" + end assert_dom_equal expected, output_buffer end def test_nested_fields_for_with_index_on_both - assert_deprecated do - form_for(:post, @post, :index => 1) do |f| - concat f.fields_for(:comment, @post, :index => 5) { |c| - concat c.text_field(:title) - } - end + form_for(@post, :index => 1) do |f| + concat f.fields_for(:comment, @post, :index => 5) { |c| + concat c.text_field(:title) + } end - expected = whole_form do - "" - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', 'put') do + "" + end assert_dom_equal expected, output_buffer end def test_nested_fields_for_with_auto_index - assert_deprecated do - form_for("post[]", @post) do |f| - concat f.fields_for(:comment, @post) { |c| - concat c.text_field(:title) - } - end + form_for(@post, :as => "post[]") do |f| + concat f.fields_for(:comment, @post) { |c| + concat c.text_field(:title) + } end - expected = whole_form do - "" - end + expected = whole_form('/posts/123', 'post[]_edit', 'post[]_edit', 'put') do + "" + end assert_dom_equal expected, output_buffer end def test_nested_fields_for_with_index_radio_button - assert_deprecated do - form_for(:post, @post) do |f| - concat f.fields_for(:comment, @post, :index => 5) { |c| - concat c.radio_button(:title, "hello") - } - end + form_for(@post) do |f| + concat f.fields_for(:comment, @post, :index => 5) { |c| + concat c.radio_button(:title, "hello") + } end - expected = whole_form do - "" - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', 'put') do + "" + end assert_dom_equal expected, output_buffer end def test_nested_fields_for_with_auto_index_on_both - assert_deprecated do - form_for("post[]", @post) do |f| - concat f.fields_for("comment[]", @post) { |c| - concat c.text_field(:title) - } - end + form_for(@post, :as => "post[]") do |f| + concat f.fields_for("comment[]", @post) { |c| + concat c.text_field(:title) + } end - expected = whole_form do - "" - end + expected = whole_form('/posts/123', 'post[]_edit', 'post[]_edit', 'put') do + "" + end assert_dom_equal expected, output_buffer end def test_nested_fields_for_with_index_and_auto_index - assert_deprecated do - output_buffer = form_for("post[]", @post) do |f| - concat f.fields_for(:comment, @post, :index => 5) { |c| - concat c.text_field(:title) - } - end - - output_buffer << form_for(:post, @post, :index => 1) do |f| - concat f.fields_for("comment[]", @post) { |c| - concat c.text_field(:title) - } - end - - expected = whole_form do - "" - end + whole_form do - "" - end - - assert_dom_equal expected, output_buffer + output_buffer = form_for(@post, :as => "post[]") do |f| + concat f.fields_for(:comment, @post, :index => 5) { |c| + concat c.text_field(:title) + } end + + output_buffer << form_for(@post, :as => :post, :index => 1) do |f| + concat f.fields_for("comment[]", @post) { |c| + concat c.text_field(:title) + } + end + + expected = whole_form('/posts/123', 'post[]_edit', 'post[]_edit', 'put') do + "" + end + whole_form('/posts/123', 'post_edit', 'post_edit', 'put') do + "" + end + + assert_dom_equal expected, output_buffer end def test_nested_fields_for_with_a_new_record_on_a_nested_attributes_one_to_one_association @post.author = Author.new - assert_deprecated do - form_for(:post, @post) do |f| - concat f.text_field(:title) - concat f.fields_for(:author) { |af| - concat af.text_field(:name) - } - end + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + concat af.text_field(:name) + } end - expected = whole_form do - '' + - '' - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '' + + '' + end assert_dom_equal expected, output_buffer end def test_nested_fields_for_with_explicitly_passed_object_on_a_nested_attributes_one_to_one_association - assert_deprecated do - form_for(:post, @post) do |f| - f.fields_for(:author, Author.new(123)) do |af| - assert_not_nil af.object - assert_equal 123, af.object.id - end + form_for(@post) do |f| + f.fields_for(:author, Author.new(123)) do |af| + assert_not_nil af.object + assert_equal 123, af.object.id end end end @@ -1021,20 +1068,92 @@ class FormHelperTest < ActionView::TestCase def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association @post.author = Author.new(321) - assert_deprecated do - form_for(:post, @post) do |f| - concat f.text_field(:title) - concat f.fields_for(:author) { |af| - concat af.text_field(:name) - } - end + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + concat af.text_field(:name) + } end - expected = whole_form do - '' + - '' + - '' - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '' + + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_using_erb_and_inline_block + @post.author = Author.new(321) + + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + af.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '' + + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id + @post.author = Author.new(321) + + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:author, :include_id => false) { |af| + af.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id_inherited + @post.author = Author.new(321) + + form_for(@post, :include_id => false) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + af.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id_override + @post.author = Author.new(321) + + form_for(@post, :include_id => false) do |f| + concat f.text_field(:title) + concat f.fields_for(:author, :include_id => true) { |af| + af.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '' + + '' + + '' + end assert_dom_equal expected, output_buffer end @@ -1042,21 +1161,19 @@ class FormHelperTest < ActionView::TestCase def test_nested_fields_for_with_existing_records_on_a_nested_attributes_one_to_one_association_with_explicit_hidden_field_placement @post.author = Author.new(321) - assert_deprecated do - form_for(:post, @post) do |f| - concat f.text_field(:title) - concat f.fields_for(:author) { |af| - concat af.hidden_field(:id) - concat af.text_field(:name) - } - end + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + concat af.hidden_field(:id) + concat af.text_field(:name) + } end - expected = whole_form do - '' + - '' + - '' - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '' + + '' + + '' + end assert_dom_equal expected, output_buffer end @@ -1064,24 +1181,125 @@ class FormHelperTest < ActionView::TestCase def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association @post.comments = Array.new(2) { |id| Comment.new(id + 1) } - assert_deprecated do - form_for(:post, @post) do |f| - concat f.text_field(:title) - @post.comments.each do |comment| - concat f.fields_for(:comments, comment) { |cf| - concat cf.text_field(:name) - } - end + form_for(@post) do |f| + concat f.text_field(:title) + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + concat cf.text_field(:name) + } end end - expected = whole_form do - '' + - '' + - '' + - '' + - '' - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '' + + '' + + '' + + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + @post.author = Author.new(321) + + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + concat af.text_field(:name) + } + @post.comments.each do |comment| + concat f.fields_for(:comments, comment, :include_id => false) { |cf| + concat cf.text_field(:name) + } + end + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '' + + '' + + '' + + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id_inherited + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + @post.author = Author.new(321) + + form_for(@post, :include_id => false) do |f| + concat f.text_field(:title) + concat f.fields_for(:author) { |af| + concat af.text_field(:name) + } + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + concat cf.text_field(:name) + } + end + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '' + + '' + + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id_override + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + @post.author = Author.new(321) + + form_for(@post, :include_id => false) do |f| + concat f.text_field(:title) + concat f.fields_for(:author, :include_id => true) { |af| + concat af.text_field(:name) + } + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + concat cf.text_field(:name) + } + end + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '' + + '' + + '' + + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_using_erb_and_inline_block + @post.comments = Array.new(2) { |id| Comment.new(id + 1) } + + form_for(@post) do |f| + concat f.text_field(:title) + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + cf.text_field(:name) + } + end + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '' + + '' + + '' + + '' + + '' + end assert_dom_equal expected, output_buffer end @@ -1089,25 +1307,23 @@ class FormHelperTest < ActionView::TestCase def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_explicit_hidden_field_placement @post.comments = Array.new(2) { |id| Comment.new(id + 1) } - assert_deprecated do - form_for(:post, @post) do |f| - concat f.text_field(:title) - @post.comments.each do |comment| - concat f.fields_for(:comments, comment) { |cf| - concat cf.hidden_field(:id) - concat cf.text_field(:name) - } - end + form_for(@post) do |f| + concat f.text_field(:title) + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + concat cf.hidden_field(:id) + concat cf.text_field(:name) + } end end - expected = whole_form do - '' + - '' + - '' + - '' + - '' - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '' + + '' + + '' + + '' + + '' + end assert_dom_equal expected, output_buffer end @@ -1115,22 +1331,20 @@ class FormHelperTest < ActionView::TestCase def test_nested_fields_for_with_new_records_on_a_nested_attributes_collection_association @post.comments = [Comment.new, Comment.new] - assert_deprecated do - form_for(:post, @post) do |f| - concat f.text_field(:title) - @post.comments.each do |comment| - concat f.fields_for(:comments, comment) { |cf| - concat cf.text_field(:name) - } - end + form_for(@post) do |f| + concat f.text_field(:title) + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + concat cf.text_field(:name) + } end end - expected = whole_form do - '' + - '' + - '' - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '' + + '' + + '' + end assert_dom_equal expected, output_buffer end @@ -1138,40 +1352,36 @@ class FormHelperTest < ActionView::TestCase def test_nested_fields_for_with_existing_and_new_records_on_a_nested_attributes_collection_association @post.comments = [Comment.new(321), Comment.new] - assert_deprecated do - form_for(:post, @post) do |f| - concat f.text_field(:title) - @post.comments.each do |comment| - concat f.fields_for(:comments, comment) { |cf| - concat cf.text_field(:name) - } - end + form_for(@post) do |f| + concat f.text_field(:title) + @post.comments.each do |comment| + concat f.fields_for(:comments, comment) { |cf| + concat cf.text_field(:name) + } end end - expected = whole_form do - '' + - '' + - '' + - '' - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '' + + '' + + '' + + '' + end assert_dom_equal expected, output_buffer end def test_nested_fields_for_with_an_empty_supplied_attributes_collection - assert_deprecated do - form_for(:post, @post) do |f| - concat f.text_field(:title) - f.fields_for(:comments, []) do |cf| - concat cf.text_field(:name) - end + form_for(@post) do |f| + concat f.text_field(:title) + f.fields_for(:comments, []) do |cf| + concat cf.text_field(:name) end end - expected = whole_form do - '' - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '' + end assert_dom_equal expected, output_buffer end @@ -1179,22 +1389,41 @@ class FormHelperTest < ActionView::TestCase def test_nested_fields_for_with_existing_records_on_a_supplied_nested_attributes_collection @post.comments = Array.new(2) { |id| Comment.new(id + 1) } - assert_deprecated do - form_for(:post, @post) do |f| - concat f.text_field(:title) - concat f.fields_for(:comments, @post.comments) { |cf| - concat cf.text_field(:name) - } - end + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:comments, @post.comments) { |cf| + concat cf.text_field(:name) + } end - expected = whole_form do - '' + - '' + - '' + - '' + - '' - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '' + + '' + + '' + + '' + + '' + end + + assert_dom_equal expected, output_buffer + end + + def test_nested_fields_for_arel_like + @post.comments = ArelLike.new + + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:comments, @post.comments) { |cf| + concat cf.text_field(:name) + } + end + + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '' + + '' + + '' + + '' + + '' + end assert_dom_equal expected, output_buffer end @@ -1203,22 +1432,20 @@ class FormHelperTest < ActionView::TestCase comments = Array.new(2) { |id| Comment.new(id + 1) } @post.comments = [] - assert_deprecated do - form_for(:post, @post) do |f| - concat f.text_field(:title) - concat f.fields_for(:comments, comments) { |cf| - concat cf.text_field(:name) - } - end + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:comments, comments) { |cf| + concat cf.text_field(:name) + } end - expected = whole_form do - '' + - '' + - '' + - '' + - '' - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '' + + '' + + '' + + '' + + '' + end assert_dom_equal expected, output_buffer end @@ -1227,22 +1454,20 @@ class FormHelperTest < ActionView::TestCase @post.comments = [Comment.new(321), Comment.new] yielded_comments = [] - assert_deprecated do - form_for(:post, @post) do |f| - concat f.text_field(:title) - concat f.fields_for(:comments) { |cf| - concat cf.text_field(:name) - yielded_comments << cf.object - } - end + form_for(@post) do |f| + concat f.text_field(:title) + concat f.fields_for(:comments) { |cf| + concat cf.text_field(:name) + yielded_comments << cf.object + } end - expected = whole_form do - '' + - '' + - '' + - '' - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '' + + '' + + '' + + '' + end assert_dom_equal expected, output_buffer assert_equal yielded_comments, @post.comments @@ -1251,18 +1476,16 @@ class FormHelperTest < ActionView::TestCase def test_nested_fields_for_with_child_index_option_override_on_a_nested_attributes_collection_association @post.comments = [] - assert_deprecated do - form_for(:post, @post) do |f| - concat f.fields_for(:comments, Comment.new(321), :child_index => 'abc') { |cf| - concat cf.text_field(:name) - } - end + form_for(@post) do |f| + concat f.fields_for(:comments, Comment.new(321), :child_index => 'abc') { |cf| + concat cf.text_field(:name) + } end - expected = whole_form do - '' + - '' - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '' + + '' + end assert_dom_equal expected, output_buffer end @@ -1274,43 +1497,41 @@ class FormHelperTest < ActionView::TestCase @post.tags[0].relevances = [] @post.tags[1].relevances = [] - assert_deprecated do - form_for(:post, @post) do |f| - concat f.fields_for(:comments, @post.comments[0]) { |cf| - concat cf.text_field(:name) - concat cf.fields_for(:relevances, CommentRelevance.new(314)) { |crf| - concat crf.text_field(:value) - } + form_for(@post) do |f| + concat f.fields_for(:comments, @post.comments[0]) { |cf| + concat cf.text_field(:name) + concat cf.fields_for(:relevances, CommentRelevance.new(314)) { |crf| + concat crf.text_field(:value) } - concat f.fields_for(:tags, @post.tags[0]) { |tf| - concat tf.text_field(:value) - concat tf.fields_for(:relevances, TagRelevance.new(3141)) { |trf| - concat trf.text_field(:value) - } + } + concat f.fields_for(:tags, @post.tags[0]) { |tf| + concat tf.text_field(:value) + concat tf.fields_for(:relevances, TagRelevance.new(3141)) { |trf| + concat trf.text_field(:value) } - concat f.fields_for('tags', @post.tags[1]) { |tf| - concat tf.text_field(:value) - concat tf.fields_for(:relevances, TagRelevance.new(31415)) { |trf| - concat trf.text_field(:value) - } + } + concat f.fields_for('tags', @post.tags[1]) { |tf| + concat tf.text_field(:value) + concat tf.fields_for(:relevances, TagRelevance.new(31415)) { |trf| + concat trf.text_field(:value) } - end + } end - expected = whole_form do - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + end assert_dom_equal expected, output_buffer end @@ -1438,47 +1659,40 @@ class FormHelperTest < ActionView::TestCase end def test_form_for_and_fields_for - assert_deprecated do - form_for(:post, @post, :html => { :id => 'create-post' }) do |post_form| - concat post_form.text_field(:title) - concat post_form.text_area(:body) + form_for(@post, :as => :post, :html => { :id => 'create-post' }) do |post_form| + concat post_form.text_field(:title) + concat post_form.text_area(:body) - concat fields_for(:parent_post, @post) { |parent_fields| - concat parent_fields.check_box(:secret) - } - end + concat fields_for(:parent_post, @post) { |parent_fields| + concat parent_fields.check_box(:secret) + } end - expected = - "
" + - snowman + + expected = whole_form('/posts/123', 'create-post', 'post_edit', :method => 'put') do "" + "" + "" + - "" + - "
" + "" + end assert_dom_equal expected, output_buffer end def test_form_for_and_fields_for_with_object - assert_deprecated do - form_for(:post, @post, :html => { :id => 'create-post' }) do |post_form| - concat post_form.text_field(:title) - concat post_form.text_area(:body) + form_for(@post, :as => :post, :html => { :id => 'create-post' }) do |post_form| + concat post_form.text_field(:title) + concat post_form.text_area(:body) - concat post_form.fields_for(@comment) { |comment_fields| - concat comment_fields.text_field(:name) - } - end + concat post_form.fields_for(@comment) { |comment_fields| + concat comment_fields.text_field(:name) + } end - expected = - whole_form("http://www.example.com", "create-post") do - "" + - "" + - "" - end + expected = whole_form('/posts/123', 'create-post', 'post_edit', :method => 'put') do + "" + + "" + + "" + end assert_dom_equal expected, output_buffer end @@ -1494,63 +1708,63 @@ class FormHelperTest < ActionView::TestCase end def test_form_for_with_labelled_builder - assert_deprecated do - form_for(:post, @post, :builder => LabelledFormBuilder) do |f| - concat f.text_field(:title) - concat f.text_area(:body) - concat f.check_box(:secret) - end + form_for(@post, :builder => LabelledFormBuilder) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) end - expected = whole_form do - "
" + - "
" + - "
" - end + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do + "
" + + "
" + + "
" + end assert_dom_equal expected, output_buffer end def snowman(method = nil) txt = %{
} - txt << %{} - txt << %{} if method + txt << %{} + if (method && !['get','post'].include?(method.to_s)) + txt << %{} + end txt << %{
} end - def form_text(action = "http://www.example.com", id = nil, html_class = nil, remote = nil) + def form_text(action = "/", id = nil, html_class = nil, remote = nil, multipart = nil, method = nil) txt = %{
} + method = method.to_s == "get" ? "get" : "post" + txt << %{ method="#{method}">} end - def whole_form(action = "http://www.example.com", id = nil, html_class = nil, options = nil) + def whole_form(action = "/", id = nil, html_class = nil, options = nil) contents = block_given? ? yield : "" if options.is_a?(Hash) - method, remote = options.values_at(:method, :remote) + method, remote, multipart = options.values_at(:method, :remote, :multipart) else method = options end - form_text(action, id, html_class, remote) + snowman(method) + contents + "
" + form_text(action, id, html_class, remote, multipart, method) + snowman(method) + contents + "" end def test_default_form_builder old_default_form_builder, ActionView::Base.default_form_builder = ActionView::Base.default_form_builder, LabelledFormBuilder - assert_deprecated do - form_for(:post, @post) do |f| - concat f.text_field(:title) - concat f.text_area(:body) - concat f.check_box(:secret) - end + form_for(@post) do |f| + concat f.text_field(:title) + concat f.text_area(:body) + concat f.check_box(:secret) end - expected = whole_form do + expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do "
" + "
" + "
" @@ -1579,12 +1793,10 @@ class FormHelperTest < ActionView::TestCase def test_form_for_with_labelled_builder_with_nested_fields_for_without_options_hash klass = nil - assert_deprecated do - form_for(:post, @post, :builder => LabelledFormBuilder) do |f| - f.fields_for(:comments, Comment.new) do |nested_fields| - klass = nested_fields.class - '' - end + form_for(@post, :builder => LabelledFormBuilder) do |f| + f.fields_for(:comments, Comment.new) do |nested_fields| + klass = nested_fields.class + '' end end @@ -1594,12 +1806,10 @@ class FormHelperTest < ActionView::TestCase def test_form_for_with_labelled_builder_with_nested_fields_for_with_options_hash klass = nil - assert_deprecated do - form_for(:post, @post, :builder => LabelledFormBuilder) do |f| - f.fields_for(:comments, Comment.new, :index => 'foo') do |nested_fields| - klass = nested_fields.class - '' - end + form_for(@post, :builder => LabelledFormBuilder) do |f| + f.fields_for(:comments, Comment.new, :index => 'foo') do |nested_fields| + klass = nested_fields.class + '' end end @@ -1611,12 +1821,10 @@ class FormHelperTest < ActionView::TestCase def test_form_for_with_labelled_builder_with_nested_fields_for_with_custom_builder klass = nil - assert_deprecated do - form_for(:post, @post, :builder => LabelledFormBuilder) do |f| - f.fields_for(:comments, Comment.new, :builder => LabelledFormBuilderSubclass) do |nested_fields| - klass = nested_fields.class - '' - end + form_for(@post, :builder => LabelledFormBuilder) do |f| + f.fields_for(:comments, Comment.new, :builder => LabelledFormBuilderSubclass) do |nested_fields| + klass = nested_fields.class + '' end end @@ -1624,39 +1832,29 @@ class FormHelperTest < ActionView::TestCase end def test_form_for_with_html_options_adds_options_to_form_tag - assert_deprecated do - form_for(:post, @post, :html => {:id => 'some_form', :class => 'some_class'}) do |f| end - end - expected = whole_form("http://www.example.com", "some_form", "some_class") + form_for(@post, :html => {:id => 'some_form', :class => 'some_class'}) do |f| end + expected = whole_form("/posts/123", "some_form", "some_class", 'put') assert_dom_equal expected, output_buffer end def test_form_for_with_string_url_option - assert_deprecated do - form_for(:post, @post, :url => 'http://www.otherdomain.com') do |f| end - end + form_for(@post, :url => 'http://www.otherdomain.com') do |f| end - assert_equal whole_form("http://www.otherdomain.com"), output_buffer - # assert_equal '
', output_buffer + assert_equal whole_form("http://www.otherdomain.com", 'edit_post_123', 'edit_post', 'put'), output_buffer end def test_form_for_with_hash_url_option - assert_deprecated do - form_for(:post, @post, :url => {:controller => 'controller', :action => 'action'}) do |f| end - end + form_for(@post, :url => {:controller => 'controller', :action => 'action'}) do |f| end assert_equal 'controller', @url_for_options[:controller] assert_equal 'action', @url_for_options[:action] end def test_form_for_with_record_url_option - assert_deprecated do - form_for(:post, @post, :url => @post) do |f| end - end + form_for(@post, :url => @post) do |f| end - expected = whole_form("/posts/123") - # expected = "
" + expected = whole_form("/posts/123", 'edit_post_123', 'edit_post', 'put') assert_equal expected, output_buffer end @@ -1682,14 +1880,14 @@ class FormHelperTest < ActionView::TestCase @comment.save form_for([@post, @comment]) {} - expected = whole_form(comment_path(@post, @comment), "edit_comment_1", "edit_comment", "put") + expected = whole_form(post_comment_path(@post, @comment), "edit_comment_1", "edit_comment", "put") assert_dom_equal expected, output_buffer end def test_form_for_with_new_object_in_list form_for([@post, @comment]) {} - expected = whole_form(comments_path(@post), "new_comment", "new_comment") + expected = whole_form(post_comments_path(@post), "new_comment", "new_comment") assert_dom_equal expected, output_buffer end @@ -1697,14 +1895,14 @@ class FormHelperTest < ActionView::TestCase @comment.save form_for([:admin, @post, @comment]) {} - expected = whole_form(admin_comment_path(@post, @comment), "edit_comment_1", "edit_comment", "put") + expected = whole_form(admin_post_comment_path(@post, @comment), "edit_comment_1", "edit_comment", "put") assert_dom_equal expected, output_buffer end def test_form_for_with_new_object_and_namespace_in_list form_for([:admin, @post, @comment]) {} - expected = whole_form(admin_comments_path(@post), "new_comment", "new_comment") + expected = whole_form(admin_post_comments_path(@post), "new_comment", "new_comment") assert_dom_equal expected, output_buffer end @@ -1721,35 +1919,8 @@ class FormHelperTest < ActionView::TestCase end protected - def comments_path(post) - "/posts/#{post.id}/comments" - end - alias_method :post_comments_path, :comments_path - - def comment_path(post, comment) - "/posts/#{post.id}/comments/#{comment.id}" - end - alias_method :post_comment_path, :comment_path - - def admin_comments_path(post) - "/admin/posts/#{post.id}/comments" - end - alias_method :admin_post_comments_path, :admin_comments_path - - def admin_comment_path(post, comment) - "/admin/posts/#{post.id}/comments/#{comment.id}" - end - alias_method :admin_post_comment_path, :admin_comment_path - - def posts_path - "/posts" - end - - def post_path(post) - "/posts/#{post.id}" - end - def protect_against_forgery? false end + end diff --git a/actionpack/test/template/form_options_helper_test.rb b/actionpack/test/template/form_options_helper_test.rb index d14e5020c7..93ff7ba0fd 100644 --- a/actionpack/test/template/form_options_helper_test.rb +++ b/actionpack/test/template/form_options_helper_test.rb @@ -1,13 +1,19 @@ require 'abstract_unit' require 'tzinfo' +class Map < Hash + def category + "" + end +end + TZInfo::Timezone.cattr_reader :loaded_zones class FormOptionsHelperTest < ActionView::TestCase tests ActionView::Helpers::FormOptionsHelper silence_warnings do - Post = Struct.new('Post', :title, :author_name, :body, :secret, :written_on, :category, :origin) + Post = Struct.new('Post', :title, :author_name, :body, :secret, :written_on, :category, :origin, :allow_comments) Continent = Struct.new('Continent', :continent_name, :countries) Country = Struct.new('Country', :country_id, :country_name) Firm = Struct.new('Firm', :time_zone) @@ -130,6 +136,13 @@ class FormOptionsHelperTest < ActionView::TestCase ) end + def test_boolean_array_options_for_select_with_selection_and_disabled_value + assert_dom_equal( + "\n", + options_for_select([ true, false ], :selected => false, :disabled => nil) + ) + end + def test_array_options_for_string_include_in_other_string_bug_fix assert_dom_equal( "\n", @@ -176,6 +189,68 @@ class FormOptionsHelperTest < ActionView::TestCase ) end + def test_collection_options_with_preselected_value_as_string_and_option_value_is_integer + albums = [ Album.new(1, "first","rap"), Album.new(2, "second","pop")] + assert_dom_equal( + %(\n), + options_from_collection_for_select(albums, "id", "genre", :selected => "1") + ) + end + + def test_collection_options_with_preselected_value_as_integer_and_option_value_is_string + albums = [ Album.new("1", "first","rap"), Album.new("2", "second","pop")] + + assert_dom_equal( + %(\n), + options_from_collection_for_select(albums, "id", "genre", :selected => 1) + ) + end + + def test_collection_options_with_preselected_value_as_string_and_option_value_is_float + albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")] + + assert_dom_equal( + %(\n), + options_from_collection_for_select(albums, "id", "genre", :selected => "2.0") + ) + end + + def test_collection_options_with_preselected_value_as_nil + albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")] + + assert_dom_equal( + %(\n), + options_from_collection_for_select(albums, "id", "genre", :selected => nil) + ) + end + + def test_collection_options_with_disabled_value_as_nil + albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")] + + assert_dom_equal( + %(\n), + options_from_collection_for_select(albums, "id", "genre", :disabled => nil) + ) + end + + def test_collection_options_with_disabled_value_as_array + albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")] + + assert_dom_equal( + %(\n), + options_from_collection_for_select(albums, "id", "genre", :disabled => ["1.0", 2.0]) + ) + end + + def test_collection_options_with_preselected_values_as_string_array_and_option_value_is_float + albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop"), Album.new(3.0, "third","country") ] + + assert_dom_equal( + %(\n\n), + options_from_collection_for_select(albums, "id", "genre", ["1.0","3.0"]) + ) + end + def test_option_groups_from_collection_for_select assert_dom_equal( "\n\n", @@ -289,6 +364,10 @@ class FormOptionsHelperTest < ActionView::TestCase opts end + def test_time_zone_options_returns_html_safe_string + assert time_zone_options_for_select.html_safe? + end + def test_select @post = Post.new @post.category = "" @@ -298,6 +377,15 @@ class FormOptionsHelperTest < ActionView::TestCase ) end + def test_select_with_boolean_method + @post = Post.new + @post.allow_comments = false + assert_dom_equal( + "", + select("post", "allow_comments", %w( true false )) + ) + end + def test_select_under_fields_for @post = Post.new @post.category = "" @@ -305,13 +393,26 @@ class FormOptionsHelperTest < ActionView::TestCase output_buffer = fields_for :post, @post do |f| concat f.select(:category, %w( abe hest)) end - + assert_dom_equal( "", output_buffer ) end + def test_fields_for_with_record_inherited_from_hash + map = Map.new + + output_buffer = fields_for :map, map do |f| + concat f.select(:category, %w( abe hest)) + end + + assert_dom_equal( + "", + output_buffer + ) + end + def test_select_under_fields_for_with_index @post = Post.new @post.category = "" @@ -438,11 +539,11 @@ class FormOptionsHelperTest < ActionView::TestCase def test_select_with_index_option @album = Album.new @album.id = 1 - - expected = "" + + expected = "" assert_dom_equal( - expected, + expected, select("album[]", "genre", %w[rap rock country], {}, { :index => nil }) ) end @@ -491,7 +592,7 @@ class FormOptionsHelperTest < ActionView::TestCase output_buffer = fields_for :post, @post do |f| concat f.collection_select(:author_name, dummy_posts, :author_name, :author_name) end - + assert_dom_equal( "", output_buffer @@ -599,7 +700,7 @@ class FormOptionsHelperTest < ActionView::TestCase output_buffer = fields_for :firm, @firm do |f| concat f.time_zone_select(:time_zone) end - + assert_dom_equal( "} - txt << %{} if method + txt << %{} + if (method && !['get','post'].include?(method.to_s)) + txt << %{} + end txt << %{
} end def form_text(action = "http://www.example.com", options = {}) - remote, enctype, html_class, id = options.values_at(:remote, :enctype, :html_class, :id) + remote, enctype, html_class, id, method = options.values_at(:remote, :enctype, :html_class, :id, :method) + + method = method.to_s == "get" ? "get" : "post" txt = %{
} + txt << %{ method="#{method}">} end def whole_form(action = "http://www.example.com", options = {}) @@ -201,12 +205,6 @@ class FormTagHelperTest < ActionView::TestCase assert_dom_equal expected, actual end - def test_select_tag_with_array_options - assert_deprecated /array/ do - select_tag "people", [""] - end - end - def test_text_area_tag_size_string actual = text_area_tag "body", "hello world", "size" => "20x40" expected = %() @@ -387,6 +385,57 @@ class FormTagHelperTest < ActionView::TestCase ) end + def test_button_tag + assert_dom_equal( + %(), + button_tag + ) + end + + def test_button_tag_with_submit_type + assert_dom_equal( + %(), + button_tag("Save", :type => "submit") + ) + end + + def test_button_tag_with_button_type + assert_dom_equal( + %(), + button_tag("Button", :type => "button") + ) + end + + def test_button_tag_with_reset_type + assert_dom_equal( + %(), + button_tag("Reset", :type => "reset") + ) + end + + def test_button_tag_with_disabled_option + assert_dom_equal( + %(), + button_tag("Reset", :type => "reset", :disabled => true) + ) + end + + def test_button_tag_escape_content + assert_dom_equal( + %(), + button_tag("Reset", :type => "reset", :disabled => true) + ) + end + + def test_button_tag_with_block + assert_dom_equal('', button_tag { 'Content' }) + end + + def test_button_tag_with_block_and_options + output = button_tag(:name => 'temptation', :type => 'button') { content_tag(:strong, 'Do not press me') } + assert_dom_equal('', output) + end + def test_image_submit_tag_with_confirmation assert_dom_equal( %(), diff --git a/actionpack/test/template/html-scanner/document_test.rb b/actionpack/test/template/html-scanner/document_test.rb index c68f04fa75..ddfb351595 100644 --- a/actionpack/test/template/html-scanner/document_test.rb +++ b/actionpack/test/template/html-scanner/document_test.rb @@ -15,7 +15,7 @@ class DocumentTest < Test::Unit::TestCase assert_match %r{\s+}m, doc.root.children[1].content assert_equal "html", doc.root.children[2].name end - + def test_find_img doc = HTML::Document.new <<-HTML.strip diff --git a/actionpack/test/template/html-scanner/node_test.rb b/actionpack/test/template/html-scanner/node_test.rb index b0df36877e..f4b9b198e8 100644 --- a/actionpack/test/template/html-scanner/node_test.rb +++ b/actionpack/test/template/html-scanner/node_test.rb @@ -1,39 +1,39 @@ require 'abstract_unit' class NodeTest < Test::Unit::TestCase - + class MockNode def initialize(matched, value) @matched = matched @value = value end - + def find(conditions) @matched && self end - + def to_s @value.to_s end end - + def setup @node = HTML::Node.new("parent") @node.children.concat [MockNode.new(false,1), MockNode.new(true,"two"), MockNode.new(false,:three)] end - + def test_match assert !@node.match("foo") end - + def test_tag assert !@node.tag? end - + def test_to_s assert_equal "1twothree", @node.to_s end - + def test_find assert_equal "two", @node.find('blah').to_s end @@ -58,7 +58,7 @@ class NodeTest < Test::Unit::TestCase assert node.attributes.has_key?("bar") assert "", node.to_s end - + def test_parse_with_unclosed_tag s = "on my mind\nall day long") - assert_equal "0wn3d", sanitizer.sanitize("0wn3d") - assert_equal "Magic", sanitizer.sanitize("Magic") - assert_equal "FrrFox", sanitizer.sanitize("FrrFox") + assert_equal "0wn3d", sanitizer.sanitize("0wn3d") + assert_equal "Magic", sanitizer.sanitize("Magic") + assert_equal "FrrFox", sanitizer.sanitize("FrrFox") assert_equal "My mind\nall day long", sanitizer.sanitize("My mind\nall day long") assert_equal "all day long", sanitizer.sanitize("<a href='hello'>all day long</a>") @@ -58,7 +58,7 @@ class SanitizerTest < ActionController::TestCase raw = %{href="javascript:bang" foo, bar} assert_sanitized raw, %{href="javascript:bang" foo, bar} end - + def test_sanitize_image_src raw = %{src="javascript:bang" foo, bar} assert_sanitized raw, %{src="javascript:bang" foo, bar} @@ -130,6 +130,13 @@ class SanitizerTest < ActionController::TestCase assert sanitizer.send(:contains_bad_protocols?, 'src', "#{proto}://bad") end end + + def test_should_accept_good_protocols_ignoring_case + sanitizer = HTML::WhiteListSanitizer.new + HTML::WhiteListSanitizer.allowed_protocols.each do |proto| + assert !sanitizer.send(:contains_bad_protocols?, 'src', "#{proto.capitalize}://good") + end + end def test_should_accept_good_protocols sanitizer = HTML::WhiteListSanitizer.new @@ -147,9 +154,9 @@ class SanitizerTest < ActionController::TestCase assert_sanitized %(), "" end - [%(), - %(), - %(), + [%(), + %(), + %(), %(">), %(), %(), @@ -166,28 +173,28 @@ class SanitizerTest < ActionController::TestCase assert_sanitized img_hack, "" end end - + def test_should_sanitize_tag_broken_up_by_null assert_sanitized %(alert(\"XSS\")), "alert(\"XSS\")" end - + def test_should_sanitize_invalid_script_tag assert_sanitized %(), "" end - + def test_should_sanitize_script_tag_with_multiple_open_brackets assert_sanitized %(<), "<" assert_sanitized %(