git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
David Heinemeier Hansson 2004-11-24 01:04:44 +00:00
commit db045dbbf6
296 changed files with 30881 additions and 0 deletions

19
actionmailer/CHANGELOG Normal file
View File

@ -0,0 +1,19 @@
*0.4* (5)
* Consolidated the server configuration options into Base#server_settings= and expanded that with controls for authentication and more [Marten]
NOTE: This is an API change that could potentially break your application if you used the old application form. Please do change!
* Added Base#deliveries as an accessor for an array of emails sent out through that ActionMailer class when using the :test delivery option. [bitsweat]
* Added Base#perform_deliveries= which can be set to false to turn off the actual delivery of the email through smtp or sendmail.
This is especially useful for functional testing that shouldn't send off real emails, but still trigger delivery_* methods.
* Added option to specify delivery method with Base#delivery_method=. Default is :smtp and :sendmail is currently the only other option.
Sendmail is assumed to be present at "/usr/sbin/sendmail" if that option is used. [Kent Sibilev]
* Dropped "include TMail" as it added to much baggage into the default namespace (like Version) [Chad Fowler]
*0.3*
* First release

21
actionmailer/MIT-LICENSE Normal file
View File

@ -0,0 +1,21 @@
Copyright (c) 2004 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

102
actionmailer/README Executable file
View File

@ -0,0 +1,102 @@
= Action Mailer -- Easy email delivery and testing
Action Mailer is framework for designing email-service layers. These layers
are used to consolidate code for sending out forgotten passwords, welcoming
wishes on signup, invoices for billing, and any other use case that requires
a written notification to either a person or another system.
The framework works by setting up all the email details, except the body,
in methods on the service layer. Subject, recipients, sender, and timestamp
are all set up this way. An example of such a method:
def signed_up(recipient)
@recipients = recipient
@subject = "[Signed up] Welcome #{recipient}"
@from = "system@loudthinking.com"
@sent_on = Time.local(2004, 12, 12)
@body["recipient"] = recipient
end
The body of the email is created by using an Action View template (regular
ERb) that has the content of the @body hash available as instance variables.
So the corresponding body template for the method above could look like this:
Hello there,
Mr. <%= @recipient %>
And if the recipient was given as "david@loudthinking.com", the email
generated would look like this:
Date: Sun, 12 Dec 2004 00:00:00 +0100
From: system@loudthinking.com
To: david@loudthinking.com
Subject: [Signed up] Welcome david@loudthinking.com
Hello there,
Mr. david@loudthinking.com
You never actually call the instance methods like signed_up directly. Instead,
you call class methods like deliver_* and create_* that are automatically
created for each instance method. So if the signed_up method sat on
ApplicationMailer, it would look like this:
ApplicationMailer.create_signed_up("david@loudthinking.com") # => tmail object for testing
ApplicationMailer.deliver_signed_up("david@loudthinking.com") # sends the email
ApplicationMailer.new.signed_up("david@loudthinking.com") # won't work!
== Dependencies
Action Mailer requires that the Action Pack is either available to be required immediately
or is accessible as a GEM.
== Bundled software
* tmail 0.10.8 by Minero Aoki released under LGPL
Read more on http://i.loveruby.net/en/prog/tmail.html
* Text::Format 0.63 by Austin Ziegler released under OpenSource
Read more on http://www.halostatue.ca/ruby/Text__Format.html
== Download
The latest version of Action Mailer can be found at
* http://rubyforge.org/project/showfiles.php?group_id=361
Documentation can be found at
* http://actionmailer.rubyonrails.org
== Installation
You can install Action Mailer with the following command.
% [sudo] ruby install.rb
from its distribution directory.
== License
Action Mailer is released under the MIT license.
== Support
The Action Mailer homepage is http://actionmailer.rubyonrails.org. You can find
the Action Mailer RubyForge page at http://rubyforge.org/projects/actionmailer.
And as Jim from Rake says:
Feel free to submit commits or feature requests. If you send a patch,
remember to update the corresponding unit tests. If fact, I prefer
new feature to be submitted in the form of new unit tests.
For other information, feel free to ask on the ruby-talk mailing list (which
is mirrored to comp.lang.ruby) or contact mailto:david@loudthinking.com.

107
actionmailer/Rakefile Executable file
View File

@ -0,0 +1,107 @@
require 'rubygems'
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
require 'rake/packagetask'
require 'rake/gempackagetask'
require 'rake/contrib/rubyforgepublisher'
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
PKG_NAME = 'actionmailer'
PKG_VERSION = '0.4.0' + PKG_BUILD
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
desc "Default Task"
task :default => [ :test ]
# Run the unit tests
Rake::TestTask.new { |t|
t.libs << "test"
t.pattern = 'test/*_test.rb'
t.verbose = true
}
# Genereate the RDoc documentation
Rake::RDocTask.new { |rdoc|
rdoc.rdoc_dir = 'doc'
rdoc.title = "Action Mailer -- Easy email delivery and testing"
rdoc.options << '--line-numbers --inline-source --main README'
rdoc.rdoc_files.include('README', 'CHANGELOG')
rdoc.rdoc_files.include('lib/action_mailer.rb')
rdoc.rdoc_files.include('lib/action_mailer/*.rb')
}
# Create compressed packages
spec = Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
s.name = PKG_NAME
s.summary = "Service layer for easy email delivery and testing."
s.description = %q{Makes it trivial to test and deliver emails sent from a single service layer.}
s.version = PKG_VERSION
s.author = "David Heinemeier Hansson"
s.email = "david@loudthinking.com"
s.rubyforge_project = "actionmailer"
s.homepage = "http://actionmailer.rubyonrails.org"
s.add_dependency('actionpack', '>= 0.9.5')
s.has_rdoc = true
s.requirements << 'none'
s.require_path = 'lib'
s.autorequire = 'action_mailer'
s.files = [ "rakefile", "install.rb", "README", "CHANGELOG", "MIT-LICENSE" ]
s.files = s.files + Dir.glob( "lib/**/*" ).delete_if { |item| item.include?( "CVS" ) }
s.files = s.files + Dir.glob( "test/**/*" ).delete_if { |item| item.include?( "CVS" ) }
end
Rake::GemPackageTask.new(spec) do |p|
p.gem_spec = spec
p.need_tar = true
p.need_zip = true
end
# Publish beta gem
desc "Publish the API documentation"
task :pgem => [:package] do
Rake::SshFilePublisher.new("davidhh@one.textdrive.com", "domains/rubyonrails.org/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
end
# Publish documentation
desc "Publish the API documentation"
task :pdoc => [:rdoc] do
Rake::SshDirPublisher.new("davidhh@one.textdrive.com", "domains/rubyonrails.org/am", "doc").upload
end
desc "Publish to RubyForge"
task :rubyforge do
Rake::RubyForgePublisher.new('actionmailer', 'webster132').upload
end
desc "Count lines in the main rake file"
task :lines do
lines = 0
codelines = 0
Dir.foreach("lib/action_mailer") { |file_name|
next unless file_name =~ /.*rb/
f = File.open("lib/action_mailer/" + file_name)
while line = f.gets
lines += 1
next if line =~ /^\s*$/
next if line =~ /^\s*#/
codelines += 1
end
}
puts "Lines #{lines}, LOC #{codelines}"
end

61
actionmailer/install.rb Normal file
View File

@ -0,0 +1,61 @@
require 'rbconfig'
require 'find'
require 'ftools'
include Config
# this was adapted from rdoc's install.rb by ways of Log4r
$sitedir = CONFIG["sitelibdir"]
unless $sitedir
version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"]
$libdir = File.join(CONFIG["libdir"], "ruby", version)
$sitedir = $:.find {|x| x =~ /site_ruby/ }
if !$sitedir
$sitedir = File.join($libdir, "site_ruby")
elsif $sitedir !~ Regexp.quote(version)
$sitedir = File.join($sitedir, version)
end
end
makedirs = %w{ action_mailer/vendor action_mailer/vendor/text action_mailer/vendor/tmail }
makedirs.each {|f| File::makedirs(File.join($sitedir, *f.split(/\//)))}
# deprecated files that should be removed
# deprecated = %w{ }
# files to install in library path
files = %w-
action_mailer.rb
action_mailer/base.rb
action_mailer/mail_helper.rb
action_mailer/vendor/text/format.rb
action_mailer/vendor/tmail.rb
action_mailer/vendor/tmail/address.rb
action_mailer/vendor/tmail/base64.rb
action_mailer/vendor/tmail/config.rb
action_mailer/vendor/tmail/encode.rb
action_mailer/vendor/tmail/facade.rb
action_mailer/vendor/tmail/header.rb
action_mailer/vendor/tmail/info.rb
action_mailer/vendor/tmail/loader.rb
action_mailer/vendor/tmail/mail.rb
action_mailer/vendor/tmail/mailbox.rb
action_mailer/vendor/tmail/mbox.rb
action_mailer/vendor/tmail/net.rb
action_mailer/vendor/tmail/obsolete.rb
action_mailer/vendor/tmail/parser.rb
action_mailer/vendor/tmail/port.rb
action_mailer/vendor/tmail/scanner.rb
action_mailer/vendor/tmail/scanner_r.rb
action_mailer/vendor/tmail/stringio.rb
action_mailer/vendor/tmail/tmail.rb
action_mailer/vendor/tmail/utils.rb
-
# the acual gruntwork
Dir.chdir("lib")
# File::safe_unlink *deprecated.collect{|f| File.join($sitedir, f.split(/\//))}
files.each {|f|
File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true)
}

View File

@ -0,0 +1,43 @@
#--
# Copyright (c) 2004 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
begin
require 'action_controller'
rescue LoadError
# Action Pack is not already available, try RubyGems
require 'rubygems'
require_gem 'actionpack', '>= 0.9.0'
end
$:.unshift(File.dirname(__FILE__) + "/action_mailer/vendor/")
require 'action_mailer/base'
require 'action_mailer/mail_helper'
require 'action_mailer/vendor/tmail'
require 'net/smtp'
ActionView::Base.class_eval { include MailHelper }
old_verbose, $VERBOSE = $VERBOSE, nil
TMail::Encoder.const_set("MAX_LINE_LEN", 200)
$VERBOSE = old_verbose

View File

@ -0,0 +1,152 @@
module ActionMailer #:nodoc:
# Usage:
#
# class ApplicationMailer < ActionMailer::Base
# def post_notification(recipients, post)
# @recipients = recipients
# @subject = "[#{post.account.name} #{post.title}]"
# @body["post"] = post
# @from = post.author.email_address_with_name
# end
#
# def comment_notification(recipient, comment)
# @recipients = recipient.email_address_with_name
# @subject = "[#{comment.post.project.client.firm.account.name}]" +
# " Re: #{comment.post.title}"
# @body["comment"] = comment
# @from = comment.author.email_address_with_name
# @sent_on = comment.posted_on
# end
# end
#
# # After this post_notification will look for "templates/application_mailer/post_notification.rhtml"
# ApplicationMailer.template_root = "templates"
#
# ApplicationMailer.create_comment_notification(david, hello_world) # => a tmail object
# ApplicationMailer.deliver_comment_notification(david, hello_world) # sends the email
class Base
private_class_method :new
# Template root determines the base from which template references will be made.
cattr_accessor :template_root
# The logger is used for generating information on the mailing run if available.
# Can be set to nil for no logging. Compatible with both Ruby's own Logger and Log4r loggers.
cattr_accessor :logger
# Allows detailed configuration of the server:
# * <tt>:address</tt> Allows you to use a remote mail server. Just change it away from it's default "localhost" setting.
# * <tt>:port</tt> On the off change that your mail server doesn't run on port 25, you can change it.
# * <tt>:domain</tt> If you need to specify a HELO domain, you can do it here.
# * <tt>:user_name</tt> If your mail server requires authentication, set the username and password in these two settings.
# * <tt>:password</tt> If your mail server requires authentication, set the username and password in these two settings.
# * <tt>:authentication</tt> If your mail server requires authentication, you need to specify the authentication type here.
# This is a symbol and one of :plain, :login, :cram_md5
@@server_settings = {
:address => "localhost",
:port => 25,
:domain => 'localhost.localdomain',
:user_name => nil,
:password => nil,
:authentication => nil
}
cattr_accessor :server_settings
# Whether or not errors should be raised if the email fails to be delivered
@@raise_delivery_errors = true
cattr_accessor :raise_delivery_errors
# Defines a delivery method. Possible values are :smtp (default), :sendmail, and :test.
# Sendmail is assumed to be present at "/usr/sbin/sendmail".
@@delivery_method = :smtp
cattr_accessor :delivery_method
# Determines whether deliver_* methods are actually carried out. By default they are,
# but this can be turned off to help functional testing.
@@perform_deliveries = true
cattr_accessor :perform_deliveries
# Keeps an array of all the emails sent out through the Action Mailer with delivery_method :test. Most useful
# for unit and functional testing.
@@deliveries = []
cattr_accessor :deliveries
attr_accessor :recipients, :subject, :body, :from, :sent_on, :bcc, :cc
class << self
def method_missing(method_symbol, *parameters)#:nodoc:
case method_symbol.id2name
when /^create_([_a-z]*)/
create_from_action($1, *parameters)
when /^deliver_([_a-z]*)/
begin
deliver(send("create_" + $1, *parameters))
rescue Object => e
raise e if raise_delivery_errors
end
end
end
def mail(to, subject, body, from, timestamp = nil) #:nodoc:
deliver(create(to, subject, body, from, timestamp))
end
def create(to, subject, body, from, timestamp = nil) #:nodoc:
m = TMail::Mail.new
m.to, m.subject, m.body, m.from = to, subject, body, from
m.date = timestamp.respond_to?("to_time") ? timestamp.to_time : (timestamp || Time.now)
return m
end
def deliver(mail) #:nodoc:
logger.info "Sent mail:\n #{mail.encoded}" unless logger.nil?
send("perform_delivery_#{delivery_method}", mail) if perform_deliveries
end
private
def perform_delivery_smtp(mail)
Net::SMTP.start(server_settings[:address], server_settings[:port], server_settings[:domain],
server_settings[:user_name], server_settings[:password], server_settings[:authentication]) do |smtp|
smtp.sendmail(mail.encoded, mail.from_address, mail.destinations)
end
end
def perform_delivery_sendmail(mail)
IO.popen("/usr/sbin/sendmail -i -t","w+") do |sm|
sm.print(mail.encoded)
sm.flush
end
end
def perform_delivery_test(mail)
deliveries << mail
end
def create_from_action(method_name, *parameters)
mailer = new
mailer.body = {}
mailer.send(method_name, *parameters)
if String === mailer.body
mail = create(mailer.recipients, mailer.subject, mailer.body, mailer.from, mailer.sent_on)
else
mail = create(mailer.recipients, mailer.subject, render_body(mailer, method_name), mailer.from, mailer.sent_on)
end
mail.bcc = @bcc if @bcc
mail.cc = @cc if @cc
return mail
end
def render_body(mailer, method_name)
ActionView::Base.new(template_path, mailer.body).render_file(method_name)
end
def template_path
template_root + "/" + Inflector.underscore(self.to_s)
end
end
end
end

View File

@ -0,0 +1,17 @@
require 'action_mailer/vendor/text/format'
module MailHelper#:nodoc:
def block_format(text)
formatted = text.split(/\n\r\n/).collect { |paragraph|
Text::Format.new(
:columns => 72, :first_indent => 2, :body_indent => 2, :text => paragraph
).format
}.join("\n")
# Make list points stand on their own line
formatted.gsub!(/[ ]*([*]+) ([^*]*)/) { |s| " #{$1} #{$2.strip}\n" }
formatted.gsub!(/[ ]*([#]+) ([^#]*)/) { |s| " #{$1} #{$2.strip}\n" }
formatted
end
end

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,4 @@
require 'tmail/info'
require 'tmail/mail'
require 'tmail/mailbox'
require 'tmail/obsolete'

View File

@ -0,0 +1,223 @@
#
# address.rb
#
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# This program is free software.
# You can distribute/modify this program under the terms of
# the GNU Lesser General Public License version 2 or later.
#
require 'tmail/encode'
require 'tmail/parser'
module TMail
class Address
include TextUtils
def Address.parse( str )
Parser.parse :ADDRESS, str
end
def address_group?
false
end
def initialize( local, domain )
if domain
domain.each do |s|
raise SyntaxError, 'empty word in domain' if s.empty?
end
end
@local = local
@domain = domain
@name = nil
@routes = []
end
attr_reader :name
def name=( str )
@name = str
@name = nil if str and str.empty?
end
alias phrase name
alias phrase= name=
attr_reader :routes
def inspect
"#<#{self.class} #{address()}>"
end
def local
return nil unless @local
return '""' if @local.size == 1 and @local[0].empty?
@local.map {|i| quote_atom(i) }.join('.')
end
def domain
return nil unless @domain
join_domain(@domain)
end
def spec
s = self.local
d = self.domain
if s and d
s + '@' + d
else
s
end
end
alias address spec
def ==( other )
other.respond_to? :spec and self.spec == other.spec
end
alias eql? ==
def hash
@local.hash ^ @domain.hash
end
def dup
obj = self.class.new(@local.dup, @domain.dup)
obj.name = @name.dup if @name
obj.routes.replace @routes
obj
end
include StrategyInterface
def accept( strategy, dummy1 = nil, dummy2 = nil )
unless @local
strategy.meta '<>' # empty return-path
return
end
spec_p = (not @name and @routes.empty?)
if @name
strategy.phrase @name
strategy.space
end
tmp = spec_p ? '' : '<'
unless @routes.empty?
tmp << @routes.map {|i| '@' + i }.join(',') << ':'
end
tmp << self.spec
tmp << '>' unless spec_p
strategy.meta tmp
strategy.lwsp ''
end
end
class AddressGroup
include Enumerable
def address_group?
true
end
def initialize( name, addrs )
@name = name
@addresses = addrs
end
attr_reader :name
def ==( other )
other.respond_to? :to_a and @addresses == other.to_a
end
alias eql? ==
def hash
map {|i| i.hash }.hash
end
def []( idx )
@addresses[idx]
end
def size
@addresses.size
end
def empty?
@addresses.empty?
end
def each( &block )
@addresses.each(&block)
end
def to_a
@addresses.dup
end
alias to_ary to_a
def include?( a )
@addresses.include? a
end
def flatten
set = []
@addresses.each do |a|
if a.respond_to? :flatten
set.concat a.flatten
else
set.push a
end
end
set
end
def each_address( &block )
flatten.each(&block)
end
def add( a )
@addresses.push a
end
alias push add
def delete( a )
@addresses.delete a
end
include StrategyInterface
def accept( strategy, dummy1 = nil, dummy2 = nil )
strategy.phrase @name
strategy.meta ':'
strategy.space
first = true
each do |mbox|
if first
first = false
else
strategy.meta ','
end
strategy.space
mbox.accept strategy
end
strategy.meta ';'
strategy.lwsp ''
end
end
end # module TMail

View File

@ -0,0 +1,52 @@
#
# base64.rb
#
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# This program is free software.
# You can distribute/modify this program under the terms of
# the GNU Lesser General Public License version 2 or later.
#
module TMail
module Base64
module_function
def rb_folding_encode( str, eol = "\n", limit = 60 )
[str].pack('m')
end
def rb_encode( str )
[str].pack('m').tr( "\r\n", '' )
end
def rb_decode( str, strict = false )
str.unpack('m')
end
begin
require 'tmail/base64.so'
alias folding_encode c_folding_encode
alias encode c_encode
alias decode c_decode
class << self
alias folding_encode c_folding_encode
alias encode c_encode
alias decode c_decode
end
rescue LoadError
alias folding_encode rb_folding_encode
alias encode rb_encode
alias decode rb_decode
class << self
alias folding_encode rb_folding_encode
alias encode rb_encode
alias decode rb_decode
end
end
end
end

View File

@ -0,0 +1,50 @@
#
# config.rb
#
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# This program is free software.
# You can distribute/modify this program under the terms of
# the GNU Lesser General Public License version 2 or later.
#
module TMail
class Config
def initialize( strict )
@strict_parse = strict
@strict_base64decode = strict
end
def strict_parse?
@strict_parse
end
attr_writer :strict_parse
def strict_base64decode?
@strict_base64decode
end
attr_writer :strict_base64decode
def new_body_port( mail )
StringPort.new
end
alias new_preamble_port new_body_port
alias new_part_port new_body_port
end
DEFAULT_CONFIG = Config.new(false)
DEFAULT_STRICT_CONFIG = Config.new(true)
def Config.to_config( arg )
return DEFAULT_STRICT_CONFIG if arg == true
return DEFAULT_CONFIG if arg == false
arg or DEFAULT_CONFIG
end
end

View File

@ -0,0 +1,447 @@
#
# encode.rb
#
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# This program is free software.
# You can distribute/modify this program under the terms of
# the GNU Lesser General Public License version 2 or later.
#
require 'nkf'
require 'tmail/base64.rb'
require 'tmail/stringio'
require 'tmail/utils'
module TMail
module StrategyInterface
def create_dest( obj )
case obj
when nil
StringOutput.new
when String
StringOutput.new(obj)
when IO, StringOutput
obj
else
raise TypeError, 'cannot handle this type of object for dest'
end
end
module_function :create_dest
def encoded( eol = "\r\n", charset = 'j', dest = nil )
accept_strategy Encoder, eol, charset, dest
end
def decoded( eol = "\n", charset = 'e', dest = nil )
accept_strategy Decoder, eol, charset, dest
end
alias to_s decoded
def accept_strategy( klass, eol, charset, dest = nil )
dest ||= ''
accept klass.new(create_dest(dest), charset, eol)
dest
end
end
###
### MIME B encoding decoder
###
class Decoder
include TextUtils
encoded = '=\?(?:iso-2022-jp|euc-jp|shift_jis)\?[QB]\?[a-z0-9+/=]+\?='
ENCODED_WORDS = /#{encoded}(?:\s+#{encoded})*/i
OUTPUT_ENCODING = {
'EUC' => 'e',
'SJIS' => 's',
}
def self.decode( str, encoding = nil )
encoding ||= (OUTPUT_ENCODING[$KCODE] || 'j')
opt = '-m' + encoding
str.gsub(ENCODED_WORDS) {|s| NKF.nkf(opt, s) }
end
def initialize( dest, encoding = nil, eol = "\n" )
@f = StrategyInterface.create_dest(dest)
@encoding = (/\A[ejs]/ === encoding) ? encoding[0,1] : nil
@eol = eol
end
def decode( str )
self.class.decode(str, @encoding)
end
private :decode
def terminate
end
def header_line( str )
@f << decode(str)
end
def header_name( nm )
@f << nm << ': '
end
def header_body( str )
@f << decode(str)
end
def space
@f << ' '
end
alias spc space
def lwsp( str )
@f << str
end
def meta( str )
@f << str
end
def text( str )
@f << decode(str)
end
def phrase( str )
@f << quote_phrase(decode(str))
end
def kv_pair( k, v )
@f << k << '=' << v
end
def puts( str = nil )
@f << str if str
@f << @eol
end
def write( str )
@f << str
end
end
###
### MIME B-encoding encoder
###
#
# FIXME: This class can handle only (euc-jp/shift_jis -> iso-2022-jp).
#
class Encoder
include TextUtils
BENCODE_DEBUG = false unless defined?(BENCODE_DEBUG)
def Encoder.encode( str )
e = new()
e.header_body str
e.terminate
e.dest.string
end
SPACER = "\t"
MAX_LINE_LEN = 70
OPTIONS = {
'EUC' => '-Ej -m0',
'SJIS' => '-Sj -m0',
'UTF8' => nil, # FIXME
'NONE' => nil
}
def initialize( dest = nil, encoding = nil, eol = "\r\n", limit = nil )
@f = StrategyInterface.create_dest(dest)
@opt = OPTIONS[$KCODE]
@eol = eol
reset
end
def normalize_encoding( str )
if @opt
then NKF.nkf(@opt, str)
else str
end
end
def reset
@text = ''
@lwsp = ''
@curlen = 0
end
def terminate
add_lwsp ''
reset
end
def dest
@f
end
def puts( str = nil )
@f << str if str
@f << @eol
end
def write( str )
@f << str
end
#
# add
#
def header_line( line )
scanadd line
end
def header_name( name )
add_text name.split(/-/).map {|i| i.capitalize }.join('-')
add_text ':'
add_lwsp ' '
end
def header_body( str )
scanadd normalize_encoding(str)
end
def space
add_lwsp ' '
end
alias spc space
def lwsp( str )
add_lwsp str.sub(/[\r\n]+[^\r\n]*\z/, '')
end
def meta( str )
add_text str
end
def text( str )
scanadd normalize_encoding(str)
end
def phrase( str )
str = normalize_encoding(str)
if CONTROL_CHAR === str
scanadd str
else
add_text quote_phrase(str)
end
end
# FIXME: implement line folding
#
def kv_pair( k, v )
v = normalize_encoding(v)
if token_safe?(v)
add_text k + '=' + v
elsif not CONTROL_CHAR === v
add_text k + '=' + quote_token(v)
else
# apply RFC2231 encoding
kv = k + '*=' + "iso-2022-jp'ja'" + encode_value(v)
add_text kv
end
end
def encode_value( str )
str.gsub(TOKEN_UNSAFE) {|s| '%%%02x' % s[0] }
end
private
def scanadd( str, force = false )
types = ''
strs = []
until str.empty?
if m = /\A[^\e\t\r\n ]+/.match(str)
types << (force ? 'j' : 'a')
strs.push m[0]
elsif m = /\A[\t\r\n ]+/.match(str)
types << 's'
strs.push m[0]
elsif m = /\A\e../.match(str)
esc = m[0]
str = m.post_match
if esc != "\e(B" and m = /\A[^\e]+/.match(str)
types << 'j'
strs.push m[0]
end
else
raise 'TMail FATAL: encoder scan fail'
end
str = m.post_match
end
do_encode types, strs
end
def do_encode( types, strs )
#
# result : (A|E)(S(A|E))*
# E : W(SW)*
# W : (J|A)+ but must contain J # (J|A)*J(J|A)*
# A : <<A character string not to be encoded>>
# J : <<A character string to be encoded>>
# S : <<LWSP>>
#
# An encoding unit is `E'.
# Input (parameter `types') is (J|A)(J|A|S)*(J|A)
#
if BENCODE_DEBUG
puts
puts '-- do_encode ------------'
puts types.split(//).join(' ')
p strs
end
e = /[ja]*j[ja]*(?:s[ja]*j[ja]*)*/
while m = e.match(types)
pre = m.pre_match
concat_A_S pre, strs[0, pre.size] unless pre.empty?
concat_E m[0], strs[m.begin(0) ... m.end(0)]
types = m.post_match
strs.slice! 0, m.end(0)
end
concat_A_S types, strs
end
def concat_A_S( types, strs )
i = 0
types.each_byte do |t|
case t
when ?a then add_text strs[i]
when ?s then add_lwsp strs[i]
else
raise "TMail FATAL: unknown flag: #{t.chr}"
end
i += 1
end
end
METHOD_ID = {
?j => :extract_J,
?e => :extract_E,
?a => :extract_A,
?s => :extract_S
}
def concat_E( types, strs )
if BENCODE_DEBUG
puts '---- concat_E'
puts "types=#{types.split(//).join(' ')}"
puts "strs =#{strs.inspect}"
end
flush() unless @text.empty?
chunk = ''
strs.each_with_index do |s,i|
mid = METHOD_ID[types[i]]
until s.empty?
unless c = __send__(mid, chunk.size, s)
add_with_encode chunk unless chunk.empty?
flush
chunk = ''
fold
c = __send__(mid, 0, s)
raise 'TMail FATAL: extract fail' unless c
end
chunk << c
end
end
add_with_encode chunk unless chunk.empty?
end
def extract_J( chunksize, str )
size = max_bytes(chunksize, str.size) - 6
size = (size % 2 == 0) ? (size) : (size - 1)
return nil if size <= 0
"\e$B#{str.slice!(0, size)}\e(B"
end
def extract_A( chunksize, str )
size = max_bytes(chunksize, str.size)
return nil if size <= 0
str.slice!(0, size)
end
alias extract_S extract_A
def max_bytes( chunksize, ssize )
(restsize() - '=?iso-2022-jp?B??='.size) / 4 * 3 - chunksize
end
#
# free length buffer
#
def add_text( str )
@text << str
# puts '---- text -------------------------------------'
# puts "+ #{str.inspect}"
# puts "txt >>>#{@text.inspect}<<<"
end
def add_with_encode( str )
@text << "=?iso-2022-jp?B?#{Base64.encode(str)}?="
end
def add_lwsp( lwsp )
# puts '---- lwsp -------------------------------------'
# puts "+ #{lwsp.inspect}"
fold if restsize() <= 0
flush
@lwsp = lwsp
end
def flush
# puts '---- flush ----'
# puts "spc >>>#{@lwsp.inspect}<<<"
# puts "txt >>>#{@text.inspect}<<<"
@f << @lwsp << @text
@curlen += (@lwsp.size + @text.size)
@text = ''
@lwsp = ''
end
def fold
# puts '---- fold ----'
@f << @eol
@curlen = 0
@lwsp = SPACER
end
def restsize
MAX_LINE_LEN - (@curlen + @lwsp.size + @text.size)
end
end
end # module TMail

View File

@ -0,0 +1,531 @@
#
# facade.rb
#
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# This program is free software.
# You can distribute/modify this program under the terms of
# the GNU Lesser General Public License version 2 or later.
#
require 'tmail/utils'
module TMail
class Mail
def header_string( name, default = nil )
h = @header[name.downcase] or return default
h.to_s
end
###
### attributes
###
include TextUtils
def set_string_array_attr( key, strs )
strs.flatten!
if strs.empty?
@header.delete key.downcase
else
store key, strs.join(', ')
end
strs
end
private :set_string_array_attr
def set_string_attr( key, str )
if str
store key, str
else
@header.delete key.downcase
end
str
end
private :set_string_attr
def set_addrfield( name, arg )
if arg
h = HeaderField.internal_new(name, @config)
h.addrs.replace [arg].flatten
@header[name] = h
else
@header.delete name
end
arg
end
private :set_addrfield
def addrs2specs( addrs )
return nil unless addrs
list = addrs.map {|addr|
if addr.address_group?
then addr.map {|a| a.spec }
else addr.spec
end
}.flatten
return nil if list.empty?
list
end
private :addrs2specs
#
# date time
#
def date( default = nil )
if h = @header['date']
h.date
else
default
end
end
def date=( time )
if time
store 'Date', time2str(time)
else
@header.delete 'date'
end
time
end
def strftime( fmt, default = nil )
if t = date
t.strftime(fmt)
else
default
end
end
#
# destination
#
def to_addrs( default = nil )
if h = @header['to']
h.addrs
else
default
end
end
def cc_addrs( default = nil )
if h = @header['cc']
h.addrs
else
default
end
end
def bcc_addrs( default = nil )
if h = @header['bcc']
h.addrs
else
default
end
end
def to_addrs=( arg )
set_addrfield 'to', arg
end
def cc_addrs=( arg )
set_addrfield 'cc', arg
end
def bcc_addrs=( arg )
set_addrfield 'bcc', arg
end
def to( default = nil )
addrs2specs(to_addrs(nil)) || default
end
def cc( default = nil )
addrs2specs(cc_addrs(nil)) || default
end
def bcc( default = nil )
addrs2specs(bcc_addrs(nil)) || default
end
def to=( *strs )
set_string_array_attr 'To', strs
end
def cc=( *strs )
set_string_array_attr 'Cc', strs
end
def bcc=( *strs )
set_string_array_attr 'Bcc', strs
end
#
# originator
#
def from_addrs( default = nil )
if h = @header['from']
h.addrs
else
default
end
end
def from_addrs=( arg )
set_addrfield 'from', arg
end
def from( default = nil )
addrs2specs(from_addrs(nil)) || default
end
def from=( *strs )
set_string_array_attr 'From', strs
end
def friendly_from( default = nil )
h = @header['from']
a, = h.addrs
return default unless a
return a.phrase if a.phrase
return h.comments.join(' ') unless h.comments.empty?
a.spec
end
def reply_to_addrs( default = nil )
if h = @header['reply-to']
h.addrs
else
default
end
end
def reply_to_addrs=( arg )
set_addrfield 'reply-to', arg
end
def reply_to( default = nil )
addrs2specs(reply_to_addrs(nil)) || default
end
def reply_to=( *strs )
set_string_array_attr 'Reply-To', strs
end
def sender_addr( default = nil )
f = @header['sender'] or return default
f.addr or return default
end
def sender_addr=( addr )
if addr
h = HeaderField.internal_new('sender', @config)
h.addr = addr
@header['sender'] = h
else
@header.delete 'sender'
end
addr
end
def sender( default )
f = @header['sender'] or return default
a = f.addr or return default
a.spec
end
def sender=( str )
set_string_attr 'Sender', str
end
#
# subject
#
def subject( default = nil )
if h = @header['subject']
h.body
else
default
end
end
def subject=( str )
set_string_attr 'Subject', str
end
#
# identity & threading
#
def message_id( default = nil )
if h = @header['message-id']
h.id || default
else
default
end
end
def message_id=( str )
set_string_attr 'Message-Id', str
end
def in_reply_to( default = nil )
if h = @header['in-reply-to']
h.ids
else
default
end
end
def in_reply_to=( *idstrs )
set_string_array_attr 'In-Reply-To', idstrs
end
def references( default = nil )
if h = @header['references']
h.refs
else
default
end
end
def references=( *strs )
set_string_array_attr 'References', strs
end
#
# MIME headers
#
def mime_version( default = nil )
if h = @header['mime-version']
h.version || default
else
default
end
end
def mime_version=( m, opt = nil )
if opt
if h = @header['mime-version']
h.major = m
h.minor = opt
else
store 'Mime-Version', "#{m}.#{opt}"
end
else
store 'Mime-Version', m
end
m
end
def content_type( default = nil )
if h = @header['content-type']
h.content_type || default
else
default
end
end
def main_type( default = nil )
if h = @header['content-type']
h.main_type || default
else
default
end
end
def sub_type( default = nil )
if h = @header['content-type']
h.sub_type || default
else
default
end
end
def set_content_type( str, sub = nil, param = nil )
if sub
main, sub = str, sub
else
main, sub = str.split(%r</>, 2)
raise ArgumentError, "sub type missing: #{str.inspect}" unless sub
end
if h = @header['content-type']
h.main_type = main
h.sub_type = sub
h.params.clear
else
store 'Content-Type', "#{main}/#{sub}"
end
@header['content-type'].params.replace param if param
str
end
alias content_type= set_content_type
def type_param( name, default = nil )
if h = @header['content-type']
h[name] || default
else
default
end
end
def charset( default = nil )
if h = @header['content-type']
h['charset'] or default
else
default
end
end
def charset=( str )
if str
if h = @header[ 'content-type' ]
h['charset'] = str
else
store 'Content-Type', "text/plain; charset=#{str}"
end
end
str
end
def transfer_encoding( default = nil )
if h = @header['content-transfer-encoding']
h.encoding || default
else
default
end
end
def transfer_encoding=( str )
set_string_attr 'Content-Transfer-Encoding', str
end
alias encoding transfer_encoding
alias encoding= transfer_encoding=
alias content_transfer_encoding transfer_encoding
alias content_transfer_encoding= transfer_encoding=
def disposition( default = nil )
if h = @header['content-disposition']
h.disposition || default
else
default
end
end
alias content_disposition disposition
def set_disposition( str, params = nil )
if h = @header['content-disposition']
h.disposition = str
h.params.clear
else
h = store('Content-Disposition', str)
end
h.params.replace params if params
end
alias disposition= set_disposition
alias set_content_disposition set_disposition
alias content_disposition= set_disposition
def disposition_param( name, default = nil )
if h = @header['content-disposition']
h[name] || default
else
default
end
end
###
### utils
###
def create_reply
mail = TMail::Mail.parse('')
mail.subject = 'Re: ' + subject('').sub(/\A(?:\[[^\]]+\])?(?:\s*Re:)*\s*/i, '')
mail.to_addrs = reply_addresses([])
mail.in_reply_to = [message_id(nil)].compact
mail.references = references([]) + [message_id(nil)].compact
mail.mime_version = '1.0'
mail
end
def base64_encode
store 'Content-Transfer-Encoding', 'Base64'
self.body = Base64.folding_encode(self.body)
end
def base64_decode
if /base64/i === self.transfer_encoding('')
store 'Content-Transfer-Encoding', '8bit'
self.body = Base64.decode(self.body, @config.strict_base64decode?)
end
end
def destinations( default = nil )
ret = []
%w( to cc bcc ).each do |nm|
if h = @header[nm]
h.addrs.each {|i| ret.push i.address }
end
end
ret.empty? ? default : ret
end
def each_destination( &block )
destinations([]).each do |i|
if Address === i
yield i
else
i.each(&block)
end
end
end
alias each_dest each_destination
def reply_addresses( default = nil )
reply_to_addrs(nil) or from_addrs(nil) or default
end
def error_reply_addresses( default = nil )
if s = sender(nil)
[s]
else
from_addrs(default)
end
end
def multipart?
main_type('').downcase == 'multipart'
end
end # class Mail
end # module TMail

View File

@ -0,0 +1,893 @@
#
# header.rb
#
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# This program is free software.
# You can distribute/modify this program under the terms of
# the GNU Lesser General Public License version 2 or later.
#
require 'tmail/encode'
require 'tmail/address'
require 'tmail/parser'
require 'tmail/config'
require 'tmail/utils'
module TMail
class HeaderField
include TextUtils
class << self
alias newobj new
def new( name, body, conf = DEFAULT_CONFIG )
klass = FNAME_TO_CLASS[name.downcase] || UnstructuredHeader
klass.newobj body, conf
end
def new_from_port( port, name, conf = DEFAULT_CONFIG )
re = Regep.new('\A(' + Regexp.quote(name) + '):', 'i')
str = nil
port.ropen {|f|
f.each do |line|
if m = re.match(line) then str = m.post_match.strip
elsif str and /\A[\t ]/ === line then str << ' ' << line.strip
elsif /\A-*\s*\z/ === line then break
elsif str then break
end
end
}
new(name, str, Config.to_config(conf))
end
def internal_new( name, conf )
FNAME_TO_CLASS[name].newobj('', conf, true)
end
end # class << self
def initialize( body, conf, intern = false )
@body = body
@config = conf
@illegal = false
@parsed = false
if intern
@parsed = true
parse_init
end
end
def inspect
"#<#{self.class} #{@body.inspect}>"
end
def illegal?
@illegal
end
def empty?
ensure_parsed
return true if @illegal
isempty?
end
private
def ensure_parsed
return if @parsed
@parsed = true
parse
end
# defabstract parse
# end
def clear_parse_status
@parsed = false
@illegal = false
end
public
def body
ensure_parsed
v = Decoder.new(s = '')
do_accept v
v.terminate
s
end
def body=( str )
@body = str
clear_parse_status
end
include StrategyInterface
def accept( strategy, dummy1 = nil, dummy2 = nil )
ensure_parsed
do_accept strategy
strategy.terminate
end
# abstract do_accept
end
class UnstructuredHeader < HeaderField
def body
ensure_parsed
@body
end
def body=( arg )
ensure_parsed
@body = arg
end
private
def parse_init
end
def parse
@body = Decoder.decode(@body.gsub(/\n|\r\n|\r/, ''))
end
def isempty?
not @body
end
def do_accept( strategy )
strategy.text @body
end
end
class StructuredHeader < HeaderField
def comments
ensure_parsed
@comments
end
private
def parse
save = nil
begin
parse_init
do_parse
rescue SyntaxError
if not save and mime_encoded? @body
save = @body
@body = Decoder.decode(save)
retry
elsif save
@body = save
end
@illegal = true
raise if @config.strict_parse?
end
end
def parse_init
@comments = []
init
end
def do_parse
obj = Parser.parse(self.class::PARSE_TYPE, @body, @comments)
set obj if obj
end
end
class DateTimeHeader < StructuredHeader
PARSE_TYPE = :DATETIME
def date
ensure_parsed
@date
end
def date=( arg )
ensure_parsed
@date = arg
end
private
def init
@date = nil
end
def set( t )
@date = t
end
def isempty?
not @date
end
def do_accept( strategy )
strategy.meta time2str(@date)
end
end
class AddressHeader < StructuredHeader
PARSE_TYPE = :MADDRESS
def addrs
ensure_parsed
@addrs
end
private
def init
@addrs = []
end
def set( a )
@addrs = a
end
def isempty?
@addrs.empty?
end
def do_accept( strategy )
first = true
@addrs.each do |a|
if first
first = false
else
strategy.meta ','
strategy.space
end
a.accept strategy
end
@comments.each do |c|
strategy.space
strategy.meta '('
strategy.text c
strategy.meta ')'
end
end
end
class ReturnPathHeader < AddressHeader
PARSE_TYPE = :RETPATH
def addr
addrs()[0]
end
def spec
a = addr() or return nil
a.spec
end
def routes
a = addr() or return nil
a.routes
end
private
def do_accept( strategy )
a = addr()
strategy.meta '<'
unless a.routes.empty?
strategy.meta a.routes.map {|i| '@' + i }.join(',')
strategy.meta ':'
end
spec = a.spec
strategy.meta spec if spec
strategy.meta '>'
end
end
class SingleAddressHeader < AddressHeader
def addr
addrs()[0]
end
private
def do_accept( strategy )
a = addr()
a.accept strategy
@comments.each do |c|
strategy.space
strategy.meta '('
strategy.text c
strategy.meta ')'
end
end
end
class MessageIdHeader < StructuredHeader
def id
ensure_parsed
@id
end
def id=( arg )
ensure_parsed
@id = arg
end
private
def init
@id = nil
end
def isempty?
not @id
end
def do_parse
@id = @body.slice(MESSAGE_ID) or
raise SyntaxError, "wrong Message-ID format: #{@body}"
end
def do_accept( strategy )
strategy.meta @id
end
end
class ReferencesHeader < StructuredHeader
def refs
ensure_parsed
@refs
end
def each_id
self.refs.each do |i|
yield i if MESSAGE_ID === i
end
end
def ids
ensure_parsed
@ids
end
def each_phrase
self.refs.each do |i|
yield i unless MESSAGE_ID === i
end
end
def phrases
ret = []
each_phrase {|i| ret.push i }
ret
end
private
def init
@refs = []
@ids = []
end
def isempty?
@ids.empty?
end
def do_parse
str = @body
while m = MESSAGE_ID.match(str)
pre = m.pre_match.strip
@refs.push pre unless pre.empty?
@refs.push s = m[0]
@ids.push s
str = m.post_match
end
str = str.strip
@refs.push str unless str.empty?
end
def do_accept( strategy )
first = true
@ids.each do |i|
if first
first = false
else
strategy.space
end
strategy.meta i
end
end
end
class ReceivedHeader < StructuredHeader
PARSE_TYPE = :RECEIVED
def from
ensure_parsed
@from
end
def from=( arg )
ensure_parsed
@from = arg
end
def by
ensure_parsed
@by
end
def by=( arg )
ensure_parsed
@by = arg
end
def via
ensure_parsed
@via
end
def via=( arg )
ensure_parsed
@via = arg
end
def with
ensure_parsed
@with
end
def id
ensure_parsed
@id
end
def id=( arg )
ensure_parsed
@id = arg
end
def _for
ensure_parsed
@_for
end
def _for=( arg )
ensure_parsed
@_for = arg
end
def date
ensure_parsed
@date
end
def date=( arg )
ensure_parsed
@date = arg
end
private
def init
@from = @by = @via = @with = @id = @_for = nil
@with = []
@date = nil
end
def set( args )
@from, @by, @via, @with, @id, @_for, @date = *args
end
def isempty?
@with.empty? and not (@from or @by or @via or @id or @_for or @date)
end
def do_accept( strategy )
list = []
list.push 'from ' + @from if @from
list.push 'by ' + @by if @by
list.push 'via ' + @via if @via
@with.each do |i|
list.push 'with ' + i
end
list.push 'id ' + @id if @id
list.push 'for <' + @_for + '>' if @_for
first = true
list.each do |i|
strategy.space unless first
strategy.meta i
first = false
end
if @date
strategy.meta ';'
strategy.space
strategy.meta time2str(@date)
end
end
end
class KeywordsHeader < StructuredHeader
PARSE_TYPE = :KEYWORDS
def keys
ensure_parsed
@keys
end
private
def init
@keys = []
end
def set( a )
@keys = a
end
def isempty?
@keys.empty?
end
def do_accept( strategy )
first = true
@keys.each do |i|
if first
first = false
else
strategy.meta ','
end
strategy.meta i
end
end
end
class EncryptedHeader < StructuredHeader
PARSE_TYPE = :ENCRYPTED
def encrypter
ensure_parsed
@encrypter
end
def encrypter=( arg )
ensure_parsed
@encrypter = arg
end
def keyword
ensure_parsed
@keyword
end
def keyword=( arg )
ensure_parsed
@keyword = arg
end
private
def init
@encrypter = nil
@keyword = nil
end
def set( args )
@encrypter, @keyword = args
end
def isempty?
not (@encrypter or @keyword)
end
def do_accept( strategy )
if @key
strategy.meta @encrypter + ','
strategy.space
strategy.meta @keyword
else
strategy.meta @encrypter
end
end
end
class MimeVersionHeader < StructuredHeader
PARSE_TYPE = :MIMEVERSION
def major
ensure_parsed
@major
end
def major=( arg )
ensure_parsed
@major = arg
end
def minor
ensure_parsed
@minor
end
def minor=( arg )
ensure_parsed
@minor = arg
end
def version
sprintf('%d.%d', major, minor)
end
private
def init
@major = nil
@minor = nil
end
def set( args )
@major, @minor = *args
end
def isempty?
not (@major or @minor)
end
def do_accept( strategy )
strategy.meta sprintf('%d.%d', @major, @minor)
end
end
class ContentTypeHeader < StructuredHeader
PARSE_TYPE = :CTYPE
def main_type
ensure_parsed
@main
end
def main_type=( arg )
ensure_parsed
@main = arg.downcase
end
def sub_type
ensure_parsed
@sub
end
def sub_type=( arg )
ensure_parsed
@sub = arg.downcase
end
def content_type
ensure_parsed
@sub ? sprintf('%s/%s', @main, @sub) : @main
end
def params
ensure_parsed
@params
end
def []( key )
ensure_parsed
@params and @params[key]
end
def []=( key, val )
ensure_parsed
(@params ||= {})[key] = val
end
private
def init
@main = @sub = @params = nil
end
def set( args )
@main, @sub, @params = *args
end
def isempty?
not (@main or @sub)
end
def do_accept( strategy )
if @sub
strategy.meta sprintf('%s/%s', @main, @sub)
else
strategy.meta @main
end
@params.each do |k,v|
strategy.meta ';'
strategy.space
strategy.kv_pair k, v
end
end
end
class ContentTransferEncodingHeader < StructuredHeader
PARSE_TYPE = :CENCODING
def encoding
ensure_parsed
@encoding
end
def encoding=( arg )
ensure_parsed
@encoding = arg
end
private
def init
@encoding = nil
end
def set( s )
@encoding = s
end
def isempty?
not @encoding
end
def do_accept( strategy )
strategy.meta @encoding.capitalize
end
end
class ContentDispositionHeader < StructuredHeader
PARSE_TYPE = :CDISPOSITION
def disposition
ensure_parsed
@disposition
end
def disposition=( str )
ensure_parsed
@disposition = str.downcase
end
def params
ensure_parsed
@params
end
def []( key )
ensure_parsed
@params and @params[key]
end
def []=( key, val )
ensure_parsed
(@params ||= {})[key] = val
end
private
def init
@disposition = @params = nil
end
def set( args )
@disposition, @params = *args
end
def isempty?
not @disposition and (not @params or @params.empty?)
end
def do_accept( strategy )
strategy.meta @disposition
@params.each do |k,v|
strategy.meta ';'
strategy.space
strategy.kv_pair k, v
end
end
end
class HeaderField # redefine
FNAME_TO_CLASS = {
'date' => DateTimeHeader,
'resent-date' => DateTimeHeader,
'to' => AddressHeader,
'cc' => AddressHeader,
'bcc' => AddressHeader,
'from' => AddressHeader,
'reply-to' => AddressHeader,
'resent-to' => AddressHeader,
'resent-cc' => AddressHeader,
'resent-bcc' => AddressHeader,
'resent-from' => AddressHeader,
'resent-reply-to' => AddressHeader,
'sender' => SingleAddressHeader,
'resent-sender' => SingleAddressHeader,
'return-path' => ReturnPathHeader,
'message-id' => MessageIdHeader,
'resent-message-id' => MessageIdHeader,
'in-reply-to' => ReferencesHeader,
'received' => ReceivedHeader,
'references' => ReferencesHeader,
'keywords' => KeywordsHeader,
'encrypted' => EncryptedHeader,
'mime-version' => MimeVersionHeader,
'content-type' => ContentTypeHeader,
'content-transfer-encoding' => ContentTransferEncodingHeader,
'content-disposition' => ContentDispositionHeader,
'content-id' => MessageIdHeader,
'subject' => UnstructuredHeader,
'comments' => UnstructuredHeader,
'content-description' => UnstructuredHeader
}
end
end # module TMail

View File

@ -0,0 +1,16 @@
#
# info.rb
#
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# This program is free software.
# You can distribute/modify this program under the terms of
# the GNU Lesser General Public License version 2 or later.
#
module TMail
Version = '0.10.7'
Copyright = 'Copyright (c) 1998-2002 Minero Aoki'
end

View File

@ -0,0 +1 @@
require 'tmail/mailbox'

View File

@ -0,0 +1,420 @@
#
# mail.rb
#
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# This program is free software.
# You can distribute/modify this program under the terms of
# the GNU Lesser General Public License version 2 or later.
#
require 'tmail/facade'
require 'tmail/encode'
require 'tmail/header'
require 'tmail/port'
require 'tmail/config'
require 'tmail/utils'
require 'socket'
module TMail
class Mail
class << self
def load( fname )
new(FilePort.new(fname))
end
alias load_from load
alias loadfrom load
def parse( str )
new(StringPort.new(str))
end
end
def initialize( port = nil, conf = DEFAULT_CONFIG )
@port = port || StringPort.new
@config = Config.to_config(conf)
@header = {}
@body_port = nil
@body_parsed = false
@epilogue = ''
@parts = []
@port.ropen {|f|
parse_header f
parse_body f unless @port.reproducible?
}
end
attr_reader :port
def inspect
"\#<#{self.class} port=#{@port.inspect} bodyport=#{@body_port.inspect}>"
end
#
# to_s interfaces
#
public
include StrategyInterface
def write_back( eol = "\n", charset = 'e' )
parse_body
@port.wopen {|stream| encoded eol, charset, stream }
end
def accept( strategy )
with_multipart_encoding(strategy) {
ordered_each do |name, field|
next if field.empty?
strategy.header_name canonical(name)
field.accept strategy
strategy.puts
end
strategy.puts
body_port().ropen {|r|
strategy.write r.read
}
}
end
private
def canonical( name )
name.split(/-/).map {|s| s.capitalize }.join('-')
end
def with_multipart_encoding( strategy )
if parts().empty? # DO NOT USE @parts
yield
else
bound = ::TMail.new_boundary
if @header.key? 'content-type'
@header['content-type'].params['boundary'] = bound
else
store 'Content-Type', %<multipart/mixed; boundary="#{bound}">
end
yield
parts().each do |tm|
strategy.puts
strategy.puts '--' + bound
tm.accept strategy
end
strategy.puts
strategy.puts '--' + bound + '--'
strategy.write epilogue()
end
end
###
### header
###
public
ALLOW_MULTIPLE = {
'received' => true,
'resent-date' => true,
'resent-from' => true,
'resent-sender' => true,
'resent-to' => true,
'resent-cc' => true,
'resent-bcc' => true,
'resent-message-id' => true,
'comments' => true,
'keywords' => true
}
USE_ARRAY = ALLOW_MULTIPLE
def header
@header.dup
end
def []( key )
@header[key.downcase]
end
alias fetch []
def []=( key, val )
dkey = key.downcase
if val.nil?
@header.delete dkey
return nil
end
case val
when String
header = new_hf(key, val)
when HeaderField
;
when Array
ALLOW_MULTIPLE.include? dkey or
raise ArgumentError, "#{key}: Header must not be multiple"
@header[dkey] = val
return val
else
header = new_hf(key, val.to_s)
end
if ALLOW_MULTIPLE.include? dkey
(@header[dkey] ||= []).push header
else
@header[dkey] = header
end
val
end
alias store []=
def each_header
@header.each do |key, val|
[val].flatten.each {|v| yield key, v }
end
end
alias each_pair each_header
def each_header_name( &block )
@header.each_key(&block)
end
alias each_key each_header_name
def each_field( &block )
@header.values.flatten.each(&block)
end
alias each_value each_field
FIELD_ORDER = %w(
return-path received
resent-date resent-from resent-sender resent-to
resent-cc resent-bcc resent-message-id
date from sender reply-to to cc bcc
message-id in-reply-to references
subject comments keywords
mime-version content-type content-transfer-encoding
content-disposition content-description
)
def ordered_each
list = @header.keys
FIELD_ORDER.each do |name|
if list.delete(name)
[@header[name]].flatten.each {|v| yield name, v }
end
end
list.each do |name|
[@header[name]].flatten.each {|v| yield name, v }
end
end
def clear
@header.clear
end
def delete( key )
@header.delete key.downcase
end
def delete_if
@header.delete_if do |key,val|
if Array === val
val.delete_if {|v| yield key, v }
val.empty?
else
yield key, val
end
end
end
def keys
@header.keys
end
def key?( key )
@header.key? key.downcase
end
def values_at( *args )
args.map {|k| @header[k.downcase] }.flatten
end
alias indexes values_at
alias indices values_at
private
def parse_header( f )
name = field = nil
unixfrom = nil
while line = f.gets
case line
when /\A[ \t]/ # continue from prev line
raise SyntaxError, 'mail is began by space' unless field
field << ' ' << line.strip
when /\A([^\: \t]+):\s*/ # new header line
add_hf name, field if field
name = $1
field = $' #.strip
when /\A\-*\s*\z/ # end of header
add_hf name, field if field
name = field = nil
break
when /\AFrom (\S+)/
unixfrom = $1
else
raise SyntaxError, "wrong mail header: '#{line.inspect}'"
end
end
add_hf name, field if name
if unixfrom
add_hf 'Return-Path', "<#{unixfrom}>" unless @header['return-path']
end
end
def add_hf( name, field )
key = name.downcase
field = new_hf(name, field)
if ALLOW_MULTIPLE.include? key
(@header[key] ||= []).push field
else
@header[key] = field
end
end
def new_hf( name, field )
HeaderField.new(name, field, @config)
end
###
### body
###
public
def body_port
parse_body
@body_port
end
def each( &block )
body_port().ropen {|f| f.each(&block) }
end
def body
parse_body
@body_port.ropen {|f|
return f.read
}
end
def body=( str )
parse_body
@body_port.wopen {|f| f.write str }
str
end
alias preamble body
alias preamble= body=
def epilogue
parse_body
@epilogue.dup
end
def epilogue=( str )
parse_body
@epilogue = str
str
end
def parts
parse_body
@parts
end
def each_part( &block )
parts().each(&block)
end
private
def parse_body( f = nil )
return if @body_parsed
if f
parse_body_0 f
else
@port.ropen {|f|
skip_header f
parse_body_0 f
}
end
@body_parsed = true
end
def skip_header( f )
while line = f.gets
return if /\A[\r\n]*\z/ === line
end
end
def parse_body_0( f )
if multipart?
read_multipart f
else
@body_port = @config.new_body_port(self)
@body_port.wopen {|w|
w.write f.read
}
end
end
def read_multipart( src )
bound = @header['content-type'].params['boundary']
is_sep = /\A--#{Regexp.quote bound}(?:--)?[ \t]*(?:\n|\r\n|\r)/
lastbound = "--#{bound}--"
ports = [ @config.new_preamble_port(self) ]
begin
f = ports.last.wopen
while line = src.gets
if is_sep === line
f.close
break if line.strip == lastbound
ports.push @config.new_part_port(self)
f = ports.last.wopen
else
f << line
end
end
@epilogue = (src.read || '')
ensure
f.close if f and not f.closed?
end
@body_port = ports.shift
@parts = ports.map {|p| self.class.new(p, @config) }
end
end # class Mail
end # module TMail

View File

@ -0,0 +1,414 @@
#
# mailbox.rb
#
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# This program is free software.
# You can distribute/modify this program under the terms of
# the GNU Lesser General Public License version 2 or later.
#
require 'tmail/port'
require 'socket'
require 'mutex_m'
unless [].respond_to?(:sort_by)
module Enumerable#:nodoc:
def sort_by
map {|i| [yield(i), i] }.sort {|a,b| a.first <=> b.first }.map {|i| i[1] }
end
end
end
module TMail
class MhMailbox
PORT_CLASS = MhPort
def initialize( dir )
edir = File.expand_path(dir)
raise ArgumentError, "not directory: #{dir}"\
unless FileTest.directory? edir
@dirname = edir
@last_file = nil
@last_atime = nil
end
def directory
@dirname
end
alias dirname directory
attr_accessor :last_atime
def inspect
"#<#{self.class} #{@dirname}>"
end
def close
end
def new_port
PORT_CLASS.new(next_file_name())
end
def each_port
mail_files().each do |path|
yield PORT_CLASS.new(path)
end
@last_atime = Time.now
end
alias each each_port
def reverse_each_port
mail_files().reverse_each do |path|
yield PORT_CLASS.new(path)
end
@last_atime = Time.now
end
alias reverse_each reverse_each_port
# old #each_mail returns Port
#def each_mail
# each_port do |port|
# yield Mail.new(port)
# end
#end
def each_new_port( mtime = nil, &block )
mtime ||= @last_atime
return each_port(&block) unless mtime
return unless File.mtime(@dirname) >= mtime
mail_files().each do |path|
yield PORT_CLASS.new(path) if File.mtime(path) > mtime
end
@last_atime = Time.now
end
private
def mail_files
Dir.entries(@dirname)\
.select {|s| /\A\d+\z/ === s }\
.map {|s| s.to_i }\
.sort\
.map {|i| "#{@dirname}/#{i}" }\
.select {|path| FileTest.file? path }
end
def next_file_name
unless n = @last_file
n = 0
Dir.entries(@dirname)\
.select {|s| /\A\d+\z/ === s }\
.map {|s| s.to_i }.sort\
.each do |i|
next unless FileTest.file? "#{@dirname}/#{i}"
n = i
end
end
begin
n += 1
end while FileTest.exist? "#{@dirname}/#{n}"
@last_file = n
"#{@dirname}/#{n}"
end
end # MhMailbox
MhLoader = MhMailbox
class UNIXMbox
def UNIXMbox.lock( fname )
begin
f = File.open(fname)
f.flock File::LOCK_EX
yield f
ensure
f.flock File::LOCK_UN
f.close if f and not f.closed?
end
end
class << self
alias newobj new
end
def UNIXMbox.new( fname, tmpdir = nil, readonly = false )
tmpdir = ENV['TEMP'] || ENV['TMP'] || '/tmp'
newobj(fname, "#{tmpdir}/ruby_tmail_#{$$}_#{rand()}", readonly, false)
end
def UNIXMbox.static_new( fname, dir, readonly = false )
newobj(fname, dir, readonly, true)
end
def initialize( fname, mhdir, readonly, static )
@filename = fname
@readonly = readonly
@closed = false
Dir.mkdir mhdir
@real = MhMailbox.new(mhdir)
@finalizer = UNIXMbox.mkfinal(@real, @filename, !@readonly, !static)
ObjectSpace.define_finalizer self, @finalizer
end
def UNIXMbox.mkfinal( mh, mboxfile, writeback_p, cleanup_p )
lambda {
if writeback_p
lock(mboxfile) {|f|
mh.each_port do |port|
f.puts create_from_line(port)
port.ropen {|r|
f.puts r.read
}
end
}
end
if cleanup_p
Dir.foreach(mh.dirname) do |fname|
next if /\A\.\.?\z/ === fname
File.unlink "#{mh.dirname}/#{fname}"
end
Dir.rmdir mh.dirname
end
}
end
# make _From line
def UNIXMbox.create_from_line( port )
sprintf 'From %s %s',
fromaddr(), TextUtils.time2str(File.mtime(port.filename))
end
def UNIXMbox.fromaddr
h = HeaderField.new_from_port(port, 'Return-Path') ||
HeaderField.new_from_port(port, 'From') or return 'nobody'
a = h.addrs[0] or return 'nobody'
a.spec
end
private_class_method :fromaddr
def close
return if @closed
ObjectSpace.undefine_finalizer self
@finalizer.call
@finalizer = nil
@real = nil
@closed = true
@updated = nil
end
def each_port( &block )
close_check
update
@real.each_port(&block)
end
alias each each_port
def reverse_each_port( &block )
close_check
update
@real.reverse_each_port(&block)
end
alias reverse_each reverse_each_port
# old #each_mail returns Port
#def each_mail( &block )
# each_port do |port|
# yield Mail.new(port)
# end
#end
def each_new_port( mtime = nil )
close_check
update
@real.each_new_port(mtime) {|p| yield p }
end
def new_port
close_check
@real.new_port
end
private
def close_check
@closed and raise ArgumentError, 'accessing already closed mbox'
end
def update
return if FileTest.zero?(@filename)
return if @updated and File.mtime(@filename) < @updated
w = nil
port = nil
time = nil
UNIXMbox.lock(@filename) {|f|
begin
f.each do |line|
if /\AFrom / === line
w.close if w
File.utime time, time, port.filename if time
port = @real.new_port
w = port.wopen
time = fromline2time(line)
else
w.print line if w
end
end
ensure
if w and not w.closed?
w.close
File.utime time, time, port.filename if time
end
end
f.truncate(0) unless @readonly
@updated = Time.now
}
end
def fromline2time( line )
m = /\AFrom \S+ \w+ (\w+) (\d+) (\d+):(\d+):(\d+) (\d+)/.match(line) \
or return nil
Time.local(m[6].to_i, m[1], m[2].to_i, m[3].to_i, m[4].to_i, m[5].to_i)
end
end # UNIXMbox
MboxLoader = UNIXMbox
class Maildir
extend Mutex_m
PORT_CLASS = MaildirPort
@seq = 0
def Maildir.unique_number
synchronize {
@seq += 1
return @seq
}
end
def initialize( dir = nil )
@dirname = dir || ENV['MAILDIR']
raise ArgumentError, "not directory: #{@dirname}"\
unless FileTest.directory? @dirname
@new = "#{@dirname}/new"
@tmp = "#{@dirname}/tmp"
@cur = "#{@dirname}/cur"
end
def directory
@dirname
end
def inspect
"#<#{self.class} #{@dirname}>"
end
def close
end
def each_port
mail_files(@cur).each do |path|
yield PORT_CLASS.new(path)
end
end
alias each each_port
def reverse_each_port
mail_files(@cur).reverse_each do |path|
yield PORT_CLASS.new(path)
end
end
alias reverse_each reverse_each_port
def new_port
fname = nil
tmpfname = nil
newfname = nil
begin
fname = "#{Time.now.to_i}.#{$$}_#{Maildir.unique_number}.#{Socket.gethostname}"
tmpfname = "#{@tmp}/#{fname}"
newfname = "#{@new}/#{fname}"
end while FileTest.exist? tmpfname
if block_given?
File.open(tmpfname, 'w') {|f| yield f }
File.rename tmpfname, newfname
PORT_CLASS.new(newfname)
else
File.open(tmpfname, 'w') {|f| f.write "\n\n" }
PORT_CLASS.new(tmpfname)
end
end
def each_new_port
mail_files(@new).each do |path|
dest = @cur + '/' + File.basename(path)
File.rename path, dest
yield PORT_CLASS.new(dest)
end
check_tmp
end
TOO_OLD = 60 * 60 * 36 # 36 hour
def check_tmp
old = Time.now.to_i - TOO_OLD
each_filename(@tmp) do |full, fname|
if FileTest.file? full and
File.stat(full).mtime.to_i < old
File.unlink full
end
end
end
private
def mail_files( dir )
Dir.entries(dir)\
.select {|s| s[0] != ?. }\
.sort_by {|s| s.slice(/\A\d+/).to_i }\
.map {|s| "#{dir}/#{s}" }\
.select {|path| FileTest.file? path }
end
def each_filename( dir )
Dir.foreach(dir) do |fname|
path = "#{dir}/#{fname}"
if fname[0] != ?. and FileTest.file? path
yield path, fname
end
end
end
end # Maildir
MaildirLoader = Maildir
end # module TMail

View File

@ -0,0 +1 @@
require 'tmail/mailbox'

View File

@ -0,0 +1,261 @@
#
# net.rb
#
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# This program is free software.
# You can distribute/modify this program under the terms of
# the GNU Lesser General Public License version 2 or later.
#
require 'nkf'
module TMail
class Mail
def send_to( smtp )
do_send_to(smtp) do
ready_to_send
end
end
def send_text_to( smtp )
do_send_to(smtp) do
ready_to_send
mime_encode
end
end
def do_send_to( smtp )
from = from_address or raise ArgumentError, 'no from address'
(dests = destinations).empty? and raise ArgumentError, 'no receipient'
yield
send_to_0 smtp, from, dests
end
private :do_send_to
def send_to_0( smtp, from, to )
smtp.ready(from, to) do |f|
encoded "\r\n", 'j', f, ''
end
end
def ready_to_send
delete_no_send_fields
add_message_id
add_date
end
NOSEND_FIELDS = %w(
received
bcc
)
def delete_no_send_fields
NOSEND_FIELDS.each do |nm|
delete nm
end
delete_if {|n,v| v.empty? }
end
def add_message_id( fqdn = nil )
self.message_id = ::TMail::new_msgid(fqdn)
end
def add_date
self.date = Time.now
end
def mime_encode
if parts.empty?
mime_encode_singlepart
else
mime_encode_multipart true
end
end
def mime_encode_singlepart
self.mime_version = '1.0'
b = body
if NKF.guess(b) != NKF::BINARY
mime_encode_text b
else
mime_encode_binary b
end
end
def mime_encode_text( body )
self.body = NKF.nkf('-j -m0', body)
self.set_content_type 'text', 'plain', {'charset' => 'iso-2022-jp'}
self.encoding = '7bit'
end
def mime_encode_binary( body )
self.body = [body].pack('m')
self.set_content_type 'application', 'octet-stream'
self.encoding = 'Base64'
end
def mime_encode_multipart( top = true )
self.mime_version = '1.0' if top
self.set_content_type 'multipart', 'mixed'
e = encoding(nil)
if e and not /\A(?:7bit|8bit|binary)\z/i === e
raise ArgumentError,
'using C.T.Encoding with multipart mail is not permitted'
end
end
def create_empty_mail
self.class.new(StringPort.new(''), @config)
end
def create_reply
setup_reply create_empty_mail()
end
def setup_reply( m )
if tmp = reply_addresses(nil)
m.to_addrs = tmp
end
mid = message_id(nil)
tmp = references(nil) || []
tmp.push mid if mid
m.in_reply_to = [mid] if mid
m.references = tmp unless tmp.empty?
m.subject = 'Re: ' + subject('').sub(/\A(?:\s*re:)+/i, '')
m
end
def create_forward
setup_forward create_empty_mail()
end
def setup_forward( mail )
m = Mail.new(StringPort.new(''))
m.body = decoded
m.set_content_type 'message', 'rfc822'
m.encoding = encoding('7bit')
mail.parts.push m
end
end
class DeleteFields
NOSEND_FIELDS = %w(
received
bcc
)
def initialize( nosend = nil, delempty = true )
@no_send_fields = nosend || NOSEND_FIELDS.dup
@delete_empty_fields = delempty
end
attr :no_send_fields
attr :delete_empty_fields, true
def exec( mail )
@no_send_fields.each do |nm|
delete nm
end
delete_if {|n,v| v.empty? } if @delete_empty_fields
end
end
class AddMessageId
def initialize( fqdn = nil )
@fqdn = fqdn
end
attr :fqdn, true
def exec( mail )
mail.message_id = ::TMail::new_msgid(@fqdn)
end
end
class AddDate
def exec( mail )
mail.date = Time.now
end
end
class MimeEncodeAuto
def initialize( s = nil, m = nil )
@singlepart_composer = s || MimeEncodeSingle.new
@multipart_composer = m || MimeEncodeMulti.new
end
attr :singlepart_composer
attr :multipart_composer
def exec( mail )
if mail._builtin_multipart?
then @multipart_composer
else @singlepart_composer end.exec mail
end
end
class MimeEncodeSingle
def exec( mail )
mail.mime_version = '1.0'
b = mail.body
if NKF.guess(b) != NKF::BINARY
on_text b
else
on_binary b
end
end
def on_text( body )
mail.body = NKF.nkf('-j -m0', body)
mail.set_content_type 'text', 'plain', {'charset' => 'iso-2022-jp'}
mail.encoding = '7bit'
end
def on_binary( body )
mail.body = [body].pack('m')
mail.set_content_type 'application', 'octet-stream'
mail.encoding = 'Base64'
end
end
class MimeEncodeMulti
def exec( mail, top = true )
mail.mime_version = '1.0' if top
mail.set_content_type 'multipart', 'mixed'
e = encoding(nil)
if e and not /\A(?:7bit|8bit|binary)\z/i === e
raise ArgumentError,
'using C.T.Encoding with multipart mail is not permitted'
end
mail.parts.each do |m|
exec m, false if m._builtin_multipart?
end
end
end
end # module TMail

View File

@ -0,0 +1,116 @@
#
# obsolete.rb
#
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# This program is free software.
# You can distribute/modify this program under the terms of
# the GNU Lesser General Public License version 2 or later.
#
module TMail
# mail.rb
class Mail
alias include? key?
alias has_key? key?
def values
ret = []
each_field {|v| ret.push v }
ret
end
def value?( val )
HeaderField === val or return false
[ @header[val.name.downcase] ].flatten.include? val
end
alias has_value? value?
end
# facade.rb
class Mail
def from_addr( default = nil )
addr, = from_addrs(nil)
addr || default
end
def from_address( default = nil )
if a = from_addr(nil)
a.spec
else
default
end
end
alias from_address= from_addrs=
def from_phrase( default = nil )
if a = from_addr(nil)
a.phrase
else
default
end
end
alias msgid message_id
alias msgid= message_id=
alias each_dest each_destination
end
# address.rb
class Address
alias route routes
alias addr spec
def spec=( str )
@local, @domain = str.split(/@/,2).map {|s| s.split(/\./) }
end
alias addr= spec=
alias address= spec=
end
# mbox.rb
class MhMailbox
alias new_mail new_port
alias each_mail each_port
alias each_newmail each_new_port
end
class UNIXMbox
alias new_mail new_port
alias each_mail each_port
alias each_newmail each_new_port
end
class Maildir
alias new_mail new_port
alias each_mail each_port
alias each_newmail each_new_port
end
# utils.rb
extend TextUtils
class << self
alias msgid? message_id?
alias boundary new_boundary
alias msgid new_message_id
alias new_msgid new_message_id
end
def Mail.boundary
::TMail.new_boundary
end
def Mail.msgid
::TMail.new_message_id
end
end # module TMail

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,358 @@
#
# port.rb
#
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# This program is free software.
# You can distribute/modify this program under the terms of
# the GNU Lesser General Public License version 2 or later.
#
require 'tmail/stringio'
module TMail
class Port
def reproducible?
false
end
end
###
### FilePort
###
class FilePort < Port
def initialize( fname )
@filename = File.expand_path(fname)
super()
end
attr_reader :filename
alias ident filename
def ==( other )
other.respond_to?(:filename) and @filename == other.filename
end
alias eql? ==
def hash
@filename.hash
end
def inspect
"#<#{self.class}:#{@filename}>"
end
def reproducible?
true
end
def size
File.size @filename
end
def ropen( &block )
File.open(@filename, &block)
end
def wopen( &block )
File.open(@filename, 'w', &block)
end
def aopen( &block )
File.open(@filename, 'a', &block)
end
def read_all
ropen {|f|
return f.read
}
end
def remove
File.unlink @filename
end
def move_to( port )
begin
File.link @filename, port.filename
rescue Errno::EXDEV
copy_to port
end
File.unlink @filename
end
alias mv move_to
def copy_to( port )
if FilePort === port
copy_file @filename, port.filename
else
File.open(@filename) {|r|
port.wopen {|w|
while s = r.sysread(4096)
w.write << s
end
} }
end
end
alias cp copy_to
private
# from fileutils.rb
def copy_file( src, dest )
st = r = w = nil
File.open(src, 'rb') {|r|
File.open(dest, 'wb') {|w|
st = r.stat
begin
while true
w.write r.sysread(st.blksize)
end
rescue EOFError
end
} }
end
end
module MailFlags
def seen=( b )
set_status 'S', b
end
def seen?
get_status 'S'
end
def replied=( b )
set_status 'R', b
end
def replied?
get_status 'R'
end
def flagged=( b )
set_status 'F', b
end
def flagged?
get_status 'F'
end
private
def procinfostr( str, tag, true_p )
a = str.upcase.split(//)
a.push true_p ? tag : nil
a.delete tag unless true_p
a.compact.sort.join('').squeeze
end
end
class MhPort < FilePort
include MailFlags
private
def set_status( tag, flag )
begin
tmpfile = @filename + '.tmailtmp.' + $$.to_s
File.open(tmpfile, 'w') {|f|
write_status f, tag, flag
}
File.unlink @filename
File.link tmpfile, @filename
ensure
File.unlink tmpfile
end
end
def write_status( f, tag, flag )
stat = ''
File.open(@filename) {|r|
while line = r.gets
if line.strip.empty?
break
elsif m = /\AX-TMail-Status:/i.match(line)
stat = m.post_match.strip
else
f.print line
end
end
s = procinfostr(stat, tag, flag)
f.puts 'X-TMail-Status: ' + s unless s.empty?
f.puts
while s = r.read(2048)
f.write s
end
}
end
def get_status( tag )
File.foreach(@filename) {|line|
return false if line.strip.empty?
if m = /\AX-TMail-Status:/i.match(line)
return m.post_match.strip.include?(tag[0])
end
}
false
end
end
class MaildirPort < FilePort
def move_to_new
new = replace_dir(@filename, 'new')
File.rename @filename, new
@filename = new
end
def move_to_cur
new = replace_dir(@filename, 'cur')
File.rename @filename, new
@filename = new
end
def replace_dir( path, dir )
"#{File.dirname File.dirname(path)}/#{dir}/#{File.basename path}"
end
private :replace_dir
include MailFlags
private
MAIL_FILE = /\A(\d+\.[\d_]+\.[^:]+)(?:\:(\d),(\w+)?)?\z/
def set_status( tag, flag )
if m = MAIL_FILE.match(File.basename(@filename))
s, uniq, type, info, = m.to_a
return if type and type != '2' # do not change anything
newname = File.dirname(@filename) + '/' +
uniq + ':2,' + procinfostr(info.to_s, tag, flag)
else
newname = @filename + ':2,' + tag
end
File.link @filename, newname
File.unlink @filename
@filename = newname
end
def get_status( tag )
m = MAIL_FILE.match(File.basename(@filename)) or return false
m[2] == '2' and m[3].to_s.include?(tag[0])
end
end
###
### StringPort
###
class StringPort < Port
def initialize( str = '' )
@buffer = str
super()
end
def string
@buffer
end
def to_s
@buffer.dup
end
alias read_all to_s
def size
@buffer.size
end
def ==( other )
StringPort === other and @buffer.equal? other.string
end
alias eql? ==
def hash
@buffer.id.hash
end
def inspect
"#<#{self.class}:id=#{sprintf '0x%x', @buffer.id}>"
end
def reproducible?
true
end
def ropen( &block )
@buffer or raise Errno::ENOENT, "#{inspect} is already removed"
StringInput.open(@buffer, &block)
end
def wopen( &block )
@buffer = ''
StringOutput.new(@buffer, &block)
end
def aopen( &block )
@buffer ||= ''
StringOutput.new(@buffer, &block)
end
def remove
@buffer = nil
end
alias rm remove
def copy_to( port )
port.wopen {|f|
f.write @buffer
}
end
alias cp copy_to
def move_to( port )
if StringPort === port
str = @buffer
port.instance_eval { @buffer = str }
else
copy_to port
end
remove
end
end
end # module TMail

View File

@ -0,0 +1,22 @@
#
# scanner.rb
#
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# This program is free software.
# You can distribute/modify this program under the terms of
# the GNU Lesser General Public License version 2 or later.
#
require 'tmail/utils'
module TMail
require 'tmail/scanner_r.rb'
begin
raise LoadError, 'Turn off Ruby extention by user choice' if ENV['NORUBYEXT']
require 'tmail/scanner_c.so'
Scanner = Scanner_C
rescue LoadError
Scanner = Scanner_R
end
end

View File

@ -0,0 +1,244 @@
#
# scanner_r.rb
#
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# This program is free software.
# You can distribute/modify this program under the terms of
# the GNU Lesser General Public License version 2 or later.
#
require 'tmail/config'
module TMail
class Scanner_R
Version = '0.10.7'
Version.freeze
MIME_HEADERS = {
:CTYPE => true,
:CENCODING => true,
:CDISPOSITION => true
}
alnum = 'a-zA-Z0-9'
atomsyms = %q[ _#!$%&`'*+-{|}~^/=? ].strip
tokensyms = %q[ _#!$%&`'*+-{|}~^. ].strip
atomchars = alnum + Regexp.quote(atomsyms)
tokenchars = alnum + Regexp.quote(tokensyms)
iso2022str = '\e(?!\(B)..(?:[^\e]+|\e(?!\(B)..)*\e\(B'
eucstr = '(?:[\xa1-\xfe][\xa1-\xfe])+'
sjisstr = '(?:[\x81-\x9f\xe0-\xef][\x40-\x7e\x80-\xfc])+'
utf8str = '(?:[\xc0-\xdf][\x80-\xbf]|[\xe0-\xef][\x80-\xbf][\x80-\xbf])+'
quoted_with_iso2022 = /\A(?:[^\\\e"]+|#{iso2022str})+/n
domlit_with_iso2022 = /\A(?:[^\\\e\]]+|#{iso2022str})+/n
comment_with_iso2022 = /\A(?:[^\\\e()]+|#{iso2022str})+/n
quoted_without_iso2022 = /\A[^\\"]+/n
domlit_without_iso2022 = /\A[^\\\]]+/n
comment_without_iso2022 = /\A[^\\()]+/n
PATTERN_TABLE = {}
PATTERN_TABLE['EUC'] =
[
/\A(?:[#{atomchars}]+|#{iso2022str}|#{eucstr})+/n,
/\A(?:[#{tokenchars}]+|#{iso2022str}|#{eucstr})+/n,
quoted_with_iso2022,
domlit_with_iso2022,
comment_with_iso2022
]
PATTERN_TABLE['SJIS'] =
[
/\A(?:[#{atomchars}]+|#{iso2022str}|#{sjisstr})+/n,
/\A(?:[#{tokenchars}]+|#{iso2022str}|#{sjisstr})+/n,
quoted_with_iso2022,
domlit_with_iso2022,
comment_with_iso2022
]
PATTERN_TABLE['UTF8'] =
[
/\A(?:[#{atomchars}]+|#{utf8str})+/n,
/\A(?:[#{tokenchars}]+|#{utf8str})+/n,
quoted_without_iso2022,
domlit_without_iso2022,
comment_without_iso2022
]
PATTERN_TABLE['NONE'] =
[
/\A[#{atomchars}]+/n,
/\A[#{tokenchars}]+/n,
quoted_without_iso2022,
domlit_without_iso2022,
comment_without_iso2022
]
def initialize( str, scantype, comments )
init_scanner str
@comments = comments || []
@debug = false
# fix scanner mode
@received = (scantype == :RECEIVED)
@is_mime_header = MIME_HEADERS[scantype]
atom, token, @quoted_re, @domlit_re, @comment_re = PATTERN_TABLE[$KCODE]
@word_re = (MIME_HEADERS[scantype] ? token : atom)
end
attr_accessor :debug
def scan( &block )
if @debug
scan_main do |arr|
s, v = arr
printf "%7d %-10s %s\n",
rest_size(),
s.respond_to?(:id2name) ? s.id2name : s.inspect,
v.inspect
yield arr
end
else
scan_main(&block)
end
end
private
RECV_TOKEN = {
'from' => :FROM,
'by' => :BY,
'via' => :VIA,
'with' => :WITH,
'id' => :ID,
'for' => :FOR
}
def scan_main
until eof?
if skip(/\A[\n\r\t ]+/n) # LWSP
break if eof?
end
if s = readstr(@word_re)
if @is_mime_header
yield :TOKEN, s
else
# atom
if /\A\d+\z/ === s
yield :DIGIT, s
elsif @received
yield RECV_TOKEN[s.downcase] || :ATOM, s
else
yield :ATOM, s
end
end
elsif skip(/\A"/)
yield :QUOTED, scan_quoted_word()
elsif skip(/\A\[/)
yield :DOMLIT, scan_domain_literal()
elsif skip(/\A\(/)
@comments.push scan_comment()
else
c = readchar()
yield c, c
end
end
yield false, '$'
end
def scan_quoted_word
scan_qstr(@quoted_re, /\A"/, 'quoted-word')
end
def scan_domain_literal
'[' + scan_qstr(@domlit_re, /\A\]/, 'domain-literal') + ']'
end
def scan_qstr( pattern, terminal, type )
result = ''
until eof?
if s = readstr(pattern) then result << s
elsif skip(terminal) then return result
elsif skip(/\A\\/) then result << readchar()
else
raise "TMail FATAL: not match in #{type}"
end
end
scan_error! "found unterminated #{type}"
end
def scan_comment
result = ''
nest = 1
content = @comment_re
until eof?
if s = readstr(content) then result << s
elsif skip(/\A\)/) then nest -= 1
return result if nest == 0
result << ')'
elsif skip(/\A\(/) then nest += 1
result << '('
elsif skip(/\A\\/) then result << readchar()
else
raise 'TMail FATAL: not match in comment'
end
end
scan_error! 'found unterminated comment'
end
# string scanner
def init_scanner( str )
@src = str
end
def eof?
@src.empty?
end
def rest_size
@src.size
end
def readstr( re )
if m = re.match(@src)
@src = m.post_match
m[0]
else
nil
end
end
def readchar
readstr(/\A./)
end
def skip( re )
if m = re.match(@src)
@src = m.post_match
true
else
false
end
end
def scan_error!( msg )
raise SyntaxError, msg
end
end
end # module TMail

View File

@ -0,0 +1,260 @@
#
# stringio.rb
#
# Copyright (c) 1999-2003 Minero Aoki <aamine@loveruby.net>
#
# This program is free software.
# You can distribute/modify this program under the terms of
# the GNU Lesser General Public License version 2 or later.
#
# Id: stringio.rb,v 1.10 2003/04/27 22:02:14 aamine Exp
#
class StringInput#:nodoc:
include Enumerable
class << self
def new( str )
if block_given?
begin
f = super
yield f
ensure
f.close if f
end
else
super
end
end
alias open new
end
def initialize( str )
@src = str
@pos = 0
@closed = false
@lineno = 0
end
attr_reader :lineno
def string
@src
end
def inspect
"#<#{self.class}:#{@closed ? 'closed' : 'open'},src=#{@src[0,30].inspect}>"
end
def close
stream_check!
@pos = nil
@closed = true
end
def closed?
@closed
end
def pos
stream_check!
[@pos, @src.size].min
end
alias tell pos
def seek( offset, whence = IO::SEEK_SET )
stream_check!
case whence
when IO::SEEK_SET
@pos = offset
when IO::SEEK_CUR
@pos += offset
when IO::SEEK_END
@pos = @src.size - offset
else
raise ArgumentError, "unknown seek flag: #{whence}"
end
@pos = 0 if @pos < 0
@pos = [@pos, @src.size + 1].min
offset
end
def rewind
stream_check!
@pos = 0
end
def eof?
stream_check!
@pos > @src.size
end
def each( &block )
stream_check!
begin
@src.each(&block)
ensure
@pos = 0
end
end
def gets
stream_check!
if idx = @src.index(?\n, @pos)
idx += 1 # "\n".size
line = @src[ @pos ... idx ]
@pos = idx
@pos += 1 if @pos == @src.size
else
line = @src[ @pos .. -1 ]
@pos = @src.size + 1
end
@lineno += 1
line
end
def getc
stream_check!
ch = @src[@pos]
@pos += 1
@pos += 1 if @pos == @src.size
ch
end
def read( len = nil )
stream_check!
return read_all unless len
str = @src[@pos, len]
@pos += len
@pos += 1 if @pos == @src.size
str
end
alias sysread read
def read_all
stream_check!
return nil if eof?
rest = @src[@pos ... @src.size]
@pos = @src.size + 1
rest
end
def stream_check!
@closed and raise IOError, 'closed stream'
end
end
class StringOutput#:nodoc:
class << self
def new( str = '' )
if block_given?
begin
f = super
yield f
ensure
f.close if f
end
else
super
end
end
alias open new
end
def initialize( str = '' )
@dest = str
@closed = false
end
def close
@closed = true
end
def closed?
@closed
end
def string
@dest
end
alias value string
alias to_str string
def size
@dest.size
end
alias pos size
def inspect
"#<#{self.class}:#{@dest ? 'open' : 'closed'},#{id}>"
end
def print( *args )
stream_check!
raise ArgumentError, 'wrong # of argument (0 for >1)' if args.empty?
args.each do |s|
raise ArgumentError, 'nil not allowed' if s.nil?
@dest << s.to_s
end
nil
end
def puts( *args )
stream_check!
args.each do |str|
@dest << (s = str.to_s)
@dest << "\n" unless s[-1] == ?\n
end
@dest << "\n" if args.empty?
nil
end
def putc( ch )
stream_check!
@dest << ch.chr
nil
end
def printf( *args )
stream_check!
@dest << sprintf(*args)
nil
end
def write( str )
stream_check!
s = str.to_s
@dest << s
s.size
end
alias syswrite write
def <<( str )
stream_check!
@dest << str.to_s
self
end
private
def stream_check!
@closed and raise IOError, 'closed stream'
end
end

View File

@ -0,0 +1 @@
require 'tmail'

View File

@ -0,0 +1,215 @@
#
# utils.rb
#
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
#
# This program is free software.
# You can distribute/modify this program under the terms of
# the GNU Lesser General Public License version 2 or later.
#
module TMail
class SyntaxError < StandardError; end
def TMail.new_boundary
'mimepart_' + random_tag
end
def TMail.new_message_id( fqdn = nil )
fqdn ||= ::Socket.gethostname
"<#{random_tag()}@#{fqdn}.tmail>"
end
def TMail.random_tag
@uniq += 1
t = Time.now
sprintf('%x%x_%x%x%d%x',
t.to_i, t.tv_usec,
$$, Thread.current.id, @uniq, rand(255))
end
private_class_method :random_tag
@uniq = 0
module TextUtils
aspecial = '()<>[]:;.@\\,"'
tspecial = '()<>[];:@\\,"/?='
lwsp = " \t\r\n"
control = '\x00-\x1f\x7f-\xff'
ATOM_UNSAFE = /[#{Regexp.quote aspecial}#{control}#{lwsp}]/n
PHRASE_UNSAFE = /[#{Regexp.quote aspecial}#{control}]/n
TOKEN_UNSAFE = /[#{Regexp.quote tspecial}#{control}#{lwsp}]/n
CONTROL_CHAR = /[#{control}]/n
def atom_safe?( str )
not ATOM_UNSAFE === str
end
def quote_atom( str )
(ATOM_UNSAFE === str) ? dquote(str) : str
end
def quote_phrase( str )
(PHRASE_UNSAFE === str) ? dquote(str) : str
end
def token_safe?( str )
not TOKEN_UNSAFE === str
end
def quote_token( str )
(TOKEN_UNSAFE === str) ? dquote(str) : str
end
def dquote( str )
'"' + str.gsub(/["\\]/n) {|s| '\\' + s } + '"'
end
private :dquote
def join_domain( arr )
arr.map {|i|
if /\A\[.*\]\z/ === i
i
else
quote_atom(i)
end
}.join('.')
end
ZONESTR_TABLE = {
'jst' => 9 * 60,
'eet' => 2 * 60,
'bst' => 1 * 60,
'met' => 1 * 60,
'gmt' => 0,
'utc' => 0,
'ut' => 0,
'nst' => -(3 * 60 + 30),
'ast' => -4 * 60,
'edt' => -4 * 60,
'est' => -5 * 60,
'cdt' => -5 * 60,
'cst' => -6 * 60,
'mdt' => -6 * 60,
'mst' => -7 * 60,
'pdt' => -7 * 60,
'pst' => -8 * 60,
'a' => -1 * 60,
'b' => -2 * 60,
'c' => -3 * 60,
'd' => -4 * 60,
'e' => -5 * 60,
'f' => -6 * 60,
'g' => -7 * 60,
'h' => -8 * 60,
'i' => -9 * 60,
# j not use
'k' => -10 * 60,
'l' => -11 * 60,
'm' => -12 * 60,
'n' => 1 * 60,
'o' => 2 * 60,
'p' => 3 * 60,
'q' => 4 * 60,
'r' => 5 * 60,
's' => 6 * 60,
't' => 7 * 60,
'u' => 8 * 60,
'v' => 9 * 60,
'w' => 10 * 60,
'x' => 11 * 60,
'y' => 12 * 60,
'z' => 0 * 60
}
def timezone_string_to_unixtime( str )
if m = /([\+\-])(\d\d?)(\d\d)/.match(str)
sec = (m[2].to_i * 60 + m[3].to_i) * 60
m[1] == '-' ? -sec : sec
else
min = ZONESTR_TABLE[str.downcase] or
raise SyntaxError, "wrong timezone format '#{str}'"
min * 60
end
end
WDAY = %w( Sun Mon Tue Wed Thu Fri Sat TMailBUG )
MONTH = %w( TMailBUG Jan Feb Mar Apr May Jun
Jul Aug Sep Oct Nov Dec TMailBUG )
def time2str( tm )
# [ruby-list:7928]
gmt = Time.at(tm.to_i)
gmt.gmtime
offset = tm.to_i - Time.local(*gmt.to_a[0,6].reverse).to_i
# DO NOT USE strftime: setlocale() breaks it
sprintf '%s, %s %s %d %02d:%02d:%02d %+.2d%.2d',
WDAY[tm.wday], tm.mday, MONTH[tm.month],
tm.year, tm.hour, tm.min, tm.sec,
*(offset / 60).divmod(60)
end
MESSAGE_ID = /<[^\@>]+\@[^>\@]+>/
def message_id?( str )
MESSAGE_ID === str
end
MIME_ENCODED = /=\?[^\s?=]+\?[QB]\?[^\s?=]+\?=/i
def mime_encoded?( str )
MIME_ENCODED === str
end
def decode_params( hash )
new = Hash.new
encoded = nil
hash.each do |key, value|
if m = /\*(?:(\d+)\*)?\z/.match(key)
((encoded ||= {})[m.pre_match] ||= [])[(m[1] || 0).to_i] = value
else
new[key] = to_kcode(value)
end
end
if encoded
encoded.each do |key, strings|
new[key] = decode_RFC2231(strings.join(''))
end
end
new
end
NKF_FLAGS = {
'EUC' => '-e -m',
'SJIS' => '-s -m'
}
def to_kcode( str )
flag = NKF_FLAGS[$KCODE] or return str
NKF.nkf(flag, str)
end
RFC2231_ENCODED = /\A(?:iso-2022-jp|euc-jp|shift_jis|us-ascii)?'[a-z]*'/in
def decode_RFC2231( str )
m = RFC2231_ENCODED.match(str) or return str
NKF.nkf(NKF_FLAGS[$KCODE],
m.post_match.gsub(/%[\da-f]{2}/in) {|s| s[1,2].hex.chr })
end
end
end

View File

@ -0,0 +1,3 @@
Hello there,
Mr. <%= @recipient %>

View File

@ -0,0 +1,3 @@
Hello there,
Mr. <%= @recipient %>

View File

@ -0,0 +1,92 @@
$:.unshift(File.dirname(__FILE__) + "/../lib/")
require 'test/unit'
require 'action_mailer'
class TestMailer < ActionMailer::Base
def signed_up(recipient)
@recipients = recipient
@subject = "[Signed up] Welcome #{recipient}"
@from = "system@loudthinking.com"
@sent_on = Time.local(2004, 12, 12)
@body["recipient"] = recipient
end
def cancelled_account(recipient)
@recipients = recipient
@subject = "[Cancelled] Goodbye #{recipient}"
@from = "system@loudthinking.com"
@sent_on = Time.local(2004, 12, 12)
@body = "Goodbye, Mr. #{recipient}"
end
end
TestMailer.template_root = File.dirname(__FILE__) + "/fixtures"
class ActionMailerTest < Test::Unit::TestCase
def setup
ActionMailer::Base.delivery_method = :test
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.deliveries = []
@recipient = 'test@localhost'
end
def test_signed_up
expected = TMail::Mail.new
expected.to = @recipient
expected.subject = "[Signed up] Welcome #{@recipient}"
expected.body = "Hello there, \n\nMr. #{@recipient}"
expected.from = "system@loudthinking.com"
expected.date = Time.local(2004, 12, 12)
created = nil
assert_nothing_raised { created = TestMailer.create_signed_up(@recipient) }
assert_not_nil created
assert_equal expected.encoded, created.encoded
assert_nothing_raised { TestMailer.deliver_signed_up(@recipient) }
assert_not_nil ActionMailer::Base.deliveries.first
assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
end
def test_cancelled_account
expected = TMail::Mail.new
expected.to = @recipient
expected.subject = "[Cancelled] Goodbye #{@recipient}"
expected.body = "Goodbye, Mr. #{@recipient}"
expected.from = "system@loudthinking.com"
expected.date = Time.local(2004, 12, 12)
created = nil
assert_nothing_raised { created = TestMailer.create_cancelled_account(@recipient) }
assert_not_nil created
assert_equal expected.encoded, created.encoded
assert_nothing_raised { TestMailer.deliver_cancelled_account(@recipient) }
assert_not_nil ActionMailer::Base.deliveries.first
assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
end
def test_instances_are_nil
assert_nil ActionMailer::Base.new
assert_nil TestMailer.new
end
def test_deliveries_array
assert_not_nil ActionMailer::Base.deliveries
assert_equal 0, ActionMailer::Base.deliveries.size
TestMailer.deliver_signed_up(@recipient)
assert_equal 1, ActionMailer::Base.deliveries.size
assert_not_nil ActionMailer::Base.deliveries.first
end
def test_perform_deliveries_flag
ActionMailer::Base.perform_deliveries = false
TestMailer.deliver_signed_up(@recipient)
assert_equal 0, ActionMailer::Base.deliveries.size
ActionMailer::Base.perform_deliveries = true
TestMailer.deliver_signed_up(@recipient)
assert_equal 1, ActionMailer::Base.deliveries.size
end
end

738
actionpack/CHANGELOG Normal file
View File

@ -0,0 +1,738 @@
*CVS*
* Upgraded to Builder 1.2.1
* Added :module as an alias for :controller_prefix to url_for and friends, so you can do redirect_to(:module => "shop", :controller => "purchases")
and go to /shop/purchases/
* Added support for controllers in modules through @params["module"].
* Added reloading for dependencies under cached environments like FastCGI and mod_ruby. This makes it possible to use those environments for development.
This is turned on by default, but can be turned off with ActionController::Base.reload_dependencies = false in production environments.
NOTE: This will only have an effect if you use the new model, service, and observer class methods to mark dependencies. All libraries loaded through
require will be "forever" cached. You can, however, use ActionController::Base.load_or_require("library") to get this behavior outside of the new
dependency style.
* Added that controllers will automatically require their own helper if possible. So instead of doing:
class MsgController < AbstractApplicationController
helper :msg
end
...you can just do:
class MsgController < AbstractApplicationController
end
* Added dependencies_on(layer) to query the dependencies of a controller. Examples:
MsgController.dependencies_on(:model) # => [ :post, :comment, :attachment ]
MsgController.dependencies_on(:service) # => [ :notification_service ]
MsgController.dependencies_on(:observer) # => [ :comment_observer ]
* Added a new dependency model with the class methods model, service, and observer. Example:
class MsgController < AbstractApplicationController
model :post, :comment, :attachment
service :notification_service
observer :comment_observer
end
These new "keywords" remove the need for explicitly calling 'require' in most cases. The observer method even instantiates the
observer as well as requiring it.
* Fixed that link_to would escape & in the url again after url_for already had done so
*0.9.5* (28)
* Added helper_method to designate that a given private or protected method you should available as a helper in the view. [bitsweat]
* Fixed assert_rendered_file so it actually verifies if that was the rendered file [htonl]
* Added the option for sharing partial spacer templates just like partials themselves [radsaq]
* Fixed that Russia was named twice in country_select [alexey]
* Fixed request_origin to use remote_ip instead of remote_addr [bitsweat]
* Fixed link_to breakage when nil was passed for html_options [alexey]
* Fixed redirect_to on a virtual server setup with apache with a port other than the default where it would forget the port number [seanohalpin]
* Fixed that auto-loading webrick on Windows would cause file uploads to fail [bitsweat]
* Fixed issues with sending files on WEBrick by setting the proper binmode [bitsweat]
* Added send_data as an alternative to send_file when the stream is not read off the filesystem but from a database or generated live [bitsweat]
* Added a new way to include helpers that doesn't require the include hack and can go without the explicit require. [bitsweat]
Before:
module WeblogHelper
def self.append_features(controller) #:nodoc:
controller.ancestors.include?(ActionController::Base) ? controller.add_template_helper(self) : super
end
end
require 'weblog_helper'
class WeblogController < ActionController::Base
include WeblogHelper
end
After:
module WeblogHelper
end
class WeblogController < ActionController::Base
helper :weblog
end
* Added a default content-type of "text/xml" to .rxml renders [Ryan Platte]
* Fixed that when /controller/index was requested by the browser, url_for would generates wrong URLs [Ryan Platte]
* Fixed a bug that would share cookies between users when using FastCGI and mod_ruby [The Robot Co-op]
* Added an optional third hash parameter to the process method in functional tests that takes the session data to be used [alexey]
* Added UrlHelper#mail_to to make it easier to create mailto: style ahrefs
* Added better error messages for layouts declared with the .rhtml extension (which they shouldn't) [geech]
* Added another case to DateHelper#distance_in_minutes to return "less than a minute" instead of "0 minutes" and "1 minute" instead of "1 minutes"
* Added a hidden field to checkboxes generated with FormHelper#check_box that will make sure that the unchecked value (usually 0)
is sent even if the checkbox is not checked. This relieves the controller from doing custom checking if the the checkbox wasn't
checked. BEWARE: This might conflict with your run-on-the-mill work-around code. [Tobias Luetke]
* Fixed error_message_on to just use the first if more than one error had been added [marcel]
* Fixed that URL rewriting with /controller/ was working but /controller was not and that you couldn't use :id on index [geech]
* Fixed a bug with link_to where the :confirm option wouldn't be picked up if the link was a straight url instead of an option hash
* Changed scaffolding of forms to use <label> tags instead of <b> to please W3C [evl]
* Added DateHelper#distance_of_time_in_words_to_now(from_time) that works like distance_of_time_in_words,
but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>.
* Added assert_flash_equal(expected, key, message), assert_session_equal(expected, key, message),
assert_assigned_equal(expected, key, message) to test the contents of flash, session, and template assigns.
* Improved the failure report on assert_success when the action triggered a redirection [alexey].
* Added "markdown" to accompany "textilize" as a TextHelper method for converting text to HTML using the Markdown syntax.
BlueCloth must be installed in order for this method to become available.
* Made sure that an active session exists before we attempt to delete it [Samuel]
* Changed link_to with Javascript confirmation to use onclick instead of onClick for XHTML validity [Scott Barron]
*0.9.0 (43)*
* Added support for Builder-based templates for files with the .rxml extension. These new templates are an alternative to ERb that
are especially useful for generating XML content, such as this RSS example from Basecamp:
xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do
xml.channel do
xml.title(@feed_title)
xml.link(@url)
xml.description "Basecamp: Recent items"
xml.language "en-us"
xml.ttl "40"
for item in @recent_items
xml.item do
xml.title(item_title(item))
xml.description(item_description(item)) if item_description(item)
xml.pubDate(item_pubDate(item))
xml.guid(@person.firm.account.url + @recent_items.url(item))
xml.link(@person.firm.account.url + @recent_items.url(item))
xml.tag!("dc:creator", item.author_name) if item_has_creator?(item)
end
end
end
end
...which will generate something like:
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>Web Site Redesign</title>
<link>http://www.basecamphq.com/clients/travelcenter/1/</link>
<description>Basecamp: Recent items</description>
<language>en-us</language>
<ttl>40</ttl>
<item>
<title>Post: don't you know</title>
<description>&amp;lt;p&amp;gt;deeper and down&amp;lt;/p&amp;gt;</description>
<pubDate>Fri, 20 Aug 2004 21:13:50 CEST</pubDate>
<guid>http://www.basecamphq.com/clients/travelcenter/1/msg/assets/96976/comments</guid>
<link>http://www.basecamphq.com/clients/travelcenter/1/msg/assets/96976/comments</link>
<dc:creator>David H. Heinemeier</dc:creator>
</item>
<item>
<title>Milestone completed: Design Comp 2</title>
<pubDate>Mon, 9 Aug 2004 14:42:06 CEST</pubDate>
<guid>http://www.basecamphq.com/clients/travelcenter/1/milestones/#49</guid>
<link>http://www.basecamphq.com/clients/travelcenter/1/milestones/#49</link>
</item>
</channel>
</rss>
The "xml" local variable is automatically available in .rxml templates. You construct the template by calling a method with the name
of the tag you want. Options for the tag can be specified as a hash parameter to that method.
Builder-based templates can be mixed and matched with the regular ERb ones. The only thing that differentiates them is the extension.
No new methods have been added to the public interface to handle them.
Action Pack ships with a version of Builder, but it will use the RubyGems version if you have one installed.
Read more about Builder on: http://onestepback.org/index.cgi/Tech/Ruby/StayingSimple.rdoc
[Builder is created by Jim Weirich]
* Added much improved support for functional testing [what-a-day].
# Old style
def test_failing_authenticate
@request.request_uri = "/login/authenticate"
@request.action = "authenticate"
@request.request_parameters["user_name"] = "nop"
@request.request_parameters["password"] = ""
response = LoginController.process_test(@request)
assert_equal "The username and/or password you entered is invalid.", response.session["flash"]["alert"]
assert_equal "http://37signals.basecamp.com/login/", response.headers["location"]
end
# New style
def test_failing_authenticate
process :authenticate, "user_name" => "nop", "password" => ""
assert_flash_has 'alert'
assert_redirected_to :action => "index"
end
See a full example on http://codepaste.org/view/paste/334
* Increased performance by up to 100% with a revised cookie class that fixes the performance problems with the
default one that ships with 1.8.1 and below. It replaces the inheritance on SimpleDelegator with DelegateClass(Array)
following the suggestion from Matz on:
http://groups.google.com/groups?th=e3a4e68ba042f842&seekm=c3sioe%241qvm%241%40news.cybercity.dk#link14
* Added caching for compiled ERb templates. On Basecamp, it gave between 8.5% and 71% increase in performance [Andreas Schwarz].
* Added implicit counter variable to render_collection_of_partials [Marcel]. From the docs:
<%= render_collection_of_partials "ad", @advertisements %>
This will render "advertiser/_ad.rhtml" and pass the local variable +ad+ to the template for display. An iteration counter
will automatically be made available to the template with a name of the form +partial_name_counter+. In the case of the
example above, the template would be fed +ad_counter+.
* Fixed problems with two sessions being maintained on reset_session that would particularly screw up ActiveRecordStore.
* Fixed reset_session to start an entirely new session instead of merely deleting the old. So you can now safely access @session
after calling reset_ression and expect it to work.
* Added @request.get?, @request.post?, @request.put?, @request.delete? as convenience query methods for @request.method [geech]
* Added @request.method that'll return a symbol representing the HTTP method, such as :get, :post, :put, :delete [geech]
* Changed @request.remote_ip and @request.host to work properly even when a proxy is in front of the application [geech]
* Added JavaScript confirm feature to link_to. Documentation:
The html_options have a special feature for creating javascript confirm alerts where if you pass
:confirm => 'Are you sure?', the link will be guarded with a JS popup asking that question.
If the user accepts, the link is processed, otherwise not.
* Added link_to_unless_current as a UrlHelper method [Sam Stephenson]. Documentation:
Creates a link tag of the given +name+ using an URL created by the set of +options+, unless the current
controller, action, and id are the same as the link's, in which case only the name is returned (or the
given block is yielded, if one exists). This is useful for creating link bars where you don't want to link
to the page currently being viewed.
* Fixed that UrlRewriter (the driver for url_for, link_to, etc) would blow up when the anchor was an integer [alexey]
* Added that layouts defined with no directory defaults to layouts. So layout "weblog/standard" will use
weblog/standard (as always), but layout "standard" will use layouts/standard.
* Fixed that partials (or any template starting with an underscore) was publically viewable [Marten]
* Added HTML escaping to text_area helper.
* Added :overwrite_params to url_for and friends to keep the parameters as they were passed to the current action and only overwrite a subset.
The regular :params will clear the slate so you need to manually add in existing parameters if you want to reuse them. [raphinou]
* Fixed scaffolding problem with composite named objects [Moo Jester]
* Added the possibility for shared partials. Example:
<%= render_partial "advertisement/ad", ad %>
This will render the partial "advertisement/_ad.rhtml" regardless of which controller this is being called from.
[Jacob Fugal]
* Fixed crash when encountering forms that have empty-named fields [James Prudente]
* Added check_box form helper method now accepts true/false as well as 1/0 [what-a-day]
* Fixed the lacking creation of all directories with install.rb [Dave Steinberg]
* Fixed that date_select returns valid XHTML selected options [Andreas Schwarz]
* Fixed referencing an action with the same name as a controller in url_for [what-a-day]
* Fixed the destructive nature of Base#attributes= on the argument [Kevin Watt]
* Changed ActionControllerError to decent from StandardError instead of Exception. It can now be caught by a generic rescue.
* Added SessionRestoreError that is raised when a session being restored holds objects where there is no class available.
* Added block as option for inline filters. So what used to be written as:
before_filter Proc { |controller| return false if controller.params["stop_action"] }
...can now be as:
before_filter { |controller| return false if controller.params["stop_action"] }
[Jeremy Kemper]
* Made the following methods public (was protected): url_for, controller_class_name, controller_name, action_name
This makes it easier to write filters without cheating around the encapsulation with send.
* ActionController::Base#reset_session now sticks even if you access @session afterwards [Kent Sibilev]
* Improved the exception logging so the log file gets almost as much as in-browser debugging.
* Changed base class setup from AbstractTemplate/ERbTemplate to ActionView::Base. This change should be harmless unless you were
accessing Action View directly in which case you now need to reference the Base class.\
* Added that render_collection_of_partials returns nil if the collection is empty. This makes showing a “no items” message easier.
For example: <%= render_collection_of_partials("message", @messages) || "No messages found." %> [Sam Stephenson]
* Added :month_before_year as an option to date_select to get the month select before the year. Especially useful for credit card forms.
* Added :add_month_numbers to select_month to get options like "3 - March".
* Removed Base.has_active_layout? as it couldn't answer the question without the instance. Use Base#active_layout instead.
* Removed redundant call to update on ActionController::Base#close_session [Andreas Schwarz]
* Fixed that DRb Store accidently started its own server (instead of just client) [Andreas]
* Fixed strip_links so it now works across multiple lines [Chad Fowler]
* Fixed the TemplateError exception to show the proper trace on to_s (useful for unit test debugging)
* Implemented class inheritable attributes without eval [Caio Chassot]
* Made TextHelper#concat accept binding as it would otherwise not work
* The FormOptionsHelper will now call to_s on the keys and values used to generate options
*0.8.5*
* Introduced passing of locally scoped variables between templates:
You can pass local variables to sub templates by using a hash of with the variable
names as keys and the objects as values:
<%= render "shared/header", { "headline" => "Welcome", "person" => person } %>
These can now be accessed in shared/header with:
Headline: <%= headline %>
First name: <%= person.first_name %>
* Introduced the concept of partials as a certain type of sub templates:
There's also a convenience method for rendering sub templates within the current
controller that depends on a single object (we call this kind of sub templates for
partials). It relies on the fact that partials should follow the naming convention
of being prefixed with an underscore -- as to separate them from regular templates
that could be rendered on their own. In the template for Advertiser#buy, we could have:
<% for ad in @advertisements %>
<%= render_partial "ad", ad %>
<% end %>
This would render "advertiser/_ad.rhtml" and pass the local variable +ad+
for the template to display.
== Rendering a collection of partials
The example of partial use describes a familar pattern where a template needs
to iterate over a array and render a sub template for each of the elements.
This pattern has been implemented as a single method that accepts an array and
renders a partial by the same name of as the elements contained within. So the
three-lined example in "Using partials" can be rewritten with a single line:
<%= render_collection_of_partials "ad", @advertisements %>
So this will render "advertiser/_ad.rhtml" and pass the local variable +ad+ for
the template to display.
* Improved send_file by allowing a wide range of options to be applied [Jeremy Kemper]:
Sends the file by streaming it 4096 bytes at a time. This way the
whole file doesn't need to be read into memory at once. This makes
it feasible to send even large files.
Be careful to sanitize the path parameter if it coming from a web
page. send_file(@params['path'] allows a malicious user to
download any file on your server.
Options:
* <tt>:filename</tt> - specifies the filename the browser will see.
Defaults to File.basename(path).
* <tt>:type</tt> - specifies an HTTP content type.
Defaults to 'application/octet-stream'.
* <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
Valid values are 'inline' and 'attachment' (default).
* <tt>:buffer_size</tt> - specifies size (in bytes) of the buffer used to stream
the file. Defaults to 4096.
The default Content-Type and Content-Disposition headers are
set to download arbitrary binary files in as many browsers as
possible. IE versions 4, 5, 5.5, and 6 are all known to have
a variety of quirks (especially when downloading over SSL).
Simple download:
send_file '/path/to.zip'
Show a JPEG in browser:
send_file '/path/to.jpeg', :type => 'image/jpeg', :disposition => 'inline'
Read about the other Content-* HTTP headers if you'd like to
provide the user with more information (such as Content-Description).
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
Also be aware that the document may be cached by proxies and browsers.
The Pragma and Cache-Control headers declare how the file may be cached
by intermediaries. They default to require clients to validate with
the server before releasing cached responses. See
http://www.mnot.net/cache_docs/ for an overview of web caching and
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
for the Cache-Control header spec.
* Added pluralize method to the TextHelper that makes it easy to get strings like "1 message", "3 messages"
* Added proper escaping for the rescues [Andreas Schwartz]
* Added proper escaping for the option and collection tags [Andreas Schwartz]
* Fixed NaN errors on benchmarking [Jim Weirich]
* Fixed query string parsing for URLs that use the escaped versions of & or ; as part of a key or value
* Fixed bug with custom Content-Type headers being in addition to rather than instead of the default header.
(This bug didn't matter with neither CGI or mod_ruby, but FCGI exploded on it) [With help from Ara T. Howard]
*0.8.0*
* Added select, collection_select, and country_select to make it easier for Active Records to set attributes through
drop-down lists of options. Example:
<%= select "person", "gender", %w( Male Female ) %>
...would give the following:
<select name="person[gender]" id="person_gender"><option>Male</option><option>Female</option></select>
* Added an option for getting multiple values on a single form name into an array instead of having the last one overwrite.
This is especially useful for groups of checkboxes, which can now be written as:
<input type="checkbox" name="rights[]" value="CREATE" />
<input type="checkbox" name="rights[]" value="UPDATE" />
<input type="checkbox" name="rights[]" value="DELETE" />
...and retrieved in the controller action with:
@params["rights"] # => [ "CREATE", "UPDATE", "DELETE" ]
The old behavior (where the last one wins, "DELETE" in the example) is still available. Just don't add "[]" to the
end of the name. [Scott Baron]
* Added send_file which uses the new render_text block acceptance to make it feasible to send large files.
The files is sent with a bunch of voodoo HTTP headers required to get arbitrary files to download as
expected in as many browsers as possible (eg, IE hacks). Example:
def play_movie
send_file "/movies/that_movie.avi"
end
[Jeremy Kemper]
* render_text now accepts a block for deferred rendering. Useful for streaming large files, displaying
a “please wait” message during a complex search, etc. Streaming example:
render_text do |response|
File.open(path, 'rb') do |file|
while buf = file.read(1024)
print buf
end
end
end
[Jeremy Kemper]
* Added a new Tag Helper that can generate generic tags programmatically insted of through HTML. Example:
tag("br", "clear" => "all") => <br clear="all" />
...that's usually not terribly interesting (unless you have a lot of options already in a hash), but it
gives way for more specific tags, like the new form tag:
form_tag({ :controller => "weblog", :action => "update" }, { :multipart => "true", "style" => "width: 200px"}) =>
<form action="/weblog/update" enctype="multipart/formdata" style="width: 200px">
There's even a "pretty" version for people who don't like to open tags in code and close them in HTML:
<%= start_form_tag :action => "update" %>
# all the input fields
<%= end_form_tag %>
(end_form_tag just returns "</form>")
* The selected parameter in options_for_select may now also an array of values to be selected when
using a multiple select. Example:
options_for_select([ "VISA", "Mastercard", "Discover" ], ["VISA", "Discover"]) =>
<option selected>VISA</option>\n<option>Mastercard</option>\n<option selected>Discover</option>
[Scott Baron]
* Changed the URL rewriter so controller_prefix and action_prefix can be used in isolation. You can now do:
url_for(:controller_prefix => "clients")
...or:
url_for(:action_prefix => "category/messages")
Neither would have worked in isolation before (:controller_prefix required a :controller and :action_prefix required an :action)
* Started process of a cleaner separation between Action Controller and ERb-based Action Views by introducing an
abstract base class for views. And Amita adapter could be fitted in more easily now.
* The date helper methods date_select and datetime_select now also use the field error wrapping
(div with class fieldWithErrors by default).
* The date helper methods date_select and datetime_select can now discard selects
* Added option on AbstractTemplate to specify a different field error wrapping. Example:
ActionView::AbstractTemplate.field_error_proc = Proc.new do |html, instance|
"<p>#{instance.method_name + instance.error_message}</p><div style='background-color: red'>#{html}</div>"
end
...would give the following on a Post#title (text field) error:
<p>Title can't be empty</p>
<div style='background-color: red'>
<input id="post_title" name="post[title]" size="30" type="text" value="Hello World" />
</div>
* The UrlHelper methods url_for and link_to will now by default only return paths, not complete URIs.
That should make it easier to fit a Rails application behind a proxy or load-balancer.
You can overwrite this by passing :only_path => false as part of the options. [Suggested by U235]
* Fixed bug with having your own layout for use with scaffolding [Kevin Radloff]
* Fixed bug where redirect_to_path didn't append the port on non-standard ports [dhawkins]
* Scaffolding plays nicely with single-table inheritance (LoadErrors are caught) [Jeremy Kemper]
* Scaffolding plays nice with plural models like Category/categories [Jeremy Kemper]
* Fixed missing suffix appending in scaffolding [Kevin Radloff]
*0.7.9*
* The "form" method now present boolean fields from PostgreSQL as drop-down menu. [Scott]
* Scaffolding now automatically attempts to require the class that's being scaffolded.
* Scaffolding will use the current active layout, instead of its own, if one has been specified. Example:
class WeblogController < ActionController::Base
layout "layouts/weblog"
scaffold :post
end
[Suggested by Scott]
* Changed url_for (and all the that drives, like redirect_to, link_to, link_for) so you can pass it a symbol instead of a hash.
This symbol is a method reference which is then called to calculate the url. Example:
class WeblogController < ActionController::Base
def update
# do some update
redirect_to :dashboard_url
end
protected
def dashboard_url
if @project.active?
url_for :controller => "project", :action => "dashboard"
else
url_for :controller => "account", :action => "dashboard"
end
end
end
* Added default_url_options to specialize behavior for all url_for (and friends) calls:
Overwrite to implement a number of default options that all url_for-based methods will use.
The default options should come in form of a hash, just like the one you would use for
url_for directly. Example:
def default_url_options(options)
{ :controller_prefix => @project.active? ? "projects/" : "accounts/" }
end
As you can infer from the example, this is mostly useful for situations where you want to
centralize dynamic dissions about the urls as they stem from the business domain. Please note
that any individual url_for call can always override the defaults set by this method.
* Changed url_for so that an "id" passed in the :params is not treated special. You need to use the dedicated :id to get
the special auto path-params treatment. Considering the url http://localhost:81/friends/list
url_for(:action => "show", :params => { "id" => 5 })
...used to give http://localhost:81/friends/show/5
......now gives http://localhost:81/friends/show?id=5
If you want the automated id behavior, do:
url_for(:action => "show", :id => 5 )
....which gives http://localhost:81/friends/show/5
* Fixed problem with anchor being inserted before path parameters with url_for (and friends)
*0.7.8*
* Fixed session bug where you couldn't store any objects that didn't exist in the standard library
(such as Active Record objects).
* Added reset_session method for Action Controller objects to clear out all objects in the session.
* Fixed that exceptions raised during filters are now also caught by the default rescues
* Added new around_filter for doing before and after filtering with a single object [Florian Weber]:
class WeblogController < ActionController::Base
around_filter BenchmarkingFilter.new
# Before this action is performed, BenchmarkingFilter#before(controller) is executed
def index
end
# After this action has been performed, BenchmarkingFilter#after(controller) is executed
end
class BenchmarkingFilter
def initialize
@runtime
end
def before
start_timer
end
def after
stop_timer
report_result
end
end
* Added the options for specifying a different name and id for the form helper methods than what is guessed [Florian Weber]:
text_field "post", "title"
...just gives: <input id="post_title" name="post[title]" size="30" type="text" value="" />
text_field "post", "title", "id" => "title_for_post", "name" => "first_post_title"
...can now give: <input id="title_for_post" name="first_post_title" size="30" type="text" value="" />
* Added DebugHelper with a single "debug" method for doing pretty dumps of objects in the view
(now used in the default rescues to better present the contents of session and template variables)
* Added note to log about the templates rendered within layouts (before just the layout was shown)
* Fixed redirects on https setups [Andreas]
* Fixed scaffolding problem on the edit action when using :suffix => true [Scott]
* Fixed scaffolding problem where implementing list.rhtml wouldn't work for the index action
* URLs generated now uses &amp; instead of just & so pages using it can validate with W3C [Spotted by Andreas]
*0.7.7*
* Fixed bug in CGI extension that prevented multipart forms from working
*0.7.6*
* Included ERB::Util so all templates can easily escape HTML content with <%=h @person.content %>
* All requests are now considered local by default, so everyone will be exposed to detailed debugging screens on errors.
When the application is ready to go public, set ActionController::Base.consider_all_requests_local to false,
and implement the protected method local_request? in the controller to determine when debugging screens should be shown.
* Fixed three bugs with the url_for/redirect_to/link_to handling. Considering the url http://localhost:81/friends/show/1
url_for(:action => "list")
...used to give http://localhost:81/friends/list/1
......now gives http://localhost:81/friends/list
url_for(:controller => "friends", :action => "destroy", :id => 5)
...used to give http://localhost:81/friends/destroy
......now gives http://localhost:81/friends/destroy/5
Considering the url http://localhost:81/teachers/show/t
url_for(:action => "list", :id => 5)
...used to give http://localhost:81/5eachers/list/t
......now gives http://localhost:81/teachers/list/5
[Reported by David Morton & Radsaq]
* Logs exception to logfile in addition to showing them for local requests
* Protects the eruby load behind a begin/rescue block. eRuby is not required to run ActionController.
* Fixed install.rb to also install clean_logger and the templates
* Added ActiveRecordStore as a session option. Read more in lib/action_controller/session/active_record_store.rb [Tim Bates]
* Change license to MIT License (and included license file in package)
* Application error page now returns status code 500 instead of 200
* Fixed using Procs as layout handlers [Florian Weber]
* Fixed bug with using redirects ports other than 80
* Added index method that calls list on scaffolding
*0.7.5*
* First public release

21
actionpack/MIT-LICENSE Normal file
View File

@ -0,0 +1,21 @@
Copyright (c) 2004 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

418
actionpack/README Executable file
View File

@ -0,0 +1,418 @@
= Action Pack -- On rails from request to response
Action Pack splits the response to a web request into a controller part
(performing the logic) and a view part (rendering a template). This two-step
approach is known as an action, which will normally create, read, update, or
delete (CRUD for short) some sort of model part (often backed by a database)
before choosing either to render a template or redirecting to another action.
Action Pack implements these actions as public methods on Action Controllers
and uses Action Views to implement the template rendering. Action Controllers
are then responsible for handling all the actions relating to a certain part
of an application. This grouping usually consists of actions for lists and for
CRUDs revolving around a single (or a few) model objects. So ContactController
would be responsible for listing contacts, creating, deleting, and updating
contacts. A WeblogController could be responsible for both posts and comments.
Action View templates are written using embedded Ruby in tags mingled in with
the HTML. To avoid cluttering the templates with code, a bunch of helper
classes provide common behavior for forms, dates, and strings. And it's easy
to add specific helpers to keep the separation as the application evolves.
Note: Some of the features, such as scaffolding and form building, are tied to
ActiveRecord[http://activerecord.rubyonrails.org] (an object-relational
mapping package), but that doesn't mean that Action Pack depends on Active
Record. Action Pack is an independent package that can be used with any sort
of backend (Instiki[http://www.instiki.org], which is based on an older version
of Action Pack, uses Madeleine for example). Read more about the role Action
Pack can play when used together with Active Record on
http://www.rubyonrails.org.
A short rundown of the major features:
* Actions grouped in controller as methods instead of separate command objects
and can therefore helper share methods.
BlogController < ActionController::Base
def display
@customer = find_customer
end
def update
@customer = find_customer
@customer.attributes = @params["customer"]
@customer.save ?
redirect_to(:action => "display") :
render("customer/edit")
end
private
def find_customer() Customer.find(@params["id"]) end
end
Learn more in link:classes/ActionController/Base.html
* Embedded Ruby for templates (no new "easy" template language)
<% for post in @posts %>
Title: <%= post.title %>
<% end %>
All post titles: <%= @post.collect{ |p| p.title }.join ", " %>
<% unless @person.is_client? %>
Not for clients to see...
<% end %>
Learn more in link:classes/ActionView.html
* Builder-based templates (great for XML content, like RSS)
xml.rss("version" => "2.0") do
xml.channel do
xml.title(@feed_title)
xml.link(@url)
xml.description "Basecamp: Recent items"
xml.language "en-us"
xml.ttl "40"
for item in @recent_items
xml.item do
xml.title(item_title(item))
xml.description(item_description(item))
xml.pubDate(item_pubDate(item))
xml.guid(@recent_items.url(item))
xml.link(@recent_items.url(item))
end
end
end
end
* Filters for pre and post processing of the response (as methods, procs, and classes)
class WeblogController < ActionController::Base
before_filter :authenticate, :cache, :audit
after_filter { |c| c.response.body = GZip::compress(c.response.body) }
after_filter LocalizeFilter
def list
# Before this action is run, the user will be authenticated, the cache
# will be examined to see if a valid copy of the results already
# exist, and the action will be logged for auditing.
# After this action has run, the output will first be localized then
# compressed to minimize bandwith usage
end
private
def authenticate
# Implement the filter will full access to both request and response
end
end
Learn more in link:classes/ActionController/Filters/ClassMethods.html
* Helpers for forms, dates, action links, and text
<%= text_field "post", "title", "size" => 30 %>
<%= html_date_select(Date.today) %>
<%= link_to "New post", :controller => "post", :action => "new" %>
<%= truncate(post.title, 25) %>
Learn more in link:classes/ActionView/Helpers.html
* Layout sharing for template reuse (think simple version of Struts
Tiles[http://jakarta.apache.org/struts/userGuide/dev_tiles.html])
class WeblogController < ActionController::Base
layout "weblog_layout"
def hello_world
end
end
Layout file (called weblog_layout):
<html><body><%= @content_for_layout %></body></html>
Template for hello_world action:
<h1>Hello world</h1>
Result of running hello_world action:
<html><body><h1>Hello world</h1></body></html>
Learn more in link:classes/ActionController/Layout.html
* Advanced redirection that makes pretty urls easy
RewriteRule ^/library/books/([A-Z]+)([0-9]+)/([-_a-zA-Z0-9]+)$ \
/books_controller.cgi?action=$3&type=$1&code=$2 [QSA] [L]
Accessing /library/books/ISBN/0743536703/show calls BooksController#show
From that URL, you can rewrite the redirect in a number of ways:
redirect_to(:action => "edit") =>
/library/books/ISBN/0743536703/edit
redirect_to(:path_params => { "type" => "XTC", "code" => "12354345" }) =>
/library/books/XTC/12354345/show
redirect_to(:controller_prefix => "admin", :controller => "accounts") =>
/admin/accounts/
Learn more in link:classes/ActionController/Base.html
* Easy testing of both controller and template result through TestRequest/Response
class LoginControllerTest < Test::Unit::TestCase
def setup
@controller = LoginController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
end
def test_failing_authenticate
process :authenticate, "user_name" => "nop", "password" => ""
assert_flash_has 'alert'
assert_redirected_to :action => "index"
end
end
Learn more in link:classes/ActionController/TestRequest.html
* Automated benchmarking and integrated logging
Processing WeblogController#index (for 127.0.0.1 at Fri May 28 00:41:55)
Parameters: {"action"=>"index", "controller"=>"weblog"}
Rendering weblog/index (200 OK)
Completed in 0.029281 (34 reqs/sec)
If Active Record is used as the model, you'll have the database debugging
as well:
Processing WeblogController#create (for 127.0.0.1 at Sat Jun 19 14:04:23)
Params: {"controller"=>"weblog", "action"=>"create",
"post"=>{"title"=>"this is good"} }
SQL (0.000627) INSERT INTO posts (title) VALUES('this is good')
Redirected to http://test/weblog/display/5
Completed in 0.221764 (4 reqs/sec) | DB: 0.059920 (27%)
You specify a logger through a class method, such as:
ActionController::Base.logger = Logger.new("Application Log")
ActionController::Base.logger = Log4r::Logger.new("Application Log")
* Powerful debugging mechanism for local requests
All exceptions raised on actions performed on the request of a local user
will be presented with a tailored debugging screen that includes exception
message, stack trace, request parameters, session contents, and the
half-finished response.
Learn more in link:classes/ActionController/Rescue.html
* Scaffolding for Action Record model objects
require 'account' # must be an Active Record class
class AccountController < ActionController::Base
scaffold :account
end
The AccountController now has the full CRUD range of actions and default
templates: list, show, destroy, new, create, edit, update
Learn more in link:classes/ActionController/Scaffolding/ClassMethods.html
* Form building for Active Record model objects
The post object has a title (varchar), content (text), and
written_on (date)
<%= form "post" %>
...will generate something like (the selects will have more options of
course):
<form action="create" method="POST">
<p>
<b>Title:</b><br/>
<input type="text" name="post[title]" value="<%= @post.title %>" />
</p>
<p>
<b>Content:</b><br/>
<textarea name="post[content]"><%= @post.title %></textarea>
</p>
<p>
<b>Written on:</b><br/>
<select name='post[written_on(3i)]'><option>18</option></select>
<select name='post[written_on(2i)]'><option value='7'>July</option></select>
<select name='post[written_on(1i)]'><option>2004</option></select>
</p>
<input type="submit" value="Create">
</form>
This form generates a @params["post"] array that can be used directly in a save action:
class WeblogController < ActionController::Base
def save
post = Post.create(@params["post"])
redirect_to :action => "display", :path_params => { "id" => post.id }
end
end
Learn more in link:classes/ActionView/Helpers/ActiveRecordHelper.html
* Automated mapping of URLs to controller/action pairs through Apache's
mod_rewrite
Requesting /blog/display/5 will call BlogController#display and
make 5 available as an instance variable through @params["id"]
* Runs on top of CGI, FCGI, and mod_ruby
See the address_book_controller example for all three forms
== Simple example
This example will implement a simple weblog system using inline templates and
an Active Record model. The first thing we need to do is setup an .htaccess to
interpret pretty URLs into something the controller can use. Let's use the
simplest form for starters:
RewriteRule ^weblog/([-_a-zA-Z0-9]+)/([0-9]+)$ \
/weblog_controller.cgi?action=$2&id=$3 [QSA]
RewriteRule ^weblog/([-_a-zA-Z0-9]+)$ \
/weblog_controller.cgi?action=$2 [QSA]
RewriteRule ^weblog/$ \
/weblog_controller.cgi?action=index [QSA]
Now we'll be able to access URLs like weblog/display/5 and have
WeblogController#display called with { "id" => 5 } in the @params array
available for the action. So let's build that WeblogController with just a few
methods:
require 'action_controller'
require 'post'
class WeblogController < ActionController::Base
layout "weblog/layout"
def index
@posts = Post.find_all
end
def display
@post = Post.find(@params["id"])
end
def new
@post = Post.new
end
def create
@post = Post.create(@params["post"])
@post.save
redirect_to :action => "display", :id => @post.id
end
end
WeblogController::Base.template_root = File.dirname(__FILE__)
WeblogController.process_cgi if $0 == __FILE__
The last two lines are responsible for telling ActionController where the
template files are located and actually running the controller on a new
request from the web-server (like to be Apache).
And the templates look like this:
weblog/layout.rhtml:
<html><body>
<%= @content_for_layout %>
</body></html>
weblog/index.rhtml:
<% for post in @posts %>
<p><%= link_to(post.title, :action => "display", :id => post.id %></p>
<% end %>
weblog/display.rhtml:
<p>
<b><%= post.title %></b><br/>
<b><%= post.content %></b>
</p>
weblog/new.rhtml:
<%= form "post" %>
This simple setup will list all the posts in the system on the index page,
which is called by accessing /weblog/. It uses the form builder for the Active
Record model to make the new screen, which in turns hand everything over to
the create action (that's the default target for the form builder when given a
new model). After creating the post, it'll redirect to the display page using
an URL such as /weblog/display/5 (where 5 is the id of the post.
== Examples
Action Pack ships with three examples that all demonstrate an increasingly
detailed view of the possibilities. First is blog_controller that is just a
single file for the whole MVC (but still split into separate parts). Second is
the debate_controller that uses separate template files and multiple screens.
Third is the address_book_controller that uses the layout feature to separate
template casing from content.
Please note that you might need to change the "shebang" line to
#!/usr/local/env ruby, if your Ruby is not placed in /usr/local/bin/ruby
== Download
The latest version of Action Pack can be found at
* http://rubyforge.org/project/showfiles.php?group_id=249
Documentation can be found at
* http://actionpack.rubyonrails.org
== Installation
You can install Action Pack with the following command.
% [sudo] ruby install.rb
from its distribution directory.
== License
Action Pack is released under the same license as Ruby.
== Support
The Action Pack homepage is http://actionpack.rubyonrails.org. You can find
the Action Pack RubyForge page at http://rubyforge.org/projects/actionpack.
And as Jim from Rake says:
Feel free to submit commits or feature requests. If you send a patch,
remember to update the corresponding unit tests. If fact, I prefer
new feature to be submitted in the form of new unit tests.
For other information, feel free to ask on the ruby-talk mailing list (which
is mirrored to comp.lang.ruby) or contact mailto:david@loudthinking.com.

View File

@ -0,0 +1,25 @@
== Running with Rake
The easiest way to run the unit tests is through Rake. The default task runs
the entire test suite for all classes. For more information, checkout the
full array of rake tasks with "rake -T"
Rake can be found at http://rake.rubyforge.org
== Running by hand
If you only want to run a single test suite, or don't want to bother with Rake,
you can do so with something like:
ruby controller/base_test.rb
== Dependency on ActiveRecord and database setup
Test cases in test/controller/active_record_assertions.rb depend on having
activerecord installed and configured in a particular way. See comment in the
test file itself for details. If ActiveRecord is not in
actionpack/../activerecord directory, these tests are skipped. If activerecord
is installed, but not configured as expected, the tests will fail.
Other tests are runnable from a fresh copy of actionpack without any configuration.

105
actionpack/Rakefile Executable file
View File

@ -0,0 +1,105 @@
require 'rubygems'
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
require 'rake/packagetask'
require 'rake/gempackagetask'
require 'rake/contrib/rubyforgepublisher'
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
PKG_NAME = 'actionpack'
PKG_VERSION = '0.9.5' + PKG_BUILD
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
desc "Default Task"
task :default => [ :test ]
# Run the unit tests
Rake::TestTask.new { |t|
t.libs << "test"
t.pattern = 'test/*/*_test.rb'
t.verbose = true
}
# Genereate the RDoc documentation
Rake::RDocTask.new { |rdoc|
rdoc.rdoc_dir = 'doc'
rdoc.title = "Action Pack -- On rails from request to response"
rdoc.options << '--line-numbers --inline-source --main README'
rdoc.rdoc_files.include('README', 'RUNNING_UNIT_TESTS', 'CHANGELOG')
rdoc.rdoc_files.include('lib/**/*.rb')
}
# Create compressed packages
dist_dirs = [ "lib", "test", "examples" ]
spec = Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
s.name = PKG_NAME
s.version = PKG_VERSION
s.summary = "Web-flow and rendering framework putting the VC in MVC."
s.description = %q{Eases web-request routing, handling, and response as a half-way front, half-way page controller. Implemented with specific emphasis on enabling easy unit/integration testing that doesn't require a browser.}
s.author = "David Heinemeier Hansson"
s.email = "david@loudthinking.com"
s.rubyforge_project = "actionpack"
s.homepage = "http://actionpack.rubyonrails.org"
s.has_rdoc = true
s.requirements << 'none'
s.require_path = 'lib'
s.autorequire = 'action_controller'
s.files = [ "rakefile", "install.rb", "README", "RUNNING_UNIT_TESTS", "CHANGELOG", "MIT-LICENSE", "examples/.htaccess" ]
dist_dirs.each do |dir|
s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "CVS" ) }
end
s.files.delete "examples/benchmark.rb"
s.files.delete "examples/benchmark_with_ar.fcgi"
end
Rake::GemPackageTask.new(spec) do |p|
p.gem_spec = spec
p.need_tar = true
p.need_zip = true
end
# Publish beta gem
desc "Publish the API documentation"
task :pgem => [:package] do
Rake::SshFilePublisher.new("davidhh@one.textdrive.com", "domains/rubyonrails.org/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
`ssh davidhh@one.textdrive.com './gemupdate.sh'`
end
# Publish documentation
desc "Publish the API documentation"
task :pdoc => [:rdoc] do
Rake::SshDirPublisher.new("davidhh@one.textdrive.com", "domains/rubyonrails.org/ap", "doc").upload
end
desc "Count lines in the main rake file"
task :lines do
lines = 0
codelines = 0
Dir.foreach("lib/action_controller") { |file_name|
next unless file_name =~ /.*rb/
f = File.open("lib/action_controller/" + file_name)
while line = f.gets
lines += 1
next if line =~ /^\s*$/
next if line =~ /^\s*#/
codelines += 1
end
}
puts "Lines #{lines}, LOC #{codelines}"
end

View File

@ -0,0 +1,24 @@
<IfModule mod_ruby.c>
RubyRequire apache/ruby-run
RubySafeLevel 0
<Files *.rbx>
SetHandler ruby-object
RubyHandler Apache::RubyRun.instance
</Files>
</IfModule>
RewriteEngine On
RewriteRule ^fcgi/([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/([0-9]+)$ /$1_controller.fcgi?action=$2&id=$3 [QSA]
RewriteRule ^fcgi/([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)$ /$1_controller.fcgi?action=$2 [QSA]
RewriteRule ^fcgi/([-_a-zA-Z0-9]+)/$ /$1_controller.fcgi?action=index [QSA]
RewriteRule ^modruby/([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/([0-9]+)$ /$1_controller.rbx?action=$2&id=$3 [QSA]
RewriteRule ^modruby/([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)$ /$1_controller.rbx?action=$2 [QSA]
RewriteRule ^modruby/([-_a-zA-Z0-9]+)/$ /$1_controller.rbx?action=index [QSA]
RewriteRule ^([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/([0-9]+)$ /$1_controller.cgi?action=$2&id=$3 [QSA]
RewriteRule ^([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)$ /$1_controller.cgi?action=$2 [QSA]
RewriteRule ^([-_a-zA-Z0-9]+)/$ /$1_controller.cgi?action=index [QSA]

View File

@ -0,0 +1,33 @@
<h1>Address Book</h1>
<% if @people.empty? %>
<p>No people in the address book yet</p>
<% else %>
<table>
<tr><th>Name</th><th>Email Address</th><th>Phone Number</th></tr>
<% for person in @people %>
<tr><td><%= person.name %></td><td><%= person.email_address %></td><td><%= person.phone_number %></td></tr>
<% end %>
</table>
<% end %>
<form action="create_person">
<p>
Name:<br />
<input type="text" name="person[name]">
</p>
<p>
Email address:<br />
<input type="text" name="person[email_address]">
</p>
<p>
Phone number:<br />
<input type="text" name="person[phone_number]">
</p>
<p>
<input type="submit" value="Create Person">
</p>
</form>

View File

@ -0,0 +1,8 @@
<html>
<head>
<title><%= @title || "Untitled" %></title>
</head>
<body>
<%= @content_for_layout %>
</body>
</html>

View File

@ -0,0 +1,9 @@
#!/usr/local/bin/ruby
require "address_book_controller"
begin
AddressBookController.process_cgi(CGI.new)
rescue => e
CGI.new.out { "#{e.class}: #{e.message}" }
end

View File

@ -0,0 +1,6 @@
#!/usr/local/bin/ruby
require "address_book_controller"
require "fcgi"
FCGI.each_cgi { |cgi| AddressBookController.process_cgi(cgi) }

View File

@ -0,0 +1,52 @@
$:.unshift(File.dirname(__FILE__) + "/../lib")
require "action_controller"
require "action_controller/test_process"
Person = Struct.new("Person", :id, :name, :email_address, :phone_number)
class AddressBookService
attr_reader :people
def initialize() @people = [] end
def create_person(data) people.unshift(Person.new(next_person_id, data["name"], data["email_address"], data["phone_number"])) end
def find_person(topic_id) people.select { |person| person.id == person.to_i }.first end
def next_person_id() people.first.id + 1 end
end
class AddressBookController < ActionController::Base
layout "address_book/layout"
before_filter :initialize_session_storage
# Could also have used a proc
# before_filter proc { |c| c.instance_variable_set("@address_book", c.session["address_book"] ||= AddressBookService.new) }
def index
@title = "Address Book"
@people = @address_book.people
end
def person
@person = @address_book.find_person(@params["id"])
end
def create_person
@address_book.create_person(@params["person"])
redirect_to :action => "index"
end
private
def initialize_session_storage
@address_book = @session["address_book"] ||= AddressBookService.new
end
end
ActionController::Base.template_root = File.dirname(__FILE__)
# ActionController::Base.logger = Logger.new("debug.log") # Remove first comment to turn on logging in current dir
begin
AddressBookController.process_cgi(CGI.new) if $0 == __FILE__
rescue => e
CGI.new.out { "#{e.class}: #{e.message}" }
end

View File

@ -0,0 +1,4 @@
#!/usr/local/bin/ruby
require "address_book_controller"
AddressBookController.process_cgi(CGI.new)

View File

@ -0,0 +1,52 @@
$:.unshift(File.dirname(__FILE__) + "/../lib")
require "action_controller"
require 'action_controller/test_process'
Person = Struct.new("Person", :name, :address, :age)
class BenchmarkController < ActionController::Base
def message
render_text "hello world"
end
def list
@people = [ Person.new("David"), Person.new("Mary") ]
render_template "hello: <% for person in @people %>Name: <%= person.name %><% end %>"
end
def form_helper
@person = Person.new "david", "hyacintvej", 24
render_template(
"<% person = Person.new 'Mary', 'hyacintvej', 22 %> " +
"change the name <%= text_field 'person', 'name' %> and <%= text_field 'person', 'address' %> and <%= text_field 'person', 'age' %>"
)
end
end
#ActionController::Base.template_root = File.dirname(__FILE__)
require "benchmark"
RUNS = ARGV[0] ? ARGV[0].to_i : 50
require "profile" if ARGV[1]
runtime = Benchmark.measure {
RUNS.times { BenchmarkController.process_test(ActionController::TestRequest.new({ "action" => "list" })) }
}
puts "List: #{RUNS / runtime.real}"
runtime = Benchmark.measure {
RUNS.times { BenchmarkController.process_test(ActionController::TestRequest.new({ "action" => "message" })) }
}
puts "Message: #{RUNS / runtime.real}"
runtime = Benchmark.measure {
RUNS.times { BenchmarkController.process_test(ActionController::TestRequest.new({ "action" => "form_helper" })) }
}
puts "Form helper: #{RUNS / runtime.real}"

View File

@ -0,0 +1,89 @@
#!/usr/local/bin/ruby
begin
$:.unshift(File.dirname(__FILE__) + "/../lib")
$:.unshift(File.dirname(__FILE__) + "/../../../edge/activerecord/lib")
require 'fcgi'
require 'action_controller'
require 'action_controller/test_process'
require 'active_record'
class Post < ActiveRecord::Base; end
ActiveRecord::Base.establish_connection(:adapter => "mysql", :database => "basecamp")
SESSION_OPTIONS = { "database_manager" => CGI::Session::MemoryStore }
class TestController < ActionController::Base
def index
render_template <<-EOT
<% for post in Post.find_all(nil,nil,100) %>
<%= post.title %>
<% end %>
EOT
end
def show_one
render_template <<-EOT
<%= Post.find_first.title %>
EOT
end
def text
render_text "hello world"
end
def erb_text
render_template "hello <%= 'world' %>"
end
def erb_loop
render_template <<-EOT
<% for post in 1..100 %>
<%= post %>
<% end %>
EOT
end
def rescue_action(e) puts e.message + e.backtrace.join("\n") end
end
if ARGV.empty? && ENV["REQUEST_URI"]
FCGI.each_cgi do |cgi|
TestController.process(ActionController::CgiRequest.new(cgi, SESSION_OPTIONS), ActionController::CgiResponse.new(cgi)).out
end
else
if ARGV.empty?
cgi = CGI.new
end
require 'benchmark'
require 'profile' if ARGV[2] == "profile"
RUNS = ARGV[1] ? ARGV[1].to_i : 50
runtime = Benchmark::measure {
RUNS.times {
if ARGV.empty?
TestController.process(ActionController::CgiRequest.new(cgi, SESSION_OPTIONS), ActionController::CgiResponse.new(cgi))
else
response = TestController.process_test(
ActionController::TestRequest.new({"action" => ARGV[0]})
)
puts(response.body) if ARGV[2] == "show"
end
}
}
puts "Runs: #{RUNS}"
puts "Avg. runtime: #{runtime.real / RUNS}"
puts "Requests/second: #{RUNS / runtime.real}"
end
rescue Exception => e
# CGI.new.out { "<pre>" + e.message + e.backtrace.join("\n") + "</pre>" }
$stderr << e.message + e.backtrace.join("\n")
end

View File

@ -0,0 +1,53 @@
#!/usr/local/bin/ruby
$:.unshift(File.dirname(__FILE__) + "/../lib")
require "action_controller"
Post = Struct.new("Post", :title, :body)
class BlogController < ActionController::Base
before_filter :initialize_session_storage
def index
@posts = @session["posts"]
render_template <<-"EOF"
<html><body>
<%= @flash["alert"] %>
<h1>Posts</h1>
<% @posts.each do |post| %>
<p><b><%= post.title %></b><br /><%= post.body %></p>
<% end %>
<h1>Create post</h1>
<form action="create">
Title: <input type="text" name="post[title]"><br>
Body: <textarea name="post[body]"></textarea><br>
<input type="submit" value="save">
</form>
</body></html>
EOF
end
def create
@session["posts"].unshift(Post.new(@params["post"]["title"], @params["post"]["body"]))
flash["alert"] = "New post added!"
redirect_to :action => "index"
end
private
def initialize_session_storage
@session["posts"] = [] if @session["posts"].nil?
end
end
ActionController::Base.template_root = File.dirname(__FILE__)
# ActionController::Base.logger = Logger.new("debug.log") # Remove first comment to turn on logging in current dir
begin
BlogController.process_cgi(CGI.new) if $0 == __FILE__
rescue => e
CGI.new.out { "#{e.class}: #{e.message}" }
end

View File

@ -0,0 +1,14 @@
<html>
<body>
<h1>Topics</h1>
<%= link_to "New topic", :action => "new_topic" %>
<ul>
<% for topic in @topics %>
<li><%= link_to "#{topic.title} (#{topic.replies.length} replies)", :action => "topic", :path_params => { "id" => topic.id } %></li>
<% end %>
</ul>
</body>
</html>

View File

@ -0,0 +1,22 @@
<html>
<body>
<h1>New topic</h1>
<form action="<%= url_for(:action => "create_topic") %>" method="post">
<p>
Title:<br>
<input type="text" name="topic[title]">
</p>
<p>
Body:<br>
<textarea name="topic[body]" style="width: 200px; height: 200px"></textarea>
</p>
<p>
<input type="submit" value="Create topic">
</p>
</form>
</body>
</html>

View File

@ -0,0 +1,32 @@
<html>
<body>
<h1><%= @topic.title %></h1>
<p><%= @topic.body %></p>
<%= link_to "Back to topics", :action => "index" %>
<% unless @topic.replies.empty? %>
<h2>Replies</h2>
<ol>
<% for reply in @topic.replies %>
<li><%= reply.body %></li>
<% end %>
</ol>
<% end %>
<h2>Reply to this topic</h2>
<form action="<%= url_for(:action => "create_reply") %>" method="post">
<input type="hidden" name="reply[topic_id]" value="<%= @topic.id %>">
<p>
<textarea name="reply[body]" style="width: 200px; height: 200px"></textarea>
</p>
<p>
<input type="submit" value="Create reply">
</p>
</form>
</body>
</html>

View File

@ -0,0 +1,57 @@
#!/usr/local/bin/ruby
$:.unshift(File.dirname(__FILE__) + "/../lib")
require "action_controller"
Topic = Struct.new("Topic", :id, :title, :body, :replies)
Reply = Struct.new("Reply", :body)
class DebateService
attr_reader :topics
def initialize() @topics = [] end
def create_topic(data) topics.unshift(Topic.new(next_topic_id, data["title"], data["body"], [])) end
def create_reply(data) find_topic(data["topic_id"]).replies << Reply.new(data["body"]) end
def find_topic(topic_id) topics.select { |topic| topic.id == topic_id.to_i }.first end
def next_topic_id() topics.first.id + 1 end
end
class DebateController < ActionController::Base
before_filter :initialize_session_storage
def index
@topics = @debate.topics
end
def topic
@topic = @debate.find_topic(@params["id"])
end
# def new_topic() end <-- This is not needed as the template doesn't require any assigns
def create_topic
@debate.create_topic(@params["topic"])
redirect_to :action => "index"
end
def create_reply
@debate.create_reply(@params["reply"])
redirect_to :action => "topic", :path_params => { "id" => @params["reply"]["topic_id"] }
end
private
def initialize_session_storage
@session["debate"] = DebateService.new if @session["debate"].nil?
@debate = @session["debate"]
end
end
ActionController::Base.template_root = File.dirname(__FILE__)
# ActionController::Base.logger = Logger.new("debug.log") # Remove first comment to turn on logging in current dir
begin
DebateController.process_cgi(CGI.new) if $0 == __FILE__
rescue => e
CGI.new.out { "#{e.class}: #{e.message}" }
end

97
actionpack/install.rb Normal file
View File

@ -0,0 +1,97 @@
require 'rbconfig'
require 'find'
require 'ftools'
include Config
# this was adapted from rdoc's install.rb by ways of Log4r
$sitedir = CONFIG["sitelibdir"]
unless $sitedir
version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"]
$libdir = File.join(CONFIG["libdir"], "ruby", version)
$sitedir = $:.find {|x| x =~ /site_ruby/ }
if !$sitedir
$sitedir = File.join($libdir, "site_ruby")
elsif $sitedir !~ Regexp.quote(version)
$sitedir = File.join($sitedir, version)
end
end
makedirs = %w{ action_controller/assertions action_controller/cgi_ext
action_controller/session action_controller/support
action_controller/templates action_controller/templates/rescues
action_controller/templates/scaffolds
action_view/helpers action_view/vendor action_view/vendor/builder
}
makedirs.each {|f| File::makedirs(File.join($sitedir, *f.split(/\//)))}
# deprecated files that should be removed
# deprecated = %w{ }
# files to install in library path
files = %w-
action_controller.rb
action_controller/assertions/action_pack_assertions.rb
action_controller/assertions/active_record_assertions.rb
action_controller/base.rb
action_controller/benchmarking.rb
action_controller/cgi_ext/cgi_ext.rb
action_controller/cgi_ext/cgi_methods.rb
action_controller/cgi_process.rb
action_controller/filters.rb
action_controller/flash.rb
action_controller/helpers.rb
action_controller/layout.rb
action_controller/request.rb
action_controller/rescue.rb
action_controller/response.rb
action_controller/scaffolding.rb
action_controller/session/active_record_store.rb
action_controller/session/drb_server.rb
action_controller/session/drb_store.rb
action_controller/support/class_inheritable_attributes.rb
action_controller/support/class_attribute_accessors.rb
action_controller/support/clean_logger.rb
action_controller/support/cookie_performance_fix.rb
action_controller/support/inflector.rb
action_controller/templates/rescues/_request_and_response.rhtml
action_controller/templates/rescues/diagnostics.rhtml
action_controller/templates/rescues/layout.rhtml
action_controller/templates/rescues/missing_template.rhtml
action_controller/templates/rescues/template_error.rhtml
action_controller/templates/rescues/unknown_action.rhtml
action_controller/templates/scaffolds/edit.rhtml
action_controller/templates/scaffolds/layout.rhtml
action_controller/templates/scaffolds/list.rhtml
action_controller/templates/scaffolds/new.rhtml
action_controller/templates/scaffolds/show.rhtml
action_controller/test_process.rb
action_controller/url_rewriter.rb
action_view.rb
action_view/base.rb
action_view/helpers/active_record_helper.rb
action_view/helpers/date_helper.rb
action_view/helpers/debug_helper.rb
action_view/helpers/form_helper.rb
action_view/helpers/form_options_helper.rb
action_view/helpers/text_helper.rb
action_view/helpers/tag_helper.rb
action_view/helpers/url_helper.rb
action_view/partials.rb
action_view/template_error.rb
action_view/vendor/builder.rb
action_view/vendor/builder/blankslate.rb
action_view/vendor/builder/xmlbase.rb
action_view/vendor/builder/xmlevents.rb
action_view/vendor/builder/xmlmarkup.rb
-
# the acual gruntwork
Dir.chdir("lib")
# File::safe_unlink *deprecated.collect{|f| File.join($sitedir, f.split(/\//))}
files.each {|f|
File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true)
}

View File

@ -0,0 +1,51 @@
#--
# Copyright (c) 2004 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
$:.unshift(File.dirname(__FILE__))
require 'action_controller/support/clean_logger'
require 'action_controller/base'
require 'action_controller/rescue'
require 'action_controller/benchmarking'
require 'action_controller/filters'
require 'action_controller/layout'
require 'action_controller/flash'
require 'action_controller/scaffolding'
require 'action_controller/helpers'
require 'action_controller/dependencies'
require 'action_controller/cgi_process'
ActionController::Base.class_eval do
include ActionController::Filters
include ActionController::Layout
include ActionController::Flash
include ActionController::Benchmarking
include ActionController::Rescue
include ActionController::Scaffolding
include ActionController::Helpers
include ActionController::Dependencies
end
require 'action_view'
ActionController::Base.template_class = ActionView::Base

View File

@ -0,0 +1,199 @@
require 'test/unit'
require 'test/unit/assertions'
require 'rexml/document'
module Test #:nodoc:
module Unit #:nodoc:
# Adds a wealth of assertions to do functional testing of Action Controllers.
module Assertions
# -- basic assertions ---------------------------------------------------
# ensure that the web request has been serviced correctly
def assert_success(message=nil)
response = acquire_assertion_target
if response.success?
# to count the assertion
assert_block("") { true }
else
if response.redirect?
msg = build_message(message, "Response unexpectedly redirect to <?>", response.redirect_url)
else
msg = build_message(message, "unsuccessful request (response code = <?>)",
response.response_code)
end
assert_block(msg) { false }
end
end
# ensure the request was rendered with the appropriate template file
def assert_rendered_file(expected=nil, message=nil)
response = acquire_assertion_target
rendered = expected ? response.rendered_file(!expected.include?('/')) : response.rendered_file
msg = build_message(message, "expecting <?> but rendering with <?>", expected, rendered)
assert_block(msg) do
if expected.nil?
response.rendered_with_file?
else
expected == rendered
end
end
end
# -- session assertions -------------------------------------------------
# ensure that the session has an object with the specified name
def assert_session_has(key=nil, message=nil)
response = acquire_assertion_target
msg = build_message(message, "<?> is not in the session <?>", key, response.session)
assert_block(msg) { response.has_session_object?(key) }
end
# ensure that the session has no object with the specified name
def assert_session_has_no(key=nil, message=nil)
response = acquire_assertion_target
msg = build_message(message, "<?> is in the session <?>", key, response.session)
assert_block(msg) { !response.has_session_object?(key) }
end
def assert_session_equal(expected = nil, key = nil, message = nil)
response = acquire_assertion_target
msg = build_message(message, "<?> expected in session['?'] but was <?>", expected, key, response.session[key])
assert_block(msg) { expected == response.session[key] }
end
# -- flash assertions ---------------------------------------------------
# ensure that the flash has an object with the specified name
def assert_flash_has(key=nil, message=nil)
response = acquire_assertion_target
msg = build_message(message, "<?> is not in the flash <?>", key, response.flash)
assert_block(msg) { response.has_flash_object?(key) }
end
# ensure that the flash has no object with the specified name
def assert_flash_has_no(key=nil, message=nil)
response = acquire_assertion_target
msg = build_message(message, "<?> is in the flash <?>", key, response.flash)
assert_block(msg) { !response.has_flash_object?(key) }
end
# ensure the flash exists
def assert_flash_exists(message=nil)
response = acquire_assertion_target
msg = build_message(message, "the flash does not exist <?>", response.session['flash'] )
assert_block(msg) { response.has_flash? }
end
# ensure the flash does not exist
def assert_flash_not_exists(message=nil)
response = acquire_assertion_target
msg = build_message(message, "the flash exists <?>", response.flash)
assert_block(msg) { !response.has_flash? }
end
# ensure the flash is empty but existant
def assert_flash_empty(message=nil)
response = acquire_assertion_target
msg = build_message(message, "the flash is not empty <?>", response.flash)
assert_block(msg) { !response.has_flash_with_contents? }
end
# ensure the flash is not empty
def assert_flash_not_empty(message=nil)
response = acquire_assertion_target
msg = build_message(message, "the flash is empty")
assert_block(msg) { response.has_flash_with_contents? }
end
def assert_flash_equal(expected = nil, key = nil, message = nil)
response = acquire_assertion_target
msg = build_message(message, "<?> expected in flash['?'] but was <?>", expected, key, response.flash[key])
assert_block(msg) { expected == response.flash[key] }
end
# -- redirection assertions ---------------------------------------------
# ensure we have be redirected
def assert_redirect(message=nil)
response = acquire_assertion_target
msg = build_message(message, "response is not a redirection (response code is <?>)", response.response_code)
assert_block(msg) { response.redirect? }
end
def assert_redirected_to(options = {}, message=nil)
assert_redirect(message)
response = acquire_assertion_target
msg = build_message(message, "response is not a redirection to all of the options supplied (redirection is <?>)", response.redirected_to)
assert_block(msg) do
if options.is_a?(Symbol)
response.redirected_to == options
else
options.keys.all? { |k| options[k] == response.redirected_to[k] }
end
end
end
# ensure our redirection url is an exact match
def assert_redirect_url(url=nil, message=nil)
assert_redirect(message)
response = acquire_assertion_target
msg = build_message(message, "<?> is not the redirected location <?>", url, response.redirect_url)
assert_block(msg) { response.redirect_url == url }
end
# ensure our redirection url matches a pattern
def assert_redirect_url_match(pattern=nil, message=nil)
assert_redirect(message)
response = acquire_assertion_target
msg = build_message(message, "<?> was not found in the location: <?>", pattern, response.redirect_url)
assert_block(msg) { response.redirect_url_match?(pattern) }
end
# -- template assertions ------------------------------------------------
# ensure that a template object with the given name exists
def assert_template_has(key=nil, message=nil)
response = acquire_assertion_target
msg = build_message(message, "<?> is not a template object", key )
assert_block(msg) { response.has_template_object?(key) }
end
# ensure that a template object with the given name does not exist
def assert_template_has_no(key=nil,message=nil)
response = acquire_assertion_target
msg = build_message(message, "<?> is a template object <?>", key, response.template_objects[key])
assert_block(msg) { !response.has_template_object?(key) }
end
# ensures that the object assigned to the template on +key+ is equal to +expected+ object.
def assert_assigned_equal(expected = nil, key = nil, message = nil)
response = acquire_assertion_target
msg = build_message(message, "<?> expected in assigns['?'] but was <?>", expected, key, response.template.assigns[key.to_s])
assert_block(msg) { expected == response.template.assigns[key.to_s] }
end
# Asserts that the template returns the +expected+ string or array based on the XPath +expression+.
# This will only work if the template rendered a valid XML document.
def assert_template_xpath_match(expression=nil, expected=nil, message=nil)
response = acquire_assertion_target
xml, matches = REXML::Document.new(response.body), []
xml.elements.each(expression) { |e| matches << e.text }
matches = matches.first if matches.length < 2
msg = build_message(message, "<?> found <?>, not <?>", expression, matches, expected)
assert_block(msg) { matches == expected }
end
# -- helper functions ---------------------------------------------------
# get the TestResponse object that these assertions depend upon
def acquire_assertion_target
target = ActionController::TestResponse.assertion_target
assert_block( "Unable to acquire the TestResponse.assertion_target. Please set this before calling this assertion." ) { !target.nil? }
target
end
end # Assertions
end # Unit
end # Test

View File

@ -0,0 +1,65 @@
require 'test/unit'
require 'test/unit/assertions'
# active_record is assumed to be loaded by this point
module Test #:nodoc:
module Unit #:nodoc:
module Assertions
# Assert the template object with the given name is an Active Record descendant and is valid.
def assert_valid_record(key = nil, message = nil)
record = find_record_in_template(key)
msg = build_message(message, "Active Record is invalid <?>)", record.errors.full_messages)
assert_block(msg) { record.valid? }
end
# Assert the template object with the given name is an Active Record descendant and is invalid.
def assert_invalid_record(key = nil, message = nil)
record = find_record_in_template(key)
msg = build_message(message, "Active Record is valid)")
assert_block(msg) { !record.valid? }
end
# Assert the template object with the given name is an Active Record descendant and the specified column(s) are valid.
def assert_valid_column_on_record(key = nil, columns = "", message = nil)
record = find_record_in_template(key)
record.validate
cols = glue_columns(columns)
cols.delete_if { |col| !record.errors.invalid?(col) }
msg = build_message(message, "Active Record has invalid columns <?>)", cols.join(",") )
assert_block(msg) { cols.empty? }
end
# Assert the template object with the given name is an Active Record descendant and the specified column(s) are invalid.
def assert_invalid_column_on_record(key = nil, columns = "", message = nil)
record = find_record_in_template(key)
record.validate
cols = glue_columns(columns)
cols.delete_if { |col| record.errors.invalid?(col) }
msg = build_message(message, "Active Record has valid columns <?>)", cols.join(",") )
assert_block(msg) { cols.empty? }
end
private
def glue_columns(columns)
cols = []
cols << columns if columns.class == String
cols += columns if columns.class == Array
cols
end
def find_record_in_template(key = nil)
response = acquire_assertion_target
assert_template_has(key)
record = response.template_objects[key]
assert_not_nil(record)
assert_kind_of ActiveRecord::Base, record
return record
end
end
end
end

View File

@ -0,0 +1,689 @@
require 'action_controller/request'
require 'action_controller/response'
require 'action_controller/url_rewriter'
require 'action_controller/support/class_attribute_accessors'
require 'action_controller/support/class_inheritable_attributes'
require 'action_controller/support/inflector'
module ActionController #:nodoc:
class ActionControllerError < StandardError #:nodoc:
end
class SessionRestoreError < ActionControllerError #:nodoc:
end
class MissingTemplate < ActionControllerError #:nodoc:
end
class UnknownAction < ActionControllerError #:nodoc:
end
# Action Controllers are made up of one or more actions that performs its purpose and then either renders a template or
# redirects to another action. An action is defined as a public method on the controller, which will automatically be
# made accessible to the web-server through a mod_rewrite mapping. A sample controller could look like this:
#
# class GuestBookController < ActionController::Base
# def index
# @entries = Entry.find_all
# end
#
# def sign
# Entry.create(@params["entry"])
# redirect_to :action => "index"
# end
# end
#
# GuestBookController.template_root = "templates/"
# GuestBookController.process_cgi
#
# All actions assume that you want to render a template matching the name of the action at the end of the performance
# unless you tell it otherwise. The index action complies with this assumption, so after populating the @entries instance
# variable, the GuestBookController will render "templates/guestbook/index.rhtml".
#
# Unlike index, the sign action isn't interested in rendering a template. So after performing its main purpose (creating a
# new entry in the guest book), it sheds the rendering assumption and initiates a redirect instead. This redirect works by
# returning an external "302 Moved" HTTP response that takes the user to the index action.
#
# The index and sign represent the two basic action archetypes used in Action Controllers. Get-and-show and do-and-redirect.
# Most actions are variations of these themes.
#
# Also note that it's the final call to <tt>process_cgi</tt> that actually initiates the action performance. It will extract
# request and response objects from the CGI
#
# == Requests
#
# Requests are processed by the Action Controller framework by extracting the value of the "action" key in the request parameters.
# This value should hold the name of the action to be performed. Once the action has been identified, the remaining
# request parameters, the session (if one is available), and the full request with all the http headers are made available to
# the action through instance variables. Then the action is performed.
#
# The full request object is available in @request and is primarily used to query for http headers. These queries are made by
# accessing the environment hash, like this:
#
# def hello_ip
# location = @request.env["REMOTE_ADDRESS"]
# render_text "Hello stranger from #{location}"
# end
#
# == Parameters
#
# All request parameters whether they come from a GET or POST request, or from the URL, are available through the @params hash.
# So an action that was performed through /weblog/list?category=All&limit=5 will include { "category" => "All", "limit" => 5 }
# in @params.
#
# It's also possible to construct multi-dimensional parameter hashes by specifying keys using brackets, such as:
#
# <input type="text" name="post[name]" value="david">
# <input type="text" name="post[address]" value="hyacintvej">
#
# A request stemming from a form holding these inputs will include { "post" # => { "name" => "david", "address" => "hyacintvej" } }.
# If the address input had been named "post[address][street]", the @params would have included
# { "post" => { "address" => { "street" => "hyacintvej" } } }. There's no limit to the depth of the nesting.
#
# == Sessions
#
# Sessions allows you to store objects in memory between requests. This is useful for objects that are not yet ready to be persisted,
# such as a Signup object constructed in a multi-paged process, or objects that don't change much and are needed all the time, such
# as a User object for a system that requires login. The session should not be used, however, as a cache for objects where it's likely
# they could be changed unknowingly. It's usually too much work to keep it all synchronized -- something databases already excel at.
#
# You can place objects in the session by using the <tt>@session</tt> hash:
#
# @session["person"] = Person.authenticate(user_name, password)
#
# And retrieved again through the same hash:
#
# Hello #{@session["person"]}
#
# Any object can be placed in the session (as long as it can be Marshalled). But remember that 1000 active sessions each storing a
# 50kb object could lead to a 50MB memory overhead. In other words, think carefully about size and caching before resorting to the use
# of the session.
#
# == Responses
#
# Each action results in a response, which holds the headers and document to be sent to the user's browser. The actual response
# object is generated automatically through the use of renders and redirects, so it's normally nothing you'll need to be concerned about.
#
# == Renders
#
# Action Controller sends content to the user by using one of five rendering methods. The most versatile and common is the rendering
# of a template. Included in the Action Pack is the Action View, which enables rendering of ERb templates. It's automatically configured.
# The controller passes objects to the view by assigning instance variables:
#
# def show
# @post = Post.find(@params["id"])
# end
#
# Which are then automatically available to the view:
#
# Title: <%= @post.title %>
#
# You don't have to rely on the automated rendering. Especially actions that could result in the rendering of different templates will use
# the manual rendering methods:
#
# def search
# @results = Search.find(@params["query"])
# case @results
# when 0 then render "weblog/no_results"
# when 1 then render_action "show"
# when 2..10 then render_action "show_many"
# end
# end
#
# Read more about writing ERb and Builder templates in link:classes/ActionView/Base.html.
#
# == Redirects
#
# Redirecting is what actions that update the model do when they're done. The <tt>save_post</tt> method shouldn't be responsible for also
# showing the post once it's saved -- that's the job for <tt>show_post</tt>. So once <tt>save_post</tt> has completed its business, it'll
# redirect to <tt>show_post</tt>. All redirects are external, which means that when the user refreshes his browser, it's not going to save
# the post again, but rather just show it one more time.
#
# This sounds fairly simple, but the redirection is complicated by the quest for a phenomenon known as "pretty urls". Instead of accepting
# the dreadful beings that is "weblog_controller?action=show&post_id=5", Action Controller goes out of its way to represent the former as
# "/weblog/show/5". And this is even the simple case. As an example of a more advanced pretty url consider
# "/library/books/ISBN/0743536703/show", which can be mapped to books_controller?action=show&type=ISBN&id=0743536703.
#
# Redirects work by rewriting the URL of the current action. So if the show action was called by "/library/books/ISBN/0743536703/show",
# we can redirect to an edit action simply by doing <tt>redirect_to(:action => "edit")</tt>, which could throw the user to
# "/library/books/ISBN/0743536703/edit". Naturally, you'll need to setup the .htaccess (or other means of URL rewriting for the web server)
# to point to the proper controller and action in the first place, but once you have, it can be rewritten with ease.
#
# Let's consider a bunch of examples on how to go from "/library/books/ISBN/0743536703/edit" to somewhere else:
#
# redirect_to(:action => "show", :action_prefix => "XTC/123") =>
# "http://www.singlefile.com/library/books/XTC/123/show"
#
# redirect_to(:path_params => {"type" => "EXBC"}) =>
# "http://www.singlefile.com/library/books/EXBC/0743536703/show"
#
# redirect_to(:controller => "settings") =>
# "http://www.singlefile.com/library/settings/"
#
# For more examples of redirecting options, have a look at the unit test in test/controller/url_test.rb. It's very readable and will give
# you an excellent understanding of the different options and what they do.
#
# == Environments
#
# Action Controller works out of the box with CGI, FastCGI, and mod_ruby. CGI and mod_ruby controllers are triggered just the same using:
#
# WeblogController.process_cgi
#
# FastCGI controllers are triggered using:
#
# FCGI.each_cgi{ |cgi| WeblogController.process_cgi(cgi) }
class Base
include ClassInheritableAttributes
DEFAULT_RENDER_STATUS_CODE = "200 OK"
DEFAULT_SEND_FILE_OPTIONS = {
:type => 'application/octet_stream',
:disposition => 'attachment',
:stream => true,
:buffer_size => 4096
}
# Determines whether the view has access to controller internals @request, @response, @session, and @template.
# By default, it does.
@@view_controller_internals = true
cattr_accessor :view_controller_internals
# All requests are considered local by default, so everyone will be exposed to detailed debugging screens on errors.
# When the application is ready to go public, this should be set to false, and the protected method <tt>local_request?</tt>
# should instead be implemented in the controller to determine when debugging screens should be shown.
@@consider_all_requests_local = true
cattr_accessor :consider_all_requests_local
# When turned on (which is default), all dependencies are included using "load". This mean that any change is instant in cached
# environments like mod_ruby or FastCGI. When set to false, "require" is used, which is faster but requires server restart to
# be effective.
@@reload_dependencies = true
cattr_accessor :reload_dependencies
# Template root determines the base from which template references will be made. So a call to render("test/template")
# will be converted to "#{template_root}/test/template.rhtml".
cattr_accessor :template_root
# The logger is used for generating information on the action run-time (including benchmarking) if available.
# Can be set to nil for no logging. Compatible with both Ruby's own Logger and Log4r loggers.
cattr_accessor :logger
# Determines which template class should be used by ActionController.
cattr_accessor :template_class
# Turn on +ignore_missing_templates+ if you want to unit test actions without making the associated templates.
cattr_accessor :ignore_missing_templates
# Holds the request object that's primarily used to get environment variables through access like
# <tt>@request.env["REQUEST_URI"]</tt>.
attr_accessor :request
# Holds a hash of all the GET, POST, and Url parameters passed to the action. Accessed like <tt>@params["post_id"]</tt>
# to get the post_id. No type casts are made, so all values are returned as strings.
attr_accessor :params
# Holds the response object that's primarily used to set additional HTTP headers through access like
# <tt>@response.headers["Cache-Control"] = "no-cache"</tt>. Can also be used to access the final body HTML after a template
# has been rendered through @response.body -- useful for <tt>after_filter</tt>s that wants to manipulate the output,
# such as a OutputCompressionFilter.
attr_accessor :response
# Holds a hash of objects in the session. Accessed like <tt>@session["person"]</tt> to get the object tied to the "person"
# key. The session will hold any type of object as values, but the key should be a string.
attr_accessor :session
# Holds a hash of header names and values. Accessed like <tt>@headers["Cache-Control"]</tt> to get the value of the Cache-Control
# directive. Values should always be specified as strings.
attr_accessor :headers
# Holds a hash of cookie names and values. Accessed like <tt>@cookies["user_name"]</tt> to get the value of the user_name cookie.
# This hash is read-only. You set new cookies using the cookie method.
attr_accessor :cookies
# Holds the hash of variables that are passed on to the template class to be made available to the view. This hash
# is generated by taking a snapshot of all the instance variables in the current scope just before a template is rendered.
attr_accessor :assigns
class << self
# Factory for the standard create, process loop where the controller is discarded after processing.
def process(request, response) #:nodoc:
new.process(request, response)
end
# Converts the class name from something like "OneModule::TwoModule::NeatController" to "NeatController".
def controller_class_name
Inflector.demodulize(name)
end
# Converts the class name from something like "OneModule::TwoModule::NeatController" to "neat".
def controller_name
Inflector.underscore(controller_class_name.sub(/Controller/, ""))
end
# Loads the <tt>file_name</tt> if reload_dependencies is true or requires if it's false.
def require_or_load(file_name)
reload_dependencies ? load("#{file_name}.rb") : require(file_name)
end
end
public
# Extracts the action_name from the request parameters and performs that action.
def process(request, response) #:nodoc:
initialize_template_class(response)
assign_shortcuts(request, response)
initialize_current_url
log_processing unless logger.nil?
perform_action
close_session
return @response
end
# Returns an URL that has been rewritten according to the hash of +options+ (for doing a complete redirect, use redirect_to). The
# valid keys in options are specified below with an example going from "/library/books/ISBN/0743536703/show" (mapped to
# books_controller?action=show&type=ISBN&id=0743536703):
#
# .---> controller .--> action
# /library/books/ISBN/0743536703/show
# '------> '--------------> action_prefix
# controller_prefix
#
# * <tt>:controller_prefix</tt> - specifies the string before the controller name, which would be "/library" for the example.
# Called with "/shop" gives "/shop/books/ISBN/0743536703/show".
# * <tt>:controller</tt> - specifies a new controller and clears out everything after the controller name (including the action,
# the pre- and suffix, and all params), so called with "settings" gives "/library/settings/".
# * <tt>:action_prefix</tt> - specifies the string between the controller name and the action name, which would
# be "/ISBN/0743536703" for the example. Called with "/XTC/123/" gives "/library/books/XTC/123/show".
# * <tt>:action</tt> - specifies a new action, so called with "edit" gives "/library/books/ISBN/0743536703/edit"
# * <tt>:action_suffix</tt> - specifies the string after the action name, which would be empty for the example.
# Called with "/detailed" gives "/library/books/ISBN/0743536703/detailed".
# * <tt>:path_params</tt> - specifies a hash that contains keys mapping to the request parameter names. In the example,
# { "type" => "ISBN", "id" => "0743536703" } would be the path_params. It serves as another way of replacing part of
# the action_prefix or action_suffix. So passing { "type" => "XTC" } would give "/library/books/XTC/0743536703/show".
# * <tt>:id</tt> - shortcut where ":id => 5" can be used instead of specifying :path_params => { "id" => 5 }.
# Called with "123" gives "/library/books/ISBN/123/show".
# * <tt>:params</tt> - specifies a hash that represents the regular request parameters, such as { "cat" => 1,
# "origin" => "there"} that would give "?cat=1&origin=there". Called with { "temporary" => 1 } in the example would give
# "/library/books/ISBN/0743536703/show?temporary=1"
# * <tt>:anchor</tt> - specifies the anchor name to be appended to the path. Called with "x14" would give
# "/library/books/ISBN/0743536703/show#x14"
# * <tt>:only_path</tt> - if true, returns the absolute URL (omitting the protocol, host name, and port).
#
# Naturally, you can combine multiple options in a single redirect. Examples:
#
# redirect_to(:controller_prefix => "/shop", :controller => "settings")
# redirect_to(:action => "edit", :id => 3425)
# redirect_to(:action => "edit", :path_params => { "type" => "XTC" }, :params => { "temp" => 1})
# redirect_to(:action => "publish", :action_prefix => "/published", :anchor => "x14")
#
# Instead of passing an options hash, you can also pass a method reference in the form of a symbol. Consider this example:
#
# class WeblogController < ActionController::Base
# def update
# # do some update
# redirect_to :dashboard_url
# end
#
# protected
# def dashboard_url
# url_for :controller => (@project.active? ? "project" : "account"), :action => "dashboard"
# end
# end
def url_for(options = {}, *parameters_for_method_reference) #:doc:
case options
when String then options
when Symbol then send(options, *parameters_for_method_reference)
when Hash then @url.rewrite(rewrite_options(options))
end
end
def module_name
@params["module"]
end
# Converts the class name from something like "OneModule::TwoModule::NeatController" to "NeatController".
def controller_class_name
self.class.controller_class_name
end
# Converts the class name from something like "OneModule::TwoModule::NeatController" to "neat".
def controller_name
self.class.controller_name
end
# Returns the name of the action this controller is processing.
def action_name
@params["action"] || "index"
end
protected
# Renders the template specified by <tt>template_name</tt>, which defaults to the name of the current controller and action.
# So calling +render+ in WeblogController#show will attempt to render "#{template_root}/weblog/show.rhtml" or
# "#{template_root}/weblog/show.rxml" (in that order). The template_root is set on the ActionController::Base class and is
# shared by all controllers. It's also possible to pass a status code using the second parameter. This defaults to "200 OK",
# but can be changed, such as by calling <tt>render("weblog/error", "500 Error")</tt>.
def render(template_name = nil, status = nil) #:doc:
render_file(template_name || default_template_name, status, true)
end
# Works like render, but instead of requiring a full template name, you can get by with specifying the action name. So calling
# <tt>render_action "show_many"</tt> in WeblogController#display will render "#{template_root}/weblog/show_many.rhtml" or
# "#{template_root}/weblog/show_many.rxml".
def render_action(action_name, status = nil) #:doc:
render default_template_name(action_name), status
end
# Works like render, but disregards the template_root and requires a full path to the template that needs to be rendered. Can be
# used like <tt>render_file "/Users/david/Code/Ruby/template"</tt> to render "/Users/david/Code/Ruby/template.rhtml" or
# "/Users/david/Code/Ruby/template.rxml".
def render_file(template_path, status = nil, use_full_path = false) #:doc:
assert_existance_of_template_file(template_path) if use_full_path
logger.info("Rendering #{template_path} (#{status || DEFAULT_RENDER_STATUS_CODE})") unless logger.nil?
add_variables_to_assigns
render_text(@template.render_file(template_path, use_full_path), status)
end
# Renders the +template+ string, which is useful for rendering short templates you don't want to bother having a file for. So
# you'd call <tt>render_template "Hello, <%= @user.name %>"</tt> to greet the current user. Or if you want to render as Builder
# template, you could do <tt>render_template "xml.h1 @user.name", nil, "rxml"</tt>.
def render_template(template, status = nil, type = "rhtml") #:doc:
add_variables_to_assigns
render_text(@template.render_template(type, template), status)
end
# Renders the +text+ string without parsing it through any template engine. Useful for rendering static information as it's
# considerably faster than rendering through the template engine.
# Use block for response body if provided (useful for deferred rendering or streaming output).
def render_text(text = nil, status = nil, &block) #:doc:
add_variables_to_assigns
@response.headers["Status"] = status || DEFAULT_RENDER_STATUS_CODE
@response.body = block_given? ? block : text
@performed_render = true
end
# Sends the file by streaming it 4096 bytes at a time. This way the
# whole file doesn't need to be read into memory at once. This makes
# it feasible to send even large files.
#
# Be careful to sanitize the path parameter if it coming from a web
# page. send_file(@params['path']) allows a malicious user to
# download any file on your server.
#
# Options:
# * <tt>:filename</tt> - suggests a filename for the browser to use.
# Defaults to File.basename(path).
# * <tt>:type</tt> - specifies an HTTP content type.
# Defaults to 'application/octet-stream'.
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
# Valid values are 'inline' and 'attachment' (default).
# * <tt>:streaming</tt> - whether to send the file to the user agent as it is read (true)
# or to read the entire file before sending (false). Defaults to true.
# * <tt>:buffer_size</tt> - specifies size (in bytes) of the buffer used to stream the file.
# Defaults to 4096.
#
# The default Content-Type and Content-Disposition headers are
# set to download arbitrary binary files in as many browsers as
# possible. IE versions 4, 5, 5.5, and 6 are all known to have
# a variety of quirks (especially when downloading over SSL).
#
# Simple download:
# send_file '/path/to.zip'
#
# Show a JPEG in browser:
# send_file '/path/to.jpeg', :type => 'image/jpeg', :disposition => 'inline'
#
# Read about the other Content-* HTTP headers if you'd like to
# provide the user with more information (such as Content-Description).
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
#
# Also be aware that the document may be cached by proxies and browsers.
# The Pragma and Cache-Control headers declare how the file may be cached
# by intermediaries. They default to require clients to validate with
# the server before releasing cached responses. See
# http://www.mnot.net/cache_docs/ for an overview of web caching and
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
# for the Cache-Control header spec.
def send_file(path, options = {})
raise MissingFile unless File.file?(path) and File.readable?(path)
options[:length] ||= File.size(path)
options[:filename] ||= File.basename(path)
send_file_headers! options
if options[:stream]
render_text do
logger.info "Streaming file #{path}" unless logger.nil?
len = options[:buffer_size] || 4096
File.open(path, 'rb') do |file|
begin
while true
$stdout.syswrite file.sysread(len)
end
rescue EOFError
end
end
end
else
logger.info "Sending file #{path}" unless logger.nil?
File.open(path, 'rb') { |file| render_text file.read }
end
end
# Send binary data to the user as a file download. May set content type, apparent file name,
# and specify whether to show data inline or download as an attachment.
#
# Options:
# * <tt>:filename</tt> - Suggests a filename for the browser to use.
# * <tt>:type</tt> - specifies an HTTP content type.
# Defaults to 'application/octet-stream'.
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
# Valid values are 'inline' and 'attachment' (default).
#
# Generic data download:
# send_data buffer
#
# Download a dynamically-generated tarball:
# send_data generate_tgz('dir'), :filename => 'dir.tgz'
#
# Display an image Active Record in the browser:
# send_data image.data, :type => image.content_type, :disposition => 'inline'
#
# See +send_file+ for more information on HTTP Content-* headers and caching.
def send_data(data, options = {})
logger.info "Sending data #{options[:filename]}" unless logger.nil?
send_file_headers! options.merge(:length => data.size)
render_text data
end
def rewrite_options(options)
if defaults = default_url_options(options)
defaults.merge(options)
else
options
end
end
# Overwrite to implement a number of default options that all url_for-based methods will use. The default options should come in
# the form of a hash, just like the one you would use for url_for directly. Example:
#
# def default_url_options(options)
# { :controller_prefix => @project.active? ? "projects/" : "accounts/" }
# end
#
# As you can infer from the example, this is mostly useful for situations where you want to centralize dynamic decisions about the
# urls as they stem from the business domain. Please note that any individual url_for call can always override the defaults set
# by this method.
def default_url_options(options) #:doc:
end
# Redirects the browser to an URL that has been rewritten according to the hash of +options+ using a "302 Moved" HTTP header.
# See url_for for a description of the valid options.
def redirect_to(options = {}, *parameters_for_method_reference) #:doc:
if parameters_for_method_reference.empty?
@response.redirected_to = options
redirect_to_url(url_for(options))
else
@response.redirected_to, @response.redirected_to_method_params = options, parameters_for_method_reference
redirect_to_url(url_for(options, *parameters_for_method_reference))
end
end
# Redirects the browser to the specified <tt>path</tt> within the current host (specified with a leading /). Used to sidestep
# the URL rewriting and go directly to a known path. Example: <tt>redirect_to_path "/images/screenshot.jpg"</tt>.
def redirect_to_path(path) #:doc:
redirect_to_url(@request.protocol + @request.host_with_port + path)
end
# Redirects the browser to the specified <tt>url</tt>. Used to redirect outside of the current application. Example:
# <tt>redirect_to_url "http://www.rubyonrails.org"</tt>.
def redirect_to_url(url) #:doc:
logger.info("Redirected to #{url}") unless logger.nil?
@response.redirect(url)
@performed_redirect = true
end
# Creates a new cookie that is sent along-side the next render or redirect command. API is the same as for CGI::Cookie.
# Examples:
#
# cookie("name", "value1", "value2", ...)
# cookie("name" => "name", "value" => "value")
# cookie('name' => 'name',
# 'value' => ['value1', 'value2', ...],
# 'path' => 'path', # optional
# 'domain' => 'domain', # optional
# 'expires' => Time.now, # optional
# 'secure' => true # optional
# )
def cookie(*options) #:doc:
@response.headers["cookie"] << CGI::Cookie.new(*options)
end
# Resets the session by clearsing out all the objects stored within and initializing a new session object.
def reset_session #:doc:
@request.reset_session
@session = @request.session
@response.session = @session
end
private
def initialize_template_class(response)
begin
response.template = template_class.new(template_root, {}, self)
rescue
raise "You must assign a template class through ActionController.template_class= before processing a request"
end
@performed_render = @performed_redirect = false
end
def assign_shortcuts(request, response)
@request, @params, @cookies = request, request.parameters, request.cookies
@response = response
@response.session = request.session
@session = @response.session
@template = @response.template
@assigns = @response.template.assigns
@headers = @response.headers
end
def initialize_current_url
@url = UrlRewriter.new(@request, controller_name, action_name)
end
def log_processing
logger.info "\n\nProcessing #{controller_class_name}\##{action_name} (for #{request_origin})"
logger.info " Parameters: #{@params.inspect}"
end
def perform_action
if action_methods.include?(action_name)
send(action_name)
render unless @performed_render || @performed_redirect
elsif template_exists? && template_public?
render
else
raise UnknownAction, "No action responded to #{action_name}", caller
end
end
def action_methods
action_controller_classes = self.class.ancestors.reject{ |a| [Object, Kernel].include?(a) }
action_controller_classes.inject([]) { |action_methods, klass| action_methods + klass.instance_methods(false) }
end
def add_variables_to_assigns
add_instance_variables_to_assigns
add_class_variables_to_assigns if view_controller_internals
end
def add_instance_variables_to_assigns
protected_variables_cache = protected_instance_variables
instance_variables.each do |var|
next if protected_variables_cache.include?(var)
@assigns[var[1..-1]] = instance_variable_get(var)
end
end
def add_class_variables_to_assigns
%w( template_root logger template_class ignore_missing_templates ).each do |cvar|
@assigns[cvar] = self.send(cvar)
end
end
def protected_instance_variables
if view_controller_internals
[ "@assigns", "@performed_redirect", "@performed_render" ]
else
[ "@assigns", "@performed_redirect", "@performed_render", "@request", "@response", "@session", "@cookies", "@template" ]
end
end
def request_origin
"#{@request.remote_ip} at #{Time.now.to_s}"
end
def close_session
@session.close unless @session.nil? || Hash === @session
end
def template_exists?(template_name = default_template_name)
@template.file_exists?(template_name)
end
def template_public?(template_name = default_template_name)
@template.file_public?(template_name)
end
def assert_existance_of_template_file(template_name)
unless template_exists?(template_name) || ignore_missing_templates
full_template_path = @template.send(:full_template_path, template_name, 'rhtml')
template_type = (template_name =~ /layouts/i) ? 'layout' : 'template'
raise(MissingTemplate, "Missing #{template_type} #{full_template_path}")
end
end
def send_file_headers!(options)
options.update(DEFAULT_SEND_FILE_OPTIONS.merge(options))
[:length, :type, :disposition].each do |arg|
raise ArgumentError, ":#{arg} option required" if options[arg].nil?
end
disposition = options[:disposition] || 'attachment'
disposition <<= %(; filename="#{options[:filename]}") if options[:filename]
@headers.update(
'Content-Length' => options[:length],
'Content-Type' => options[:type],
'Content-Disposition' => disposition,
'Content-Transfer-Encoding' => 'binary'
);
end
def default_template_name(default_action_name = action_name)
module_name ? "#{module_name}/#{controller_name}/#{default_action_name}" : "#{controller_name}/#{default_action_name}"
end
end
end

View File

@ -0,0 +1,49 @@
require 'benchmark'
module ActionController #:nodoc:
# The benchmarking module times the performance of actions and reports to the logger. If the Active Record
# package has been included, a separate timing section for database calls will be added as well.
module Benchmarking #:nodoc:
def self.append_features(base)
super
base.class_eval {
alias_method :perform_action_without_benchmark, :perform_action
alias_method :perform_action, :perform_action_with_benchmark
alias_method :render_without_benchmark, :render
alias_method :render, :render_with_benchmark
}
end
def render_with_benchmark(template_name = default_template_name, status = "200 OK")
if logger.nil?
render_without_benchmark(template_name, status)
else
@rendering_runtime = Benchmark::measure{ render_without_benchmark(template_name, status) }.real
end
end
def perform_action_with_benchmark
if logger.nil?
perform_action_without_benchmark
else
runtime = [Benchmark::measure{ perform_action_without_benchmark }.real, 0.0001].max
log_message = "Completed in #{sprintf("%4f", runtime)} (#{(1 / runtime).floor} reqs/sec)"
log_message << rendering_runtime(runtime) if @rendering_runtime
log_message << active_record_runtime(runtime) if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
logger.info(log_message)
end
end
private
def rendering_runtime(runtime)
" | Rendering: #{sprintf("%f", @rendering_runtime)} (#{sprintf("%d", (@rendering_runtime / runtime) * 100)}%)"
end
def active_record_runtime(runtime)
db_runtime = ActiveRecord::Base.connection.reset_runtime
db_percentage = (db_runtime / runtime) * 100
" | DB: #{sprintf("%f", db_runtime)} (#{sprintf("%d", db_percentage)}%)"
end
end
end

View File

@ -0,0 +1,43 @@
require 'cgi'
require 'cgi/session'
require 'cgi/session/pstore'
require 'action_controller/cgi_ext/cgi_methods'
# Wrapper around the CGIMethods that have been secluded to allow testing without
# an instatiated CGI object
class CGI #:nodoc:
class << self
alias :escapeHTML_fail_on_nil :escapeHTML
def escapeHTML(string)
escapeHTML_fail_on_nil(string) unless string.nil?
end
end
# Returns a parameter hash including values from both the request (POST/GET)
# and the query string with the latter taking precedence.
def parameters
request_parameters.update(query_parameters)
end
def query_parameters
CGIMethods.parse_query_parameters(query_string)
end
def request_parameters
CGIMethods.parse_request_parameters(params)
end
def redirect(where)
header({
"Status" => "302 Moved",
"location" => "#{where}"
})
end
def session(parameters = nil)
parameters = {} if parameters.nil?
parameters['database_manager'] = CGI::Session::PStore
CGI::Session.new(self, parameters)
end
end

View File

@ -0,0 +1,91 @@
require 'cgi'
# Static methods for parsing the query and request parameters that can be used in
# a CGI extension class or testing in isolation.
class CGIMethods #:nodoc:
public
# Returns a hash with the pairs from the query string. The implicit hash construction that is done in
# parse_request_params is not done here.
def CGIMethods.parse_query_parameters(query_string)
parsed_params = {}
query_string.split(/[&;]/).each { |p|
k, v = p.split('=')
k = CGI.unescape(k) unless k.nil?
v = CGI.unescape(v) unless v.nil?
if k =~ /(.*)\[\]$/
if parsed_params.has_key? $1
parsed_params[$1] << v
else
parsed_params[$1] = [v]
end
else
parsed_params[k] = v.nil? ? nil : v
end
}
return parsed_params
end
# Returns the request (POST/GET) parameters in a parsed form where pairs such as "customer[address][street]" /
# "Somewhere cool!" are translated into a full hash hierarchy, like
# { "customer" => { "address" => { "street" => "Somewhere cool!" } } }
def CGIMethods.parse_request_parameters(params)
parsed_params = {}
for key, value in params
value = [value] if key =~ /.*\[\]$/
CGIMethods.build_deep_hash(
CGIMethods.get_typed_value(value[0]),
parsed_params,
CGIMethods.get_levels(key)
)
end
return parsed_params
end
private
def CGIMethods.get_typed_value(value)
if value.respond_to?(:content_type) && !value.content_type.empty?
# Uploaded file
value
elsif value.respond_to?(:read)
# Value as part of a multipart request
value.read
elsif value.class == Array
value
else
# Standard value (not a multipart request)
value.to_s
end
end
def CGIMethods.get_levels(key_string)
return [] if key_string.nil? or key_string.empty?
levels = []
main, existance = /(\w+)(\[)?.?/.match(key_string).captures
levels << main
unless existance.nil?
hash_part = key_string.sub(/\w+\[/, "")
hash_part.slice!(-1, 1)
levels += hash_part.split(/\]\[/)
end
levels
end
def CGIMethods.build_deep_hash(value, hash, levels)
if levels.length == 0
value;
elsif hash.nil?
{ levels.first => CGIMethods.build_deep_hash(value, nil, levels[1..-1]) }
else
hash.update({ levels.first => CGIMethods.build_deep_hash(value, hash[levels.first], levels[1..-1]) })
end
end
end

View File

@ -0,0 +1,124 @@
require 'action_controller/cgi_ext/cgi_ext'
require 'action_controller/support/cookie_performance_fix'
require 'action_controller/session/drb_store'
require 'action_controller/session/active_record_store'
module ActionController #:nodoc:
class Base
# Process a request extracted from an CGI object and return a response. Pass false as <tt>session_options</tt> to disable
# sessions (large performance increase if sessions are not needed). The <tt>session_options</tt> are the same as for CGI::Session:
#
# * <tt>:database_manager</tt> - standard options are CGI::Session::FileStore, CGI::Session::MemoryStore, and CGI::Session::PStore
# (default). Additionally, there is CGI::Session::DRbStore and CGI::Session::ActiveRecordStore. Read more about these in
# lib/action_controller/session.
# * <tt>:session_key</tt> - the parameter name used for the session id. Defaults to '_session_id'.
# * <tt>:session_id</tt> - the session id to use. If not provided, then it is retrieved from the +session_key+ parameter
# of the request, or automatically generated for a new session.
# * <tt>:new_session</tt> - if true, force creation of a new session. If not set, a new session is only created if none currently
# exists. If false, a new session is never created, and if none currently exists and the +session_id+ option is not set,
# an ArgumentError is raised.
# * <tt>:session_expires</tt> - the time the current session expires, as a +Time+ object. If not set, the session will continue
# indefinitely.
# * <tt>:session_domain</tt> - the hostname domain for which this session is valid. If not set, defaults to the hostname of the
# server.
# * <tt>:session_secure</tt> - if +true+, this session will only work over HTTPS.
# * <tt>:session_path</tt> - the path for which this session applies. Defaults to the directory of the CGI script.
def self.process_cgi(cgi = CGI.new, session_options = {})
new.process_cgi(cgi, session_options)
end
def process_cgi(cgi, session_options = {}) #:nodoc:
process(CgiRequest.new(cgi, session_options), CgiResponse.new(cgi)).out
end
end
class CgiRequest < AbstractRequest #:nodoc:
attr_accessor :cgi
DEFAULT_SESSION_OPTIONS =
{ "database_manager" => CGI::Session::PStore, "prefix" => "ruby_sess.", "session_path" => "/" }
def initialize(cgi, session_options = {})
@cgi = cgi
@session_options = session_options
super()
end
def query_parameters
@cgi.query_string ? CGIMethods.parse_query_parameters(@cgi.query_string) : {}
end
def request_parameters
CGIMethods.parse_request_parameters(@cgi.params)
end
def env
@cgi.send(:env_table)
end
def cookies
@cgi.cookies.freeze
end
def host
env["HTTP_X_FORWARDED_HOST"] || @cgi.host.split(":").first
end
def session
return @session unless @session.nil?
begin
@session = (@session_options == false ? {} : CGI::Session.new(@cgi, DEFAULT_SESSION_OPTIONS.merge(@session_options)))
@session["__valid_session"]
return @session
rescue ArgumentError => e
@session.delete if @session
raise(
ActionController::SessionRestoreError,
"Session contained objects where the class definition wasn't available. " +
"Remember to require classes for all objects kept in the session. " +
"The session has been deleted."
)
end
end
def reset_session
@session.delete
@session = (@session_options == false ? {} : new_session)
end
def method_missing(method_id, *arguments)
@cgi.send(method_id, *arguments) rescue super
end
private
def new_session
CGI::Session.new(@cgi, DEFAULT_SESSION_OPTIONS.merge(@session_options).merge("new_session" => true))
end
end
class CgiResponse < AbstractResponse #:nodoc:
def initialize(cgi)
@cgi = cgi
super()
end
def out
convert_content_type!(@headers)
$stdout.binmode if $stdout.respond_to?(:binmode)
print @cgi.header(@headers)
if @body.respond_to?(:call)
@body.call(self)
else
print @body
end
end
private
def convert_content_type!(headers)
if headers["Content-Type"]
headers["type"] = headers["Content-Type"]
headers.delete "Content-Type"
end
end
end
end

View File

@ -0,0 +1,49 @@
module ActionController #:nodoc:
module Dependencies #:nodoc:
def self.append_features(base)
super
base.extend(ClassMethods)
end
module ClassMethods
def model(*models)
require_dependencies(:model, models)
depend_on(:model, models)
end
def service(*services)
require_dependencies(:service, services)
depend_on(:service, services)
end
def observer(*observers)
require_dependencies(:observer, observers)
depend_on(:observer, observers)
instantiate_observers(observers)
end
def dependencies_on(layer) # :nodoc:
read_inheritable_attribute("#{layer}_dependencies")
end
def depend_on(layer, dependencies)
write_inheritable_array("#{layer}_dependencies", dependencies)
end
private
def instantiate_observers(observers)
observers.flatten.each { |observer| Object.const_get(Inflector.classify(observer.to_s)).instance }
end
def require_dependencies(layer, dependencies)
dependencies.flatten.each do |dependency|
begin
require_or_load(dependency.to_s)
rescue LoadError
raise LoadError, "Missing #{layer} #{dependency}.rb"
end
end
end
end
end
end

View File

@ -0,0 +1,279 @@
module ActionController #:nodoc:
module Filters #:nodoc:
def self.append_features(base)
super
base.extend(ClassMethods)
base.class_eval { include ActionController::Filters::InstanceMethods }
end
# Filters enable controllers to run shared pre and post processing code for its actions. These filters can be used to do
# authentication, caching, or auditing before the intended action is performed. Or to do localization or output
# compression after the action has been performed.
#
# Filters have access to the request, response, and all the instance variables set by other filters in the chain
# or by the action (in the case of after filters). Additionally, it's possible for a pre-processing <tt>before_filter</tt>
# to halt the processing before the intended action is processed by returning false. This is especially useful for
# filters like authentication where you're not interested in allowing the action to be performed if the proper
# credentials are not in order.
#
# == Filter inheritance
#
# Controller inheritance hierarchies share filters downwards, but subclasses can also add new filters without
# affecting the superclass. For example:
#
# class BankController < ActionController::Base
# before_filter :audit
#
# private
# def audit
# # record the action and parameters in an audit log
# end
# end
#
# class VaultController < BankController
# before_filter :verify_credentials
#
# private
# def verify_credentials
# # make sure the user is allowed into the vault
# end
# end
#
# Now any actions performed on the BankController will have the audit method called before. On the VaultController,
# first the audit method is called, then the verify_credentials method. If the audit method returns false, then
# verify_credentials and the intended action is never called.
#
# == Filter types
#
# A filter can take one of three forms: method reference (symbol), external class, or inline method (proc). The first
# is the most common and works by referencing a protected or private method somewhere in the inheritance hierarchy of
# the controller by use of a symbol. In the bank example above, both BankController and VaultController use this form.
#
# Using an external class makes for more easily reused generic filters, such as output compression. External filter classes
# are implemented by having a static +filter+ method on any class and then passing this class to the filter method. Example:
#
# class OutputCompressionFilter
# def self.filter(controller)
# controller.response.body = compress(controller.response.body)
# end
# end
#
# class NewspaperController < ActionController::Base
# after_filter OutputCompressionFilter
# end
#
# The filter method is passed the controller instance and is hence granted access to all aspects of the controller and can
# manipulate them as it sees fit.
#
# The inline method (using a proc) can be used to quickly do something small that doesn't require a lot of explanation.
# Or just as a quick test. It works like this:
#
# class WeblogController < ActionController::Base
# before_filter { |controller| return false if controller.params["stop_action"] }
# end
#
# As you can see, the block expects to be passed the controller after it has assigned the request to the internal variables.
# This means that the block has access to both the request and response objects complete with convenience methods for params,
# session, template, and assigns. Note: The inline method doesn't strictly has to be a block. Any object that responds to call
# and returns 1 or -1 on arity will do (such as a Proc or an Method object).
#
# == Filter chain ordering
#
# Using <tt>before_filter</tt> and <tt>after_filter</tt> appends the specified filters to the existing chain. That's usually
# just fine, but some times you care more about the order in which the filters are executed. When that's the case, you
# can use <tt>prepend_before_filter</tt> and <tt>prepend_after_filter</tt>. Filters added by these methods will be put at the
# beginning of their respective chain and executed before the rest. For example:
#
# class ShoppingController
# before_filter :verify_open_shop
#
# class CheckoutController
# prepend_before_filter :ensure_items_in_cart, :ensure_items_in_stock
#
# The filter chain for the CheckoutController is now <tt>:ensure_items_in_cart, :ensure_items_in_stock,</tt>
# <tt>:verify_open_shop</tt>. So if either of the ensure filters return false, we'll never get around to see if the shop
# is open or not.
#
# You may pass multiple filter arguments of each type as well as a filter block.
# If a block is given, it is treated as the last argument.
#
# == Around filters
#
# In addition to the individual before and after filters, it's also possible to specify that a single object should handle
# both the before and after call. That's especially usefuly when you need to keep state active between the before and after,
# such as the example of a benchmark filter below:
#
# class WeblogController < ActionController::Base
# around_filter BenchmarkingFilter.new
#
# # Before this action is performed, BenchmarkingFilter#before(controller) is executed
# def index
# end
# # After this action has been performed, BenchmarkingFilter#after(controller) is executed
# end
#
# class BenchmarkingFilter
# def initialize
# @runtime
# end
#
# def before
# start_timer
# end
#
# def after
# stop_timer
# report_result
# end
# end
module ClassMethods
# The passed <tt>filters</tt> will be appended to the array of filters that's run _before_ actions
# on this controller are performed.
def append_before_filter(*filters, &block)
filters << block if block_given?
append_filter_to_chain("before", filters)
end
# The passed <tt>filters</tt> will be prepended to the array of filters that's run _before_ actions
# on this controller are performed.
def prepend_before_filter(*filters, &block)
filters << block if block_given?
prepend_filter_to_chain("before", filters)
end
# Short-hand for append_before_filter since that's the most common of the two.
alias :before_filter :append_before_filter
# The passed <tt>filters</tt> will be appended to the array of filters that's run _after_ actions
# on this controller are performed.
def append_after_filter(*filters, &block)
filters << block if block_given?
append_filter_to_chain("after", filters)
end
# The passed <tt>filters</tt> will be prepended to the array of filters that's run _after_ actions
# on this controller are performed.
def prepend_after_filter(*filters, &block)
filters << block if block_given?
prepend_filter_to_chain("after", filters)
end
# Short-hand for append_after_filter since that's the most common of the two.
alias :after_filter :append_after_filter
# The passed <tt>filters</tt> will have their +before+ method appended to the array of filters that's run both before actions
# on this controller are performed and have their +after+ method prepended to the after actions. The filter objects must all
# respond to both +before+ and +after+. So if you do append_around_filter A.new, B.new, the callstack will look like:
#
# B#before
# A#before
# A#after
# B#after
def append_around_filter(filters)
for filter in [filters].flatten
ensure_filter_responds_to_before_and_after(filter)
append_before_filter { |c| filter.before(c) }
prepend_after_filter { |c| filter.after(c) }
end
end
# The passed <tt>filters</tt> will have their +before+ method prepended to the array of filters that's run both before actions
# on this controller are performed and have their +after+ method appended to the after actions. The filter objects must all
# respond to both +before+ and +after+. So if you do prepend_around_filter A.new, B.new, the callstack will look like:
#
# A#before
# B#before
# B#after
# A#after
def prepend_around_filter(filters)
for filter in [filters].flatten
ensure_filter_responds_to_before_and_after(filter)
prepend_before_filter { |c| filter.before(c) }
append_after_filter { |c| filter.after(c) }
end
end
# Short-hand for append_around_filter since that's the most common of the two.
alias :around_filter :append_around_filter
# Returns all the before filters for this class and all its ancestors.
def before_filters #:nodoc:
read_inheritable_attribute("before_filters")
end
# Returns all the after filters for this class and all its ancestors.
def after_filters #:nodoc:
read_inheritable_attribute("after_filters")
end
private
def append_filter_to_chain(condition, filters)
write_inheritable_array("#{condition}_filters", filters)
end
def prepend_filter_to_chain(condition, filters)
write_inheritable_attribute("#{condition}_filters", filters + read_inheritable_attribute("#{condition}_filters"))
end
def ensure_filter_responds_to_before_and_after(filter)
unless filter.respond_to?(:before) && filter.respond_to?(:after)
raise ActionControllerError, "Filter object must respond to both before and after"
end
end
end
module InstanceMethods # :nodoc:
def self.append_features(base)
super
base.class_eval {
alias_method :perform_action_without_filters, :perform_action
alias_method :perform_action, :perform_action_with_filters
}
end
def perform_action_with_filters
return if before_action == false
perform_action_without_filters
after_action
end
# Calls all the defined before-filter filters, which are added by using "before_filter :method".
# If any of the filters return false, no more filters will be executed and the action is aborted.
def before_action #:doc:
call_filters(self.class.before_filters)
end
# Calls all the defined after-filter filters, which are added by using "after_filter :method".
# If any of the filters return false, no more filters will be executed.
def after_action #:doc:
call_filters(self.class.after_filters)
end
private
def call_filters(filters)
filters.each do |filter|
if Symbol === filter
if self.send(filter) == false then return false end
elsif filter_block?(filter)
if filter.call(self) == false then return false end
elsif filter_class?(filter)
if filter.filter(self) == false then return false end
else
raise(
ActionControllerError,
"Filters need to be either a symbol, proc/method, or class implementing a static filter method"
)
end
end
end
def filter_block?(filter)
filter.respond_to?("call") && (filter.arity == 1 || filter.arity == -1)
end
def filter_class?(filter)
filter.respond_to?("filter")
end
end
end
end

View File

@ -0,0 +1,65 @@
module ActionController #:nodoc:
# The flash provides a way to pass temporary objects between actions. Anything you place in the flash will be exposed
# to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create action
# that sets <tt>flash["notice"] = "Succesfully created"</tt> before redirecting to a display action that can then expose
# the flash to its template. Actually, that exposure is automatically done. Example:
#
# class WeblogController < ActionController::Base
# def create
# # save post
# flash["notice"] = "Succesfully created post"
# redirect_to :action => "display", :params => { "id" => post.id }
# end
#
# def display
# # doesn't need to assign the flash notice to the template, that's done automatically
# end
# end
#
# display.rhtml
# <% if @flash["notice"] %><div class="notice"><%= @flash["notice"] %></div><% end %>
#
# This example just places a string in the flash, but you can put any object in there. And of course, you can put as many
# as you like at a time too. Just remember: They'll be gone by the time the next action has been performed.
module Flash
def self.append_features(base) #:nodoc:
super
base.before_filter(:fire_flash)
base.after_filter(:clear_flash)
end
protected
# Access the contents of the flash. Use <tt>flash["notice"]</tt> to read a notice you put there or
# <tt>flash["notice"] = "hello"</tt> to put a new one.
def flash #:doc:
if @session["flash"].nil?
@session["flash"] = {}
@session["flashes"] ||= 0
end
@session["flash"]
end
# Can be called by any action that would like to keep the current content of the flash around for one more action.
def keep_flash #:doc:
@session["flashes"] = 0
end
private
# Records that the contents of @session["flash"] was flashed to the action
def fire_flash
if @session["flash"]
@session["flashes"] += 1 unless @session["flash"].empty?
@assigns["flash"] = @session["flash"]
else
@assigns["flash"] = {}
end
end
def clear_flash
if @session["flash"] && (@session["flashes"].nil? || @session["flashes"] >= 1)
@session["flash"] = {}
@session["flashes"] = 0
end
end
end
end

View File

@ -0,0 +1,100 @@
module ActionController #:nodoc:
module Helpers #:nodoc:
def self.append_features(base)
super
base.class_eval { class << self; alias_method :inherited_without_helper, :inherited; end }
base.extend(ClassMethods)
end
# The template helpers serves to relieve the templates from including the same inline code again and again. It's a
# set of standardized methods for working with forms (FormHelper), dates (DateHelper), texts (TextHelper), and
# Active Records (ActiveRecordHelper) that's available to all templates by default.
#
# It's also really easy to make your own helpers and it's much encouraged to keep the template files free
# from complicated logic. It's even encouraged to bundle common compositions of methods from other helpers
# (often the common helpers) as they're used by the specific application.
#
# module MyHelper
# def hello_world() "hello world" end
# end
#
# MyHelper can now be included in a controller, like this:
#
# class MyController < ActionController::Base
# helper :my_helper
# end
#
# ...and, same as above, used in any template rendered from MyController, like this:
#
# Let's hear what the helper has to say: <tt><%= hello_world %></tt>
module ClassMethods
# Makes all the (instance) methods in the helper module available to templates rendered through this controller.
# See ActionView::Helpers (link:classes/ActionView/Helpers.html) for more about making your own helper modules
# available to the templates.
def add_template_helper(helper_module)
template_class.class_eval "include #{helper_module}"
end
# Declare a helper. If you use this method in your controller, you don't
# have to do the +self.append_features+ incantation in your helper class.
# helper :foo
# requires 'foo_helper' and includes FooHelper in the template class.
# helper FooHelper
# includes FooHelper in the template class.
# helper { def foo() "#{bar} is the very best" end }
# evaluates the block in the template class, adding method #foo.
# helper(:three, BlindHelper) { def mice() 'mice' end }
# does all three.
def helper(*args, &block)
args.flatten.each do |arg|
case arg
when Module
add_template_helper(arg)
when String, Symbol
file_name = Inflector.underscore(arg.to_s.downcase) + '_helper'
class_name = Inflector.camelize(file_name)
begin
require_or_load(file_name)
rescue LoadError
raise LoadError, "Missing helper file helpers/#{file_name}.rb"
end
raise ArgumentError, "Missing #{class_name} module in helpers/#{file_name}.rb" unless Object.const_defined?(class_name)
add_template_helper(Object.const_get(class_name))
else
raise ArgumentError, 'helper expects String, Symbol, or Module argument'
end
end
# Evaluate block in template class if given.
template_class.module_eval(&block) if block_given?
end
# Declare a controller method as a helper. For example,
# helper_method :link_to
# def link_to(name, options) ... end
# makes the link_to controller method available in the view.
def helper_method(*methods)
template_class.controller_delegate(*methods)
end
# Declare a controller attribute as a helper. For example,
# helper_attr :name
# attr_accessor :name
# makes the name and name= controller methods available in the view.
# The is a convenience wrapper for helper_method.
def helper_attr(*attrs)
attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") }
end
private
def inherited(child)
inherited_without_helper(child)
begin
child.helper(child.controller_name)
rescue LoadError
# No default helper available for this controller
end
end
end
end
end

View File

@ -0,0 +1,149 @@
module ActionController #:nodoc:
module Layout #:nodoc:
def self.append_features(base)
super
base.extend(ClassMethods)
base.class_eval do
alias_method :render_without_layout, :render
alias_method :render, :render_with_layout
end
end
# Layouts reverse the common pattern of including shared headers and footers in many templates to isolate changes in
# repeated setups. The inclusion pattern has pages that look like this:
#
# <%= render "shared/header" %>
# Hello World
# <%= render "shared/footer" %>
#
# This approach is a decent way of keeping common structures isolated from the changing content, but it's verbose
# and if you ever want to change the structure of these two includes, you'll have to change all the templates.
#
# With layouts, you can flip it around and have the common structure know where to insert changing content. This means
# that the header and footer is only mentioned in one place, like this:
#
# <!-- The header part of this layout -->
# <%= @content_for_layout %>
# <!-- The footer part of this layout -->
#
# And then you have content pages that look like this:
#
# hello world
#
# Not a word about common structures. At rendering time, the content page is computed and then inserted in the layout,
# like this:
#
# <!-- The header part of this layout -->
# hello world
# <!-- The footer part of this layout -->
#
# == Accessing shared variables
#
# Layouts have access to variables specified in the content pages and vice versa. This allows you to have layouts with
# references that won't materialize before rendering time:
#
# <h1><%= @page_title %></h1>
# <%= @content_for_layout %>
#
# ...and content pages that fulfill these references _at_ rendering time:
#
# <% @page_title = "Welcome" %>
# Off-world colonies offers you a chance to start a new life
#
# The result after rendering is:
#
# <h1>Welcome</h1>
# Off-world colonies offers you a chance to start a new life
#
# == Inheritance for layouts
#
# Layouts are shared downwards in the inheritance hierarchy, but not upwards. Examples:
#
# class BankController < ActionController::Base
# layout "layouts/bank_standard"
#
# class InformationController < BankController
#
# class VaultController < BankController
# layout :access_level_layout
#
# class EmployeeController < BankController
# layout nil
#
# The InformationController uses "layouts/bank_standard" inherited from the BankController, the VaultController overwrites
# and picks the layout dynamically, and the EmployeeController doesn't want to use a layout at all.
#
# == Types of layouts
#
# Layouts are basically just regular templates, but the name of this template needs not be specified statically. Sometimes
# you want to alternate layouts depending on runtime information, such as whether someone is logged in or not. This can
# be done either by specifying a method reference as a symbol or using an inline method (as a proc).
#
# The method reference is the preferred approach to variable layouts and is used like this:
#
# class WeblogController < ActionController::Base
# layout :writers_and_readers
#
# def index
# # fetching posts
# end
#
# private
# def writers_and_readers
# logged_in? ? "writer_layout" : "reader_layout"
# end
#
# Now when a new request for the index action is processed, the layout will vary depending on whether the person accessing
# is logged in or not.
#
# If you want to use an inline method, such as a proc, do something like this:
#
# class WeblogController < ActionController::Base
# layout proc{ |controller| controller.logged_in? ? "writer_layout" : "reader_layout" }
#
# Of course, the most common way of specifying a layout is still just as a plain template path:
#
# class WeblogController < ActionController::Base
# layout "layouts/weblog_standard"
#
# == Avoiding the use of a layout
#
# If you have a layout that by default is applied to all the actions of a controller, you still have the option to rendering
# a given action without a layout. Just use the method <tt>render_without_layout</tt>, which works just like Base.render --
# it just doesn't apply any layouts.
module ClassMethods
# If a layout is specified, all actions rendered through render and render_action will have their result assigned
# to <tt>@content_for_layout</tt>, which can then be used by the layout to insert their contents with
# <tt><%= @content_for_layout %></tt>. This layout can itself depend on instance variables assigned during action
# performance and have access to them as any normal template would.
def layout(template_name)
write_inheritable_attribute "layout", template_name
end
end
# Returns the name of the active layout. If the layout was specified as a method reference (through a symbol), this method
# is called and the return value is used. Likewise if the layout was specified as an inline method (through a proc or method
# object). If the layout was defined without a directory, layouts is assumed. So <tt>layout "weblog/standard"</tt> will return
# weblog/standard, but <tt>layout "standard"</tt> will return layouts/standard.
def active_layout(passed_layout = nil)
layout = passed_layout || self.class.read_inheritable_attribute("layout")
active_layout = case layout
when Symbol then send(layout)
when Proc then layout.call(self)
when String then layout
end
active_layout.include?("/") ? active_layout : "layouts/#{active_layout}" if active_layout
end
def render_with_layout(template_name = default_template_name, status = nil, layout = nil) #:nodoc:
if layout ||= active_layout
add_variables_to_assigns
logger.info("Rendering #{template_name} within #{layout}") unless logger.nil?
@content_for_layout = @template.render_file(template_name, true)
render_without_layout(layout, status)
else
render_without_layout(template_name, status)
end
end
end
end

View File

@ -0,0 +1,99 @@
module ActionController
# These methods are available in both the production and test Request objects.
class AbstractRequest
# Returns both GET and POST parameters in a single hash.
def parameters
@parameters ||= request_parameters.update(query_parameters)
end
def method
env['REQUEST_METHOD'].downcase.intern
end
def get?
method == :get
end
def post?
method == :post
end
def put?
method == :put
end
def delete?
method == :delete
end
# Determine originating IP address. REMOTE_ADDR is the standard
# but will fail if the user is behind a proxy. HTTP_CLIENT_IP and/or
# HTTP_X_FORWARDED_FOR are set by proxies so check for these before
# falling back to REMOTE_ADDR. HTTP_X_FORWARDED_FOR may be a comma-
# delimited list in the case of multiple chained proxies; the first is
# the originating IP.
def remote_ip
if env['HTTP_CLIENT_IP']
env['HTTP_CLIENT_IP']
elsif env['HTTP_X_FORWARDED_FOR']
remote_ip = env['HTTP_X_FORWARDED_FOR'].split(',').reject { |ip|
ip =~ /^unknown$|^(10|172\.16|192\.168)\./i
}.first
remote_ip ? remote_ip.strip : env['REMOTE_ADDR']
else
env['REMOTE_ADDR']
end
end
def request_uri
env["REQUEST_URI"]
end
def protocol
port == 443 ? "https://" : "http://"
end
def path
request_uri ? request_uri.split("?").first : ""
end
def port
env["SERVER_PORT"].to_i
end
def host_with_port
if env['HTTP_HOST']
env['HTTP_HOST']
elsif (protocol == "http://" && port == 80) || (protocol == "https://" && port == 443)
host
else
host + ":#{port}"
end
end
#--
# Must be implemented in the concrete request
#++
def query_parameters
end
def request_parameters
end
def env
end
def host
end
def cookies
end
def session
end
def reset_session
end
end
end

View File

@ -0,0 +1,94 @@
module ActionController #:nodoc:
# Actions that fail to perform as expected throw exceptions. These exceptions can either be rescued for the public view
# (with a nice user-friendly explanation) or for the developers view (with tons of debugging information). The developers view
# is already implemented by the Action Controller, but the public view should be tailored to your specific application. So too
# could the decision on whether something is a public or a developer request.
#
# You can tailor the rescuing behavior and appearance by overwriting the following two stub methods.
module Rescue
def self.append_features(base) #:nodoc:
super
base.class_eval do
alias_method :perform_action_without_rescue, :perform_action
alias_method :perform_action, :perform_action_with_rescue
end
end
protected
# Exception handler called when the performance of an action raises an exception.
def rescue_action(exception)
log_error(exception) unless logger.nil?
if consider_all_requests_local || local_request?
rescue_action_locally(exception)
else
rescue_action_in_public(exception)
end
end
# Overwrite to implement custom logging of errors. By default logs as fatal.
def log_error(exception) #:doc:
if ActionView::TemplateError === exception
logger.fatal(exception.to_s)
else
logger.fatal(
"\n\n#{exception.class} (#{exception.message}):\n " +
clean_backtrace(exception).join("\n ") +
"\n\n"
)
end
end
# Overwrite to implement public exception handling (for requests answering false to <tt>local_request?</tt>).
def rescue_action_in_public(exception) #:doc:
render_text "<html><body><h1>Application error (Rails)</h1></body></html>"
end
# Overwrite to expand the meaning of a local request in order to show local rescues on other occurances than
# the remote IP being 127.0.0.1. For example, this could include the IP of the developer machine when debugging
# remotely.
def local_request? #:doc:
@request.remote_addr == "127.0.0.1"
end
# Renders a detailed diagnostics screen on action exceptions.
def rescue_action_locally(exception)
@exception = exception
@rescues_path = File.dirname(__FILE__) + "/templates/rescues/"
add_variables_to_assigns
@contents = @template.render_file(template_path_for_local_rescue(exception), false)
@headers["Content-Type"] = "text/html"
render_file(rescues_path("layout"), "500 Internal Error")
end
private
def perform_action_with_rescue #:nodoc:
begin
perform_action_without_rescue
rescue Exception => exception
rescue_action(exception)
end
end
def rescues_path(template_name)
File.dirname(__FILE__) + "/templates/rescues/#{template_name}.rhtml"
end
def template_path_for_local_rescue(exception)
rescues_path(
case exception
when MissingTemplate then "missing_template"
when UnknownAction then "unknown_action"
when ActionView::TemplateError then "template_error"
else "diagnostics"
end
)
end
def clean_backtrace(exception)
base_dir = File.expand_path(File.dirname(__FILE__) + "/../../../../")
exception.backtrace.collect { |line| line.gsub(base_dir, "").gsub("/public/../config/environments/../../", "").gsub("/public/../", "") }
end
end
end

View File

@ -0,0 +1,15 @@
module ActionController
class AbstractResponse #:nodoc:
DEFAULT_HEADERS = { "Cache-Control" => "no-cache" }
attr_accessor :body, :headers, :session, :cookies, :assigns, :template, :redirected_to, :redirected_to_method_params
def initialize
@body, @headers, @session, @assigns = "", DEFAULT_HEADERS.merge("cookie" => []), [], []
end
def redirect(to_url)
@headers["Status"] = "302 Moved"
@headers["location"] = to_url
end
end
end

View File

@ -0,0 +1,183 @@
module ActionController
module Scaffolding # :nodoc:
def self.append_features(base)
super
base.extend(ClassMethods)
end
# Scaffolding is a way to quickly put an Active Record class online by providing a series of standardized actions
# for listing, showing, creating, updating, and destroying objects of the class. These standardized actions come
# with both controller logic and default templates that through introspection already know which fields to display
# and which input types to use. Example:
#
# class WeblogController < ActionController::Base
# scaffold :entry
# end
#
# This tiny piece of code will add all of the following methods to the controller:
#
# class WeblogController < ActionController::Base
# def index
# list
# end
#
# def list
# @entries = Entry.find_all
# render_scaffold "list"
# end
#
# def show
# @entry = Entry.find(@params["id"])
# render_scaffold
# end
#
# def destroy
# Entry.find(@params["id"]).destroy
# redirect_to :action => "list"
# end
#
# def new
# @entry = Entry.new
# render_scaffold
# end
#
# def create
# @entry = Entry.new(@params["entry"])
# if @entry.save
# flash["notice"] = "Entry was succesfully created"
# redirect_to :action => "list"
# else
# render "entry/new"
# end
# end
#
# def edit
# @entry = Entry.find(@params["id"])
# render_scaffold
# end
#
# def update
# @entry = Entry.find(@params["entry"]["id"])
# @entry.attributes = @params["entry"]
#
# if @entry.save
# flash["notice"] = "Entry was succesfully updated"
# redirect_to :action => "show/" + @entry.id.to_s
# else
# render "entry/edit"
# end
# end
# end
#
# The <tt>render_scaffold</tt> method will first check to see if you've made your own template (like "weblog/show.rhtml" for
# the show action) and if not, then render the generic template for that action. This gives you the possibility of using the
# scaffold while you're building your specific application. Start out with a totally generic setup, then replace one template
# and one action at a time while relying on the rest of the scaffolded templates and actions.
module ClassMethods
# Adds a swath of generic CRUD actions to the controller. The +model_id+ is automatically converted into a class name unless
# one is specifically provide through <tt>options[:class_name]</tt>. So <tt>scaffold :post</tt> would use Post as the class
# and @post/@posts for the instance variables.
#
# It's possible to use more than one scaffold in a single controller by specifying <tt>options[:suffix] = true</tt>. This will
# make <tt>scaffold :post, :suffix => true</tt> use method names like list_post, show_post, and create_post
# instead of just list, show, and post. If suffix is used, then no index method is added.
def scaffold(model_id, options = {})
validate_options([ :class_name, :suffix ], options.keys)
require "#{model_id.id2name}" rescue logger.warn "Couldn't auto-require #{model_id.id2name}.rb" unless logger.nil?
singular_name = model_id.id2name
class_name = options[:class_name] || Inflector.camelize(singular_name)
plural_name = Inflector.pluralize(singular_name)
suffix = options[:suffix] ? "_#{singular_name}" : ""
unless options[:suffix]
module_eval <<-"end_eval", __FILE__, __LINE__
def index
list
end
end_eval
end
module_eval <<-"end_eval", __FILE__, __LINE__
def list#{suffix}
@#{plural_name} = #{class_name}.find_all
render#{suffix}_scaffold "list#{suffix}"
end
def show#{suffix}
@#{singular_name} = #{class_name}.find(@params["id"])
render#{suffix}_scaffold
end
def destroy#{suffix}
#{class_name}.find(@params["id"]).destroy
redirect_to :action => "list#{suffix}"
end
def new#{suffix}
@#{singular_name} = #{class_name}.new
render#{suffix}_scaffold
end
def create#{suffix}
@#{singular_name} = #{class_name}.new(@params["#{singular_name}"])
if @#{singular_name}.save
flash["notice"] = "#{class_name} was succesfully created"
redirect_to :action => "list#{suffix}"
else
render "#{singular_name}/new#{suffix}"
end
end
def edit#{suffix}
@#{singular_name} = #{class_name}.find(@params["id"])
render#{suffix}_scaffold
end
def update#{suffix}
@#{singular_name} = #{class_name}.find(@params["#{singular_name}"]["id"])
@#{singular_name}.attributes = @params["#{singular_name}"]
if @#{singular_name}.save
flash["notice"] = "#{class_name} was succesfully updated"
redirect_to :action => "show#{suffix}/" + @#{singular_name}.id.to_s
else
render "#{singular_name}/edit#{suffix}"
end
end
private
def render#{suffix}_scaffold(action = caller_method_name(caller))
if template_exists?("\#{controller_name}/\#{action}")
render_action(action)
else
@scaffold_class = #{class_name}
@scaffold_singular_name, @scaffold_plural_name = "#{singular_name}", "#{plural_name}"
@scaffold_suffix = "#{suffix}"
add_instance_variables_to_assigns
@content_for_layout = @template.render_file(scaffold_path(action.sub(/#{suffix}$/, "")), false)
self.active_layout ? render_file(self.active_layout, "200 OK", true) : render_file(scaffold_path("layout"))
end
end
def scaffold_path(template_name)
File.dirname(__FILE__) + "/templates/scaffolds/" + template_name + ".rhtml"
end
def caller_method_name(caller)
caller.first.scan(/`(.*)'/).first.first # ' ruby-mode
end
end_eval
end
private
# Raises an exception if an invalid option has been specified to prevent misspellings from slipping through
def validate_options(valid_option_keys, supplied_option_keys)
unknown_option_keys = supplied_option_keys - valid_option_keys
raise(ActionController::ActionControllerError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty?
end
end
end
end

View File

@ -0,0 +1,72 @@
begin
require 'active_record'
require 'cgi'
require 'cgi/session'
# Contributed by Tim Bates
class CGI
class Session
# ActiveRecord database based session storage class.
#
# Implements session storage in a database using the ActiveRecord ORM library. Assumes that the database
# has a table called +sessions+ with columns +id+ (numeric, primary key), +sessid+ and +data+ (text).
# The session data is stored in the +data+ column in YAML format; the user is responsible for ensuring that
# only data that can be YAMLized is stored in the session.
class ActiveRecordStore
# The ActiveRecord class which corresponds to the database table.
class Session < ActiveRecord::Base
serialize :data
# Isn't this class definition beautiful?
end
# Create a new ActiveRecordStore instance. This constructor is used internally by CGI::Session.
# The user does not generally need to call it directly.
#
# +session+ is the session for which this instance is being created.
#
# +option+ is currently ignored as no options are recognized.
#
# This session's ActiveRecord database row will be created if it does not exist, or opened if it does.
def initialize(session, option=nil)
@session = Session.find_first(["sessid = '%s'", session.session_id])
if @session
@data = @session.data
else
@session = Session.new("sessid" => session.session_id, "data" => {})
end
end
# Update and close the session's ActiveRecord object.
def close
return unless @session
update
@session = nil
end
# Close and destroy the session's ActiveRecord object.
def delete
return unless @session
@session.destroy
@session = nil
end
# Restore session state from the session's ActiveRecord object.
def restore
return unless @session
@data = @session.data
end
# Save session state in the session's ActiveRecord object.
def update
return unless @session
@session.data = @data
@session.save
end
end #ActiveRecordStore
end #Session
end #CGI
rescue LoadError
# Couldn't load Active Record, so don't make this store available
end

View File

@ -0,0 +1,9 @@
#!/usr/local/bin/ruby -w
# This is a really simple session storage daemon, basically just a hash,
# which is enabled for DRb access.
require 'drb'
DRb.start_service('druby://127.0.0.1:9192', Hash.new)
DRb.thread.join

View File

@ -0,0 +1,31 @@
require 'cgi'
require 'cgi/session'
require 'drb'
class CGI #:nodoc:all
class Session
class DRbStore
@@session_data = DRbObject.new(nil, 'druby://localhost:9192')
def initialize(session, option=nil)
@session_id = session.session_id
end
def restore
@h = @@session_data[@session_id] || {}
end
def update
@@session_data[@session_id] = @h
end
def close
update
end
def delete
@@session_data.delete(@session_id)
end
end
end
end

View File

@ -0,0 +1,57 @@
# Extends the class object with class and instance accessors for class attributes,
# just like the native attr* accessors for instance attributes.
class Class # :nodoc:
def cattr_reader(*syms)
syms.each do |sym|
class_eval <<-EOS
if ! defined? @@#{sym.id2name}
@@#{sym.id2name} = nil
end
def self.#{sym.id2name}
@@#{sym}
end
def #{sym.id2name}
@@#{sym}
end
def call_#{sym.id2name}
case @@#{sym.id2name}
when Symbol then send(@@#{sym})
when Proc then @@#{sym}.call(self)
when String then @@#{sym}
else nil
end
end
EOS
end
end
def cattr_writer(*syms)
syms.each do |sym|
class_eval <<-EOS
if ! defined? @@#{sym.id2name}
@@#{sym.id2name} = nil
end
def self.#{sym.id2name}=(obj)
@@#{sym.id2name} = obj
end
def self.set_#{sym.id2name}(obj)
@@#{sym.id2name} = obj
end
def #{sym.id2name}=(obj)
@@#{sym} = obj
end
EOS
end
end
def cattr_accessor(*syms)
cattr_reader(*syms)
cattr_writer(*syms)
end
end

View File

@ -0,0 +1,37 @@
# Allows attributes to be shared within an inheritance hierarchy, but where each descendant gets a copy of
# their parents' attributes, instead of just a pointer to the same. This means that the child can add elements
# to, for example, an array without those additions being shared with either their parent, siblings, or
# children, which is unlike the regular class-level attributes that are shared across the entire hierarchy.
module ClassInheritableAttributes # :nodoc:
def self.append_features(base)
super
base.extend(ClassMethods)
end
module ClassMethods # :nodoc:
@@classes ||= {}
def inheritable_attributes
@@classes[self] ||= {}
end
def write_inheritable_attribute(key, value)
inheritable_attributes[key] = value
end
def write_inheritable_array(key, elements)
write_inheritable_attribute(key, []) if read_inheritable_attribute(key).nil?
write_inheritable_attribute(key, read_inheritable_attribute(key) + elements)
end
def read_inheritable_attribute(key)
inheritable_attributes[key]
end
private
def inherited(child)
@@classes[child] = inheritable_attributes.dup
end
end
end

View File

@ -0,0 +1,10 @@
require 'logger'
class Logger #:nodoc:
private
remove_const "Format"
Format = "%s\n"
def format_message(severity, timestamp, msg, progname)
Format % [msg]
end
end

View File

@ -0,0 +1,121 @@
CGI.module_eval { remove_const "Cookie" }
class CGI #:nodoc:
# This is a cookie class that fixes the performance problems with the default one that ships with 1.8.1 and below.
# It replaces the inheritance on SimpleDelegator with DelegateClass(Array) following the suggestion from Matz on
# http://groups.google.com/groups?th=e3a4e68ba042f842&seekm=c3sioe%241qvm%241%40news.cybercity.dk#link14
class Cookie < DelegateClass(Array)
# Create a new CGI::Cookie object.
#
# The contents of the cookie can be specified as a +name+ and one
# or more +value+ arguments. Alternatively, the contents can
# be specified as a single hash argument. The possible keywords of
# this hash are as follows:
#
# name:: the name of the cookie. Required.
# value:: the cookie's value or list of values.
# path:: the path for which this cookie applies. Defaults to the
# base directory of the CGI script.
# domain:: the domain for which this cookie applies.
# expires:: the time at which this cookie expires, as a +Time+ object.
# secure:: whether this cookie is a secure cookie or not (default to
# false). Secure cookies are only transmitted to HTTPS
# servers.
#
# These keywords correspond to attributes of the cookie object.
def initialize(name = "", *value)
options = if name.kind_of?(String)
{ "name" => name, "value" => value }
else
name
end
unless options.has_key?("name")
raise ArgumentError, "`name' required"
end
@name = options["name"]
@value = Array(options["value"])
# simple support for IE
if options["path"]
@path = options["path"]
else
%r|^(.*/)|.match(ENV["SCRIPT_NAME"])
@path = ($1 or "")
end
@domain = options["domain"]
@expires = options["expires"]
@secure = options["secure"] == true ? true : false
super(@value)
end
def __setobj__(obj)
@_dc_obj = obj
end
attr_accessor("name", "value", "path", "domain", "expires")
attr_reader("secure")
# Set whether the Cookie is a secure cookie or not.
#
# +val+ must be a boolean.
def secure=(val)
@secure = val if val == true or val == false
@secure
end
# Convert the Cookie to its string representation.
def to_s
buf = ""
buf += @name + '='
if @value.kind_of?(String)
buf += CGI::escape(@value)
else
buf += @value.collect{|v| CGI::escape(v) }.join("&")
end
if @domain
buf += '; domain=' + @domain
end
if @path
buf += '; path=' + @path
end
if @expires
buf += '; expires=' + CGI::rfc1123_date(@expires)
end
if @secure == true
buf += '; secure'
end
buf
end
# Parse a raw cookie string into a hash of cookie-name=>Cookie
# pairs.
#
# cookies = CGI::Cookie::parse("raw_cookie_string")
# # { "name1" => cookie1, "name2" => cookie2, ... }
#
def self.parse(raw_cookie)
cookies = Hash.new([])
return cookies unless raw_cookie
raw_cookie.split(/; /).each do |pairs|
name, values = pairs.split('=',2)
next unless name and values
name = CGI::unescape(name)
values ||= ""
values = values.split('&').collect{|v| CGI::unescape(v) }
unless cookies.has_key?(name)
cookies[name] = new({ "name" => name, "value" => values })
end
end
cookies
end
end # class Cookie
end

View File

@ -0,0 +1,78 @@
# The Inflector transforms words from singular to plural, class names to table names, modulized class names to ones without,
# and class names to foreign keys.
module Inflector
extend self
def pluralize(word)
result = word.dup
plural_rules.each do |(rule, replacement)|
break if result.gsub!(rule, replacement)
end
return result
end
def singularize(word)
result = word.dup
singular_rules.each do |(rule, replacement)|
break if result.gsub!(rule, replacement)
end
return result
end
def camelize(lower_case_and_underscored_word)
lower_case_and_underscored_word.gsub(/(^|_)(.)/){$2.upcase}
end
def underscore(camel_cased_word)
camel_cased_word.gsub(/([A-Z]+)([A-Z])/,'\1_\2').gsub(/([a-z])([A-Z])/,'\1_\2').downcase
end
def demodulize(class_name_in_module)
class_name_in_module.gsub(/^.*::/, '')
end
def tableize(class_name)
pluralize(underscore(class_name))
end
def classify(table_name)
camelize(singularize(table_name))
end
def foreign_key(class_name, separate_class_name_and_id_with_underscore = true)
Inflector.underscore(Inflector.demodulize(class_name)) +
(separate_class_name_and_id_with_underscore ? "_id" : "id")
end
private
def plural_rules #:doc:
[
[/(x|ch|ss)$/, '\1es'], # search, switch, fix, box, process, address
[/([^aeiouy]|qu)y$/, '\1ies'], # query, ability, agency
[/(?:([^f])fe|([lr])f)$/, '\1\2ves'], # half, safe, wife
[/sis$/, 'ses'], # basis, diagnosis
[/([ti])um$/, '\1a'], # datum, medium
[/person$/, 'people'], # person, salesperson
[/man$/, 'men'], # man, woman, spokesman
[/child$/, 'children'], # child
[/s$/, 's'], # no change (compatibility)
[/$/, 's']
]
end
def singular_rules #:doc:
[
[/(x|ch|ss)es$/, '\1'],
[/([^aeiouy]|qu)ies$/, '\1y'],
[/([lr])ves$/, '\1f'],
[/([^f])ves$/, '\1fe'],
[/(analy|ba|diagno|parenthe|progno|synop|the)ses$/, '\1sis'],
[/([ti])a$/, '\1um'],
[/people$/, 'person'],
[/men$/, 'man'],
[/status$/, 'status'],
[/children$/, 'child'],
[/s$/, '']
]
end
end

View File

@ -0,0 +1,28 @@
<%
base_dir = File.expand_path(File.dirname(__FILE__))
request_parameters_without_action = @request.parameters.clone
request_parameters_without_action.delete("action")
request_parameters_without_action.delete("controller")
request_dump = request_parameters_without_action.inspect.gsub(/,/, ",\n")
session_dump = @request.session.instance_variable_get("@data").inspect.gsub(/,/, ",\n")
response_dump = @response.inspect.gsub(/,/, ",\n")
template_assigns = @response.template.instance_variable_get("@assigns")
%w( response exception template session request template_root template_class url ignore_missing_templates logger cookies headers params ).each { |t| template_assigns.delete(t) }
template_dump = template_assigns.inspect.gsub(/,/, ",\n")
%>
<h2 style="margin-top: 30px">Request</h2>
<p><b>Parameters</b>: <%=h request_dump == "{}" ? "None" : request_dump %></p>
<p><a href="#" onclick="document.getElementById('session_dump').style.display='block'; return false;">Show session dump</a></p>
<div id="session_dump" style="display:none"><%= debug(@request.session.instance_variable_get("@data")) %></div>
<h2 style="margin-top: 30px">Response</h2>
<b>Headers</b>: <%=h @response.headers.inspect.gsub(/,/, ",\n") %><br/>
<p><a href="#" onclick="document.getElementById('template_dump').style.display='block'; return false;">Show template parameters</a></p>
<div id="template_dump" style="display:none"><%= debug(template_assigns) %></div>

View File

@ -0,0 +1,22 @@
<%
base_dir = File.expand_path(File.dirname(__FILE__))
clean_backtrace = @exception.backtrace.collect { |line| line.gsub(base_dir, "").gsub("/../config/environments/../../", "") }
app_trace = clean_backtrace.reject { |line| line[0..6] == "vendor/" || line.include?("dispatch.cgi") }
framework_trace = clean_backtrace - app_trace
%>
<h1>
<%=h @exception.class.to_s %> in
<%=h @request.parameters["controller"].capitalize %>#<%=h @request.parameters["action"] %>
</h1>
<p><%=h @exception.message %></p>
<% unless app_trace.empty? %><pre><code><%=h app_trace.collect { |line| line.gsub("/../", "") }.join("\n") %></code></pre><% end %>
<% unless framework_trace.empty? %>
<a href="#" onclick="document.getElementById('framework_trace').style.display='block'; return false;">Show framework trace</a>
<pre id="framework_trace" style="display:none"><code><%=h framework_trace.join("\n") %></code></pre>
<% end %>
<%= render_file(@rescues_path + "/_request_and_response.rhtml", false) %>

View File

@ -0,0 +1,29 @@
<html>
<head>
<title>Action Controller: Exception caught</title>
<style>
body { background-color: #fff; color: #333; }
body, p, ol, ul, td {
font-family: verdana, arial, helvetica, sans-serif;
font-size: 13px;
line-height: 18px;
}
pre {
background-color: #eee;
padding: 10px;
font-size: 11px;
}
a { color: #000; }
a:visited { color: #666; }
a:hover { color: #fff; background-color:#000; }
</style>
</head>
<body>
<%= @contents %>
</body>
</html>

View File

@ -0,0 +1,2 @@
<h1>Template is missing</h1>
<p><%=h @exception.message %></p>

View File

@ -0,0 +1,26 @@
<%
base_dir = File.expand_path(File.dirname(__FILE__))
framework_trace = @exception.original_exception.backtrace.collect do |line|
line.gsub(base_dir, "").gsub("/../config/environments/../../", "")
end
%>
<h1>
<%=h @exception.original_exception.class.to_s %> in
<%=h @request.parameters["controller"].capitalize %>#<%=h @request.parameters["action"] %>
</h1>
<p>
Showing <i><%=h @exception.file_name %></i> where line <b>#<%=h @exception.line_number %></b> raised
<u><%=h @exception.message %></u>
</p>
<pre><code><%=h @exception.source_extract %></code></pre>
<p><%=h @exception.sub_template_message %></p>
<a href="#" onclick="document.getElementById('framework_trace').style.display='block'">Show template trace</a>
<pre id="framework_trace" style="display:none"><code><%=h framework_trace.join("\n") %></code></pre>
<%= render_file(@rescues_path + "/_request_and_response.rhtml", false) %>

View File

@ -0,0 +1,2 @@
<h1>Unknown action</h1>
<p><%=h @exception.message %></p>

View File

@ -0,0 +1,6 @@
<h1>Editing <%= @scaffold_singular_name %></h1>
<%= form(@scaffold_singular_name, :action => "../update" + @scaffold_suffix) %>
<%= link_to "Show", :action => "show#{@scaffold_suffix}", :id => instance_variable_get("@#{@scaffold_singular_name}").id %> |
<%= link_to "Back", :action => "list#{@scaffold_suffix}" %>

View File

@ -0,0 +1,29 @@
<html>
<head>
<title>Scaffolding</title>
<style>
body { background-color: #fff; color: #333; }
body, p, ol, ul, td {
font-family: verdana, arial, helvetica, sans-serif;
font-size: 13px;
line-height: 18px;
}
pre {
background-color: #eee;
padding: 10px;
font-size: 11px;
}
a { color: #000; }
a:visited { color: #666; }
a:hover { color: #fff; background-color:#000; }
</style>
</head>
<body>
<%= @content_for_layout %>
</body>
</html>

View File

@ -0,0 +1,24 @@
<h1>Listing <%= @scaffold_plural_name %></h1>
<table>
<tr>
<% for column in @scaffold_class.content_columns %>
<th><%= column.human_name %></th>
<% end %>
</tr>
<% for entry in instance_variable_get("@#{@scaffold_plural_name}") %>
<tr>
<% for column in @scaffold_class.content_columns %>
<td><%= entry.send(column.name) %></td>
<% end %>
<td><%= link_to "Show", :action => "show#{@scaffold_suffix}", :id => entry.id %></td>
<td><%= link_to "Edit", :action => "edit#{@scaffold_suffix}", :id => entry.id %></td>
<td><%= link_to "Destroy", :action => "destroy#{@scaffold_suffix}", :id => entry.id %></td>
</tr>
<% end %>
</table>
<br />
<%= link_to "New #{@scaffold_singular_name}", :action => "new#{@scaffold_suffix}" %>

View File

@ -0,0 +1,5 @@
<h1>New <%= @scaffold_singular_name %></h1>
<%= form(@scaffold_singular_name, :action => "create" + @scaffold_suffix) %>
<%= link_to "Back", :action => "list#{@scaffold_suffix}" %>

View File

@ -0,0 +1,9 @@
<% for column in @scaffold_class.content_columns %>
<p>
<b><%= column.human_name %>:</b>
<%= instance_variable_get("@#{@scaffold_singular_name}").send(column.name) %>
</p>
<% end %>
<%= link_to "Edit", :action => "edit#{@scaffold_suffix}", :id => instance_variable_get("@#{@scaffold_singular_name}").id %> |
<%= link_to "Back", :action => "list#{@scaffold_suffix}" %>

View File

@ -0,0 +1,195 @@
require File.dirname(__FILE__) + '/assertions/action_pack_assertions'
require File.dirname(__FILE__) + '/assertions/active_record_assertions'
module ActionController #:nodoc:
class Base
# Process a test request called with a +TestRequest+ object.
def self.process_test(request)
new.process_test(request)
end
def process_test(request) #:nodoc:
process(request, TestResponse.new)
end
end
class TestRequest < AbstractRequest #:nodoc:
attr_writer :cookies
attr_accessor :query_parameters, :request_parameters, :session, :env
attr_accessor :host, :path, :request_uri, :remote_addr
def initialize(query_parameters = nil, request_parameters = nil, session = nil)
@query_parameters = query_parameters || {}
@request_parameters = request_parameters || {}
@session = session || TestSession.new
initialize_containers
initialize_default_values
super()
end
def reset_session
@session = {}
end
def cookies
@cookies.freeze
end
def action=(action_name)
@query_parameters.update({ "action" => action_name })
@parameters = nil
end
def request_uri=(uri)
@request_uri = uri
@path = uri.split("?").first
end
private
def initialize_containers
@env, @cookies = {}, {}
end
def initialize_default_values
@host = "test.host"
@request_uri = "/"
@remote_addr = "127.0.0.1"
@env["SERVER_PORT"] = 80
end
end
class TestResponse < AbstractResponse #:nodoc:
# the class attribute ties a TestResponse to the assertions
class << self
attr_accessor :assertion_target
end
# initializer
def initialize
TestResponse.assertion_target=self# if TestResponse.assertion_target.nil?
super()
end
# the response code of the request
def response_code
headers['Status'][0,3].to_i rescue 0
end
# was the response successful?
def success?
response_code == 200
end
# was the URL not found?
def missing?
response_code == 404
end
# were we redirected?
def redirect?
(300..399).include?(response_code)
end
# was there a server-side error?
def server_error?
(500..599).include?(response_code)
end
# returns the redirection location or nil
def redirect_url
redirect? ? headers['location'] : nil
end
# does the redirect location match this regexp pattern?
def redirect_url_match?( pattern )
return false if redirect_url.nil?
p = Regexp.new(pattern) if pattern.class == String
p = pattern if pattern.class == Regexp
return false if p.nil?
p.match(redirect_url) != nil
end
# returns the template path of the file which was used to
# render this response (or nil)
def rendered_file(with_controller=false)
unless template.first_render.nil?
unless with_controller
template.first_render
else
template.first_render.split('/').last || template.first_render
end
end
end
# was this template rendered by a file?
def rendered_with_file?
!rendered_file.nil?
end
# a shortcut to the flash (or an empty hash if no flash.. hey! that rhymes!)
def flash
session['flash'] || {}
end
# do we have a flash?
def has_flash?
!session['flash'].nil?
end
# do we have a flash that has contents?
def has_flash_with_contents?
!flash.empty?
end
# does the specified flash object exist?
def has_flash_object?(name=nil)
!flash[name].nil?
end
# does the specified object exist in the session?
def has_session_object?(name=nil)
!session[name].nil?
end
# a shortcut to the template.assigns
def template_objects
template.assigns || {}
end
# does the specified template object exist?
def has_template_object?(name=nil)
!template_objects[name].nil?
end
end
class TestSession #:nodoc:
def initialize(attributes = {})
@attributes = attributes
end
def [](key)
@attributes[key]
end
def []=(key, value)
@attributes[key] = value
end
def update() end
def close() end
def delete() @attributes = {} end
end
end
class Test::Unit::TestCase #:nodoc:
private
# execute the request and set/volley the response
def process(action, parameters = nil, session = nil)
@request.action = action.to_s
@request.parameters.update(parameters) unless parameters.nil?
@request.session = ActionController::TestSession.new(session) unless session.nil?
@controller.process(@request, @response)
end
end

View File

@ -0,0 +1,170 @@
module ActionController
# Rewrites urls for Base.redirect_to and Base.url_for in the controller.
class UrlRewriter #:nodoc:
VALID_OPTIONS = [:action, :action_prefix, :action_suffix, :module, :controller, :controller_prefix, :anchor, :params, :path_params, :id, :only_path, :overwrite_params ]
def initialize(request, controller, action)
@request, @controller, @action = request, controller, action
@rewritten_path = @request.path ? @request.path.dup : ""
end
def rewrite(options = {})
validate_options(VALID_OPTIONS, options.keys)
rewrite_url(
rewrite_path(@rewritten_path, options),
options
)
end
def to_s
to_str
end
def to_str
"#{@request.protocol}, #{@request.host_with_port}, #{@request.path}, #{@controller}, #{@action}, #{@request.parameters.inspect}"
end
private
def validate_options(valid_option_keys, supplied_option_keys)
unknown_option_keys = supplied_option_keys - valid_option_keys
raise(ActionController::ActionControllerError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty?
end
def rewrite_url(path, options)
rewritten_url = ""
rewritten_url << @request.protocol unless options[:only_path]
rewritten_url << @request.host_with_port unless options[:only_path]
rewritten_url << path
rewritten_url << build_query_string(new_parameters(options)) if options[:params] || options[:overwrite_params]
rewritten_url << "##{options[:anchor]}" if options[:anchor]
return rewritten_url
end
def rewrite_path(path, options)
include_id_in_path_params(options)
path = rewrite_action(path, options) if options[:action] || options[:action_prefix]
path = rewrite_path_params(path, options) if options[:path_params]
path = rewrite_controller(path, options) if options[:controller] || options[:controller_prefix]
return path
end
def rewrite_path_params(path, options)
index_action = options[:action] == 'index' || options[:action].nil? && @action == 'index'
id_only = options[:path_params].size == 1 && options[:path_params]['id']
if index_action && id_only
path += '/' unless path[-1..-1] == '/'
path += "index/#{options[:path_params]['id']}"
path
else
options[:path_params].inject(path) do |path, pair|
if options[:action].nil? && @request.parameters[pair.first]
path.sub(/\b#{@request.parameters[pair.first]}\b/, pair.last.to_s)
else
path += "/#{pair.last}"
end
end
end
end
def rewrite_action(path, options)
# This regex assumes that "index" actions won't be included in the URL
all, controller_prefix, action_prefix, action_suffix =
/^\/(.*)#{@controller}\/(.*)#{@action == "index" ? "" : @action}(.*)/.match(path).to_a
if @action == "index"
if action_prefix == "index"
# we broke the parsing assumption that this would be excluded, so
# don't tell action_name about our little boo-boo
path = path.sub(action_prefix, action_name(options, nil))
elsif action_prefix && !action_prefix.empty?
path = path.sub(action_prefix, action_name(options, action_prefix))
else
path = path.sub(%r(#{@controller}/?), @controller + "/" + action_name(options)) # " ruby-mode
end
else
path = path.sub((action_prefix || "") + @action + (action_suffix || ""), action_name(options, action_prefix))
end
if options[:controller_prefix] && !options[:controller]
ensure_slash_suffix(options, :controller_prefix)
if controller_prefix
path = path.sub(controller_prefix, options[:controller_prefix])
else
path = options[:controller_prefix] + path
end
end
return path
end
def rewrite_controller(path, options)
all, controller_prefix = /^\/(.*?)#{@controller}/.match(path).to_a
path = "/"
path << controller_name(options, controller_prefix)
path << action_name(options) if options[:action]
path << path_params_in_list(options) if options[:path_params]
return path
end
def action_name(options, action_prefix = nil, action_suffix = nil)
ensure_slash_suffix(options, :action_prefix)
ensure_slash_prefix(options, :action_suffix)
prefix = options[:action_prefix] || action_prefix || ""
suffix = options[:action] == "index" ? "" : (options[:action_suffix] || action_suffix || "")
name = (options[:action] == "index" ? "" : options[:action]) || ""
return prefix + name + suffix
end
def controller_name(options, controller_prefix)
options[:controller_prefix] = "#{options[:module]}/#{options[:controller_prefix]}" if options[:module]
ensure_slash_suffix(options, :controller_prefix)
controller_name = options[:controller_prefix] || controller_prefix || ""
controller_name << (options[:controller] + "/") if options[:controller]
return controller_name
end
def path_params_in_list(options)
options[:path_params].inject("") { |path, pair| path += "/#{pair.last}" }
end
def ensure_slash_suffix(options, key)
options[key] = options[key] + "/" if options[key] && !options[key].empty? && options[key][-1..-1] != "/"
end
def ensure_slash_prefix(options, key)
options[key] = "/" + options[key] if options[key] && !options[key].empty? && options[key][0..1] != "/"
end
def include_id_in_path_params(options)
options[:path_params] = (options[:path_params] || {}).merge({"id" => options[:id]}) if options[:id]
end
def new_parameters(options)
parameters = options[:params] || existing_parameters
parameters.update(options[:overwrite_params]) if options[:overwrite_params]
parameters.reject { |key,value| value.nil? }
end
def existing_parameters
@request.parameters.reject { |key, value| %w( id action controller).include?(key) }
end
# Returns a query string with escaped keys and values from the passed hash. If the passed hash contains an "id" it'll
# be added as a path element instead of a regular parameter pair.
def build_query_string(hash)
elements = []
query_string = ""
hash.each { |key, value| elements << "#{CGI.escape(key)}=#{CGI.escape(value.to_s)}" }
unless elements.empty? then query_string << ("?" + elements.join("&")) end
return query_string
end
end
end

View File

@ -0,0 +1,49 @@
#--
# Copyright (c) 2004 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
begin
require 'rubygems'
require 'builder'
rescue LoadError
# RubyGems is not available, use included Builder
$:.unshift(File.dirname(__FILE__) + "/action_view/vendor")
require 'action_view/vendor/builder'
ensure
# Temporary patch until it's in Builder 1.2.2
class BlankSlate
class << self
def hide(name)
undef_method name if instance_methods.include?(name) and name !~ /^(__|instance_eval)/
end
end
end
end
require 'action_view/base'
require 'action_view/partials'
ActionView::Base.class_eval do
include ActionView::Partials
end
ActionView::Base.load_helpers(File.dirname(__FILE__) + "/action_view/helpers/")

View File

@ -0,0 +1,264 @@
require 'erb'
module ActionView #:nodoc:
class ActionViewError < StandardError #:nodoc:
end
# Action View templates can be written in two ways. If the template file has a +.rhtml+ extension then it uses a mixture of ERb
# (included in Ruby) and HTML. If the template file has a +.rxml+ extension then Jim Weirich's Builder::XmlMarkup library is used.
#
# = ERb
#
# You trigger ERb by using embeddings such as <% %> and <%= %>. The difference is whether you want output or not. Consider the
# following loop for names:
#
# <b>Names of all the people</b>
# <% for person in @people %>
# Name: <%= person.name %><br/>
# <% end %>
#
# The loop is setup in regular embedding tags (<% %>) and the name is written using the output embedding tag (<%= %>). Note that this
# is not just a usage suggestion. Regular output functions like print or puts won't work with ERb templates. So this would be wrong:
#
# Hi, Mr. <% puts "Frodo" %>
#
# (If you absolutely must write from within a function, you can use the TextHelper#concat)
#
# == Using sub templates
#
# Using sub templates allows you to sidestep tedious replication and extract common display structures in shared templates. The
# classic example is the use of a header and footer (even though the Action Pack-way would be to use Layouts):
#
# <%= render "shared/header" %>
# Something really specific and terrific
# <%= render "shared/footer" %>
#
# As you see, we use the output embeddings for the render methods. The render call itself will just return a string holding the
# result of the rendering. The output embedding writes it to the current template.
#
# But you don't have to restrict yourself to static includes. Templates can share variables amongst themselves by using instance
# variables defined in using the regular embedding tags. Like this:
#
# <% @page_title = "A Wonderful Hello" %>
# <%= render "shared/header" %>
#
# Now the header can pick up on the @page_title variable and use it for outputting a title tag:
#
# <title><%= @page_title %></title>
#
# == Passing local variables to sub templates
#
# You can pass local variables to sub templates by using a hash with the variable names as keys and the objects as values:
#
# <%= render "shared/header", { "headline" => "Welcome", "person" => person } %>
#
# These can now be accessed in shared/header with:
#
# Headline: <%= headline %>
# First name: <%= person.first_name %>
#
# == Template caching
#
# The parsing of ERb templates are cached by default, but the reading of them are not. This means that the application by default
# will reflect changes to the templates immediatly. If you'd like to sacrifice that immediacy for the speed gain given by also
# caching the loading of templates (reading from the file systen), you can turn that on with
# <tt>ActionView::Base.cache_template_loading = true</tt>.
#
# == Builder
#
# Builder templates are a more programatic alternative to ERb. They are especially useful for generating XML content. An +XmlMarkup+ object
# named +xml+ is automatically made available to templates with a +.rxml+ extension.
#
# Here are some basic examples:
#
# xml.em("emphasized") # => <em>emphasized</em>
# xml.em { xml.b("emp & bold") } # => <em><b>emph &amp; bold</b></em>
# xml.a("A Link", "href"=>"http://onestepback.org") # => <a href="http://onestepback.org">A Link</a>
# xm.target("name"=>"compile", "option"=>"fast") # => <target option="fast" name="compile"\>
# # NOTE: order of attributes is not specified.
#
# Any method with a block will be treated as an XML markup tag with nested markup in the block. For example, the following:
#
# xml.div {
# xml.h1(@person.name)
# xml.p(@person.bio)
# }
#
# would produce something like:
#
# <div>
# <h1>David Heinemeier Hansson</h1>
# <p>A product of Danish Design during the Winter of '79...</p>
# </div>
#
# A full-length RSS example actually used on Basecamp:
#
# xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do
# xml.channel do
# xml.title(@feed_title)
# xml.link(@url)
# xml.description "Basecamp: Recent items"
# xml.language "en-us"
# xml.ttl "40"
#
# for item in @recent_items
# xml.item do
# xml.title(item_title(item))
# xml.description(item_description(item)) if item_description(item)
# xml.pubDate(item_pubDate(item))
# xml.guid(@person.firm.account.url + @recent_items.url(item))
# xml.link(@person.firm.account.url + @recent_items.url(item))
#
# xml.tag!("dc:creator", item.author_name) if item_has_creator?(item)
# end
# end
# end
# end
#
# More builder documentation can be found at http://builder.rubyforge.org.
class Base
include ERB::Util
attr_reader :first_render
attr_accessor :base_path, :assigns, :template_extension
attr_accessor :controller
# Turn on to cache the reading of templates from the file system. Doing so means that you have to restart the server
# when changing templates, but that rendering will be faster.
@@cache_template_loading = false
cattr_accessor :cache_template_loading
@@compiled_erb_templates = {}
@@loaded_templates = {}
def self.load_helpers(helper_dir)#:nodoc:
Dir.foreach(helper_dir) do |helper_file|
next unless helper_file =~ /_helper.rb$/
require helper_dir + helper_file
helper_module_name = helper_file.capitalize.gsub(/_([a-z])/) { |m| $1.capitalize }[0..-4]
class_eval("include ActionView::Helpers::#{helper_module_name}") if Helpers.const_defined?(helper_module_name)
end
end
def self.controller_delegate(*methods)
methods.flatten.each do |method|
class_eval <<-end_eval
def #{method}(*args, &block)
controller.send(%(#{method}), *args, &block)
end
end_eval
end
end
def initialize(base_path = nil, assigns_for_first_render = {}, controller = nil)#:nodoc:
@base_path, @assigns = base_path, assigns_for_first_render
@controller = controller
end
# Renders the template present at <tt>template_path</tt>. If <tt>use_full_path</tt> is set to true,
# it's relative to the template_root, otherwise it's absolute. The hash in <tt>local_assigns</tt>
# is made available as local variables.
def render_file(template_path, use_full_path = true, local_assigns = {})
@first_render = template_path if @first_render.nil?
if use_full_path
template_extension = pick_template_extension(template_path)
template_file_name = full_template_path(template_path, template_extension)
else
template_file_name = template_path
template_extension = template_path.split(".").last
end
template_source = read_template_file(template_file_name)
begin
render_template(template_extension, template_source, local_assigns)
rescue Exception => e
if TemplateError === e
e.sub_template_of(template_file_name)
raise e
else
raise TemplateError.new(@base_path, template_file_name, @assigns, template_source, e)
end
end
end
# Renders the template present at <tt>template_path</tt> (relative to the template_root).
# The hash in <tt>local_assigns</tt> is made available as local variables.
def render(template_path, local_assigns = {})
render_file(template_path, true, local_assigns)
end
# Renders the +template+ which is given as a string as either rhtml or rxml depending on <tt>template_extension</tt>.
# The hash in <tt>local_assigns</tt> is made available as local variables.
def render_template(template_extension, template, local_assigns = {})
b = binding
local_assigns.each { |key, value| eval "#{key} = local_assigns[\"#{key}\"]", b }
@assigns.each { |key, value| instance_variable_set "@#{key}", value }
xml = Builder::XmlMarkup.new(:indent => 2)
send(pick_rendering_method(template_extension), template, binding)
end
def pick_template_extension(template_path)#:nodoc:
if erb_template_exists?(template_path)
"rhtml"
elsif builder_template_exists?(template_path)
"rxml"
else
raise ActionViewError, "No rhtml or rxml template found for #{template_path}"
end
end
def pick_rendering_method(template_extension)#:nodoc:
(template_extension == "rxml" ? "rxml" : "rhtml") + "_render"
end
def erb_template_exists?(template_path)#:nodoc:
template_exists?(template_path, "rhtml")
end
def builder_template_exists?(template_path)#:nodoc:
template_exists?(template_path, "rxml")
end
def file_exists?(template_path)#:nodoc:
erb_template_exists?(template_path) || builder_template_exists?(template_path)
end
# Returns true is the file may be rendered implicitly.
def file_public?(template_path)#:nodoc:
template_path.split("/").last[0,1] != "_"
end
private
def full_template_path(template_path, extension)
"#{@base_path}/#{template_path}.#{extension}"
end
def template_exists?(template_path, extension)
FileTest.exists?(full_template_path(template_path, extension))
end
def read_template_file(template_path)
unless cache_template_loading && @@loaded_templates[template_path]
@@loaded_templates[template_path] = File.read(template_path)
end
@@loaded_templates[template_path]
end
def rhtml_render(template, binding)
@@compiled_erb_templates[template] ||= ERB.new(template)
@@compiled_erb_templates[template].result(binding)
end
def rxml_render(template, binding)
@controller.headers["Content-Type"] ||= 'text/xml'
eval(template, binding)
end
end
end
require 'action_view/template_error'

View File

@ -0,0 +1,171 @@
require 'cgi'
require File.dirname(__FILE__) + '/form_helper'
module ActionView
class Base
@@field_error_proc = Proc.new{ |html_tag, instance| "<div class=\"fieldWithErrors\">#{html_tag}</div>" }
cattr_accessor :field_error_proc
end
module Helpers
# The Active Record Helper makes it easier to create forms for records kept in instance variables. The most far-reaching is the form
# method that creates a complete form for all the basic content types of the record (not associations or aggregations, though). This
# is a great of making the record quickly available for editing, but likely to prove lacklusters for a complicated real-world form.
# In that case, it's better to use the input method and the specialized form methods in link:classes/ActionView/Helpers/FormHelper.html
module ActiveRecordHelper
# Returns a default input tag for the type of object returned by the method. Example
# (title is a VARCHAR column and holds "Hello World"):
# input("post", "title") =>
# <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" />
def input(record_name, method)
InstanceTag.new(record_name, method, self).to_tag
end
# Returns an entire form with input tags and everything for a specified Active Record object. Example
# (post is a new record that has a title using VARCHAR and a body using TEXT):
# form("post") =>
# <form action='create' method='POST'>
# <p>
# <label for="post_title">Title</label><br />
# <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" />
# </p>
# <p>
# <label for="post_body">Body</label><br />
# <textarea cols="40" id="post_body" name="post[body]" rows="20" wrap="virtual">
# Back to the hill and over it again!
# </textarea>
# </p>
# <input type='submit' value='Create' />
# </form>
#
# It's possible to specialize the form builder by using a different action name and by supplying another
# block renderer. Example (entry is a new record that has a message attribute using VARCHAR):
#
# form("entry", :action => "sign", :input_block =>
# Proc.new { |record, column| "#{column.human_name}: #{input(record, column.name)}<br />" }) =>
#
# <form action='sign' method='POST'>
# Message:
# <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" /><br />
# <input type='submit' value='Sign' />
# </form>
def form(record_name, options = {})
record = instance_eval("@#{record_name}")
action = options[:action] || (record.new_record? ? "create" : "update")
id_field = record.new_record? ? "" : InstanceTag.new(record_name, "id", self).to_input_field_tag("hidden")
"<form action='#{action}' method='POST'>" +
id_field + all_input_tags(record, record_name, options) +
"<input type='submit' value='#{action.gsub(/[^A-Za-z]/, "").capitalize}' />" +
"</form>"
end
# Returns a string containing the error message attached to the +method+ on the +object+, if one exists.
# This error message is wrapped in a DIV tag, which can be specialized to include both a +prepend_text+ and +append_text+
# to properly introduce the error and a +css_class+ to style it accordingly. Examples (post has an error message
# "can't be empty" on the title attribute):
#
# <%= error_message_on "post", "title" %> =>
# <div class="formError">can't be empty</div>
#
# <%= error_message_on "post", "title", "Title simply ", " (or it won't work)", "inputError" %> =>
# <div class="inputError">Title simply can't be empty (or it won't work)</div>
def error_message_on(object, method, prepend_text = "", append_text = "", css_class = "formError")
if errors = instance_eval("@#{object}").errors.on(method)
"<div class=\"#{css_class}\">#{prepend_text + (errors.is_a?(Array) ? errors.first : errors) + append_text}</div>"
end
end
def error_messages_for(object_name)
object = instance_eval("@#{object_name}")
unless object.errors.empty?
"<div id=\"errorExplanation\">" +
"<h2>#{object.errors.count} error#{"s" unless object.errors.count == 1} prohibited this #{object_name.gsub("_", " ")} from being saved</h2>" +
"<p>There were problems with the following fields (marked in red below):</p>" +
"<ul>#{object.errors.full_messages.collect { |msg| "<li>#{msg}</li>"}}</ul>" +
"</div>"
end
end
private
def all_input_tags(record, record_name, options)
input_block = options[:input_block] || default_input_block
record.class.content_columns.collect{ |column| input_block.call(record_name, column) }.join("\n")
end
def default_input_block
Proc.new { |record, column| "<p><label for=\"#{record}_#{column.name}\">#{column.human_name}</label><br />#{input(record, column.name)}</p>" }
end
end
class InstanceTag #:nodoc:
def to_tag(options = {})
case column_type
when :string
field_type = @method_name.include?("password") ? "password" : "text"
to_input_field_tag(field_type, options)
when :text
to_text_area_tag(options)
when :integer, :float
to_input_field_tag("text", options)
when :date
to_date_select_tag(options)
when :datetime
to_datetime_select_tag(options)
when :boolean
to_boolean_select_tag(options)
end
end
alias_method :tag_without_error_wrapping, :tag
def tag(name, options)
if object.respond_to?("errors") && object.errors.respond_to?("on")
error_wrapping(tag_without_error_wrapping(name, options), object.errors.on(@method_name))
else
tag_without_error_wrapping(name, options)
end
end
alias_method :content_tag_without_error_wrapping, :content_tag
def content_tag(name, value, options)
if object.respond_to?("errors") && object.errors.respond_to?("on")
error_wrapping(content_tag_without_error_wrapping(name, value, options), object.errors.on(@method_name))
else
content_tag_without_error_wrapping(name, value, options)
end
end
alias_method :to_date_select_tag_without_error_wrapping, :to_date_select_tag
def to_date_select_tag(options = {})
if object.respond_to?("errors") && object.errors.respond_to?("on")
error_wrapping(to_date_select_tag_without_error_wrapping(options), object.errors.on(@method_name))
else
to_date_select_tag_without_error_wrapping(options)
end
end
alias_method :to_datetime_select_tag_without_error_wrapping, :to_datetime_select_tag
def to_datetime_select_tag(options = {})
if object.respond_to?("errors") && object.errors.respond_to?("on")
error_wrapping(to_datetime_select_tag_without_error_wrapping(options), object.errors.on(@method_name))
else
to_datetime_select_tag_without_error_wrapping(options)
end
end
def error_wrapping(html_tag, has_error)
has_error ? Base.field_error_proc.call(html_tag, self) : html_tag
end
def error_message
object.errors.on(@method_name)
end
def column_type
object.send("column_for_attribute", @method_name).type
end
end
end
end

View File

@ -0,0 +1,230 @@
require "date"
module ActionView
module Helpers
# The Date Helper primarily creates select/option tags for different kinds of dates and date elements. All of the select-type methods
# share a number of common options that are as follows:
#
# * <tt>:prefix</tt> - overwrites the default prefix of "date" used for the select names. So specifying "birthday" would give
# birthday[month] instead of date[month] if passed to the select_month method.
# * <tt>:include_blank</tt> - set to true if it should be possible to set an empty date.
# * <tt>:discard_type</tt> - set to true if you want to discard the type part of the select name. If set to true, the select_month
# method would use simply "date" (which can be overwritten using <tt>:prefix</tt>) instead of "date[month]".
module DateHelper
DEFAULT_PREFIX = "date" unless const_defined?("DEFAULT_PREFIX")
# Reports the approximate distance in time between to Time objects. For example, if the distance is 47 minutes, it'll return
# "about 1 hour". See the source for the complete wording list.
def distance_of_time_in_words(from_time, to_time)
distance_in_minutes = ((to_time - from_time) / 60).round
case distance_in_minutes
when 0 then "less than a minute"
when 1 then "1 minute"
when 2..45 then "#{distance_in_minutes} minutes"
when 46..90 then "about 1 hour"
when 90..1440 then "about #{(distance_in_minutes.to_f / 60.0).round} hours"
when 1441..2880 then "1 day"
else "#{(distance_in_minutes / 1440).round} days"
end
end
# Like distance_of_time_in_words, but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>.
def distance_of_time_in_words_to_now(from_time)
distance_of_time_in_words(from_time, Time.now)
end
# Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based attribute (identified by
# +method+) on an object assigned to the template (identified by +object+). It's possible to tailor the selects through the +options+ hash,
# which both accepts all the keys that each of the individual select builders does (like :use_month_numbers for select_month) and a range
# of discard options. The discard options are <tt>:discard_month</tt> and <tt>:discard_day</tt>. Set to true, they'll drop the respective
# select. Discarding the month select will also automatically discard the day select.
#
# NOTE: Discarded selects will default to 1. So if no month select is available, January will be assumed. Additionally, you can get the
# month select before the year by setting :month_before_year to true in the options. This is especially useful for credit card forms.
# Examples:
#
# date_select("post", "written_on")
# date_select("post", "written_on", :start_year => 1995)
# date_select("post", "written_on", :start_year => 1995, :use_month_numbers => true,
# :discard_day => true, :include_blank => true)
#
# The selects are prepared for multi-parameter assignment to an Active Record object.
def date_select(object, method, options = {})
InstanceTag.new(object, method, self).to_date_select_tag(options)
end
# Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a specified datetime-based
# attribute (identified by +method+) on an object assigned to the template (identified by +object+). Examples:
#
# datetime_select("post", "written_on")
# datetime_select("post", "written_on", :start_year => 1995)
#
# The selects are prepared for multi-parameter assignment to an Active Record object.
def datetime_select(object, method, options = {})
InstanceTag.new(object, method, self).to_datetime_select_tag(options)
end
# Returns a set of html select-tags (one for year, month, and day) pre-selected with the +date+.
def select_date(date = Date.today, options = {})
select_year(date, options) + select_month(date, options) + select_day(date, options)
end
# Returns a set of html select-tags (one for year, month, day, hour, and minute) preselected the +datetime+.
def select_datetime(datetime = Time.now, options = {})
select_year(datetime, options) + select_month(datetime, options) + select_day(datetime, options) +
select_hour(datetime, options) + select_minute(datetime, options)
end
# Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected.
# The <tt>minute</tt> can also be substituted for a minute number.
def select_minute(datetime, options = {})
minute_options = []
0.upto(59) do |minute|
minute_options << ((datetime.kind_of?(Fixnum) ? datetime : datetime.min) == minute ?
"<option selected=\"selected\">#{leading_zero_on_single_digits(minute)}</option>\n" :
"<option>#{leading_zero_on_single_digits(minute)}</option>\n"
)
end
select_html("minute", minute_options, options[:prefix], options[:include_blank], options[:discard_type])
end
# Returns a select tag with options for each of the hours 0 through 23 with the current hour selected.
# The <tt>hour</tt> can also be substituted for a hour number.
def select_hour(datetime, options = {})
hour_options = []
0.upto(23) do |hour|
hour_options << ((datetime.kind_of?(Fixnum) ? datetime : datetime.hour) == hour ?
"<option selected=\"selected\">#{leading_zero_on_single_digits(hour)}</option>\n" :
"<option>#{leading_zero_on_single_digits(hour)}</option>\n"
)
end
select_html("hour", hour_options, options[:prefix], options[:include_blank], options[:discard_type])
end
# Returns a select tag with options for each of the days 1 through 31 with the current day selected.
# The <tt>date</tt> can also be substituted for a hour number.
def select_day(date, options = {})
day_options = []
1.upto(31) do |day|
day_options << ((date.kind_of?(Fixnum) ? date : date.day) == day ?
"<option selected=\"selected\">#{day}</option>\n" :
"<option>#{day}</option>\n"
)
end
select_html("day", day_options, options[:prefix], options[:include_blank], options[:discard_type])
end
# Returns a select tag with options for each of the months January through December with the current month selected.
# The month names are presented as keys (what's shown to the user) and the month numbers (1-12) are used as values
# (what's submitted to the server). It's also possible to use month numbers for the presentation instead of names --
# set the <tt>:use_month_numbers</tt> key in +options+ to true for this to happen. If you want both numbers and names,
# set the <tt>:add_month_numbers</tt> key in +options+ to true. Examples:
#
# select_month(Date.today) # Will use keys like "January", "March"
# select_month(Date.today, :use_month_numbers => true) # Will use keys like "1", "3"
# select_month(Date.today, :add_month_numbers => true) # Will use keys like "1 - January", "3 - March"
def select_month(date, options = {})
month_options = []
1.upto(12) do |month_number|
month_name = if options[:use_month_numbers]
month_number
elsif options[:add_month_numbers]
month_number.to_s + " - " + Date::MONTHNAMES[month_number]
else
Date::MONTHNAMES[month_number]
end
month_options << ((date.kind_of?(Fixnum) ? date : date.month) == month_number ?
"<option value='#{month_number}' selected=\"selected\">#{month_name}</option>\n" :
"<option value='#{month_number}'>#{month_name}</option>\n"
)
end
select_html("month", month_options, options[:prefix], options[:include_blank], options[:discard_type])
end
# Returns a select tag with options for each of the five years on each side of the current, which is selected. The five year radius
# can be changed using the <tt>:start_year</tt> and <tt>:end_year</tt> keys in the +options+. The <tt>date</tt> can also be substituted
# for a year given as a number. Example:
#
# select_year(Date.today, :start_year => 1992, :end_year => 2007)
def select_year(date, options = {})
year_options = []
unless date.kind_of?(Fixnum) then default_start_year, default_end_year = date.year - 5, date.year + 5 end
(options[:start_year] || default_start_year).upto(options[:end_year] || default_end_year) do |year|
year_options << ((date.kind_of?(Fixnum) ? date : date.year) == year ?
"<option selected=\"selected\">#{year}</option>\n" :
"<option>#{year}</option>\n"
)
end
select_html("year", year_options, options[:prefix], options[:include_blank], options[:discard_type])
end
private
def select_html(type, options, prefix = nil, include_blank = false, discard_type = false)
select_html = "<select name='#{prefix || DEFAULT_PREFIX}"
select_html << "[#{type}]" unless discard_type
select_html << "'>\n"
select_html << "<option></option>\n" if include_blank
select_html << options.to_s
select_html << "</select>\n"
return select_html
end
def leading_zero_on_single_digits(number)
number > 9 ? number : "0#{number}"
end
end
class InstanceTag #:nodoc:
include DateHelper
def to_date_select_tag(options = {})
defaults = { :discard_type => true }
options = defaults.merge(options)
options_with_prefix = Proc.new { |position| options.update({ :prefix => "#{@object_name}[#{@method_name}(#{position}i)]" }) }
date = options[:include_blank] ? (value || 0) : (value || Date.today)
date_select = ""
if options[:month_before_year]
date_select << select_month(date, options_with_prefix.call(2)) unless options[:discard_month]
date_select << select_year(date, options_with_prefix.call(1))
else
date_select << select_year(date, options_with_prefix.call(1))
date_select << select_month(date, options_with_prefix.call(2)) unless options[:discard_month]
end
date_select << select_day(date, options_with_prefix.call(3)) unless options[:discard_day] || options[:discard_month]
return date_select
end
def to_datetime_select_tag(options = {})
defaults = { :discard_type => true }
options = defaults.merge(options)
options_with_prefix = Proc.new { |position| options.update({ :prefix => "#{@object_name}[#{@method_name}(#{position}i)]" }) }
datetime = options[:include_blank] ? (value || 0) : (value || Time.now)
datetime_select = select_year(datetime, options_with_prefix.call(1))
datetime_select << select_month(datetime, options_with_prefix.call(2)) unless options[:discard_month]
datetime_select << select_day(datetime, options_with_prefix.call(3)) unless options[:discard_day] || options[:discard_month]
datetime_select << " &mdash; " + select_hour(datetime, options_with_prefix.call(4)) unless options[:discard_hour]
datetime_select << " : " + select_minute(datetime, options_with_prefix.call(5)) unless options[:discard_minute] || options[:discard_hour]
return datetime_select
end
end
end
end

View File

@ -0,0 +1,17 @@
module ActionView
module Helpers
# Provides a set of methods for making it easier to locate problems.
module DebugHelper
# Returns a <pre>-tag set with the +object+ dumped by YAML. Very readable way to inspect an object.
def debug(object)
begin
Marshal::dump(object)
"<pre class='debug_dump'>#{h(object.to_yaml).gsub(" ", "&nbsp; ")}</pre>"
rescue Object => e
# Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback
"<code class='debug_dump'>#{h(object.inspect)}</code>"
end
end
end
end
end

View File

@ -0,0 +1,182 @@
require 'cgi'
require File.dirname(__FILE__) + '/date_helper'
require File.dirname(__FILE__) + '/tag_helper'
module ActionView
module Helpers
# Provides a set of methods for working with forms and especially forms related to objects assigned to the template.
# The following is an example of a complete form for a person object that works for both creates and updates built
# with all the form helpers. The <tt>@person</tt> object was assigned by an action on the controller:
# <form action="save_person" method="post">
# Name:
# <%= text_field "person", "name", "size" => 20 %>
#
# Password:
# <%= password_field "person", "password", "maxsize" => 20 %>
#
# Single?:
# <%= check_box "person", "single" %>
#
# Description:
# <%= text_area "person", "description", "cols" => 20 %>
#
# <input type="submit" value="Save">
# </form>
#
# ...is compiled to:
#
# <form action="save_person" method="post">
# Name:
# <input type="text" id="person_name" name="person[name]"
# size="20" value="<%= @person.name %>" />
#
# Password:
# <input type="password" id="person_password" name="person[password]"
# size="20" maxsize="20" value="<%= @person.password %>" />
#
# Single?:
# <input type="checkbox" id="person_single" name="person[single] value="1" />
#
# Description:
# <textarea cols="20" rows="40" id="person_description" name="person[description]">
# <%= @person.description %>
# </textarea>
#
# <input type="submit" value="Save">
# </form>
#
# There's also methods for helping to build form tags in link:classes/ActionView/Helpers/FormOptionsHelper.html,
# link:classes/ActionView/Helpers/DateHelper.html, and link:classes/ActionView/Helpers/ActiveRecordHelper.html
module FormHelper
# Returns an input tag of the "text" type tailored for accessing a specified attribute (identified by +method+) on an object
# assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
# hash with +options+.
#
# Examples (call, result):
# text_field("post", "title", "size" => 20)
# <input type="text" id="post_title" name="post[title]" size="20" value="#{@post.title}" />
def text_field(object, method, options = {})
InstanceTag.new(object, method, self).to_input_field_tag("text", options)
end
# Works just like text_field, but returns a input tag of the "password" type instead.
def password_field(object, method, options = {})
InstanceTag.new(object, method, self).to_input_field_tag("password", options)
end
# Works just like text_field, but returns a input tag of the "hidden" type instead.
def hidden_field(object, method, options = {})
InstanceTag.new(object, method, self).to_input_field_tag("hidden", options)
end
# Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+)
# on an object assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
# hash with +options+.
#
# Example (call, result):
# text_area("post", "body", "cols" => 20, "rows" => 40)
# <textarea cols="20" rows="40" id="post_body" name="post[body]">
# #{@post.body}
# </textarea>
def text_area(object, method, options = {})
InstanceTag.new(object, method, self).to_text_area_tag(options)
end
# Returns a checkbox tag tailored for accessing a specified attribute (identified by +method+) on an object
# assigned to the template (identified by +object+). It's intended that +method+ returns an integer and if that
# integer is above zero, then the checkbox is checked. Additional options on the input tag can be passed as a
# hash with +options+. The +checked_value+ defaults to 1 while the default +unchecked_value+
# is set to 0 which is convenient for boolean values. Usually unchecked checkboxes don't post anything.
# We work around this problem by adding a hidden value with the same name as the checkbox.
#
# Example (call, result). Imagine that @post.validated? returns 1:
# check_box("post", "validated")
# <input type="checkbox" id="post_validate" name="post[validated] value="1" checked="checked" /><input name="post[validated]" type="hidden" value="0" />
#
# Example (call, result). Imagine that @puppy.gooddog returns no:
# check_box("puppy", "gooddog", {}, "yes", "no")
# <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog] value="yes" /><input name="puppy[gooddog]" type="hidden" value="no" />
def check_box(object, method, options = {}, checked_value = "1", unchecked_value = "0")
InstanceTag.new(object, method, self).to_check_box_tag(options, checked_value, unchecked_value)
end
end
class InstanceTag #:nodoc:
include Helpers::TagHelper
attr_reader :method_name, :object_name
DEFAULT_FIELD_OPTIONS = { "size" => 30 } unless const_defined?("DEFAULT_FIELD_OPTIONS")
DEFAULT_TEXT_AREA_OPTIONS = { "wrap" => "virtual", "cols" => 40, "rows" => 20 } unless const_defined?("DEFAULT_TEXT_AREA_OPTIONS")
def initialize(object_name, method_name, template_object, local_binding = nil)
@object_name, @method_name = object_name, method_name
@template_object, @local_binding = template_object, local_binding
end
def to_input_field_tag(field_type, options = {})
html_options = DEFAULT_FIELD_OPTIONS.merge(options)
html_options.merge!({ "size" => options["maxlength"]}) if options["maxlength"] && !options["size"]
html_options.merge!({ "type" => field_type, "value" => value.to_s })
add_default_name_and_id(html_options)
tag("input", html_options)
end
def to_text_area_tag(options = {})
options = DEFAULT_TEXT_AREA_OPTIONS.merge(options)
add_default_name_and_id(options)
content_tag("textarea", html_escape(value), options)
end
def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0")
options.merge!({"checked" => "checked"}) if !value.nil? && ((value.is_a?(TrueClass) || value.is_a?(FalseClass)) ? value : value.to_i > 0)
options.merge!({ "type" => "checkbox", "value" => checked_value })
add_default_name_and_id(options)
tag("input", options) << tag("input", ({ "name" => options['name'], "type" => "hidden", "value" => unchecked_value }))
end
def to_date_tag()
defaults = { "discard_type" => true }
date = value || Date.today
options = Proc.new { |position| defaults.update({ :prefix => "#{@object_name}[#{@method_name}(#{position}i)]" }) }
html_day_select(date, options.call(3)) +
html_month_select(date, options.call(2)) +
html_year_select(date, options.call(1))
end
def to_boolean_select_tag(options = {})
add_default_name_and_id(options)
tag_text = "<select"
tag_text << tag_options(options)
tag_text << "><option value=\"false\""
tag_text << " selected" if value == false
tag_text << ">False</option><option value=\"true\""
tag_text << " selected" if value
tag_text << ">True</option></select>"
end
def object
@template_object.instance_variable_get "@#{@object_name}"
end
def value
object.send(@method_name) unless object.nil?
end
private
def add_default_name_and_id(options)
options['name'] = tag_name unless options.has_key? "name"
options['id'] = tag_id unless options.has_key? "id"
end
def tag_name
"#{@object_name}[#{@method_name}]"
end
def tag_id
"#{@object_name}_#{@method_name}"
end
end
end
end

View File

@ -0,0 +1,212 @@
require 'cgi'
require 'erb'
require File.dirname(__FILE__) + '/form_helper'
module ActionView
module Helpers
# Provides a number of methods for turning different kinds of containers into a set of option tags. Neither of the methods provide
# the actual select tag, so you'll need to construct that in HTML manually.
module FormOptionsHelper
include ERB::Util
def select(object, method, choices, options = {}, html_options = {})
InstanceTag.new(object, method, self).to_select_tag(choices, options, html_options)
end
def collection_select(object, method, collection, value_method, text_method, options = {}, html_options = {})
InstanceTag.new(object, method, self).to_collection_select_tag(collection, value_method, text_method, options, html_options)
end
def country_select(object, method, priority_countries = nil, options = {}, html_options = {})
InstanceTag.new(object, method, self).to_country_select_tag(priority_countries, options, html_options)
end
# Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container
# where the elements respond to first and last (such as a two-element array), the "lasts" serve as option values and
# the "firsts" as option text. Hashes are turned into this form automatically, so the keys become "firsts" and values
# become lasts. If +selected+ is specified, the matching "last" or element will get the selected option-tag. +Selected+
# may also be an array of values to be selected when using a multiple select.
#
# Examples (call, result):
# options_for_select([["Dollar", "$"], ["Kroner", "DKK"]])
# <option value="$">Dollar</option>\n<option value="DKK">Kroner</option>
#
# options_for_select([ "VISA", "Mastercard" ], "Mastercard")
# <option>VISA</option>\n<option selected="selected">Mastercard</option>
#
# options_for_select({ "Basic" => "$20", "Plus" => "$40" }, "$40")
# <option value="$20">Basic</option>\n<option value="$40" selected="selected">Plus</option>
#
# options_for_select([ "VISA", "Mastercard", "Discover" ], ["VISA", "Discover"])
# <option selected="selected">VISA</option>\n<option>Mastercard</option>\n<option selected="selected">Discover</option>
def options_for_select(container, selected = nil)
container = container.to_a if Hash === container
options_for_select = container.inject([]) do |options, element|
if element.respond_to?(:first) && element.respond_to?(:last)
is_selected = ( (selected.respond_to?(:include?) ? selected.include?(element.last) : element.last == selected) )
if is_selected
options << "<option value=\"#{html_escape(element.last.to_s)}\" selected=\"selected\">#{html_escape(element.first.to_s)}</option>"
else
options << "<option value=\"#{html_escape(element.last.to_s)}\">#{html_escape(element.first.to_s)}</option>"
end
else
is_selected = ( (selected.respond_to?(:include?) ? selected.include?(element) : element == selected) )
options << ((is_selected) ? "<option selected=\"selected\">#{html_escape(element.to_s)}</option>" : "<option>#{html_escape(element.to_s)}</option>")
end
end
options_for_select.join("\n")
end
# Returns a string of option tags that has been compiled by iterating over the +collection+ and assigning the
# the result of a call to the +value_method+ as the option value and the +text_method+ as the option text.
# If +selected_value+ is specified, the element returning a match on +value_method+ will get the selected option tag.
#
# Example (call, result). Imagine a loop iterating over each +person+ in <tt>@project.people</tt> to generate a input tag:
# options_from_collection_for_select(@project.people, "id", "name")
# <option value="#{person.id}">#{person.name}</option>
def options_from_collection_for_select(collection, value_method, text_method, selected_value = nil)
options_for_select(
collection.inject([]) { |options, object| options << [ object.send(text_method), object.send(value_method) ] },
selected_value
)
end
# Returns a string of option tags, like options_from_collection_for_select, but surrounds them by <optgroup> tags.
#
# An array of group objects are passed. Each group should return an array of options when calling group_method
# Each group should should return its name when calling group_label_method.
#
# html_option_groups_from_collection(@continents, "countries", "contient_name", "country_id", "country_name", @selected_country.id)
#
# Could become:
# <optgroup label="Africa">
# <select>Egypt</select>
# <select>Rwanda</select>
# ...
# </optgroup>
# <optgroup label="Asia">
# <select>China</select>
# <select>India</select>
# <select>Japan</select>
# ...
# </optgroup>
#
# with objects of the following classes:
# class Continent
# def initialize(p_name, p_countries) @continent_name = p_name; @countries = p_countries; end
# def continent_name() @continent_name; end
# def countries() @countries; end
# end
# class Country
# def initialize(id, name) @id = id; @name = name end
# def country_id() @id; end
# def country_name() @name; end
# end
def option_groups_from_collection_for_select(collection, group_method, group_label_method,
option_key_method, option_value_method, selected_key = nil)
collection.inject("") do |options_for_select, group|
group_label_string = eval("group.#{group_label_method}")
options_for_select += "<optgroup label=\"#{html_escape(group_label_string)}\">"
options_for_select += options_from_collection_for_select(eval("group.#{group_method}"), option_key_method, option_value_method, selected_key)
options_for_select += '</optgroup>'
end
end
# Returns a string of option tags for pretty much any country in the world. Supply a country name as +selected+ to
# have it marked as the selected option tag. You can also supply an array of countries as +priority_countries+, so
# that they will be listed above the rest of the (long) list.
def country_options_for_select(selected = nil, priority_countries = nil)
country_options = ""
if priority_countries
country_options += options_for_select(priority_countries, selected)
country_options += "<option>-------------</option>\n"
end
if priority_countries && priority_countries.include?(selected)
country_options += options_for_select(COUNTRIES - priority_countries, selected)
else
country_options += options_for_select(COUNTRIES, selected)
end
return country_options
end
private
# All the countries included in the country_options output.
COUNTRIES = [ "Albania", "Algeria", "American Samoa", "Andorra", "Angola", "Anguilla",
"Antarctica", "Antigua And Barbuda", "Argentina", "Armenia", "Aruba", "Australia",
"Austria", "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus",
"Belgium", "Belize", "Benin", "Bermuda", "Bhutan", "Bolivia", "Bosnia and Herzegowina",
"Botswana", "Bouvet Island", "Brazil", "British Indian Ocean Territory",
"Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burma", "Burundi", "Cambodia",
"Cameroon", "Canada", "Cape Verde", "Cayman Islands", "Central African Republic",
"Chad", "Chile", "China", "Christmas Island", "Cocos (Keeling) Islands", "Colombia",
"Comoros", "Congo", "Congo, the Democratic Republic of the", "Cook Islands",
"Costa Rica", "Cote d'Ivoire", "Croatia", "Cyprus", "Czech Republic", "Denmark",
"Djibouti", "Dominica", "Dominican Republic", "East Timor", "Ecuador", "Egypt",
"El Salvador", "England", "Equatorial Guinea", "Eritrea", "Espana", "Estonia",
"Ethiopia", "Falkland Islands", "Faroe Islands", "Fiji", "Finland", "France",
"French Guiana", "French Polynesia", "French Southern Territories", "Gabon", "Gambia",
"Georgia", "Germany", "Ghana", "Gibraltar", "Great Britain", "Greece", "Greenland",
"Grenada", "Guadeloupe", "Guam", "Guatemala", "Guinea", "Guinea-Bissau", "Guyana",
"Haiti", "Heard and Mc Donald Islands", "Honduras", "Hong Kong", "Hungary", "Iceland",
"India", "Indonesia", "Ireland", "Israel", "Italy", "Jamaica", "Japan", "Jordan",
"Kazakhstan", "Kenya", "Kiribati", "Korea, Republic of", "Korea (South)", "Kuwait",
"Kyrgyzstan", "Lao People's Democratic Republic", "Latvia", "Lebanon", "Lesotho",
"Liberia", "Liechtenstein", "Lithuania", "Luxembourg", "Macau", "Macedonia",
"Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands",
"Martinique", "Mauritania", "Mauritius", "Mayotte", "Mexico",
"Micronesia, Federated States of", "Moldova, Republic of", "Monaco", "Mongolia",
"Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", "Nepal",
"Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", "Nicaragua",
"Niger", "Nigeria", "Niue", "Norfolk Island", "Northern Ireland",
"Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau", "Panama",
"Papua New Guinea", "Paraguay", "Peru", "Philippines", "Pitcairn", "Poland",
"Portugal", "Puerto Rico", "Qatar", "Reunion", "Romania", "Russia", "Rwanda",
"Saint Kitts and Nevis", "Saint Lucia", "Saint Vincent and the Grenadines",
"Samoa (Independent)", "San Marino", "Sao Tome and Principe", "Saudi Arabia",
"Scotland", "Senegal", "Seychelles", "Sierra Leone", "Singapore", "Slovakia",
"Slovenia", "Solomon Islands", "Somalia", "South Africa",
"South Georgia and the South Sandwich Islands", "South Korea", "Spain", "Sri Lanka",
"St. Helena", "St. Pierre and Miquelon", "Suriname", "Svalbard and Jan Mayen Islands",
"Swaziland", "Sweden", "Switzerland", "Taiwan", "Tajikistan", "Tanzania", "Thailand",
"Togo", "Tokelau", "Tonga", "Trinidad", "Trinidad and Tobago", "Tunisia", "Turkey",
"Turkmenistan", "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine",
"United Arab Emirates", "United Kingdom", "United States",
"United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu",
"Vatican City State (Holy See)", "Venezuela", "Viet Nam", "Virgin Islands (British)",
"Virgin Islands (U.S.)", "Wales", "Wallis and Futuna Islands", "Western Sahara",
"Yemen", "Zambia", "Zimbabwe" ] unless const_defined?("COUNTRIES")
end
class InstanceTag #:nodoc:
include FormOptionsHelper
def to_select_tag(choices, options, html_options)
add_default_name_and_id(html_options)
content_tag("select", add_blank_option(options_for_select(choices, value), options[:include_blank]), html_options)
end
def to_collection_select_tag(collection, value_method, text_method, options, html_options)
add_default_name_and_id(html_options)
content_tag(
"select", add_blank_option(options_from_collection_for_select(collection, value_method, text_method, value), options[:include_blank]), html_options
)
end
def to_country_select_tag(priority_countries, options, html_options)
add_default_name_and_id(html_options)
content_tag("select", add_blank_option(country_options_for_select(value, priority_countries), options[:include_blank]), html_options)
end
private
def add_blank_option(option_tags, add_blank)
add_blank ? "<option></option>\n" + option_tags : option_tags
end
end
end
end

View File

@ -0,0 +1,59 @@
require 'cgi'
module ActionView
module Helpers
# This is poor man's Builder for the rare cases where you need to programmatically make tags but can't use Builder.
module TagHelper
include ERB::Util
# Examples:
# * tag("br") => <br />
# * tag("input", { "type" => "text"}) => <input type="text" />
def tag(name, options = {}, open = false)
"<#{name + tag_options(options)}" + (open ? ">" : " />")
end
# Examples:
# * content_tag("p", "Hello world!") => <p>Hello world!</p>
# * content_tag("div", content_tag("p", "Hello world!"), "class" => "strong") =>
# <div class="strong"><p>Hello world!</p></div>
def content_tag(name, content, options = {})
"<#{name + tag_options(options)}>#{content}</#{name}>"
end
# Starts a form tag that points the action to an url configured with <tt>url_for_options</tt> just like
# ActionController::Base#url_for.
def form_tag(url_for_options, options = {}, *parameters_for_url)
html_options = { "method" => "POST" }.merge(options)
if html_options[:multipart]
html_options["enctype"] = "multipart/form-data"
html_options.delete(:multipart)
end
html_options["action"] = url_for(url_for_options, *parameters_for_url)
tag("form", html_options, true)
end
alias_method :start_form_tag, :form_tag
# Outputs "</form>"
def end_form_tag
"</form>"
end
private
def tag_options(options)
if options.empty?
""
else
" " + options.collect { |pair|
"#{pair.first}=\"#{html_escape(pair.last)}\""
}.sort.join(" ")
end
end
end
end
end

View File

@ -0,0 +1,111 @@
module ActionView
module Helpers #:nodoc:
# Provides a set of methods for working with text strings that can help unburden the level of inline Ruby code in the
# templates. In the example below we iterate over a collection of posts provided to the template and prints each title
# after making sure it doesn't run longer than 20 characters:
# <% for post in @posts %>
# Title: <%= truncate(post.title, 20) %>
# <% end %>
module TextHelper
# The regular puts and print are outlawed in eRuby. It's recommended to use the <%= "hello" %> form instead of print "hello".
# If you absolutely must use a method-based output, you can use concat. It's use like this <% concat "hello", binding %>. Notice that
# it doesn't have an equal sign in front. Using <%= concat "hello" %> would result in a double hello.
def concat(string, binding)
eval("_erbout", binding).concat(string)
end
# Truncates +text+ to the length of +length+ and replaces the last three characters with the +truncate_string+
# if the +text+ is longer than +length+.
def truncate(text, length = 30, truncate_string = "...")
if text.nil? then return end
if text.length > length then text[0..(length - 3)] + truncate_string else text end
end
# Highlights the +phrase+ where it is found in the +text+ by surrounding it like
# <strong class="highlight">I'm a highlight phrase</strong>. The highlighter can be specialized by
# passing +highlighter+ as single-quoted string with \1 where the phrase is supposed to be inserted.
# N.B.: The +phrase+ is sanitized to include only letters, digits, and spaces before use.
def highlight(text, phrase, highlighter = '<strong class="highlight">\1</strong>')
if text.nil? || phrase.nil? then return end
text.gsub(/(#{escape_regexp(phrase)})/i, highlighter) unless text.nil?
end
# Extracts an excerpt from the +text+ surrounding the +phrase+ with a number of characters on each side determined
# by +radius+. If the phrase isn't found, nil is returned. Ex:
# excerpt("hello my world", "my", 3) => "...lo my wo..."
def excerpt(text, phrase, radius = 100, excerpt_string = "...")
if text.nil? || phrase.nil? then return end
phrase = escape_regexp(phrase)
if found_pos = text =~ /(#{phrase})/i
start_pos = [ found_pos - radius, 0 ].max
end_pos = [ found_pos + phrase.length + radius, text.length ].min
prefix = start_pos > 0 ? excerpt_string : ""
postfix = end_pos < text.length ? excerpt_string : ""
prefix + text[start_pos..end_pos].strip + postfix
else
nil
end
end
# Attempts to pluralize the +singular+ word unless +count+ is 1. See source for pluralization rules.
def pluralize(count, singular, plural = nil)
"#{count} " + if count == 1
singular
elsif plural
plural
elsif Object.const_defined?("Inflector")
Inflector.pluralize(singular)
else
singular + "s"
end
end
begin
require "redcloth"
# Returns the text with all the Textile codes turned into HTML-tags.
# <i>This method is only available if RedCloth can be required</i>.
def textilize(text)
RedCloth.new(text).to_html
end
# Returns the text with all the Textile codes turned into HTML-tags, but without the regular bounding <p> tag.
# <i>This method is only available if RedCloth can be required</i>.
def textilize_without_paragraph(text)
textiled = textilize(text)
if textiled[0..2] == "<p>" then textiled = textiled[3..-1] end
if textiled[-4..-1] == "</p>" then textiled = textiled[0..-5] end
return textiled
end
rescue LoadError
# We can't really help what's not there
end
begin
require "bluecloth"
# Returns the text with all the Markdown codes turned into HTML-tags.
# <i>This method is only available if BlueCloth can be required</i>.
def markdown(text)
BlueCloth.new(text).to_html
end
rescue LoadError
# We can't really help what's not there
end
# Turns all links into words, like "<a href="something">else</a>" to "else".
def strip_links(text)
text.gsub(/<a.*>(.*)<\/a>/m, '\1')
end
private
# Returns a version of the text that's safe to use in a regular expression without triggering engine features.
def escape_regexp(text)
text.gsub(/([\\|?+*\/\)\(])/) { |m| "\\#{$1}" }
end
end
end
end

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