Merge branch 'bzr/golem' of /Users/distler/Sites/code/instiki

This commit is contained in:
Jacques Distler 2009-08-04 15:45:56 -05:00
commit c14c1f3c5a
257 changed files with 4346 additions and 1682 deletions

View file

@ -58,7 +58,7 @@ class AdminControllerTest < ActionController::TestCase
process 'create_system', 'password' => 'a_password', 'web_name' => 'My Wiki', process 'create_system', 'password' => 'a_password', 'web_name' => 'My Wiki',
'web_address' => 'my_wiki' 'web_address' => 'my_wiki'
assert_redirected_to :web => @wiki.webs.keys.first, :action => 'show', :id => 'HomePage' assert_redirected_to :web => @wiki.webs.keys.first, :controller => 'wiki', :action => 'show', :id => 'HomePage'
assert_equal wiki_before, @wiki assert_equal wiki_before, @wiki
# and no new web should be created either # and no new web should be created either
assert_equal old_size, @wiki.webs.size assert_equal old_size, @wiki.webs.size
@ -68,7 +68,7 @@ class AdminControllerTest < ActionController::TestCase
def test_create_system_no_form_and_wiki_already_initialized def test_create_system_no_form_and_wiki_already_initialized
assert @wiki.setup? assert @wiki.setup?
process('create_system') process('create_system')
assert_redirected_to :web => @wiki.webs.keys.first, :action => 'show', :id => 'HomePage' assert_redirected_to :web => @wiki.webs.keys.first, :controller => 'wiki', :action => 'show', :id => 'HomePage'
assert(@response.has_flash_object?(:error)) assert(@response.has_flash_object?(:error))
end end
@ -78,7 +78,7 @@ class AdminControllerTest < ActionController::TestCase
process 'create_web', 'system_password' => 'pswd', 'name' => 'Wiki Two', 'address' => 'wiki2' process 'create_web', 'system_password' => 'pswd', 'name' => 'Wiki Two', 'address' => 'wiki2'
assert_redirected_to :web => 'wiki2', :action => 'new', :id => 'HomePage' assert_redirected_to :web => 'wiki2', :controller => 'wiki', :action => 'new', :id => 'HomePage'
wiki2 = @wiki.webs['wiki2'] wiki2 = @wiki.webs['wiki2']
assert wiki2 assert wiki2
assert_equal 'Wiki Two', wiki2.name assert_equal 'Wiki Two', wiki2.name
@ -90,7 +90,7 @@ class AdminControllerTest < ActionController::TestCase
process 'create_web', 'system_password' => 'instiki', 'name' => 'Wiki Two', 'address' => 'wiki2' process 'create_web', 'system_password' => 'instiki', 'name' => 'Wiki Two', 'address' => 'wiki2'
assert_redirected_to :web => 'wiki2', :action => 'new', :id => 'HomePage' assert_redirected_to :web => 'wiki2', :controller => 'wiki', :action => 'new', :id => 'HomePage'
end end
def test_create_web_failed_authentication def test_create_web_failed_authentication
@ -98,7 +98,7 @@ class AdminControllerTest < ActionController::TestCase
process 'create_web', 'system_password' => 'wrong', 'name' => 'Wiki Two', 'address' => 'wiki2' process 'create_web', 'system_password' => 'wrong', 'name' => 'Wiki Two', 'address' => 'wiki2'
assert_redirected_to :web => nil, :action => 'create_web' assert_redirected_to :controller => 'admin', :action => 'create_web'
assert_nil @wiki.webs['wiki2'] assert_nil @wiki.webs['wiki2']
end end
@ -108,7 +108,6 @@ class AdminControllerTest < ActionController::TestCase
assert_response :success assert_response :success
end end
def test_edit_web_no_form def test_edit_web_no_form
process 'edit_web', 'web' => 'wiki1' process 'edit_web', 'web' => 'wiki1'
# this action simply renders a form # this action simply renders a form
@ -125,7 +124,7 @@ class AdminControllerTest < ActionController::TestCase
'brackets_only' => 'on', 'count_pages' => 'on', 'allow_uploads' => 'on', 'brackets_only' => 'on', 'count_pages' => 'on', 'allow_uploads' => 'on',
'max_upload_size' => '300') 'max_upload_size' => '300')
assert_redirected_to :web => 'renamed_wiki1', :action => 'show', :id => 'HomePage' assert_redirected_to :web => 'renamed_wiki1', :controller => 'wiki', :action => 'show', :id => 'HomePage'
@web = Web.find(@web.id) @web = Web.find(@web.id)
assert_equal 'renamed_wiki1', @web.address assert_equal 'renamed_wiki1', @web.address
assert_equal 'Renamed Wiki1', @web.name assert_equal 'Renamed Wiki1', @web.name
@ -164,7 +163,7 @@ class AdminControllerTest < ActionController::TestCase
# safe_mode, published, brackets_only, count_pages, allow_uploads not set # safe_mode, published, brackets_only, count_pages, allow_uploads not set
# and should become false # and should become false
assert_redirected_to :web => 'renamed_wiki1', :action => 'show', :id => 'HomePage' assert_redirected_to :web => 'renamed_wiki1', :controller => 'wiki', :action => 'show', :id => 'HomePage'
@web = Web.find(@web.id) @web = Web.find(@web.id)
assert !@web.safe_mode? assert !@web.safe_mode?
assert !@web.published? assert !@web.published?
@ -237,7 +236,7 @@ class AdminControllerTest < ActionController::TestCase
# third pass does not destroy HomePage # third pass does not destroy HomePage
r = process('remove_orphaned_pages', 'web' => 'wiki1', 'system_password_orphaned' => 'pswd') r = process('remove_orphaned_pages', 'web' => 'wiki1', 'system_password_orphaned' => 'pswd')
assert_redirected_to :action => 'list' assert_redirected_to :web => 'wiki1', :controller => 'wiki', :action => 'list'
@web.pages(true) @web.pages(true)
assert_equal page_order, @web.select.sort, assert_equal page_order, @web.select.sort,
"Pages are not as expected: #{@web.select.sort.map {|p| p.name}.inspect}" "Pages are not as expected: #{@web.select.sort.map {|p| p.name}.inspect}"
@ -271,14 +270,14 @@ class AdminControllerTest < ActionController::TestCase
# third pass does does nothing, since there are no pages in the # third pass does does nothing, since there are no pages in the
# 'leaves' category. # 'leaves' category.
r = process('remove_orphaned_pages_in_category', 'web' => 'wiki1', 'category' => 'leaves', 'system_password_orphaned_in_category' => 'pswd') r = process('remove_orphaned_pages_in_category', 'web' => 'wiki1', 'category' => 'leaves', 'system_password_orphaned_in_category' => 'pswd')
assert_redirected_to :action => 'list' assert_redirected_to :controller => 'wiki', :web => 'wiki1', :action => 'list'
@web.pages(true) @web.pages(true)
assert_equal page_order, @web.select.sort, assert_equal page_order, @web.select.sort,
"Pages are not as expected: #{@web.select.sort.map {|p| p.name}.inspect}" "Pages are not as expected: #{@web.select.sort.map {|p| p.name}.inspect}"
# fourth pass destroys Oak # fourth pass destroys Oak
r = process('remove_orphaned_pages_in_category', 'web' => 'wiki1', 'category' => 'trees', 'system_password_orphaned_in_category' => 'pswd') r = process('remove_orphaned_pages_in_category', 'web' => 'wiki1', 'category' => 'trees', 'system_password_orphaned_in_category' => 'pswd')
assert_redirected_to :action => 'list' assert_redirected_to :controller => 'wiki', :web => 'wiki1', :action => 'list'
@web.pages(true) @web.pages(true)
page_order.delete(@oak) page_order.delete(@oak)
assert_equal page_order, @web.select.sort, assert_equal page_order, @web.select.sort,

View file

@ -39,7 +39,7 @@ class WikiControllerTest < ActionController::TestCase
set_web_property :password, 'pswd' set_web_property :password, 'pswd'
get :authenticate, :web => 'wiki1', :password => 'pswd' get :authenticate, :web => 'wiki1', :password => 'pswd'
assert_redirected_to :web => 'wiki1', :action => 'show', :id => 'HomePage' assert_redirected_to :web => 'wiki1', :controller => 'wiki', :action => 'show', :id => 'HomePage'
assert_equal 'pswd', @response.cookies['wiki1'] assert_equal 'pswd', @response.cookies['wiki1']
end end
@ -47,7 +47,7 @@ class WikiControllerTest < ActionController::TestCase
set_web_property :password, 'pswd' set_web_property :password, 'pswd'
r = process('authenticate', 'web' => 'wiki1', 'password' => 'wrong password') r = process('authenticate', 'web' => 'wiki1', 'password' => 'wrong password')
assert_redirected_to :action => 'login', :web => 'wiki1' assert_redirected_to :action => 'login', :controller => 'wiki', :web => 'wiki1'
assert_nil r.cookies['web_address'] assert_nil r.cookies['web_address']
end end
@ -72,7 +72,7 @@ class WikiControllerTest < ActionController::TestCase
r = process('cancel_edit', 'web' => 'wiki1', 'id' => 'Oak') r = process('cancel_edit', 'web' => 'wiki1', 'id' => 'Oak')
assert_redirected_to :action => 'show', :id => 'Oak' assert_redirected_to :web => 'wiki1', :controller => 'wiki', :action => 'show', :id => 'Oak'
assert !Page.find(@oak.id).locked?(Time.now) assert !Page.find(@oak.id).locked?(Time.now)
end end
@ -85,7 +85,7 @@ class WikiControllerTest < ActionController::TestCase
def test_edit_page_locked_page def test_edit_page_locked_page
@home.lock(Time.now, 'Locky') @home.lock(Time.now, 'Locky')
process 'edit', 'web' => 'wiki1', 'id' => 'HomePage' process 'edit', 'web' => 'wiki1', 'id' => 'HomePage'
assert_redirected_to :action => 'locked' assert_redirected_to :web => 'wiki1', :controller => 'wiki', :action => 'locked', :id => 'HomePage'
end end
def test_edit_page_break_lock def test_edit_page_break_lock
@ -233,19 +233,19 @@ class WikiControllerTest < ActionController::TestCase
# delete extra web fixture # delete extra web fixture
webs(:instiki).destroy webs(:instiki).destroy
process('index') process('index')
assert_redirected_to :web => 'wiki1', :action => 'show', :id => 'HomePage' assert_redirected_to :web => 'wiki1', :controller => 'wiki', :action => 'show', :id => 'HomePage'
end end
def test_index_multiple_webs def test_index_multiple_webs
@wiki.create_web('Test Wiki 2', 'wiki2') @wiki.create_web('Test Wiki 2', 'wiki2')
process('index') process('index')
assert_redirected_to :action => 'web_list' assert_redirected_to :controller => 'wiki', :action => 'web_list'
end end
def test_index_multiple_webs_web_explicit def test_index_multiple_webs_web_explicit
@wiki.create_web('Test Wiki 2', 'wiki2') @wiki.create_web('Test Wiki 2', 'wiki2')
process('index', 'web' => 'wiki2') process('index', 'web' => 'wiki2')
assert_redirected_to :web => 'wiki2', :action => 'show', :id => 'HomePage' assert_redirected_to :web => 'wiki2', :controller => 'wiki', :action => 'show', :id => 'HomePage'
end end
def test_index_wiki_not_initialized def test_index_wiki_not_initialized
@ -598,7 +598,7 @@ class WikiControllerTest < ActionController::TestCase
r = process 'save', 'web' => 'wiki1', 'id' => 'NewPage', 'content' => 'Contents of a new page', r = process 'save', 'web' => 'wiki1', 'id' => 'NewPage', 'content' => 'Contents of a new page',
'author' => 'AuthorOfNewPage' 'author' => 'AuthorOfNewPage'
assert_redirected_to :web => 'wiki1', :action => 'show', :id => 'NewPage' assert_redirected_to :web => 'wiki1', :controller => 'wiki', :action => 'show', :id => 'NewPage'
assert_equal 'AuthorOfNewPage', r.cookies['author'] assert_equal 'AuthorOfNewPage', r.cookies['author']
assert_match @eternity, r.headers["Set-Cookie"][0] assert_match @eternity, r.headers["Set-Cookie"][0]
new_page = @wiki.read_page('wiki1', 'NewPage') new_page = @wiki.read_page('wiki1', 'NewPage')
@ -610,7 +610,7 @@ class WikiControllerTest < ActionController::TestCase
r = process 'save', 'web' => 'wiki1', 'id' => 'NewPage', 'content' => "Contents of a new page\r\n\000", r = process 'save', 'web' => 'wiki1', 'id' => 'NewPage', 'content' => "Contents of a new page\r\n\000",
'author' => 'AuthorOfNewPage' 'author' => 'AuthorOfNewPage'
assert_redirected_to :web => 'wiki1', :action => 'new', :id => 'NewPage', :content => '' assert_redirected_to :web => 'wiki1', :controller => 'wiki', :action => 'new', :id => 'NewPage', :content => ''
assert_equal 'AuthorOfNewPage', r.cookies['author'] assert_equal 'AuthorOfNewPage', r.cookies['author']
assert_match @eternity, r.headers["Set-Cookie"][0] assert_match @eternity, r.headers["Set-Cookie"][0]
end end
@ -619,7 +619,7 @@ class WikiControllerTest < ActionController::TestCase
r = process 'save', 'web' => 'wiki1', 'id' => 'NewPage', 'content' => "Contents of a new page\r\n&#xfffe;", r = process 'save', 'web' => 'wiki1', 'id' => 'NewPage', 'content' => "Contents of a new page\r\n&#xfffe;",
'author' => 'AuthorOfNewPage' 'author' => 'AuthorOfNewPage'
assert_redirected_to :web => 'wiki1', :action => 'new', :id => 'NewPage' assert_redirected_to :web => 'wiki1', :controller => 'wiki', :action => 'new', :id => 'NewPage', :content => ''
assert_equal 'AuthorOfNewPage', r.cookies['author'] assert_equal 'AuthorOfNewPage', r.cookies['author']
assert_match @eternity, r.headers["Set-Cookie"][0] assert_match @eternity, r.headers["Set-Cookie"][0]
end end
@ -628,7 +628,7 @@ class WikiControllerTest < ActionController::TestCase
r = process 'save', 'web' => 'wiki1', 'id' => 'NewPage', 'content' => "Contents of a new page\r\n&#65535;", r = process 'save', 'web' => 'wiki1', 'id' => 'NewPage', 'content' => "Contents of a new page\r\n&#65535;",
'author' => 'AuthorOfNewPage' 'author' => 'AuthorOfNewPage'
assert_redirected_to :web => 'wiki1', :action => 'new', :id => 'NewPage' assert_redirected_to :web => 'wiki1', :controller => 'wiki', :action => 'new', :id => 'NewPage', :content => ''
assert_equal 'AuthorOfNewPage', r.cookies['author'] assert_equal 'AuthorOfNewPage', r.cookies['author']
assert_match @eternity, r.headers["Set-Cookie"][0] assert_match @eternity, r.headers["Set-Cookie"][0]
end end
@ -640,7 +640,7 @@ class WikiControllerTest < ActionController::TestCase
r = process 'save', 'web' => 'wiki1', 'id' => 'HomePage', 'content' => 'Revised HomePage', r = process 'save', 'web' => 'wiki1', 'id' => 'HomePage', 'content' => 'Revised HomePage',
'author' => 'Batman' 'author' => 'Batman'
assert_redirected_to :web => 'wiki1', :action => 'show', :id => 'HomePage' assert_redirected_to :web => 'wiki1', :controller => 'wiki', :action => 'show', :id => 'HomePage'
assert_equal 'Batman', r.cookies['author'] assert_equal 'Batman', r.cookies['author']
home_page = @wiki.read_page('wiki1', 'HomePage') home_page = @wiki.read_page('wiki1', 'HomePage')
assert_equal current_revisions+1, home_page.revisions.size assert_equal current_revisions+1, home_page.revisions.size
@ -656,7 +656,7 @@ class WikiControllerTest < ActionController::TestCase
r = process 'save', 'web' => 'wiki1', 'id' => 'HomePage', 'content' => "Revised HomePage\000", r = process 'save', 'web' => 'wiki1', 'id' => 'HomePage', 'content' => "Revised HomePage\000",
'author' => 'Batman' 'author' => 'Batman'
assert_redirected_to :web => 'wiki1', :action => 'edit', :id => 'HomePage', assert_redirected_to :web => 'wiki1', :controller => 'wiki', :action => 'edit', :id => 'HomePage',
:content => 'HisWay would be MyWay $\sin(x)\begin{svg}<svg/>\end{svg}\includegraphics[width' + :content => 'HisWay would be MyWay $\sin(x)\begin{svg}<svg/>\end{svg}\includegraphics[width' +
'=3em]{foo}$ in kinda ThatWay in HisWay though MyWay \OverThere -- see SmartEng' + '=3em]{foo}$ in kinda ThatWay in HisWay though MyWay \OverThere -- see SmartEng' +
'ine in that SmartEngineGUI' 'ine in that SmartEngineGUI'
@ -688,7 +688,10 @@ class WikiControllerTest < ActionController::TestCase
r = process 'save', {'web' => 'wiki1', 'id' => 'HomePage', r = process 'save', {'web' => 'wiki1', 'id' => 'HomePage',
'content' => @home.revisions.last.content.dup + "\n Try viagra.\n", 'content' => @home.revisions.last.content.dup + "\n Try viagra.\n",
'author' => 'SomeOtherAuthor'}, {:return_to => '/wiki1/show/HomePage'} 'author' => 'SomeOtherAuthor'}, {:return_to => '/wiki1/show/HomePage'}
assert_redirected_to :action => 'edit', :web => 'wiki1', :id => 'HomePage' assert_redirected_to :action => 'edit', :controller => 'wiki', :web => 'wiki1', :id => 'HomePage',
:content => 'HisWay would be MyWay $\sin(x)\begin{svg}<svg/>\end{svg}\includegraphics[width=3e'+
'm]{foo}$ in kinda ThatWay in HisWay though MyWay \OverThere -- see SmartEngine in'+
" that SmartEngineGUI\n Try viagra.\n"
assert r.flash[:error].to_s == "Your edit was blocked by spam filtering" assert r.flash[:error].to_s == "Your edit was blocked by spam filtering"
end end
@ -700,7 +703,10 @@ class WikiControllerTest < ActionController::TestCase
'content' => @home.revisions.last.content.dup, 'content' => @home.revisions.last.content.dup,
'author' => 'SomeOtherAuthor'}, {:return_to => '/wiki1/show/HomePage'} 'author' => 'SomeOtherAuthor'}, {:return_to => '/wiki1/show/HomePage'}
assert_redirected_to :action => 'edit', :web => 'wiki1', :id => 'HomePage' assert_redirected_to :action => 'edit', :controller => 'wiki', :web => 'wiki1', :id => 'HomePage',
:content => 'HisWay would be MyWay $\sin(x)\begin{svg}<svg/>\end{svg}\includegraphics[width=3e'+
'm]{foo}$ in kinda ThatWay in HisWay though MyWay \OverThere -- see SmartEngine in'+
' that SmartEngineGUI'
assert r.flash[:error].to_s == "You have tried to save page 'HomePage' without changing its content" assert r.flash[:error].to_s == "You have tried to save page 'HomePage' without changing its content"
revisions_after = @home.revisions.size revisions_after = @home.revisions.size
@ -717,7 +723,7 @@ class WikiControllerTest < ActionController::TestCase
'content' => @liquor.revisions.last.content.dup, 'new_name' => 'booze', 'content' => @liquor.revisions.last.content.dup, 'new_name' => 'booze',
'author' => 'SomeOtherAuthor'}, {:return_to => '/wiki1/show/booze'} 'author' => 'SomeOtherAuthor'}, {:return_to => '/wiki1/show/booze'}
assert_redirected_to :action => 'show', :web => 'wiki1', :id => 'booze' assert_redirected_to :action => 'show', :controller => 'wiki', :web => 'wiki1', :id => 'booze'
revisions_after = @liquor.revisions.size revisions_after = @liquor.revisions.size
assert_equal revisions_before + 1, revisions_after assert_equal revisions_before + 1, revisions_after
@ -741,20 +747,20 @@ class WikiControllerTest < ActionController::TestCase
def test_save_invalid_author_name def test_save_invalid_author_name
r = process 'save', 'web' => 'wiki1', 'id' => 'NewPage', 'content' => 'Contents of a new page', r = process 'save', 'web' => 'wiki1', 'id' => 'NewPage', 'content' => 'Contents of a new page',
'author' => 'foo.bar' 'author' => 'foo.bar'
assert_redirected_to :action => 'new', :web => 'wiki1', :id => 'NewPage' assert_redirected_to :action => 'new', :controller => 'wiki', :web => 'wiki1', :id => 'NewPage'
assert r.flash[:error].to_s == 'Your name cannot contain a "."' assert r.flash[:error].to_s == 'Your name cannot contain a "."'
r = process 'save', 'web' => 'wiki1', 'id' => 'AnotherPage', 'content' => 'Contents of a new page', r = process 'save', 'web' => 'wiki1', 'id' => 'AnotherPage', 'content' => 'Contents of a new page',
'author' => "\000" 'author' => "\000"
assert_redirected_to :action => 'new', :web => 'wiki1', :id => 'AnotherPage' assert_redirected_to :action => 'new', :controller => 'wiki', :web => 'wiki1', :id => 'AnotherPage'
assert r.flash[:error].to_s == "Your name was not valid utf-8" assert r.flash[:error].to_s == "Your name was not valid utf-8"
end end
def test_search def test_search
r = process 'search', 'web' => 'wiki1', 'query' => '\s[A-Z]ak' r = process 'search', 'web' => 'wiki1', 'query' => '\s[A-Z]ak'
assert_redirected_to :action => 'show', :id => 'Oak' assert_redirected_to :web => 'wiki1', :action => 'show', :controller => 'wiki', :id => 'Oak'
end end
def test_search_multiple_results def test_search_multiple_results
@ -873,7 +879,7 @@ class WikiControllerTest < ActionController::TestCase
def test_show_page_nonexistant_page def test_show_page_nonexistant_page
process('show', 'id' => 'UnknownPage', 'web' => 'wiki1') process('show', 'id' => 'UnknownPage', 'web' => 'wiki1')
assert_redirected_to :web => 'wiki1', :action => 'new', :id => 'UnknownPage' assert_redirected_to :web => 'wiki1', :controller => 'wiki', :action => 'new', :id => 'UnknownPage'
end end
def test_show_no_page def test_show_no_page

View file

@ -1,3 +1,7 @@
*2.3.3 (July 12, 2009)*
* No changes, just a version bump.
*2.3.2 [Final] (March 15, 2009)* *2.3.2 [Final] (March 15, 2009)*
* Fixed that ActionMailer should send correctly formatted Return-Path in MAIL FROM for SMTP #1842 [Matt Jones] * Fixed that ActionMailer should send correctly formatted Return-Path in MAIL FROM for SMTP #1842 [Matt Jones]

View file

@ -4,7 +4,6 @@ require 'rake/testtask'
require 'rake/rdoctask' require 'rake/rdoctask'
require 'rake/packagetask' require 'rake/packagetask'
require 'rake/gempackagetask' require 'rake/gempackagetask'
require 'rake/contrib/sshpublisher'
require File.join(File.dirname(__FILE__), 'lib', 'action_mailer', 'version') require File.join(File.dirname(__FILE__), 'lib', 'action_mailer', 'version')
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : '' PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
@ -55,7 +54,7 @@ spec = Gem::Specification.new do |s|
s.rubyforge_project = "actionmailer" s.rubyforge_project = "actionmailer"
s.homepage = "http://www.rubyonrails.org" s.homepage = "http://www.rubyonrails.org"
s.add_dependency('actionpack', '= 2.3.2' + PKG_BUILD) s.add_dependency('actionpack', '= 2.3.3' + PKG_BUILD)
s.has_rdoc = true s.has_rdoc = true
s.requirements << 'none' s.requirements << 'none'
@ -76,12 +75,14 @@ end
desc "Publish the API documentation" desc "Publish the API documentation"
task :pgem => [:package] do task :pgem => [:package] do
require 'rake/contrib/sshpublisher'
Rake::SshFilePublisher.new("gems.rubyonrails.org", "/u/sites/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload Rake::SshFilePublisher.new("gems.rubyonrails.org", "/u/sites/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
`ssh gems.rubyonrails.org '/u/sites/gems/gemupdate.sh'` `ssh gems.rubyonrails.org '/u/sites/gems/gemupdate.sh'`
end end
desc "Publish the API documentation" desc "Publish the API documentation"
task :pdoc => [:rdoc] do task :pdoc => [:rdoc] do
require 'rake/contrib/sshpublisher'
Rake::SshDirPublisher.new("wrath.rubyonrails.org", "public_html/am", "doc").upload Rake::SshDirPublisher.new("wrath.rubyonrails.org", "public_html/am", "doc").upload
end end

View file

@ -674,7 +674,7 @@ module ActionMailer #:nodoc:
def perform_delivery_smtp(mail) def perform_delivery_smtp(mail)
destinations = mail.destinations destinations = mail.destinations
mail.ready_to_send mail.ready_to_send
sender = (mail['return-path'] && mail['return-path'].spec) || mail.from sender = (mail['return-path'] && mail['return-path'].spec) || mail['from']
smtp = Net::SMTP.new(smtp_settings[:address], smtp_settings[:port]) smtp = Net::SMTP.new(smtp_settings[:address], smtp_settings[:port])
smtp.enable_starttls_auto if smtp_settings[:enable_starttls_auto] && smtp.respond_to?(:enable_starttls_auto) smtp.enable_starttls_auto if smtp_settings[:enable_starttls_auto] && smtp.respond_to?(:enable_starttls_auto)

View file

@ -2,7 +2,7 @@ module ActionMailer
module VERSION #:nodoc: module VERSION #:nodoc:
MAJOR = 2 MAJOR = 2
MINOR = 3 MINOR = 3
TINY = 2 TINY = 3
STRING = [MAJOR, MINOR, TINY].join('.') STRING = [MAJOR, MINOR, TINY].join('.')
end end

View file

@ -1,9 +1,6 @@
require 'rubygems' require 'rubygems'
require 'test/unit' require 'test/unit'
gem 'mocha', '>= 0.9.5'
require 'mocha'
$:.unshift "#{File.dirname(__FILE__)}/../lib" $:.unshift "#{File.dirname(__FILE__)}/../lib"
$:.unshift "#{File.dirname(__FILE__)}/../../activesupport/lib" $:.unshift "#{File.dirname(__FILE__)}/../../activesupport/lib"
$:.unshift "#{File.dirname(__FILE__)}/../../actionpack/lib" $:.unshift "#{File.dirname(__FILE__)}/../../actionpack/lib"

View file

@ -1,3 +1,8 @@
*2.3.3 (July 12, 2009)*
* Fixed that TestResponse.cookies was returning cookies unescaped #1867 [Doug McInnes]
*2.3.2 [Final] (March 15, 2009)* *2.3.2 [Final] (March 15, 2009)*
* Fixed that redirection would just log the options, not the final url (which lead to "Redirected to #<Post:0x23150b8>") [DHH] * Fixed that redirection would just log the options, not the final url (which lead to "Redirected to #<Post:0x23150b8>") [DHH]

View file

@ -4,7 +4,6 @@ require 'rake/testtask'
require 'rake/rdoctask' require 'rake/rdoctask'
require 'rake/packagetask' require 'rake/packagetask'
require 'rake/gempackagetask' require 'rake/gempackagetask'
require 'rake/contrib/sshpublisher'
require File.join(File.dirname(__FILE__), 'lib', 'action_pack', 'version') require File.join(File.dirname(__FILE__), 'lib', 'action_pack', 'version')
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : '' PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
@ -80,7 +79,8 @@ spec = Gem::Specification.new do |s|
s.has_rdoc = true s.has_rdoc = true
s.requirements << 'none' s.requirements << 'none'
s.add_dependency('activesupport', '= 2.3.2' + PKG_BUILD) s.add_dependency('activesupport', '= 2.3.3' + PKG_BUILD)
s.add_dependency('rack', '~> 1.0.0')
s.require_path = 'lib' s.require_path = 'lib'
s.autorequire = 'action_controller' s.autorequire = 'action_controller'
@ -136,12 +136,14 @@ task :update_js => [ :update_scriptaculous ]
desc "Publish the API documentation" desc "Publish the API documentation"
task :pgem => [:package] do task :pgem => [:package] do
require 'rake/contrib/sshpublisher'
Rake::SshFilePublisher.new("gems.rubyonrails.org", "/u/sites/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload Rake::SshFilePublisher.new("gems.rubyonrails.org", "/u/sites/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
`ssh gems.rubyonrails.org '/u/sites/gems/gemupdate.sh'` `ssh gems.rubyonrails.org '/u/sites/gems/gemupdate.sh'`
end end
desc "Publish the API documentation" desc "Publish the API documentation"
task :pdoc => [:rdoc] do task :pdoc => [:rdoc] do
require 'rake/contrib/sshpublisher'
Rake::SshDirPublisher.new("wrath.rubyonrails.org", "public_html/ap", "doc").upload Rake::SshDirPublisher.new("wrath.rubyonrails.org", "public_html/ap", "doc").upload
end end

View file

@ -32,10 +32,10 @@ rescue LoadError
end end
begin begin
gem 'rack', '~> 1.0.0' gem 'rack', '~> 1.1.0'
require 'rack' require 'rack'
rescue Gem::LoadError rescue Gem::LoadError
require 'action_controller/vendor/rack-1.0/rack' require 'action_controller/vendor/rack-1.1.pre/rack'
end end
module ActionController module ActionController
@ -45,7 +45,6 @@ module ActionController
[Base, CGIHandler, CgiRequest, Request, Response, Http::Headers, UrlRewriter, UrlWriter] [Base, CGIHandler, CgiRequest, Request, Response, Http::Headers, UrlRewriter, UrlWriter]
end end
autoload :AbstractRequest, 'action_controller/request'
autoload :Base, 'action_controller/base' autoload :Base, 'action_controller/base'
autoload :Benchmarking, 'action_controller/benchmarking' autoload :Benchmarking, 'action_controller/benchmarking'
autoload :Caching, 'action_controller/caching' autoload :Caching, 'action_controller/caching'

View file

@ -63,7 +63,10 @@ module ActionController
# Support partial arguments for hash redirections # Support partial arguments for hash redirections
if options.is_a?(Hash) && @response.redirected_to.is_a?(Hash) if options.is_a?(Hash) && @response.redirected_to.is_a?(Hash)
return true if options.all? {|(key, value)| @response.redirected_to[key] == value} if options.all? {|(key, value)| @response.redirected_to[key] == value}
::ActiveSupport::Deprecation.warn("Using assert_redirected_to with partial hash arguments is deprecated. Specify the full set arguments instead", caller)
return true
end
end end
redirected_to_after_normalisation = normalize_argument_to_redirection(@response.redirected_to) redirected_to_after_normalisation = normalize_argument_to_redirection(@response.redirected_to)
@ -82,6 +85,9 @@ module ActionController
# # assert that the "new" view template was rendered # # assert that the "new" view template was rendered
# assert_template "new" # assert_template "new"
# #
# # assert that the "new" view template was rendered with Symbol
# assert_template :new
#
# # assert that the "_customer" partial was rendered twice # # assert that the "_customer" partial was rendered twice
# assert_template :partial => '_customer', :count => 2 # assert_template :partial => '_customer', :count => 2
# #
@ -91,7 +97,7 @@ module ActionController
def assert_template(options = {}, message = nil) def assert_template(options = {}, message = nil)
clean_backtrace do clean_backtrace do
case options case options
when NilClass, String when NilClass, String, Symbol
rendered = @response.rendered[:template].to_s rendered = @response.rendered[:template].to_s
msg = build_message(message, msg = build_message(message,
"expecting <?> but rendering with <?>", "expecting <?> but rendering with <?>",
@ -100,7 +106,7 @@ module ActionController
if options.nil? if options.nil?
@response.rendered[:template].blank? @response.rendered[:template].blank?
else else
rendered.to_s.match(options) rendered.to_s.match(options.to_s)
end end
end end
when Hash when Hash
@ -123,6 +129,8 @@ module ActionController
assert @response.rendered[:partials].empty?, assert @response.rendered[:partials].empty?,
"Expected no partials to be rendered" "Expected no partials to be rendered"
end end
else
raise ArgumentError
end end
end end
end end

View file

@ -491,6 +491,10 @@ module ActionController #:nodoc:
filtered_parameters[key] = '[FILTERED]' filtered_parameters[key] = '[FILTERED]'
elsif value.is_a?(Hash) elsif value.is_a?(Hash)
filtered_parameters[key] = filter_parameters(value) filtered_parameters[key] = filter_parameters(value)
elsif value.is_a?(Array)
filtered_parameters[key] = value.collect do |item|
filter_parameters(item)
end
elsif block_given? elsif block_given?
key = key.dup key = key.dup
value = value.dup if value value = value.dup if value
@ -950,8 +954,9 @@ module ActionController #:nodoc:
response.content_type ||= Mime::JS response.content_type ||= Mime::JS
render_for_text(js, options[:status]) render_for_text(js, options[:status])
elsif json = options[:json] elsif options.include?(:json)
json = json.to_json unless json.is_a?(String) json = options[:json]
json = ActiveSupport::JSON.encode(json) unless json.is_a?(String)
json = "#{options[:callback]}(#{json})" unless options[:callback].blank? json = "#{options[:callback]}(#{json})" unless options[:callback].blank?
response.content_type ||= Mime::JSON response.content_type ||= Mime::JSON
render_for_text(json, options[:status]) render_for_text(json, options[:status])

View file

@ -27,7 +27,7 @@ module ActionController #:nodoc:
autoload :Actions, 'action_controller/caching/actions' autoload :Actions, 'action_controller/caching/actions'
autoload :Fragments, 'action_controller/caching/fragments' autoload :Fragments, 'action_controller/caching/fragments'
autoload :Pages, 'action_controller/caching/pages' autoload :Pages, 'action_controller/caching/pages'
autoload :Sweeper, 'action_controller/caching/sweeping' autoload :Sweeper, 'action_controller/caching/sweeper'
autoload :Sweeping, 'action_controller/caching/sweeping' autoload :Sweeping, 'action_controller/caching/sweeping'
def self.included(base) #:nodoc: def self.included(base) #:nodoc:

View file

@ -61,7 +61,9 @@ module ActionController #:nodoc:
filter_options = { :only => actions, :if => options.delete(:if), :unless => options.delete(:unless) } filter_options = { :only => actions, :if => options.delete(:if), :unless => options.delete(:unless) }
cache_filter = ActionCacheFilter.new(:layout => options.delete(:layout), :cache_path => options.delete(:cache_path), :store_options => options) cache_filter = ActionCacheFilter.new(:layout => options.delete(:layout), :cache_path => options.delete(:cache_path), :store_options => options)
around_filter(cache_filter, filter_options) around_filter(filter_options) do |controller, action|
cache_filter.filter(controller, action)
end
end end
end end
@ -83,6 +85,12 @@ module ActionController #:nodoc:
@options = options @options = options
end end
def filter(controller, action)
should_continue = before(controller)
action.call if should_continue
after(controller)
end
def before(controller) def before(controller)
cache_path = ActionCachePath.new(controller, path_options_for(controller, @options.slice(:cache_path))) cache_path = ActionCachePath.new(controller, path_options_for(controller, @options.slice(:cache_path)))
if cache = controller.read_fragment(cache_path.path, @options[:store_options]) if cache = controller.read_fragment(cache_path.path, @options[:store_options])

View file

@ -0,0 +1,45 @@
require 'active_record'
module ActionController #:nodoc:
module Caching
class Sweeper < ActiveRecord::Observer #:nodoc:
attr_accessor :controller
def before(controller)
self.controller = controller
callback(:before) if controller.perform_caching
end
def after(controller)
callback(:after) if controller.perform_caching
# Clean up, so that the controller can be collected after this request
self.controller = nil
end
protected
# gets the action cache path for the given options.
def action_path_for(options)
ActionController::Caching::Actions::ActionCachePath.path_for(controller, options)
end
# Retrieve instance variables set in the controller.
def assigns(key)
controller.instance_variable_get("@#{key}")
end
private
def callback(timing)
controller_callback_method_name = "#{timing}_#{controller.controller_name.underscore}"
action_callback_method_name = "#{controller_callback_method_name}_#{controller.action_name}"
__send__(controller_callback_method_name) if respond_to?(controller_callback_method_name, true)
__send__(action_callback_method_name) if respond_to?(action_callback_method_name, true)
end
def method_missing(method, *arguments, &block)
return if @controller.nil?
@controller.__send__(method, *arguments, &block)
end
end
end
end

View file

@ -51,47 +51,5 @@ module ActionController #:nodoc:
end end
end end
end end
if defined?(ActiveRecord) and defined?(ActiveRecord::Observer)
class Sweeper < ActiveRecord::Observer #:nodoc:
attr_accessor :controller
def before(controller)
self.controller = controller
callback(:before) if controller.perform_caching
end
def after(controller)
callback(:after) if controller.perform_caching
# Clean up, so that the controller can be collected after this request
self.controller = nil
end
protected
# gets the action cache path for the given options.
def action_path_for(options)
ActionController::Caching::Actions::ActionCachePath.path_for(controller, options)
end
# Retrieve instance variables set in the controller.
def assigns(key)
controller.instance_variable_get("@#{key}")
end
private
def callback(timing)
controller_callback_method_name = "#{timing}_#{controller.controller_name.underscore}"
action_callback_method_name = "#{controller_callback_method_name}_#{controller.action_name}"
__send__(controller_callback_method_name) if respond_to?(controller_callback_method_name, true)
__send__(action_callback_method_name) if respond_to?(action_callback_method_name, true)
end
def method_missing(method, *arguments, &block)
return if @controller.nil?
@controller.__send__(method, *arguments, &block)
end
end
end
end end
end end

View file

@ -1,4 +1,19 @@
require 'erb'
module ActionController module ActionController
# The Failsafe middleware is usually the top-most middleware in the Rack
# middleware chain. It returns the underlying middleware's response, but if
# the underlying middle raises an exception then Failsafe will log the
# exception into the Rails log file, and will attempt to return an error
# message response.
#
# Failsafe is a last resort for logging errors and for telling the HTTP
# client that something went wrong. Do not confuse this with the
# ActionController::Rescue module, which is responsible for catching
# exceptions at deeper levels. Unlike Failsafe, which is as simple as
# possible, Rescue provides features that allow developers to hook into
# the error handling logic, and can customize the error message response
# based on the HTTP client's IP.
class Failsafe class Failsafe
cattr_accessor :error_file_path cattr_accessor :error_file_path
self.error_file_path = Rails.public_path if defined?(Rails.public_path) self.error_file_path = Rails.public_path if defined?(Rails.public_path)
@ -11,7 +26,7 @@ module ActionController
@app.call(env) @app.call(env)
rescue Exception => exception rescue Exception => exception
# Reraise exception in test environment # Reraise exception in test environment
if env["rack.test"] if defined?(Rails) && Rails.env.test?
raise exception raise exception
else else
failsafe_response(exception) failsafe_response(exception)
@ -21,18 +36,37 @@ module ActionController
private private
def failsafe_response(exception) def failsafe_response(exception)
log_failsafe_exception(exception) log_failsafe_exception(exception)
[500, {'Content-Type' => 'text/html'}, failsafe_response_body] [500, {'Content-Type' => 'text/html'}, [failsafe_response_body]]
rescue Exception => failsafe_error # Logger or IO errors rescue Exception => failsafe_error # Logger or IO errors
$stderr.puts "Error during failsafe response: #{failsafe_error}" $stderr.puts "Error during failsafe response: #{failsafe_error}"
end end
def failsafe_response_body def failsafe_response_body
error_path = "#{self.class.error_file_path}/500.html" error_template_path = "#{self.class.error_file_path}/500.html"
if File.exist?(error_path) if File.exist?(error_template_path)
File.read(error_path) begin
else result = render_template(error_template_path)
"<html><body><h1>500 Internal Server Error</h1></body></html>" rescue Exception
result = nil
end end
else
result = nil
end
if result.nil?
result = "<html><body><h1>500 Internal Server Error</h1>" <<
"If you are the administrator of this website, then please read this web " <<
"application's log file to find out what went wrong.</body></html>"
end
result
end
# The default 500.html uses the h() method.
def h(text) # :nodoc:
ERB::Util.h(text)
end
def render_template(filename)
ERB.new(File.read(filename)).result(binding)
end end
def log_failsafe_exception(exception) def log_failsafe_exception(exception)

View file

@ -120,6 +120,11 @@ module ActionController #:nodoc:
(@used.keys - keys).each{ |k| @used.delete(k) } (@used.keys - keys).each{ |k| @used.delete(k) }
end end
def store(session, key = "flash")
return if self.empty?
session[key] = self
end
private private
# Used internally by the <tt>keep</tt> and <tt>discard</tt> methods # Used internally by the <tt>keep</tt> and <tt>discard</tt> methods
# use() # marks the entire flash as used # use() # marks the entire flash as used
@ -139,7 +144,10 @@ module ActionController #:nodoc:
protected protected
def perform_action_with_flash def perform_action_with_flash
perform_action_without_flash perform_action_without_flash
remove_instance_variable(:@_flash) if defined? @_flash if defined? @_flash
@_flash.store(session)
remove_instance_variable(:@_flash)
end
end end
def reset_session_with_flash def reset_session_with_flash
@ -151,8 +159,8 @@ module ActionController #:nodoc:
# read a notice you put there or <tt>flash["notice"] = "hello"</tt> # read a notice you put there or <tt>flash["notice"] = "hello"</tt>
# to put a new one. # to put a new one.
def flash #:doc: def flash #:doc:
unless defined? @_flash if !defined?(@_flash)
@_flash = session["flash"] ||= FlashHash.new @_flash = session["flash"] || FlashHash.new
@_flash.sweep @_flash.sweep
end end

View file

@ -183,7 +183,7 @@ module ActionController
request.env['REDIRECT_X_HTTP_AUTHORIZATION'] request.env['REDIRECT_X_HTTP_AUTHORIZATION']
end end
# Raises error unless the request credentials response value matches the expected value. # Returns false unless the request credentials response value matches the expected value.
# First try the password as a ha1 digest password. If this fails, then try it as a plain # First try the password as a ha1 digest password. If this fails, then try it as a plain
# text password. # text password.
def validate_digest_response(request, realm, &password_procedure) def validate_digest_response(request, realm, &password_procedure)
@ -192,9 +192,12 @@ module ActionController
if valid_nonce && realm == credentials[:realm] && opaque == credentials[:opaque] if valid_nonce && realm == credentials[:realm] && opaque == credentials[:opaque]
password = password_procedure.call(credentials[:username]) password = password_procedure.call(credentials[:username])
return false unless password
method = request.env['rack.methodoverride.original_method'] || request.env['REQUEST_METHOD']
[true, false].any? do |password_is_ha1| [true, false].any? do |password_is_ha1|
expected = expected_response(request.env['REQUEST_METHOD'], request.env['REQUEST_URI'], credentials, password, password_is_ha1) expected = expected_response(method, request.env['REQUEST_URI'], credentials, password, password_is_ha1)
expected == credentials[:response] expected == credentials[:response]
end end
end end

View file

@ -292,9 +292,7 @@ module ActionController
"rack.errors" => StringIO.new, "rack.errors" => StringIO.new,
"rack.multithread" => true, "rack.multithread" => true,
"rack.multiprocess" => true, "rack.multiprocess" => true,
"rack.run_once" => false, "rack.run_once" => false
"rack.test" => true
) )
(headers || {}).each do |key, value| (headers || {}).each do |key, value|
@ -311,12 +309,7 @@ module ActionController
ActionController::Base.clear_last_instantiation! ActionController::Base.clear_last_instantiation!
app = @application app = Rack::Lint.new(@application)
# Rack::Lint doesn't accept String headers or bodies in Ruby 1.9
unless RUBY_VERSION >= '1.9.0' && Rack.release <= '0.9.0'
app = Rack::Lint.new(app)
end
status, headers, body = app.call(env) status, headers, body = app.call(env)
@request_count += 1 @request_count += 1
@ -333,7 +326,7 @@ module ActionController
end end
@body = "" @body = ""
if body.is_a?(String) if body.respond_to?(:to_str)
@body << body @body << body
else else
body.each { |part| @body << part } body.each { |part| @body << part }
@ -416,7 +409,7 @@ module ActionController
def multipart_requestify(params, first=true) def multipart_requestify(params, first=true)
returning Hash.new do |p| returning Hash.new do |p|
params.each do |key, value| params.each do |key, value|
k = first ? CGI.escape(key.to_s) : "[#{CGI.escape(key.to_s)}]" k = first ? key.to_s : "[#{key.to_s}]"
if Hash === value if Hash === value
multipart_requestify(value, false).each do |subkey, subvalue| multipart_requestify(value, false).each do |subkey, subvalue|
p[k + subkey] = subvalue p[k + subkey] = subvalue

View file

@ -7,7 +7,6 @@ use "ActionController::Failsafe"
use lambda { ActionController::Base.session_store }, use lambda { ActionController::Base.session_store },
lambda { ActionController::Base.session_options } lambda { ActionController::Base.session_options }
use "ActionController::RewindableInput"
use "ActionController::ParamsParser" use "ActionController::ParamsParser"
use "Rack::MethodOverride" use "Rack::MethodOverride"
use "Rack::Head" use "Rack::Head"

View file

@ -1,14 +1,45 @@
module ActionController module ActionController
class Reloader class Reloader
class BodyWrapper
def initialize(body)
@body = body
end
def close
@body.close if @body.respond_to?(:close)
ensure
Dispatcher.cleanup_application
end
def method_missing(*args, &block)
@body.send(*args, &block)
end
def respond_to?(symbol, include_private = false)
symbol == :close || @body.respond_to?(symbol, include_private)
end
end
def initialize(app) def initialize(app)
@app = app @app = app
end end
def call(env) def call(env)
Dispatcher.reload_application Dispatcher.reload_application
@app.call(env) status, headers, body = @app.call(env)
ensure # We do not want to call 'cleanup_application' in an ensure block
Dispatcher.cleanup_application # because the returned Rack response body may lazily generate its data. This
# is for example the case if one calls
#
# render :text => lambda { ... code here which refers to application models ... }
#
# in an ActionController.
#
# Instead, we will want to cleanup the application code after the request is
# completely finished. So we wrap the body in a BodyWrapper class so that
# when the Rack handler calls #close during the end of the request, we get to
# run our cleanup code.
[status, headers, BodyWrapper.new(body)]
end end
end end
end end

View file

@ -95,6 +95,10 @@ module ActionController
end end
end end
def media_type
content_type.to_s
end
# Returns the accepted MIME type for the request. # Returns the accepted MIME type for the request.
def accepts def accepts
@accepts ||= begin @accepts ||= begin
@ -383,7 +387,7 @@ EOM
alias_method :params, :parameters alias_method :params, :parameters
def path_parameters=(parameters) #:nodoc: def path_parameters=(parameters) #:nodoc:
@env["rack.routing_args"] = parameters @env["action_controller.request.path_parameters"] = parameters
@symbolized_path_parameters = @parameters = nil @symbolized_path_parameters = @parameters = nil
end end
@ -399,7 +403,7 @@ EOM
# #
# See <tt>symbolized_path_parameters</tt> for symbolized keys. # See <tt>symbolized_path_parameters</tt> for symbolized keys.
def path_parameters def path_parameters
@env["rack.routing_args"] ||= {} @env["action_controller.request.path_parameters"] ||= {}
end end
# The request body is an IO input stream. If the RAW_POST_DATA environment # The request body is an IO input stream. If the RAW_POST_DATA environment

View file

@ -151,8 +151,8 @@ module ActionController # :nodoc:
if @body.respond_to?(:call) if @body.respond_to?(:call)
@writer = lambda { |x| callback.call(x) } @writer = lambda { |x| callback.call(x) }
@body.call(self, self) @body.call(self, self)
elsif @body.is_a?(String) elsif @body.respond_to?(:to_str)
@body.each_line(&callback) yield @body
else else
@body.each(&callback) @body.each(&callback)
end end

View file

@ -1,28 +0,0 @@
module ActionController
class RewindableInput
class RewindableIO < ActiveSupport::BasicObject
def initialize(io)
@io = io
@rewindable = io.is_a?(::StringIO)
end
def method_missing(method, *args, &block)
unless @rewindable
@io = ::StringIO.new(@io.read)
@rewindable = true
end
@io.__send__(method, *args, &block)
end
end
def initialize(app)
@app = app
end
def call(env)
env['rack.input'] = RewindableIO.new(env['rack.input'])
@app.call(env)
end
end
end

View file

@ -305,6 +305,7 @@ module ActionController
end end
def add_route(path, options = {}) def add_route(path, options = {})
options.each { |k, v| options[k] = v.to_s if [:controller, :action].include?(k) && v.is_a?(Symbol) }
route = builder.build(path, options) route = builder.build(path, options)
routes << route routes << route
route route
@ -436,7 +437,7 @@ module ActionController
def recognize(request) def recognize(request)
params = recognize_path(request.path, extract_request_environment(request)) params = recognize_path(request.path, extract_request_environment(request))
request.path_parameters = params.with_indifferent_access request.path_parameters = params.with_indifferent_access
"#{params[:controller].camelize}Controller".constantize "#{params[:controller].to_s.camelize}Controller".constantize
end end
def recognize_path(path, environment={}) def recognize_path(path, environment={})

View file

@ -161,7 +161,7 @@ module ActionController #:nodoc:
content_type = content_type.to_s.strip # fixes a problem with extra '\r' with some browsers content_type = content_type.to_s.strip # fixes a problem with extra '\r' with some browsers
headers.merge!( headers.merge!(
'Content-Length' => options[:length], 'Content-Length' => options[:length].to_s,
'Content-Type' => content_type, 'Content-Type' => content_type,
'Content-Disposition' => disposition, 'Content-Disposition' => disposition,
'Content-Transfer-Encoding' => 'binary' 'Content-Transfer-Encoding' => 'binary'

View file

@ -1,3 +1,4 @@
require 'rack/session/abstract/id'
module ActionController #:nodoc: module ActionController #:nodoc:
class TestRequest < Request #:nodoc: class TestRequest < Request #:nodoc:
attr_accessor :cookies, :session_options attr_accessor :cookies, :session_options
@ -13,6 +14,8 @@ module ActionController #:nodoc:
@query_parameters = {} @query_parameters = {}
@session = TestSession.new @session = TestSession.new
default_rack_options = Rack::Session::Abstract::ID::DEFAULT_OPTIONS
@session_options ||= {:id => generate_sid(default_rack_options[:sidbits])}.merge(default_rack_options)
initialize_default_values initialize_default_values
initialize_containers initialize_containers
@ -110,6 +113,7 @@ module ActionController #:nodoc:
end end
def recycle! def recycle!
@env["action_controller.request.request_parameters"] = {}
self.query_parameters = {} self.query_parameters = {}
self.path_parameters = {} self.path_parameters = {}
@headers, @request_method, @accepts, @content_type = nil, nil, nil, nil @headers, @request_method, @accepts, @content_type = nil, nil, nil, nil
@ -120,6 +124,10 @@ module ActionController #:nodoc:
end end
private private
def generate_sid(sidbits)
"%0#{sidbits / 4}x" % rand(2**sidbits - 1)
end
def initialize_containers def initialize_containers
@cookies = {} @cookies = {}
end end
@ -250,7 +258,7 @@ module ActionController #:nodoc:
def cookies def cookies
cookies = {} cookies = {}
Array(headers['Set-Cookie']).each do |cookie| Array(headers['Set-Cookie']).each do |cookie|
key, value = cookie.split(";").first.split("=") key, value = cookie.split(";").first.split("=").map {|val| Rack::Utils.unescape(val)}
cookies[key] = value cookies[key] = value
end end
cookies cookies

View file

@ -1,64 +0,0 @@
require 'thread'
module Rack
# Rack::Reloader checks on every request, but at most every +secs+
# seconds, if a file loaded changed, and reloads it, logging to
# rack.errors.
#
# It is recommended you use ShowExceptions to catch SyntaxErrors etc.
class Reloader
def initialize(app, secs=10)
@app = app
@secs = secs # reload every @secs seconds max
@last = Time.now
end
def call(env)
if Time.now > @last + @secs
Thread.exclusive {
reload!(env['rack.errors'])
@last = Time.now
}
end
@app.call(env)
end
def reload!(stderr=$stderr)
need_reload = $LOADED_FEATURES.find_all { |loaded|
begin
if loaded =~ /\A[.\/]/ # absolute filename or 1.9
abs = loaded
else
abs = $LOAD_PATH.map { |path| ::File.join(path, loaded) }.
find { |file| ::File.exist? file }
end
if abs
::File.mtime(abs) > @last - @secs rescue false
else
false
end
end
}
need_reload.each { |l|
$LOADED_FEATURES.delete l
}
need_reload.each { |to_load|
begin
if require to_load
stderr.puts "#{self.class}: reloaded `#{to_load}'"
end
rescue LoadError, SyntaxError => e
raise e # Possibly ShowExceptions
end
}
stderr.flush
need_reload
end
end
end

View file

@ -3,7 +3,8 @@
# Rack is freely distributable under the terms of an MIT-style license. # Rack is freely distributable under the terms of an MIT-style license.
# See COPYING or http://www.opensource.org/licenses/mit-license.php. # See COPYING or http://www.opensource.org/licenses/mit-license.php.
$:.unshift(File.expand_path(File.dirname(__FILE__))) path = File.expand_path(File.dirname(__FILE__))
$:.unshift(path) unless $:.include?(path)
# The Rack main module, serving as a namespace for all core Rack # The Rack main module, serving as a namespace for all core Rack
@ -14,7 +15,7 @@ $:.unshift(File.expand_path(File.dirname(__FILE__)))
module Rack module Rack
# The Rack protocol version number implemented. # The Rack protocol version number implemented.
VERSION = [0,1] VERSION = [1,0]
# Return the Rack protocol version as a dotted string. # Return the Rack protocol version as a dotted string.
def self.version def self.version
@ -23,7 +24,7 @@ module Rack
# Return the Rack release as a dotted string. # Return the Rack release as a dotted string.
def self.release def self.release
"1.0 bundled" "1.0"
end end
autoload :Builder, "rack/builder" autoload :Builder, "rack/builder"

View file

@ -26,6 +26,8 @@ module Rack
headers = Utils::HeaderHash.new(headers) headers = Utils::HeaderHash.new(headers)
if etag_matches?(env, headers) || modified_since?(env, headers) if etag_matches?(env, headers) || modified_since?(env, headers)
status = 304 status = 304
headers.delete('Content-Type')
headers.delete('Content-Length')
body = [] body = []
end end
[status, headers, body] [status, headers, body]

View file

@ -33,17 +33,15 @@ module Rack
case encoding case encoding
when "gzip" when "gzip"
headers['Content-Encoding'] = "gzip"
headers.delete('Content-Length')
mtime = headers.key?("Last-Modified") ? mtime = headers.key?("Last-Modified") ?
Time.httpdate(headers["Last-Modified"]) : Time.now Time.httpdate(headers["Last-Modified"]) : Time.now
body = self.class.gzip(body, mtime) [status, headers, GzipStream.new(body, mtime)]
size = Rack::Utils.bytesize(body)
headers = headers.merge("Content-Encoding" => "gzip", "Content-Length" => size.to_s)
[status, headers, [body]]
when "deflate" when "deflate"
body = self.class.deflate(body) headers['Content-Encoding'] = "deflate"
size = Rack::Utils.bytesize(body) headers.delete('Content-Length')
headers = headers.merge("Content-Encoding" => "deflate", "Content-Length" => size.to_s) [status, headers, DeflateStream.new(body)]
[status, headers, [body]]
when "identity" when "identity"
[status, headers, body] [status, headers, body]
when nil when nil
@ -52,18 +50,28 @@ module Rack
end end
end end
def self.gzip(body, mtime) class GzipStream
io = StringIO.new def initialize(body, mtime)
gzip = Zlib::GzipWriter.new(io) @body = body
gzip.mtime = mtime @mtime = mtime
# TODO: Add streaming
body.each { |part| gzip << part }
gzip.close
return io.string
end end
def each(&block)
@writer = block
gzip =::Zlib::GzipWriter.new(self)
gzip.mtime = @mtime
@body.each { |part| gzip << part }
@body.close if @body.respond_to?(:close)
gzip.close
@writer = nil
end
def write(data)
@writer.call(data)
end
end
class DeflateStream
DEFLATE_ARGS = [ DEFLATE_ARGS = [
Zlib::DEFAULT_COMPRESSION, Zlib::DEFAULT_COMPRESSION,
# drop the zlib header which causes both Safari and IE to choke # drop the zlib header which causes both Safari and IE to choke
@ -72,14 +80,17 @@ module Rack
Zlib::DEFAULT_STRATEGY Zlib::DEFAULT_STRATEGY
] ]
# Loosely based on Mongrel's Deflate handler def initialize(body)
def self.deflate(body) @body = body
deflater = Zlib::Deflate.new(*DEFLATE_ARGS) end
# TODO: Add streaming def each
body.each { |part| deflater << part } deflater = ::Zlib::Deflate.new(*DEFLATE_ARGS)
@body.each { |part| yield deflater.deflate(part) }
return deflater.finish @body.close if @body.respond_to?(:close)
yield deflater.finish
nil
end
end end
end end
end end

View file

@ -10,16 +10,37 @@ module Rack
module Handler module Handler
def self.get(server) def self.get(server)
return unless server return unless server
server = server.to_s
if klass = @handlers[server] if klass = @handlers[server]
obj = Object obj = Object
klass.split("::").each { |x| obj = obj.const_get(x) } klass.split("::").each { |x| obj = obj.const_get(x) }
obj obj
else else
Rack::Handler.const_get(server.capitalize) try_require('rack/handler', server)
const_get(server)
end end
end end
# Transforms server-name constants to their canonical form as filenames,
# then tries to require them but silences the LoadError if not found
#
# Naming convention:
#
# Foo # => 'foo'
# FooBar # => 'foo_bar.rb'
# FooBAR # => 'foobar.rb'
# FOObar # => 'foobar.rb'
# FOOBAR # => 'foobar.rb'
# FooBarBaz # => 'foo_bar_baz.rb'
def self.try_require(prefix, const_name)
file = const_name.gsub(/^[A-Z]+/) { |pre| pre.downcase }.
gsub(/[A-Z]+[^A-Z]/, '_\&').downcase
require(::File.join(prefix, file))
rescue LoadError
end
def self.register(server, klass) def self.register(server, klass)
@handlers ||= {} @handlers ||= {}
@handlers[server] = klass @handlers[server] = klass

View file

@ -15,7 +15,7 @@ module Rack
env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/" env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
env.update({"rack.version" => [0,1], env.update({"rack.version" => [1,0],
"rack.input" => $stdin, "rack.input" => $stdin,
"rack.errors" => $stderr, "rack.errors" => $stderr,

View file

@ -1,6 +1,17 @@
require 'fcgi' require 'fcgi'
require 'socket' require 'socket'
require 'rack/content_length' require 'rack/content_length'
require 'rack/rewindable_input'
class FCGI::Stream
alias _rack_read_without_buffer read
def read(n, buffer=nil)
buf = _rack_read_without_buffer n
buffer.replace(buf.to_s) if buffer
buf
end
end
module Rack module Rack
module Handler module Handler
@ -13,34 +24,18 @@ module Rack
} }
end end
module ProperStream # :nodoc:
def each # This is missing by default.
while line = gets
yield line
end
end
def read(*args)
if args.empty?
super || "" # Empty string on EOF.
else
super
end
end
end
def self.serve(request, app) def self.serve(request, app)
app = Rack::ContentLength.new(app) app = Rack::ContentLength.new(app)
env = request.env env = request.env
env.delete "HTTP_CONTENT_LENGTH" env.delete "HTTP_CONTENT_LENGTH"
request.in.extend ProperStream
env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/" env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
env.update({"rack.version" => [0,1], rack_input = RewindableInput.new(request.in)
"rack.input" => request.in,
env.update({"rack.version" => [1,0],
"rack.input" => rack_input,
"rack.errors" => request.err, "rack.errors" => request.err,
"rack.multithread" => false, "rack.multithread" => false,
@ -57,12 +52,16 @@ module Rack
env.delete "CONTENT_TYPE" if env["CONTENT_TYPE"] == "" env.delete "CONTENT_TYPE" if env["CONTENT_TYPE"] == ""
env.delete "CONTENT_LENGTH" if env["CONTENT_LENGTH"] == "" env.delete "CONTENT_LENGTH" if env["CONTENT_LENGTH"] == ""
begin
status, headers, body = app.call(env) status, headers, body = app.call(env)
begin begin
send_headers request.out, status, headers send_headers request.out, status, headers
send_body request.out, body send_body request.out, body
ensure ensure
body.close if body.respond_to? :close body.close if body.respond_to? :close
end
ensure
rack_input.close
request.finish request.finish
end end
end end

View file

@ -15,7 +15,7 @@ module Rack
env = ENV.to_hash env = ENV.to_hash
env.delete "HTTP_CONTENT_LENGTH" env.delete "HTTP_CONTENT_LENGTH"
env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/" env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
env.update({"rack.version" => [0,1], env.update({"rack.version" => [1,0],
"rack.input" => StringIO.new($stdin.read.to_s), "rack.input" => StringIO.new($stdin.read.to_s),
"rack.errors" => $stderr, "rack.errors" => $stderr,
"rack.multithread" => false, "rack.multithread" => false,

View file

@ -45,7 +45,7 @@ module Rack
env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/" env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
env.update({"rack.version" => [0,1], env.update({"rack.version" => [1,0],
"rack.input" => request.body || StringIO.new(""), "rack.input" => request.body || StringIO.new(""),
"rack.errors" => $stderr, "rack.errors" => $stderr,

View file

@ -32,7 +32,7 @@ module Rack
env["PATH_INFO"] = env["REQUEST_PATH"] env["PATH_INFO"] = env["REQUEST_PATH"]
env["QUERY_STRING"] ||= "" env["QUERY_STRING"] ||= ""
env["SCRIPT_NAME"] = "" env["SCRIPT_NAME"] = ""
env.update({"rack.version" => [0,1], env.update({"rack.version" => [1,0],
"rack.input" => StringIO.new(input_body), "rack.input" => StringIO.new(input_body),
"rack.errors" => $stderr, "rack.errors" => $stderr,

View file

@ -23,7 +23,7 @@ module Rack
env = req.meta_vars env = req.meta_vars
env.delete_if { |k, v| v.nil? } env.delete_if { |k, v| v.nil? }
env.update({"rack.version" => [0,1], env.update({"rack.version" => [1,0],
"rack.input" => StringIO.new(req.body.to_s), "rack.input" => StringIO.new(req.body.to_s),
"rack.errors" => $stderr, "rack.errors" => $stderr,

View file

@ -88,9 +88,9 @@ module Rack
## within the application. This may be an ## within the application. This may be an
## empty string, if the request URL targets ## empty string, if the request URL targets
## the application root and does not have a ## the application root and does not have a
## trailing slash. This information should be ## trailing slash. This value may be
## decoded by the server if it comes from a ## percent-encoded when I originating from
## URL. ## a URL.
## <tt>QUERY_STRING</tt>:: The portion of the request URL that ## <tt>QUERY_STRING</tt>:: The portion of the request URL that
## follows the <tt>?</tt>, if any. May be ## follows the <tt>?</tt>, if any. May be
@ -111,19 +111,48 @@ module Rack
## In addition to this, the Rack environment must include these ## In addition to this, the Rack environment must include these
## Rack-specific variables: ## Rack-specific variables:
## <tt>rack.version</tt>:: The Array [0,1], representing this version of Rack. ## <tt>rack.version</tt>:: The Array [1,0], representing this version of Rack.
## <tt>rack.url_scheme</tt>:: +http+ or +https+, depending on the request URL. ## <tt>rack.url_scheme</tt>:: +http+ or +https+, depending on the request URL.
## <tt>rack.input</tt>:: See below, the input stream. ## <tt>rack.input</tt>:: See below, the input stream.
## <tt>rack.errors</tt>:: See below, the error stream. ## <tt>rack.errors</tt>:: See below, the error stream.
## <tt>rack.multithread</tt>:: true if the application object may be simultaneously invoked by another thread in the same process, false otherwise. ## <tt>rack.multithread</tt>:: true if the application object may be simultaneously invoked by another thread in the same process, false otherwise.
## <tt>rack.multiprocess</tt>:: true if an equivalent application object may be simultaneously invoked by another process, false otherwise. ## <tt>rack.multiprocess</tt>:: true if an equivalent application object may be simultaneously invoked by another process, false otherwise.
## <tt>rack.run_once</tt>:: true if the server expects (but does not guarantee!) that the application will only be invoked this one time during the life of its containing process. Normally, this will only be true for a server based on CGI (or something similar). ## <tt>rack.run_once</tt>:: true if the server expects (but does not guarantee!) that the application will only be invoked this one time during the life of its containing process. Normally, this will only be true for a server based on CGI (or something similar).
##
## Additional environment specifications have approved to
## standardized middleware APIs. None of these are required to
## be implemented by the server.
## <tt>rack.session</tt>:: A hash like interface for storing request session data.
## The store must implement:
if session = env['rack.session']
## store(key, value) (aliased as []=);
assert("session #{session.inspect} must respond to store and []=") {
session.respond_to?(:store) && session.respond_to?(:[]=)
}
## fetch(key, default = nil) (aliased as []);
assert("session #{session.inspect} must respond to fetch and []") {
session.respond_to?(:fetch) && session.respond_to?(:[])
}
## delete(key);
assert("session #{session.inspect} must respond to delete") {
session.respond_to?(:delete)
}
## clear;
assert("session #{session.inspect} must respond to clear") {
session.respond_to?(:clear)
}
end
## The server or the application can store their own data in the ## The server or the application can store their own data in the
## environment, too. The keys must contain at least one dot, ## environment, too. The keys must contain at least one dot,
## and should be prefixed uniquely. The prefix <tt>rack.</tt> ## and should be prefixed uniquely. The prefix <tt>rack.</tt>
## is reserved for use with the Rack core distribution and must ## is reserved for use with the Rack core distribution and other
## not be used otherwise. ## accepted specifications and must not be used otherwise.
## ##
%w[REQUEST_METHOD SERVER_NAME SERVER_PORT %w[REQUEST_METHOD SERVER_NAME SERVER_PORT
@ -202,9 +231,12 @@ module Rack
end end
## === The Input Stream ## === The Input Stream
##
## The input stream is an IO-like object which contains the raw HTTP
## POST data. If it is a file then it must be opened in binary mode.
def check_input(input) def check_input(input)
## The input stream must respond to +gets+, +each+ and +read+. ## The input stream must respond to +gets+, +each+, +read+ and +rewind+.
[:gets, :each, :read].each { |method| [:gets, :each, :read, :rewind].each { |method|
assert("rack.input #{input} does not respond to ##{method}") { assert("rack.input #{input} does not respond to ##{method}") {
input.respond_to? method input.respond_to? method
} }
@ -222,10 +254,6 @@ module Rack
@input.size @input.size
end end
def rewind
@input.rewind
end
## * +gets+ must be called without arguments and return a string, ## * +gets+ must be called without arguments and return a string,
## or +nil+ on EOF. ## or +nil+ on EOF.
def gets(*args) def gets(*args)
@ -237,21 +265,44 @@ module Rack
v v
end end
## * +read+ must be called without or with one integer argument ## * +read+ behaves like IO#read. Its signature is <tt>read([length, [buffer]])</tt>.
## and return a string, or +nil+ on EOF. ## If given, +length+ must be an non-negative Integer (>= 0) or +nil+, and +buffer+ must
## be a String and may not be nil. If +length+ is given and not nil, then this method
## reads at most +length+ bytes from the input stream. If +length+ is not given or nil,
## then this method reads all data until EOF.
## When EOF is reached, this method returns nil if +length+ is given and not nil, or ""
## if +length+ is not given or is nil.
## If +buffer+ is given, then the read data will be placed into +buffer+ instead of a
## newly created String object.
def read(*args) def read(*args)
assert("rack.input#read called with too many arguments") { assert("rack.input#read called with too many arguments") {
args.size <= 1 args.size <= 2
} }
if args.size == 1 if args.size >= 1
assert("rack.input#read called with non-integer argument") { assert("rack.input#read called with non-integer and non-nil length") {
args.first.kind_of? Integer args.first.kind_of?(Integer) || args.first.nil?
}
assert("rack.input#read called with a negative length") {
args.first.nil? || args.first >= 0
} }
end end
if args.size >= 2
assert("rack.input#read called with non-String buffer") {
args[1].kind_of?(String)
}
end
v = @input.read(*args) v = @input.read(*args)
assert("rack.input#read didn't return a String") {
assert("rack.input#read didn't return nil or a String") {
v.nil? or v.instance_of? String v.nil? or v.instance_of? String
} }
if args[0].nil?
assert("rack.input#read(nil) returned nil on EOF") {
!v.nil?
}
end
v v
end end
@ -266,6 +317,23 @@ module Rack
} }
end end
## * +rewind+ must be called without arguments. It rewinds the input
## stream back to the beginning. It must not raise Errno::ESPIPE:
## that is, it may not be a pipe or a socket. Therefore, handler
## developers must buffer the input data into some rewindable object
## if the underlying input stream is not rewindable.
def rewind(*args)
assert("rack.input#rewind called with arguments") { args.size == 0 }
assert("rack.input#rewind raised Errno::ESPIPE") {
begin
@input.rewind
true
rescue Errno::ESPIPE
false
end
}
end
## * +close+ must never be called on the input stream. ## * +close+ must never be called on the input stream.
def close(*args) def close(*args)
assert("rack.input#close must not be called") { false } assert("rack.input#close must not be called") { false }
@ -316,13 +384,14 @@ module Rack
## === The Status ## === The Status
def check_status(status) def check_status(status)
## The status, if parsed as integer (+to_i+), must be greater than or equal to 100. ## This is an HTTP status. When parsed as integer (+to_i+), it must be
## greater than or equal to 100.
assert("Status must be >=100 seen as integer") { status.to_i >= 100 } assert("Status must be >=100 seen as integer") { status.to_i >= 100 }
end end
## === The Headers ## === The Headers
def check_headers(header) def check_headers(header)
## The header must respond to each, and yield values of key and value. ## The header must respond to +each+, and yield values of key and value.
assert("headers object should respond to #each, but doesn't (got #{header.class} as headers)") { assert("headers object should respond to #each, but doesn't (got #{header.class} as headers)") {
header.respond_to? :each header.respond_to? :each
} }
@ -344,7 +413,8 @@ module Rack
## The values of the header must be Strings, ## The values of the header must be Strings,
assert("a header value must be a String, but the value of " + assert("a header value must be a String, but the value of " +
"'#{key}' is a #{value.class}") { value.kind_of? String } "'#{key}' is a #{value.class}") { value.kind_of? String }
## consisting of lines (for multiple header values) seperated by "\n". ## consisting of lines (for multiple header values, e.g. multiple
## <tt>Set-Cookie</tt> values) seperated by "\n".
value.split("\n").each { |item| value.split("\n").each { |item|
## The lines must not contain characters below 037. ## The lines must not contain characters below 037.
assert("invalid header value #{key}: #{item.inspect}") { assert("invalid header value #{key}: #{item.inspect}") {
@ -416,7 +486,7 @@ module Rack
## === The Body ## === The Body
def each def each
@closed = false @closed = false
## The Body must respond to #each ## The Body must respond to +each+
@body.each { |part| @body.each { |part|
## and must only yield String values. ## and must only yield String values.
assert("Body yielded non-string value #{part.inspect}") { assert("Body yielded non-string value #{part.inspect}") {
@ -425,14 +495,19 @@ module Rack
yield part yield part
} }
## ##
## If the Body responds to #close, it will be called after iteration. ## The Body itself should not be an instance of String, as this will
## break in Ruby 1.9.
##
## If the Body responds to +close+, it will be called after iteration.
# XXX howto: assert("Body has not been closed") { @closed } # XXX howto: assert("Body has not been closed") { @closed }
## ##
## If the Body responds to #to_path, it must return a String ## If the Body responds to +to_path+, it must return a String
## identifying the location of a file whose contents are identical ## identifying the location of a file whose contents are identical
## to that produced by calling #each. ## to that produced by calling +each+; this may be used by the
## server as an alternative, possibly more efficient way to
## transport the response.
if @body.respond_to?(:to_path) if @body.respond_to?(:to_path)
assert("The file identified by body.to_path does not exist") { assert("The file identified by body.to_path does not exist") {

View file

@ -40,7 +40,7 @@ module Rack
end end
DEFAULT_ENV = { DEFAULT_ENV = {
"rack.version" => [0,1], "rack.version" => [1,0],
"rack.input" => StringIO.new, "rack.input" => StringIO.new,
"rack.errors" => StringIO.new, "rack.errors" => StringIO.new,
"rack.multithread" => true, "rack.multithread" => true,
@ -73,14 +73,17 @@ module Rack
# Return the Rack environment used for a request to +uri+. # Return the Rack environment used for a request to +uri+.
def self.env_for(uri="", opts={}) def self.env_for(uri="", opts={})
uri = URI(uri) uri = URI(uri)
uri.path = "/#{uri.path}" unless uri.path[0] == ?/
env = DEFAULT_ENV.dup env = DEFAULT_ENV.dup
env["REQUEST_METHOD"] = opts[:method] || "GET" env["REQUEST_METHOD"] = opts[:method] ? opts[:method].to_s.upcase : "GET"
env["SERVER_NAME"] = uri.host || "example.org" env["SERVER_NAME"] = uri.host || "example.org"
env["SERVER_PORT"] = uri.port ? uri.port.to_s : "80" env["SERVER_PORT"] = uri.port ? uri.port.to_s : "80"
env["QUERY_STRING"] = uri.query.to_s env["QUERY_STRING"] = uri.query.to_s
env["PATH_INFO"] = (!uri.path || uri.path.empty?) ? "/" : uri.path env["PATH_INFO"] = (!uri.path || uri.path.empty?) ? "/" : uri.path
env["rack.url_scheme"] = uri.scheme || "http" env["rack.url_scheme"] = uri.scheme || "http"
env["HTTPS"] = env["rack.url_scheme"] == "https" ? "on" : "off"
env["SCRIPT_NAME"] = opts[:script_name] || "" env["SCRIPT_NAME"] = opts[:script_name] || ""
@ -90,6 +93,27 @@ module Rack
env["rack.errors"] = StringIO.new env["rack.errors"] = StringIO.new
end end
if params = opts[:params]
if env["REQUEST_METHOD"] == "GET"
params = Utils.parse_nested_query(params) if params.is_a?(String)
params.update(Utils.parse_nested_query(env["QUERY_STRING"]))
env["QUERY_STRING"] = Utils.build_nested_query(params)
elsif !opts.has_key?(:input)
opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
if params.is_a?(Hash)
if data = Utils::Multipart.build_multipart(params)
opts[:input] = data
opts["CONTENT_LENGTH"] ||= data.length.to_s
opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Utils::Multipart::MULTIPART_BOUNDARY}"
else
opts[:input] = Utils.build_nested_query(params)
end
else
opts[:input] = params
end
end
end
opts[:input] ||= "" opts[:input] ||= ""
if String === opts[:input] if String === opts[:input]
env["rack.input"] = StringIO.new(opts[:input]) env["rack.input"] = StringIO.new(opts[:input])
@ -125,7 +149,7 @@ module Rack
@body = "" @body = ""
body.each { |part| @body << part } body.each { |part| @body << part }
@errors = errors.string @errors = errors.string if errors.respond_to?(:string)
end end
# Status # Status

View file

@ -0,0 +1,106 @@
# Copyright (c) 2009 Michael Fellinger m.fellinger@gmail.com
# All files in this distribution are subject to the terms of the Ruby license.
require 'pathname'
module Rack
# High performant source reloader
#
# This class acts as Rack middleware.
#
# What makes it especially suited for use in a production environment is that
# any file will only be checked once and there will only be made one system
# call stat(2).
#
# Please note that this will not reload files in the background, it does so
# only when actively called.
#
# It is performing a check/reload cycle at the start of every request, but
# also respects a cool down time, during which nothing will be done.
class Reloader
def initialize(app, cooldown = 10, backend = Stat)
@app = app
@cooldown = cooldown
@last = (Time.now - cooldown)
@cache = {}
@mtimes = {}
extend backend
end
def call(env)
if @cooldown and Time.now > @last + @cooldown
if Thread.list.size > 1
Thread.exclusive{ reload! }
else
reload!
end
@last = Time.now
end
@app.call(env)
end
def reload!(stderr = $stderr)
rotation do |file, mtime|
previous_mtime = @mtimes[file] ||= mtime
safe_load(file, mtime, stderr) if mtime > previous_mtime
end
end
# A safe Kernel::load, issuing the hooks depending on the results
def safe_load(file, mtime, stderr = $stderr)
load(file)
stderr.puts "#{self.class}: reloaded `#{file}'"
file
rescue LoadError, SyntaxError => ex
stderr.puts ex
ensure
@mtimes[file] = mtime
end
module Stat
def rotation
files = [$0, *$LOADED_FEATURES].uniq
paths = ['./', *$LOAD_PATH].uniq
files.map{|file|
next if file =~ /\.(so|bundle)$/ # cannot reload compiled files
found, stat = figure_path(file, paths)
next unless found and stat and mtime = stat.mtime
@cache[file] = found
yield(found, mtime)
}.compact
end
# Takes a relative or absolute +file+ name, a couple possible +paths+ that
# the +file+ might reside in. Returns the full path and File::Stat for the
# path.
def figure_path(file, paths)
found = @cache[file]
found = file if !found and Pathname.new(file).absolute?
found, stat = safe_stat(found)
return found, stat if found
paths.each do |possible_path|
path = ::File.join(possible_path, file)
found, stat = safe_stat(path)
return ::File.expand_path(found), stat if found
end
end
def safe_stat(file)
return unless file
stat = ::File.stat(file)
return file, stat if stat.file?
rescue Errno::ENOENT, Errno::ENOTDIR
@cache.delete(file) and false
end
end
end
end

View file

@ -17,7 +17,7 @@ module Rack
# The environment of the request. # The environment of the request.
attr_reader :env attr_reader :env
def self.new(env) def self.new(env, *args)
if self == Rack::Request if self == Rack::Request
env["rack.request"] ||= super env["rack.request"] ||= super
else else
@ -38,6 +38,8 @@ module Rack
def query_string; @env["QUERY_STRING"].to_s end def query_string; @env["QUERY_STRING"].to_s end
def content_length; @env['CONTENT_LENGTH'] end def content_length; @env['CONTENT_LENGTH'] end
def content_type; @env['CONTENT_TYPE'] end def content_type; @env['CONTENT_TYPE'] end
def session; @env['rack.session'] ||= {} end
def session_options; @env['rack.session.options'] ||= {} end
# The media type (type/subtype) portion of the CONTENT_TYPE header # The media type (type/subtype) portion of the CONTENT_TYPE header
# without any media type parameters. e.g., when CONTENT_TYPE is # without any media type parameters. e.g., when CONTENT_TYPE is
@ -46,7 +48,7 @@ module Rack
# For more information on the use of media types in HTTP, see: # For more information on the use of media types in HTTP, see:
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7 # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
def media_type def media_type
content_type && content_type.split(/\s*[;,]\s*/, 2)[0].downcase content_type && content_type.split(/\s*[;,]\s*/, 2).first.downcase
end end
# The media type parameters provided in CONTENT_TYPE as a Hash, or # The media type parameters provided in CONTENT_TYPE as a Hash, or
@ -92,6 +94,14 @@ module Rack
'multipart/form-data' 'multipart/form-data'
] ]
# The set of media-types. Requests that do not indicate
# one of the media types presents in this list will not be eligible
# for param parsing like soap attachments or generic multiparts
PARSEABLE_DATA_MEDIA_TYPES = [
'multipart/related',
'multipart/mixed'
]
# Determine whether the request body contains form-data by checking # Determine whether the request body contains form-data by checking
# the request media_type against registered form-data media-types: # the request media_type against registered form-data media-types:
# "application/x-www-form-urlencoded" and "multipart/form-data". The # "application/x-www-form-urlencoded" and "multipart/form-data". The
@ -101,6 +111,12 @@ module Rack
FORM_DATA_MEDIA_TYPES.include?(media_type) FORM_DATA_MEDIA_TYPES.include?(media_type)
end end
# Determine whether the request body contains data by checking
# the request media_type against registered parse-data media-types
def parseable_data?
PARSEABLE_DATA_MEDIA_TYPES.include?(media_type)
end
# Returns the data recieved in the query string. # Returns the data recieved in the query string.
def GET def GET
if @env["rack.request.query_string"] == query_string if @env["rack.request.query_string"] == query_string
@ -119,7 +135,7 @@ module Rack
def POST def POST
if @env["rack.request.form_input"].eql? @env["rack.input"] if @env["rack.request.form_input"].eql? @env["rack.input"]
@env["rack.request.form_hash"] @env["rack.request.form_hash"]
elsif form_data? elsif form_data? || parseable_data?
@env["rack.request.form_input"] = @env["rack.input"] @env["rack.request.form_input"] = @env["rack.input"]
unless @env["rack.request.form_hash"] = unless @env["rack.request.form_hash"] =
Utils::Multipart.parse_multipart(env) Utils::Multipart.parse_multipart(env)
@ -131,12 +147,7 @@ module Rack
@env["rack.request.form_vars"] = form_vars @env["rack.request.form_vars"] = form_vars
@env["rack.request.form_hash"] = Utils.parse_nested_query(form_vars) @env["rack.request.form_hash"] = Utils.parse_nested_query(form_vars)
begin @env["rack.input"].rewind
@env["rack.input"].rewind if @env["rack.input"].respond_to?(:rewind)
rescue Errno::ESPIPE
# Handles exceptions raised by input streams that cannot be rewound
# such as when using plain CGI under Apache
end
end end
@env["rack.request.form_hash"] @env["rack.request.form_hash"]
else else
@ -212,10 +223,12 @@ module Rack
url url
end end
def path
script_name + path_info
end
def fullpath def fullpath
path = script_name + path_info query_string.empty? ? path : "#{path}?#{query_string}"
path << "?" << query_string unless query_string.empty?
path
end end
def accept_encoding def accept_encoding

View file

@ -95,6 +95,10 @@ module Rack
:expires => Time.at(0) }.merge(value)) :expires => Time.at(0) }.merge(value))
end end
def redirect(target, status=302)
self.status = status
self["Location"] = target
end
def finish(&block) def finish(&block)
@block = block @block = block
@ -120,7 +124,7 @@ module Rack
# #
def write(str) def write(str)
s = str.to_s s = str.to_s
@length += s.size @length += Rack::Utils.bytesize(s)
@writer.call s @writer.call s
header["Content-Length"] = @length.to_s header["Content-Length"] = @length.to_s

View file

@ -0,0 +1,98 @@
require 'tempfile'
module Rack
# Class which can make any IO object rewindable, including non-rewindable ones. It does
# this by buffering the data into a tempfile, which is rewindable.
#
# rack.input is required to be rewindable, so if your input stream IO is non-rewindable
# by nature (e.g. a pipe or a socket) then you can wrap it in an object of this class
# to easily make it rewindable.
#
# Don't forget to call #close when you're done. This frees up temporary resources that
# RewindableInput uses, though it does *not* close the original IO object.
class RewindableInput
def initialize(io)
@io = io
@rewindable_io = nil
@unlinked = false
end
def gets
make_rewindable unless @rewindable_io
@rewindable_io.gets
end
def read(*args)
make_rewindable unless @rewindable_io
@rewindable_io.read(*args)
end
def each(&block)
make_rewindable unless @rewindable_io
@rewindable_io.each(&block)
end
def rewind
make_rewindable unless @rewindable_io
@rewindable_io.rewind
end
# Closes this RewindableInput object without closing the originally
# wrapped IO oject. Cleans up any temporary resources that this RewindableInput
# has created.
#
# This method may be called multiple times. It does nothing on subsequent calls.
def close
if @rewindable_io
if @unlinked
@rewindable_io.close
else
@rewindable_io.close!
end
@rewindable_io = nil
end
end
private
# Ruby's Tempfile class has a bug. Subclass it and fix it.
class Tempfile < ::Tempfile
def _close
@tmpfile.close if @tmpfile
@data[1] = nil if @data
@tmpfile = nil
end
end
def make_rewindable
# Buffer all data into a tempfile. Since this tempfile is private to this
# RewindableInput object, we chmod it so that nobody else can read or write
# it. On POSIX filesystems we also unlink the file so that it doesn't
# even have a file entry on the filesystem anymore, though we can still
# access it because we have the file handle open.
@rewindable_io = Tempfile.new('RackRewindableInput')
@rewindable_io.chmod(0000)
if filesystem_has_posix_semantics?
@rewindable_io.unlink
@unlinked = true
end
buffer = ""
while @io.read(1024 * 4, buffer)
entire_buffer_written_out = false
while !entire_buffer_written_out
written = @rewindable_io.write(buffer)
entire_buffer_written_out = written == buffer.size
if !entire_buffer_written_out
buffer.slice!(0 .. written - 1)
end
end
end
@rewindable_io.rewind
end
def filesystem_has_posix_semantics?
RUBY_PLATFORM !~ /(mswin|mingw|cygwin|java)/
end
end
end

View file

@ -30,7 +30,7 @@ module Rack
location = location.chomp('/') location = location.chomp('/')
[host, location, app] [host, location, app]
}.sort_by { |(h, l, a)| [-l.size, h.to_s.size] } # Longest path first }.sort_by { |(h, l, a)| [h ? -h.size : (-1.0 / 0.0), -l.size] } # Longest path first
end end
def call(env) def call(env)

View file

@ -1,3 +1,5 @@
# -*- encoding: binary -*-
require 'set' require 'set'
require 'tempfile' require 'tempfile'
@ -63,7 +65,7 @@ module Rack
module_function :parse_nested_query module_function :parse_nested_query
def normalize_params(params, name, v = nil) def normalize_params(params, name, v = nil)
name =~ %r([\[\]]*([^\[\]]+)\]*) name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
k = $1 || '' k = $1 || ''
after = $' || '' after = $' || ''
@ -73,12 +75,12 @@ module Rack
params[k] = v params[k] = v
elsif after == "[]" elsif after == "[]"
params[k] ||= [] params[k] ||= []
raise TypeError unless params[k].is_a?(Array) raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
params[k] << v params[k] << v
elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$) elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
child_key = $1 child_key = $1
params[k] ||= [] params[k] ||= []
raise TypeError unless params[k].is_a?(Array) raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
if params[k].last.is_a?(Hash) && !params[k].last.key?(child_key) if params[k].last.is_a?(Hash) && !params[k].last.key?(child_key)
normalize_params(params[k].last, child_key, v) normalize_params(params[k].last, child_key, v)
else else
@ -86,6 +88,7 @@ module Rack
end end
else else
params[k] ||= {} params[k] ||= {}
raise TypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Hash)
params[k] = normalize_params(params[k], after, v) params[k] = normalize_params(params[k], after, v)
end end
@ -104,6 +107,25 @@ module Rack
end end
module_function :build_query module_function :build_query
def build_nested_query(value, prefix = nil)
case value
when Array
value.map { |v|
build_nested_query(v, "#{prefix}[]")
}.join("&")
when Hash
value.map { |k, v|
build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
}.join("&")
when String
raise ArgumentError, "value must be a Hash" if prefix.nil?
"#{prefix}=#{escape(value)}"
else
prefix
end
end
module_function :build_nested_query
# Escape ampersands, brackets and quotes to their HTML/XML entities. # Escape ampersands, brackets and quotes to their HTML/XML entities.
def escape_html(string) def escape_html(string)
string.to_s.gsub("&", "&amp;"). string.to_s.gsub("&", "&amp;").
@ -288,11 +310,39 @@ module Rack
# Usually, Rack::Request#POST takes care of calling this. # Usually, Rack::Request#POST takes care of calling this.
module Multipart module Multipart
class UploadedFile
# The filename, *not* including the path, of the "uploaded" file
attr_reader :original_filename
# The content type of the "uploaded" file
attr_accessor :content_type
def initialize(path, content_type = "text/plain", binary = false)
raise "#{path} file does not exist" unless ::File.exist?(path)
@content_type = content_type
@original_filename = ::File.basename(path)
@tempfile = Tempfile.new(@original_filename)
@tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding)
@tempfile.binmode if binary
FileUtils.copy_file(path, @tempfile.path)
end
def path
@tempfile.path
end
alias_method :local_path, :path
def method_missing(method_name, *args, &block) #:nodoc:
@tempfile.__send__(method_name, *args, &block)
end
end
EOL = "\r\n" EOL = "\r\n"
MULTIPART_BOUNDARY = "AaB03x"
def self.parse_multipart(env) def self.parse_multipart(env)
unless env['CONTENT_TYPE'] =~ unless env['CONTENT_TYPE'] =~
%r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|n
nil nil
else else
boundary = "--#{$1}" boundary = "--#{$1}"
@ -301,13 +351,16 @@ module Rack
buf = "" buf = ""
content_length = env['CONTENT_LENGTH'].to_i content_length = env['CONTENT_LENGTH'].to_i
input = env['rack.input'] input = env['rack.input']
input.rewind
boundary_size = boundary.size + EOL.size boundary_size = Utils.bytesize(boundary) + EOL.size
bufsize = 16384 bufsize = 16384
content_length -= boundary_size content_length -= boundary_size
status = input.read(boundary_size) read_buffer = ''
status = input.read(boundary_size, read_buffer)
raise EOFError, "bad content body" unless status == boundary + EOL raise EOFError, "bad content body" unless status == boundary + EOL
rx = /(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/n rx = /(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/n
@ -318,15 +371,15 @@ module Rack
filename = content_type = name = nil filename = content_type = name = nil
until head && buf =~ rx until head && buf =~ rx
if !head && i = buf.index("\r\n\r\n") if !head && i = buf.index(EOL+EOL)
head = buf.slice!(0, i+2) # First \r\n head = buf.slice!(0, i+2) # First \r\n
buf.slice!(0, 2) # Second \r\n buf.slice!(0, 2) # Second \r\n
filename = head[/Content-Disposition:.* filename="?([^\";]*)"?/ni, 1] filename = head[/Content-Disposition:.* filename="?([^\";]*)"?/ni, 1]
content_type = head[/Content-Type: (.*)\r\n/ni, 1] content_type = head[/Content-Type: (.*)#{EOL}/ni, 1]
name = head[/Content-Disposition:.* name="?([^\";]*)"?/ni, 1] name = head[/Content-Disposition:.*\s+name="?([^\";]*)"?/ni, 1] || head[/Content-ID:\s*([^#{EOL}]*)/ni, 1]
if filename if content_type || filename
body = Tempfile.new("RackMultipart") body = Tempfile.new("RackMultipart")
body.binmode if body.respond_to?(:binmode) body.binmode if body.respond_to?(:binmode)
end end
@ -339,7 +392,7 @@ module Rack
body << buf.slice!(0, buf.size - (boundary_size+4)) body << buf.slice!(0, buf.size - (boundary_size+4))
end end
c = input.read(bufsize < content_length ? bufsize : content_length) c = input.read(bufsize < content_length ? bufsize : content_length, read_buffer)
raise EOFError, "bad content body" if c.nil? || c.empty? raise EOFError, "bad content body" if c.nil? || c.empty?
buf << c buf << c
content_length -= c.size content_length -= c.size
@ -368,6 +421,12 @@ module Rack
data = {:filename => filename, :type => content_type, data = {:filename => filename, :type => content_type,
:name => name, :tempfile => body, :head => head} :name => name, :tempfile => body, :head => head}
elsif !filename && content_type
body.rewind
# Generic multipart cases, not coming from a form
data = {:type => content_type,
:name => name, :tempfile => body, :head => head}
else else
data = body data = body
end end
@ -377,16 +436,81 @@ module Rack
break if buf.empty? || content_length == -1 break if buf.empty? || content_length == -1
} }
begin input.rewind
input.rewind if input.respond_to?(:rewind)
rescue Errno::ESPIPE
# Handles exceptions raised by input streams that cannot be rewound
# such as when using plain CGI under Apache
end
params params
end end
end end
def self.build_multipart(params, first = true)
if first
unless params.is_a?(Hash)
raise ArgumentError, "value must be a Hash"
end
multipart = false
query = lambda { |value|
case value
when Array
value.each(&query)
when Hash
value.values.each(&query)
when UploadedFile
multipart = true
end
}
params.values.each(&query)
return nil unless multipart
end
flattened_params = Hash.new
params.each do |key, value|
k = first ? key.to_s : "[#{key}]"
case value
when Array
value.map { |v|
build_multipart(v, false).each { |subkey, subvalue|
flattened_params["#{k}[]#{subkey}"] = subvalue
}
}
when Hash
build_multipart(value, false).each { |subkey, subvalue|
flattened_params[k + subkey] = subvalue
}
else
flattened_params[k] = value
end
end
if first
flattened_params.map { |name, file|
if file.respond_to?(:original_filename)
::File.open(file.path, "rb") do |f|
f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
<<-EOF
--#{MULTIPART_BOUNDARY}\r
Content-Disposition: form-data; name="#{name}"; filename="#{Utils.escape(file.original_filename)}"\r
Content-Type: #{file.content_type}\r
Content-Length: #{::File.stat(file.path).size}\r
\r
#{f.read}\r
EOF
end
else
<<-EOF
--#{MULTIPART_BOUNDARY}\r
Content-Disposition: form-data; name="#{name}"\r
\r
#{file}\r
EOF
end
}.join + "--#{MULTIPART_BOUNDARY}--\r"
else
flattened_params
end
end
end end
end end
end end

View file

@ -2,7 +2,7 @@ module ActionPack #:nodoc:
module VERSION #:nodoc: module VERSION #:nodoc:
MAJOR = 2 MAJOR = 2
MINOR = 3 MINOR = 3
TINY = 2 TINY = 3
STRING = [MAJOR, MINOR, TINY].join('.') STRING = [MAJOR, MINOR, TINY].join('.')
end end

View file

@ -11,7 +11,7 @@ module ActionView #:nodoc:
autoload :FormHelper, 'action_view/helpers/form_helper' autoload :FormHelper, 'action_view/helpers/form_helper'
autoload :FormOptionsHelper, 'action_view/helpers/form_options_helper' autoload :FormOptionsHelper, 'action_view/helpers/form_options_helper'
autoload :FormTagHelper, 'action_view/helpers/form_tag_helper' autoload :FormTagHelper, 'action_view/helpers/form_tag_helper'
autoload :JavascriptHelper, 'action_view/helpers/javascript_helper' autoload :JavaScriptHelper, 'action_view/helpers/javascript_helper'
autoload :NumberHelper, 'action_view/helpers/number_helper' autoload :NumberHelper, 'action_view/helpers/number_helper'
autoload :PrototypeHelper, 'action_view/helpers/prototype_helper' autoload :PrototypeHelper, 'action_view/helpers/prototype_helper'
autoload :RecordIdentificationHelper, 'action_view/helpers/record_identification_helper' autoload :RecordIdentificationHelper, 'action_view/helpers/record_identification_helper'

View file

@ -272,14 +272,17 @@ module ActionView
# javascript_include_tag :all, :cache => true, :recursive => true # javascript_include_tag :all, :cache => true, :recursive => true
def javascript_include_tag(*sources) def javascript_include_tag(*sources)
options = sources.extract_options!.stringify_keys options = sources.extract_options!.stringify_keys
cache = options.delete("cache") concat = options.delete("concat")
cache = concat || options.delete("cache")
recursive = options.delete("recursive") recursive = options.delete("recursive")
if ActionController::Base.perform_caching && cache if concat || (ActionController::Base.perform_caching && cache)
joined_javascript_name = (cache == true ? "all" : cache) + ".js" joined_javascript_name = (cache == true ? "all" : cache) + ".js"
joined_javascript_path = File.join(JAVASCRIPTS_DIR, joined_javascript_name) joined_javascript_path = File.join(joined_javascript_name[/^#{File::SEPARATOR}/] ? ASSETS_DIR : JAVASCRIPTS_DIR, joined_javascript_name)
write_asset_file_contents(joined_javascript_path, compute_javascript_paths(sources, recursive)) unless File.exists?(joined_javascript_path) unless ActionController::Base.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) javascript_src_tag(joined_javascript_name, options)
else else
expand_javascript_sources(sources, recursive).collect { |source| javascript_src_tag(source, options) }.join("\n") expand_javascript_sources(sources, recursive).collect { |source| javascript_src_tag(source, options) }.join("\n")
@ -410,16 +413,25 @@ module ActionView
# The <tt>:recursive</tt> option is also available for caching: # The <tt>:recursive</tt> option is also available for caching:
# #
# stylesheet_link_tag :all, :cache => true, :recursive => true # stylesheet_link_tag :all, :cache => true, :recursive => true
#
# To force concatenation (even in development mode) set <tt>:concat</tt> 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) def stylesheet_link_tag(*sources)
options = sources.extract_options!.stringify_keys options = sources.extract_options!.stringify_keys
cache = options.delete("cache") concat = options.delete("concat")
cache = concat || options.delete("cache")
recursive = options.delete("recursive") recursive = options.delete("recursive")
if ActionController::Base.perform_caching && cache if concat || (ActionController::Base.perform_caching && cache)
joined_stylesheet_name = (cache == true ? "all" : cache) + ".css" joined_stylesheet_name = (cache == true ? "all" : cache) + ".css"
joined_stylesheet_path = File.join(STYLESHEETS_DIR, joined_stylesheet_name) joined_stylesheet_path = File.join(joined_stylesheet_name[/^#{File::SEPARATOR}/] ? ASSETS_DIR : STYLESHEETS_DIR, joined_stylesheet_name)
write_asset_file_contents(joined_stylesheet_path, compute_stylesheet_paths(sources, recursive)) unless File.exists?(joined_stylesheet_path) unless ActionController::Base.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) stylesheet_tag(joined_stylesheet_name, options)
else else
expand_stylesheet_sources(sources, recursive).collect { |source| stylesheet_tag(source, options) }.join("\n") expand_stylesheet_sources(sources, recursive).collect { |source| stylesheet_tag(source, options) }.join("\n")

View file

@ -493,7 +493,8 @@ module ActionView
# Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object
# assigned to the template (identified by +object+). The text of label will default to the attribute name unless you specify # assigned to the template (identified by +object+). The text of label will default to the attribute name unless you specify
# it explicitly. Additional options on the label tag can be passed as a hash with +options+. These options will be tagged # it explicitly. Additional options on the label tag can be passed as a hash with +options+. These options will be tagged
# onto the HTML as an HTML element attribute as in the example shown. # onto the HTML as an HTML element attribute as in the example shown, except for the <tt>:value</tt> option, which is designed to
# target labels for radio_button tags (where the value is used in the ID of the input tag).
# #
# ==== Examples # ==== Examples
# label(:post, :title) # label(:post, :title)
@ -505,6 +506,9 @@ module ActionView
# label(:post, :title, "A short title", :class => "title_label") # label(:post, :title, "A short title", :class => "title_label")
# # => <label for="post_title" class="title_label">A short title</label> # # => <label for="post_title" class="title_label">A short title</label>
# #
# label(:post, :privacy, "Public Post", :value => "public")
# # => <label for="post_privacy_public">Public Post</label>
#
def label(object_name, method, text = nil, options = {}) def label(object_name, method, text = nil, options = {})
InstanceTag.new(object_name, method, self, options.delete(:object)).to_label_tag(text, options) InstanceTag.new(object_name, method, self, options.delete(:object)).to_label_tag(text, options)
end end
@ -720,8 +724,9 @@ module ActionView
def to_label_tag(text = nil, options = {}) def to_label_tag(text = nil, options = {})
options = options.stringify_keys options = options.stringify_keys
tag_value = options.delete("value")
name_and_id = options.dup name_and_id = options.dup
add_default_name_and_id(name_and_id) add_default_name_and_id_for_value(tag_value, name_and_id)
options.delete("index") options.delete("index")
options["for"] ||= name_and_id["id"] options["for"] ||= name_and_id["id"]
content = (text.blank? ? nil : text.to_s) || method_name.humanize content = (text.blank? ? nil : text.to_s) || method_name.humanize
@ -753,11 +758,7 @@ module ActionView
checked = self.class.radio_button_checked?(value(object), tag_value) checked = self.class.radio_button_checked?(value(object), tag_value)
end end
options["checked"] = "checked" if checked options["checked"] = "checked" if checked
pretty_tag_value = tag_value.to_s.gsub(/\s/, "_").gsub(/\W/, "").downcase add_default_name_and_id_for_value(tag_value, options)
options["id"] ||= defined?(@auto_index) ?
"#{tag_id_with_index(@auto_index)}_#{pretty_tag_value}" :
"#{tag_id}_#{pretty_tag_value}"
add_default_name_and_id(options)
tag("input", options) tag("input", options)
end end
@ -858,6 +859,17 @@ module ActionView
end end
private private
def add_default_name_and_id_for_value(tag_value, options)
if tag_value
pretty_tag_value = tag_value.to_s.gsub(/\s/, "_").gsub(/\W/, "").downcase
specified_id = options["id"]
add_default_name_and_id(options)
options["id"] += "_#{pretty_tag_value}" unless specified_id
else
add_default_name_and_id(options)
end
end
def add_default_name_and_id(options) def add_default_name_and_id(options)
if options.has_key?("index") if options.has_key?("index")
options["name"] ||= tag_name_with_index(options["index"]) options["name"] ||= tag_name_with_index(options["index"])
@ -905,6 +917,7 @@ module ActionView
attr_accessor :object_name, :object, :options attr_accessor :object_name, :object, :options
def initialize(object_name, object, template, options, proc) def initialize(object_name, object, template, options, proc)
@nested_child_index = {}
@object_name, @object, @template, @options, @proc = object_name, object, template, options, proc @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc
@default_options = @options ? @options.slice(:index) : {} @default_options = @options ? @options.slice(:index) : {}
if @object_name.to_s.match(/\[\]$/) if @object_name.to_s.match(/\[\]$/)
@ -1007,7 +1020,7 @@ module ActionView
explicit_child_index = args.last[:child_index] if args.last.is_a?(Hash) explicit_child_index = args.last[:child_index] if args.last.is_a?(Hash)
children.map do |child| children.map do |child|
fields_for_nested_model("#{name}[#{explicit_child_index || nested_child_index}]", child, args, block) fields_for_nested_model("#{name}[#{explicit_child_index || nested_child_index(name)}]", child, args, block)
end.join end.join
else else
fields_for_nested_model(name, explicit_object || association, args, block) fields_for_nested_model(name, explicit_object || association, args, block)
@ -1025,9 +1038,9 @@ module ActionView
end end
end end
def nested_child_index def nested_child_index(name)
@nested_child_index ||= -1 @nested_child_index[name] ||= -1
@nested_child_index += 1 @nested_child_index[name] += 1
end end
end end
end end

View file

@ -230,6 +230,8 @@ module ActionView
# #
# NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag. # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag.
def options_for_select(container, selected = nil) def options_for_select(container, selected = nil)
return container if String === container
container = container.to_a if Hash === container container = container.to_a if Hash === container
selected, disabled = extract_selected_and_disabled(selected) selected, disabled = extract_selected_and_disabled(selected)

View file

@ -230,6 +230,8 @@ module ActionView
# * <tt>:rows</tt> - Specify the number of rows in the textarea # * <tt>:rows</tt> - Specify the number of rows in the textarea
# * <tt>:cols</tt> - Specify the number of columns in the textarea # * <tt>:cols</tt> - Specify the number of columns in the textarea
# * <tt>:disabled</tt> - If set to true, the user will not be able to use this input. # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
# * <tt>:escape</tt> - By default, the contents of the text input are HTML escaped.
# If you need unescaped contents, set this to false.
# * Any other key creates standard HTML attributes for the tag. # * Any other key creates standard HTML attributes for the tag.
# #
# ==== Examples # ==== Examples
@ -257,7 +259,10 @@ module ActionView
options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split) options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split)
end end
content_tag :textarea, content, { "name" => name, "id" => name }.update(options.stringify_keys) escape = options.key?("escape") ? options.delete("escape") : true
content = html_escape(content) if escape
content_tag :textarea, content, { "name" => name, "id" => sanitize_to_id(name) }.update(options.stringify_keys)
end end
# Creates a check box form input tag. # Creates a check box form input tag.
@ -353,7 +358,8 @@ module ActionView
disable_with << ";#{options.delete('onclick')}" if options['onclick'] disable_with << ";#{options.delete('onclick')}" if options['onclick']
options["onclick"] = "if (window.hiddenCommit) { window.hiddenCommit.setAttribute('value', this.value); }" options["onclick"] = "if (window.hiddenCommit) { window.hiddenCommit.setAttribute('value', this.value); }"
options["onclick"] << "else { hiddenCommit = this.cloneNode(false);hiddenCommit.setAttribute('type', 'hidden');this.form.appendChild(hiddenCommit); }" options["onclick"] << "else { hiddenCommit = document.createElement('input');hiddenCommit.type = 'hidden';"
options["onclick"] << "hiddenCommit.value = this.value;hiddenCommit.name = this.name;this.form.appendChild(hiddenCommit); }"
options["onclick"] << "this.setAttribute('originalValue', this.value);this.disabled = true;#{disable_with};" options["onclick"] << "this.setAttribute('originalValue', this.value);this.disabled = true;#{disable_with};"
options["onclick"] << "result = (this.form.onsubmit ? (this.form.onsubmit() ? this.form.submit() : false) : this.form.submit());" options["onclick"] << "result = (this.form.onsubmit ? (this.form.onsubmit() ? this.form.submit() : false) : this.form.submit());"
options["onclick"] << "if (result == false) { this.value = this.getAttribute('originalValue');this.disabled = false; }return result;" options["onclick"] << "if (result == false) { this.value = this.getAttribute('originalValue');this.disabled = false; }return result;"
@ -444,10 +450,10 @@ module ActionView
'' ''
when /^post$/i, "", nil when /^post$/i, "", nil
html_options["method"] = "post" html_options["method"] = "post"
protect_against_forgery? ? content_tag(:div, token_tag, :style => 'margin:0;padding:0') : '' protect_against_forgery? ? content_tag(:div, token_tag, :style => 'margin:0;padding:0;display:inline') : ''
else else
html_options["method"] = "post" html_options["method"] = "post"
content_tag(:div, tag(:input, :type => "hidden", :name => "_method", :value => method) + token_tag, :style => 'margin:0;padding:0') content_tag(:div, tag(:input, :type => "hidden", :name => "_method", :value => method) + token_tag, :style => 'margin:0;padding:0;display:inline')
end end
end end

View file

@ -686,7 +686,7 @@ module ActionView
# Returns an object whose <tt>to_json</tt> evaluates to +code+. Use this to pass a literal JavaScript # Returns an object whose <tt>to_json</tt> evaluates to +code+. Use this to pass a literal JavaScript
# expression as an argument to another JavaScriptGenerator method. # expression as an argument to another JavaScriptGenerator method.
def literal(code) def literal(code)
ActiveSupport::JSON::Variable.new(code.to_s) ::ActiveSupport::JSON::Variable.new(code.to_s)
end end
# Returns a collection reference by finding it through a CSS +pattern+ in the DOM. This collection can then be # Returns a collection reference by finding it through a CSS +pattern+ in the DOM. This collection can then be
@ -973,7 +973,7 @@ module ActionView
def loop_on_multiple_args(method, ids) def loop_on_multiple_args(method, ids)
record(ids.size>1 ? record(ids.size>1 ?
"#{javascript_object_for(ids)}.each(#{method})" : "#{javascript_object_for(ids)}.each(#{method})" :
"#{method}(#{ids.first.to_json})") "#{method}(#{::ActiveSupport::JSON.encode(ids.first)})")
end end
def page def page
@ -997,7 +997,7 @@ module ActionView
end end
def javascript_object_for(object) def javascript_object_for(object)
object.respond_to?(:to_json) ? object.to_json : object.inspect ::ActiveSupport::JSON.encode(object)
end end
def arguments_for_call(arguments, block = nil) def arguments_for_call(arguments, block = nil)
@ -1139,7 +1139,7 @@ module ActionView
class JavaScriptElementProxy < JavaScriptProxy #:nodoc: class JavaScriptElementProxy < JavaScriptProxy #:nodoc:
def initialize(generator, id) def initialize(generator, id)
@id = id @id = id
super(generator, "$(#{id.to_json})") super(generator, "$(#{::ActiveSupport::JSON.encode(id)})")
end end
# Allows access of element attributes through +attribute+. Examples: # Allows access of element attributes through +attribute+. Examples:
@ -1211,7 +1211,7 @@ module ActionView
enumerate :eachSlice, :variable => variable, :method_args => [number], :yield_args => %w(value index), :return => true, &block enumerate :eachSlice, :variable => variable, :method_args => [number], :yield_args => %w(value index), :return => true, &block
else else
add_variable_assignment!(variable) add_variable_assignment!(variable)
append_enumerable_function!("eachSlice(#{number.to_json});") append_enumerable_function!("eachSlice(#{::ActiveSupport::JSON.encode(number)});")
end end
end end
@ -1232,7 +1232,7 @@ module ActionView
def pluck(variable, property) def pluck(variable, property)
add_variable_assignment!(variable) add_variable_assignment!(variable)
append_enumerable_function!("pluck(#{property.to_json});") append_enumerable_function!("pluck(#{::ActiveSupport::JSON.encode(property)});")
end end
def zip(variable, *arguments, &block) def zip(variable, *arguments, &block)
@ -1296,7 +1296,7 @@ module ActionView
class JavaScriptElementCollectionProxy < JavaScriptCollectionProxy #:nodoc:\ class JavaScriptElementCollectionProxy < JavaScriptCollectionProxy #:nodoc:\
def initialize(generator, pattern) def initialize(generator, pattern)
super(generator, "$$(#{pattern.to_json})") super(generator, "$$(#{::ActiveSupport::JSON.encode(pattern)})")
end end
end end
end end

View file

@ -43,7 +43,7 @@ module ActionView
# You can change the behaviour with various options, see # You can change the behaviour with various options, see
# http://script.aculo.us for more documentation. # http://script.aculo.us for more documentation.
def visual_effect(name, element_id = false, js_options = {}) def visual_effect(name, element_id = false, js_options = {})
element = element_id ? element_id.to_json : "element" element = element_id ? ActiveSupport::JSON.encode(element_id) : "element"
js_options[:queue] = if js_options[:queue].is_a?(Hash) js_options[:queue] = if js_options[:queue].is_a?(Hash)
'{' + js_options[:queue].map {|k, v| k == :limit ? "#{k}:#{v}" : "#{k}:'#{v}'" }.join(',') + '}' '{' + js_options[:queue].map {|k, v| k == :limit ? "#{k}:#{v}" : "#{k}:'#{v}'" }.join(',') + '}'
@ -138,7 +138,7 @@ module ActionView
end end
def sortable_element_js(element_id, options = {}) #:nodoc: def sortable_element_js(element_id, options = {}) #:nodoc:
options[:with] ||= "Sortable.serialize(#{element_id.to_json})" options[:with] ||= "Sortable.serialize(#{ActiveSupport::JSON.encode(element_id)})"
options[:onUpdate] ||= "function(){" + remote_function(options) + "}" options[:onUpdate] ||= "function(){" + remote_function(options) + "}"
options.delete_if { |key, value| PrototypeHelper::AJAX_OPTIONS.include?(key) } options.delete_if { |key, value| PrototypeHelper::AJAX_OPTIONS.include?(key) }
@ -149,7 +149,7 @@ module ActionView
options[:containment] = array_or_string_for_javascript(options[:containment]) if options[:containment] options[:containment] = array_or_string_for_javascript(options[:containment]) if options[:containment]
options[:only] = array_or_string_for_javascript(options[:only]) if options[:only] options[:only] = array_or_string_for_javascript(options[:only]) if options[:only]
%(Sortable.create(#{element_id.to_json}, #{options_for_javascript(options)});) %(Sortable.create(#{ActiveSupport::JSON.encode(element_id)}, #{options_for_javascript(options)});)
end end
# Makes the element with the DOM ID specified by +element_id+ draggable. # Makes the element with the DOM ID specified by +element_id+ draggable.
@ -164,7 +164,7 @@ module ActionView
end end
def draggable_element_js(element_id, options = {}) #:nodoc: def draggable_element_js(element_id, options = {}) #:nodoc:
%(new Draggable(#{element_id.to_json}, #{options_for_javascript(options)});) %(new Draggable(#{ActiveSupport::JSON.encode(element_id)}, #{options_for_javascript(options)});)
end end
# Makes the element with the DOM ID specified by +element_id+ receive # Makes the element with the DOM ID specified by +element_id+ receive
@ -219,7 +219,7 @@ module ActionView
# Confirmation happens during the onDrop callback, so it can be removed from the options # Confirmation happens during the onDrop callback, so it can be removed from the options
options.delete(:confirm) if options[:confirm] options.delete(:confirm) if options[:confirm]
%(Droppables.add(#{element_id.to_json}, #{options_for_javascript(options)});) %(Droppables.add(#{ActiveSupport::JSON.encode(element_id)}, #{options_for_javascript(options)});)
end end
end end
end end

View file

@ -271,8 +271,8 @@ module ActionView
end end
# Returns the text with all the Markdown codes turned into HTML tags. # Returns the text with all the Markdown codes turned into HTML tags.
# <i>This method requires BlueCloth[http://www.deveiate.org/projects/BlueCloth] # <i>This method requires BlueCloth[http://www.deveiate.org/projects/BlueCloth] or another
# to be available</i>. # Markdown library to be installed.</i>.
# #
# ==== Examples # ==== Examples
# markdown("We are using __Markdown__ now!") # markdown("We are using __Markdown__ now!")
@ -288,7 +288,7 @@ module ActionView
# markdown('![The ROR logo](http://rubyonrails.com/images/rails.png "Ruby on Rails")') # markdown('![The ROR logo](http://rubyonrails.com/images/rails.png "Ruby on Rails")')
# # => '<p><img src="http://rubyonrails.com/images/rails.png" alt="The ROR logo" title="Ruby on Rails" /></p>' # # => '<p><img src="http://rubyonrails.com/images/rails.png" alt="The ROR logo" title="Ruby on Rails" /></p>'
def markdown(text) def markdown(text)
text.blank? ? "" : BlueCloth.new(text).to_html text.blank? ? "" : Markdown.new(text).to_html
end end
# Returns +text+ transformed into HTML using simple formatting rules. # Returns +text+ transformed into HTML using simple formatting rules.

View file

@ -1,4 +1,4 @@
require 'action_view/helpers/javascript_helper' #require 'action_view/helpers/javascript_helper'
module ActionView module ActionView
module Helpers #:nodoc: module Helpers #:nodoc:

View file

@ -61,7 +61,7 @@ module ActionView #:nodoc:
end end
end end
return Template.new(original_template_path, original_template_path.to_s =~ /\A\// ? "" : ".") if File.file?(original_template_path) return Template.new(original_template_path) if File.file?(original_template_path)
raise MissingTemplate.new(self, original_template_path, format) raise MissingTemplate.new(self, original_template_path, format)
end end

View file

@ -107,9 +107,8 @@ module ActionView #:nodoc:
attr_accessor :locale, :name, :format, :extension attr_accessor :locale, :name, :format, :extension
delegate :to_s, :to => :path delegate :to_s, :to => :path
def initialize(template_path, load_path) def initialize(template_path, load_path = nil)
@template_path = template_path.dup @template_path, @load_path = template_path.dup, load_path
@load_path, @filename = load_path, File.join(load_path, template_path)
@base_path, @name, @locale, @format, @extension = split(template_path) @base_path, @name, @locale, @format, @extension = split(template_path)
@base_path.to_s.gsub!(/\/$/, '') # Push to split method @base_path.to_s.gsub!(/\/$/, '') # Push to split method
@ -180,6 +179,12 @@ module ActionView #:nodoc:
@@exempt_from_layout.any? { |exempted| path =~ exempted } @@exempt_from_layout.any? { |exempted| path =~ exempted }
end end
def filename
# no load_path means this is an "absolute pathed" template
load_path ? File.join(load_path, template_path) : template_path
end
memoize :filename
def source def source
File.read(filename) File.read(filename)
end end
@ -212,46 +217,30 @@ module ActionView #:nodoc:
end end
def valid_locale?(locale) def valid_locale?(locale)
I18n.available_locales.include?(locale.to_sym) locale && I18n.available_locales.include?(locale.to_sym)
end end
# Returns file split into an array # Returns file split into an array
# [base_path, name, locale, format, extension] # [base_path, name, locale, format, extension]
def split(file) def split(file)
if m = file.to_s.match(/^(.*\/)?([^\.]+)\.(.*)$/) if m = file.to_s.match(/^(.*\/)?([^\.]+)\.(.*)$/)
base_path = m[1] [m[1], m[2], *parse_extensions(m[3])]
name = m[2]
extensions = m[3]
else
return
end
locale = nil
format = nil
extension = nil
if m = extensions.split(".")
if valid_locale?(m[0]) && m[1] && valid_extension?(m[2]) # All three
locale = m[0]
format = m[1]
extension = m[2]
elsif m[0] && m[1] && valid_extension?(m[2]) # Multipart formats
format = "#{m[0]}.#{m[1]}"
extension = m[2]
elsif valid_locale?(m[0]) && valid_extension?(m[1]) # locale and extension
locale = m[0]
extension = m[1]
elsif valid_extension?(m[1]) # format and extension
format = m[0]
extension = m[1]
elsif valid_extension?(m[0]) # Just extension
extension = m[0]
else # No extension
format = m[0]
end end
end end
[base_path, name, locale, format, extension] # returns parsed extensions as an array
# [locale, format, extension]
def parse_extensions(extensions)
exts = extensions.split(".")
if extension = valid_extension?(exts.last) && exts.pop || nil
locale = valid_locale?(exts.first) && exts.shift || nil
format = exts.join('.') if exts.any? # join('.') is needed for multipart templates
else # no extension, just format
format = exts.last
end
[locale, format, extension]
end end
end end
end end

View file

@ -8,7 +8,7 @@ require 'yaml'
require 'stringio' require 'stringio'
require 'test/unit' require 'test/unit'
gem 'mocha', '>= 0.9.5' gem 'mocha', '>= 0.9.7'
require 'mocha' require 'mocha'
begin begin

View file

@ -27,9 +27,9 @@ class ActiveRecordStoreTest < ActionController::IntegrationTest
end end
def call_reset_session def call_reset_session
session[:bar] session[:foo]
reset_session reset_session
session[:bar] = "baz" session[:foo] = "baz"
head :ok head :ok
end end
@ -86,7 +86,7 @@ class ActiveRecordStoreTest < ActionController::IntegrationTest
get '/get_session_value' get '/get_session_value'
assert_response :success assert_response :success
assert_equal 'foo: nil', response.body assert_equal 'foo: "baz"', response.body
get '/get_session_id' get '/get_session_id'
assert_response :success assert_response :success

View file

@ -12,6 +12,9 @@ class ActionPackAssertionsController < ActionController::Base
# a standard template # a standard template
def hello_xml_world() render :template => "test/hello_xml_world"; end def hello_xml_world() render :template => "test/hello_xml_world"; end
# a standard partial
def partial() render :partial => 'test/partial'; end
# a redirect to an internal location # a redirect to an internal location
def redirect_internal() redirect_to "/nothing"; end def redirect_internal() redirect_to "/nothing"; end
@ -333,6 +336,30 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
assert 'hello_world', @response.rendered[:template].to_s assert 'hello_world', @response.rendered[:template].to_s
end end
def test_assert_template_with_partial
get :partial
assert_template :partial => '_partial'
end
def test_assert_template_with_nil
get :nothing
assert_template nil
end
def test_assert_template_with_string
get :hello_world
assert_template 'hello_world'
end
def test_assert_template_with_symbol
get :hello_world
assert_template :hello_world
end
def test_assert_template_with_bad_argument
assert_raise(ArgumentError) { assert_template 1 }
end
# check the redirection location # check the redirection location
def test_redirection_location def test_redirection_location
process :redirect_internal process :redirect_internal

View file

@ -1,5 +1,6 @@
require 'fileutils' require 'fileutils'
require 'abstract_unit' require 'abstract_unit'
require 'active_record_unit'
CACHE_DIR = 'test_cache' CACHE_DIR = 'test_cache'
# Don't change '/../temp/' cavalierly or you might hose something you don't want hosed # Don't change '/../temp/' cavalierly or you might hose something you don't want hosed
@ -7,6 +8,10 @@ FILE_STORE_PATH = File.join(File.dirname(__FILE__), '/../temp/', CACHE_DIR)
ActionController::Base.page_cache_directory = FILE_STORE_PATH ActionController::Base.page_cache_directory = FILE_STORE_PATH
ActionController::Base.cache_store = :file_store, FILE_STORE_PATH ActionController::Base.cache_store = :file_store, FILE_STORE_PATH
# Force sweeper classes to load
ActionController::Caching::Sweeper
ActionController::Caching::Sweeping
class PageCachingTestController < ActionController::Base class PageCachingTestController < ActionController::Base
caches_page :ok, :no_content, :if => Proc.new { |c| !c.request.format.json? } caches_page :ok, :no_content, :if => Proc.new { |c| !c.request.format.json? }
caches_page :found, :not_found caches_page :found, :not_found
@ -152,6 +157,7 @@ class ActionCachingTestController < ActionController::Base
caches_action :edit, :cache_path => Proc.new { |c| c.params[:id] ? "http://test.host/#{c.params[:id]};edit" : "http://test.host/edit" } caches_action :edit, :cache_path => Proc.new { |c| c.params[:id] ? "http://test.host/#{c.params[:id]};edit" : "http://test.host/edit" }
caches_action :with_layout caches_action :with_layout
caches_action :layout_false, :layout => false caches_action :layout_false, :layout => false
caches_action :record_not_found, :four_oh_four, :simple_runtime_error
layout 'talk_from_action.erb' layout 'talk_from_action.erb'
@ -174,6 +180,18 @@ class ActionCachingTestController < ActionController::Base
render :text => @cache_this, :layout => true render :text => @cache_this, :layout => true
end end
def record_not_found
raise ActiveRecord::RecordNotFound, "oops!"
end
def four_oh_four
render :text => "404'd!", :status => 404
end
def simple_runtime_error
raise "oops!"
end
alias_method :show, :index alias_method :show, :index
alias_method :edit, :index alias_method :edit, :index
alias_method :destroy, :index alias_method :destroy, :index
@ -456,6 +474,27 @@ class ActionCacheTest < ActionController::TestCase
assert_response :success assert_response :success
end end
def test_record_not_found_returns_404_for_multiple_requests
get :record_not_found
assert_response 404
get :record_not_found
assert_response 404
end
def test_four_oh_four_returns_404_for_multiple_requests
get :four_oh_four
assert_response 404
get :four_oh_four
assert_response 404
end
def test_simple_runtime_error_returns_500_for_multiple_requests
get :simple_runtime_error
assert_response 500
get :simple_runtime_error
assert_response 500
end
private private
def content_to_cache def content_to_cache
assigns(:cache_this) assigns(:cache_this)

View file

@ -6,6 +6,10 @@ class CookieTest < ActionController::TestCase
cookies["user_name"] = "david" cookies["user_name"] = "david"
end end
def set_with_with_escapable_characters
cookies["that & guy"] = "foo & bar => baz"
end
def authenticate_for_fourteen_days def authenticate_for_fourteen_days
cookies["user_name"] = { "value" => "david", "expires" => Time.utc(2005, 10, 10,5) } cookies["user_name"] = { "value" => "david", "expires" => Time.utc(2005, 10, 10,5) }
end end
@ -53,6 +57,12 @@ class CookieTest < ActionController::TestCase
assert_equal({"user_name" => "david"}, @response.cookies) assert_equal({"user_name" => "david"}, @response.cookies)
end end
def test_setting_with_escapable_characters
get :set_with_with_escapable_characters
assert_equal ["that+%26+guy=foo+%26+bar+%3D%3E+baz; path=/"], @response.headers["Set-Cookie"]
assert_equal({"that & guy" => "foo & bar => baz"}, @response.cookies)
end
def test_setting_cookie_for_fourteen_days def test_setting_cookie_for_fourteen_days
get :authenticate_for_fourteen_days get :authenticate_for_fourteen_days
assert_equal ["user_name=david; path=/; expires=Mon, 10-Oct-2005 05:00:00 GMT"], @response.headers["Set-Cookie"] assert_equal ["user_name=david; path=/; expires=Mon, 10-Oct-2005 05:00:00 GMT"], @response.headers["Set-Cookie"]

View file

@ -25,7 +25,8 @@ class DispatcherTest < Test::Unit::TestCase
def test_clears_dependencies_after_dispatch_if_in_loading_mode def test_clears_dependencies_after_dispatch_if_in_loading_mode
ActiveSupport::Dependencies.expects(:clear).once ActiveSupport::Dependencies.expects(:clear).once
dispatch(false) # Close the response so dependencies kicks in
dispatch(false).last.close
end end
def test_reloads_routes_before_dispatch_if_in_loading_mode def test_reloads_routes_before_dispatch_if_in_loading_mode
@ -49,13 +50,14 @@ class DispatcherTest < Test::Unit::TestCase
Dispatcher.any_instance.expects(:dispatch).raises('b00m') Dispatcher.any_instance.expects(:dispatch).raises('b00m')
ActionController::Failsafe.any_instance.expects(:log_failsafe_exception) ActionController::Failsafe.any_instance.expects(:log_failsafe_exception)
response = nil
assert_nothing_raised do assert_nothing_raised do
assert_equal [ response = dispatch
500,
{"Content-Type" => "text/html"},
"<html><body><h1>500 Internal Server Error</h1></body></html>"
], dispatch
end end
assert_equal 3, response.size
assert_equal 500, response[0]
assert_equal({"Content-Type" => "text/html"}, response[1])
assert_match /500 Internal Server Error/, response[2].join
end end
def test_prepare_callbacks def test_prepare_callbacks
@ -94,7 +96,7 @@ class DispatcherTest < Test::Unit::TestCase
def dispatch(cache_classes = true) def dispatch(cache_classes = true)
ActionController::Routing::RouteSet.any_instance.stubs(:call).returns([200, {}, 'response']) ActionController::Routing::RouteSet.any_instance.stubs(:call).returns([200, {}, 'response'])
Dispatcher.define_dispatcher_callbacks(cache_classes) Dispatcher.define_dispatcher_callbacks(cache_classes)
Dispatcher.new.call({}) Dispatcher.new.call({'rack.input' => StringIO.new('')})
end end
def assert_subclasses(howmany, klass, message = klass.subclasses.inspect) def assert_subclasses(howmany, klass, message = klass.subclasses.inspect)

View file

@ -0,0 +1,60 @@
require 'abstract_unit'
require 'stringio'
require 'logger'
class FailsafeTest < ActionController::TestCase
FIXTURE_PUBLIC = "#{File.dirname(__FILE__)}/../fixtures/failsafe".freeze
def setup
@old_error_file_path = ActionController::Failsafe.error_file_path
ActionController::Failsafe.error_file_path = FIXTURE_PUBLIC
@app = mock
@log_io = StringIO.new
@logger = Logger.new(@log_io)
@failsafe = ActionController::Failsafe.new(@app)
@failsafe.stubs(:failsafe_logger).returns(@logger)
end
def teardown
ActionController::Failsafe.error_file_path = @old_error_file_path
end
def app_will_raise_error!
@app.expects(:call).then.raises(RuntimeError.new("Printer on fire"))
end
def test_calls_app_and_returns_its_return_value
@app.expects(:call).returns([200, { "Content-Type" => "text/html" }, "ok"])
assert_equal [200, { "Content-Type" => "text/html" }, "ok"], @failsafe.call({})
end
def test_writes_to_log_file_on_exception
app_will_raise_error!
@failsafe.call({})
assert_match /Printer on fire/, @log_io.string # Logs exception message.
assert_match /failsafe_test\.rb/, @log_io.string # Logs backtrace.
end
def test_returns_500_internal_server_error_on_exception
app_will_raise_error!
response = @failsafe.call({})
assert_equal 3, response.size # It is a valid Rack response.
assert_equal 500, response[0] # Status is 500.
end
def test_renders_error_page_file_with_erb
app_will_raise_error!
response = @failsafe.call({})
assert_equal 500, response[0]
assert_equal "hello my world", response[2].join
end
def test_returns_a_default_message_if_erb_rendering_failed
app_will_raise_error!
@failsafe.expects(:render_template).raises(RuntimeError.new("Harddisk is crashing"))
response = @failsafe.call({})
assert_equal 500, response[0]
assert_match /500 Internal Server Error/, response[2].join
assert_match %r(please read this web application's log file), response[2].join
end
end

View file

@ -23,7 +23,8 @@ class FilterParamTest < Test::Unit::TestCase
[{'foo'=>'bar', 'bar'=>'foo'},{'foo'=>'[FILTERED]', 'bar'=>'foo'},%w'foo baz'], [{'foo'=>'bar', 'bar'=>'foo'},{'foo'=>'[FILTERED]', 'bar'=>'foo'},%w'foo baz'],
[{'foo'=>'bar', 'baz'=>'foo'},{'foo'=>'[FILTERED]', 'baz'=>'[FILTERED]'},%w'foo baz'], [{'foo'=>'bar', 'baz'=>'foo'},{'foo'=>'[FILTERED]', 'baz'=>'[FILTERED]'},%w'foo baz'],
[{'bar'=>{'foo'=>'bar','bar'=>'foo'}},{'bar'=>{'foo'=>'[FILTERED]','bar'=>'foo'}},%w'fo'], [{'bar'=>{'foo'=>'bar','bar'=>'foo'}},{'bar'=>{'foo'=>'[FILTERED]','bar'=>'foo'}},%w'fo'],
[{'foo'=>{'foo'=>'bar','bar'=>'foo'}},{'foo'=>'[FILTERED]'},%w'f banana']] [{'foo'=>{'foo'=>'bar','bar'=>'foo'}},{'foo'=>'[FILTERED]'},%w'f banana'],
[{'baz'=>[{'foo'=>'baz'}]}, {'baz'=>[{'foo'=>'[FILTERED]'}]}, %w(foo)]]
test_hashes.each do |before_filter, after_filter, filter_words| test_hashes.each do |before_filter, after_filter, filter_words|
FilterParamController.filter_parameter_logging(*filter_words) FilterParamController.filter_parameter_logging(*filter_words)

Some files were not shown because too many files have changed in this diff Show more