/text/src/test/resources/examples/ruby/rails.in.rb
Ruby | 15466 lines | 11031 code | 1665 blank | 2770 comment | 670 complexity | cc3b4273ae9f93be89092edb70074a1b MD5 | raw file
Possible License(s): EPL-1.0, MPL-2.0-no-copyleft-exception
- require 'rbconfig'
- require 'find'
- require 'ftools'
- include Config
- # this was adapted from rdoc's install.rb by way 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
- # the acual gruntwork
- Dir.chdir("lib")
- Find.find("action_mailer", "action_mailer.rb") { |f|
- if f[-3..-1] == ".rb"
- File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true)
- else
- File::makedirs(File.join($sitedir, *f.split(/\//)))
- end
- }
- module ActionMailer
- module AdvAttrAccessor #:nodoc:
- def self.append_features(base)
- super
- base.extend(ClassMethods)
- end
- module ClassMethods #:nodoc:
- def adv_attr_accessor(*names)
- names.each do |name|
- ivar = "@#{name}"
- define_method("#{name}=") do |value|
- instance_variable_set(ivar, value)
- end
- define_method(name) do |*parameters|
- raise ArgumentError, "expected 0 or 1 parameters" unless parameters.length <= 1
- if parameters.empty?
- if instance_variables.include?(ivar)
- instance_variable_get(ivar)
- end
- else
- instance_variable_set(ivar, parameters.first)
- end
- end
- end
- end
- end
- end
- end
- require 'action_mailer/adv_attr_accessor'
- require 'action_mailer/part'
- require 'action_mailer/part_container'
- require 'action_mailer/utils'
- require 'tmail/net'
- module ActionMailer #:nodoc:
- # ActionMailer allows you to send email from your application using a mailer model and views.
- #
- # = Mailer Models
- # To use ActionMailer, you need to create a mailer model.
- #
- # $ script/generate mailer Notifier
- #
- # The generated model inherits from ActionMailer::Base. Emails are defined by creating methods within the model which are then
- # used to set variables to be used in the mail template, to change options on the mail, or
- # to add attachments.
- #
- # Examples:
- #
- # class Notifier < ActionMailer::Base
- # def signup_notification(recipient)
- # recipients recipient.email_address_with_name
- # from "system@example.com"
- # subject "New account information"
- # body "account" => recipient
- # end
- # end
- #
- # Mailer methods have the following configuration methods available.
- #
- # * <tt>recipients</tt> - Takes one or more email addresses. These addresses are where your email will be delivered to. Sets the <tt>To:</tt> header.
- # * <tt>subject</tt> - The subject of your email. Sets the <tt>Subject:</tt> header.
- # * <tt>from</tt> - Who the email you are sending is from. Sets the <tt>From:</tt> header.
- # * <tt>cc</tt> - Takes one or more email addresses. These addresses will receive a carbon copy of your email. Sets the <tt>Cc:</tt> header.
- # * <tt>bcc</tt> - Takes one or more email address. These addresses will receive a blind carbon copy of your email. Sets the <tt>Bcc</tt> header.
- # * <tt>sent_on</tt> - The date on which the message was sent. If not set, the header wil be set by the delivery agent.
- # * <tt>content_type</tt> - Specify the content type of the message. Defaults to <tt>text/plain</tt>.
- # * <tt>headers</tt> - Specify additional headers to be set for the message, e.g. <tt>headers 'X-Mail-Count' => 107370</tt>.
- #
- # The <tt>body</tt> method has special behavior. It takes a hash which generates an instance variable
- # named after each key in the hash containing the value that that key points to.
- #
- # So, for example, <tt>body "account" => recipient</tt> would result
- # in an instance variable <tt>@account</tt> with the value of <tt>recipient</tt> being accessible in the
- # view.
- #
- # = Mailer Views
- # Like ActionController, each mailer class has a corresponding view directory
- # in which each method of the class looks for a template with its name.
- # To define a template to be used with a mailing, create an <tt>.rhtml</tt> file with the same name as the method
- # in your mailer model. For example, in the mailer defined above, the template at
- # <tt>app/views/notifier/signup_notification.rhtml</tt> would be used to generate the email.
- #
- # Variables defined in the model are accessible as instance variables in the view.
- #
- # Emails by default are sent in plain text, so a sample view for our model example might look like this:
- #
- # Hi <%= @account.name %>,
- # Thanks for joining our service! Please check back often.
- #
- # = Sending Mail
- # Once a mailer action and template are defined, you can deliver your message or create it and save it
- # for delivery later:
- #
- # Notifier.deliver_signup_notification(david) # sends the email
- # mail = Notifier.create_signup_notification(david) # => a tmail object
- # Notifier.deliver(mail)
- #
- # You never instantiate your mailer class. Rather, your delivery instance
- # methods are automatically wrapped in class methods that start with the word
- # <tt>deliver_</tt> followed by the name of the mailer method that you would
- # like to deliver. The <tt>signup_notification</tt> method defined above is
- # delivered by invoking <tt>Notifier.deliver_signup_notification</tt>.
- #
- # = HTML Email
- # To send mail as HTML, make sure your view (the <tt>.rhtml</tt> file) generates HTML and
- # set the content type to html.
- #
- # class MyMailer < ActionMailer::Base
- # def signup_notification(recipient)
- # recipients recipient.email_address_with_name
- # subject "New account information"
- # body "account" => recipient
- # from "system@example.com"
- # content_type "text/html" # Here's where the magic happens
- # end
- # end
- #
- # = Multipart Email
- # You can explicitly specify multipart messages:
- #
- # class ApplicationMailer < ActionMailer::Base
- # def signup_notification(recipient)
- # recipients recipient.email_address_with_name
- # subject "New account information"
- # from "system@example.com"
- #
- # part :content_type => "text/html",
- # :body => render_message("signup-as-html", :account => recipient)
- #
- # part "text/plain" do |p|
- # p.body = render_message("signup-as-plain", :account => recipient)
- # p.transfer_encoding = "base64"
- # end
- # end
- # end
- #
- # Multipart messages can also be used implicitly because ActionMailer will automatically
- # detect and use multipart templates, where each template is named after the name of the action, followed
- # by the content type. Each such detected template will be added as separate part to the message.
- #
- # For example, if the following templates existed:
- # * signup_notification.text.plain.rhtml
- # * signup_notification.text.html.rhtml
- # * signup_notification.text.xml.rxml
- # * signup_notification.text.x-yaml.rhtml
- #
- # Each would be rendered and added as a separate part to the message,
- # with the corresponding content type. The same body hash is passed to
- # each template.
- #
- # = Attachments
- # Attachments can be added by using the +attachment+ method.
- #
- # Example:
- #
- # class ApplicationMailer < ActionMailer::Base
- # # attachments
- # def signup_notification(recipient)
- # recipients recipient.email_address_with_name
- # subject "New account information"
- # from "system@example.com"
- #
- # attachment :content_type => "image/jpeg",
- # :body => File.read("an-image.jpg")
- #
- # attachment "application/pdf" do |a|
- # a.body = generate_your_pdf_here()
- # end
- # end
- # end
- #
- # = Configuration options
- #
- # These options are specified on the class level, like <tt>ActionMailer::Base.template_root = "/my/templates"</tt>
- #
- # * <tt>template_root</tt> - template root determines the base from which template references will be made.
- #
- # * <tt>logger</tt> - 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.
- #
- # * <tt>server_settings</tt> - Allows detailed configuration of the server:
- # * <tt>:address</tt> Allows you to use a remote mail server. Just change it from its default "localhost" setting.
- # * <tt>:port</tt> On the off chance 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 in this setting.
- # * <tt>:password</tt> If your mail server requires authentication, set the password in this setting.
- # * <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
- #
- # * <tt>raise_delivery_errors</tt> - whether or not errors should be raised if the email fails to be delivered.
- #
- # * <tt>delivery_method</tt> - Defines a delivery method. Possible values are :smtp (default), :sendmail, and :test.
- # Sendmail is assumed to be present at "/usr/sbin/sendmail".
- #
- # * <tt>perform_deliveries</tt> - Determines whether deliver_* methods are actually carried out. By default they are,
- # but this can be turned off to help functional testing.
- #
- # * <tt>deliveries</tt> - Keeps an array of all the emails sent out through the Action Mailer with delivery_method :test. Most useful
- # for unit and functional testing.
- #
- # * <tt>default_charset</tt> - The default charset used for the body and to encode the subject. Defaults to UTF-8. You can also
- # pick a different charset from inside a method with <tt>@charset</tt>.
- # * <tt>default_content_type</tt> - The default content type used for the main part of the message. Defaults to "text/plain". You
- # can also pick a different content type from inside a method with <tt>@content_type</tt>.
- # * <tt>default_mime_version</tt> - The default mime version used for the message. Defaults to nil. You
- # can also pick a different value from inside a method with <tt>@mime_version</tt>. When multipart messages are in
- # use, <tt>@mime_version</tt> will be set to "1.0" if it is not set inside a method.
- # * <tt>default_implicit_parts_order</tt> - When a message is built implicitly (i.e. multiple parts are assembled from templates
- # which specify the content type in their filenames) this variable controls how the parts are ordered. Defaults to
- # ["text/html", "text/enriched", "text/plain"]. Items that appear first in the array have higher priority in the mail client
- # and appear last in the mime encoded message. You can also pick a different order from inside a method with
- # <tt>@implicit_parts_order</tt>.
- class Base
- include AdvAttrAccessor, PartContainer
- # Action Mailer subclasses should be reloaded by the dispatcher in Rails
- # when Dependencies.mechanism = :load.
- include Reloadable::Subclasses
-
- private_class_method :new #:nodoc:
- class_inheritable_accessor :template_root
- cattr_accessor :logger
- @@server_settings = {
- :address => "localhost",
- :port => 25,
- :domain => 'localhost.localdomain',
- :user_name => nil,
- :password => nil,
- :authentication => nil
- }
- cattr_accessor :server_settings
- @@raise_delivery_errors = true
- cattr_accessor :raise_delivery_errors
- @@delivery_method = :smtp
- cattr_accessor :delivery_method
-
- @@perform_deliveries = true
- cattr_accessor :perform_deliveries
-
- @@deliveries = []
- cattr_accessor :deliveries
- @@default_charset = "utf-8"
- cattr_accessor :default_charset
- @@default_content_type = "text/plain"
- cattr_accessor :default_content_type
-
- @@default_mime_version = nil
- cattr_accessor :default_mime_version
- @@default_implicit_parts_order = [ "text/html", "text/enriched", "text/plain" ]
- cattr_accessor :default_implicit_parts_order
- # Specify the BCC addresses for the message
- adv_attr_accessor :bcc
-
- # Define the body of the message. This is either a Hash (in which case it
- # specifies the variables to pass to the template when it is rendered),
- # or a string, in which case it specifies the actual text of the message.
- adv_attr_accessor :body
-
- # Specify the CC addresses for the message.
- adv_attr_accessor :cc
-
- # Specify the charset to use for the message. This defaults to the
- # +default_charset+ specified for ActionMailer::Base.
- adv_attr_accessor :charset
-
- # Specify the content type for the message. This defaults to <tt>text/plain</tt>
- # in most cases, but can be automatically set in some situations.
- adv_attr_accessor :content_type
-
- # Specify the from address for the message.
- adv_attr_accessor :from
-
- # Specify additional headers to be added to the message.
- adv_attr_accessor :headers
-
- # Specify the order in which parts should be sorted, based on content-type.
- # This defaults to the value for the +default_implicit_parts_order+.
- adv_attr_accessor :implicit_parts_order
-
- # Override the mailer name, which defaults to an inflected version of the
- # mailer's class name. If you want to use a template in a non-standard
- # location, you can use this to specify that location.
- adv_attr_accessor :mailer_name
-
- # Defaults to "1.0", but may be explicitly given if needed.
- adv_attr_accessor :mime_version
-
- # The recipient addresses for the message, either as a string (for a single
- # address) or an array (for multiple addresses).
- adv_attr_accessor :recipients
-
- # The date on which the message was sent. If not set (the default), the
- # header will be set by the delivery agent.
- adv_attr_accessor :sent_on
-
- # Specify the subject of the message.
- adv_attr_accessor :subject
-
- # Specify the template name to use for current message. This is the "base"
- # template name, without the extension or directory, and may be used to
- # have multiple mailer methods share the same template.
- adv_attr_accessor :template
- # The mail object instance referenced by this mailer.
- attr_reader :mail
- class << self
- def method_missing(method_symbol, *parameters)#:nodoc:
- case method_symbol.id2name
- when /^create_([_a-z]\w*)/ then new($1, *parameters).mail
- when /^deliver_([_a-z]\w*)/ then new($1, *parameters).deliver!
- when "new" then nil
- else super
- end
- end
- # Receives a raw email, parses it into an email object, decodes it,
- # instantiates a new mailer, and passes the email object to the mailer
- # object's #receive method. If you want your mailer to be able to
- # process incoming messages, you'll need to implement a #receive
- # method that accepts the email object as a parameter:
- #
- # class MyMailer < ActionMailer::Base
- # def receive(mail)
- # ...
- # end
- # end
- def receive(raw_email)
- logger.info "Received mail:\n #{raw_email}" unless logger.nil?
- mail = TMail::Mail.parse(raw_email)
- mail.base64_decode
- new.receive(mail)
- end
- # Deliver the given mail object directly. This can be used to deliver
- # a preconstructed mail object, like:
- #
- # email = MyMailer.create_some_mail(parameters)
- # email.set_some_obscure_header "frobnicate"
- # MyMailer.deliver(email)
- def deliver(mail)
- new.deliver!(mail)
- end
- end
- # Instantiate a new mailer object. If +method_name+ is not +nil+, the mailer
- # will be initialized according to the named method. If not, the mailer will
- # remain uninitialized (useful when you only need to invoke the "receive"
- # method, for instance).
- def initialize(method_name=nil, *parameters) #:nodoc:
- create!(method_name, *parameters) if method_name
- end
- # Initialize the mailer via the given +method_name+. The body will be
- # rendered and a new TMail::Mail object created.
- def create!(method_name, *parameters) #:nodoc:
- initialize_defaults(method_name)
- send(method_name, *parameters)
- # If an explicit, textual body has not been set, we check assumptions.
- unless String === @body
- # First, we look to see if there are any likely templates that match,
- # which include the content-type in their file name (i.e.,
- # "the_template_file.text.html.rhtml", etc.). Only do this if parts
- # have not already been specified manually.
- if @parts.empty?
- templates = Dir.glob("#{template_path}/#{@template}.*")
- templates.each do |path|
- # TODO: don't hardcode rhtml|rxml
- basename = File.basename(path)
- next unless md = /^([^\.]+)\.([^\.]+\.[^\+]+)\.(rhtml|rxml)$/.match(basename)
- template_name = basename
- content_type = md.captures[1].gsub('.', '/')
- @parts << Part.new(:content_type => content_type,
- :disposition => "inline", :charset => charset,
- :body => render_message(template_name, @body))
- end
- unless @parts.empty?
- @content_type = "multipart/alternative"
- @parts = sort_parts(@parts, @implicit_parts_order)
- end
- end
- # Then, if there were such templates, we check to see if we ought to
- # also render a "normal" template (without the content type). If a
- # normal template exists (or if there were no implicit parts) we render
- # it.
- template_exists = @parts.empty?
- template_exists ||= Dir.glob("#{template_path}/#{@template}.*").any? { |i| File.basename(i).split(".").length == 2 }
- @body = render_message(@template, @body) if template_exists
- # Finally, if there are other message parts and a textual body exists,
- # we shift it onto the front of the parts and set the body to nil (so
- # that create_mail doesn't try to render it in addition to the parts).
- if !@parts.empty? && String === @body
- @parts.unshift Part.new(:charset => charset, :body => @body)
- @body = nil
- end
- end
- # If this is a multipart e-mail add the mime_version if it is not
- # already set.
- @mime_version ||= "1.0" if !@parts.empty?
- # build the mail object itself
- @mail = create_mail
- end
- # Delivers a TMail::Mail object. By default, it delivers the cached mail
- # object (from the #create! method). If no cached mail object exists, and
- # no alternate has been given as the parameter, this will fail.
- def deliver!(mail = @mail)
- raise "no mail object available for delivery!" unless mail
- logger.info "Sent mail:\n #{mail.encoded}" unless logger.nil?
- begin
- send("perform_delivery_#{delivery_method}", mail) if perform_deliveries
- rescue Object => e
- raise e if raise_delivery_errors
- end
- return mail
- end
- private
- # Set up the default values for the various instance variables of this
- # mailer. Subclasses may override this method to provide different
- # defaults.
- def initialize_defaults(method_name)
- @charset ||= @@default_charset.dup
- @content_type ||= @@default_content_type.dup
- @implicit_parts_order ||= @@default_implicit_parts_order.dup
- @template ||= method_name
- @mailer_name ||= Inflector.underscore(self.class.name)
- @parts ||= []
- @headers ||= {}
- @body ||= {}
- @mime_version = @@default_mime_version.dup if @@default_mime_version
- end
- def render_message(method_name, body)
- render :file => method_name, :body => body
- end
- def render(opts)
- body = opts.delete(:body)
- initialize_template_class(body).render(opts)
- end
- def template_path
- "#{template_root}/#{mailer_name}"
- end
- def initialize_template_class(assigns)
- ActionView::Base.new(template_path, assigns, self)
- end
- def sort_parts(parts, order = [])
- order = order.collect { |s| s.downcase }
- parts = parts.sort do |a, b|
- a_ct = a.content_type.downcase
- b_ct = b.content_type.downcase
- a_in = order.include? a_ct
- b_in = order.include? b_ct
- s = case
- when a_in && b_in
- order.index(a_ct) <=> order.index(b_ct)
- when a_in
- -1
- when b_in
- 1
- else
- a_ct <=> b_ct
- end
- # reverse the ordering because parts that come last are displayed
- # first in mail clients
- (s * -1)
- end
- parts
- end
- def create_mail
- m = TMail::Mail.new
- m.subject, = quote_any_if_necessary(charset, subject)
- m.to, m.from = quote_any_address_if_necessary(charset, recipients, from)
- m.bcc = quote_address_if_necessary(bcc, charset) unless bcc.nil?
- m.cc = quote_address_if_necessary(cc, charset) unless cc.nil?
- m.mime_version = mime_version unless mime_version.nil?
- m.date = sent_on.to_time rescue sent_on if sent_on
- headers.each { |k, v| m[k] = v }
- real_content_type, ctype_attrs = parse_content_type
- if @parts.empty?
- m.set_content_type(real_content_type, nil, ctype_attrs)
- m.body = Utils.normalize_new_lines(body)
- else
- if String === body
- part = TMail::Mail.new
- part.body = Utils.normalize_new_lines(body)
- part.set_content_type(real_content_type, nil, ctype_attrs)
- part.set_content_disposition "inline"
- m.parts << part
- end
- @parts.each do |p|
- part = (TMail::Mail === p ? p : p.to_mail(self))
- m.parts << part
- end
-
- if real_content_type =~ /multipart/
- ctype_attrs.delete "charset"
- m.set_content_type(real_content_type, nil, ctype_attrs)
- end
- end
- @mail = m
- end
- def perform_delivery_smtp(mail)
- destinations = mail.destinations
- mail.ready_to_send
- 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, destinations)
- end
- end
- def perform_delivery_sendmail(mail)
- IO.popen("/usr/sbin/sendmail -i -t","w+") do |sm|
- sm.print(mail.encoded.gsub(/\r/, ''))
- sm.flush
- end
- end
- def perform_delivery_test(mail)
- deliveries << mail
- end
- end
- end
- module ActionMailer
- module Helpers #:nodoc:
- def self.append_features(base) #:nodoc:
- super
- # Initialize the base module to aggregate its helpers.
- base.class_inheritable_accessor :master_helper_module
- base.master_helper_module = Module.new
- # Extend base with class methods to declare helpers.
- base.extend(ClassMethods)
- base.class_eval do
- # Wrap inherited to create a new master helper module for subclasses.
- class << self
- alias_method :inherited_without_helper, :inherited
- alias_method :inherited, :inherited_with_helper
- end
- # Wrap initialize_template_class to extend new template class
- # instances with the master helper module.
- alias_method :initialize_template_class_without_helper, :initialize_template_class
- alias_method :initialize_template_class, :initialize_template_class_with_helper
- end
- end
- 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) #:nodoc:
- master_helper_module.module_eval "include #{helper_module}"
- end
- # Declare a helper:
- # 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 = arg.to_s.underscore + '_helper'
- class_name = file_name.camelize
-
- begin
- require_dependency(file_name)
- rescue LoadError => load_error
- requiree = / -- (.*?)(\.rb)?$/.match(load_error).to_a[1]
- msg = (requiree == file_name) ? "Missing helper file helpers/#{file_name}.rb" : "Can't load file: #{requiree}"
- raise LoadError.new(msg).copy_blame!(load_error)
- end
- add_template_helper(class_name.constantize)
- else
- raise ArgumentError, 'helper expects String, Symbol, or Module argument'
- end
- end
- # Evaluate block in template class if given.
- master_helper_module.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)
- methods.flatten.each do |method|
- master_helper_module.module_eval <<-end_eval
- def #{method}(*args, &block)
- controller.send(%(#{method}), *args, &block)
- end
- end_eval
- end
- 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_with_helper(child)
- inherited_without_helper(child)
- begin
- child.master_helper_module = Module.new
- child.master_helper_module.send :include, master_helper_module
- child.helper child.name.underscore
- rescue MissingSourceFile => e
- raise unless e.is_missing?("helpers/#{child.name.underscore}_helper")
- end
- end
- end
- private
- # Extend the template class instance with our controller's helper module.
- def initialize_template_class_with_helper(assigns)
- returning(template = initialize_template_class_without_helper(assigns)) do
- template.extend self.class.master_helper_module
- end
- end
- end
- endrequire 'text/format'
- module MailHelper
- # Uses Text::Format to take the text and format it, indented two spaces for
- # each line, and wrapped at 72 columns.
- def block_format(text)
- 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
- require 'action_mailer/adv_attr_accessor'
- require 'action_mailer/part_container'
- require 'action_mailer/utils'
- module ActionMailer
- # Represents a subpart of an email message. It shares many similar
- # attributes of ActionMailer::Base. Although you can create parts manually
- # and add them to the #parts list of the mailer, it is easier
- # to use the helper methods in ActionMailer::PartContainer.
- class Part
- include ActionMailer::AdvAttrAccessor
- include ActionMailer::PartContainer
- # Represents the body of the part, as a string. This should not be a
- # Hash (like ActionMailer::Base), but if you want a template to be rendered
- # into the body of a subpart you can do it with the mailer's #render method
- # and assign the result here.
- adv_attr_accessor :body
-
- # Specify the charset for this subpart. By default, it will be the charset
- # of the containing part or mailer.
- adv_attr_accessor :charset
-
- # The content disposition of this part, typically either "inline" or
- # "attachment".
- adv_attr_accessor :content_disposition
-
- # The content type of the part.
- adv_attr_accessor :content_type
-
- # The filename to use for this subpart (usually for attachments).
- adv_attr_accessor :filename
-
- # Accessor for specifying additional headers to include with this part.
- adv_attr_accessor :headers
-
- # The transfer encoding to use for this subpart, like "base64" or
- # "quoted-printable".
- adv_attr_accessor :transfer_encoding
- # Create a new part from the given +params+ hash. The valid params keys
- # correspond to the accessors.
- def initialize(params)
- @content_type = params[:content_type]
- @content_disposition = params[:disposition] || "inline"
- @charset = params[:charset]
- @body = params[:body]
- @filename = params[:filename]
- @transfer_encoding = params[:transfer_encoding] || "quoted-printable"
- @headers = params[:headers] || {}
- @parts = []
- end
- # Convert the part to a mail object which can be included in the parts
- # list of another mail object.
- def to_mail(defaults)
- part = TMail::Mail.new
- real_content_type, ctype_attrs = parse_content_type(defaults)
- if @parts.empty?
- part.content_transfer_encoding = transfer_encoding || "quoted-printable"
- case (transfer_encoding || "").downcase
- when "base64" then
- part.body = TMail::Base64.folding_encode(body)
- when "quoted-printable"
- part.body = [Utils.normalize_new_lines(body)].pack("M*")
- else
- part.body = body
- end
- # Always set the content_type after setting the body and or parts!
- # Also don't set filename and name when there is none (like in
- # non-attachment parts)
- if content_disposition == "attachment"
- ctype_attrs.delete "charset"
- part.set_content_type(real_content_type, nil,
- squish("name" => filename).merge(ctype_attrs))
- part.set_content_disposition(content_disposition,
- squish("filename" => filename).merge(ctype_attrs))
- else
- part.set_content_type(real_content_type, nil, ctype_attrs)
- part.set_content_disposition(content_disposition)
- end
- else
- if String === body
- part = TMail::Mail.new
- part.body = body
- part.set_content_type(real_content_type, nil, ctype_attrs)
- part.set_content_disposition "inline"
- m.parts << part
- end
-
- @parts.each do |p|
- prt = (TMail::Mail === p ? p : p.to_mail(defaults))
- part.parts << prt
- end
-
- part.set_content_type(real_content_type, nil, ctype_attrs) if real_content_type =~ /multipart/
- end
- headers.each { |k,v| part[k] = v }
- part
- end
- private
- def squish(values={})
- values.delete_if { |k,v| v.nil? }
- end
- end
- end
- module ActionMailer
- # Accessors and helpers that ActionMailer::Base and ActionMailer::Part have
- # in common. Using these helpers you can easily add subparts or attachments
- # to your message:
- #
- # def my_mail_message(...)
- # ...
- # part "text/plain" do |p|
- # p.body "hello, world"
- # p.transfer_encoding "base64"
- # end
- #
- # attachment "image/jpg" do |a|
- # a.body = File.read("hello.jpg")
- # a.filename = "hello.jpg"
- # end
- # end
- module PartContainer
- # The list of subparts of this container
- attr_reader :parts
- # Add a part to a multipart message, with the given content-type. The
- # part itself is yielded to the block so that other properties (charset,
- # body, headers, etc.) can be set on it.
- def part(params)
- params = {:content_type => params} if String === params
- part = Part.new(params)
- yield part if block_given?
- @parts << part
- end
- # Add an attachment to a multipart message. This is simply a part with the
- # content-disposition set to "attachment".
- def attachment(params, &block)
- params = { :content_type => params } if String === params
- params = { :disposition => "attachment",
- :transfer_encoding => "base64" }.merge(params)
- part(params, &block)
- end
- private
-
- def parse_content_type(defaults=nil)
- return [defaults && defaults.content_type, {}] if content_type.blank?
- ctype, *attrs = content_type.split(/;\s*/)
- attrs = attrs.inject({}) { |h,s| k,v = s.split(/=/, 2); h[k] = v; h }
- [ctype, {"charset" => charset || defaults && defaults.charset}.merge(attrs)]
- end
- end
- end
- module ActionMailer
- module Quoting #:nodoc:
- # Convert the given text into quoted printable format, with an instruction
- # that the text be eventually interpreted in the given charset.
- def quoted_printable(text, charset)
- text = text.gsub( /[^a-z ]/i ) { quoted_printable_encode($&) }.
- gsub( / /, "_" )
- "=?#{charset}?Q?#{text}?="
- end
- # Convert the given character to quoted printable format, taking into
- # account multi-byte characters (if executing with $KCODE="u", for instance)
- def quoted_printable_encode(character)
- result = ""
- character.each_byte { |b| result << "=%02x" % b }
- result
- end
- # A quick-and-dirty regexp for determining whether a string contains any
- # characters that need escaping.
- if !defined?(CHARS_NEEDING_QUOTING)
- CHARS_NEEDING_QUOTING = /[\000-\011\013\014\016-\037\177-\377]/
- end
- # Quote the given text if it contains any "illegal" characters
- def quote_if_necessary(text, charset)
- (text =~ CHARS_NEEDING_QUOTING) ?
- quoted_printable(text, charset) :
- text
- end
- # Quote any of the given strings if they contain any "illegal" characters
- def quote_any_if_necessary(charset, *args)
- args.map { |v| quote_if_necessary(v, charset) }
- end
- # Quote the given address if it needs to be. The address may be a
- # regular email address, or it can be a phrase followed by an address in
- # brackets. The phrase is the only part that will be quoted, and only if
- # it needs to be. This allows extended characters to be used in the
- # "to", "from", "cc", and "bcc" headers.
- def quote_address_if_necessary(address, charset)
- if Array === address
- address.map { |a| quote_address_if_necessary(a, charset) }
- elsif address =~ /^(\S.*)\s+(<.*>)$/
- address = $2
- phrase = quote_if_necessary($1.gsub(/^['"](.*)['"]$/, '\1'), charset)
- "\"#{phrase}\" #{address}"
- else
- address
- end
- end
- # Quote any of the given addresses, if they need to be.
- def quote_any_address_if_necessary(charset, *args)
- args.map { |v| quote_address_if_necessary(v, charset) }
- end
- end
- end
- module ActionMailer
- module Utils #:nodoc:
- def normalize_new_lines(text)
- text.to_s.gsub(/\r\n?/, "\n")
- end
- module_function :normalize_new_lines
- end
- end
- #--
- # Text::Format for Ruby
- # Version 0.63
- #
- # Copyright (c) 2002 - 2003 Austin Ziegler
- #
- # $Id: format.rb,v 1.1.1.1 2004/10/14 11:59:57 webster132 Exp $
- #
- # ==========================================================================
- # Revision History ::
- # YYYY.MM.DD Change ID Developer
- # Description
- # --------------------------------------------------------------------------
- # 2002.10.18 Austin Ziegler
- # Fixed a minor problem with tabs not being counted. Changed
- # abbreviations from Hash to Array to better suit Ruby's
- # capabilities. Fixed problems with the way that Array arguments
- # are handled in calls to the major object types, excepting in
- # Text::Format#expand and Text::Format#unexpand (these will
- # probably need to be fixed).
- # 2002.10.30 Austin Ziegler
- # Fixed the ordering of the <=> for binary tests. Fixed
- # Text::Format#expand and Text::Format#unexpand to handle array
- # arguments better.
- # 2003.01.24 Austin Ziegler
- # Fixed a problem with Text::Format::RIGHT_FILL handling where a
- # single word is larger than #columns. Removed Comparable
- # capabilities (<=> doesn't make sense; == does). Added Symbol
- # equivalents for the Hash initialization. Hash initialization has
- # been modified so that values are set as follows (Symbols are
- # highest priority; strings are middle; defaults are lowest):
- # @columns = arg[:columns] || arg['columns'] || @columns
- # Added #hard_margins, #split_rules, #hyphenator, and #split_words.
- # 2003.02.07 Austin Ziegler
- # Fixed the installer for proper case-sensitive handling.
- # 2003.03.28 Austin Ziegler
- # Added the ability for a hyphenator to receive the formatter
- # object. Fixed a bug for strings matching /\A\s*\Z/ failing
- # entirely. Fixed a test case failing under 1.6.8.
- # 2003.04.04 Austin Ziegler
- # Handle the case of hyphenators returning nil for first/rest.
- # 2003.09.17 Austin Ziegler
- # Fixed a problem where #paragraphs(" ") was raising
- # NoMethodError.
- #
- # ==========================================================================
- #++
- module Text #:nodoc:
- # Text::Format for Ruby is copyright 2002 - 2005 by Austin Ziegler. It
- # is available under Ruby's licence, the Perl Artistic licence, or the
- # GNU GPL version 2 (or at your option, any later version). As a
- # special exception, for use with official Rails (provided by the
- # rubyonrails.org development team) and any project created with
- # official Rails, the following alternative MIT-style licence may be
- # used:
- #
- # == Text::Format Licence for Rails and Rails Applications
- # 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 names of its contributors may not be used to endorse or
- # promote products derived from this software without specific prior
- # written permission.
- #
- # 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.
- class Format
- VERSION = '0.63'
- # Local abbreviations. More can be added with Text::Format.abbreviations
- ABBREV = [ 'Mr', 'Mrs', 'Ms', 'Jr', 'Sr' ]
- # Formatting values
- LEFT_ALIGN = 0
- RIGHT_ALIGN = 1
- RIGHT_FILL = 2
- JUSTIFY = 3
- # Word split modes (only applies when #hard_margins is true).
- SPLIT_FIXED = 1
- SPLIT_CONTINUATION = 2
- SPLIT_HYPHENATION = 4
- SPLIT_CONTINUATION_FIXED = SPLIT_CONTINUATION | SPLIT_FIXED
- SPLIT_HYPHENATION_FIXED = SPLIT_HYPHENATION | SPLIT_FIXED
- SPLIT_HYPHENATION_CONTINUATION = SPLIT_HYPHENATION | SPLIT_CONTINUATION
- SPLIT_ALL = SPLIT_HYPHENATION | SPLIT_CONTINUATION | SPLIT_FIXED
- # Words forcibly split by Text::Format will be stored as split words.
- # This class represents a word forcibly split.
- class SplitWord
- # The word that was split.
- attr_reader :word
- # The first part of the word that was split.
- attr_reader :first
- # The remainder of the word that was split.
- attr_reader :rest
- def initialize(word, first, rest) #:nodoc:
- @word = word
- @first = first
- @rest = rest
- end
- end
- private
- LEQ_RE = /[.?!]['"]?$/
- def brk_re(i) #:nodoc:
- %r/((?:\S+\s+){#{i}})(.+)/
- end
- def posint(p) #:nodoc:
- p.to_i.abs
- end
- public
- # Compares two Text::Format objects. All settings of the objects are
- # compared *except* #hyphenator. Generated results (e.g., #split_words)
- # are not compared, either.
- def ==(o)
- (@text == o.text) &&
- (@columns == o.columns) &&
- (@left_margin == o.left_margin) &&
- (@right_margin == o.right_margin) &&
- (@hard_margins == o.hard_margins) &&
- (@split_rules == o.split_rules) &&
- (@first_indent == o.first_indent) &&
- (@body_indent == o.body_indent) &&
- (@tag_text == o.tag_text) &&
- (@tabstop == o.tabstop) &&
- (@format_style == o.format_style) &&
- (@extra_space == o.extra_space) &&
- (@tag_paragraph == o.tag_paragraph) &&
- (@nobreak == o.nobreak) &&
- (@abbreviations == o.abbreviations) &&
- (@nobreak_regex == o.nobreak_regex)
- end
- # The text to be manipulated. Note that value is optional, but if the
- # formatting functions are called without values, this text is what will
- # be formatted.
- #
- # *Default*:: <tt>[]</tt>
- # <b>Used in</b>:: All methods
- attr_accessor :text
- # The total width of the format area. The margins, indentation, and text
- # are formatted into this space.
- #
- # COLUMNS
- # <-------------------------------------------------------------->
- # <-----------><------><---------------------------><------------>
- # left margin indent text is formatted into here right margin
- #
- # *Default*:: <tt>72</tt>
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>,
- # <tt>#center</tt>
- attr_reader :columns
- # The total width of the format area. The margins, indentation, and text
- # are formatted into this space. The value provided is silently
- # converted to a positive integer.
- #
- # COLUMNS
- # <-------------------------------------------------------------->
- # <-----------><------><---------------------------><------------>
- # left margin indent text is formatted into here right margin
- #
- # *Default*:: <tt>72</tt>
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>,
- # <tt>#center</tt>
- def columns=(c)
- @columns = posint(c)
- end
- # The number of spaces used for the left margin.
- #
- # columns
- # <-------------------------------------------------------------->
- # <-----------><------><---------------------------><------------>
- # LEFT MARGIN indent text is formatted into here right margin
- #
- # *Default*:: <tt>0</tt>
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>,
- # <tt>#center</tt>
- attr_reader :left_margin
- # The number of spaces used for the left margin. The value provided is
- # silently converted to a positive integer value.
- #
- # columns
- # <-------------------------------------------------------------->
- # <-----------><------><---------------------------><------------>
- # LEFT MARGIN indent text is formatted into here right margin
- #
- # *Default*:: <tt>0</tt>
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>,
- # <tt>#center</tt>
- def left_margin=(left)
- @left_margin = posint(left)
- end
- # The number of spaces used for the right margin.
- #
- # columns
- # <-------------------------------------------------------------->
- # <-----------><------><---------------------------><------------>
- # left margin indent text is formatted into here RIGHT MARGIN
- #
- # *Default*:: <tt>0</tt>
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>,
- # <tt>#center</tt>
- attr_reader :right_margin
- # The number of spaces used for the right margin. The value provided is
- # silently converted to a positive integer value.
- #
- # columns
- # <-------------------------------------------------------------->
- # <-----------><------><---------------------------><------------>
- # left margin indent text is formatted into here RIGHT MARGIN
- #
- # *Default*:: <tt>0</tt>
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>,
- # <tt>#center</tt>
- def right_margin=(r)
- @right_margin = posint(r)
- end
- # The number of spaces to indent the first line of a paragraph.
- #
- # columns
- # <-------------------------------------------------------------->
- # <-----------><------><---------------------------><------------>
- # left margin INDENT text is formatted into here right margin
- #
- # *Default*:: <tt>4</tt>
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
- attr_reader :first_indent
- # The number of spaces to indent the first line of a paragraph. The
- # value provided is silently converted to a positive integer value.
- #
- # columns
- # <-------------------------------------------------------------->
- # <-----------><------><---------------------------><------------>
- # left margin INDENT text is formatted into here right margin
- #
- # *Default*:: <tt>4</tt>
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
- def first_indent=(f)
- @first_indent = posint(f)
- end
- # The number of spaces to indent all lines after the first line of a
- # paragraph.
- #
- # columns
- # <-------------------------------------------------------------->
- # <-----------><------><---------------------------><------------>
- # left margin INDENT text is formatted into here right margin
- #
- # *Default*:: <tt>0</tt>
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
- attr_reader :body_indent
- # The number of spaces to indent all lines after the first line of
- # a paragraph. The value provided is silently converted to a
- # positive integer value.
- #
- # columns
- # <-------------------------------------------------------------->
- # <-----------><------><---------------------------><------------>
- # left margin INDENT text is formatted into here right margin
- #
- # *Default*:: <tt>0</tt>
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
- def body_indent=(b)
- @body_indent = posint(b)
- end
- # Normally, words larger than the format area will be placed on a line
- # by themselves. Setting this to +true+ will force words larger than the
- # format area to be split into one or more "words" each at most the size
- # of the format area. The first line and the original word will be
- # placed into <tt>#split_words</tt>. Note that this will cause the
- # output to look *similar* to a #format_style of JUSTIFY. (Lines will be
- # filled as much as possible.)
- #
- # *Default*:: +false+
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
- attr_accessor :hard_margins
- # An array of words split during formatting if #hard_margins is set to
- # +true+.
- # #split_words << Text::Format::SplitWord.new(word, first, rest)
- attr_reader :split_words
- # The object responsible for hyphenating. It must respond to
- # #hyphenate_to(word, size) or #hyphenate_to(word, size, formatter) and
- # return an array of the word split into two parts; if there is a
- # hyphenation mark to be applied, responsibility belongs to the
- # hyphenator object. The size is the MAXIMUM size permitted, including
- # any hyphenation marks. If the #hyphenate_to method has an arity of 3,
- # the formatter will be provided to the method. This allows the
- # hyphenator to make decisions about the hyphenation based on the
- # formatting rules.
- #
- # *Default*:: +nil+
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
- attr_reader :hyphenator
- # The object responsible for hyphenating. It must respond to
- # #hyphenate_to(word, size) and return an array of the word hyphenated
- # into two parts. The size is the MAXIMUM size permitted, including any
- # hyphenation marks.
- #
- # *Default*:: +nil+
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
- def hyphenator=(h)
- raise ArgumentError, "#{h.inspect} is not a valid hyphenator." unless h.respond_to?(:hyphenate_to)
- arity = h.method(:hyphenate_to).arity
- raise ArgumentError, "#{h.inspect} must have exactly two or three arguments." unless [2, 3].include?(arity)
- @hyphenator = h
- @hyphenator_arity = arity
- end
- # Specifies the split mode; used only when #hard_margins is set to
- # +true+. Allowable values are:
- # [+SPLIT_FIXED+] The word will be split at the number of
- # characters needed, with no marking at all.
- # repre
- # senta
- # ion
- # [+SPLIT_CONTINUATION+] The word will be split at the number of
- # characters needed, with a C-style continuation
- # character. If a word is the only item on a
- # line and it cannot be split into an
- # appropriate size, SPLIT_FIXED will be used.
- # repr\
- # esen\
- # tati\
- # on
- # [+SPLIT_HYPHENATION+] The word will be split according to the
- # hyphenator specified in #hyphenator. If there
- # is no #hyphenator specified, works like
- # SPLIT_CONTINUATION. The example is using
- # TeX::Hyphen. If a word is the only item on a
- # line and it cannot be split into an
- # appropriate size, SPLIT_CONTINUATION mode will
- # be used.
- # rep-
- # re-
- # sen-
- # ta-
- # tion
- #
- # *Default*:: <tt>Text::Format::SPLIT_FIXED</tt>
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
- attr_reader :split_rules
- # Specifies the split mode; used only when #hard_margins is set to
- # +true+. Allowable values are:
- # [+SPLIT_FIXED+] The word will be split at the number of
- # characters needed, with no marking at all.
- # repre
- # senta
- # ion
- # [+SPLIT_CONTINUATION+] The word will be split at the number of
- # characters needed, with a C-style continuation
- # character.
- # repr\
- # esen\
- # tati\
- # on
- # [+SPLIT_HYPHENATION+] The word will be split according to the
- # hyphenator specified in #hyphenator. If there
- # is no #hyphenator specified, works like
- # SPLIT_CONTINUATION. The example is using
- # TeX::Hyphen as the #hyphenator.
- # rep-
- # re-
- # sen-
- # ta-
- # tion
- #
- # These values can be bitwise ORed together (e.g., <tt>SPLIT_FIXED |
- # SPLIT_CONTINUATION</tt>) to provide fallback split methods. In the
- # example given, an attempt will be made to split the word using the
- # rules of SPLIT_CONTINUATION; if there is not enough room, the word
- # will be split with the rules of SPLIT_FIXED. These combinations are
- # also available as the following values:
- # * +SPLIT_CONTINUATION_FIXED+
- # * +SPLIT_HYPHENATION_FIXED+
- # * +SPLIT_HYPHENATION_CONTINUATION+
- # * +SPLIT_ALL+
- #
- # *Default*:: <tt>Text::Format::SPLIT_FIXED</tt>
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
- def split_rules=(s)
- raise ArgumentError, "Invalid value provided for split_rules." if ((s < SPLIT_FIXED) || (s > SPLIT_ALL))
- @split_rules = s
- end
- # Indicates whether sentence terminators should be followed by a single
- # space (+false+), or two spaces (+true+).
- #
- # *Default*:: +false+
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
- attr_accessor :extra_space
- # Defines the current abbreviations as an array. This is only used if
- # extra_space is turned on.
- #
- # If one is abbreviating "President" as "Pres." (abbreviations =
- # ["Pres"]), then the results of formatting will be as illustrated in
- # the table below:
- #
- # extra_space | include? | !include?
- # true | Pres. Lincoln | Pres. Lincoln
- # false | Pres. Lincoln | Pres. Lincoln
- #
- # *Default*:: <tt>{}</tt>
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
- attr_accessor :abbreviations
- # Indicates whether the formatting of paragraphs should be done with
- # tagged paragraphs. Useful only with <tt>#tag_text</tt>.
- #
- # *Default*:: +false+
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
- attr_accessor :tag_paragraph
- # The array of text to be placed before each paragraph when
- # <tt>#tag_paragraph</tt> is +true+. When <tt>#format()</tt> is called,
- # only the first element of the array is used. When <tt>#paragraphs</tt>
- # is called, then each entry in the array will be used once, with
- # corresponding paragraphs. If the tag elements are exhausted before the
- # text is exhausted, then the remaining paragraphs will not be tagged.
- # Regardless of indentation settings, a blank line will be inserted
- # between all paragraphs when <tt>#tag_paragraph</tt> is +true+.
- #
- # *Default*:: <tt>[]</tt>
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
- attr_accessor :tag_text
- # Indicates whether or not the non-breaking space feature should be
- # used.
- #
- # *Default*:: +false+
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
- attr_accessor :nobreak
- # A hash which holds the regular expressions on which spaces should not
- # be broken. The hash is set up such that the key is the first word and
- # the value is the second word.
- #
- # For example, if +nobreak_regex+ contains the following hash:
- #
- # { '^Mrs?\.$' => '\S+$', '^\S+$' => '^(?:S|J)r\.$'}
- #
- # Then "Mr. Jones", "Mrs. Jones", and "Jones Jr." would not be broken.
- # If this simple matching algorithm indicates that there should not be a
- # break at the current end of line, then a backtrack is done until there
- # are two words on which line breaking is permitted. If two such words
- # are not found, then the end of the line will be broken *regardless*.
- # If there is a single word on the current line, then no backtrack is
- # done and the word is stuck on the end.
- #
- # *Default*:: <tt>{}</tt>
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
- attr_accessor :nobreak_regex
- # Indicates the number of spaces that a single tab represents.
- #
- # *Default*:: <tt>8</tt>
- # <b>Used in</b>:: <tt>#expand</tt>, <tt>#unexpand</tt>,
- # <tt>#paragraphs</tt>
- attr_reader :tabstop
- # Indicates the number of spaces that a single tab represents.
- #
- # *Default*:: <tt>8</tt>
- # <b>Used in</b>:: <tt>#expand</tt>, <tt>#unexpand</tt>,
- # <tt>#paragraphs</tt>
- def tabstop=(t)
- @tabstop = posint(t)
- end
- # Specifies the format style. Allowable values are:
- # [+LEFT_ALIGN+] Left justified, ragged right.
- # |A paragraph that is|
- # |left aligned.|
- # [+RIGHT_ALIGN+] Right justified, ragged left.
- # |A paragraph that is|
- # | right aligned.|
- # [+RIGHT_FILL+] Left justified, right ragged, filled to width by
- # spaces. (Essentially the same as +LEFT_ALIGN+ except
- # that lines are padded on the right.)
- # |A paragraph that is|
- # |left aligned. |
- # [+JUSTIFY+] Fully justified, words filled to width by spaces,
- # except the last line.
- # |A paragraph that|
- # |is justified.|
- #
- # *Default*:: <tt>Text::Format::LEFT_ALIGN</tt>
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
- attr_reader :format_style
- # Specifies the format style. Allowable values are:
- # [+LEFT_ALIGN+] Left justified, ragged right.
- # |A paragraph that is|
- # |left aligned.|
- # [+RIGHT_ALIGN+] Right justified, ragged left.
- # |A paragraph that is|
- # | right aligned.|
- # [+RIGHT_FILL+] Left justified, right ragged, filled to width by
- # spaces. (Essentially the same as +LEFT_ALIGN+ except
- # that lines are padded on the right.)
- # |A paragraph that is|
- # |left aligned. |
- # [+JUSTIFY+] Fully justified, words filled to width by spaces.
- # |A paragraph that|
- # |is justified.|
- #
- # *Default*:: <tt>Text::Format::LEFT_ALIGN</tt>
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
- def format_style=(fs)
- raise ArgumentError, "Invalid value provided for format_style." if ((fs < LEFT_ALIGN) || (fs > JUSTIFY))
- @format_style = fs
- end
- # Indicates that the format style is left alignment.
- #
- # *Default*:: +true+
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
- def left_align?
- return @format_style == LEFT_ALIGN
- end
- # Indicates that the format style is right alignment.
- #
- # *Default*:: +false+
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
- def right_align?
- return @format_style == RIGHT_ALIGN
- end
- # Indicates that the format style is right fill.
- #
- # *Default*:: +false+
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
- def right_fill?
- return @format_style == RIGHT_FILL
- end
- # Indicates that the format style is full justification.
- #
- # *Default*:: +false+
- # <b>Used in</b>:: <tt>#format</tt>, <tt>#paragraphs</tt>
- def justify?
- return @format_style == JUSTIFY
- end
- # The default implementation of #hyphenate_to implements
- # SPLIT_CONTINUATION.
- def hyphenate_to(word, size)
- [word[0 .. (size - 2)] + "\\", word[(size - 1) .. -1]]
- end
- private
- def __do_split_word(word, size) #:nodoc:
- [word[0 .. (size - 1)], word[size .. -1]]
- end
- def __format(to_wrap) #:nodoc:
- words = to_wrap.split(/\s+/).compact
- words.shift if words[0].nil? or words[0].empty?
- to_wrap = []
- abbrev = false
- width = @columns - @first_indent - @left_margin - @right_margin
- indent_str = ' ' * @first_indent
- first_line = true
- line = words.shift
- abbrev = __is_abbrev(line) unless line.nil? || line.empty?
- while w = words.shift
- if (w.size + line.size < (width - 1)) ||
- ((line !~ LEQ_RE || abbrev) && (w.size + line.size < width))
- line << " " if (line =~ LEQ_RE) && (not abbrev)
- line << " #{w}"
- else
- line, w = __do_break(line, w) if @nobreak
- line, w = __do_hyphenate(line, w, width) if @hard_margins
- if w.index(/\s+/)
- w, *w2 = w.split(/\s+/)
- words.unshift(w2)
- words.flatten!
- end
- to_wrap << __make_line(line, indent_str, width, w.nil?) unless line.nil?
- if first_line
- first_line = false
- width = @columns - @body_indent - @left_margin - @right_margin
- indent_str = ' ' * @body_indent
- end
- line = w
- end
- abbrev = __is_abbrev(w) unless w.nil?
- end
- loop do
- break if line.nil? or line.empty?
- line, w = __do_hyphenate(line, w, width) if @hard_margins
- to_wrap << __make_line(line, indent_str, width, w.nil?)
- line = w
- end
- if (@tag_paragraph && (to_wrap.size > 0)) then
- clr = %r{`(\w+)'}.match([caller(1)].flatten[0])[1]
- clr = "" if clr.nil?
- if ((not @tag_text[0].nil?) && (@tag_cur.size < 1) &&
- (clr != "__paragraphs")) then
- @tag_cur = @tag_text[0]
- end
- fchar = /(\S)/.match(to_wrap[0])[1]
- white = to_wrap[0].index(fchar)
- if ((white - @left_margin - 1) > @tag_cur.size) then
- white = @tag_cur.size + @left_margin
- to_wrap[0].gsub!(/^ {#{white}}/, "#{' ' * @left_margin}#{@tag_cur}")
- else
- to_wrap.unshift("#{' ' * @left_margin}#{@tag_cur}\n")
- end
- end
- to_wrap.join('')
- end
- # format lines in text into paragraphs with each element of @wrap a
- # paragraph; uses Text::Format.format for the formatting
- def __paragraphs(to_wrap) #:nodoc:
- if ((@first_indent == @body_indent) || @tag_paragraph) then
- p_end = "\n"
- else
- p_end = ''
- end
- cnt = 0
- ret = []
- to_wrap.each do |tw|
- @tag_cur = @tag_text[cnt] if @tag_paragraph
- @tag_cur = '' if @tag_cur.nil?
- line = __format(tw)
- ret << "#{line}#{p_end}" if (not line.nil?) && (line.size > 0)
- cnt += 1
- end
- ret[-1].chomp! unless ret.empty?
- ret.join('')
- end
- # center text using spaces on left side to pad it out empty lines
- # are preserved
- def __center(to_center) #:nodoc:
- tabs = 0
- width = @columns - @left_margin - @right_margin
- centered = []
- to_center.each do |tc|
- s = tc.strip
- tabs = s.count("\t")
- tabs = 0 if tabs.nil?
- ct = ((width - s.size - (tabs * @tabstop) + tabs) / 2)
- ct = (width - @left_margin - @right_margin) - ct
- centered << "#{s.rjust(ct)}\n"
- end
- centered.join('')
- end
- # expand tabs to spaces should be similar to Text::Tabs::expand
- def __expand(to_expand) #:nodoc:
- expanded = []
- to_expand.split("\n").each { |te| expanded << te.gsub(/\t/, ' ' * @tabstop) }
- expanded.join('')
- end
- def __unexpand(to_unexpand) #:nodoc:
- unexpanded = []
- to_unexpand.split("\n").each { |tu| unexpanded << tu.gsub(/ {#{@tabstop}}/, "\t") }
- unexpanded.join('')
- end
- def __is_abbrev(word) #:nodoc:
- # remove period if there is one.
- w = word.gsub(/\.$/, '') unless word.nil?
- return true if (!@extra_space || ABBREV.include?(w) || @abbreviations.include?(w))
- false
- end
- def __make_line(line, indent, width, last = false) #:nodoc:
- lmargin = " " * @left_margin
- fill = " " * (width - line.size) if right_fill? && (line.size <= width)
- if (justify? && ((not line.nil?) && (not line.empty?)) && line =~ /\S+\s+\S+/ && !last)
- spaces = width - line.size
- words = line.split(/(\s+)/)
- ws = spaces / (words.size / 2)
- spaces = spaces % (words.size / 2) if ws > 0
- words.reverse.each do |rw|
- next if (rw =~ /^\S/)
- rw.sub!(/^/, " " * ws)
- next unless (spaces > 0)
- rw.sub!(/^/, " ")
- spaces -= 1
- end
- line = words.join('')
- end
- line = "#{lmargin}#{indent}#{line}#{fill}\n" unless line.nil?
- if right_align? && (not line.nil?)
- line.sub(/^/, " " * (@columns - @right_margin - (line.size - 1)))
- else
- line
- end
- end
- def __do_hyphenate(line, next_line, width) #:nodoc:
- rline = line.dup rescue line
- rnext = next_line.dup rescue next_line
- loop do
- if rline.size == width
- break
- elsif rline.size > width
- words = rline.strip.split(/\s+/)
- word = words[-1].dup
- size = width - rline.size + word.size
- if (size <= 0)
- words[-1] = nil
- rline = words.join(' ').strip
- rnext = "#{word} #{rnext}".strip
- next
- end
- first = rest = nil
- if ((@split_rules & SPLIT_HYPHENATION) != 0)
- if @hyphenator_arity == 2
- first, rest = @hyphenator.hyphenate_to(word, size)
- else
- first, rest = @hyphenator.hyphenate_to(word, size, self)
- end
- end
- if ((@split_rules & SPLIT_CONTINUATION) != 0) and first.nil?
- first, rest = self.hyphenate_to(word, size)
- end
- if ((@split_rules & SPLIT_FIXED) != 0) and first.nil?
- first.nil? or @split_rules == SPLIT_FIXED
- first, rest = __do_split_word(word, size)
- end
- if first.nil?
- words[-1] = nil
- rest = word
- else
- words[-1] = first
- @split_words << SplitWord.new(word, first, rest)
- end
- rline = words.join(' ').strip
- rnext = "#{rest} #{rnext}".strip
- break
- else
- break if rnext.nil? or rnext.empty? or rline.nil? or rline.empty?
- words = rnext.split(/\s+/)
- word = words.shift
- size = width - rline.size - 1
- if (size <= 0)
- rnext = "#{word} #{words.join(' ')}".strip
- break
- end
- first = rest = nil
- if ((@split_rules & SPLIT_HYPHENATION) != 0)
- if @hyphenator_arity == 2
- first, rest = @hyphenator.hyphenate_to(word, size)
- else
- first, rest = @hyphenator.hyphenate_to(word, size, self)
- end
- end
- first, rest = self.hyphenate_to(word, size) if ((@split_rules & SPLIT_CONTINUATION) != 0) and first.nil?
- first, rest = __do_split_word(word, size) if ((@split_rules & SPLIT_FIXED) != 0) and first.nil?
- if (rline.size + (first ? first.size : 0)) < width
- @split_words << SplitWord.new(word, first, rest)
- rline = "#{rline} #{first}".strip
- rnext = "#{rest} #{words.join(' ')}".strip
- end
- break
- end
- end
- [rline, rnext]
- end
- def __do_break(line, next_line) #:nodoc:
- no_brk = false
- words = []
- words = line.split(/\s+/) unless line.nil?
- last_word = words[-1]
- @nobreak_regex.each { |k, v| no_brk = ((last_word =~ /#{k}/) and (next_line =~ /#{v}/)) }
- if no_brk && words.size > 1
- i = words.size
- while i > 0
- no_brk = false
- @nobreak_regex.each { |k, v| no_brk = ((words[i + 1] =~ /#{k}/) && (words[i] =~ /#{v}/)) }
- i -= 1
- break if not no_brk
- end
- if i > 0
- l = brk_re(i).match(line)
- line.sub!(brk_re(i), l[1])
- next_line = "#{l[2]} #{next_line}"
- line.sub!(/\s+$/, '')
- end
- end
- [line, next_line]
- end
- def __create(arg = nil, &block) #:nodoc:
- # Format::Text.new(text-to-wrap)
- @text = arg unless arg.nil?
- # Defaults
- @columns = 72
- @tabstop = 8
- @first_indent = 4
- @body_indent = 0
- @format_style = LEFT_ALIGN
- @left_margin = 0
- @right_margin = 0
- @extra_space = false
- @text = Array.new if @text.nil?
- @tag_paragraph = false
- @tag_text = Array.new
- @tag_cur = ""
- @abbreviations = Array.new
- @nobreak = false
- @nobreak_regex = Hash.new
- @split_words = Array.new
- @hard_margins = false
- @split_rules = SPLIT_FIXED
- @hyphenator = self
- @hyphenator_arity = self.method(:hyphenate_to).arity
- instance_eval(&block) unless block.nil?
- end
- public
- # Formats text into a nice paragraph format. The text is separated
- # into words and then reassembled a word at a time using the settings
- # of this Format object. If a word is larger than the number of
- # columns available for formatting, then that word will appear on the
- # line by itself.
- #
- # If +to_wrap+ is +nil+, then the value of <tt>#text</tt> will be
- # worked on.
- def format(to_wrap = nil)
- to_wrap = @text if to_wrap.nil?
- if to_wrap.class == Array
- __format(to_wrap[0])
- else
- __format(to_wrap)
- end
- end
- # Considers each element of text (provided or internal) as a paragraph.
- # If <tt>#first_indent</tt> is the same as <tt>#body_indent</tt>, then
- # paragraphs will be separated by a single empty line in the result;
- # otherwise, the paragraphs will follow immediately after each other.
- # Uses <tt>#format</tt> to do the heavy lifting.
- def paragraphs(to_wrap = nil)
- to_wrap = @text if to_wrap.nil?
- __paragraphs([to_wrap].flatten)
- end
- # Centers the text, preserving empty lines and tabs.
- def center(to_center = nil)
- to_center = @text if to_center.nil?
- __center([to_center].flatten)
- end
- # Replaces all tab characters in the text with <tt>#tabstop</tt> spaces.
- def expand(to_expand = nil)
- to_expand = @text if to_expand.nil?
- if to_expand.class == Array
- to_expand.collect { |te| __expand(te) }
- else
- __expand(to_expand)
- end
- end
- # Replaces all occurrences of <tt>#tabstop</tt> consecutive spaces
- # with a tab character.
- def unexpand(to_unexpand = nil)
- to_unexpand = @text if to_unexpand.nil?
- if to_unexpand.class == Array
- to_unexpand.collect { |te| v << __unexpand(te) }
- else
- __unexpand(to_unexpand)
- end
- end
- # This constructor takes advantage of a technique for Ruby object
- # construction introduced by Andy Hunt and Dave Thomas (see reference),
- # where optional values are set using commands in a block.
- #
- # Text::Format.new {
- # columns = 72
- # left_margin = 0
- # right_margin = 0
- # first_indent = 4
- # body_indent = 0
- # format_style = Text::Format::LEFT_ALIGN
- # extra_space = false
- # abbreviations = {}
- # tag_paragraph = false
- # tag_text = []
- # nobreak = false
- # nobreak_regex = {}
- # tabstop = 8
- # text = nil
- # }
- #
- # As shown above, +arg+ is optional. If +arg+ is specified and is a
- # +String+, then arg is used as the default value of <tt>#text</tt>.
- # Alternately, an existing Text::Format object can be used or a Hash can
- # be used. With all forms, a block can be specified.
- #
- # *Reference*:: "Object Construction and Blocks"
- # <http://www.pragmaticprogrammer.com/ruby/articles/insteval.html>
- #
- def initialize(arg = nil, &block)
- case arg
- when Text::Format
- __create(arg.text) do
- @columns = arg.columns
- @tabstop = arg.tabstop
- @first_indent = arg.first_indent
- @body_indent = arg.body_indent
- @format_style = arg.format_style
- @left_margin = arg.left_margin
- @right_margin = arg.right_margin
- @extra_space = arg.extra_space
- @tag_paragraph = arg.tag_paragraph
- @tag_text = arg.tag_text
- @abbreviations = arg.abbreviations
- @nobreak = arg.nobreak
- @nobreak_regex = arg.nobreak_regex
- @text = arg.text
- @hard_margins = arg.hard_margins
- @split_words = arg.split_words
- @split_rules = arg.split_rules
- @hyphenator = arg.hyphenator
- end
- instance_eval(&block) unless block.nil?
- when Hash
- __create do
- @columns = arg[:columns] || arg['columns'] || @columns
- @tabstop = arg[:tabstop] || arg['tabstop'] || @tabstop
- @first_indent = arg[:first_indent] || arg['first_indent'] || @first_indent
- @body_indent = arg[:body_indent] || arg['body_indent'] || @body_indent
- @format_style = arg[:format_style] || arg['format_style'] || @format_style
- @left_margin = arg[:left_margin] || arg['left_margin'] || @left_margin
- @right_margin = arg[:right_margin] || arg['right_margin'] || @right_margin
- @extra_space = arg[:extra_space] || arg['extra_space'] || @extra_space
- @text = arg[:text] || arg['text'] || @text
- @tag_paragraph = arg[:tag_paragraph] || arg['tag_paragraph'] || @tag_paragraph
- @tag_text = arg[:tag_text] || arg['tag_text'] || @tag_text
- @abbreviations = arg[:abbreviations] || arg['abbreviations'] || @abbreviations
- @nobreak = arg[:nobreak] || arg['nobreak'] || @nobreak
- @nobreak_regex = arg[:nobreak_regex] || arg['nobreak_regex'] || @nobreak_regex
- @hard_margins = arg[:hard_margins] || arg['hard_margins'] || @hard_margins
- @split_rules = arg[:split_rules] || arg['split_rules'] || @split_rules
- @hyphenator = arg[:hyphenator] || arg['hyphenator'] || @hyphenator
- end
- instance_eval(&block) unless block.nil?
- when String
- __create(arg, &block)
- when NilClass
- __create(&block)
- else
- raise TypeError
- end
- end
- end
- end
- if __FILE__ == $0
- require 'test/unit'
- class TestText__Format < Test::Unit::TestCase #:nodoc:
- attr_accessor :format_o
- GETTYSBURG = <<-'EOS'
- Four score and seven years ago our fathers brought forth on this
- continent a new nation, conceived in liberty and dedicated to the
- proposition that all men are created equal. Now we are engaged in
- a great civil war, testing whether that nation or any nation so
- conceived and so dedicated can long endure. We are met on a great
- battlefield of that war. We have come to dedicate a portion of
- that field as a final resting-place for those who here gave their
- lives that that nation might live. It is altogether fitting and
- proper that we should do this. But in a larger sense, we cannot
- dedicate, we cannot consecrate, we cannot hallow this ground.
- The brave men, living and dead who struggled here have consecrated
- it far above our poor power to add or detract. The world will
- little note nor long remember what we say here, but it can never
- forget what they did here. It is for us the living rather to be
- dedicated here to the unfinished work which they who fought here
- have thus far so nobly advanced. It is rather for us to be here
- dedicated to the great task remaining before us--that from these
- honored dead we take increased devotion to that cause for which
- they gave the last full measure of devotion--that we here highly
- resolve that these dead shall not have died in vain, that this
- nation under God shall have a new birth of freedom, and that
- government of the people, by the people, for the people shall
- not perish from the earth.
- -- Pres. Abraham Lincoln, 19 November 1863
- EOS
- FIVE_COL = "Four \nscore\nand s\neven \nyears\nago o\nur fa\nthers\nbroug\nht fo\nrth o\nn thi\ns con\ntinen\nt a n\new na\ntion,\nconce\nived \nin li\nberty\nand d\nedica\nted t\no the\npropo\nsitio\nn tha\nt all\nmen a\nre cr\neated\nequal\n. Now\nwe ar\ne eng\naged \nin a \ngreat\ncivil\nwar, \ntesti\nng wh\nether\nthat \nnatio\nn or \nany n\nation\nso co\nnceiv\ned an\nd so \ndedic\nated \ncan l\nong e\nndure\n. We \nare m\net on\na gre\nat ba\nttlef\nield \nof th\nat wa\nr. We\nhave \ncome \nto de\ndicat\ne a p\nortio\nn of \nthat \nfield\nas a \nfinal\nresti\nng-pl\nace f\nor th\nose w\nho he\nre ga\nve th\neir l\nives \nthat \nthat \nnatio\nn mig\nht li\nve. I\nt is \naltog\nether\nfitti\nng an\nd pro\nper t\nhat w\ne sho\nuld d\no thi\ns. Bu\nt in \na lar\nger s\nense,\nwe ca\nnnot \ndedic\nate, \nwe ca\nnnot \nconse\ncrate\n, we \ncanno\nt hal\nlow t\nhis g\nround\n. The\nbrave\nmen, \nlivin\ng and\ndead \nwho s\ntrugg\nled h\nere h\nave c\nonsec\nrated\nit fa\nr abo\nve ou\nr poo\nr pow\ner to\nadd o\nr det\nract.\nThe w\norld \nwill \nlittl\ne not\ne nor\nlong \nremem\nber w\nhat w\ne say\nhere,\nbut i\nt can\nnever\nforge\nt wha\nt the\ny did\nhere.\nIt is\nfor u\ns the\nlivin\ng rat\nher t\no be \ndedic\nated \nhere \nto th\ne unf\ninish\ned wo\nrk wh\nich t\nhey w\nho fo\nught \nhere \nhave \nthus \nfar s\no nob\nly ad\nvance\nd. It\nis ra\nther \nfor u\ns to \nbe he\nre de\ndicat\ned to\nthe g\nreat \ntask \nremai\nning \nbefor\ne us-\n-that\nfrom \nthese\nhonor\ned de\nad we\ntake \nincre\nased \ndevot\nion t\no tha\nt cau\nse fo\nr whi\nch th\ney ga\nve th\ne las\nt ful\nl mea\nsure \nof de\nvotio\nn--th\nat we\nhere \nhighl\ny res\nolve \nthat \nthese\ndead \nshall\nnot h\nave d\nied i\nn vai\nn, th\nat th\nis na\ntion \nunder\nGod s\nhall \nhave \na new\nbirth\nof fr\needom\n, and\nthat \ngover\nnment\nof th\ne peo\nple, \nby th\ne peo\nple, \nfor t\nhe pe\nople \nshall\nnot p\nerish\nfrom \nthe e\narth.\n-- Pr\nes. A\nbraha\nm Lin\ncoln,\n19 No\nvembe\nr 186\n3 \n"
- FIVE_CNT = "Four \nscore\nand \nseven\nyears\nago \nour \nfath\\\ners \nbrou\\\nght \nforth\non t\\\nhis \ncont\\\ninent\na new\nnati\\\non, \nconc\\\neived\nin l\\\niber\\\nty a\\\nnd d\\\nedic\\\nated \nto t\\\nhe p\\\nropo\\\nsiti\\\non t\\\nhat \nall \nmen \nare \ncrea\\\nted \nequa\\\nl. N\\\now we\nare \nenga\\\nged \nin a \ngreat\ncivil\nwar, \ntest\\\ning \nwhet\\\nher \nthat \nnati\\\non or\nany \nnati\\\non so\nconc\\\neived\nand \nso d\\\nedic\\\nated \ncan \nlong \nendu\\\nre. \nWe a\\\nre m\\\net on\na gr\\\neat \nbatt\\\nlefi\\\neld \nof t\\\nhat \nwar. \nWe h\\\nave \ncome \nto d\\\nedic\\\nate a\nport\\\nion \nof t\\\nhat \nfield\nas a \nfinal\nrest\\\ning-\\\nplace\nfor \nthose\nwho \nhere \ngave \ntheir\nlives\nthat \nthat \nnati\\\non m\\\night \nlive.\nIt is\nalto\\\ngeth\\\ner f\\\nitti\\\nng a\\\nnd p\\\nroper\nthat \nwe s\\\nhould\ndo t\\\nhis. \nBut \nin a \nlarg\\\ner s\\\nense,\nwe c\\\nannot\ndedi\\\ncate,\nwe c\\\nannot\ncons\\\necra\\\nte, \nwe c\\\nannot\nhall\\\now t\\\nhis \ngrou\\\nnd. \nThe \nbrave\nmen, \nlivi\\\nng a\\\nnd d\\\nead \nwho \nstru\\\nggled\nhere \nhave \ncons\\\necra\\\nted \nit f\\\nar a\\\nbove \nour \npoor \npower\nto a\\\ndd or\ndetr\\\nact. \nThe \nworld\nwill \nlitt\\\nle n\\\note \nnor \nlong \nreme\\\nmber \nwhat \nwe s\\\nay h\\\nere, \nbut \nit c\\\nan n\\\never \nforg\\\net w\\\nhat \nthey \ndid \nhere.\nIt is\nfor \nus t\\\nhe l\\\niving\nrath\\\ner to\nbe d\\\nedic\\\nated \nhere \nto t\\\nhe u\\\nnfin\\\nished\nwork \nwhich\nthey \nwho \nfoug\\\nht h\\\nere \nhave \nthus \nfar \nso n\\\nobly \nadva\\\nnced.\nIt is\nrath\\\ner f\\\nor us\nto be\nhere \ndedi\\\ncated\nto t\\\nhe g\\\nreat \ntask \nrema\\\nining\nbefo\\\nre u\\\ns--t\\\nhat \nfrom \nthese\nhono\\\nred \ndead \nwe t\\\nake \nincr\\\neased\ndevo\\\ntion \nto t\\\nhat \ncause\nfor \nwhich\nthey \ngave \nthe \nlast \nfull \nmeas\\\nure \nof d\\\nevot\\\nion-\\\n-that\nwe h\\\nere \nhigh\\\nly r\\\nesol\\\nve t\\\nhat \nthese\ndead \nshall\nnot \nhave \ndied \nin v\\\nain, \nthat \nthis \nnati\\\non u\\\nnder \nGod \nshall\nhave \na new\nbirth\nof f\\\nreed\\\nom, \nand \nthat \ngove\\\nrnme\\\nnt of\nthe \npeop\\\nle, \nby t\\\nhe p\\\neopl\\\ne, f\\\nor t\\\nhe p\\\neople\nshall\nnot \nperi\\\nsh f\\\nrom \nthe \neart\\\nh. --\nPres.\nAbra\\\nham \nLinc\\\noln, \n19 N\\\novem\\\nber \n1863 \n"
- # Tests both abbreviations and abbreviations=
- def test_abbreviations
- abbr = [" Pres. Abraham Lincoln\n", " Pres. Abraham Lincoln\n"]
- assert_nothing_raised { @format_o = Text::Format.new }
- assert_equal([], @format_o.abbreviations)
- assert_nothing_raised { @format_o.abbreviations = [ 'foo', 'bar' ] }
- assert_equal([ 'foo', 'bar' ], @format_o.abbreviations)
- assert_equal(abbr[0], @format_o.format(abbr[0]))
- assert_nothing_raised { @format_o.extra_space = true }
- assert_equal(abbr[1], @format_o.format(abbr[0]))
- assert_nothing_raised { @format_o.abbreviations = [ "Pres" ] }
- assert_equal([ "Pres" ], @format_o.abbreviations)
- assert_equal(abbr[0], @format_o.format(abbr[0]))
- assert_nothing_raised { @format_o.extra_space = false }
- assert_equal(abbr[0], @format_o.format(abbr[0]))
- end
- # Tests both body_indent and body_indent=
- def test_body_indent
- assert_nothing_raised { @format_o = Text::Format.new }
- assert_equal(0, @format_o.body_indent)
- assert_nothing_raised { @format_o.body_indent = 7 }
- assert_equal(7, @format_o.body_indent)
- assert_nothing_raised { @format_o.body_indent = -3 }
- assert_equal(3, @format_o.body_indent)
- assert_nothing_raised { @format_o.body_indent = "9" }
- assert_equal(9, @format_o.body_indent)
- assert_nothing_raised { @format_o.body_indent = "-2" }
- assert_equal(2, @format_o.body_indent)
- assert_match(/^ [^ ]/, @format_o.format(GETTYSBURG).split("\n")[1])
- end
- # Tests both columns and columns=
- def test_columns
- assert_nothing_raised { @format_o = Text::Format.new }
- assert_equal(72, @format_o.columns)
- assert_nothing_raised { @format_o.columns = 7 }
- assert_equal(7, @format_o.columns)
- assert_nothing_raised { @format_o.columns = -3 }
- assert_equal(3, @format_o.columns)
- assert_nothing_raised { @format_o.columns = "9" }
- assert_equal(9, @format_o.columns)
- assert_nothing_raised { @format_o.columns = "-2" }
- assert_equal(2, @format_o.columns)
- assert_nothing_raised { @format_o.columns = 40 }
- assert_equal(40, @format_o.columns)
- assert_match(/this continent$/,
- @format_o.format(GETTYSBURG).split("\n")[1])
- end
- # Tests both extra_space and extra_space=
- def test_extra_space
- assert_nothing_raised { @format_o = Text::Format.new }
- assert(!@format_o.extra_space)
- assert_nothing_raised { @format_o.extra_space = true }
- assert(@format_o.extra_space)
- # The behaviour of extra_space is tested in test_abbreviations. There
- # is no need to reproduce it here.
- end
- # Tests both first_indent and first_indent=
- def test_first_indent
- assert_nothing_raised { @format_o = Text::Format.new }
- assert_equal(4, @format_o.first_indent)
- assert_nothing_raised { @format_o.first_indent = 7 }
- assert_equal(7, @format_o.first_indent)
- assert_nothing_raised { @format_o.first_indent = -3 }
- assert_equal(3, @format_o.first_indent)
- assert_nothing_raised { @format_o.first_indent = "9" }
- assert_equal(9, @format_o.first_indent)
- assert_nothing_raised { @format_o.first_indent = "-2" }
- assert_equal(2, @format_o.first_indent)
- assert_match(/^ [^ ]/, @format_o.format(GETTYSBURG).split("\n")[0])
- end
- def test_format_style
- assert_nothing_raised { @format_o = Text::Format.new }
- assert_equal(Text::Format::LEFT_ALIGN, @format_o.format_style)
- assert_match(/^November 1863$/,
- @format_o.format(GETTYSBURG).split("\n")[-1])
- assert_nothing_raised {
- @format_o.format_style = Text::Format::RIGHT_ALIGN
- }
- assert_equal(Text::Format::RIGHT_ALIGN, @format_o.format_style)
- assert_match(/^ +November 1863$/,
- @format_o.format(GETTYSBURG).split("\n")[-1])
- assert_nothing_raised {
- @format_o.format_style = Text::Format::RIGHT_FILL
- }
- assert_equal(Text::Format::RIGHT_FILL, @format_o.format_style)
- assert_match(/^November 1863 +$/,
- @format_o.format(GETTYSBURG).split("\n")[-1])
- assert_nothing_raised { @format_o.format_style = Text::Format::JUSTIFY }
- assert_equal(Text::Format::JUSTIFY, @format_o.format_style)
- assert_match(/^of freedom, and that government of the people, by the people, for the$/,
- @format_o.format(GETTYSBURG).split("\n")[-3])
- assert_raises(ArgumentError) { @format_o.format_style = 33 }
- end
- def test_tag_paragraph
- assert_nothing_raised { @format_o = Text::Format.new }
- assert(!@format_o.tag_paragraph)
- assert_nothing_raised { @format_o.tag_paragraph = true }
- assert(@format_o.tag_paragraph)
- assert_not_equal(@format_o.paragraphs([GETTYSBURG, GETTYSBURG]),
- Text::Format.new.paragraphs([GETTYSBURG, GETTYSBURG]))
- end
- def test_tag_text
- assert_nothing_raised { @format_o = Text::Format.new }
- assert_equal([], @format_o.tag_text)
- assert_equal(@format_o.format(GETTYSBURG),
- Text::Format.new.format(GETTYSBURG))
- assert_nothing_raised {
- @format_o.tag_paragraph = true
- @format_o.tag_text = ["Gettysburg Address", "---"]
- }
- assert_not_equal(@format_o.format(GETTYSBURG),
- Text::Format.new.format(GETTYSBURG))
- assert_not_equal(@format_o.paragraphs([GETTYSBURG, GETTYSBURG]),
- Text::Format.new.paragraphs([GETTYSBURG, GETTYSBURG]))
- assert_not_equal(@format_o.paragraphs([GETTYSBURG, GETTYSBURG,
- GETTYSBURG]),
- Text::Format.new.paragraphs([GETTYSBURG, GETTYSBURG,
- GETTYSBURG]))
- end
- def test_justify?
- assert_nothing_raised { @format_o = Text::Format.new }
- assert(!@format_o.justify?)
- assert_nothing_raised {
- @format_o.format_style = Text::Format::RIGHT_ALIGN
- }
- assert(!@format_o.justify?)
- assert_nothing_raised {
- @format_o.format_style = Text::Format::RIGHT_FILL
- }
- assert(!@format_o.justify?)
- assert_nothing_raised {
- @format_o.format_style = Text::Format::JUSTIFY
- }
- assert(@format_o.justify?)
- # The format testing is done in test_format_style
- end
- def test_left_align?
- assert_nothing_raised { @format_o = Text::Format.new }
- assert(@format_o.left_align?)
- assert_nothing_raised {
- @format_o.format_style = Text::Format::RIGHT_ALIGN
- }
- assert(!@format_o.left_align?)
- assert_nothing_raised {
- @format_o.format_style = Text::Format::RIGHT_FILL
- }
- assert(!@format_o.left_align?)
- assert_nothing_raised { @format_o.format_style = Text::Format::JUSTIFY }
- assert(!@format_o.left_align?)
- # The format testing is done in test_format_style
- end
- def test_left_margin
- assert_nothing_raised { @format_o = Text::Format.new }
- assert_equal(0, @format_o.left_margin)
- assert_nothing_raised { @format_o.left_margin = -3 }
- assert_equal(3, @format_o.left_margin)
- assert_nothing_raised { @format_o.left_margin = "9" }
- assert_equal(9, @format_o.left_margin)
- assert_nothing_raised { @format_o.left_margin = "-2" }
- assert_equal(2, @format_o.left_margin)
- assert_nothing_raised { @format_o.left_margin = 7 }
- assert_equal(7, @format_o.left_margin)
- assert_nothing_raised {
- ft = @format_o.format(GETTYSBURG).split("\n")
- assert_match(/^ {11}Four score/, ft[0])
- assert_match(/^ {7}November/, ft[-1])
- }
- end
- def test_hard_margins
- assert_nothing_raised { @format_o = Text::Format.new }
- assert(!@format_o.hard_margins)
- assert_nothing_raised {
- @format_o.hard_margins = true
- @format_o.columns = 5
- @format_o.first_indent = 0
- @format_o.format_style = Text::Format::RIGHT_FILL
- }
- assert(@format_o.hard_margins)
- assert_equal(FIVE_COL, @format_o.format(GETTYSBURG))
- assert_nothing_raised {
- @format_o.split_rules |= Text::Format::SPLIT_CONTINUATION
- assert_equal(Text::Format::SPLIT_CONTINUATION_FIXED,
- @format_o.split_rules)
- }
- assert_equal(FIVE_CNT, @format_o.format(GETTYSBURG))
- end
- # Tests both nobreak and nobreak_regex, since one is only useful
- # with the other.
- def test_nobreak
- assert_nothing_raised { @format_o = Text::Format.new }
- assert(!@format_o.nobreak)
- assert(@format_o.nobreak_regex.empty?)
- assert_nothing_raised {
- @format_o.nobreak = true
- @format_o.nobreak_regex = { '^this$' => '^continent$' }
- @format_o.columns = 77
- }
- assert(@format_o.nobreak)
- assert_equal({ '^this$' => '^continent$' }, @format_o.nobreak_regex)
- assert_match(/^this continent/,
- @format_o.format(GETTYSBURG).split("\n")[1])
- end
- def test_right_align?
- assert_nothing_raised { @format_o = Text::Format.new }
- assert(!@format_o.right_align?)
- assert_nothing_raised {
- @format_o.format_style = Text::Format::RIGHT_ALIGN
- }
- assert(@format_o.right_align?)
- assert_nothing_raised {
- @format_o.format_style = Text::Format::RIGHT_FILL
- }
- assert(!@format_o.right_align?)
- assert_nothing_raised { @format_o.format_style = Text::Format::JUSTIFY }
- assert(!@format_o.right_align?)
- # The format testing is done in test_format_style
- end
- def test_right_fill?
- assert_nothing_raised { @format_o = Text::Format.new }
- assert(!@format_o.right_fill?)
- assert_nothing_raised {
- @format_o.format_style = Text::Format::RIGHT_ALIGN
- }
- assert(!@format_o.right_fill?)
- assert_nothing_raised {
- @format_o.format_style = Text::Format::RIGHT_FILL
- }
- assert(@format_o.right_fill?)
- assert_nothing_raised {
- @format_o.format_style = Text::Format::JUSTIFY
- }
- assert(!@format_o.right_fill?)
- # The format testing is done in test_format_style
- end
- def test_right_margin
- assert_nothing_raised { @format_o = Text::Format.new }
- assert_equal(0, @format_o.right_margin)
- assert_nothing_raised { @format_o.right_margin = -3 }
- assert_equal(3, @format_o.right_margin)
- assert_nothing_raised { @format_o.right_margin = "9" }
- assert_equal(9, @format_o.right_margin)
- assert_nothing_raised { @format_o.right_margin = "-2" }
- assert_equal(2, @format_o.right_margin)
- assert_nothing_raised { @format_o.right_margin = 7 }
- assert_equal(7, @format_o.right_margin)
- assert_nothing_raised {
- ft = @format_o.format(GETTYSBURG).split("\n")
- assert_match(/^ {4}Four score.*forth on$/, ft[0])
- assert_match(/^November/, ft[-1])
- }
- end
- def test_tabstop
- assert_nothing_raised { @format_o = Text::Format.new }
- assert_equal(8, @format_o.tabstop)
- assert_nothing_raised { @format_o.tabstop = 7 }
- assert_equal(7, @format_o.tabstop)
- assert_nothing_raised { @format_o.tabstop = -3 }
- assert_equal(3, @format_o.tabstop)
- assert_nothing_raised { @format_o.tabstop = "9" }
- assert_equal(9, @format_o.tabstop)
- assert_nothing_raised { @format_o.tabstop = "-2" }
- assert_equal(2, @format_o.tabstop)
- end
- def test_text
- assert_nothing_raised { @format_o = Text::Format.new }
- assert_equal([], @format_o.text)
- assert_nothing_raised { @format_o.text = "Test Text" }
- assert_equal("Test Text", @format_o.text)
- assert_nothing_raised { @format_o.text = ["Line 1", "Line 2"] }
- assert_equal(["Line 1", "Line 2"], @format_o.text)
- end
- def test_s_new
- # new(NilClass) { block }
- assert_nothing_raised do
- @format_o = Text::Format.new {
- self.text = "Test 1, 2, 3"
- }
- end
- assert_equal("Test 1, 2, 3", @format_o.text)
- # new(Hash Symbols)
- assert_nothing_raised { @format_o = Text::Format.new(:columns => 72) }
- assert_equal(72, @format_o.columns)
- # new(Hash String)
- assert_nothing_raised { @format_o = Text::Format.new('columns' => 72) }
- assert_equal(72, @format_o.columns)
- # new(Hash) { block }
- assert_nothing_raised do
- @format_o = Text::Format.new('columns' => 80) {
- self.text = "Test 4, 5, 6"
- }
- end
- assert_equal("Test 4, 5, 6", @format_o.text)
- assert_equal(80, @format_o.columns)
- # new(Text::Format)
- assert_nothing_raised do
- fo = Text::Format.new(@format_o)
- assert(fo == @format_o)
- end
- # new(Text::Format) { block }
- assert_nothing_raised do
- fo = Text::Format.new(@format_o) { self.columns = 79 }
- assert(fo != @format_o)
- end
- # new(String)
- assert_nothing_raised { @format_o = Text::Format.new("Test A, B, C") }
- assert_equal("Test A, B, C", @format_o.text)
- # new(String) { block }
- assert_nothing_raised do
- @format_o = Text::Format.new("Test X, Y, Z") { self.columns = -5 }
- end
- assert_equal("Test X, Y, Z", @format_o.text)
- assert_equal(5, @format_o.columns)
- end
- def test_center
- assert_nothing_raised { @format_o = Text::Format.new }
- assert_nothing_raised do
- ct = @format_o.center(GETTYSBURG.split("\n")).split("\n")
- assert_match(/^ Four score and seven years ago our fathers brought forth on this/, ct[0])
- assert_match(/^ not perish from the earth./, ct[-3])
- end
- end
- def test_expand
- assert_nothing_raised { @format_o = Text::Format.new }
- assert_equal(" ", @format_o.expand("\t "))
- assert_nothing_raised { @format_o.tabstop = 4 }
- assert_equal(" ", @format_o.expand("\t "))
- end
- def test_unexpand
- assert_nothing_raised { @format_o = Text::Format.new }
- assert_equal("\t ", @format_o.unexpand(" "))
- assert_nothing_raised { @format_o.tabstop = 4 }
- assert_equal("\t ", @format_o.unexpand(" "))
- end
- def test_space_only
- assert_equal("", Text::Format.new.format(" "))
- assert_equal("", Text::Format.new.format("\n"))
- assert_equal("", Text::Format.new.format(" "))
- assert_equal("", Text::Format.new.format(" \n"))
- assert_equal("", Text::Format.new.paragraphs("\n"))
- assert_equal("", Text::Format.new.paragraphs(" "))
- assert_equal("", Text::Format.new.paragraphs(" "))
- assert_equal("", Text::Format.new.paragraphs(" \n"))
- assert_equal("", Text::Format.new.paragraphs(["\n"]))
- assert_equal("", Text::Format.new.paragraphs([" "]))
- assert_equal("", Text::Format.new.paragraphs([" "]))
- assert_equal("", Text::Format.new.paragraphs([" \n"]))
- end
- def test_splendiferous
- h = nil
- test = "This is a splendiferous test"
- assert_nothing_raised { @format_o = Text::Format.new(:columns => 6, :left_margin => 0, :indent => 0, :first_indent => 0) }
- assert_match(/^splendiferous$/, @format_o.format(test))
- assert_nothing_raised { @format_o.hard_margins = true }
- assert_match(/^lendif$/, @format_o.format(test))
- assert_nothing_raised { h = Object.new }
- assert_nothing_raised do
- @format_o.split_rules = Text::Format::SPLIT_HYPHENATION
- class << h #:nodoc:
- def hyphenate_to(word, size)
- return ["", word] if size < 2
- [word[0 ... size], word[size .. -1]]
- end
- end
- @format_o.hyphenator = h
- end
- assert_match(/^iferou$/, @format_o.format(test))
- assert_nothing_raised { h = Object.new }
- assert_nothing_raised do
- class << h #:nodoc:
- def hyphenate_to(word, size, formatter)
- return ["", word] if word.size < formatter.columns
- [word[0 ... size], word[size .. -1]]
- end
- end
- @format_o.hyphenator = h
- end
- assert_match(/^ferous$/, @format_o.format(test))
- end
- end
- end
- #
- # address.rb
- #
- #--
- # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
- #
- # 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.
- #
- # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
- # with permission of Minero Aoki.
- #++
- 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
- require 'stringio'
- module TMail
- class Attachment < StringIO
- attr_accessor :original_filename, :content_type
- end
- class Mail
- def has_attachments?
- multipart? && parts.any? { |part| attachment?(part) }
- end
- def attachment?(part)
- (part['content-disposition'] && part['content-disposition'].disposition == "attachment") ||
- part.header['content-type'].main_type != "text"
- end
- def attachments
- if multipart?
- parts.collect { |part|
- if attachment?(part)
- content = part.body # unquoted automatically by TMail#body
- file_name = (part['content-location'] &&
- part['content-location'].body) ||
- part.sub_header("content-type", "name") ||
- part.sub_header("content-disposition", "filename")
-
- next if file_name.blank? || content.blank?
-
- attachment = Attachment.new(content)
- attachment.original_filename = file_name.strip
- attachment.content_type = part.content_type
- attachment
- end
- }.compact
- end
- end
- end
- end
- #
- # base64.rb
- #
- #--
- # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
- #
- # 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.
- #
- # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
- # with permission of Minero Aoki.
- #++
- 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
- #
- # config.rb
- #
- #--
- # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
- #
- # 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.
- #
- # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
- # with permission of Minero Aoki.
- #++
- 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
- #
- # encode.rb
- #
- #--
- # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
- #
- # 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.
- #
- # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
- # with permission of Minero Aoki.
- #++
- 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 )
- return if v.nil?
- 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) unless m.nil?
- 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
- #
- # facade.rb
- #
- #--
- # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
- #
- # 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.
- #
- # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
- # with permission of Minero Aoki.
- #++
- 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
- alias quoted_subject subject
- 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
- store('Content-Disposition', str)
- h = @header['content-disposition']
- 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
- #
- # header.rb
- #
- #--
- # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
- #
- # 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.
- #
- # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
- # with permission of Minero Aoki.
- #++
- 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|
- if v
- strategy.meta ';'
- strategy.space
- strategy.kv_pair k, v
- end
- 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
- #
- # info.rb
- #
- #--
- # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
- #
- # 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.
- #
- # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
- # with permission of Minero Aoki.
- #++
- module TMail
- Version = '0.10.7'
- Copyright = 'Copyright (c) 1998-2002 Minero Aoki'
- end
- require 'tmail/mailbox'
- #
- # mail.rb
- #
- #--
- # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
- #
- # 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.
- #
- # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
- # with permission of Minero Aoki.
- #++
- require 'tmail/facade'
- require 'tmail/encode'
- require 'tmail/header'
- require 'tmail/port'
- require 'tmail/config'
- require 'tmail/utils'
- require 'tmail/attachments'
- require 'tmail/quoting'
- 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
- def sub_header(key, param)
- (hdr = self[key]) ? hdr[param] : nil
- 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
- when /^charset=.*/
-
- 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 quoted_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
- #
- # mailbox.rb
- #
- #--
- # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
- #
- # 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.
- #
- # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
- # with permission of Minero Aoki.
- #++
- 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
- require 'tmail/mailbox'
- #
- # net.rb
- #
- #--
- # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
- #
- # 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.
- #
- # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
- # with permission of Minero Aoki.
- #++
- 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_message_id(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
- #
- # obsolete.rb
- #
- #--
- # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
- #
- # 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.
- #
- # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
- # with permission of Minero Aoki.
- #++
- 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
- #
- # DO NOT MODIFY!!!!
- # This file is automatically generated by racc 1.4.3
- # from racc grammer file "parser.y".
- #
- #
- # parser.rb: generated by racc (runtime embedded)
- #
- ###### racc/parser.rb
- unless $".index 'racc/parser.rb'
- $".push 'racc/parser.rb'
- self.class.module_eval <<'..end /home/aamine/lib/ruby/racc/parser.rb modeval..idb76f2e220d', '/home/aamine/lib/ruby/racc/parser.rb', 1
- #
- # parser.rb
- #
- # Copyright (c) 1999-2003 Minero Aoki <aamine@loveruby.net>
- #
- # This program is free software.
- # You can distribute/modify this program under the same terms of ruby.
- #
- # As a special exception, when this code is copied by Racc
- # into a Racc output file, you may use that output file
- # without restriction.
- #
- # $Id: parser.rb,v 1.1.1.1 2004/10/14 11:59:58 webster132 Exp $
- #
- unless defined? NotImplementedError
- NotImplementedError = NotImplementError
- end
- module Racc
- class ParseError < StandardError; end
- end
- unless defined?(::ParseError)
- ParseError = Racc::ParseError
- end
- module Racc
- unless defined? Racc_No_Extentions
- Racc_No_Extentions = false
- end
- class Parser
- Racc_Runtime_Version = '1.4.3'
- Racc_Runtime_Revision = '$Revision: 1.1.1.1 $'.split(/\s+/)[1]
- Racc_Runtime_Core_Version_R = '1.4.3'
- Racc_Runtime_Core_Revision_R = '$Revision: 1.1.1.1 $'.split(/\s+/)[1]
- begin
- require 'racc/cparse'
- # Racc_Runtime_Core_Version_C = (defined in extention)
- Racc_Runtime_Core_Revision_C = Racc_Runtime_Core_Id_C.split(/\s+/)[2]
- unless new.respond_to?(:_racc_do_parse_c, true)
- raise LoadError, 'old cparse.so'
- end
- if Racc_No_Extentions
- raise LoadError, 'selecting ruby version of racc runtime core'
- end
- Racc_Main_Parsing_Routine = :_racc_do_parse_c
- Racc_YY_Parse_Method = :_racc_yyparse_c
- Racc_Runtime_Core_Version = Racc_Runtime_Core_Version_C
- Racc_Runtime_Core_Revision = Racc_Runtime_Core_Revision_C
- Racc_Runtime_Type = 'c'
- rescue LoadError
- Racc_Main_Parsing_Routine = :_racc_do_parse_rb
- Racc_YY_Parse_Method = :_racc_yyparse_rb
- Racc_Runtime_Core_Version = Racc_Runtime_Core_Version_R
- Racc_Runtime_Core_Revision = Racc_Runtime_Core_Revision_R
- Racc_Runtime_Type = 'ruby'
- end
- def self.racc_runtime_type
- Racc_Runtime_Type
- end
- private
- def _racc_setup
- @yydebug = false unless self.class::Racc_debug_parser
- @yydebug = false unless defined? @yydebug
- if @yydebug
- @racc_debug_out = $stderr unless defined? @racc_debug_out
- @racc_debug_out ||= $stderr
- end
- arg = self.class::Racc_arg
- arg[13] = true if arg.size < 14
- arg
- end
- def _racc_init_sysvars
- @racc_state = [0]
- @racc_tstack = []
- @racc_vstack = []
- @racc_t = nil
- @racc_val = nil
- @racc_read_next = true
- @racc_user_yyerror = false
- @racc_error_status = 0
- end
- ###
- ### do_parse
- ###
- def do_parse
- __send__ Racc_Main_Parsing_Routine, _racc_setup(), false
- end
- def next_token
- raise NotImplementedError, "#{self.class}\#next_token is not defined"
- end
- def _racc_do_parse_rb( arg, in_debug )
- action_table, action_check, action_default, action_pointer,
- goto_table, goto_check, goto_default, goto_pointer,
- nt_base, reduce_table, token_table, shift_n,
- reduce_n, use_result, * = arg
- _racc_init_sysvars
- tok = act = i = nil
- nerr = 0
- catch(:racc_end_parse) {
- while true
- if i = action_pointer[@racc_state[-1]]
- if @racc_read_next
- if @racc_t != 0 # not EOF
- tok, @racc_val = next_token()
- unless tok # EOF
- @racc_t = 0
- else
- @racc_t = (token_table[tok] or 1) # error token
- end
- racc_read_token(@racc_t, tok, @racc_val) if @yydebug
- @racc_read_next = false
- end
- end
- i += @racc_t
- if i >= 0 and
- act = action_table[i] and
- action_check[i] == @racc_state[-1]
- ;
- else
- act = action_default[@racc_state[-1]]
- end
- else
- act = action_default[@racc_state[-1]]
- end
- while act = _racc_evalact(act, arg)
- end
- end
- }
- end
- ###
- ### yyparse
- ###
- def yyparse( recv, mid )
- __send__ Racc_YY_Parse_Method, recv, mid, _racc_setup(), true
- end
- def _racc_yyparse_rb( recv, mid, arg, c_debug )
- action_table, action_check, action_default, action_pointer,
- goto_table, goto_check, goto_default, goto_pointer,
- nt_base, reduce_table, token_table, shift_n,
- reduce_n, use_result, * = arg
- _racc_init_sysvars
- tok = nil
- act = nil
- i = nil
- nerr = 0
- catch(:racc_end_parse) {
- until i = action_pointer[@racc_state[-1]]
- while act = _racc_evalact(action_default[@racc_state[-1]], arg)
- end
- end
- recv.__send__(mid) do |tok, val|
- # $stderr.puts "rd: tok=#{tok}, val=#{val}"
- unless tok
- @racc_t = 0
- else
- @racc_t = (token_table[tok] or 1) # error token
- end
- @racc_val = val
- @racc_read_next = false
- i += @racc_t
- if i >= 0 and
- act = action_table[i] and
- action_check[i] == @racc_state[-1]
- ;
- # $stderr.puts "01: act=#{act}"
- else
- act = action_default[@racc_state[-1]]
- # $stderr.puts "02: act=#{act}"
- # $stderr.puts "curstate=#{@racc_state[-1]}"
- end
- while act = _racc_evalact(act, arg)
- end
- while not (i = action_pointer[@racc_state[-1]]) or
- not @racc_read_next or
- @racc_t == 0 # $
- if i and i += @racc_t and
- i >= 0 and
- act = action_table[i] and
- action_check[i] == @racc_state[-1]
- ;
- # $stderr.puts "03: act=#{act}"
- else
- # $stderr.puts "04: act=#{act}"
- act = action_default[@racc_state[-1]]
- end
- while act = _racc_evalact(act, arg)
- end
- end
- end
- }
- end
- ###
- ### common
- ###
- def _racc_evalact( act, arg )
- # $stderr.puts "ea: act=#{act}"
- action_table, action_check, action_default, action_pointer,
- goto_table, goto_check, goto_default, goto_pointer,
- nt_base, reduce_table, token_table, shift_n,
- reduce_n, use_result, * = arg
- nerr = 0 # tmp
- if act > 0 and act < shift_n
- #
- # shift
- #
- if @racc_error_status > 0
- @racc_error_status -= 1 unless @racc_t == 1 # error token
- end
- @racc_vstack.push @racc_val
- @racc_state.push act
- @racc_read_next = true
- if @yydebug
- @racc_tstack.push @racc_t
- racc_shift @racc_t, @racc_tstack, @racc_vstack
- end
- elsif act < 0 and act > -reduce_n
- #
- # reduce
- #
- code = catch(:racc_jump) {
- @racc_state.push _racc_do_reduce(arg, act)
- false
- }
- if code
- case code
- when 1 # yyerror
- @racc_user_yyerror = true # user_yyerror
- return -reduce_n
- when 2 # yyaccept
- return shift_n
- else
- raise RuntimeError, '[Racc Bug] unknown jump code'
- end
- end
- elsif act == shift_n
- #
- # accept
- #
- racc_accept if @yydebug
- throw :racc_end_parse, @racc_vstack[0]
- elsif act == -reduce_n
- #
- # error
- #
- case @racc_error_status
- when 0
- unless arg[21] # user_yyerror
- nerr += 1
- on_error @racc_t, @racc_val, @racc_vstack
- end
- when 3
- if @racc_t == 0 # is $
- throw :racc_end_parse, nil
- end
- @racc_read_next = true
- end
- @racc_user_yyerror = false
- @racc_error_status = 3
- while true
- if i = action_pointer[@racc_state[-1]]
- i += 1 # error token
- if i >= 0 and
- (act = action_table[i]) and
- action_check[i] == @racc_state[-1]
- break
- end
- end
- throw :racc_end_parse, nil if @racc_state.size < 2
- @racc_state.pop
- @racc_vstack.pop
- if @yydebug
- @racc_tstack.pop
- racc_e_pop @racc_state, @racc_tstack, @racc_vstack
- end
- end
- return act
- else
- raise RuntimeError, "[Racc Bug] unknown action #{act.inspect}"
- end
- racc_next_state(@racc_state[-1], @racc_state) if @yydebug
- nil
- end
- def _racc_do_reduce( arg, act )
- action_table, action_check, action_default, action_pointer,
- goto_table, goto_check, goto_default, goto_pointer,
- nt_base, reduce_table, token_table, shift_n,
- reduce_n, use_result, * = arg
- state = @racc_state
- vstack = @racc_vstack
- tstack = @racc_tstack
- i = act * -3
- len = reduce_table[i]
- reduce_to = reduce_table[i+1]
- method_id = reduce_table[i+2]
- void_array = []
- tmp_t = tstack[-len, len] if @yydebug
- tmp_v = vstack[-len, len]
- tstack[-len, len] = void_array if @yydebug
- vstack[-len, len] = void_array
- state[-len, len] = void_array
- # tstack must be updated AFTER method call
- if use_result
- vstack.push __send__(method_id, tmp_v, vstack, tmp_v[0])
- else
- vstack.push __send__(method_id, tmp_v, vstack)
- end
- tstack.push reduce_to
- racc_reduce(tmp_t, reduce_to, tstack, vstack) if @yydebug
- k1 = reduce_to - nt_base
- if i = goto_pointer[k1]
- i += state[-1]
- if i >= 0 and (curstate = goto_table[i]) and goto_check[i] == k1
- return curstate
- end
- end
- goto_default[k1]
- end
- def on_error( t, val, vstack )
- raise ParseError, sprintf("\nparse error on value %s (%s)",
- val.inspect, token_to_str(t) || '?')
- end
- def yyerror
- throw :racc_jump, 1
- end
- def yyaccept
- throw :racc_jump, 2
- end
- def yyerrok
- @racc_error_status = 0
- end
- # for debugging output
- def racc_read_token( t, tok, val )
- @racc_debug_out.print 'read '
- @racc_debug_out.print tok.inspect, '(', racc_token2str(t), ') '
- @racc_debug_out.puts val.inspect
- @racc_debug_out.puts
- end
- def racc_shift( tok, tstack, vstack )
- @racc_debug_out.puts "shift #{racc_token2str tok}"
- racc_print_stacks tstack, vstack
- @racc_debug_out.puts
- end
- def racc_reduce( toks, sim, tstack, vstack )
- out = @racc_debug_out
- out.print 'reduce '
- if toks.empty?
- out.print ' <none>'
- else
- toks.each {|t| out.print ' ', racc_token2str(t) }
- end
- out.puts " --> #{racc_token2str(sim)}"
-
- racc_print_stacks tstack, vstack
- @racc_debug_out.puts
- end
- def racc_accept
- @racc_debug_out.puts 'accept'
- @racc_debug_out.puts
- end
- def racc_e_pop( state, tstack, vstack )
- @racc_debug_out.puts 'error recovering mode: pop token'
- racc_print_states state
- racc_print_stacks tstack, vstack
- @racc_debug_out.puts
- end
- def racc_next_state( curstate, state )
- @racc_debug_out.puts "goto #{curstate}"
- racc_print_states state
- @racc_debug_out.puts
- end
- def racc_print_stacks( t, v )
- out = @racc_debug_out
- out.print ' ['
- t.each_index do |i|
- out.print ' (', racc_token2str(t[i]), ' ', v[i].inspect, ')'
- end
- out.puts ' ]'
- end
- def racc_print_states( s )
- out = @racc_debug_out
- out.print ' ['
- s.each {|st| out.print ' ', st }
- out.puts ' ]'
- end
- def racc_token2str( tok )
- self.class::Racc_token_to_s_table[tok] or
- raise RuntimeError, "[Racc Bug] can't convert token #{tok} to string"
- end
- def token_to_str( t )
- self.class::Racc_token_to_s_table[t]
- end
- end
- end
- ..end /home/aamine/lib/ruby/racc/parser.rb modeval..idb76f2e220d
- end # end of racc/parser.rb
- #
- # parser.rb
- #
- #--
- # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
- #
- # 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.
- #
- # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
- # with permission of Minero Aoki.
- #++
- require 'tmail/scanner'
- require 'tmail/utils'
- module TMail
- class Parser < Racc::Parser
- module_eval <<'..end parser.y modeval..id43721faf1c', 'parser.y', 331
- include TextUtils
- def self.parse( ident, str, cmt = nil )
- new.parse(ident, str, cmt)
- end
- MAILP_DEBUG = false
- def initialize
- self.debug = MAILP_DEBUG
- end
- def debug=( flag )
- @yydebug = flag && Racc_debug_parser
- @scanner_debug = flag
- end
- def debug
- @yydebug
- end
- def parse( ident, str, comments = nil )
- @scanner = Scanner.new(str, ident, comments)
- @scanner.debug = @scanner_debug
- @first = [ident, ident]
- result = yyparse(self, :parse_in)
- comments.map! {|c| to_kcode(c) } if comments
- result
- end
- private
- def parse_in( &block )
- yield @first
- @scanner.scan(&block)
- end
-
- def on_error( t, val, vstack )
- raise SyntaxError, "parse error on token #{racc_token2str t}"
- end
- ..end parser.y modeval..id43721faf1c
- ##### racc 1.4.3 generates ###
- racc_reduce_table = [
- 0, 0, :racc_error,
- 2, 35, :_reduce_1,
- 2, 35, :_reduce_2,
- 2, 35, :_reduce_3,
- 2, 35, :_reduce_4,
- 2, 35, :_reduce_5,
- 2, 35, :_reduce_6,
- 2, 35, :_reduce_7,
- 2, 35, :_reduce_8,
- 2, 35, :_reduce_9,
- 2, 35, :_reduce_10,
- 2, 35, :_reduce_11,
- 2, 35, :_reduce_12,
- 6, 36, :_reduce_13,
- 0, 48, :_reduce_none,
- 2, 48, :_reduce_none,
- 3, 49, :_reduce_16,
- 5, 49, :_reduce_17,
- 1, 50, :_reduce_18,
- 7, 37, :_reduce_19,
- 0, 51, :_reduce_none,
- 2, 51, :_reduce_21,
- 0, 52, :_reduce_none,
- 2, 52, :_reduce_23,
- 1, 58, :_reduce_24,
- 3, 58, :_reduce_25,
- 2, 58, :_reduce_26,
- 0, 53, :_reduce_none,
- 2, 53, :_reduce_28,
- 0, 54, :_reduce_29,
- 3, 54, :_reduce_30,
- 0, 55, :_reduce_none,
- 2, 55, :_reduce_32,
- 2, 55, :_reduce_33,
- 0, 56, :_reduce_none,
- 2, 56, :_reduce_35,
- 1, 61, :_reduce_36,
- 1, 61, :_reduce_37,
- 0, 57, :_reduce_none,
- 2, 57, :_reduce_39,
- 1, 38, :_reduce_none,
- 1, 38, :_reduce_none,
- 3, 38, :_reduce_none,
- 1, 46, :_reduce_none,
- 1, 46, :_reduce_none,
- 1, 46, :_reduce_none,
- 1, 39, :_reduce_none,
- 2, 39, :_reduce_47,
- 1, 64, :_reduce_48,
- 3, 64, :_reduce_49,
- 1, 68, :_reduce_none,
- 1, 68, :_reduce_none,
- 1, 69, :_reduce_52,
- 3, 69, :_reduce_53,
- 1, 47, :_reduce_none,
- 1, 47, :_reduce_none,
- 2, 47, :_reduce_56,
- 2, 67, :_reduce_none,
- 3, 65, :_reduce_58,
- 2, 65, :_reduce_59,
- 1, 70, :_reduce_60,
- 2, 70, :_reduce_61,
- 4, 62, :_reduce_62,
- 3, 62, :_reduce_63,
- 2, 72, :_reduce_none,
- 2, 73, :_reduce_65,
- 4, 73, :_reduce_66,
- 3, 63, :_reduce_67,
- 1, 63, :_reduce_68,
- 1, 74, :_reduce_none,
- 2, 74, :_reduce_70,
- 1, 71, :_reduce_71,
- 3, 71, :_reduce_72,
- 1, 59, :_reduce_73,
- 3, 59, :_reduce_74,
- 1, 76, :_reduce_75,
- 2, 76, :_reduce_76,
- 1, 75, :_reduce_none,
- 1, 75, :_reduce_none,
- 1, 75, :_reduce_none,
- 1, 77, :_reduce_none,
- 1, 77, :_reduce_none,
- 1, 77, :_reduce_none,
- 1, 66, :_reduce_none,
- 2, 66, :_reduce_none,
- 3, 60, :_reduce_85,
- 1, 40, :_reduce_86,
- 3, 40, :_reduce_87,
- 1, 79, :_reduce_none,
- 2, 79, :_reduce_89,
- 1, 41, :_reduce_90,
- 2, 41, :_reduce_91,
- 3, 42, :_reduce_92,
- 5, 43, :_reduce_93,
- 3, 43, :_reduce_94,
- 0, 80, :_reduce_95,
- 5, 80, :_reduce_96,
- 1, 82, :_reduce_none,
- 1, 82, :_reduce_none,
- 1, 44, :_reduce_99,
- 3, 45, :_reduce_100,
- 0, 81, :_reduce_none,
- 1, 81, :_reduce_none,
- 1, 78, :_reduce_none,
- 1, 78, :_reduce_none,
- 1, 78, :_reduce_none,
- 1, 78, :_reduce_none,
- 1, 78, :_reduce_none,
- 1, 78, :_reduce_none,
- 1, 78, :_reduce_none ]
- racc_reduce_n = 110
- racc_shift_n = 168
- racc_action_table = [
- -70, -69, 23, 25, 146, 147, 29, 31, 105, 106,
- 16, 17, 20, 22, 136, 27, -70, -69, 32, 101,
- -70, -69, 154, 100, 113, 115, -70, -69, -70, 109,
- 75, 23, 25, 101, 155, 29, 31, 142, 143, 16,
- 17, 20, 22, 107, 27, 23, 25, 32, 98, 29,
- 31, 96, 94, 16, 17, 20, 22, 78, 27, 23,
- 25, 32, 112, 29, 31, 74, 91, 16, 17, 20,
- 22, 88, 117, 92, 81, 32, 23, 25, 80, 123,
- 29, 31, 100, 125, 16, 17, 20, 22, 126, 23,
- 25, 109, 32, 29, 31, 91, 128, 16, 17, 20,
- 22, 129, 27, 23, 25, 32, 101, 29, 31, 101,
- 130, 16, 17, 20, 22, 79, 52, 23, 25, 32,
- 78, 29, 31, 133, 78, 16, 17, 20, 22, 77,
- 23, 25, 75, 32, 29, 31, 65, 62, 16, 17,
- 20, 22, 139, 23, 25, 101, 32, 29, 31, 60,
- 100, 16, 17, 20, 22, 44, 27, 101, 148, 32,
- 23, 25, 120, 149, 29, 31, 152, 153, 16, 17,
- 20, 22, 42, 27, 157, 159, 32, 23, 25, 120,
- 40, 29, 31, 15, 164, 16, 17, 20, 22, 40,
- 27, 23, 25, 32, 68, 29, 31, 166, 167, 16,
- 17, 20, 22, nil, 27, 23, 25, 32, nil, 29,
- 31, 74, nil, 16, 17, 20, 22, nil, 23, 25,
- nil, 32, 29, 31, nil, nil, 16, 17, 20, 22,
- nil, 23, 25, nil, 32, 29, 31, nil, nil, 16,
- 17, 20, 22, nil, 23, 25, nil, 32, 29, 31,
- nil, nil, 16, 17, 20, 22, nil, 23, 25, nil,
- 32, 29, 31, nil, nil, 16, 17, 20, 22, nil,
- 27, 23, 25, 32, nil, 29, 31, nil, nil, 16,
- 17, 20, 22, nil, 23, 25, nil, 32, 29, 31,
- nil, nil, 16, 17, 20, 22, nil, 23, 25, nil,
- 32, 29, 31, nil, nil, 16, 17, 20, 22, nil,
- 84, 25, nil, 32, 29, 31, nil, 87, 16, 17,
- 20, 22, 4, 6, 7, 8, 9, 10, 11, 12,
- 13, 1, 2, 3, 84, 25, nil, nil, 29, 31,
- nil, 87, 16, 17, 20, 22, 84, 25, nil, nil,
- 29, 31, nil, 87, 16, 17, 20, 22, 84, 25,
- nil, nil, 29, 31, nil, 87, 16, 17, 20, 22,
- 84, 25, nil, nil, 29, 31, nil, 87, 16, 17,
- 20, 22, 84, 25, nil, nil, 29, 31, nil, 87,
- 16, 17, 20, 22, 84, 25, nil, nil, 29, 31,
- nil, 87, 16, 17, 20, 22 ]
- racc_action_check = [
- 75, 28, 68, 68, 136, 136, 68, 68, 72, 72,
- 68, 68, 68, 68, 126, 68, 75, 28, 68, 67,
- 75, 28, 143, 66, 86, 86, 75, 28, 75, 75,
- 28, 3, 3, 86, 143, 3, 3, 134, 134, 3,
- 3, 3, 3, 73, 3, 152, 152, 3, 62, 152,
- 152, 60, 56, 152, 152, 152, 152, 51, 152, 52,
- 52, 152, 80, 52, 52, 52, 50, 52, 52, 52,
- 52, 45, 89, 52, 42, 52, 71, 71, 41, 96,
- 71, 71, 97, 98, 71, 71, 71, 71, 100, 7,
- 7, 101, 71, 7, 7, 102, 104, 7, 7, 7,
- 7, 105, 7, 8, 8, 7, 108, 8, 8, 111,
- 112, 8, 8, 8, 8, 40, 8, 9, 9, 8,
- 36, 9, 9, 117, 121, 9, 9, 9, 9, 33,
- 10, 10, 70, 9, 10, 10, 13, 12, 10, 10,
- 10, 10, 130, 2, 2, 131, 10, 2, 2, 11,
- 135, 2, 2, 2, 2, 6, 2, 138, 139, 2,
- 90, 90, 90, 140, 90, 90, 141, 142, 90, 90,
- 90, 90, 5, 90, 148, 151, 90, 127, 127, 127,
- 4, 127, 127, 1, 157, 127, 127, 127, 127, 159,
- 127, 26, 26, 127, 26, 26, 26, 163, 164, 26,
- 26, 26, 26, nil, 26, 27, 27, 26, nil, 27,
- 27, 27, nil, 27, 27, 27, 27, nil, 155, 155,
- nil, 27, 155, 155, nil, nil, 155, 155, 155, 155,
- nil, 122, 122, nil, 155, 122, 122, nil, nil, 122,
- 122, 122, 122, nil, 76, 76, nil, 122, 76, 76,
- nil, nil, 76, 76, 76, 76, nil, 38, 38, nil,
- 76, 38, 38, nil, nil, 38, 38, 38, 38, nil,
- 38, 55, 55, 38, nil, 55, 55, nil, nil, 55,
- 55, 55, 55, nil, 94, 94, nil, 55, 94, 94,
- nil, nil, 94, 94, 94, 94, nil, 59, 59, nil,
- 94, 59, 59, nil, nil, 59, 59, 59, 59, nil,
- 114, 114, nil, 59, 114, 114, nil, 114, 114, 114,
- 114, 114, 0, 0, 0, 0, 0, 0, 0, 0,
- 0, 0, 0, 0, 77, 77, nil, nil, 77, 77,
- nil, 77, 77, 77, 77, 77, 44, 44, nil, nil,
- 44, 44, nil, 44, 44, 44, 44, 44, 113, 113,
- nil, nil, 113, 113, nil, 113, 113, 113, 113, 113,
- 88, 88, nil, nil, 88, 88, nil, 88, 88, 88,
- 88, 88, 74, 74, nil, nil, 74, 74, nil, 74,
- 74, 74, 74, 74, 129, 129, nil, nil, 129, 129,
- nil, 129, 129, 129, 129, 129 ]
- racc_action_pointer = [
- 320, 152, 129, 17, 165, 172, 137, 75, 89, 103,
- 116, 135, 106, 105, nil, nil, nil, nil, nil, nil,
- nil, nil, nil, nil, nil, nil, 177, 191, 1, nil,
- nil, nil, nil, 109, nil, nil, 94, nil, 243, nil,
- 99, 64, 74, nil, 332, 52, nil, nil, nil, nil,
- 50, 31, 45, nil, nil, 257, 36, nil, nil, 283,
- 22, nil, 16, nil, nil, nil, -3, -10, -12, nil,
- 103, 62, -8, 15, 368, 0, 230, 320, nil, nil,
- 47, nil, nil, nil, nil, nil, 4, nil, 356, 50,
- 146, nil, nil, nil, 270, nil, 65, 56, 52, nil,
- 57, 62, 79, nil, 68, 81, nil, nil, 77, nil,
- nil, 80, 96, 344, 296, nil, nil, 108, nil, nil,
- nil, 98, 217, nil, nil, nil, -19, 163, nil, 380,
- 128, 116, nil, nil, 14, 124, -26, nil, 128, 141,
- 148, 141, 152, 7, nil, nil, nil, nil, 160, nil,
- nil, 149, 31, nil, nil, 204, nil, 167, nil, 174,
- nil, nil, nil, 169, 184, nil, nil, nil ]
- racc_action_default = [
- -110, -110, -110, -110, -14, -110, -20, -110, -110, -110,
- -110, -110, -110, -110, -10, -95, -106, -107, -77, -44,
- -108, -11, -109, -79, -43, -103, -110, -110, -60, -104,
- -55, -105, -78, -68, -54, -71, -45, -12, -110, -1,
- -110, -110, -110, -2, -110, -22, -51, -48, -50, -3,
- -40, -41, -110, -46, -4, -86, -5, -88, -6, -90,
- -110, -7, -95, -8, -9, -99, -101, -61, -59, -56,
- -69, -110, -110, -110, -110, -75, -110, -110, -57, -15,
- -110, 168, -73, -80, -82, -21, -24, -81, -110, -27,
- -110, -83, -47, -89, -110, -91, -110, -101, -110, -100,
- -102, -75, -58, -52, -110, -110, -64, -63, -65, -76,
- -72, -67, -110, -110, -110, -26, -23, -110, -29, -49,
- -84, -42, -87, -92, -94, -95, -110, -110, -62, -110,
- -110, -25, -74, -28, -31, -101, -110, -53, -66, -110,
- -110, -34, -110, -110, -93, -96, -98, -97, -110, -18,
- -13, -38, -110, -30, -33, -110, -32, -16, -19, -14,
- -35, -36, -37, -110, -110, -39, -85, -17 ]
- racc_goto_table = [
- 39, 67, 70, 73, 24, 37, 69, 66, 36, 38,
- 57, 59, 55, 67, 108, 83, 90, 111, 69, 99,
- 85, 49, 53, 76, 158, 134, 141, 70, 73, 151,
- 118, 89, 45, 156, 160, 150, 140, 21, 14, 19,
- 119, 102, 64, 63, 61, 83, 70, 104, 83, 58,
- 124, 132, 56, 131, 97, 54, 93, 43, 5, 83,
- 95, 145, 76, nil, 116, 76, nil, nil, 127, 138,
- 103, nil, nil, nil, 38, nil, nil, 110, nil, nil,
- nil, nil, nil, nil, 83, 83, nil, nil, 144, nil,
- nil, nil, nil, nil, nil, 57, 121, 122, nil, nil,
- 83, nil, nil, nil, nil, nil, nil, nil, nil, nil,
- nil, nil, nil, nil, nil, nil, nil, 135, nil, nil,
- nil, nil, nil, 93, nil, nil, nil, 70, 162, 137,
- 70, 163, 161, 38, nil, nil, nil, nil, nil, nil,
- nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
- nil, nil, nil, nil, nil, 165 ]
- racc_goto_check = [
- 2, 37, 37, 29, 13, 13, 28, 46, 31, 36,
- 41, 41, 45, 37, 25, 44, 32, 25, 28, 47,
- 24, 4, 4, 42, 23, 20, 21, 37, 29, 22,
- 19, 18, 17, 26, 27, 16, 15, 12, 11, 33,
- 34, 35, 10, 9, 8, 44, 37, 29, 44, 7,
- 47, 43, 6, 25, 46, 5, 41, 3, 1, 44,
- 41, 48, 42, nil, 24, 42, nil, nil, 32, 25,
- 13, nil, nil, nil, 36, nil, nil, 41, nil, nil,
- nil, nil, nil, nil, 44, 44, nil, nil, 47, nil,
- nil, nil, nil, nil, nil, 41, 31, 45, nil, nil,
- 44, nil, nil, nil, nil, nil, nil, nil, nil, nil,
- nil, nil, nil, nil, nil, nil, nil, 46, nil, nil,
- nil, nil, nil, 41, nil, nil, nil, 37, 29, 13,
- 37, 29, 28, 36, nil, nil, nil, nil, nil, nil,
- nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
- nil, nil, nil, nil, nil, 2 ]
- racc_goto_pointer = [
- nil, 58, -4, 51, 14, 47, 43, 39, 33, 31,
- 29, 37, 35, 2, nil, -94, -105, 26, -14, -59,
- -93, -108, -112, -127, -24, -60, -110, -118, -20, -24,
- nil, 6, -34, 37, -50, -27, 6, -25, nil, nil,
- nil, 1, -5, -63, -29, 3, -8, -47, -75 ]
- racc_goto_default = [
- nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
- nil, nil, nil, 48, 41, nil, nil, nil, nil, nil,
- nil, nil, nil, nil, nil, 86, nil, nil, 30, 34,
- 50, 51, nil, 46, 47, nil, 26, 28, 71, 72,
- 33, 35, 114, 82, 18, nil, nil, nil, nil ]
- racc_token_table = {
- false => 0,
- Object.new => 1,
- :DATETIME => 2,
- :RECEIVED => 3,
- :MADDRESS => 4,
- :RETPATH => 5,
- :KEYWORDS => 6,
- :ENCRYPTED => 7,
- :MIMEVERSION => 8,
- :CTYPE => 9,
- :CENCODING => 10,
- :CDISPOSITION => 11,
- :ADDRESS => 12,
- :MAILBOX => 13,
- :DIGIT => 14,
- :ATOM => 15,
- "," => 16,
- ":" => 17,
- :FROM => 18,
- :BY => 19,
- "@" => 20,
- :DOMLIT => 21,
- :VIA => 22,
- :WITH => 23,
- :ID => 24,
- :FOR => 25,
- ";" => 26,
- "<" => 27,
- ">" => 28,
- "." => 29,
- :QUOTED => 30,
- :TOKEN => 31,
- "/" => 32,
- "=" => 33 }
- racc_use_result_var = false
- racc_nt_base = 34
- Racc_arg = [
- racc_action_table,
- racc_action_check,
- racc_action_default,
- racc_action_pointer,
- racc_goto_table,
- racc_goto_check,
- racc_goto_default,
- racc_goto_pointer,
- racc_nt_base,
- racc_reduce_table,
- racc_token_table,
- racc_shift_n,
- racc_reduce_n,
- racc_use_result_var ]
- Racc_token_to_s_table = [
- '$end',
- 'error',
- 'DATETIME',
- 'RECEIVED',
- 'MADDRESS',
- 'RETPATH',
- 'KEYWORDS',
- 'ENCRYPTED',
- 'MIMEVERSION',
- 'CTYPE',
- 'CENCODING',
- 'CDISPOSITION',
- 'ADDRESS',
- 'MAILBOX',
- 'DIGIT',
- 'ATOM',
- '","',
- '":"',
- 'FROM',
- 'BY',
- '"@"',
- 'DOMLIT',
- 'VIA',
- 'WITH',
- 'ID',
- 'FOR',
- '";"',
- '"<"',
- '">"',
- '"."',
- 'QUOTED',
- 'TOKEN',
- '"/"',
- '"="',
- '$start',
- 'content',
- 'datetime',
- 'received',
- 'addrs_TOP',
- 'retpath',
- 'keys',
- 'enc',
- 'version',
- 'ctype',
- 'cencode',
- 'cdisp',
- 'addr_TOP',
- 'mbox',
- 'day',
- 'hour',
- 'zone',
- 'from',
- 'by',
- 'via',
- 'with',
- 'id',
- 'for',
- 'received_datetime',
- 'received_domain',
- 'domain',
- 'msgid',
- 'received_addrspec',
- 'routeaddr',
- 'spec',
- 'addrs',
- 'group_bare',
- 'commas',
- 'group',
- 'addr',
- 'mboxes',
- 'addr_phrase',
- 'local_head',
- 'routes',
- 'at_domains',
- 'local',
- 'word',
- 'dots',
- 'domword',
- 'atom',
- 'phrase',
- 'params',
- 'opt_semicolon',
- 'value']
- Racc_debug_parser = false
- ##### racc system variables end #####
- # reduce 0 omitted
- module_eval <<'.,.,', 'parser.y', 16
- def _reduce_1( val, _values)
- val[1]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 17
- def _reduce_2( val, _values)
- val[1]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 18
- def _reduce_3( val, _values)
- val[1]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 19
- def _reduce_4( val, _values)
- val[1]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 20
- def _reduce_5( val, _values)
- val[1]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 21
- def _reduce_6( val, _values)
- val[1]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 22
- def _reduce_7( val, _values)
- val[1]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 23
- def _reduce_8( val, _values)
- val[1]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 24
- def _reduce_9( val, _values)
- val[1]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 25
- def _reduce_10( val, _values)
- val[1]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 26
- def _reduce_11( val, _values)
- val[1]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 27
- def _reduce_12( val, _values)
- val[1]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 33
- def _reduce_13( val, _values)
- t = Time.gm(val[3].to_i, val[2], val[1].to_i, 0, 0, 0)
- (t + val[4] - val[5]).localtime
- end
- .,.,
- # reduce 14 omitted
- # reduce 15 omitted
- module_eval <<'.,.,', 'parser.y', 42
- def _reduce_16( val, _values)
- (val[0].to_i * 60 * 60) +
- (val[2].to_i * 60)
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 47
- def _reduce_17( val, _values)
- (val[0].to_i * 60 * 60) +
- (val[2].to_i * 60) +
- (val[4].to_i)
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 54
- def _reduce_18( val, _values)
- timezone_string_to_unixtime(val[0])
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 59
- def _reduce_19( val, _values)
- val
- end
- .,.,
- # reduce 20 omitted
- module_eval <<'.,.,', 'parser.y', 65
- def _reduce_21( val, _values)
- val[1]
- end
- .,.,
- # reduce 22 omitted
- module_eval <<'.,.,', 'parser.y', 71
- def _reduce_23( val, _values)
- val[1]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 77
- def _reduce_24( val, _values)
- join_domain(val[0])
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 81
- def _reduce_25( val, _values)
- join_domain(val[2])
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 85
- def _reduce_26( val, _values)
- join_domain(val[0])
- end
- .,.,
- # reduce 27 omitted
- module_eval <<'.,.,', 'parser.y', 91
- def _reduce_28( val, _values)
- val[1]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 96
- def _reduce_29( val, _values)
- []
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 100
- def _reduce_30( val, _values)
- val[0].push val[2]
- val[0]
- end
- .,.,
- # reduce 31 omitted
- module_eval <<'.,.,', 'parser.y', 107
- def _reduce_32( val, _values)
- val[1]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 111
- def _reduce_33( val, _values)
- val[1]
- end
- .,.,
- # reduce 34 omitted
- module_eval <<'.,.,', 'parser.y', 117
- def _reduce_35( val, _values)
- val[1]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 123
- def _reduce_36( val, _values)
- val[0].spec
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 127
- def _reduce_37( val, _values)
- val[0].spec
- end
- .,.,
- # reduce 38 omitted
- module_eval <<'.,.,', 'parser.y', 134
- def _reduce_39( val, _values)
- val[1]
- end
- .,.,
- # reduce 40 omitted
- # reduce 41 omitted
- # reduce 42 omitted
- # reduce 43 omitted
- # reduce 44 omitted
- # reduce 45 omitted
- # reduce 46 omitted
- module_eval <<'.,.,', 'parser.y', 146
- def _reduce_47( val, _values)
- [ Address.new(nil, nil) ]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 148
- def _reduce_48( val, _values)
- val
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 149
- def _reduce_49( val, _values)
- val[0].push val[2]; val[0]
- end
- .,.,
- # reduce 50 omitted
- # reduce 51 omitted
- module_eval <<'.,.,', 'parser.y', 156
- def _reduce_52( val, _values)
- val
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 160
- def _reduce_53( val, _values)
- val[0].push val[2]
- val[0]
- end
- .,.,
- # reduce 54 omitted
- # reduce 55 omitted
- module_eval <<'.,.,', 'parser.y', 168
- def _reduce_56( val, _values)
- val[1].phrase = Decoder.decode(val[0])
- val[1]
- end
- .,.,
- # reduce 57 omitted
- module_eval <<'.,.,', 'parser.y', 176
- def _reduce_58( val, _values)
- AddressGroup.new(val[0], val[2])
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 178
- def _reduce_59( val, _values)
- AddressGroup.new(val[0], [])
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 181
- def _reduce_60( val, _values)
- val[0].join('.')
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 182
- def _reduce_61( val, _values)
- val[0] << ' ' << val[1].join('.')
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 186
- def _reduce_62( val, _values)
- val[2].routes.replace val[1]
- val[2]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 191
- def _reduce_63( val, _values)
- val[1]
- end
- .,.,
- # reduce 64 omitted
- module_eval <<'.,.,', 'parser.y', 196
- def _reduce_65( val, _values)
- [ val[1].join('.') ]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 197
- def _reduce_66( val, _values)
- val[0].push val[3].join('.'); val[0]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 199
- def _reduce_67( val, _values)
- Address.new( val[0], val[2] )
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 200
- def _reduce_68( val, _values)
- Address.new( val[0], nil )
- end
- .,.,
- # reduce 69 omitted
- module_eval <<'.,.,', 'parser.y', 203
- def _reduce_70( val, _values)
- val[0].push ''; val[0]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 206
- def _reduce_71( val, _values)
- val
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 209
- def _reduce_72( val, _values)
- val[1].times do
- val[0].push ''
- end
- val[0].push val[2]
- val[0]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 217
- def _reduce_73( val, _values)
- val
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 220
- def _reduce_74( val, _values)
- val[1].times do
- val[0].push ''
- end
- val[0].push val[2]
- val[0]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 227
- def _reduce_75( val, _values)
- 0
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 228
- def _reduce_76( val, _values)
- 1
- end
- .,.,
- # reduce 77 omitted
- # reduce 78 omitted
- # reduce 79 omitted
- # reduce 80 omitted
- # reduce 81 omitted
- # reduce 82 omitted
- # reduce 83 omitted
- # reduce 84 omitted
- module_eval <<'.,.,', 'parser.y', 243
- def _reduce_85( val, _values)
- val[1] = val[1].spec
- val.join('')
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 247
- def _reduce_86( val, _values)
- val
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 248
- def _reduce_87( val, _values)
- val[0].push val[2]; val[0]
- end
- .,.,
- # reduce 88 omitted
- module_eval <<'.,.,', 'parser.y', 251
- def _reduce_89( val, _values)
- val[0] << ' ' << val[1]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 255
- def _reduce_90( val, _values)
- val.push nil
- val
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 260
- def _reduce_91( val, _values)
- val
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 265
- def _reduce_92( val, _values)
- [ val[0].to_i, val[2].to_i ]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 270
- def _reduce_93( val, _values)
- [ val[0].downcase, val[2].downcase, decode_params(val[3]) ]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 274
- def _reduce_94( val, _values)
- [ val[0].downcase, nil, decode_params(val[1]) ]
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 279
- def _reduce_95( val, _values)
- {}
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 283
- def _reduce_96( val, _values)
- val[0][ val[2].downcase ] = val[4]
- val[0]
- end
- .,.,
- # reduce 97 omitted
- # reduce 98 omitted
- module_eval <<'.,.,', 'parser.y', 292
- def _reduce_99( val, _values)
- val[0].downcase
- end
- .,.,
- module_eval <<'.,.,', 'parser.y', 297
- def _reduce_100( val, _values)
- [ val[0].downcase, decode_params(val[1]) ]
- end
- .,.,
- # reduce 101 omitted
- # reduce 102 omitted
- # reduce 103 omitted
- # reduce 104 omitted
- # reduce 105 omitted
- # reduce 106 omitted
- # reduce 107 omitted
- # reduce 108 omitted
- # reduce 109 omitted
- def _reduce_none( val, _values)
- val[0]
- end
- end # class Parser
- end # module TMail
- #
- # port.rb
- #
- #--
- # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
- #
- # 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.
- #
- # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
- # with permission of Minero Aoki.
- #++
- 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.object_id.hash
- end
- def inspect
- "#<#{self.class}:id=#{sprintf '0x%x', @buffer.object_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
- module TMail
- class Mail
- def subject(to_charset = 'utf-8')
- Unquoter.unquote_and_convert_to(quoted_subject, to_charset)
- end
- def unquoted_body(to_charset = 'utf-8')
- from_charset = sub_header("content-type", "charset")
- case (content_transfer_encoding || "7bit").downcase
- when "quoted-printable"
- Unquoter.unquote_quoted_printable_and_convert_to(quoted_body,
- to_charset, from_charset, true)
- when "base64"
- Unquoter.unquote_base64_and_convert_to(quoted_body, to_charset,
- from_charset)
- when "7bit", "8bit"
- Unquoter.convert_to(quoted_body, to_charset, from_charset)
- when "binary"
- quoted_body
- else
- quoted_body
- end
- end
- def body(to_charset = 'utf-8', &block)
- attachment_presenter = block || Proc.new { |file_name| "Attachment: #{file_name}\n" }
-
- if multipart?
- parts.collect { |part|
- header = part["content-type"]
- if part.multipart?
- part.body(to_charset, &attachment_presenter)
- elsif header.nil?
- ""
- elsif !attachment?(part)
- part.unquoted_body(to_charset)
- else
- attachment_presenter.call(header["name"] || "(unnamed)")
- end
- }.join
- else
- unquoted_body(to_charset)
- end
- end
- end
- class Unquoter
- class << self
- def unquote_and_convert_to(text, to_charset, from_charset = "iso-8859-1", preserve_underscores=false)
- return "" if text.nil?
- if text =~ /^=\?(.*?)\?(.)\?(.*)\?=$/
- from_charset = $1
- quoting_method = $2
- text = $3
- case quoting_method.upcase
- when "Q" then
- unquote_quoted_printable_and_convert_to(text, to_charset, from_charset, preserve_underscores)
- when "B" then
- unquote_base64_and_convert_to(text, to_charset, from_charset)
- else
- raise "unknown quoting method #{quoting_method.inspect}"
- end
- else
- convert_to(text, to_charset, from_charset)
- end
- end
-
- def unquote_quoted_printable_and_convert_to(text, to, from, preserve_underscores=false)
- text = text.gsub(/_/, " ") unless preserve_underscores
- convert_to(text.unpack("M*").first, to, from)
- end
-
- def unquote_base64_and_convert_to(text, to, from)
- convert_to(Base64.decode(text).first, to, from)
- end
- begin
- require 'iconv'
- def convert_to(text, to, from)
- return text unless to && from
- text ? Iconv.iconv(to, from, text).first : ""
- rescue Iconv::IllegalSequence, Errno::EINVAL
- # the 'from' parameter specifies a charset other than what the text
- # actually is...not much we can do in this case but just return the
- # unconverted text.
- #
- # Ditto if either parameter represents an unknown charset, like
- # X-UNKNOWN.
- text
- end
- rescue LoadError
- # Not providing quoting support
- def convert_to(text, to, from)
- warn "Action Mailer: iconv not loaded; ignoring conversion from #{from} to #{to} (#{__FILE__}:#{__LINE__})"
- text
- end
- end
- end
- end
- end
- if __FILE__ == $0
- require 'test/unit'
- class TC_Unquoter < Test::Unit::TestCase
- def test_unquote_quoted_printable
- a ="=?ISO-8859-1?Q?[166417]_Bekr=E6ftelse_fra_Rejsefeber?="
- b = TMail::Unquoter.unquote_and_convert_to(a, 'utf-8')
- assert_equal "[166417] Bekr\303\246ftelse fra Rejsefeber", b
- end
- def test_unquote_base64
- a ="=?ISO-8859-1?B?WzE2NjQxN10gQmVrcuZmdGVsc2UgZnJhIFJlanNlZmViZXI=?="
- b = TMail::Unquoter.unquote_and_convert_to(a, 'utf-8')
- assert_equal "[166417] Bekr\303\246ftelse fra Rejsefeber", b
- end
- def test_unquote_without_charset
- a ="[166417]_Bekr=E6ftelse_fra_Rejsefeber"
- b = TMail::Unquoter.unquote_and_convert_to(a, 'utf-8')
- assert_equal "[166417]_Bekr=E6ftelse_fra_Rejsefeber", b
- end
- end
- end
- #
- # scanner.rb
- #
- #--
- # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
- #
- # 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.
- #
- # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
- # with permission of Minero Aoki.
- #++
- 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
- #
- # scanner_r.rb
- #
- #--
- # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
- #
- # 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.
- #
- # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
- # with permission of Minero Aoki.
- #++
- 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
- #
- # stringio.rb
- #
- #--
- # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
- #
- # 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.
- #
- # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
- # with permission of Minero Aoki.
- #++
- 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
- require 'tmail'
- #
- # utils.rb
- #
- #--
- # Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
- #
- # 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.
- #
- # Note: Originally licensed under LGPL v2+. Using MIT license for Rails
- # with permission of Minero Aoki.
- #++
- 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.object_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
- begin
- NKF.nkf(NKF_FLAGS[$KCODE],
- m.post_match.gsub(/%[\da-f]{2}/in) {|s| s[1,2].hex.chr })
- rescue
- m.post_match.gsub(/%[\da-f]{2}/in, "")
- end
- end
- end
- end
- require 'tmail/info'
- require 'tmail/mail'
- require 'tmail/mailbox'
- module ActionMailer
- module VERSION #:nodoc:
- MAJOR = 1
- MINOR = 2
- TINY = 5
- STRING = [MAJOR, MINOR, TINY].join('.')
- end
- end
- #--
- # 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
- begin
- require File.dirname(__FILE__) + '/../../actionpack/lib/action_controller'
- rescue LoadError
- require 'rubygems'
- require_gem 'actionpack', '>= 1.9.1'
- end
- end
- $:.unshift(File.dirname(__FILE__) + "/action_mailer/vendor/")
- require 'action_mailer/base'
- require 'action_mailer/helpers'
- require 'action_mailer/mail_helper'
- require 'action_mailer/quoting'
- require 'tmail'
- require 'net/smtp'
- ActionMailer::Base.class_eval do
- include ActionMailer::Quoting
- include ActionMailer::Helpers
- helper MailHelper
- end
- silence_warnings { TMail::Encoder.const_set("MAX_LINE_LEN", 200) }module TestHelper
- def test_format(text)
- "<em><strong><small>#{text}</small></strong></em>"
- end
- end
- $:.unshift(File.dirname(__FILE__) + "/../lib/")
- $:.unshift File.dirname(__FILE__) + "/fixtures/helpers"
- require 'test/unit'
- require 'action_mailer'
- module MailerHelper
- def person_name
- "Mr. Joe Person"
- end
- end
- class HelperMailer < ActionMailer::Base
- helper MailerHelper
- helper :test
- def use_helper(recipient)
- recipients recipient
- subject "using helpers"
- from "tester@example.com"
- end
- def use_test_helper(recipient)
- recipients recipient
- subject "using helpers"
- from "tester@example.com"
- self.body = { :text => "emphasize me!" }
- end
- def use_mail_helper(recipient)
- recipients recipient
- subject "using mailing helpers"
- from "tester@example.com"
- self.body = { :text =>
- "But soft! What light through yonder window breaks? It is the east, " +
- "and Juliet is the sun. Arise, fair sun, and kill the envious moon, " +
- "which is sick and pale with grief that thou, her maid, art far more " +
- "fair than she. Be not her maid, for she is envious! Her vestal " +
- "livery is but sick and green, and none but fools do wear it. Cast " +
- "it off!"
- }
- end
- def use_helper_method(recipient)
- recipients recipient
- subject "using helpers"
- from "tester@example.com"
- self.body = { :text => "emphasize me!" }
- end
- private
- def name_of_the_mailer_class
- self.class.name
- end
- helper_method :name_of_the_mailer_class
- end
- HelperMailer.template_root = File.dirname(__FILE__) + "/fixtures"
- class MailerHelperTest < Test::Unit::TestCase
- def new_mail( charset="utf-8" )
- mail = TMail::Mail.new
- mail.set_content_type "text", "plain", { "charset" => charset } if charset
- mail
- end
- def setup
- ActionMailer::Base.delivery_method = :test
- ActionMailer::Base.perform_deliveries = true
- ActionMailer::Base.deliveries = []
- @recipient = 'test@localhost'
- end
- def test_use_helper
- mail = HelperMailer.create_use_helper(@recipient)
- assert_match %r{Mr. Joe Person}, mail.encoded
- end
- def test_use_test_helper
- mail = HelperMailer.create_use_test_helper(@recipient)
- assert_match %r{<em><strong><small>emphasize me!}, mail.encoded
- end
- def test_use_helper_method
- mail = HelperMailer.create_use_helper_method(@recipient)
- assert_match %r{HelperMailer}, mail.encoded
- end
- def test_use_mail_helper
- mail = HelperMailer.create_use_mail_helper(@recipient)
- assert_match %r{ But soft!}, mail.encoded
- assert_match %r{east, and\n Juliet}, mail.encoded
- end
- end
- $:.unshift(File.dirname(__FILE__) + "/../lib/")
- require 'test/unit'
- require 'action_mailer'
- class RenderMailer < ActionMailer::Base
- def inline_template(recipient)
- recipients recipient
- subject "using helpers"
- from "tester@example.com"
- body render(:inline => "Hello, <%= @world %>", :body => { :world => "Earth" })
- end
- def file_template(recipient)
- recipients recipient
- subject "using helpers"
- from "tester@example.com"
- body render(:file => "signed_up", :body => { :recipient => recipient })
- end
- def initialize_defaults(method_name)
- super
- mailer_name "test_mailer"
- end
- end
- RenderMailer.template_root = File.dirname(__FILE__) + "/fixtures"
- class RenderHelperTest < Test::Unit::TestCase
- def setup
- ActionMailer::Base.delivery_method = :test
- ActionMailer::Base.perform_deliveries = true
- ActionMailer::Base.deliveries = []
- @recipient = 'test@localhost'
- end
- def test_inline_template
- mail = RenderMailer.create_inline_template(@recipient)
- assert_equal "Hello, Earth", mail.body.strip
- end
- def test_file_template
- mail = RenderMailer.create_file_template(@recipient)
- assert_equal "Hello there, \n\nMr. test@localhost", mail.body.strip
- end
- end
- $:.unshift(File.dirname(__FILE__) + "/../lib/")
- require 'test/unit'
- require 'action_mailer'
- class MockSMTP
- def self.deliveries
- @@deliveries
- end
- def initialize
- @@deliveries = []
- end
- def sendmail(mail, from, to)
- @@deliveries << [mail, from, to]
- end
- end
- class Net::SMTP
- def self.start(*args)
- yield MockSMTP.new
- end
- end
- class FunkyPathMailer < ActionMailer::Base
- self.template_root = "#{File.dirname(__FILE__)}/fixtures/path.with.dots"
- def multipart_with_template_path_with_dots(recipient)
- recipients recipient
- subject "Have a lovely picture"
- from "Chad Fowler <chad@chadfowler.com>"
- attachment :content_type => "image/jpeg",
- :body => "not really a jpeg, we're only testing, after all"
- end
- def template_path
- "#{File.dirname(__FILE__)}/fixtures/path.with.dots"
- end
- end
- 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)
- self.recipients = recipient
- self.subject = "[Cancelled] Goodbye #{recipient}"
- self.from = "system@loudthinking.com"
- self.sent_on = Time.local(2004, 12, 12)
- self.body = "Goodbye, Mr. #{recipient}"
- end
- def cc_bcc(recipient)
- recipients recipient
- subject "testing bcc/cc"
- from "system@loudthinking.com"
- sent_on Time.local(2004, 12, 12)
- cc "nobody@loudthinking.com"
- bcc "root@loudthinking.com"
- body "Nothing to see here."
- end
- def iso_charset(recipient)
- @recipients = recipient
- @subject = "testing isø charsets"
- @from = "system@loudthinking.com"
- @sent_on = Time.local 2004, 12, 12
- @cc = "nobody@loudthinking.com"
- @bcc = "root@loudthinking.com"
- @body = "Nothing to see here."
- @charset = "iso-8859-1"
- end
- def unencoded_subject(recipient)
- @recipients = recipient
- @subject = "testing unencoded subject"
- @from = "system@loudthinking.com"
- @sent_on = Time.local 2004, 12, 12
- @cc = "nobody@loudthinking.com"
- @bcc = "root@loudthinking.com"
- @body = "Nothing to see here."
- end
- def extended_headers(recipient)
- @recipients = recipient
- @subject = "testing extended headers"
- @from = "Grytøyr <stian1@example.net>"
- @sent_on = Time.local 2004, 12, 12
- @cc = "Grytøyr <stian2@example.net>"
- @bcc = "Grytøyr <stian3@example.net>"
- @body = "Nothing to see here."
- @charset = "iso-8859-1"
- end
- def utf8_body(recipient)
- @recipients = recipient
- @subject = "testing utf-8 body"
- @from = "Foo áëô îü <extended@example.net>"
- @sent_on = Time.local 2004, 12, 12
- @cc = "Foo áëô îü <extended@example.net>"
- @bcc = "Foo áëô îü <extended@example.net>"
- @body = "åœö blah"
- @charset = "utf-8"
- end
- def multipart_with_mime_version(recipient)
- recipients recipient
- subject "multipart with mime_version"
- from "test@example.com"
- sent_on Time.local(2004, 12, 12)
- mime_version "1.1"
- content_type "multipart/alternative"
- part "text/plain" do |p|
- p.body = "blah"
- end
- part "text/html" do |p|
- p.body = "<b>blah</b>"
- end
- end
- def multipart_with_utf8_subject(recipient)
- recipients recipient
- subject "Foo áëô îü"
- from "test@example.com"
- charset "utf-8"
- part "text/plain" do |p|
- p.body = "blah"
- end
- part "text/html" do |p|
- p.body = "<b>blah</b>"
- end
- end
- def explicitly_multipart_example(recipient, ct=nil)
- recipients recipient
- subject "multipart example"
- from "test@example.com"
- sent_on Time.local(2004, 12, 12)
- body "plain text default"
- content_type ct if ct
- part "text/html" do |p|
- p.charset = "iso-8859-1"
- p.body = "blah"
- end
- attachment :content_type => "image/jpeg", :filename => "foo.jpg",
- :body => "123456789"
- end
- def implicitly_multipart_example(recipient, cs = nil, order = nil)
- @recipients = recipient
- @subject = "multipart example"
- @from = "test@example.com"
- @sent_on = Time.local 2004, 12, 12
- @body = { "recipient" => recipient }
- @charset = cs if cs
- @implicit_parts_order = order if order
- end
- def implicitly_multipart_with_utf8
- recipients "no.one@nowhere.test"
- subject "Foo áëô îü"
- from "some.one@somewhere.test"
- template "implicitly_multipart_example"
- body ({ "recipient" => "no.one@nowhere.test" })
- end
- def html_mail(recipient)
- recipients recipient
- subject "html mail"
- from "test@example.com"
- body "<em>Emphasize</em> <strong>this</strong>"
- content_type "text/html"
- end
- def html_mail_with_underscores(recipient)
- subject "html mail with underscores"
- body %{<a href="http://google.com" target="_blank">_Google</a>}
- end
- def custom_template(recipient)
- recipients recipient
- subject "[Signed up] Welcome #{recipient}"
- from "system@loudthinking.com"
- sent_on Time.local(2004, 12, 12)
- template "signed_up"
- body["recipient"] = recipient
- end
- def various_newlines(recipient)
- recipients recipient
- subject "various newlines"
- from "test@example.com"
- body "line #1\nline #2\rline #3\r\nline #4\r\r" +
- "line #5\n\nline#6\r\n\r\nline #7"
- end
- def various_newlines_multipart(recipient)
- recipients recipient
- subject "various newlines multipart"
- from "test@example.com"
- content_type "multipart/alternative"
- part :content_type => "text/plain", :body => "line #1\nline #2\rline #3\r\nline #4\r\r"
- part :content_type => "text/html", :body => "<p>line #1</p>\n<p>line #2</p>\r<p>line #3</p>\r\n<p>line #4</p>\r\r"
- end
- def nested_multipart(recipient)
- recipients recipient
- subject "nested multipart"
- from "test@example.com"
- content_type "multipart/mixed"
- part :content_type => "multipart/alternative", :content_disposition => "inline" do |p|
- p.part :content_type => "text/plain", :body => "test text\nline #2"
- p.part :content_type => "text/html", :body => "<b>test</b> HTML<br/>\nline #2"
- end
- attachment :content_type => "application/octet-stream",:filename => "test.txt", :body => "test abcdefghijklmnopqstuvwxyz"
- end
-
- def attachment_with_custom_header(recipient)
- recipients recipient
- subject "custom header in attachment"
- from "test@example.com"
- content_type "multipart/related"
- part :content_type => "text/html", :body => 'yo'
- attachment :content_type => "image/jpeg",:filename => "test.jpeg", :body => "i am not a real picture", :headers => { 'Content-ID' => '<test@test.com>' }
- end
- def unnamed_attachment(recipient)
- recipients recipient
- subject "nested multipart"
- from "test@example.com"
- content_type "multipart/mixed"
- part :content_type => "text/plain", :body => "hullo"
- attachment :content_type => "application/octet-stream", :body => "test abcdefghijklmnopqstuvwxyz"
- end
- def headers_with_nonalpha_chars(recipient)
- recipients recipient
- subject "nonalpha chars"
- from "One: Two <test@example.com>"
- cc "Three: Four <test@example.com>"
- bcc "Five: Six <test@example.com>"
- body "testing"
- end
- def custom_content_type_attributes
- recipients "no.one@nowhere.test"
- subject "custom content types"
- from "some.one@somewhere.test"
- content_type "text/plain; format=flowed"
- body "testing"
- end
- class <<self
- attr_accessor :received_body
- end
- def receive(mail)
- self.class.received_body = mail.body
- end
- end
- TestMailer.template_root = File.dirname(__FILE__) + "/fixtures"
- class ActionMailerTest < Test::Unit::TestCase
- include ActionMailer::Quoting
- def encode( text, charset="utf-8" )
- quoted_printable( text, charset )
- end
- def new_mail( charset="utf-8" )
- mail = TMail::Mail.new
- if charset
- mail.set_content_type "text", "plain", { "charset" => charset }
- end
- mail
- end
- def setup
- ActionMailer::Base.delivery_method = :test
- ActionMailer::Base.perform_deliveries = true
- ActionMailer::Base.deliveries = []
- @recipient = 'test@localhost'
- end
- def test_nested_parts
- created = nil
- assert_nothing_raised { created = TestMailer.create_nested_multipart(@recipient)}
- assert_equal 2,created.parts.size
- assert_equal 2,created.parts.first.parts.size
-
- assert_equal "multipart/mixed", created.content_type
- assert_equal "multipart/alternative", created.parts.first.content_type
- assert_equal "text/plain", created.parts.first.parts.first.content_type
- assert_equal "text/html", created.parts.first.parts[1].content_type
- assert_equal "application/octet-stream", created.parts[1].content_type
- end
- def test_attachment_with_custom_header
- created = nil
- assert_nothing_raised { created = TestMailer.create_attachment_with_custom_header(@recipient)}
- assert_equal "<test@test.com>", created.parts[1].header['content-id'].to_s
- end
- def test_signed_up
- expected = new_mail
- 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)
- expected.mime_version = nil
- 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_custom_template
- expected = new_mail
- 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_custom_template(@recipient) }
- assert_not_nil created
- assert_equal expected.encoded, created.encoded
- end
-
- def test_cancelled_account
- expected = new_mail
- 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_cc_bcc
- expected = new_mail
- expected.to = @recipient
- expected.subject = "testing bcc/cc"
- expected.body = "Nothing to see here."
- expected.from = "system@loudthinking.com"
- expected.cc = "nobody@loudthinking.com"
- expected.bcc = "root@loudthinking.com"
- expected.date = Time.local 2004, 12, 12
- created = nil
- assert_nothing_raised do
- created = TestMailer.create_cc_bcc @recipient
- end
- assert_not_nil created
- assert_equal expected.encoded, created.encoded
- assert_nothing_raised do
- TestMailer.deliver_cc_bcc @recipient
- end
- assert_not_nil ActionMailer::Base.deliveries.first
- assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
- end
- def test_iso_charset
- expected = new_mail( "iso-8859-1" )
- expected.to = @recipient
- expected.subject = encode "testing isø charsets", "iso-8859-1"
- expected.body = "Nothing to see here."
- expected.from = "system@loudthinking.com"
- expected.cc = "nobody@loudthinking.com"
- expected.bcc = "root@loudthinking.com"
- expected.date = Time.local 2004, 12, 12
- created = nil
- assert_nothing_raised do
- created = TestMailer.create_iso_charset @recipient
- end
- assert_not_nil created
- assert_equal expected.encoded, created.encoded
- assert_nothing_raised do
- TestMailer.deliver_iso_charset @recipient
- end
- assert_not_nil ActionMailer::Base.deliveries.first
- assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
- end
- def test_unencoded_subject
- expected = new_mail
- expected.to = @recipient
- expected.subject = "testing unencoded subject"
- expected.body = "Nothing to see here."
- expected.from = "system@loudthinking.com"
- expected.cc = "nobody@loudthinking.com"
- expected.bcc = "root@loudthinking.com"
- expected.date = Time.local 2004, 12, 12
- created = nil
- assert_nothing_raised do
- created = TestMailer.create_unencoded_subject @recipient
- end
- assert_not_nil created
- assert_equal expected.encoded, created.encoded
- assert_nothing_raised do
- TestMailer.deliver_unencoded_subject @recipient
- end
- 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
- def test_unquote_quoted_printable_subject
- msg = <<EOF
- From: me@example.com
- Subject: =?utf-8?Q?testing_testing_=D6=A4?=
- Content-Type: text/plain; charset=iso-8859-1
- The body
- EOF
- mail = TMail::Mail.parse(msg)
- assert_equal "testing testing \326\244", mail.subject
- assert_equal "=?utf-8?Q?testing_testing_=D6=A4?=", mail.quoted_subject
- end
- def test_unquote_7bit_subject
- msg = <<EOF
- From: me@example.com
- Subject: this == working?
- Content-Type: text/plain; charset=iso-8859-1
- The body
- EOF
- mail = TMail::Mail.parse(msg)
- assert_equal "this == working?", mail.subject
- assert_equal "this == working?", mail.quoted_subject
- end
- def test_unquote_7bit_body
- msg = <<EOF
- From: me@example.com
- Subject: subject
- Content-Type: text/plain; charset=iso-8859-1
- Content-Transfer-Encoding: 7bit
- The=3Dbody
- EOF
- mail = TMail::Mail.parse(msg)
- assert_equal "The=3Dbody", mail.body.strip
- assert_equal "The=3Dbody", mail.quoted_body.strip
- end
- def test_unquote_quoted_printable_body
- msg = <<EOF
- From: me@example.com
- Subject: subject
- Content-Type: text/plain; charset=iso-8859-1
- Content-Transfer-Encoding: quoted-printable
- The=3Dbody
- EOF
- mail = TMail::Mail.parse(msg)
- assert_equal "The=body", mail.body.strip
- assert_equal "The=3Dbody", mail.quoted_body.strip
- end
- def test_unquote_base64_body
- msg = <<EOF
- From: me@example.com
- Subject: subject
- Content-Type: text/plain; charset=iso-8859-1
- Content-Transfer-Encoding: base64
- VGhlIGJvZHk=
- EOF
- mail = TMail::Mail.parse(msg)
- assert_equal "The body", mail.body.strip
- assert_equal "VGhlIGJvZHk=", mail.quoted_body.strip
- end
- def test_extended_headers
- @recipient = "Grytøyr <test@localhost>"
- expected = new_mail "iso-8859-1"
- expected.to = quote_address_if_necessary @recipient, "iso-8859-1"
- expected.subject = "testing extended headers"
- expected.body = "Nothing to see here."
- expected.from = quote_address_if_necessary "Grytøyr <stian1@example.net>", "iso-8859-1"
- expected.cc = quote_address_if_necessary "Grytøyr <stian2@example.net>", "iso-8859-1"
- expected.bcc = quote_address_if_necessary "Grytøyr <stian3@example.net>", "iso-8859-1"
- expected.date = Time.local 2004, 12, 12
- created = nil
- assert_nothing_raised do
- created = TestMailer.create_extended_headers @recipient
- end
- assert_not_nil created
- assert_equal expected.encoded, created.encoded
- assert_nothing_raised do
- TestMailer.deliver_extended_headers @recipient
- end
- assert_not_nil ActionMailer::Base.deliveries.first
- assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
- end
-
- def test_utf8_body_is_not_quoted
- @recipient = "Foo áëô îü <extended@example.net>"
- expected = new_mail "utf-8"
- expected.to = quote_address_if_necessary @recipient, "utf-8"
- expected.subject = "testing utf-8 body"
- expected.body = "åœö blah"
- expected.from = quote_address_if_necessary @recipient, "utf-8"
- expected.cc = quote_address_if_necessary @recipient, "utf-8"
- expected.bcc = quote_address_if_necessary @recipient, "utf-8"
- expected.date = Time.local 2004, 12, 12
- created = TestMailer.create_utf8_body @recipient
- assert_match(/åœö blah/, created.encoded)
- end
- def test_multiple_utf8_recipients
- @recipient = ["\"Foo áëô îü\" <extended@example.net>", "\"Example Recipient\" <me@example.com>"]
- expected = new_mail "utf-8"
- expected.to = quote_address_if_necessary @recipient, "utf-8"
- expected.subject = "testing utf-8 body"
- expected.body = "åœö blah"
- expected.from = quote_address_if_necessary @recipient.first, "utf-8"
- expected.cc = quote_address_if_necessary @recipient, "utf-8"
- expected.bcc = quote_address_if_necessary @recipient, "utf-8"
- expected.date = Time.local 2004, 12, 12
- created = TestMailer.create_utf8_body @recipient
- assert_match(/\nFrom: =\?utf-8\?Q\?Foo_.*?\?= <extended@example.net>\r/, created.encoded)
- assert_match(/\nTo: =\?utf-8\?Q\?Foo_.*?\?= <extended@example.net>, Example Recipient <me/, created.encoded)
- end
- def test_receive_decodes_base64_encoded_mail
- fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email")
- TestMailer.receive(fixture)
- assert_match(/Jamis/, TestMailer.received_body)
- end
- def test_receive_attachments
- fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email2")
- mail = TMail::Mail.parse(fixture)
- attachment = mail.attachments.last
- assert_equal "smime.p7s", attachment.original_filename
- assert_equal "application/pkcs7-signature", attachment.content_type
- end
- def test_decode_attachment_without_charset
- fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email3")
- mail = TMail::Mail.parse(fixture)
- attachment = mail.attachments.last
- assert_equal 1026, attachment.read.length
- end
- def test_attachment_using_content_location
- fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email12")
- mail = TMail::Mail.parse(fixture)
- assert_equal 1, mail.attachments.length
- assert_equal "Photo25.jpg", mail.attachments.first.original_filename
- end
- def test_attachment_with_text_type
- fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email13")
- mail = TMail::Mail.parse(fixture)
- assert mail.has_attachments?
- assert_equal 1, mail.attachments.length
- assert_equal "hello.rb", mail.attachments.first.original_filename
- end
- def test_decode_part_without_content_type
- fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email4")
- mail = TMail::Mail.parse(fixture)
- assert_nothing_raised { mail.body }
- end
- def test_decode_message_without_content_type
- fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email5")
- mail = TMail::Mail.parse(fixture)
- assert_nothing_raised { mail.body }
- end
- def test_decode_message_with_incorrect_charset
- fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email6")
- mail = TMail::Mail.parse(fixture)
- assert_nothing_raised { mail.body }
- end
- def test_multipart_with_mime_version
- mail = TestMailer.create_multipart_with_mime_version(@recipient)
- assert_equal "1.1", mail.mime_version
- end
-
- def test_multipart_with_utf8_subject
- mail = TestMailer.create_multipart_with_utf8_subject(@recipient)
- assert_match(/\nSubject: =\?utf-8\?Q\?Foo_.*?\?=/, mail.encoded)
- end
- def test_implicitly_multipart_with_utf8
- mail = TestMailer.create_implicitly_multipart_with_utf8
- assert_match(/\nSubject: =\?utf-8\?Q\?Foo_.*?\?=/, mail.encoded)
- end
- def test_explicitly_multipart_messages
- mail = TestMailer.create_explicitly_multipart_example(@recipient)
- assert_equal 3, mail.parts.length
- assert_nil mail.content_type
- assert_equal "text/plain", mail.parts[0].content_type
- assert_equal "text/html", mail.parts[1].content_type
- assert_equal "iso-8859-1", mail.parts[1].sub_header("content-type", "charset")
- assert_equal "inline", mail.parts[1].content_disposition
- assert_equal "image/jpeg", mail.parts[2].content_type
- assert_equal "attachment", mail.parts[2].content_disposition
- assert_equal "foo.jpg", mail.parts[2].sub_header("content-disposition", "filename")
- assert_equal "foo.jpg", mail.parts[2].sub_header("content-type", "name")
- assert_nil mail.parts[2].sub_header("content-type", "charset")
- end
- def test_explicitly_multipart_with_content_type
- mail = TestMailer.create_explicitly_multipart_example(@recipient, "multipart/alternative")
- assert_equal 3, mail.parts.length
- assert_equal "multipart/alternative", mail.content_type
- end
- def test_explicitly_multipart_with_invalid_content_type
- mail = TestMailer.create_explicitly_multipart_example(@recipient, "text/xml")
- assert_equal 3, mail.parts.length
- assert_nil mail.content_type
- end
- def test_implicitly_multipart_messages
- mail = TestMailer.create_implicitly_multipart_example(@recipient)
- assert_equal 3, mail.parts.length
- assert_equal "1.0", mail.mime_version
- assert_equal "multipart/alternative", mail.content_type
- assert_equal "text/yaml", mail.parts[0].content_type
- assert_equal "utf-8", mail.parts[0].sub_header("content-type", "charset")
- assert_equal "text/plain", mail.parts[1].content_type
- assert_equal "utf-8", mail.parts[1].sub_header("content-type", "charset")
- assert_equal "text/html", mail.parts[2].content_type
- assert_equal "utf-8", mail.parts[2].sub_header("content-type", "charset")
- end
- def test_implicitly_multipart_messages_with_custom_order
- mail = TestMailer.create_implicitly_multipart_example(@recipient, nil, ["text/yaml", "text/plain"])
- assert_equal 3, mail.parts.length
- assert_equal "text/html", mail.parts[0].content_type
- assert_equal "text/plain", mail.parts[1].content_type
- assert_equal "text/yaml", mail.parts[2].content_type
- end
- def test_implicitly_multipart_messages_with_charset
- mail = TestMailer.create_implicitly_multipart_example(@recipient, 'iso-8859-1')
- assert_equal "multipart/alternative", mail.header['content-type'].body
-
- assert_equal 'iso-8859-1', mail.parts[0].sub_header("content-type", "charset")
- assert_equal 'iso-8859-1', mail.parts[1].sub_header("content-type", "charset")
- assert_equal 'iso-8859-1', mail.parts[2].sub_header("content-type", "charset")
- end
- def test_html_mail
- mail = TestMailer.create_html_mail(@recipient)
- assert_equal "text/html", mail.content_type
- end
- def test_html_mail_with_underscores
- mail = TestMailer.create_html_mail_with_underscores(@recipient)
- assert_equal %{<a href="http://google.com" target="_blank">_Google</a>}, mail.body
- end
- def test_various_newlines
- mail = TestMailer.create_various_newlines(@recipient)
- assert_equal("line #1\nline #2\nline #3\nline #4\n\n" +
- "line #5\n\nline#6\n\nline #7", mail.body)
- end
- def test_various_newlines_multipart
- mail = TestMailer.create_various_newlines_multipart(@recipient)
- assert_equal "line #1\nline #2\nline #3\nline #4\n\n", mail.parts[0].body
- assert_equal "<p>line #1</p>\n<p>line #2</p>\n<p>line #3</p>\n<p>line #4</p>\n\n", mail.parts[1].body
- end
-
- def test_headers_removed_on_smtp_delivery
- ActionMailer::Base.delivery_method = :smtp
- TestMailer.deliver_cc_bcc(@recipient)
- assert MockSMTP.deliveries[0][2].include?("root@loudthinking.com")
- assert MockSMTP.deliveries[0][2].include?("nobody@loudthinking.com")
- assert MockSMTP.deliveries[0][2].include?(@recipient)
- assert_match %r{^Cc: nobody@loudthinking.com}, MockSMTP.deliveries[0][0]
- assert_match %r{^To: #{@recipient}}, MockSMTP.deliveries[0][0]
- assert_no_match %r{^Bcc: root@loudthinking.com}, MockSMTP.deliveries[0][0]
- end
- def test_recursive_multipart_processing
- fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email7")
- mail = TMail::Mail.parse(fixture)
- assert_equal "This is the first part.\n\nAttachment: test.rb\nAttachment: test.pdf\n\n\nAttachment: smime.p7s\n", mail.body
- end
- def test_decode_encoded_attachment_filename
- fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email8")
- mail = TMail::Mail.parse(fixture)
- attachment = mail.attachments.last
- assert_equal "01QuienTeDijat.Pitbull.mp3", attachment.original_filename
- end
- def test_wrong_mail_header
- fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email9")
- assert_raise(TMail::SyntaxError) { TMail::Mail.parse(fixture) }
- end
- def test_decode_message_with_unknown_charset
- fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email10")
- mail = TMail::Mail.parse(fixture)
- assert_nothing_raised { mail.body }
- end
- def test_decode_message_with_unquoted_atchar_in_header
- fixture = File.read(File.dirname(__FILE__) + "/fixtures/raw_email11")
- mail = TMail::Mail.parse(fixture)
- assert_not_nil mail.from
- end
- def test_empty_header_values_omitted
- result = TestMailer.create_unnamed_attachment(@recipient).encoded
- assert_match %r{Content-Type: application/octet-stream[^;]}, result
- assert_match %r{Content-Disposition: attachment[^;]}, result
- end
- def test_headers_with_nonalpha_chars
- mail = TestMailer.create_headers_with_nonalpha_chars(@recipient)
- assert !mail.from_addrs.empty?
- assert !mail.cc_addrs.empty?
- assert !mail.bcc_addrs.empty?
- assert_match(/:/, mail.from_addrs.to_s)
- assert_match(/:/, mail.cc_addrs.to_s)
- assert_match(/:/, mail.bcc_addrs.to_s)
- end
- def test_deliver_with_mail_object
- mail = TestMailer.create_headers_with_nonalpha_chars(@recipient)
- assert_nothing_raised { TestMailer.deliver(mail) }
- assert_equal 1, TestMailer.deliveries.length
- end
- def test_multipart_with_template_path_with_dots
- mail = FunkyPathMailer.create_multipart_with_template_path_with_dots(@recipient)
- assert_equal 2, mail.parts.length
- end
- def test_custom_content_type_attributes
- mail = TestMailer.create_custom_content_type_attributes
- assert_match %r{format=flowed}, mail['content-type'].to_s
- assert_match %r{charset=utf-8}, mail['content-type'].to_s
- end
- end
- class InheritableTemplateRootTest < Test::Unit::TestCase
- def test_attr
- expected = "#{File.dirname(__FILE__)}/fixtures/path.with.dots"
- assert_equal expected, FunkyPathMailer.template_root
- sub = Class.new(FunkyPathMailer)
- sub.template_root = 'test/path'
- assert_equal 'test/path', sub.template_root
- assert_equal expected, FunkyPathMailer.template_root
- end
- end
- $:.unshift(File.dirname(__FILE__) + "/../lib/")
- $:.unshift(File.dirname(__FILE__) + "/../lib/action_mailer/vendor")
- require 'test/unit'
- require 'tmail'
- require 'tempfile'
- class QuotingTest < Test::Unit::TestCase
- def test_quote_multibyte_chars
- original = "\303\246 \303\270 and \303\245"
- result = execute_in_sandbox(<<-CODE)
- $:.unshift(File.dirname(__FILE__) + "/../lib/")
- $KCODE = 'u'
- require 'jcode'
- require 'action_mailer/quoting'
- include ActionMailer::Quoting
- quoted_printable(#{original.inspect}, "UTF-8")
- CODE
- unquoted = TMail::Unquoter.unquote_and_convert_to(result, nil)
- assert_equal unquoted, original
- end
- private
- # This whole thing *could* be much simpler, but I don't think Tempfile,
- # popen and others exist on all platforms (like Windows).
- def execute_in_sandbox(code)
- test_name = "#{File.dirname(__FILE__)}/am-quoting-test.#{$$}.rb"
- res_name = "#{File.dirname(__FILE__)}/am-quoting-test.#{$$}.out"
- File.open(test_name, "w+") do |file|
- file.write(<<-CODE)
- block = Proc.new do
- #{code}
- end
- puts block.call
- CODE
- end
- system("ruby #{test_name} > #{res_name}") or raise "could not run test in sandbox"
- File.read(res_name)
- ensure
- File.delete(test_name) rescue nil
- File.delete(res_name) rescue nil
- end
- end
- $:.unshift(File.dirname(__FILE__) + "/../lib/")
- $:.unshift File.dirname(__FILE__) + "/fixtures/helpers"
- require 'test/unit'
- require 'action_mailer'
- class TMailMailTest < Test::Unit::TestCase
- def test_body
- m = TMail::Mail.new
- expected = 'something_with_underscores'
- m.encoding = 'quoted-printable'
- quoted_body = [expected].pack('*M')
- m.body = quoted_body
- assert_equal "something_with_underscores=\n", m.quoted_body
- assert_equal expected, m.body
- end
- end
- $:.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$:.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}"
- 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
- # the acual gruntwork
- Dir.chdir("lib")
- Find.find("action_controller", "action_controller.rb", "action_view", "action_view.rb") { |f|
- if f[-3..-1] == ".rb"
- File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true)
- else
- File::makedirs(File.join($sitedir, *f.split(/\//)))
- end
- }require 'test/unit'
- require 'test/unit/assertions'
- require 'rexml/document'
- require File.dirname(__FILE__) + "/vendor/html-scanner/html/document"
- module Test #:nodoc:
- module Unit #:nodoc:
- # In addition to these specific assertions, you also have easy access to various collections that the regular test/unit assertions
- # can be used against. These collections are:
- #
- # * assigns: Instance variables assigned in the action that are available for the view.
- # * session: Objects being saved in the session.
- # * flash: The flash objects currently in the session.
- # * cookies: Cookies being sent to the user on this request.
- #
- # These collections can be used just like any other hash:
- #
- # assert_not_nil assigns(:person) # makes sure that a @person instance variable was set
- # assert_equal "Dave", cookies[:name] # makes sure that a cookie called :name was set as "Dave"
- # assert flash.empty? # makes sure that there's nothing in the flash
- #
- # For historic reasons, the assigns hash uses string-based keys. So assigns[:person] won't work, but assigns["person"] will. To
- # appease our yearning for symbols, though, an alternative accessor has been deviced using a method call instead of index referencing.
- # So assigns(:person) will work just like assigns["person"], but again, assigns[:person] will not work.
- #
- # On top of the collections, you have the complete url that a given action redirected to available in redirect_to_url.
- #
- # For redirects within the same controller, you can even call follow_redirect and the redirect will be followed, triggering another
- # action call which can then be asserted against.
- #
- # == Manipulating the request collections
- #
- # The collections described above link to the response, so you can test if what the actions were expected to do happened. But
- # sometimes you also want to manipulate these collections in the incoming request. This is really only relevant for sessions
- # and cookies, though. For sessions, you just do:
- #
- # @request.session[:key] = "value"
- #
- # For cookies, you need to manually create the cookie, like this:
- #
- # @request.cookies["key"] = CGI::Cookie.new("key", "value")
- #
- # == Testing named routes
- #
- # If you're using named routes, they can be easily tested using the original named routes methods straight in the test case.
- # Example:
- #
- # assert_redirected_to page_url(:title => 'foo')
- module Assertions
- # Asserts that the response is one of the following types:
- #
- # * <tt>:success</tt>: Status code was 200
- # * <tt>:redirect</tt>: Status code was in the 300-399 range
- # * <tt>:missing</tt>: Status code was 404
- # * <tt>:error</tt>: Status code was in the 500-599 range
- #
- # You can also pass an explicit status code number as the type, like assert_response(501)
- def assert_response(type, message = nil)
- clean_backtrace do
- if [ :success, :missing, :redirect, :error ].include?(type) && @response.send("#{type}?")
- assert_block("") { true } # to count the assertion
- elsif type.is_a?(Fixnum) && @response.response_code == type
- assert_block("") { true } # to count the assertion
- else
- assert_block(build_message(message, "Expected response to be a <?>, but was <?>", type, @response.response_code)) { false }
- end
- end
- end
- # Assert that the redirection options passed in match those of the redirect called in the latest action. This match can be partial,
- # such that assert_redirected_to(:controller => "weblog") will also match the redirection of
- # redirect_to(:controller => "weblog", :action => "show") and so on.
- def assert_redirected_to(options = {}, message=nil)
- clean_backtrace do
- assert_response(:redirect, message)
- if options.is_a?(String)
- msg = build_message(message, "expected a redirect to <?>, found one to <?>", options, @response.redirect_url)
- url_regexp = %r{^(\w+://.*?(/|$|\?))(.*)$}
- eurl, epath, url, path = [options, @response.redirect_url].collect do |url|
- u, p = (url_regexp =~ url) ? [$1, $3] : [nil, url]
- [u, (p[0..0] == '/') ? p : '/' + p]
- end.flatten
- assert_equal(eurl, url, msg) if eurl && url
- assert_equal(epath, path, msg) if epath && path
- else
- @response_diff = options.diff(@response.redirected_to) if options.is_a?(Hash) && @response.redirected_to.is_a?(Hash)
- msg = build_message(message, "response is not a redirection to all of the options supplied (redirection is <?>)#{', difference: <?>' if @response_diff}",
- @response.redirected_to || @response.redirect_url, @response_diff)
- assert_block(msg) do
- if options.is_a?(Symbol)
- @response.redirected_to == options
- else
- options.keys.all? do |k|
- if k == :controller then options[k] == ActionController::Routing.controller_relative_to(@response.redirected_to[k], @controller.class.controller_path)
- else options[k] == (@response.redirected_to[k].respond_to?(:to_param) ? @response.redirected_to[k].to_param : @response.redirected_to[k] unless @response.redirected_to[k].nil?)
- end
- end
- end
- end
- end
- end
- end
- # Asserts that the request was rendered with the appropriate template file.
- def assert_template(expected = nil, message=nil)
- clean_backtrace do
- 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
- end
- # Asserts that the routing of the given path was handled correctly and that the parsed options match.
- def assert_recognizes(expected_options, path, extras={}, message=nil)
- clean_backtrace do
- path = "/#{path}" unless path[0..0] == '/'
- # Load routes.rb if it hasn't been loaded.
- ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty?
-
- # Assume given controller
- request = ActionController::TestRequest.new({}, {}, nil)
- request.path = path
- ActionController::Routing::Routes.recognize!(request)
-
- expected_options = expected_options.clone
- extras.each_key { |key| expected_options.delete key } unless extras.nil?
-
- expected_options.stringify_keys!
- msg = build_message(message, "The recognized options <?> did not match <?>",
- request.path_parameters, expected_options)
- assert_block(msg) { request.path_parameters == expected_options }
- end
- end
- # Asserts that the provided options can be used to generate the provided path.
- def assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)
- clean_backtrace do
- expected_path = "/#{expected_path}" unless expected_path[0] == ?/
- # Load routes.rb if it hasn't been loaded.
- ActionController::Routing::Routes.reload if ActionController::Routing::Routes.empty?
-
- generated_path, extra_keys = ActionController::Routing::Routes.generate(options, extras)
- found_extras = options.reject {|k, v| ! extra_keys.include? k}
- msg = build_message(message, "found extras <?>, not <?>", found_extras, extras)
- assert_block(msg) { found_extras == extras }
-
- msg = build_message(message, "The generated path <?> did not match <?>", generated_path,
- expected_path)
- assert_block(msg) { expected_path == generated_path }
- end
- end
- # Asserts that path and options match both ways; in other words, the URL generated from
- # options is the same as path, and also that the options recognized from path are the same as options
- def assert_routing(path, options, defaults={}, extras={}, message=nil)
- assert_recognizes(options, path, extras, message)
-
- controller, default_controller = options[:controller], defaults[:controller]
- if controller && controller.include?(?/) && default_controller && default_controller.include?(?/)
- options[:controller] = "/#{controller}"
- end
-
- assert_generates(path, options, defaults, extras, message)
- end
- # Asserts that there is a tag/node/element in the body of the response
- # that meets all of the given conditions. The +conditions+ parameter must
- # be a hash of any of the following keys (all are optional):
- #
- # * <tt>:tag</tt>: the node type must match the corresponding value
- # * <tt>:attributes</tt>: a hash. The node's attributes must match the
- # corresponding values in the hash.
- # * <tt>:parent</tt>: a hash. The node's parent must match the
- # corresponding hash.
- # * <tt>:child</tt>: a hash. At least one of the node's immediate children
- # must meet the criteria described by the hash.
- # * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must
- # meet the criteria described by the hash.
- # * <tt>:descendant</tt>: a hash. At least one of the node's descendants
- # must meet the criteria described by the hash.
- # * <tt>:sibling</tt>: a hash. At least one of the node's siblings must
- # meet the criteria described by the hash.
- # * <tt>:after</tt>: a hash. The node must be after any sibling meeting
- # the criteria described by the hash, and at least one sibling must match.
- # * <tt>:before</tt>: a hash. The node must be before any sibling meeting
- # the criteria described by the hash, and at least one sibling must match.
- # * <tt>:children</tt>: a hash, for counting children of a node. Accepts
- # the keys:
- # * <tt>:count</tt>: either a number or a range which must equal (or
- # include) the number of children that match.
- # * <tt>:less_than</tt>: the number of matching children must be less
- # than this number.
- # * <tt>:greater_than</tt>: the number of matching children must be
- # greater than this number.
- # * <tt>:only</tt>: another hash consisting of the keys to use
- # to match on the children, and only matching children will be
- # counted.
- # * <tt>:content</tt>: the textual content of the node must match the
- # given value. This will not match HTML tags in the body of a
- # tag--only text.
- #
- # Conditions are matched using the following algorithm:
- #
- # * if the condition is a string, it must be a substring of the value.
- # * if the condition is a regexp, it must match the value.
- # * if the condition is a number, the value must match number.to_s.
- # * if the condition is +true+, the value must not be +nil+.
- # * if the condition is +false+ or +nil+, the value must be +nil+.
- #
- # Usage:
- #
- # # assert that there is a "span" tag
- # assert_tag :tag => "span"
- #
- # # assert that there is a "span" tag with id="x"
- # assert_tag :tag => "span", :attributes => { :id => "x" }
- #
- # # assert that there is a "span" tag using the short-hand
- # assert_tag :span
- #
- # # assert that there is a "span" tag with id="x" using the short-hand
- # assert_tag :span, :attributes => { :id => "x" }
- #
- # # assert that there is a "span" inside of a "div"
- # assert_tag :tag => "span", :parent => { :tag => "div" }
- #
- # # assert that there is a "span" somewhere inside a table
- # assert_tag :tag => "span", :ancestor => { :tag => "table" }
- #
- # # assert that there is a "span" with at least one "em" child
- # assert_tag :tag => "span", :child => { :tag => "em" }
- #
- # # assert that there is a "span" containing a (possibly nested)
- # # "strong" tag.
- # assert_tag :tag => "span", :descendant => { :tag => "strong" }
- #
- # # assert that there is a "span" containing between 2 and 4 "em" tags
- # # as immediate children
- # assert_tag :tag => "span",
- # :children => { :count => 2..4, :only => { :tag => "em" } }
- #
- # # get funky: assert that there is a "div", with an "ul" ancestor
- # # and an "li" parent (with "class" = "enum"), and containing a
- # # "span" descendant that contains text matching /hello world/
- # assert_tag :tag => "div",
- # :ancestor => { :tag => "ul" },
- # :parent => { :tag => "li",
- # :attributes => { :class => "enum" } },
- # :descendant => { :tag => "span",
- # :child => /hello world/ }
- #
- # <strong>Please note</strong: #assert_tag and #assert_no_tag only work
- # with well-formed XHTML. They recognize a few tags as implicitly self-closing
- # (like br and hr and such) but will not work correctly with tags
- # that allow optional closing tags (p, li, td). <em>You must explicitly
- # close all of your tags to use these assertions.</em>
- def assert_tag(*opts)
- clean_backtrace do
- opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first
- tag = find_tag(opts)
- assert tag, "expected tag, but no tag found matching #{opts.inspect} in:\n#{@response.body.inspect}"
- end
- end
-
- # Identical to #assert_tag, but asserts that a matching tag does _not_
- # exist. (See #assert_tag for a full discussion of the syntax.)
- def assert_no_tag(*opts)
- clean_backtrace do
- opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first
- tag = find_tag(opts)
- assert !tag, "expected no tag, but found tag matching #{opts.inspect} in:\n#{@response.body.inspect}"
- end
- end
- # test 2 html strings to be equivalent, i.e. identical up to reordering of attributes
- def assert_dom_equal(expected, actual, message="")
- clean_backtrace do
- expected_dom = HTML::Document.new(expected).root
- actual_dom = HTML::Document.new(actual).root
- full_message = build_message(message, "<?> expected to be == to\n<?>.", expected_dom.to_s, actual_dom.to_s)
- assert_block(full_message) { expected_dom == actual_dom }
- end
- end
-
- # negated form of +assert_dom_equivalent+
- def assert_dom_not_equal(expected, actual, message="")
- clean_backtrace do
- expected_dom = HTML::Document.new(expected).root
- actual_dom = HTML::Document.new(actual).root
- full_message = build_message(message, "<?> expected to be != to\n<?>.", expected_dom.to_s, actual_dom.to_s)
- assert_block(full_message) { expected_dom != actual_dom }
- end
- end
- # ensures that the passed record is valid by active record standards. returns the error messages if not
- def assert_valid(record)
- clean_backtrace do
- assert record.valid?, record.errors.full_messages.join("\n")
- end
- end
-
- def clean_backtrace(&block)
- yield
- rescue AssertionFailedError => e
- path = File.expand_path(__FILE__)
- raise AssertionFailedError, e.message, e.backtrace.reject { |line| File.expand_path(line) =~ /#{path}/ }
- end
- end
- end
- end
- require 'action_controller/mime_type'
- require 'action_controller/request'
- require 'action_controller/response'
- require 'action_controller/routing'
- require 'action_controller/code_generation'
- require 'action_controller/url_rewriter'
- require 'drb'
- require 'set'
- module ActionController #:nodoc:
- class ActionControllerError < StandardError #:nodoc:
- end
- class SessionRestoreError < ActionControllerError #:nodoc:
- end
- class MissingTemplate < ActionControllerError #:nodoc:
- end
- class RoutingError < ActionControllerError #:nodoc:
- attr_reader :failures
- def initialize(message, failures=[])
- super(message)
- @failures = failures
- end
- end
- class UnknownController < ActionControllerError #:nodoc:
- end
- class UnknownAction < ActionControllerError #:nodoc:
- end
- class MissingFile < ActionControllerError #:nodoc:
- end
- class SessionOverflowError < ActionControllerError #:nodoc:
- DEFAULT_MESSAGE = 'Your session data is larger than the data column in which it is to be stored. You must increase the size of your data column if you intend to store large data.'
- def initialize(message = nil)
- super(message || DEFAULT_MESSAGE)
- end
- end
- class DoubleRenderError < ActionControllerError #:nodoc:
- DEFAULT_MESSAGE = "Render and/or redirect were called multiple times in this action. Please note that you may only call render OR redirect, and only once per action. Also note that neither redirect nor render terminate execution of the action, so if you want to exit an action after redirecting, you need to do something like \"redirect_to(...) and return\". Finally, note that to cause a before filter to halt execution of the rest of the filter chain, the filter must return false, explicitly, so \"render(...) and return false\"."
- def initialize(message = nil)
- super(message || DEFAULT_MESSAGE)
- end
- end
- class RedirectBackError < ActionControllerError #:nodoc:
- DEFAULT_MESSAGE = 'No HTTP_REFERER was set in the request to this action, so redirect_to :back could not be called successfully. If this is a test, make sure to specify @request.env["HTTP_REFERER"].'
-
- def initialize(message = nil)
- super(message || DEFAULT_MESSAGE)
- end
- end
- # Action Controllers are the core of a web request in Rails. They are made up of one or more actions that are executed
- # on request and then either render a template or redirect to another action. An action is defined as a public method
- # on the controller, which will automatically be made accessible to the web-server through Rails Routes.
- #
- # 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
- #
- # Actions, by default, render a template in the <tt>app/views</tt> directory corresponding to the name of the controller and action
- # after executing code in the action. For example, the +index+ action of the +GuestBookController+ would render the
- # template <tt>app/views/guestbook/index.rhtml</tt> by default after populating the <tt>@entries</tt> instance variable.
- #
- # Unlike index, the sign action will not render a template. After performing its main purpose (creating a
- # new entry in the guest book), it 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.
- #
- # == 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 with the request accessor and is primarily used to query for http headers. These queries
- # are made by accessing the environment hash, like this:
- #
- # def server_ip
- # location = request.env["SERVER_ADDR"]
- # render :text => "This server hosted at #{location}"
- # end
- #
- # == Parameters
- #
- # All request parameters, whether they come from a GET or POST request, or from the URL, are available through the params method
- # which returns a hash. For example, an action that was performed through <tt>/weblog/list?category=All&limit=5</tt> will include
- # <tt>{ "category" => "All", "limit" => 5 }</tt> 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 <tt>{ "post" => { "name" => "david", "address" => "hyacintvej" } }</tt>.
- # If the address input had been named "post[address][street]", the params would have included
- # <tt>{ "post" => { "address" => { "street" => "hyacintvej" } } }</tt>. There's no limit to the depth of the nesting.
- #
- # == Sessions
- #
- # Sessions allows you to store objects in between requests. This is useful for objects that are not yet ready to be persisted,
- # such as a Signup object constructed in a multi-paged process, or objects that don't change much and are needed all the time, such
- # as a User object for a system that requires login. The session should not be used, however, as a cache for objects where it's likely
- # they could be changed unknowingly. It's usually too much work to keep it all synchronized -- something databases already excel at.
- #
- # You can place objects in the session by using the <tt>session</tt> method, which accesses a hash:
- #
- # session[:person] = Person.authenticate(user_name, password)
- #
- # And retrieved again through the same hash:
- #
- # Hello #{session[:person]}
- #
- # For removing objects from the session, you can either assign a single key to nil, like <tt>session[:person] = nil</tt>, or you can
- # remove the entire session with reset_session.
- #
- # By default, sessions are stored on the file system in <tt>RAILS_ROOT/tmp/sessions</tt>. 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 store on the filesystem.
- # In other words, think carefully about size and caching before resorting to the use of the session on the filesystem.
- #
- # An alternative to storing sessions on disk is to use ActiveRecordStore to store sessions in your database, which can solve problems
- # caused by storing sessions in the file system and may speed up your application. To use ActiveRecordStore, uncomment the line:
- #
- # config.action_controller.session_store = :active_record_store
- #
- # in your <tt>environment.rb</tt> and run <tt>rake db:sessions:create</tt>.
- #
- # == Responses
- #
- # Each action results in a response, which holds the headers and document to be sent to the user's browser. The actual response
- # object is generated automatically through the use of renders and redirects and requires no user intervention.
- #
- # == Renders
- #
- # Action Controller sends content to the user by using one of five rendering methods. The most versatile and common is the rendering
- # of a template. Included in the Action Pack is the Action View, which enables rendering of ERb templates. It's automatically configured.
- # The controller passes objects to the view by assigning instance variables:
- #
- # def show
- # @post = Post.find(params[:id])
- # end
- #
- # Which are then automatically available to the view:
- #
- # Title: <%= @post.title %>
- #
- # You don't have to rely on the automated rendering. Especially actions that could result in the rendering of different templates will use
- # the manual rendering methods:
- #
- # def search
- # @results = Search.find(params[:query])
- # case @results
- # when 0 then render :action => "no_results"
- # when 1 then render :action => "show"
- # when 2..10 then render :action => "show_many"
- # end
- # end
- #
- # Read more about writing ERb and Builder templates in link:classes/ActionView/Base.html.
- #
- # == Redirects
- #
- # Redirects are used to move from one action to another. For example, after a <tt>create</tt> action, which stores a blog entry to a database,
- # we might like to show the user the new entry. Because we're following good DRY principles (Don't Repeat Yourself), we're going to reuse (and redirect to)
- # a <tt>show</tt> action that we'll assume has already been created. The code might look like this:
- #
- # def create
- # @entry = Entry.new(params[:entry])
- # if @entry.save
- # # The entry was saved correctly, redirect to show
- # redirect_to :action => 'show', :id => @entry.id
- # else
- # # things didn't go so well, do something else
- # end
- # end
- #
- # In this case, after saving our new entry to the database, the user is redirected to the <tt>show</tt> method which is then executed.
- #
- # == Calling multiple redirects or renders
- #
- # An action should conclude with a single render or redirect. Attempting to try to do either again will result in a DoubleRenderError:
- #
- # def do_something
- # redirect_to :action => "elsewhere"
- # render :action => "overthere" # raises DoubleRenderError
- # end
- #
- # If you need to redirect on the condition of something, then be sure to add "and return" to halt execution.
- #
- # def do_something
- # redirect_to(:action => "elsewhere") and return if monkeys.nil?
- # render :action => "overthere" # won't be called unless monkeys is nil
- # end
- #
- class Base
- DEFAULT_RENDER_STATUS_CODE = "200 OK"
-
- include Reloadable::Subclasses
-
- # 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
- # Protected instance variable cache
- @@protected_variables_cache = nil
- cattr_accessor :protected_variables_cache
- # Prepends all the URL-generating helpers from AssetHelper. This makes it possible to easily move javascripts, stylesheets,
- # and images to a dedicated asset server away from the main web server. Example:
- # ActionController::Base.asset_host = "http://assets.example.com"
- @@asset_host = ""
- cattr_accessor :asset_host
- # 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
-
- # Enable or disable the collection of failure information for RoutingErrors.
- # This information can be extremely useful when tweaking custom routes, but is
- # pointless once routes have been tested and verified.
- @@debug_routes = true
- cattr_accessor :debug_routes
- # Controls whether the application is thread-safe, so multi-threaded servers like WEBrick know whether to apply a mutex
- # around the performance of each action. Action Pack and Active Record are by default thread-safe, but many applications
- # may not be. Turned off by default.
- @@allow_concurrency = false
- cattr_accessor :allow_concurrency
- # Modern REST web services often need to submit complex data to the web application.
- # The param_parsers hash lets you register handlers wich will process the http body and add parameters to the
- # <tt>params</tt> hash. These handlers are invoked for post and put requests.
- #
- # By default application/xml is enabled. A XmlSimple class with the same param name as the root will be instanciated
- # in the <tt>params</tt>. This allows XML requests to mask themselves as regular form submissions, so you can have one
- # action serve both regular forms and web service requests.
- #
- # Example of doing your own parser for a custom content type:
- #
- # ActionController::Base.param_parsers[Mime::Type.lookup('application/atom+xml')] = Proc.new do |data|
- # node = REXML::Document.new(post)
- # { node.root.name => node.root }
- # end
- #
- # Note: Up until release 1.1 of Rails, Action Controller would default to using XmlSimple configured to discard the
- # root node for such requests. The new default is to keep the root, such that "<r><name>David</name></r>" results
- # in params[:r][:name] for "David" instead of params[:name]. To get the old behavior, you can
- # re-register XmlSimple as application/xml handler ike this:
- #
- # ActionController::Base.param_parsers[Mime::XML] =
- # Proc.new { |data| XmlSimple.xml_in(data, 'ForceArray' => false) }
- #
- # A YAML parser is also available and can be turned on with:
- #
- # ActionController::Base.param_parsers[Mime::YAML] = :yaml
- @@param_parsers = { Mime::XML => :xml_simple }
- cattr_accessor :param_parsers
- # 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".
- class_inheritable_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 or symbol.
- 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 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
- # Returns the name of the action this controller is processing.
- attr_accessor :action_name
-
- 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
- @controller_class_name ||= name.demodulize
- end
- # Converts the class name from something like "OneModule::TwoModule::NeatController" to "neat".
- def controller_name
- @controller_name ||= controller_class_name.sub(/Controller$/, '').underscore
- end
-
- # Converts the class name from something like "OneModule::TwoModule::NeatController" to "one_module/two_module/neat".
- def controller_path
- @controller_path ||= name.gsub(/Controller$/, '').underscore
- end
- # Return an array containing the names of public methods that have been marked hidden from the action processor.
- # By default, all methods defined in ActionController::Base and included modules are hidden.
- # More methods can be hidden using <tt>hide_actions</tt>.
- def hidden_actions
- write_inheritable_attribute(:hidden_actions, ActionController::Base.public_instance_methods) unless read_inheritable_attribute(:hidden_actions)
- read_inheritable_attribute(:hidden_actions)
- end
- # Hide each of the given methods from being callable as actions.
- def hide_action(*names)
- write_inheritable_attribute(:hidden_actions, hidden_actions | names.collect { |n| n.to_s })
- end
-
- # Replace sensitive paramater data from the request log.
- # Filters paramaters that have any of the arguments as a substring.
- # Looks in all subhashes of the param hash for keys to filter.
- # If a block is given, each key and value of the paramater hash and all
- # subhashes is passed to it, the value or key
- # can be replaced using String#replace or similar method.
- #
- # Examples:
- # filter_parameter_logging
- # => Does nothing, just slows the logging process down
- #
- # filter_parameter_logging :password
- # => replaces the value to all keys matching /password/i with "[FILTERED]"
- #
- # filter_parameter_logging :foo, "bar"
- # => replaces the value to all keys matching /foo|bar/i with "[FILTERED]"
- #
- # filter_parameter_logging { |k,v| v.reverse! if k =~ /secret/i }
- # => reverses the value to all keys matching /secret/i
- #
- # filter_parameter_logging(:foo, "bar") { |k,v| v.reverse! if k =~ /secret/i }
- # => reverses the value to all keys matching /secret/i, and
- # replaces the value to all keys matching /foo|bar/i with "[FILTERED]"
- def filter_parameter_logging(*filter_words, &block)
- parameter_filter = Regexp.new(filter_words.collect{ |s| s.to_s }.join('|'), true) if filter_words.length > 0
- define_method(:filter_parameters) do |unfiltered_parameters|
- filtered_parameters = {}
- unfiltered_parameters.each do |key, value|
- if key =~ parameter_filter
- filtered_parameters[key] = '[FILTERED]'
- elsif value.is_a?(Hash)
- filtered_parameters[key] = filter_parameters(value)
- elsif block_given?
- key, value = key.dup, value.dup
- yield key, value
- filtered_parameters[key] = value
- else
- filtered_parameters[key] = value
- end
- end
- filtered_parameters
- end
- end
- end
- public
- # Extracts the action_name from the request parameters and performs that action.
- def process(request, response, method = :perform_action, *arguments) #:nodoc:
- initialize_template_class(response)
- assign_shortcuts(request, response)
- initialize_current_url
- assign_names
- forget_variables_added_to_assigns
-
- log_processing
- send(method, *arguments)
-
- response
- ensure
- process_cleanup
- end
- # Returns a URL that has been rewritten according to the options hash and the defined Routes.
- # (For doing a complete redirect, use redirect_to).
- #
- # <tt>url_for</tt> is used to:
- #
- # All keys given to url_for are forwarded to the Route module, save for the following:
- # * <tt>:anchor</tt> -- specifies the anchor name to be appended to the path. For example,
- # <tt>url_for :controller => 'posts', :action => 'show', :id => 10, :anchor => 'comments'</tt>
- # will produce "/posts/show/10#comments".
- # * <tt>:only_path</tt> -- if true, returns the absolute URL (omitting the protocol, host name, and port)
- # * <tt>:trailing_slash</tt> -- if true, adds a trailing slash, as in "/archive/2005/". Note that this
- # is currently not recommended since it breaks caching.
- # * <tt>:host</tt> -- overrides the default (current) host if provided
- # * <tt>:protocol</tt> -- overrides the default (current) protocol if provided
- #
- # The URL is generated from the remaining keys in the hash. A URL contains two key parts: the <base> and a query string.
- # Routes composes a query string as the key/value pairs not included in the <base>.
- #
- # The default Routes setup supports a typical Rails path of "controller/action/id" where action and id are optional, with
- # action defaulting to 'index' when not given. Here are some typical url_for statements and their corresponding URLs:
- #
- # url_for :controller => 'posts', :action => 'recent' # => 'proto://host.com/posts/recent'
- # url_for :controller => 'posts', :action => 'index' # => 'proto://host.com/posts'
- # url_for :controller => 'posts', :action => 'show', :id => 10 # => 'proto://host.com/posts/show/10'
- #
- # When generating a new URL, missing values may be filled in from the current request's parameters. For example,
- # <tt>url_for :action => 'some_action'</tt> will retain the current controller, as expected. This behavior extends to
- # other parameters, including <tt>:controller</tt>, <tt>:id</tt>, and any other parameters that are placed into a Route's
- # path.
- #
- # The URL helpers such as <tt>url_for</tt> have a limited form of memory: when generating a new URL, they can look for
- # missing values in the current request's parameters. Routes attempts to guess when a value should and should not be
- # taken from the defaults. There are a few simple rules on how this is performed:
- #
- # * If the controller name begins with a slash, no defaults are used: <tt>url_for :controller => '/home'</tt>
- # * If the controller changes, the action will default to index unless provided
- #
- # The final rule is applied while the URL is being generated and is best illustrated by an example. Let us consider the
- # route given by <tt>map.connect 'people/:last/:first/:action', :action => 'bio', :controller => 'people'</tt>.
- #
- # Suppose that the current URL is "people/hh/david/contacts". Let's consider a few different cases of URLs which are generated
- # from this page.
- #
- # * <tt>url_for :action => 'bio'</tt> -- During the generation of this URL, default values will be used for the first and
- # last components, and the action shall change. The generated URL will be, "people/hh/david/bio".
- # * <tt>url_for :first => 'davids-little-brother'</tt> This generates the URL 'people/hh/davids-little-brother' -- note
- # that this URL leaves out the assumed action of 'bio'.
- #
- # However, you might ask why the action from the current request, 'contacts', isn't carried over into the new URL. The
- # answer has to do with the order in which the parameters appear in the generated path. In a nutshell, since the
- # value that appears in the slot for <tt>:first</tt> is not equal to default value for <tt>:first</tt> we stop using
- # defaults. On it's own, this rule can account for much of the typical Rails URL behavior.
- #
- # Although a convienence, defaults can occasionaly get in your way. In some cases a default persists longer than desired.
- # The default may be cleared by adding <tt>:name => nil</tt> to <tt>url_for</tt>'s options.
- # This is often required when writing form helpers, since the defaults in play may vary greatly depending upon where the
- # helper is used from. The following line will redirect to PostController's default action, regardless of the page it is
- # displayed on:
- #
- # url_for :controller => 'posts', :action => nil
- #
- # If you explicitly want to create a URL that's almost the same as the current URL, you can do so using the
- # :overwrite_params options. Say for your posts you have different views for showing and printing them.
- # Then, in the show view, you get the URL for the print view like this
- #
- # url_for :overwrite_params => { :action => 'print' }
- #
- # This takes the current URL as is and only exchanges the action. In contrast, <tt>url_for :action => 'print'</tt>
- # would have slashed-off the path components after the changed action.
- 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
- # 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
- def session_enabled?
- request.session_options[:disabled] != false
- end
- protected
- # Renders the content that will be returned to the browser as the response body.
- #
- # === Rendering an action
- #
- # Action rendering is the most common form and the type used automatically by Action Controller when nothing else is
- # specified. By default, actions are rendered within the current layout (if one exists).
- #
- # # Renders the template for the action "goal" within the current controller
- # render :action => "goal"
- #
- # # Renders the template for the action "short_goal" within the current controller,
- # # but without the current active layout
- # render :action => "short_goal", :layout => false
- #
- # # Renders the template for the action "long_goal" within the current controller,
- # # but with a custom layout
- # render :action => "long_goal", :layout => "spectacular"
- #
- # _Deprecation_ _notice_: This used to have the signatures <tt>render_action("action", status = 200)</tt>,
- # <tt>render_without_layout("controller/action", status = 200)</tt>, and
- # <tt>render_with_layout("controller/action", status = 200, layout)</tt>.
- #
- # === Rendering partials
- #
- # Partial rendering is most commonly used together with Ajax calls that only update one or a few elements on a page
- # without reloading. Rendering of partials from the controller makes it possible to use the same partial template in
- # both the full-page rendering (by calling it from within the template) and when sub-page updates happen (from the
- # controller action responding to Ajax calls). By default, the current layout is not used.
- #
- # # Renders the partial located at app/views/controller/_win.r(html|xml)
- # render :partial => "win"
- #
- # # Renders the partial with a status code of 500 (internal error)
- # render :partial => "broken", :status => 500
- #
- # # Renders the same partial but also makes a local variable available to it
- # render :partial => "win", :locals => { :name => "david" }
- #
- # # Renders a collection of the same partial by making each element of @wins available through
- # # the local variable "win" as it builds the complete response
- # render :partial => "win", :collection => @wins
- #
- # # Renders the same collection of partials, but also renders the win_divider partial in between
- # # each win partial.
- # render :partial => "win", :collection => @wins, :spacer_template => "win_divider"
- #
- # _Deprecation_ _notice_: This used to have the signatures
- # <tt>render_partial(partial_path = default_template_name, object = nil, local_assigns = {})</tt> and
- # <tt>render_partial_collection(partial_name, collection, partial_spacer_template = nil, local_assigns = {})</tt>.
- #
- # === Rendering a template
- #
- # Template rendering works just like action rendering except that it takes a path relative to the template root.
- # The current layout is automatically applied.
- #
- # # Renders the template located in [TEMPLATE_ROOT]/weblog/show.r(html|xml) (in Rails, app/views/weblog/show.rhtml)
- # render :template => "weblog/show"
- #
- # === Rendering a file
- #
- # File rendering works just like action rendering except that it takes a filesystem path. By default, the path
- # is assumed to be absolute, and the current layout is not applied.
- #
- # # Renders the template located at the absolute filesystem path
- # render :file => "/path/to/some/template.rhtml"
- # render :file => "c:/path/to/some/template.rhtml"
- #
- # # Renders a template within the current layout, and with a 404 status code
- # render :file => "/path/to/some/template.rhtml", :layout => true, :status => 404
- # render :file => "c:/path/to/some/template.rhtml", :layout => true, :status => 404
- #
- # # Renders a template relative to the template root and chooses the proper file extension
- # render :file => "some/template", :use_full_path => true
- #
- # _Deprecation_ _notice_: This used to have the signature <tt>render_file(path, status = 200)</tt>
- #
- # === Rendering text
- #
- # Rendering of text is usually used for tests or for rendering prepared content, such as a cache. By default, text
- # rendering is not done within the active layout.
- #
- # # Renders the clear text "hello world" with status code 200
- # render :text => "hello world!"
- #
- # # Renders the clear text "Explosion!" with status code 500
- # render :text => "Explosion!", :status => 500
- #
- # # Renders the clear text "Hi there!" within the current active layout (if one exists)
- # render :text => "Explosion!", :layout => true
- #
- # # Renders the clear text "Hi there!" within the layout
- # # placed in "app/views/layouts/special.r(html|xml)"
- # render :text => "Explosion!", :layout => "special"
- #
- # _Deprecation_ _notice_: This used to have the signature <tt>render_text("text", status = 200)</tt>
- #
- # === Rendering an inline template
- #
- # Rendering of an inline template works as a cross between text and action rendering where the source for the template
- # is supplied inline, like text, but its interpreted with ERb or Builder, like action. By default, ERb is used for rendering
- # and the current layout is not used.
- #
- # # Renders "hello, hello, hello, again"
- # render :inline => "<%= 'hello, ' * 3 + 'again' %>"
- #
- # # Renders "<p>Good seeing you!</p>" using Builder
- # render :inline => "xml.p { 'Good seeing you!' }", :type => :rxml
- #
- # # Renders "hello david"
- # render :inline => "<%= 'hello ' + name %>", :locals => { :name => "david" }
- #
- # _Deprecation_ _notice_: This used to have the signature <tt>render_template(template, status = 200, type = :rhtml)</tt>
- #
- # === Rendering inline JavaScriptGenerator page updates
- #
- # In addition to rendering JavaScriptGenerator page updates with Ajax in RJS templates (see ActionView::Base for details),
- # you can also pass the <tt>:update</tt> parameter to +render+, along with a block, to render page updates inline.
- #
- # render :update do |page|
- # page.replace_html 'user_list', :partial => 'user', :collection => @users
- # page.visual_effect :highlight, 'user_list'
- # end
- #
- # === Rendering nothing
- #
- # Rendering nothing is often convenient in combination with Ajax calls that perform their effect client-side or
- # when you just want to communicate a status code. Due to a bug in Safari, nothing actually means a single space.
- #
- # # Renders an empty response with status code 200
- # render :nothing => true
- #
- # # Renders an empty response with status code 401 (access denied)
- # render :nothing => true, :status => 401
- def render(options = nil, deprecated_status = nil, &block) #:doc:
- raise DoubleRenderError, "Can only render or redirect once per action" if performed?
- # Backwards compatibility
- unless options.is_a?(Hash)
- if options == :update
- options = {:update => true}
- else
- return render_file(options || default_template_name, deprecated_status, true)
- end
- end
- if content_type = options[:content_type]
- headers["Content-Type"] = content_type
- end
- if text = options[:text]
- render_text(text, options[:status])
- else
- if file = options[:file]
- render_file(file, options[:status], options[:use_full_path], options[:locals] || {})
- elsif template = options[:template]
- render_file(template, options[:status], true)
-
- elsif inline = options[:inline]
- render_template(inline, options[:status], options[:type], options[:locals] || {})
-
- elsif action_name = options[:action]
- render_action(action_name, options[:status], options[:layout])
- elsif xml = options[:xml]
- render_xml(xml, options[:status])
- elsif partial = options[:partial]
- partial = default_template_name if partial == true
- if collection = options[:collection]
- render_partial_collection(partial, collection, options[:spacer_template], options[:locals], options[:status])
- else
- render_partial(partial, ActionView::Base::ObjectWrapper.new(options[:object]), options[:locals], options[:status])
- end
- elsif options[:update]
- add_variables_to_assigns
- @template.send :evaluate_assigns
-
- generator = ActionView::Helpers::PrototypeHelper::JavaScriptGenerator.new(@template, &block)
- render_javascript(generator.to_s)
- elsif options[:nothing]
- # Safari doesn't pass the headers of the return if the response is zero length
- render_text(" ", options[:status])
-
- else
- render_file(default_template_name, options[:status], true)
-
- end
- end
- end
- # Renders according to the same rules as <tt>render</tt>, but returns the result in a string instead
- # of sending it as the response body to the browser.
- def render_to_string(options = nil, &block) #:doc:
- result = render(options, &block)
- erase_render_results
- forget_variables_added_to_assigns
- reset_variables_added_to_assigns
- result
- end
- def render_action(action_name, status = nil, with_layout = true) #:nodoc:
- template = default_template_name(action_name.to_s)
- if with_layout && !template_exempt_from_layout?(template)
- render_with_layout(template, status)
- else
- render_without_layout(template, status)
- end
- end
- def render_file(template_path, status = nil, use_full_path = false, locals = {}) #:nodoc:
- add_variables_to_assigns
- assert_existence_of_template_file(template_path) if use_full_path
- logger.info("Rendering #{template_path}" + (status ? " (#{status})" : '')) if logger
- render_text(@template.render_file(template_path, use_full_path, locals), status)
- end
- def render_template(template, status = nil, type = :rhtml, local_assigns = {}) #:nodoc:
- add_variables_to_assigns
- render_text(@template.render_template(type, template, nil, local_assigns), status)
- end
- def render_text(text = nil, status = nil) #:nodoc:
- @performed_render = true
- @response.headers['Status'] = (status || DEFAULT_RENDER_STATUS_CODE).to_s
- @response.body = text
- end
- def render_javascript(javascript, status = nil) #:nodoc:
- @response.headers['Content-Type'] = 'text/javascript; charset=UTF-8'
- render_text(javascript, status)
- end
- def render_xml(xml, status = nil) #:nodoc:
- @response.headers['Content-Type'] = 'application/xml'
- render_text(xml, status)
- end
- def render_nothing(status = nil) #:nodoc:
- render_text(' ', status)
- end
- def render_partial(partial_path = default_template_name, object = nil, local_assigns = nil, status = nil) #:nodoc:
- add_variables_to_assigns
- render_text(@template.render_partial(partial_path, object, local_assigns), status)
- end
- def render_partial_collection(partial_name, collection, partial_spacer_template = nil, local_assigns = nil, status = nil) #:nodoc:
- add_variables_to_assigns
- render_text(@template.render_partial_collection(partial_name, collection, partial_spacer_template, local_assigns), status)
- end
- def render_with_layout(template_name = default_template_name, status = nil, layout = nil) #:nodoc:
- render_with_a_layout(template_name, status, layout)
- end
- def render_without_layout(template_name = default_template_name, status = nil) #:nodoc:
- render_with_no_layout(template_name, status)
- end
- # Clears the rendered results, allowing for another render to be performed.
- def erase_render_results #:nodoc:
- @response.body = nil
- @performed_render = false
- end
-
- # Clears the redirected results from the headers, resets the status to 200 and returns
- # the URL that was used to redirect or nil if there was no redirected URL
- # Note that +redirect_to+ will change the body of the response to indicate a redirection.
- # The response body is not reset here, see +erase_render_results+
- def erase_redirect_results #:nodoc:
- @performed_redirect = false
- response.redirected_to = nil
- response.redirected_to_method_params = nil
- response.headers['Status'] = DEFAULT_RENDER_STATUS_CODE
- response.headers.delete('location')
- end
- # Erase both render and redirect results
- def erase_results #:nodoc:
- erase_render_results
- erase_redirect_results
- end
- def rewrite_options(options) #:nodoc:
- 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)
- # { :project => @project.active? ? @project.url_name : "unknown" }
- # 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 the target specified in +options+. This parameter can take one of three forms:
- #
- # * <tt>Hash</tt>: The URL will be generated by calling url_for with the +options+.
- # * <tt>String starting with protocol:// (like http://)</tt>: Is passed straight through as the target for redirection.
- # * <tt>String not containing a protocol</tt>: The current protocol and host is prepended to the string.
- # * <tt>:back</tt>: Back to the page that issued the request. Useful for forms that are triggered from multiple places.
- # Short-hand for redirect_to(request.env["HTTP_REFERER"])
- #
- # Examples:
- # redirect_to :action => "show", :id => 5
- # redirect_to "http://www.rubyonrails.org"
- # redirect_to "/images/screenshot.jpg"
- # redirect_to :back
- #
- # The redirection happens as a "302 Moved" header.
- #
- # When using <tt>redirect_to :back</tt>, if there is no referrer,
- # RedirectBackError will be raised. You may specify some fallback
- # behavior for this case by rescueing RedirectBackError.
- def redirect_to(options = {}, *parameters_for_method_reference) #:doc:
- case options
- when %r{^\w+://.*}
- raise DoubleRenderError if performed?
- logger.info("Redirected to #{options}") if logger
- response.redirect(options)
- response.redirected_to = options
- @performed_redirect = true
- when String
- redirect_to(request.protocol + request.host_with_port + options)
-
- when :back
- request.env["HTTP_REFERER"] ? redirect_to(request.env["HTTP_REFERER"]) : raise(RedirectBackError)
- else
- if parameters_for_method_reference.empty?
- redirect_to(url_for(options))
- response.redirected_to = options
- else
- redirect_to(url_for(options, *parameters_for_method_reference))
- response.redirected_to, response.redirected_to_method_params = options, parameters_for_method_reference
- end
- end
- end
-
- # Sets a HTTP 1.1 Cache-Control header. Defaults to issuing a "private" instruction, so that
- # intermediate caches shouldn't cache the response.
- #
- # Examples:
- # expires_in 20.minutes
- # expires_in 3.hours, :private => false
- # expires in 3.hours, 'max-stale' => 5.hours, :private => nil, :public => true
- #
- # This method will overwrite an existing Cache-Control header.
- # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities.
- def expires_in(seconds, options = {}) #:doc:
- cache_options = { 'max-age' => seconds, 'private' => true }.symbolize_keys.merge!(options.symbolize_keys)
- cache_options.delete_if { |k,v| v.nil? or v == false }
- cache_control = cache_options.map{ |k,v| v == true ? k.to_s : "#{k.to_s}=#{v.to_s}"}
- @response.headers["Cache-Control"] = cache_control.join(', ')
- end
-
- # Sets a HTTP 1.1 Cache-Control header of "no-cache" so no caching should occur by the browser or
- # intermediate caches (like caching proxy servers).
- def expires_now #:doc:
- @response.headers["Cache-Control"] = "no-cache"
- end
- # Resets the session by clearing 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 self.view_class
- @view_class ||=
- # create a new class based on the default template class and include helper methods
- returning Class.new(ActionView::Base) do |view_class|
- view_class.send(:include, master_helper_module)
- end
- end
- def self.view_root
- @view_root ||= template_root
- end
- def initialize_template_class(response)
- raise "You must assign a template class through ActionController.template_class= before processing a request" unless @@template_class
-
- response.template = self.class.view_class.new(self.class.view_root, {}, self)
- response.redirected_to = nil
- @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, @params.clone())
- end
- def log_processing
- if logger
- logger.info "\n\nProcessing #{controller_class_name}\##{action_name} (for #{request_origin}) [#{request.method.to_s.upcase}]"
- logger.info " Session ID: #{@session.session_id}" if @session and @session.respond_to?(:session_id)
- logger.info " Parameters: #{respond_to?(:filter_parameters) ? filter_parameters(@params).inspect : @params.inspect}"
- end
- end
-
- def perform_action
- if self.class.action_methods.include?(action_name) || self.class.action_methods.include?('method_missing')
- send(action_name)
- render unless performed?
- elsif template_exists? && template_public?
- render
- else
- raise UnknownAction, "No action responded to #{action_name}", caller
- end
- end
-
- def performed?
- @performed_render || @performed_redirect
- end
- def assign_names
- @action_name = (params['action'] || 'index')
- end
-
- def action_methods
- self.class.action_methods
- end
- def self.action_methods
- @action_methods ||= Set.new(public_instance_methods - hidden_actions)
- end
- def add_variables_to_assigns
- unless @variables_added
- add_instance_variables_to_assigns
- add_class_variables_to_assigns if view_controller_internals
- @variables_added = true
- end
- end
-
- def forget_variables_added_to_assigns
- @variables_added = nil
- end
-
- def reset_variables_added_to_assigns
- @template.instance_variable_set("@assigns_added", nil)
- end
- def add_instance_variables_to_assigns
- @@protected_variables_cache ||= protected_instance_variables.inject({}) { |h, k| h[k] = true; h }
- 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", "@request_origin", "@parent_controller" ]
- end
- end
- def request_origin
- # this *needs* to be cached!
- # otherwise you'd get different results if calling it more than once
- @request_origin ||= "#{@request.remote_ip} at #{Time.now.to_s(:db)}"
- end
-
- def complete_request_uri
- "#{@request.protocol}#{@request.host}#{@request.request_uri}"
- 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 template_exempt_from_layout?(template_name = default_template_name)
- template_name =~ /\.rjs$/ || (@template.pick_template_extension(template_name) == :rjs rescue false)
- end
- def assert_existence_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 default_template_name(action_name = self.action_name)
- if action_name
- action_name = action_name.to_s
- if action_name.include?('/') && template_path_includes_controller?(action_name)
- action_name = strip_out_controller(action_name)
- end
- end
- "#{self.class.controller_path}/#{action_name}"
- end
-
- def strip_out_controller(path)
- path.split('/', 2).last
- end
-
- def template_path_includes_controller?(path)
- self.class.controller_path.split('/')[-1] == path.split('/')[0]
- end
- def process_cleanup
- close_session
- end
- end
- end
- 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.included(base)
- base.extend(ClassMethods)
- base.class_eval do
- 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
- end
- module ClassMethods
- # Log and benchmark the workings of a single block and silence whatever logging that may have happened inside it
- # (unless <tt>use_silence</tt> is set to false).
- #
- # The benchmark is only recorded if the current level of the logger matches the <tt>log_level</tt>, which makes it
- # easy to include benchmarking statements in production software that will remain inexpensive because the benchmark
- # will only be conducted if the log level is low enough.
- def benchmark(title, log_level = Logger::DEBUG, use_silence = true)
- if logger && logger.level == log_level
- result = nil
- seconds = Benchmark.realtime { result = use_silence ? silence { yield } : yield }
- logger.add(log_level, "#{title} (#{'%.5f' % seconds})")
- result
- else
- yield
- end
- end
- # Silences the logger for the duration of the block.
- def silence
- old_logger_level, logger.level = logger.level, Logger::ERROR if logger
- yield
- ensure
- logger.level = old_logger_level if logger
- end
- end
- def render_with_benchmark(options = nil, deprecated_status = nil, &block)
- unless logger
- render_without_benchmark(options, deprecated_status, &block)
- else
- db_runtime = ActiveRecord::Base.connection.reset_runtime if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
- render_output = nil
- @rendering_runtime = Benchmark::measure{ render_output = render_without_benchmark(options, deprecated_status, &block) }.real
- if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
- @db_rt_before_render = db_runtime
- @db_rt_after_render = ActiveRecord::Base.connection.reset_runtime
- @rendering_runtime -= @db_rt_after_render
- end
- render_output
- end
- end
- def perform_action_with_benchmark
- unless logger
- perform_action_without_benchmark
- else
- runtime = [Benchmark::measure{ perform_action_without_benchmark }.real, 0.0001].max
- log_message = "Completed in #{sprintf("%.5f", 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?
- log_message << " | #{headers["Status"]}"
- log_message << " [#{complete_request_uri rescue "unknown"}]"
- logger.info(log_message)
- end
- end
-
- private
- def rendering_runtime(runtime)
- " | Rendering: #{sprintf("%.5f", @rendering_runtime)} (#{sprintf("%d", (@rendering_runtime * 100) / runtime)}%)"
- end
- def active_record_runtime(runtime)
- db_runtime = ActiveRecord::Base.connection.reset_runtime
- db_runtime += @db_rt_before_render if @db_rt_before_render
- db_runtime += @db_rt_after_render if @db_rt_after_render
- db_percentage = (db_runtime * 100) / runtime
- " | DB: #{sprintf("%.5f", db_runtime)} (#{sprintf("%d", db_percentage)}%)"
- end
- end
- end
- require 'fileutils'
- module ActionController #:nodoc:
- # Caching is a cheap way of speeding up slow applications by keeping the result of calculations, renderings, and database calls
- # around for subsequent requests. Action Controller affords you three approaches in varying levels of granularity: Page, Action, Fragment.
- #
- # You can read more about each approach and the sweeping assistance by clicking the modules below.
- #
- # Note: To turn off all caching and sweeping, set Base.perform_caching = false.
- module Caching
- def self.included(base) #:nodoc:
- base.send(:include, Pages, Actions, Fragments, Sweeping)
- base.class_eval do
- @@perform_caching = true
- cattr_accessor :perform_caching
- end
- end
- # Page caching is an approach to caching where the entire action output of is stored as a HTML file that the web server
- # can serve without going through the Action Pack. This can be as much as 100 times faster than going through the process of dynamically
- # generating the content. Unfortunately, this incredible speed-up is only available to stateless pages where all visitors
- # are treated the same. Content management systems -- including weblogs and wikis -- have many pages that are a great fit
- # for this approach, but account-based systems where people log in and manipulate their own data are often less likely candidates.
- #
- # Specifying which actions to cache is done through the <tt>caches</tt> class method:
- #
- # class WeblogController < ActionController::Base
- # caches_page :show, :new
- # end
- #
- # This will generate cache files such as weblog/show/5 and weblog/new, which match the URLs used to trigger the dynamic
- # generation. This is how the web server is able pick up a cache file when it exists and otherwise let the request pass on to
- # the Action Pack to generate it.
- #
- # Expiration of the cache is handled by deleting the cached file, which results in a lazy regeneration approach where the cache
- # is not restored before another hit is made against it. The API for doing so mimics the options from url_for and friends:
- #
- # class WeblogController < ActionController::Base
- # def update
- # List.update(params[:list][:id], params[:list])
- # expire_page :action => "show", :id => params[:list][:id]
- # redirect_to :action => "show", :id => params[:list][:id]
- # end
- # end
- #
- # Additionally, you can expire caches using Sweepers that act on changes in the model to determine when a cache is supposed to be
- # expired.
- #
- # == Setting the cache directory
- #
- # The cache directory should be the document root for the web server and is set using Base.page_cache_directory = "/document/root".
- # For Rails, this directory has already been set to RAILS_ROOT + "/public".
- #
- # == Setting the cache extension
- #
- # By default, the cache extension is .html, which makes it easy for the cached files to be picked up by the web server. If you want
- # something else, like .php or .shtml, just set Base.page_cache_extension.
- module Pages
- def self.included(base) #:nodoc:
- base.extend(ClassMethods)
- base.class_eval do
- @@page_cache_directory = defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/public" : ""
- cattr_accessor :page_cache_directory
- @@page_cache_extension = '.html'
- cattr_accessor :page_cache_extension
- end
- end
- module ClassMethods
- # Expires the page that was cached with the +path+ as a key. Example:
- # expire_page "/lists/show"
- def expire_page(path)
- return unless perform_caching
- benchmark "Expired page: #{page_cache_file(path)}" do
- File.delete(page_cache_path(path)) if File.exists?(page_cache_path(path))
- end
- end
- # Manually cache the +content+ in the key determined by +path+. Example:
- # cache_page "I'm the cached content", "/lists/show"
- def cache_page(content, path)
- return unless perform_caching
- benchmark "Cached page: #{page_cache_file(path)}" do
- FileUtils.makedirs(File.dirname(page_cache_path(path)))
- File.open(page_cache_path(path), "wb+") { |f| f.write(content) }
- end
- end
- # Caches the +actions+ using the page-caching approach that'll store the cache in a path within the page_cache_directory that
- # matches the triggering url.
- def caches_page(*actions)
- return unless perform_caching
- actions.each do |action|
- class_eval "after_filter { |c| c.cache_page if c.action_name == '#{action}' }"
- end
- end
- private
- def page_cache_file(path)
- name = ((path.empty? || path == "/") ? "/index" : URI.unescape(path))
- name << page_cache_extension unless (name.split('/').last || name).include? '.'
- return name
- end
- def page_cache_path(path)
- page_cache_directory + page_cache_file(path)
- end
- end
- # Expires the page that was cached with the +options+ as a key. Example:
- # expire_page :controller => "lists", :action => "show"
- def expire_page(options = {})
- return unless perform_caching
- if options[:action].is_a?(Array)
- options[:action].dup.each do |action|
- self.class.expire_page(url_for(options.merge({ :only_path => true, :skip_relative_url_root => true, :action => action })))
- end
- else
- self.class.expire_page(url_for(options.merge({ :only_path => true, :skip_relative_url_root => true })))
- end
- end
- # Manually cache the +content+ in the key determined by +options+. If no content is provided, the contents of @response.body is used
- # If no options are provided, the current +options+ for this action is used. Example:
- # cache_page "I'm the cached content", :controller => "lists", :action => "show"
- def cache_page(content = nil, options = {})
- return unless perform_caching && caching_allowed
- self.class.cache_page(content || @response.body, url_for(options.merge({ :only_path => true, :skip_relative_url_root => true })))
- end
- private
- def caching_allowed
- !@request.post? && @response.headers['Status'] && @response.headers['Status'].to_i < 400
- end
- end
- # Action caching is similar to page caching by the fact that the entire output of the response is cached, but unlike page caching,
- # every request still goes through the Action Pack. The key benefit of this is that filters are run before the cache is served, which
- # allows for authentication and other restrictions on whether someone is allowed to see the cache. Example:
- #
- # class ListsController < ApplicationController
- # before_filter :authenticate, :except => :public
- # caches_page :public
- # caches_action :show, :feed
- # end
- #
- # In this example, the public action doesn't require authentication, so it's possible to use the faster page caching method. But both the
- # show and feed action are to be shielded behind the authenticate filter, so we need to implement those as action caches.
- #
- # Action caching internally uses the fragment caching and an around filter to do the job. The fragment cache is named according to both
- # the current host and the path. So a page that is accessed at http://david.somewhere.com/lists/show/1 will result in a fragment named
- # "david.somewhere.com/lists/show/1". This allows the cacher to differentiate between "david.somewhere.com/lists/" and
- # "jamis.somewhere.com/lists/" -- which is a helpful way of assisting the subdomain-as-account-key pattern.
- module Actions
- def self.append_features(base) #:nodoc:
- super
- base.extend(ClassMethods)
- base.send(:attr_accessor, :rendered_action_cache)
- end
- module ClassMethods #:nodoc:
- def caches_action(*actions)
- return unless perform_caching
- around_filter(ActionCacheFilter.new(*actions))
- end
- end
- def expire_action(options = {})
- return unless perform_caching
- if options[:action].is_a?(Array)
- options[:action].dup.each do |action|
- expire_fragment(url_for(options.merge({ :action => action })).split("://").last)
- end
- else
- expire_fragment(url_for(options).split("://").last)
- end
- end
- class ActionCacheFilter #:nodoc:
- def initialize(*actions)
- @actions = actions
- end
- def before(controller)
- return unless @actions.include?(controller.action_name.intern)
- if cache = controller.read_fragment(controller.url_for.split("://").last)
- controller.rendered_action_cache = true
- controller.send(:render_text, cache)
- false
- end
- end
- def after(controller)
- return if !@actions.include?(controller.action_name.intern) || controller.rendered_action_cache
- controller.write_fragment(controller.url_for.split("://").last, controller.response.body)
- end
- end
- end
- # Fragment caching is used for caching various blocks within templates without caching the entire action as a whole. This is useful when
- # certain elements of an action change frequently or depend on complicated state while other parts rarely change or can be shared amongst multiple
- # parties. The caching is doing using the cache helper available in the Action View. A template with caching might look something like:
- #
- # <b>Hello <%= @name %></b>
- # <% cache do %>
- # All the topics in the system:
- # <%= render_collection_of_partials "topic", Topic.find_all %>
- # <% end %>
- #
- # This cache will bind to the name of action that called it. So you would be able to invalidate it using
- # <tt>expire_fragment(:controller => "topics", :action => "list")</tt> -- if that was the controller/action used. This is not too helpful
- # if you need to cache multiple fragments per action or if the action itself is cached using <tt>caches_action</tt>. So instead we should
- # qualify the name of the action used with something like:
- #
- # <% cache(:action => "list", :action_suffix => "all_topics") do %>
- #
- # That would result in a name such as "/topics/list/all_topics", which wouldn't conflict with any action cache and neither with another
- # fragment using a different suffix. Note that the URL doesn't have to really exist or be callable. We're just using the url_for system
- # to generate unique cache names that we can refer to later for expirations. The expiration call for this example would be
- # <tt>expire_fragment(:controller => "topics", :action => "list", :action_suffix => "all_topics")</tt>.
- #
- # == Fragment stores
- #
- # In order to use the fragment caching, you need to designate where the caches should be stored. This is done by assigning a fragment store
- # of which there are four different kinds:
- #
- # * FileStore: Keeps the fragments on disk in the +cache_path+, which works well for all types of environments and shares the fragments for
- # all the web server processes running off the same application directory.
- # * MemoryStore: Keeps the fragments in memory, which is fine for WEBrick and for FCGI (if you don't care that each FCGI process holds its
- # own fragment store). It's not suitable for CGI as the process is thrown away at the end of each request. It can potentially also take
- # up a lot of memory since each process keeps all the caches in memory.
- # * DRbStore: Keeps the fragments in the memory of a separate, shared DRb process. This works for all environments and only keeps one cache
- # around for all processes, but requires that you run and manage a separate DRb process.
- # * MemCacheStore: Works like DRbStore, but uses Danga's MemCache instead.
- # Requires the ruby-memcache library: gem install ruby-memcache.
- #
- # Configuration examples (MemoryStore is the default):
- #
- # ActionController::Base.fragment_cache_store = :memory_store
- # ActionController::Base.fragment_cache_store = :file_store, "/path/to/cache/directory"
- # ActionController::Base.fragment_cache_store = :drb_store, "druby://localhost:9192"
- # ActionController::Base.fragment_cache_store = :mem_cache_store, "localhost"
- # ActionController::Base.fragment_cache_store = MyOwnStore.new("parameter")
- module Fragments
- def self.append_features(base) #:nodoc:
- super
- base.class_eval do
- @@fragment_cache_store = MemoryStore.new
- cattr_reader :fragment_cache_store
- def self.fragment_cache_store=(store_option)
- store, *parameters = *([ store_option ].flatten)
- @@fragment_cache_store = if store.is_a?(Symbol)
- store_class_name = (store == :drb_store ? "DRbStore" : store.to_s.camelize)
- store_class = ActionController::Caching::Fragments.const_get(store_class_name)
- store_class.new(*parameters)
- else
- store
- end
- end
- end
- end
- def fragment_cache_key(name)
- name.is_a?(Hash) ? url_for(name).split("://").last : name
- end
- # Called by CacheHelper#cache
- def cache_erb_fragment(block, name = {}, options = nil)
- unless perform_caching then block.call; return end
- buffer = eval("_erbout", block.binding)
- if cache = read_fragment(name, options)
- buffer.concat(cache)
- else
- pos = buffer.length
- block.call
- write_fragment(name, buffer[pos..-1], options)
- end
- end
- def write_fragment(name, content, options = nil)
- return unless perform_caching
- key = fragment_cache_key(name)
- self.class.benchmark "Cached fragment: #{key}" do
- fragment_cache_store.write(key, content, options)
- end
- content
- end
- def read_fragment(name, options = nil)
- return unless perform_caching
- key = fragment_cache_key(name)
- self.class.benchmark "Fragment read: #{key}" do
- fragment_cache_store.read(key, options)
- end
- end
- # Name can take one of three forms:
- # * String: This would normally take the form of a path like "pages/45/notes"
- # * Hash: Is treated as an implicit call to url_for, like { :controller => "pages", :action => "notes", :id => 45 }
- # * Regexp: Will destroy all the matched fragments, example: %r{pages/\d*/notes} Ensure you do not specify start and finish in the regex (^$) because the actual filename matched looks like ./cache/filename/path.cache
- def expire_fragment(name, options = nil)
- return unless perform_caching
- key = fragment_cache_key(name)
- if key.is_a?(Regexp)
- self.class.benchmark "Expired fragments matching: #{key.source}" do
- fragment_cache_store.delete_matched(key, options)
- end
- else
- self.class.benchmark "Expired fragment: #{key}" do
- fragment_cache_store.delete(key, options)
- end
- end
- end
- # Deprecated -- just call expire_fragment with a regular expression
- def expire_matched_fragments(matcher = /.*/, options = nil) #:nodoc:
- expire_fragment(matcher, options)
- end
- class UnthreadedMemoryStore #:nodoc:
- def initialize #:nodoc:
- @data = {}
- end
- def read(name, options=nil) #:nodoc:
- @data[name]
- end
- def write(name, value, options=nil) #:nodoc:
- @data[name] = value
- end
- def delete(name, options=nil) #:nodoc:
- @data.delete(name)
- end
- def delete_matched(matcher, options=nil) #:nodoc:
- @data.delete_if { |k,v| k =~ matcher }
- end
- end
- module ThreadSafety #:nodoc:
- def read(name, options=nil) #:nodoc:
- @mutex.synchronize { super }
- end
- def write(name, value, options=nil) #:nodoc:
- @mutex.synchronize { super }
- end
- def delete(name, options=nil) #:nodoc:
- @mutex.synchronize { super }
- end
- def delete_matched(matcher, options=nil) #:nodoc:
- @mutex.synchronize { super }
- end
- end
- class MemoryStore < UnthreadedMemoryStore #:nodoc:
- def initialize #:nodoc:
- super
- if ActionController::Base.allow_concurrency
- @mutex = Mutex.new
- MemoryStore.send(:include, ThreadSafety)
- end
- end
- end
- class DRbStore < MemoryStore #:nodoc:
- attr_reader :address
- def initialize(address = 'druby://localhost:9192')
- super()
- @address = address
- @data = DRbObject.new(nil, address)
- end
- end
- class MemCacheStore < MemoryStore #:nodoc:
- attr_reader :addresses
- def initialize(*addresses)
- super()
- addresses = addresses.flatten
- addresses = ["localhost"] if addresses.empty?
- @addresses = addresses
- @data = MemCache.new(*addresses)
- end
- end
- class UnthreadedFileStore #:nodoc:
- attr_reader :cache_path
- def initialize(cache_path)
- @cache_path = cache_path
- end
- def write(name, value, options = nil) #:nodoc:
- ensure_cache_path(File.dirname(real_file_path(name)))
- File.open(real_file_path(name), "wb+") { |f| f.write(value) }
- rescue => e
- Base.logger.error "Couldn't create cache directory: #{name} (#{e.message})" if Base.logger
- end
- def read(name, options = nil) #:nodoc:
- File.open(real_file_path(name), 'rb') { |f| f.read } rescue nil
- end
- def delete(name, options) #:nodoc:
- File.delete(real_file_path(name))
- rescue SystemCallError => e
- # If there's no cache, then there's nothing to complain about
- end
- def delete_matched(matcher, options) #:nodoc:
- search_dir(@cache_path) do |f|
- if f =~ matcher
- begin
- File.delete(f)
- rescue Object => e
- # If there's no cache, then there's nothing to complain about
- end
- end
- end
- end
- private
- def real_file_path(name)
- '%s/%s.cache' % [@cache_path, name.gsub('?', '.').gsub(':', '.')]
- end
- def ensure_cache_path(path)
- FileUtils.makedirs(path) unless File.exists?(path)
- end
- def search_dir(dir, &callback)
- Dir.foreach(dir) do |d|
- next if d == "." || d == ".."
- name = File.join(dir, d)
- if File.directory?(name)
- search_dir(name, &callback)
- else
- callback.call name
- end
- end
- end
- end
- class FileStore < UnthreadedFileStore #:nodoc:
- def initialize(cache_path)
- super(cache_path)
- if ActionController::Base.allow_concurrency
- @mutex = Mutex.new
- FileStore.send(:include, ThreadSafety)
- end
- end
- end
- end
- # Sweepers are the terminators of the caching world and responsible for expiring caches when model objects change.
- # They do this by being half-observers, half-filters and implementing callbacks for both roles. A Sweeper example:
- #
- # class ListSweeper < ActionController::Caching::Sweeper
- # observe List, Item
- #
- # def after_save(record)
- # list = record.is_a?(List) ? record : record.list
- # expire_page(:controller => "lists", :action => %w( show public feed ), :id => list.id)
- # expire_action(:controller => "lists", :action => "all")
- # list.shares.each { |share| expire_page(:controller => "lists", :action => "show", :id => share.url_key) }
- # end
- # end
- #
- # The sweeper is assigned in the controllers that wish to have its job performed using the <tt>cache_sweeper</tt> class method:
- #
- # class ListsController < ApplicationController
- # caches_action :index, :show, :public, :feed
- # cache_sweeper :list_sweeper, :only => [ :edit, :destroy, :share ]
- # end
- #
- # In the example above, four actions are cached and three actions are responsible for expiring those caches.
- module Sweeping
- def self.append_features(base) #:nodoc:
- super
- base.extend(ClassMethods)
- end
- module ClassMethods #:nodoc:
- def cache_sweeper(*sweepers)
- return unless perform_caching
- configuration = sweepers.last.is_a?(Hash) ? sweepers.pop : {}
- sweepers.each do |sweeper|
- observer(sweeper)
- sweeper_instance = Object.const_get(Inflector.classify(sweeper)).instance
- if sweeper_instance.is_a?(Sweeper)
- around_filter(sweeper_instance, :only => configuration[:only])
- else
- after_filter(sweeper_instance, :only => configuration[:only])
- end
- end
- end
- end
- end
- if defined?(ActiveRecord) and defined?(ActiveRecord::Observer)
- class Sweeper < ActiveRecord::Observer #:nodoc:
- attr_accessor :controller
- # ActiveRecord::Observer will mark this class as reloadable even though it should not be.
- # However, subclasses of ActionController::Caching::Sweeper should be Reloadable
- include Reloadable::Subclasses
-
- def before(controller)
- self.controller = controller
- callback(:before)
- end
- def after(controller)
- callback(:after)
- # Clean up, so that the controller can be collected after this request
- self.controller = nil
- end
- private
- def callback(timing)
- controller_callback_method_name = "#{timing}_#{controller.controller_name.underscore}"
- action_callback_method_name = "#{controller_callback_method_name}_#{controller.action_name}"
- send(controller_callback_method_name) if respond_to?(controller_callback_method_name)
- send(action_callback_method_name) if respond_to?(action_callback_method_name)
- end
- def method_missing(method, *arguments)
- return if @controller.nil?
- @controller.send(method, *arguments)
- end
- end
- end
- end
- end
- 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 instantiated 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, env_table)
- 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
- require 'cgi'
- require 'action_controller/vendor/xml_simple'
- require 'action_controller/vendor/xml_node'
- # 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|
- # Ignore repeated delimiters.
- next if p.empty?
- k, v = p.split('=',2)
- v = nil if (v && v.empty?)
- k = CGI.unescape(k) if k
- v = CGI.unescape(v) if v
- unless k.include?(?[)
- parsed_params[k] = v
- else
- keys = split_key(k)
- last_key = keys.pop
- last_key = keys.pop if (use_array = last_key.empty?)
- parent = keys.inject(parsed_params) {|h, k| h[k] ||= {}}
-
- if use_array then (parent[last_key] ||= []) << v
- else parent[last_key] = v
- end
- end
- }
-
- 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 =~ /.*\[\]$/
- unless key.include?('[')
- # much faster to test for the most common case first (GET)
- # and avoid the call to build_deep_hash
- parsed_params[key] = get_typed_value(value[0])
- else
- build_deep_hash(get_typed_value(value[0]), parsed_params, get_levels(key))
- end
- end
-
- parsed_params
- end
- def self.parse_formatted_request_parameters(mime_type, raw_post_data)
- params = case strategy = ActionController::Base.param_parsers[mime_type]
- when Proc
- strategy.call(raw_post_data)
- when :xml_simple
- raw_post_data.blank? ? nil :
- typecast_xml_value(XmlSimple.xml_in(raw_post_data,
- 'forcearray' => false,
- 'forcecontent' => true,
- 'keeproot' => true,
- 'contentkey' => '__content__'))
- when :yaml
- YAML.load(raw_post_data)
- when :xml_node
- node = XmlNode.from_xml(raw_post_data)
- { node.node_name => node }
- end
-
- dasherize_keys(params || {})
- rescue Object => e
- { "exception" => "#{e.message} (#{e.class})", "backtrace" => e.backtrace,
- "raw_post_data" => raw_post_data, "format" => mime_type }
- end
- def self.typecast_xml_value(value)
- case value
- when Hash
- if value.has_key?("__content__")
- content = translate_xml_entities(value["__content__"])
- case value["type"]
- when "integer" then content.to_i
- when "boolean" then content == "true"
- when "datetime" then Time.parse(content)
- when "date" then Date.parse(content)
- else content
- end
- else
- value.empty? ? nil : value.inject({}) do |h,(k,v)|
- h[k] = typecast_xml_value(v)
- h
- end
- end
- when Array
- value.map! { |i| typecast_xml_value(i) }
- case value.length
- when 0 then nil
- when 1 then value.first
- else value
- end
- else
- raise "can't typecast #{value.inspect}"
- end
- end
- private
- def self.translate_xml_entities(value)
- value.gsub(/</, "<").
- gsub(/>/, ">").
- gsub(/"/, '"').
- gsub(/'/, "'").
- gsub(/&/, "&")
- end
- def self.dasherize_keys(params)
- case params.class.to_s
- when "Hash"
- params.inject({}) do |h,(k,v)|
- h[k.to_s.tr("-", "_")] = dasherize_keys(v)
- h
- end
- when "Array"
- params.map { |v| dasherize_keys(v) }
- else
- params
- end
- end
- # Splits the given key into several pieces. Example keys are 'name', 'person[name]',
- # 'person[name][first]', and 'people[]'. In each instance, an Array instance is returned.
- # 'person[name][first]' produces ['person', 'name', 'first']; 'people[]' produces ['people', '']
- def CGIMethods.split_key(key)
- if /^([^\[]+)((?:\[[^\]]*\])+)$/ =~ key
- keys = [$1]
-
- keys.concat($2[1..-2].split(']['))
- keys << '' if key[-2..-1] == '[]' # Have to add it since split will drop empty strings
-
- keys
- else
- [key]
- end
- end
-
- def CGIMethods.get_typed_value(value)
- # test most frequent case first
- if value.is_a?(String)
- value
- elsif value.respond_to?(:content_type) && ! value.content_type.blank?
- # Uploaded file
- unless value.respond_to?(:full_original_filename)
- class << value
- alias_method :full_original_filename, :original_filename
- # Take the basename of the upload's original filename.
- # This handles the full Windows paths given by Internet Explorer
- # (and perhaps other broken user agents) without affecting
- # those which give the lone filename.
- # The Windows regexp is adapted from Perl's File::Basename.
- def original_filename
- if md = /^(?:.*[:\\\/])?(.*)/m.match(full_original_filename)
- md.captures.first
- else
- File.basename full_original_filename
- end
- end
- end
- end
- # Return the same value after overriding original_filename.
- value
- elsif value.respond_to?(:read)
- # Value as part of a multipart request
- value.read
- elsif value.class == Array
- value.collect { |v| CGIMethods.get_typed_value(v) }
- else
- # other value (neither string nor a multipart request)
- value.to_s
- end
- end
-
- PARAMS_HASH_RE = /^([^\[]+)(\[.*\])?(.)?.*$/
- def CGIMethods.get_levels(key)
- all, main, bracketed, trailing = PARAMS_HASH_RE.match(key).to_a
- if main.nil?
- []
- elsif trailing
- [key]
- elsif bracketed
- [main] + bracketed.slice(1...-1).split('][')
- else
- [main]
- end
- 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
- 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)
- if name.kind_of?(String)
- @name = name
- @value = Array(value)
- @domain = nil
- @expires = nil
- @secure = false
- @path = nil
- else
- @name = name['name']
- @value = Array(name['value'])
- @domain = name['domain']
- @expires = name['expires']
- @secure = name['secure'] || false
- @path = name['path']
- end
-
- unless @name
- raise ArgumentError, "`name' required"
- end
- # simple support for IE
- unless @path
- %r|^(.*/)|.match(ENV['SCRIPT_NAME'])
- @path = ($1 or '')
- end
- 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([])
- if raw_cookie
- raw_cookie.split(/; ?/).each do |pairs|
- name, values = pairs.split('=',2)
- next unless name and values
- name = CGI::unescape(name)
- values = values.split('&').collect!{|v| CGI::unescape(v) }
- unless cookies.has_key?(name)
- cookies[name] = new(name, *values)
- end
- end
- end
- cookies
- end
- end # class Cookie
- end
- class CGI #:nodoc:
- # Add @request.env['RAW_POST_DATA'] for the vegans.
- module QueryExtension
- # Initialize the data from the query.
- #
- # Handles multipart forms (in particular, forms that involve file uploads).
- # Reads query parameters in the @params field, and cookies into @cookies.
- def initialize_query()
- @cookies = CGI::Cookie::parse(env_table['HTTP_COOKIE'] || env_table['COOKIE'])
- #fix some strange request environments
- if method = env_table['REQUEST_METHOD']
- method = method.to_s.downcase.intern
- else
- method = :get
- end
- if method == :post && (boundary = multipart_form_boundary)
- @multipart = true
- @params = read_multipart(boundary, Integer(env_table['CONTENT_LENGTH']))
- else
- @multipart = false
- @params = CGI::parse(read_query_params(method) || "")
- end
- end
- private
- unless defined?(MULTIPART_FORM_BOUNDARY_RE)
- MULTIPART_FORM_BOUNDARY_RE = %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)\"?|n #"
- end
- def multipart_form_boundary
- MULTIPART_FORM_BOUNDARY_RE.match(env_table['CONTENT_TYPE']).to_a.pop
- end
- if defined? MOD_RUBY
- def read_params_from_query
- Apache::request.args || ''
- end
- else
- def read_params_from_query
- # fixes CGI querystring parsing for lighttpd
- env_qs = env_table['QUERY_STRING']
- if env_qs.blank? && !(uri = env_table['REQUEST_URI']).blank?
- uri.split('?', 2)[1] || ''
- else
- env_qs
- end
- end
- end
- def read_params_from_post
- stdinput.binmode if stdinput.respond_to?(:binmode)
- content = stdinput.read(Integer(env_table['CONTENT_LENGTH'])) || ''
- # fix for Safari Ajax postings that always append \000
- content.chop! if content[-1] == 0
- content.gsub! /&_=$/, ''
- env_table['RAW_POST_DATA'] = content.freeze
- end
- def read_query_params(method)
- case method
- when :get
- read_params_from_query
- when :post, :put
- read_params_from_post
- when :cmd
- read_from_cmdline
- else # when :head, :delete, :options
- read_params_from_query
- end
- end
- end # module QueryExtension
- end
- require 'action_controller/cgi_ext/cgi_ext'
- require 'action_controller/cgi_ext/cookie_performance_fix'
- require 'action_controller/cgi_ext/raw_post_data_fix'
- 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, :session_options
- DEFAULT_SESSION_OPTIONS = {
- :database_manager => CGI::Session::PStore,
- :prefix => "ruby_sess.",
- :session_path => "/"
- } unless const_defined?(:DEFAULT_SESSION_OPTIONS)
- def initialize(cgi, session_options = {})
- @cgi = cgi
- @session_options = session_options
- @env = @cgi.send(:env_table)
- super()
- end
- def query_string
- if (qs = @cgi.query_string) && !qs.empty?
- qs
- elsif uri = @env['REQUEST_URI']
- parts = uri.split('?')
- parts.shift
- parts.join('?')
- else
- @env['QUERY_STRING'] || ''
- end
- end
- def query_parameters
- (qs = self.query_string).empty? ? {} : CGIMethods.parse_query_parameters(qs)
- end
- def request_parameters
- @request_parameters ||=
- if ActionController::Base.param_parsers.has_key?(content_type)
- CGIMethods.parse_formatted_request_parameters(content_type, @env['RAW_POST_DATA'])
- else
- CGIMethods.parse_request_parameters(@cgi.params)
- end
- end
-
- def cookies
- @cgi.cookies.freeze
- end
- def host_with_port
- if forwarded = env["HTTP_X_FORWARDED_HOST"]
- forwarded.split(/,\s?/).last
- elsif http_host = env['HTTP_HOST']
- http_host
- elsif server_name = env['SERVER_NAME']
- server_name
- else
- "#{env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
- end
- end
- def host
- host_with_port[/^[^:]+/]
- end
- def port
- if host_with_port =~ /:(\d+)$/
- $1.to_i
- else
- standard_port
- end
- end
- def session
- unless @session
- if @session_options == false
- @session = Hash.new
- else
- stale_session_check! do
- if session_options_with_string_keys['new_session'] == true
- @session = new_session
- else
- @session = CGI::Session.new(@cgi, session_options_with_string_keys)
- end
- @session['__valid_session']
- end
- end
- end
- @session
- end
- def reset_session
- @session.delete if CGI::Session === @session
- @session = new_session
- end
- def method_missing(method_id, *arguments)
- @cgi.send(method_id, *arguments) rescue super
- end
- private
- # Delete an old session if it exists then create a new one.
- def new_session
- if @session_options == false
- Hash.new
- else
- CGI::Session.new(@cgi, session_options_with_string_keys.merge("new_session" => false)).delete rescue nil
- CGI::Session.new(@cgi, session_options_with_string_keys.merge("new_session" => true))
- end
- end
- def stale_session_check!
- yield
- rescue ArgumentError => argument_error
- if argument_error.message =~ %r{undefined class/module (\w+)}
- begin
- Module.const_missing($1)
- rescue LoadError, NameError => const_error
- raise ActionController::SessionRestoreError, <<end_msg
- Session contains objects whose class definition isn\'t available.
- Remember to require the classes for all objects kept in the session.
- (Original exception: #{const_error.message} [#{const_error.class}])
- end_msg
- end
- retry
- else
- raise
- end
- end
- def session_options_with_string_keys
- @session_options_with_string_keys ||= DEFAULT_SESSION_OPTIONS.merge(@session_options).inject({}) { |options, (k,v)| options[k.to_s] = v; options }
- end
- end
- class CgiResponse < AbstractResponse #:nodoc:
- def initialize(cgi)
- @cgi = cgi
- super()
- end
- def out(output = $stdout)
- convert_content_type!(@headers)
- output.binmode if output.respond_to?(:binmode)
- output.sync = false if output.respond_to?(:sync=)
-
- begin
- output.write(@cgi.header(@headers))
- if @cgi.send(:env_table)['REQUEST_METHOD'] == 'HEAD'
- return
- elsif @body.respond_to?(:call)
- @body.call(self, output)
- else
- output.write(@body)
- end
- output.flush if output.respond_to?(:flush)
- rescue Errno::EPIPE => e
- # lost connection to the FCGI process -- ignore the output, then
- end
- end
- private
- def convert_content_type!(headers)
- if header = headers.delete("Content-Type")
- headers["type"] = header
- end
- if header = headers.delete("Content-type")
- headers["type"] = header
- end
- if header = headers.delete("content-type")
- headers["type"] = header
- end
- end
- end
- end
- module ActionController
- module CodeGeneration #:nodoc:
- class GenerationError < StandardError #:nodoc:
- end
-
- class Source #:nodoc:
- attr_reader :lines, :indentation_level
- IndentationString = ' '
- def initialize
- @lines, @indentation_level = [], 0
- end
- def line(line)
- @lines << (IndentationString * @indentation_level + line)
- end
- alias :<< :line
-
- def indent
- @indentation_level += 1
- yield
- ensure
- @indentation_level -= 1
- end
-
- def to_s() lines.join("\n") end
- end
- class CodeGenerator #:nodoc:
- attr_accessor :source, :locals
- def initialize(source = nil)
- @locals = []
- @source = source || Source.new
- end
-
- BeginKeywords = %w(if unless begin until while def).collect {|kw| kw.to_sym}
- ResumeKeywords = %w(elsif else rescue).collect {|kw| kw.to_sym}
- Keywords = BeginKeywords + ResumeKeywords
-
- def method_missing(keyword, *text)
- if Keywords.include? keyword
- if ResumeKeywords.include? keyword
- raise GenerationError, "Can only resume with #{keyword} immediately after an end" unless source.lines.last =~ /^\s*end\s*$/
- source.lines.pop # Remove the 'end'
- end
-
- line "#{keyword} #{text.join ' '}"
- begin source.indent { yield(self.dup) }
- ensure line 'end'
- end
- else
- super(keyword, *text)
- end
- end
-
- def line(*args) self.source.line(*args) end
- alias :<< :line
- def indent(*args, &block) source(*args, &block) end
- def to_s() source.to_s end
-
- def share_locals_with(other)
- other.locals = self.locals = (other.locals | locals)
- end
-
- FieldsToDuplicate = [:locals]
- def dup
- copy = self.class.new(source)
- self.class::FieldsToDuplicate.each do |sym|
- value = self.send(sym)
- value = value.dup unless value.nil? || value.is_a?(Numeric)
- copy.send("#{sym}=", value)
- end
- return copy
- end
- end
- class RecognitionGenerator < CodeGenerator #:nodoc:
- Attributes = [:after, :before, :current, :results, :constants, :depth, :move_ahead, :finish_statement]
- attr_accessor(*Attributes)
- FieldsToDuplicate = CodeGenerator::FieldsToDuplicate + Attributes
-
- def initialize(*args)
- super(*args)
- @after, @before = [], []
- @current = nil
- @results, @constants = {}, {}
- @depth = 0
- @move_ahead = nil
- @finish_statement = Proc.new {|hash_expr| hash_expr}
- end
-
- def if_next_matches(string, &block)
- test = Routing.test_condition(next_segment(true), string)
- self.if(test, &block)
- end
-
- def move_forward(places = 1)
- dup = self.dup
- dup.depth += 1
- dup.move_ahead = places
- yield dup
- end
-
- def next_segment(assign_inline = false, default = nil)
- if locals.include?(segment_name)
- code = segment_name
- else
- code = "#{segment_name} = #{path_name}[#{index_name}]"
- if assign_inline
- code = "(#{code})"
- else
- line(code)
- code = segment_name
- end
-
- locals << segment_name
- end
- code = "(#{code} || #{default.inspect})" if default
-
- return code.to_s
- end
-
- def segment_name() "segment#{depth}".to_sym end
- def path_name() :path end
- def index_name
- move_ahead, @move_ahead = @move_ahead, nil
- move_ahead ? "index += #{move_ahead}" : 'index'
- end
-
- def continue
- dup = self.dup
- dup.before << dup.current
- dup.current = dup.after.shift
- dup.go
- end
-
- def go
- if current then current.write_recognition(self)
- else self.finish
- end
- end
-
- def result(key, expression, delay = false)
- unless delay
- line "#{key}_value = #{expression}"
- expression = "#{key}_value"
- end
- results[key] = expression
- end
- def constant_result(key, object)
- constants[key] = object
- end
-
- def finish(ensure_traversal_finished = true)
- pairs = []
- (results.keys + constants.keys).uniq.each do |key|
- pairs << "#{key.to_s.inspect} => #{results[key] ? results[key] : constants[key].inspect}"
- end
- hash_expr = "{#{pairs.join(', ')}}"
-
- statement = finish_statement.call(hash_expr)
- if ensure_traversal_finished then self.if("! #{next_segment(true)}") {|gp| gp << statement}
- else self << statement
- end
- end
- end
-
- class GenerationGenerator < CodeGenerator #:nodoc:
- Attributes = [:after, :before, :current, :segments]
- attr_accessor(*Attributes)
- FieldsToDuplicate = CodeGenerator::FieldsToDuplicate + Attributes
-
- def initialize(*args)
- super(*args)
- @after, @before = [], []
- @current = nil
- @segments = []
- end
-
- def hash_name() 'hash' end
- def local_name(key) "#{key}_value" end
-
- def hash_value(key, assign = true, default = nil)
- if locals.include?(local_name(key)) then code = local_name(key)
- else
- code = "hash[#{key.to_sym.inspect}]"
- if assign
- code = "(#{local_name(key)} = #{code})"
- locals << local_name(key)
- end
- end
- code = "(#{code} || (#{default.inspect}))" if default
- return code
- end
-
- def expire_for_keys(*keys)
- return if keys.empty?
- conds = keys.collect {|key| "expire_on[#{key.to_sym.inspect}]"}
- line "not_expired, #{hash_name} = false, options if not_expired && #{conds.join(' && ')}"
- end
-
- def add_segment(*segments)
- d = dup
- d.segments.concat segments
- yield d
- end
-
- def go
- if current then current.write_generation(self)
- else self.finish
- end
- end
-
- def continue
- d = dup
- d.before << d.current
- d.current = d.after.shift
- d.go
- end
-
- def finish
- line %("/#{segments.join('/')}")
- end
- def check_conditions(conditions)
- tests = []
- generator = nil
- conditions.each do |key, condition|
- tests << (generator || self).hash_value(key, true) if condition.is_a? Regexp
- tests << Routing.test_condition((generator || self).hash_value(key, false), condition)
- generator = self.dup unless generator
- end
- return tests.join(' && ')
- end
- end
- end
- end
- module ActionController #:nodoc:
- # Components allow you to call other actions for their rendered response while executing another action. You can either delegate
- # the entire response rendering or you can mix a partial response in with your other content.
- #
- # class WeblogController < ActionController::Base
- # # Performs a method and then lets hello_world output its render
- # def delegate_action
- # do_other_stuff_before_hello_world
- # render_component :controller => "greeter", :action => "hello_world", :params => { :person => "david" }
- # end
- # end
- #
- # class GreeterController < ActionController::Base
- # def hello_world
- # render :text => "#{params[:person]} says, Hello World!"
- # end
- # end
- #
- # The same can be done in a view to do a partial rendering:
- #
- # Let's see a greeting:
- # <%= render_component :controller => "greeter", :action => "hello_world" %>
- #
- # It is also possible to specify the controller as a class constant, bypassing the inflector
- # code to compute the controller class at runtime:
- #
- # <%= render_component :controller => GreeterController, :action => "hello_world" %>
- #
- # == When to use components
- #
- # Components should be used with care. They're significantly slower than simply splitting reusable parts into partials and
- # conceptually more complicated. Don't use components as a way of separating concerns inside a single application. Instead,
- # reserve components to those rare cases where you truly have reusable view and controller elements that can be employed
- # across many applications at once.
- #
- # So to repeat: Components are a special-purpose approach that can often be replaced with better use of partials and filters.
- module Components
- def self.included(base) #:nodoc:
- base.send :include, InstanceMethods
- base.extend(ClassMethods)
- base.helper do
- def render_component(options)
- @controller.send(:render_component_as_string, options)
- end
- end
-
- # If this controller was instantiated to process a component request,
- # +parent_controller+ points to the instantiator of this controller.
- base.send :attr_accessor, :parent_controller
-
- base.class_eval do
- alias_method :process_cleanup_without_components, :process_cleanup
- alias_method :process_cleanup, :process_cleanup_with_components
-
- alias_method :set_session_options_without_components, :set_session_options
- alias_method :set_session_options, :set_session_options_with_components
-
- alias_method :flash_without_components, :flash
- alias_method :flash, :flash_with_components
- alias_method :component_request?, :parent_controller
- end
- end
- module ClassMethods
- # Track parent controller to identify component requests
- def process_with_components(request, response, parent_controller = nil) #:nodoc:
- controller = new
- controller.parent_controller = parent_controller
- controller.process(request, response)
- end
- # Set the template root to be one directory behind the root dir of the controller. Examples:
- # /code/weblog/components/admin/users_controller.rb with Admin::UsersController
- # will use /code/weblog/components as template root
- # and find templates in /code/weblog/components/admin/users/
- #
- # /code/weblog/components/admin/parties/users_controller.rb with Admin::Parties::UsersController
- # will also use /code/weblog/components as template root
- # and find templates in /code/weblog/components/admin/parties/users/
- def uses_component_template_root
- path_of_calling_controller = File.dirname(caller[0].split(/:\d+:/).first)
- path_of_controller_root = path_of_calling_controller.sub(/#{controller_path.split("/")[0..-2]}$/, "") # " (for ruby-mode)
- self.template_root = path_of_controller_root
- end
- end
- module InstanceMethods
- # Extracts the action_name from the request parameters and performs that action.
- def process_with_components(request, response, method = :perform_action, *arguments) #:nodoc:
- flash.discard if component_request?
- process_without_components(request, response, method, *arguments)
- end
-
- protected
- # Renders the component specified as the response for the current method
- def render_component(options) #:doc:
- component_logging(options) do
- render_text(component_response(options, true).body, response.headers["Status"])
- end
- end
- # Returns the component response as a string
- def render_component_as_string(options) #:doc:
- component_logging(options) do
- response = component_response(options, false)
- if redirected = response.redirected_to
- render_component_as_string(redirected)
- else
- response.body
- end
- end
- end
- def flash_with_components(refresh = false) #:nodoc:
- if @flash.nil? || refresh
- @flash =
- if @parent_controller
- @parent_controller.flash
- else
- flash_without_components
- end
- end
-
- @flash
- end
- private
- def component_response(options, reuse_response)
- klass = component_class(options)
- request = request_for_component(klass.controller_name, options)
- response = reuse_response ? @response : @response.dup
- klass.process_with_components(request, response, self)
- end
-
- # determine the controller class for the component request
- def component_class(options)
- if controller = options[:controller]
- controller.is_a?(Class) ? controller : "#{controller.camelize}Controller".constantize
- else
- self.class
- end
- end
-
- # Create a new request object based on the current request.
- # The new request inherits the session from the current request,
- # bypassing any session options set for the component controller's class
- def request_for_component(controller_name, options)
- request = @request.dup
- request.session = @request.session
-
- request.instance_variable_set(
- :@parameters,
- (options[:params] || {}).with_indifferent_access.update(
- "controller" => controller_name, "action" => options[:action], "id" => options[:id]
- )
- )
-
- request
- end
- def component_logging(options)
- if logger
- logger.info "Start rendering component (#{options.inspect}): "
- result = yield
- logger.info "\n\nEnd of component rendering"
- result
- else
- yield
- end
- end
- def set_session_options_with_components(request)
- set_session_options_without_components(request) unless component_request?
- end
- def process_cleanup_with_components
- process_cleanup_without_components unless component_request?
- end
- end
- end
- end
- module ActionController #:nodoc:
- # Cookies are read and written through ActionController#cookies. The cookies being read are what were received along with the request,
- # the cookies being written are what will be sent out with the response. Cookies are read by value (so you won't get the cookie object
- # itself back -- just the value it holds). Examples for writing:
- #
- # cookies[:user_name] = "david" # => Will set a simple session cookie
- # cookies[:login] = { :value => "XJ-122", :expires => Time.now + 360} # => Will set a cookie that expires in 1 hour
- #
- # Examples for reading:
- #
- # cookies[:user_name] # => "david"
- # cookies.size # => 2
- #
- # Example for deleting:
- #
- # cookies.delete :user_name
- #
- # All the option symbols for setting cookies are:
- #
- # * <tt>value</tt> - the cookie's value or list of values (as an array).
- # * <tt>path</tt> - the path for which this cookie applies. Defaults to the root of the application.
- # * <tt>domain</tt> - the domain for which this cookie applies.
- # * <tt>expires</tt> - the time at which this cookie expires, as a +Time+ object.
- # * <tt>secure</tt> - whether this cookie is a secure cookie or not (default to false).
- # Secure cookies are only transmitted to HTTPS servers.
- module Cookies
- protected
- # Returns the cookie container, which operates as described above.
- def cookies
- CookieJar.new(self)
- end
- # Deprecated cookie writer method
- def cookie(*options)
- @response.headers["cookie"] << CGI::Cookie.new(*options)
- end
- end
-
- class CookieJar < Hash #:nodoc:
- def initialize(controller)
- @controller, @cookies = controller, controller.instance_variable_get("@cookies")
- super()
- update(@cookies)
- end
- # Returns the value of the cookie by +name+ -- or nil if no such cookie exists. You set new cookies using either the cookie method
- # or cookies[]= (for simple name/value cookies without options).
- def [](name)
- @cookies[name.to_s].value.first if @cookies[name.to_s] && @cookies[name.to_s].respond_to?(:value)
- end
-
- def []=(name, options)
- if options.is_a?(Hash)
- options = options.inject({}) { |options, pair| options[pair.first.to_s] = pair.last; options }
- options["name"] = name.to_s
- else
- options = { "name" => name.to_s, "value" => options }
- end
-
- set_cookie(options)
- end
-
- # Removes the cookie on the client machine by setting the value to an empty string
- # and setting its expiration date into the past
- def delete(name)
- set_cookie("name" => name.to_s, "value" => "", "expires" => Time.at(0))
- end
- private
- def set_cookie(options) #:doc:
- options["path"] = "/" unless options["path"]
- cookie = CGI::Cookie.new(options)
- @controller.logger.info "Cookie set: #{cookie}" unless @controller.logger.nil?
- @controller.response.headers["cookie"] << cookie
- end
- end
- end
- module ActionController #:nodoc:
- module Dependencies #:nodoc:
- def self.append_features(base)
- super
- base.extend(ClassMethods)
- end
- # Dependencies control what classes are needed for the controller to run its course. This is an alternative to doing explicit
- # +require+ statements that bring a number of benefits. It's more succinct, communicates what type of dependency we're talking about,
- # can trigger special behavior (as in the case of +observer+), and enables Rails to be clever about reloading in cached environments
- # like FCGI. Example:
- #
- # class ApplicationController < ActionController::Base
- # model :account, :company, :person, :project, :category
- # helper :access_control
- # service :notifications, :billings
- # observer :project_change_observer
- # end
- #
- # Please note that a controller like ApplicationController will automatically attempt to require_dependency on a model of its
- # singuralized name and a helper of its name. If nothing is found, no error is raised. This is especially useful for concrete
- # controllers like PostController:
- #
- # class PostController < ApplicationController
- # # model :post (already required)
- # # helper :post (already required)
- # end
- #
- # Also note, that if the models follow the pattern of just 1 class per file in the form of MyClass => my_class.rb, then these
- # classes don't have to be required as Active Support will auto-require them.
- module ClassMethods #:nodoc:
- # Specifies a variable number of models that this controller depends on. Models are normally Active Record classes or a similar
- # backend for modelling entity classes.
- def model(*models)
- require_dependencies(:model, models)
- depend_on(:model, models)
- end
- # Specifies a variable number of services that this controller depends on. Services are normally singletons or factories, like
- # Action Mailer service or a Payment Gateway service.
- def service(*services)
- require_dependencies(:service, services)
- depend_on(:service, services)
- end
-
- # Specifies a variable number of observers that are to govern when this controller is handling actions. The observers will
- # automatically have .instance called on them to make them active on assignment.
- def observer(*observers)
- require_dependencies(:observer, observers)
- depend_on(:observer, observers)
- instantiate_observers(observers)
- end
- # Returns an array of symbols that specify the dependencies on a given layer. For the example at the top, calling
- # <tt>ApplicationController.dependencies_on(:model)</tt> would return <tt>[:account, :company, :person, :project, :category]</tt>
- def dependencies_on(layer)
- read_inheritable_attribute("#{layer}_dependencies")
- end
-
- def depend_on(layer, dependencies) #:nodoc:
- 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_dependency(dependency.to_s)
- rescue LoadError => e
- raise LoadError.new("Missing #{layer} #{dependency}.rb").copy_blame!(e)
- rescue Object => exception
- exception.blame_file! "=> #{layer} #{dependency}.rb"
- raise
- end
- end
- end
- end
- end
- end
- require 'test/unit'
- require 'test/unit/assertions'
- require 'rexml/document'
- module Test #:nodoc:
- module Unit #:nodoc:
- module Assertions
- def assert_success(message=nil) #:nodoc:
- assert_response(:success, message)
- end
- def assert_redirect(message=nil) #:nodoc:
- assert_response(:redirect, message)
- end
- def assert_rendered_file(expected=nil, message=nil) #:nodoc:
- assert_template(expected, message)
- end
- # ensure that the session has an object with the specified name
- def assert_session_has(key=nil, message=nil) #:nodoc:
- 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) #:nodoc:
- 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) #:nodoc:
- msg = build_message(message, "<?> expected in session['?'] but was <?>", expected, key, @response.session[key])
- assert_block(msg) { expected == @response.session[key] }
- end
- # -- cookie assertions ---------------------------------------------------
- def assert_no_cookie(key = nil, message = nil) #:nodoc:
- actual = @response.cookies[key]
- msg = build_message(message, "<?> not expected in cookies['?']", actual, key)
- assert_block(msg) { actual.nil? or actual.empty? }
- end
-
- def assert_cookie_equal(expected = nil, key = nil, message = nil) #:nodoc:
- actual = @response.cookies[key]
- actual = actual.first if actual
- msg = build_message(message, "<?> expected in cookies['?'] but was <?>", expected, key, actual)
- assert_block(msg) { expected == actual }
- end
-
- # -- flash assertions ---------------------------------------------------
- # ensure that the flash has an object with the specified name
- def assert_flash_has(key=nil, message=nil) #:nodoc:
- 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) #:nodoc:
- 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) #:nodoc:
- 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) #:nodoc:
- msg = build_message(message, "the flash exists <?>", @response.flash)
- assert_block(msg) { !@response.has_flash? }
- end
-
- # ensure the flash is empty but existent
- def assert_flash_empty(message=nil) #:nodoc:
- 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) #:nodoc:
- 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) #:nodoc:
- msg = build_message(message, "<?> expected in flash['?'] but was <?>", expected, key, @response.flash[key])
- assert_block(msg) { expected == @response.flash[key] }
- end
-
- # ensure our redirection url is an exact match
- def assert_redirect_url(url=nil, message=nil) #:nodoc:
- assert_redirect(message)
- 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) #:nodoc:
- assert_redirect(message)
- 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) #:nodoc:
- 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) #:nodoc:
- 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_template_equal(expected = nil, key = nil, message = nil) #:nodoc:
- 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
- alias_method :assert_assigned_equal, :assert_template_equal
- # 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) #:nodoc:
- xml, matches = REXML::Document.new(@response.body), []
- xml.elements.each(expression) { |e| matches << e.text }
- if matches.empty? then
- msg = build_message(message, "<?> not found in document", expression)
- flunk(msg)
- return
- elsif matches.length < 2 then
- matches = matches.first
- end
- msg = build_message(message, "<?> found <?>, not <?>", expression, matches, expected)
- assert_block(msg) { matches == expected }
- end
- # Assert the template object with the given name is an Active Record descendant and is valid.
- def assert_valid_record(key = nil, message = nil) #:nodoc:
- 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) #:nodoc:
- 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) #:nodoc:
- record = find_record_in_template(key)
- record.send(: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) #:nodoc:
- record = find_record_in_template(key)
- record.send(: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)
- assert_template_has(key)
- record = @response.template_objects[key]
- assert_not_nil(record)
- assert_kind_of ActiveRecord::Base, record
- return record
- end
- end
- end
- endmodule ActionController
- class Base
- protected
- # Deprecated in favor of calling redirect_to directly with the path.
- def redirect_to_path(path) #:nodoc:
- redirect_to(path)
- end
- # Deprecated in favor of calling redirect_to directly with the url. If the resource has moved permanently, it's possible to pass
- # true as the second parameter and the browser will get "301 Moved Permanently" instead of "302 Found". This can also be done through
- # just setting the headers["Status"] to "301 Moved Permanently" before using the redirect_to.
- def redirect_to_url(url, permanently = false) #:nodoc:
- headers["Status"] = "301 Moved Permanently" if permanently
- redirect_to(url)
- end
- end
- end
- module ActionController
- class AbstractRequest
- # Determine whether the body of a HTTP call is URL-encoded (default)
- # or matches one of the registered param_parsers.
- #
- # For backward compatibility, the post format is extracted from the
- # X-Post-Data-Format HTTP header if present.
- def post_format
- case content_type.to_s
- when 'application/xml'
- :xml
- when 'application/x-yaml'
- :yaml
- else
- :url_encoded
- end
- end
- # Is this a POST request formatted as XML or YAML?
- def formatted_post?
- post? && (post_format == :yaml || post_format == :xml)
- end
- # Is this a POST request formatted as XML?
- def xml_post?
- post? && post_format == :xml
- end
- # Is this a POST request formatted as YAML?
- def yaml_post?
- post? && post_format == :yaml
- end
- end
- end
- module ActionController #:nodoc:
- module Filters #:nodoc:
- def self.included(base)
- base.extend(ClassMethods)
- base.send(: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 or performing a redirect or render.
- # 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 are 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| 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 have 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 useful 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
- #
- # == Filter chain skipping
- #
- # Some times its convenient to specify a filter chain in a superclass that'll hold true for the majority of the
- # subclasses, but not necessarily all of them. The subclasses that behave in exception can then specify which filters
- # they would like to be relieved of. Examples
- #
- # class ApplicationController < ActionController::Base
- # before_filter :authenticate
- # end
- #
- # class WeblogController < ApplicationController
- # # will run the :authenticate filter
- # end
- #
- # class SignupController < ApplicationController
- # # will not run the :authenticate filter
- # skip_before_filter :authenticate
- # end
- #
- # == Filter conditions
- #
- # Filters can be limited to run for only specific actions. This can be expressed either by listing the actions to
- # exclude or the actions to include when executing the filter. Available conditions are +:only+ or +:except+, both
- # of which accept an arbitrary number of method references. For example:
- #
- # class Journal < ActionController::Base
- # # only require authentication if the current action is edit or delete
- # before_filter :authorize, :only => [ :edit, :delete ]
- #
- # private
- # def authorize
- # # redirect to login unless authenticated
- # end
- # end
- #
- # When setting conditions on inline method (proc) filters the condition must come first and be placed in parentheses.
- #
- # class UserPreferences < ActionController::Base
- # before_filter(:except => :new) { # some proc ... }
- # # ...
- # 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)
- conditions = extract_conditions!(filters)
- filters << block if block_given?
- add_action_conditions(filters, conditions)
- 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)
- conditions = extract_conditions!(filters)
- filters << block if block_given?
- add_action_conditions(filters, conditions)
- 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)
- conditions = extract_conditions!(filters)
- filters << block if block_given?
- add_action_conditions(filters, conditions)
- 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)
- conditions = extract_conditions!(filters)
- filters << block if block_given?
- add_action_conditions(filters, conditions)
- 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)
- conditions = extract_conditions!(filters)
- for filter in filters.flatten
- ensure_filter_responds_to_before_and_after(filter)
- append_before_filter(conditions || {}) { |c| filter.before(c) }
- prepend_after_filter(conditions || {}) { |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
-
- # Removes the specified filters from the +before+ filter chain. Note that this only works for skipping method-reference
- # filters, not procs. This is especially useful for managing the chain in inheritance hierarchies where only one out
- # of many sub-controllers need a different hierarchy.
- #
- # You can control the actions to skip the filter for with the <tt>:only</tt> and <tt>:except</tt> options,
- # just like when you apply the filters.
- def skip_before_filter(*filters)
- if conditions = extract_conditions!(filters)
- remove_contradicting_conditions!(filters, conditions)
- conditions[:only], conditions[:except] = conditions[:except], conditions[:only]
- add_action_conditions(filters, conditions)
- else
- for filter in filters.flatten
- write_inheritable_attribute("before_filters", read_inheritable_attribute("before_filters") - [ filter ])
- end
- end
- end
- # Removes the specified filters from the +after+ filter chain. Note that this only works for skipping method-reference
- # filters, not procs. This is especially useful for managing the chain in inheritance hierarchies where only one out
- # of many sub-controllers need a different hierarchy.
- #
- # You can control the actions to skip the filter for with the <tt>:only</tt> and <tt>:except</tt> options,
- # just like when you apply the filters.
- def skip_after_filter(*filters)
- if conditions = extract_conditions!(filters)
- remove_contradicting_conditions!(filters, conditions)
- conditions[:only], conditions[:except] = conditions[:except], conditions[:only]
- add_action_conditions(filters, conditions)
- else
- for filter in filters.flatten
- write_inheritable_attribute("after_filters", read_inheritable_attribute("after_filters") - [ filter ])
- end
- end
- end
-
- # Returns all the before filters for this class and all its ancestors.
- def before_filters #:nodoc:
- @before_filters ||= read_inheritable_attribute("before_filters") || []
- end
-
- # Returns all the after filters for this class and all its ancestors.
- def after_filters #:nodoc:
- @after_filters ||= read_inheritable_attribute("after_filters") || []
- end
-
- # Returns a mapping between filters and the actions that may run them.
- def included_actions #:nodoc:
- @included_actions ||= read_inheritable_attribute("included_actions") || {}
- end
-
- # Returns a mapping between filters and actions that may not run them.
- def excluded_actions #:nodoc:
- @excluded_actions ||= read_inheritable_attribute("excluded_actions") || {}
- end
-
- private
- def append_filter_to_chain(condition, filters)
- write_inheritable_array("#{condition}_filters", filters)
- end
- def prepend_filter_to_chain(condition, filters)
- old_filters = read_inheritable_attribute("#{condition}_filters") || []
- write_inheritable_attribute("#{condition}_filters", filters + old_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
- def extract_conditions!(filters)
- return nil unless filters.last.is_a? Hash
- filters.pop
- end
- def add_action_conditions(filters, conditions)
- return unless conditions
- included, excluded = conditions[:only], conditions[:except]
- write_inheritable_hash('included_actions', condition_hash(filters, included)) && return if included
- write_inheritable_hash('excluded_actions', condition_hash(filters, excluded)) if excluded
- end
- def condition_hash(filters, *actions)
- filters.inject({}) {|hash, filter| hash.merge(filter => actions.flatten.map {|action| action.to_s})}
- end
-
- def remove_contradicting_conditions!(filters, conditions)
- return unless conditions[:only]
- filters.each do |filter|
- next unless included_actions_for_filter = (read_inheritable_attribute('included_actions') || {})[filter]
- [*conditions[:only]].each do |conditional_action|
- conditional_action = conditional_action.to_s
- included_actions_for_filter.delete(conditional_action) if included_actions_for_filter.include?(conditional_action)
- end
- end
- end
- end
- module InstanceMethods # :nodoc:
- def self.included(base)
- base.class_eval do
- alias_method :perform_action_without_filters, :perform_action
- alias_method :perform_action, :perform_action_with_filters
- alias_method :process_without_filters, :process
- alias_method :process, :process_with_filters
- alias_method :process_cleanup_without_filters, :process_cleanup
- alias_method :process_cleanup, :process_cleanup_with_filters
- end
- end
- def perform_action_with_filters
- before_action_result = before_action
- unless before_action_result == false || performed?
- perform_action_without_filters
- after_action
- end
- @before_filter_chain_aborted = (before_action_result == false)
- end
- def process_with_filters(request, response, method = :perform_action, *arguments) #:nodoc:
- @before_filter_chain_aborted = false
- process_without_filters(request, response, method, *arguments)
- 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|
- next if action_exempted?(filter)
- filter_result = case
- when filter.is_a?(Symbol)
- self.send(filter)
- when filter_block?(filter)
- filter.call(self)
- when filter_class?(filter)
- filter.filter(self)
- else
- raise(
- ActionControllerError,
- 'Filters need to be either a symbol, proc/method, or class implementing a static filter method'
- )
- end
- if filter_result == false
- logger.info "Filter chain halted as [#{filter}] returned false" if logger
- return false
- 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
- def action_exempted?(filter)
- case
- when ia = self.class.included_actions[filter]
- !ia.include?(action_name)
- when ea = self.class.excluded_actions[filter]
- ea.include?(action_name)
- end
- end
- def process_cleanup_with_filters
- if @before_filter_chain_aborted
- close_session
- else
- process_cleanup_without_filters
- end
- end
- end
- end
- end
- 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] = "Successfully 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] = "Successfully 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.
- #
- # See docs on the FlashHash class for more details about the flash.
- module Flash
- def self.included(base)
- base.send :include, InstanceMethods
- base.class_eval do
- alias_method :assign_shortcuts_without_flash, :assign_shortcuts
- alias_method :assign_shortcuts, :assign_shortcuts_with_flash
- alias_method :process_cleanup_without_flash, :process_cleanup
- alias_method :process_cleanup, :process_cleanup_with_flash
- end
- end
-
-
- class FlashNow #:nodoc:
- def initialize(flash)
- @flash = flash
- end
-
- def []=(k, v)
- @flash[k] = v
- @flash.discard(k)
- v
- end
-
- def [](k)
- @flash[k]
- end
- end
-
- class FlashHash < Hash
- def initialize #:nodoc:
- super
- @used = {}
- end
-
- def []=(k, v) #:nodoc:
- keep(k)
- super
- end
-
- def update(h) #:nodoc:
- h.keys.each{ |k| discard(k) }
- super
- end
-
- alias :merge! :update
-
- def replace(h) #:nodoc:
- @used = {}
- super
- end
-
- # Sets a flash that will not be available to the next action, only to the current.
- #
- # flash.now[:message] = "Hello current action"
- #
- # This method enables you to use the flash as a central messaging system in your app.
- # When you need to pass an object to the next action, you use the standard flash assign (<tt>[]=</tt>).
- # When you need to pass an object to the current action, you use <tt>now</tt>, and your object will
- # vanish when the current action is done.
- #
- # Entries set via <tt>now</tt> are accessed the same way as standard entries: <tt>flash['my-key']</tt>.
- def now
- FlashNow.new self
- end
-
- # Keeps either the entire current flash or a specific flash entry available for the next action:
- #
- # flash.keep # keeps the entire flash
- # flash.keep(:notice) # keeps only the "notice" entry, the rest of the flash is discarded
- def keep(k=nil)
- use(k, false)
- end
-
- # Marks the entire flash or a single flash entry to be discarded by the end of the current action
- #
- # flash.keep # keep entire flash available for the next action
- # flash.discard(:warning) # discard the "warning" entry (it'll still be available for the current action)
- def discard(k=nil)
- use(k)
- end
-
- # Mark for removal entries that were kept, and delete unkept ones.
- #
- # This method is called automatically by filters, so you generally don't need to care about it.
- def sweep #:nodoc:
- keys.each do |k|
- unless @used[k]
- use(k)
- else
- delete(k)
- @used.delete(k)
- end
- end
- (@used.keys - keys).each{|k| @used.delete k } # clean up after keys that could have been left over by calling reject! or shift on the flash
- end
-
- private
- # Used internally by the <tt>keep</tt> and <tt>discard</tt> methods
- # use() # marks the entire flash as used
- # use('msg') # marks the "msg" entry as used
- # use(nil, false) # marks the entire flash as unused (keeps it around for one more action)
- # use('msg', false) # marks the "msg" entry as unused (keeps it around for one more action)
- def use(k=nil, v=true)
- unless k.nil?
- @used[k] = v
- else
- keys.each{|key| use key, v }
- end
- end
- end
- module InstanceMethods #:nodoc:
- def assign_shortcuts_with_flash(request, response) #:nodoc:
- assign_shortcuts_without_flash(request, response)
- flash(:refresh)
- end
-
- def process_cleanup_with_flash
- flash.sweep if @session
- process_cleanup_without_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.
- # Note that if sessions are disabled only flash.now will work.
- def flash(refresh = false) #:doc:
- if @flash.nil? || refresh
- @flash =
- if @session.is_a?(Hash)
- # @session is a Hash, if sessions are disabled
- # we don't put the flash in the session in this case
- FlashHash.new
- else
- # otherwise, @session is a CGI::Session or a TestSession
- # so make sure it gets retrieved from/saved to session storage after request processing
- @session["flash"] ||= FlashHash.new
- end
- end
-
- @flash
- end
- # deprecated. use <tt>flash.keep</tt> instead
- def keep_flash #:doc:
- warn 'keep_flash is deprecated; use flash.keep instead.'
- flash.keep
- end
- end
- end
- endmodule ActionController #:nodoc:
- module Helpers #:nodoc:
- def self.append_features(base)
- super
- # Initialize the base module to aggregate its helpers.
- base.class_inheritable_accessor :master_helper_module
- base.master_helper_module = Module.new
- # Extend base with class methods to declare helpers.
- base.extend(ClassMethods)
- base.class_eval do
- # Wrap inherited to create a new master helper module for subclasses.
- class << self
- alias_method :inherited_without_helper, :inherited
- alias_method :inherited, :inherited_with_helper
- end
- end
- end
- # The template helpers serve 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) #:nodoc:
- master_helper_module.send(:include, helper_module)
- end
- # Declare a helper:
- # 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 = arg.to_s.underscore + '_helper'
- class_name = file_name.camelize
-
- begin
- require_dependency(file_name)
- rescue LoadError => load_error
- requiree = / -- (.*?)(\.rb)?$/.match(load_error).to_a[1]
- msg = (requiree == file_name) ? "Missing helper file helpers/#{file_name}.rb" : "Can't load file: #{requiree}"
- raise LoadError.new(msg).copy_blame!(load_error)
- end
- add_template_helper(class_name.constantize)
- else
- raise ArgumentError, 'helper expects String, Symbol, or Module argument'
- end
- end
- # Evaluate block in template class if given.
- master_helper_module.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)
- methods.flatten.each do |method|
- master_helper_module.module_eval <<-end_eval
- def #{method}(*args, &block)
- controller.send(%(#{method}), *args, &block)
- end
- end_eval
- end
- 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 default_helper_module!
- module_name = name.sub(/Controller$|$/, 'Helper')
- module_path = module_name.split('::').map { |m| m.underscore }.join('/')
- require_dependency module_path
- helper module_name.constantize
- rescue LoadError
- logger.debug("#{name}: missing default helper path #{module_path}") if logger
- rescue NameError
- logger.debug("#{name}: missing default helper module #{module_name}") if logger
- end
- def inherited_with_helper(child)
- inherited_without_helper(child)
- begin
- child.master_helper_module = Module.new
- child.master_helper_module.send :include, master_helper_module
- child.send :default_helper_module!
- rescue MissingSourceFile => e
- raise unless e.is_missing?("helpers/#{child.controller_path}_helper")
- end
- end
- end
- end
- end
- require 'dispatcher'
- require 'stringio'
- require 'uri'
- module ActionController
- module Integration #:nodoc:
- # An integration Session instance represents a set of requests and responses
- # performed sequentially by some virtual user. Becase you can instantiate
- # multiple sessions and run them side-by-side, you can also mimic (to some
- # limited extent) multiple simultaneous users interacting with your system.
- #
- # Typically, you will instantiate a new session using IntegrationTest#open_session,
- # rather than instantiating Integration::Session directly.
- class Session
- include Test::Unit::Assertions
- include ActionController::TestProcess
- # The integer HTTP status code of the last request.
- attr_reader :status
- # The status message that accompanied the status code of the last request.
- attr_reader :status_message
- # The URI of the last request.
- attr_reader :path
- # The hostname used in the last request.
- attr_accessor :host
- # The remote_addr used in the last request.
- attr_accessor :remote_addr
- # The Accept header to send.
- attr_accessor :accept
- # A map of the cookies returned by the last response, and which will be
- # sent with the next request.
- attr_reader :cookies
- # A map of the headers returned by the last response.
- attr_reader :headers
- # A reference to the controller instance used by the last request.
- attr_reader :controller
- # A reference to the request instance used by the last request.
- attr_reader :request
- # A reference to the response instance used by the last request.
- attr_reader :response
- # Create an initialize a new Session instance.
- def initialize
- reset!
- end
- # Resets the instance. This can be used to reset the state information
- # in an existing session instance, so it can be used from a clean-slate
- # condition.
- #
- # session.reset!
- def reset!
- @status = @path = @headers = nil
- @result = @status_message = nil
- @https = false
- @cookies = {}
- @controller = @request = @response = nil
-
- self.host = "www.example.com"
- self.remote_addr = "127.0.0.1"
- self.accept = "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"
- unless @named_routes_configured
- # install the named routes in this session instance.
- klass = class<<self; self; end
- Routing::NamedRoutes.install(klass)
- # the helpers are made protected by default--we make them public for
- # easier access during testing and troubleshooting.
- klass.send(:public, *Routing::NamedRoutes::Helpers)
- @named_routes_configured = true
- end
- end
- # Specify whether or not the session should mimic a secure HTTPS request.
- #
- # session.https!
- # session.https!(false)
- def https!(flag=true)
- @https = flag
- end
- # Return +true+ if the session is mimicing a secure HTTPS request.
- #
- # if session.https?
- # ...
- # end
- def https?
- @https
- end
- # Set the host name to use in the next request.
- #
- # session.host! "www.example.com"
- def host!(name)
- @host = name
- end
- # Follow a single redirect response. If the last response was not a
- # redirect, an exception will be raised. Otherwise, the redirect is
- # performed on the location header.
- def follow_redirect!
- raise "not a redirect! #{@status} #{@status_message}" unless redirect?
- get(interpret_uri(headers["location"].first))
- status
- end
- # Performs a GET request, following any subsequent redirect. Note that
- # the redirects are followed until the response is not a redirect--this
- # means you may run into an infinite loop if your redirect loops back to
- # itself.
- def get_via_redirect(path, args={})
- get path, args
- follow_redirect! while redirect?
- status
- end
- # Performs a POST request, following any subsequent redirect. This is
- # vulnerable to infinite loops, the same as #get_via_redirect.
- def post_via_redirect(path, args={})
- post path, args
- follow_redirect! while redirect?
- status
- end
- # Returns +true+ if the last response was a redirect.
- def redirect?
- status/100 == 3
- end
- # Performs a GET request with the given parameters. The parameters may
- # be +nil+, a Hash, or a string that is appropriately encoded
- # (application/x-www-form-urlencoded or multipart/form-data). The headers
- # should be a hash. The keys will automatically be upcased, with the
- # prefix 'HTTP_' added if needed.
- def get(path, parameters=nil, headers=nil)
- process :get, path, parameters, headers
- end
- # Performs a POST request with the given parameters. The parameters may
- # be +nil+, a Hash, or a string that is appropriately encoded
- # (application/x-www-form-urlencoded or multipart/form-data). The headers
- # should be a hash. The keys will automatically be upcased, with the
- # prefix 'HTTP_' added if needed.
- def post(path, parameters=nil, headers=nil)
- process :post, path, parameters, headers
- end
- # Performs an XMLHttpRequest request with the given parameters, mimicing
- # the request environment created by the Prototype library. The parameters
- # may be +nil+, a Hash, or a string that is appropriately encoded
- # (application/x-www-form-urlencoded or multipart/form-data). The headers
- # should be a hash. The keys will automatically be upcased, with the
- # prefix 'HTTP_' added if needed.
- def xml_http_request(path, parameters=nil, headers=nil)
- headers = (headers || {}).merge("X-Requested-With" => "XMLHttpRequest")
- post(path, parameters, headers)
- end
- # Returns the URL for the given options, according to the rules specified
- # in the application's routes.
- def url_for(options)
- controller ? controller.url_for(options) : generic_url_rewriter.rewrite(options)
- end
- private
- class MockCGI < CGI #:nodoc:
- attr_accessor :stdinput, :stdoutput, :env_table
- def initialize(env, input=nil)
- self.env_table = env
- self.stdinput = StringIO.new(input || "")
- self.stdoutput = StringIO.new
- super()
- end
- end
- # Tailors the session based on the given URI, setting the HTTPS value
- # and the hostname.
- def interpret_uri(path)
- location = URI.parse(path)
- https! URI::HTTPS === location if location.scheme
- host! location.host if location.host
- location.query ? "#{location.path}?#{location.query}" : location.path
- end
- # Performs the actual request.
- def process(method, path, parameters=nil, headers=nil)
- data = requestify(parameters)
- path = interpret_uri(path) if path =~ %r{://}
- path = "/#{path}" unless path[0] == ?/
- @path = path
- env = {}
- if method == :get
- env["QUERY_STRING"] = data
- data = nil
- end
- env.update(
- "REQUEST_METHOD" => method.to_s.upcase,
- "REQUEST_URI" => path,
- "HTTP_HOST" => host,
- "REMOTE_ADDR" => remote_addr,
- "SERVER_PORT" => (https? ? "443" : "80"),
- "CONTENT_TYPE" => "application/x-www-form-urlencoded",
- "CONTENT_LENGTH" => data ? data.length.to_s : nil,
- "HTTP_COOKIE" => encode_cookies,
- "HTTPS" => https? ? "on" : "off",
- "HTTP_ACCEPT" => accept
- )
- (headers || {}).each do |key, value|
- key = key.to_s.upcase.gsub(/-/, "_")
- key = "HTTP_#{key}" unless env.has_key?(key) || env =~ /^X|HTTP/
- env[key] = value
- end
- unless ActionController::Base.respond_to?(:clear_last_instantiation!)
- ActionController::Base.send(:include, ControllerCapture)
- end
- ActionController::Base.clear_last_instantiation!
- cgi = MockCGI.new(env, data)
- Dispatcher.dispatch(cgi, ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, cgi.stdoutput)
- @result = cgi.stdoutput.string
- @controller = ActionController::Base.last_instantiation
- @request = @controller.request
- @response = @controller.response
- # Decorate the response with the standard behavior of the TestResponse
- # so that things like assert_response can be used in integration
- # tests.
- @response.extend(TestResponseBehavior)
- parse_result
- return status
- end
- # Parses the result of the response and extracts the various values,
- # like cookies, status, headers, etc.
- def parse_result
- headers, result_body = @result.split(/\r\n\r\n/, 2)
- @headers = Hash.new { |h,k| h[k] = [] }
- headers.each_line do |line|
- key, value = line.strip.split(/:\s*/, 2)
- @headers[key.downcase] << value
- end
- (@headers['set-cookie'] || [] ).each do |string|
- name, value = string.match(/^(.*?)=(.*?);/)[1,2]
- @cookies[name] = value
- end
- @status, @status_message = @headers["status"].first.split(/ /)
- @status = @status.to_i
- end
- # Encode the cookies hash in a format suitable for passing to a
- # request.
- def encode_cookies
- cookies.inject("") do |string, (name, value)|
- string << "#{name}=#{value}; "
- end
- end
- # Get a temporarly URL writer object
- def generic_url_rewriter
- cgi = MockCGI.new('REQUEST_METHOD' => "GET",
- 'QUERY_STRING' => "",
- "REQUEST_URI" => "/",
- "HTTP_HOST" => host,
- "SERVER_PORT" => https? ? "443" : "80",
- "HTTPS" => https? ? "on" : "off")
- ActionController::UrlRewriter.new(ActionController::CgiRequest.new(cgi), {})
- end
- def name_with_prefix(prefix, name)
- prefix ? "#{prefix}[#{name}]" : name.to_s
- end
- # Convert the given parameters to a request string. The parameters may
- # be a string, +nil+, or a Hash.
- def requestify(parameters, prefix=nil)
- if Hash === parameters
- return nil if parameters.empty?
- parameters.map { |k,v| requestify(v, name_with_prefix(prefix, k)) }.join("&")
- elsif Array === parameters
- parameters.map { |v| requestify(v, name_with_prefix(prefix, "")) }.join("&")
- elsif prefix.nil?
- parameters
- else
- "#{CGI.escape(prefix)}=#{CGI.escape(parameters.to_s)}"
- end
- end
- end
- # A module used to extend ActionController::Base, so that integration tests
- # can capture the controller used to satisfy a request.
- module ControllerCapture #:nodoc:
- def self.included(base)
- base.extend(ClassMethods)
- base.class_eval do
- class <<self
- alias_method :new_without_capture, :new
- alias_method :new, :new_with_capture
- end
- end
- end
- module ClassMethods #:nodoc:
- mattr_accessor :last_instantiation
- def clear_last_instantiation!
- self.last_instantiation = nil
- end
-
- def new_with_capture(*args)
- self.last_instantiation ||= new_without_capture(*args)
- end
- end
- end
- end
- # An IntegrationTest is one that spans multiple controllers and actions,
- # tying them all together to ensure they work together as expected. It tests
- # more completely than either unit or functional tests do, exercising the
- # entire stack, from the dispatcher to the database.
- #
- # At its simplest, you simply extend IntegrationTest and write your tests
- # using the get/post methods:
- #
- # require "#{File.dirname(__FILE__)}/test_helper"
- #
- # class ExampleTest < ActionController::IntegrationTest
- # fixtures :people
- #
- # def test_login
- # # get the login page
- # get "/login"
- # assert_equal 200, status
- #
- # # post the login and follow through to the home page
- # post "/login", :username => people(:jamis).username,
- # :password => people(:jamis).password
- # follow_redirect!
- # assert_equal 200, status
- # assert_equal "/home", path
- # end
- # end
- #
- # However, you can also have multiple session instances open per test, and
- # even extend those instances with assertions and methods to create a very
- # powerful testing DSL that is specific for your application. You can even
- # reference any named routes you happen to have defined!
- #
- # require "#{File.dirname(__FILE__)}/test_helper"
- #
- # class AdvancedTest < ActionController::IntegrationTest
- # fixtures :people, :rooms
- #
- # def test_login_and_speak
- # jamis, david = login(:jamis), login(:david)
- # room = rooms(:office)
- #
- # jamis.enter(room)
- # jamis.speak(room, "anybody home?")
- #
- # david.enter(room)
- # david.speak(room, "hello!")
- # end
- #
- # private
- #
- # module CustomAssertions
- # def enter(room)
- # # reference a named route, for maximum internal consistency!
- # get(room_url(:id => room.id))
- # assert(...)
- # ...
- # end
- #
- # def speak(room, message)
- # xml_http_request "/say/#{room.id}", :message => message
- # assert(...)
- # ...
- # end
- # end
- #
- # def login(who)
- # open_session do |sess|
- # sess.extend(CustomAssertions)
- # who = people(who)
- # sess.post "/login", :username => who.username,
- # :password => who.password
- # assert(...)
- # end
- # end
- # end
- class IntegrationTest < Test::Unit::TestCase
- # Work around a bug in test/unit caused by the default test being named
- # as a symbol (:default_test), which causes regex test filters
- # (like "ruby test.rb -n /foo/") to fail because =~ doesn't work on
- # symbols.
- def initialize(name) #:nodoc:
- super(name.to_s)
- end
- # Work around test/unit's requirement that every subclass of TestCase have
- # at least one test method. Note that this implementation extends to all
- # subclasses, as well, so subclasses of IntegrationTest may also exist
- # without any test methods.
- def run(*args) #:nodoc:
- return if @method_name == "default_test"
- super
- end
- # Because of how use_instantiated_fixtures and use_transactional_fixtures
- # are defined, we need to treat them as special cases. Otherwise, users
- # would potentially have to set their values for both Test::Unit::TestCase
- # ActionController::IntegrationTest, since by the time the value is set on
- # TestCase, IntegrationTest has already been defined and cannot inherit
- # changes to those variables. So, we make those two attributes copy-on-write.
- class << self
- def use_transactional_fixtures=(flag) #:nodoc:
- @_use_transactional_fixtures = true
- @use_transactional_fixtures = flag
- end
- def use_instantiated_fixtures=(flag) #:nodoc:
- @_use_instantiated_fixtures = true
- @use_instantiated_fixtures = flag
- end
- def use_transactional_fixtures #:nodoc:
- @_use_transactional_fixtures ?
- @use_transactional_fixtures :
- superclass.use_transactional_fixtures
- end
- def use_instantiated_fixtures #:nodoc:
- @_use_instantiated_fixtures ?
- @use_instantiated_fixtures :
- superclass.use_instantiated_fixtures
- end
- end
- # Reset the current session. This is useful for testing multiple sessions
- # in a single test case.
- def reset!
- @integration_session = open_session
- end
- %w(get post cookies assigns xml_http_request).each do |method|
- define_method(method) do |*args|
- reset! unless @integration_session
- returning @integration_session.send(method, *args) do
- copy_session_variables!
- end
- end
- end
- # Open a new session instance. If a block is given, the new session is
- # yielded to the block before being returned.
- #
- # session = open_session do |sess|
- # sess.extend(CustomAssertions)
- # end
- #
- # By default, a single session is automatically created for you, but you
- # can use this method to open multiple sessions that ought to be tested
- # simultaneously.
- def open_session
- session = Integration::Session.new
- # delegate the fixture accessors back to the test instance
- extras = Module.new { attr_accessor :delegate, :test_result }
- self.class.fixture_table_names.each do |table_name|
- name = table_name.tr(".", "_")
- next unless respond_to?(name)
- extras.send(:define_method, name) { |*args| delegate.send(name, *args) }
- end
- # delegate add_assertion to the test case
- extras.send(:define_method, :add_assertion) { test_result.add_assertion }
- session.extend(extras)
- session.delegate = self
- session.test_result = @_result
- yield session if block_given?
- session
- end
- # Copy the instance variables from the current session instance into the
- # test instance.
- def copy_session_variables! #:nodoc:
- return unless @integration_session
- %w(controller response request).each do |var|
- instance_variable_set("@#{var}", @integration_session.send(var))
- end
- end
- # Delegate unhandled messages to the current session instance.
- def method_missing(sym, *args, &block)
- reset! unless @integration_session
- returning @integration_session.send(sym, *args, &block) do
- copy_session_variables!
- end
- end
- end
- end
- module ActionController #:nodoc:
- module Layout #:nodoc:
- def self.included(base)
- base.extend(ClassMethods)
- base.class_eval do
- alias_method :render_with_no_layout, :render
- alias_method :render, :render_with_a_layout
- class << self
- alias_method :inherited_without_layout, :inherited
- alias_method :inherited, :inherited_with_layout
- end
- 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 are only mentioned in one place, like this:
- #
- # <!-- The header part of this layout -->
- # <%= yield %>
- # <!-- 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>
- # <%= yield %>
- #
- # ...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
- #
- # == Automatic layout assignment
- #
- # If there is a template in <tt>app/views/layouts/</tt> with the same name as the current controller then it will be automatically
- # set as that controller's layout unless explicitly told otherwise. Say you have a WeblogController, for example. If a template named
- # <tt>app/views/layouts/weblog.rhtml</tt> or <tt>app/views/layouts/weblog.rxml</tt> exists then it will be automatically set as
- # the layout for your WeblogController. You can create a layout with the name <tt>application.rhtml</tt> or <tt>application.rxml</tt>
- # and this will be set as the default controller if there is no layout with the same name as the current controller and there is
- # no layout explicitly assigned with the +layout+ method. Nested controllers use the same folder structure for automatic layout.
- # assignment. So an Admin::WeblogController will look for a template named <tt>app/views/layouts/admin/weblog.rhtml</tt>.
- # Setting a layout explicitly will always override the automatic behaviour for the controller where the layout is set.
- # Explicitly setting the layout in a parent class, though, will not override the child class's layout assignement if the child
- # class has a layout with the same name.
- #
- # == Inheritance for layouts
- #
- # Layouts are shared downwards in the inheritance hierarchy, but not upwards. Examples:
- #
- # class BankController < ActionController::Base
- # layout "bank_standard"
- #
- # class InformationController < BankController
- #
- # class VaultController < BankController
- # layout :access_level_layout
- #
- # class EmployeeController < BankController
- # layout nil
- #
- # The InformationController uses "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 name:
- #
- # class WeblogController < ActionController::Base
- # layout "weblog_standard"
- #
- # If no directory is specified for the template name, the template will by default by looked for in +app/views/layouts/+.
- #
- # == Conditional layouts
- #
- # If you have a layout that by default is applied to all the actions of a controller, you still have the option of rendering
- # a given action or set of actions without a layout, or restricting a layout to only a single action or a set of actions. The
- # <tt>:only</tt> and <tt>:except</tt> options can be passed to the layout call. For example:
- #
- # class WeblogController < ActionController::Base
- # layout "weblog_standard", :except => :rss
- #
- # # ...
- #
- # end
- #
- # This will assign "weblog_standard" as the WeblogController's layout except for the +rss+ action, which will not wrap a layout
- # around the rendered view.
- #
- # Both the <tt>:only</tt> and <tt>:except</tt> condition can accept an arbitrary number of method references, so
- # #<tt>:except => [ :rss, :text_only ]</tt> is valid, as is <tt>:except => :rss</tt>.
- #
- # == Using a different layout in the action render call
- #
- # If most of your actions use the same layout, it makes perfect sense to define a controller-wide layout as described above.
- # Some times you'll have exceptions, though, where one action wants to use a different layout than the rest of the controller.
- # This is possible using the <tt>render</tt> method. It's just a bit more manual work as you'll have to supply fully
- # qualified template and layout names as this example shows:
- #
- # class WeblogController < ActionController::Base
- # def help
- # render :action => "help/index", :layout => "help"
- # end
- # end
- #
- # As you can see, you pass the template as the first parameter, the status code as the second ("200" is OK), and the layout
- # as the third.
- #
- # NOTE: The old notation for rendering the view from a layout was to expose the magic <tt>@content_for_layout</tt> instance
- # variable. The preferred notation now is to use <tt>yield</tt>, as documented above.
- module ClassMethods
- # If a layout is specified, all rendered actions will have their result rendered
- # when the layout<tt>yield</tt>'s. 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, conditions = {})
- add_layout_conditions(conditions)
- write_inheritable_attribute "layout", template_name
- end
- def layout_conditions #:nodoc:
- @layout_conditions ||= read_inheritable_attribute("layout_conditions")
- end
-
- def default_layout #:nodoc:
- @default_layout ||= read_inheritable_attribute("layout")
- end
- private
- def inherited_with_layout(child)
- inherited_without_layout(child)
- child.send :include, Reloadable
- layout_match = child.name.underscore.sub(/_controller$/, '').sub(/^controllers\//, '')
- child.layout(layout_match) unless layout_list.grep(%r{layouts/#{layout_match}\.[a-z][0-9a-z]*$}).empty?
- end
- def layout_list
- Dir.glob("#{template_root}/layouts/**/*")
- end
- def add_layout_conditions(conditions)
- write_inheritable_hash "layout_conditions", normalize_conditions(conditions)
- end
- def normalize_conditions(conditions)
- conditions.inject({}) {|hash, (key, value)| hash.merge(key => [value].flatten.map {|action| action.to_s})}
- end
-
- def layout_directory_exists_cache
- @@layout_directory_exists_cache ||= Hash.new do |h, dirname|
- h[dirname] = File.directory? dirname
- end
- 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.default_layout
- active_layout = case layout
- when String then layout
- when Symbol then send(layout)
- when Proc then layout.call(self)
- end
-
- # Explicitly passed layout names with slashes are looked up relative to the template root,
- # but auto-discovered layouts derived from a nested controller will contain a slash, though be relative
- # to the 'layouts' directory so we have to check the file system to infer which case the layout name came from.
- if active_layout
- if active_layout.include?('/') && ! layout_directory?(active_layout)
- active_layout
- else
- "layouts/#{active_layout}"
- end
- end
- end
- def render_with_a_layout(options = nil, deprecated_status = nil, deprecated_layout = nil, &block) #:nodoc:
- template_with_options = options.is_a?(Hash)
- if apply_layout?(template_with_options, options) && (layout = pick_layout(template_with_options, options, deprecated_layout))
- options = options.merge :layout => false if template_with_options
- logger.info("Rendering #{options} within #{layout}") if logger
- if template_with_options
- content_for_layout = render_with_no_layout(options, &block)
- deprecated_status = options[:status] || deprecated_status
- else
- content_for_layout = render_with_no_layout(options, deprecated_status, &block)
- end
- erase_render_results
- add_variables_to_assigns
- @template.instance_variable_set("@content_for_layout", content_for_layout)
- render_text(@template.render_file(layout, true), deprecated_status)
- else
- render_with_no_layout(options, deprecated_status, &block)
- end
- end
- private
-
- def apply_layout?(template_with_options, options)
- return false if options == :update
- template_with_options ? candidate_for_layout?(options) : !template_exempt_from_layout?
- end
- def candidate_for_layout?(options)
- (options.has_key?(:layout) && options[:layout] != false) ||
- options.values_at(:text, :xml, :file, :inline, :partial, :nothing).compact.empty? &&
- !template_exempt_from_layout?(default_template_name(options[:action] || options[:template]))
- end
- def pick_layout(template_with_options, options, deprecated_layout)
- if deprecated_layout
- deprecated_layout
- elsif template_with_options
- case layout = options[:layout]
- when FalseClass
- nil
- when NilClass, TrueClass
- active_layout if action_has_layout?
- else
- active_layout(layout)
- end
- else
- active_layout if action_has_layout?
- end
- end
- def action_has_layout?
- if conditions = self.class.layout_conditions
- case
- when only = conditions[:only]
- only.include?(action_name)
- when except = conditions[:except]
- !except.include?(action_name)
- else
- true
- end
- else
- true
- end
- end
-
- # Does a layout directory for this class exist?
- # we cache this info in a class level hash
- def layout_directory?(layout_name)
- template_path = File.join(self.class.view_root, 'layouts', layout_name)
- dirname = File.dirname(template_path)
- self.class.send(:layout_directory_exists_cache)[dirname]
- end
- end
- end
- module ActionController
- # Macros are class-level calls that add pre-defined actions to the controller based on the parameters passed in.
- # Currently, they're used to bridge the JavaScript macros, like autocompletion and in-place editing, with the controller
- # backing.
- module Macros
- module AutoComplete #:nodoc:
- def self.append_features(base) #:nodoc:
- super
- base.extend(ClassMethods)
- end
- # Example:
- #
- # # Controller
- # class BlogController < ApplicationController
- # auto_complete_for :post, :title
- # end
- #
- # # View
- # <%= text_field_with_auto_complete :post, title %>
- #
- # By default, auto_complete_for limits the results to 10 entries,
- # and sorts by the given field.
- #
- # auto_complete_for takes a third parameter, an options hash to
- # the find method used to search for the records:
- #
- # auto_complete_for :post, :title, :limit => 15, :order => 'created_at DESC'
- #
- # For help on defining text input fields with autocompletion,
- # see ActionView::Helpers::JavaScriptHelper.
- #
- # For more examples, see script.aculo.us:
- # * http://script.aculo.us/demos/ajax/autocompleter
- # * http://script.aculo.us/demos/ajax/autocompleter_customized
- module ClassMethods
- def auto_complete_for(object, method, options = {})
- define_method("auto_complete_for_#{object}_#{method}") do
- find_options = {
- :conditions => [ "LOWER(#{method}) LIKE ?", '%' + params[object][method].downcase + '%' ],
- :order => "#{method} ASC",
- :limit => 10 }.merge!(options)
-
- @items = object.to_s.camelize.constantize.find(:all, find_options)
- render :inline => "<%= auto_complete_result @items, '#{method}' %>"
- end
- end
- end
- end
- end
- endmodule ActionController
- module Macros
- module InPlaceEditing #:nodoc:
- def self.append_features(base) #:nodoc:
- super
- base.extend(ClassMethods)
- end
- # Example:
- #
- # # Controller
- # class BlogController < ApplicationController
- # in_place_edit_for :post, :title
- # end
- #
- # # View
- # <%= in_place_editor_field :post, 'title' %>
- #
- # For help on defining an in place editor in the browser,
- # see ActionView::Helpers::JavaScriptHelper.
- module ClassMethods
- def in_place_edit_for(object, attribute, options = {})
- define_method("set_#{object}_#{attribute}") do
- @item = object.to_s.camelize.constantize.find(params[:id])
- @item.update_attribute(attribute, params[:value])
- render :text => @item.send(attribute)
- end
- end
- end
- end
- end
- end
- module ActionController #:nodoc:
- module MimeResponds #:nodoc:
- def self.included(base)
- base.send(:include, ActionController::MimeResponds::InstanceMethods)
- end
- module InstanceMethods
- # Without web-service support, an action which collects the data for displaying a list of people
- # might look something like this:
- #
- # def list
- # @people = Person.find(:all)
- # end
- #
- # Here's the same action, with web-service support baked in:
- #
- # def list
- # @people = Person.find(:all)
- #
- # respond_to do |wants|
- # wants.html
- # wants.xml { render :xml => @people.to_xml }
- # end
- # end
- #
- # What that says is, "if the client wants HTML in response to this action, just respond as we
- # would have before, but if the client wants XML, return them the list of people in XML format."
- # (Rails determines the desired response format from the HTTP Accept header submitted by the client.)
- #
- # Supposing you have an action that adds a new person, optionally creating their company
- # (by name) if it does not already exist, without web-services, it might look like this:
- #
- # def add
- # @company = Company.find_or_create_by_name(params[:company][:name])
- # @person = @company.people.create(params[:person])
- #
- # redirect_to(person_list_url)
- # end
- #
- # Here's the same action, with web-service support baked in:
- #
- # def add
- # company = params[:person].delete(:company)
- # @company = Company.find_or_create_by_name(company[:name])
- # @person = @company.people.create(params[:person])
- #
- # respond_to do |wants|
- # wants.html { redirect_to(person_list_url) }
- # wants.js
- # wants.xml { render :xml => @person.to_xml(:include => @company) }
- # end
- # end
- #
- # If the client wants HTML, we just redirect them back to the person list. If they want Javascript
- # (wants.js), then it is an RJS request and we render the RJS template associated with this action.
- # Lastly, if the client wants XML, we render the created person as XML, but with a twist: we also
- # include the person’s company in the rendered XML, so you get something like this:
- #
- # <person>
- # <id>...</id>
- # ...
- # <company>
- # <id>...</id>
- # <name>...</name>
- # ...
- # </company>
- # </person>
- #
- # Note, however, the extra bit at the top of that action:
- #
- # company = params[:person].delete(:company)
-