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. # # * recipients - Takes one or more email addresses. These addresses are where your email will be delivered to. Sets the To: header. # * subject - The subject of your email. Sets the Subject: header. # * from - Who the email you are sending is from. Sets the From: header. # * cc - Takes one or more email addresses. These addresses will receive a carbon copy of your email. Sets the Cc: header. # * bcc - Takes one or more email address. These addresses will receive a blind carbon copy of your email. Sets the Bcc header. # * sent_on - The date on which the message was sent. If not set, the header wil be set by the delivery agent. # * content_type - Specify the content type of the message. Defaults to text/plain. # * headers - Specify additional headers to be set for the message, e.g. headers 'X-Mail-Count' => 107370. # # The body 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, body "account" => recipient would result # in an instance variable @account with the value of recipient 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 .rhtml file with the same name as the method # in your mailer model. For example, in the mailer defined above, the template at # app/views/notifier/signup_notification.rhtml 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 # deliver_ followed by the name of the mailer method that you would # like to deliver. The signup_notification method defined above is # delivered by invoking Notifier.deliver_signup_notification. # # = HTML Email # To send mail as HTML, make sure your view (the .rhtml 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 ActionMailer::Base.template_root = "/my/templates" # # * template_root - template root determines the base from which template references will be made. # # * logger - 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. # # * server_settings - Allows detailed configuration of the server: # * :address Allows you to use a remote mail server. Just change it from its default "localhost" setting. # * :port On the off chance that your mail server doesn't run on port 25, you can change it. # * :domain If you need to specify a HELO domain, you can do it here. # * :user_name If your mail server requires authentication, set the username in this setting. # * :password If your mail server requires authentication, set the password in this setting. # * :authentication If your mail server requires authentication, you need to specify the authentication type here. # This is a symbol and one of :plain, :login, :cram_md5 # # * raise_delivery_errors - whether or not errors should be raised if the email fails to be delivered. # # * delivery_method - Defines a delivery method. Possible values are :smtp (default), :sendmail, and :test. # Sendmail is assumed to be present at "/usr/sbin/sendmail". # # * perform_deliveries - Determines whether deliver_* methods are actually carried out. By default they are, # but this can be turned off to help functional testing. # # * deliveries - Keeps an array of all the emails sent out through the Action Mailer with delivery_method :test. Most useful # for unit and functional testing. # # * default_charset - 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 @charset. # * default_content_type - 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 @content_type. # * default_mime_version - The default mime version used for the message. Defaults to nil. You # can also pick a different value from inside a method with @mime_version. When multipart messages are in # use, @mime_version will be set to "1.0" if it is not set inside a method. # * default_implicit_parts_order - 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 # @implicit_parts_order. 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 text/plain # 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*:: [] # Used in:: 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*:: 72 # Used in:: #format, #paragraphs, # #center 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*:: 72 # Used in:: #format, #paragraphs, # #center 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*:: 0 # Used in:: #format, #paragraphs, # #center 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*:: 0 # Used in:: #format, #paragraphs, # #center 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*:: 0 # Used in:: #format, #paragraphs, # #center 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*:: 0 # Used in:: #format, #paragraphs, # #center 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*:: 4 # Used in:: #format, #paragraphs 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*:: 4 # Used in:: #format, #paragraphs 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*:: 0 # Used in:: #format, #paragraphs 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*:: 0 # Used in:: #format, #paragraphs 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 #split_words. 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+ # Used in:: #format, #paragraphs 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+ # Used in:: #format, #paragraphs 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+ # Used in:: #format, #paragraphs 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*:: Text::Format::SPLIT_FIXED # Used in:: #format, #paragraphs 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., SPLIT_FIXED | # SPLIT_CONTINUATION) 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*:: Text::Format::SPLIT_FIXED # Used in:: #format, #paragraphs 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+ # Used in:: #format, #paragraphs 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*:: {} # Used in:: #format, #paragraphs attr_accessor :abbreviations # Indicates whether the formatting of paragraphs should be done with # tagged paragraphs. Useful only with #tag_text. # # *Default*:: +false+ # Used in:: #format, #paragraphs attr_accessor :tag_paragraph # The array of text to be placed before each paragraph when # #tag_paragraph is +true+. When #format() is called, # only the first element of the array is used. When #paragraphs # 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 #tag_paragraph is +true+. # # *Default*:: [] # Used in:: #format, #paragraphs attr_accessor :tag_text # Indicates whether or not the non-breaking space feature should be # used. # # *Default*:: +false+ # Used in:: #format, #paragraphs 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*:: {} # Used in:: #format, #paragraphs attr_accessor :nobreak_regex # Indicates the number of spaces that a single tab represents. # # *Default*:: 8 # Used in:: #expand, #unexpand, # #paragraphs attr_reader :tabstop # Indicates the number of spaces that a single tab represents. # # *Default*:: 8 # Used in:: #expand, #unexpand, # #paragraphs 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*:: Text::Format::LEFT_ALIGN # Used in:: #format, #paragraphs 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*:: Text::Format::LEFT_ALIGN # Used in:: #format, #paragraphs 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+ # Used in:: #format, #paragraphs def left_align? return @format_style == LEFT_ALIGN end # Indicates that the format style is right alignment. # # *Default*:: +false+ # Used in:: #format, #paragraphs def right_align? return @format_style == RIGHT_ALIGN end # Indicates that the format style is right fill. # # *Default*:: +false+ # Used in:: #format, #paragraphs def right_fill? return @format_style == RIGHT_FILL end # Indicates that the format style is full justification. # # *Default*:: +false+ # Used in:: #format, #paragraphs 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 #text 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 #first_indent is the same as #body_indent, then # paragraphs will be separated by a single empty line in the result; # otherwise, the paragraphs will follow immediately after each other. # Uses #format 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 #tabstop 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 #tabstop 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 #text. # 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" # # 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 # # 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 # # 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 # # 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 # # 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 : <> # J : <> # S : <> # # 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 # # 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 # # 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 # # 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 # # 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', % 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 # # 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 # # 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 # # 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 # # 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 ' ' 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 # # 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 # # 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 # # 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 # # 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 # # 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 # # 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) "#{text}" 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{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 " 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 " @sent_on = Time.local 2004, 12, 12 @cc = "Grytøyr " @bcc = "Grytøyr " @body = "Nothing to see here." @charset = "iso-8859-1" end def utf8_body(recipient) @recipients = recipient @subject = "testing utf-8 body" @from = "Foo áëô îü " @sent_on = Time.local 2004, 12, 12 @cc = "Foo áëô îü " @bcc = "Foo áëô îü " @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 = "blah" 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 = "blah" 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 "Emphasize this" content_type "text/html" end def html_mail_with_underscores(recipient) subject "html mail with underscores" body %{_Google} 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 => "

line #1

\n

line #2

\r

line #3

\r\n

line #4

\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 => "test HTML
\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' => '' } 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 " cc "Three: Four " bcc "Five: Six " 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 < 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 "", 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 = <" 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 ", "iso-8859-1" expected.cc = quote_address_if_necessary "Grytøyr ", "iso-8859-1" expected.bcc = quote_address_if_necessary "Grytøyr ", "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 áëô îü " 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 áëô îü\" ", "\"Example Recipient\" "] 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_.*?\?= \r/, created.encoded) assert_match(/\nTo: =\?utf-8\?Q\?Foo_.*?\?= , Example Recipient _Google}, 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 "

line #1

\n

line #2

\n

line #3

\n

line #4

\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: # # * :success: Status code was 200 # * :redirect: Status code was in the 300-399 range # * :missing: Status code was 404 # * :error: 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): # # * :tag: the node type must match the corresponding value # * :attributes: a hash. The node's attributes must match the # corresponding values in the hash. # * :parent: a hash. The node's parent must match the # corresponding hash. # * :child: a hash. At least one of the node's immediate children # must meet the criteria described by the hash. # * :ancestor: a hash. At least one of the node's ancestors must # meet the criteria described by the hash. # * :descendant: a hash. At least one of the node's descendants # must meet the criteria described by the hash. # * :sibling: a hash. At least one of the node's siblings must # meet the criteria described by the hash. # * :after: a hash. The node must be after any sibling meeting # the criteria described by the hash, and at least one sibling must match. # * :before: a hash. The node must be before any sibling meeting # the criteria described by the hash, and at least one sibling must match. # * :children: a hash, for counting children of a node. Accepts # the keys: # * :count: either a number or a range which must equal (or # include) the number of children that match. # * :less_than: the number of matching children must be less # than this number. # * :greater_than: the number of matching children must be # greater than this number. # * :only: another hash consisting of the keys to use # to match on the children, and only matching children will be # counted. # * :content: 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/ } # # Please noteYou must explicitly # close all of your tags to use these assertions.
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 app/views directory corresponding to the name of the controller and action # after executing code in the action. For example, the +index+ action of the +GuestBookController+ would render the # template app/views/guestbook/index.rhtml by default after populating the @entries 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 /weblog/list?category=All&limit=5 will include # { "category" => "All", "limit" => 5 } in params. # # It's also possible to construct multi-dimensional parameter hashes by specifying keys using brackets, such as: # # # # # A request stemming from a form holding these inputs will include { "post" => { "name" => "david", "address" => "hyacintvej" } }. # If the address input had been named "post[address][street]", the params would have included # { "post" => { "address" => { "street" => "hyacintvej" } } }. There's no limit to the depth of the nesting. # # == Sessions # # Sessions allows you to store objects in between requests. This is useful for objects that are not yet ready to be persisted, # such as a Signup object constructed in a multi-paged process, or objects that don't change much and are needed all the time, such # as a User object for a system that requires login. The session should not be used, however, as a cache for objects where it's likely # they could be changed unknowingly. It's usually too much work to keep it all synchronized -- something databases already excel at. # # You can place objects in the session by using the session method, which accesses a hash: # # session[:person] = Person.authenticate(user_name, password) # # And retrieved again through the same hash: # # Hello #{session[:person]} # # For removing objects from the session, you can either assign a single key to nil, like session[:person] = nil, or you can # remove the entire session with reset_session. # # By default, sessions are stored on the file system in RAILS_ROOT/tmp/sessions. 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 environment.rb and run rake db:sessions:create. # # == 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 create action, which stores a blog entry to a database, # we might like to show the user the new entry. Because we're following good DRY principles (Don't Repeat Yourself), we're going to reuse (and redirect to) # a show action that we'll assume has already been created. The code might look like this: # # def create # @entry = Entry.new(params[:entry]) # if @entry.save # # The entry was saved correctly, redirect to show # redirect_to :action => 'show', :id => @entry.id # else # # things didn't go so well, do something else # end # end # # In this case, after saving our new entry to the database, the user is redirected to the show method which is then executed. # # == 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 local_request? # 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 # params 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 params. 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 "David" 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 # request.env["REQUEST_URI"]. attr_accessor :request # Holds a hash of all the GET, POST, and Url parameters passed to the action. Accessed like params["post_id"] # 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 # response.headers["Cache-Control"] = "no-cache". Can also be used to access the final body HTML after a template # has been rendered through response.body -- useful for after_filters that wants to manipulate the output, # such as a OutputCompressionFilter. attr_accessor :response # Holds a hash of objects in the session. Accessed like session[:person] 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 headers["Cache-Control"] 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 hide_actions. 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). #   # url_for is used to: #   # All keys given to url_for are forwarded to the Route module, save for the following: # * :anchor -- specifies the anchor name to be appended to the path. For example, # url_for :controller => 'posts', :action => 'show', :id => 10, :anchor => 'comments' # will produce "/posts/show/10#comments". # * :only_path -- if true, returns the absolute URL (omitting the protocol, host name, and port) # * :trailing_slash -- if true, adds a trailing slash, as in "/archive/2005/". Note that this # is currently not recommended since it breaks caching. # * :host -- overrides the default (current) host if provided # * :protocol -- 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 and a query string. # Routes composes a query string as the key/value pairs not included in the . # # 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, # url_for :action => 'some_action' will retain the current controller, as expected. This behavior extends to # other parameters, including :controller, :id, and any other parameters that are placed into a Route's # path. #   # The URL helpers such as url_for 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: url_for :controller => '/home' # * 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 map.connect 'people/:last/:first/:action', :action => 'bio', :controller => 'people'. # # 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. # # * url_for :action => 'bio' -- 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". # * url_for :first => 'davids-little-brother' 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 :first is not equal to default value for :first 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 :name => nil to url_for'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, url_for :action => 'print' # 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 render_action("action", status = 200), # render_without_layout("controller/action", status = 200), and # render_with_layout("controller/action", status = 200, layout). # # === 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 # render_partial(partial_path = default_template_name, object = nil, local_assigns = {}) and # render_partial_collection(partial_name, collection, partial_spacer_template = nil, local_assigns = {}). # # === 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 render_file(path, status = 200) # # === 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 render_text("text", status = 200) # # === 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 "

Good seeing you!

" 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 render_template(template, status = 200, type = :rhtml) # # === 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 :update 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 render, 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: # # * Hash: The URL will be generated by calling url_for with the +options+. # * String starting with protocol:// (like http://): Is passed straight through as the target for redirection. # * String not containing a protocol: The current protocol and host is prepended to the string. # * :back: Back to the page that issued the request. Useful for forms that are triggered from multiple places. # Short-hand for redirect_to(request.env["HTTP_REFERER"]) # # 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 redirect_to :back, 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 use_silence is set to false). # # The benchmark is only recorded if the current level of the logger matches the log_level, 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 caches 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: # # Hello <%= @name %> # <% 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 # expire_fragment(:controller => "topics", :action => "list") -- 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 caches_action. 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 # expire_fragment(:controller => "topics", :action => "list", :action_suffix => "all_topics"). # # == 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 cache_sweeper 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 session_options to disable # sessions (large performance increase if sessions are not needed). The session_options are the same as for CGI::Session: # # * :database_manager - 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. # * :session_key - the parameter name used for the session id. Defaults to '_session_id'. # * :session_id - 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. # * :new_session - 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. # * :session_expires - the time the current session expires, as a +Time+ object. If not set, the session will continue # indefinitely. # * :session_domain - the hostname domain for which this session is valid. If not set, defaults to the hostname of the # server. # * :session_secure - if +true+, this session will only work over HTTPS. # * :session_path - 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, < 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: # # * value - the cookie's value or list of values (as an array). # * path - the path for which this cookie applies. Defaults to the root of the application. # * 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. 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 # ApplicationController.dependencies_on(:model) would return [:account, :company, :person, :project, :category] 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 before_filter # 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 before_filter and after_filter 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 prepend_before_filter and prepend_after_filter. 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 :ensure_items_in_cart, :ensure_items_in_stock, # :verify_open_shop. 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 filters 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 filters 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 filters 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 filters 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 filters 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 filters 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 :only and :except 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 :only and :except 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 flash[:notice] = "Successfully created" before redirecting to a display action that can then expose # the flash to its template. Actually, that exposure is automatically done. Example: # # class 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] %>
<%= @flash[:notice] %>
<% 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 ([]=). # When you need to pass an object to the current action, you use now, and your object will # vanish when the current action is done. # # Entries set via now are accessed the same way as standard entries: flash['my-key']. 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 keep and discard 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 flash["notice"] to read a notice you put there or # flash["notice"] = "hello" 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 flash.keep 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: <%= hello_world %> 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< "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 < 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: # # # <%= yield %> # # # 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: # # # hello world # # # == 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: # #

<%= @page_title %>

# <%= 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: # #

Welcome

# Off-world colonies offers you a chance to start a new life # # == Automatic layout assignment # # If there is a template in app/views/layouts/ 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 # app/views/layouts/weblog.rhtml or app/views/layouts/weblog.rxml exists then it will be automatically set as # the layout for your WeblogController. You can create a layout with the name application.rhtml or application.rxml # 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 app/views/layouts/admin/weblog.rhtml. # 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 # :only and :except 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 :only and :except condition can accept an arbitrary number of method references, so # #:except => [ :rss, :text_only ] is valid, as is :except => :rss. # # == 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 render 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 @content_for_layout instance # variable. The preferred notation now is to use yield, as documented above. module ClassMethods # If a layout is specified, all rendered actions will have their result rendered # when the layoutyield'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 layout "weblog/standard" will return # weblog/standard, but layout "standard" 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: # # # ... # ... # # ... # ... # ... # # # # Note, however, the extra bit at the top of that action: # # company = params[:person].delete(:company) # @company = Company.find_or_create_by_name(company[:name]) # # This is because the incoming XML document (if a web-service request is in process) can only contain a # single root-node. So, we have to rearrange things so that the request looks like this (url-encoded): # # person[name]=...&person[company][name]=...&... # # And, like this (xml-encoded): # # # ... # # ... # # # # In other words, we make the request so that it operates on a single entity—a person. Then, in the action, # we extract the company data from the request, find or create the company, and then create the new person # with the remaining data. # # Note that you can define your own XML parameter parser which would allow you to describe multiple entities # in a single request (i.e., by wrapping them all in a single root note), but if you just go with the flow # and accept Rails' defaults, life will be much easier. # # If you need to use a MIME type which isn't supported by default, you can register your own handlers in # environment.rb as follows. # # Mime::Type.register "image/jpg", :jpg # def respond_to(*types, &block) raise ArgumentError, "respond_to takes either types or a block, never bot" unless types.any? ^ block block ||= lambda { |responder| types.each { |type| responder.send(type) } } responder = Responder.new(block.binding) block.call(responder) responder.respond end end class Responder #:nodoc: DEFAULT_BLOCKS = { :html => 'Proc.new { render }', :js => 'Proc.new { render :action => "#{action_name}.rjs" }', :xml => 'Proc.new { render :action => "#{action_name}.rxml" }' } def initialize(block_binding) @block_binding = block_binding @mime_type_priority = eval("request.accepts", block_binding) @order = [] @responses = {} end def custom(mime_type, &block) mime_type = mime_type.is_a?(Mime::Type) ? mime_type : Mime::Type.lookup(mime_type.to_s) @order << mime_type if block_given? @responses[mime_type] = block else @responses[mime_type] = eval(DEFAULT_BLOCKS[mime_type.to_sym], @block_binding) end end for mime_type in %w( all html js xml rss atom yaml ) eval <<-EOT def #{mime_type}(&block) custom(Mime::#{mime_type.upcase}, &block) end EOT end def any(*args, &block) args.each { |type| send(type, &block) } end def respond for priority in @mime_type_priority if priority == Mime::ALL @responses[@order.first].call return else if priority === @order @responses[priority].call return # mime type match found, be happy and return end end end if @order.include?(Mime::ALL) @responses[Mime::ALL].call else eval 'render(:nothing => true, :status => "406 Not Acceptable")', @block_binding end end end end end module Mime class Type #:nodoc: # A simple helper class used in parsing the accept header class AcceptItem #:nodoc: attr_accessor :order, :name, :q def initialize(order, name, q=nil) @order = order @name = name.strip q ||= 0.0 if @name == "*/*" # default "*/*" to end of list @q = ((q || 1.0).to_f * 100).to_i end def to_s @name end def <=>(item) result = item.q <=> q result = order <=> item.order if result == 0 result end def ==(item) name == (item.respond_to?(:name) ? item.name : item) end end class << self def lookup(string) LOOKUP[string] end def parse(accept_header) # keep track of creation order to keep the subsequent sort stable index = 0 list = accept_header.split(/,/). map! { |i| AcceptItem.new(index += 1, *i.split(/;\s*q=/)) }.sort! # Take care of the broken text/xml entry by renaming or deleting it text_xml = list.index("text/xml") app_xml = list.index("application/xml") if text_xml && app_xml # set the q value to the max of the two list[app_xml].q = [list[text_xml].q, list[app_xml].q].max # make sure app_xml is ahead of text_xml in the list if app_xml > text_xml list[app_xml], list[text_xml] = list[text_xml], list[app_xml] app_xml, text_xml = text_xml, app_xml end # delete text_xml from the list list.delete_at(text_xml) elsif text_xml list[text_xml].name = "application/xml" end # Look for more specific xml-based types and sort them ahead of app/xml if app_xml idx = app_xml app_xml_type = list[app_xml] while(idx < list.length) type = list[idx] break if type.q < app_xml_type.q if type.name =~ /\+xml$/ list[app_xml], list[idx] = list[idx], list[app_xml] app_xml = idx end idx += 1 end end list.map! { |i| Mime::Type.lookup(i.name) }.uniq! list end end def initialize(string, symbol = nil, synonyms = []) @symbol, @synonyms = symbol, synonyms @string = string end def to_s @string end def to_str to_s end def to_sym @symbol || @string.to_sym end def ===(list) if list.is_a?(Array) (@synonyms + [ self ]).any? { |synonym| list.include?(synonym) } else super end end def ==(mime_type) (@synonyms + [ self ]).any? { |synonym| synonym.to_s == mime_type.to_s } if mime_type end end ALL = Type.new "*/*", :all HTML = Type.new "text/html", :html, %w( application/xhtml+xml ) JS = Type.new "text/javascript", :js, %w( application/javascript application/x-javascript ) XML = Type.new "application/xml", :xml, %w( text/xml application/x-xml ) RSS = Type.new "application/rss+xml", :rss ATOM = Type.new "application/atom+xml", :atom YAML = Type.new "application/x-yaml", :yaml, %w( text/yaml ) LOOKUP = Hash.new { |h, k| h[k] = Type.new(k) } LOOKUP["*/*"] = ALL LOOKUP["text/html"] = HTML LOOKUP["application/xhtml+xml"] = HTML LOOKUP["application/xml"] = XML LOOKUP["text/xml"] = XML LOOKUP["application/x-xml"] = XML LOOKUP["text/javascript"] = JS LOOKUP["application/javascript"] = JS LOOKUP["application/x-javascript"] = JS LOOKUP["text/yaml"] = YAML LOOKUP["application/x-yaml"] = YAML LOOKUP["application/rss+xml"] = RSS LOOKUP["application/atom+xml"] = ATOM endmodule ActionController # === Action Pack pagination for Active Record collections # # The Pagination module aids in the process of paging large collections of # Active Record objects. It offers macro-style automatic fetching of your # model for multiple views, or explicit fetching for single actions. And if # the magic isn't flexible enough for your needs, you can create your own # paginators with a minimal amount of code. # # The Pagination module can handle as much or as little as you wish. In the # controller, have it automatically query your model for pagination; or, # if you prefer, create Paginator objects yourself. # # Pagination is included automatically for all controllers. # # For help rendering pagination links, see # ActionView::Helpers::PaginationHelper. # # ==== Automatic pagination for every action in a controller # # class PersonController < ApplicationController # model :person # # paginate :people, :order => 'last_name, first_name', # :per_page => 20 # # # ... # end # # Each action in this controller now has access to a @people # instance variable, which is an ordered collection of model objects for the # current page (at most 20, sorted by last name and first name), and a # @person_pages Paginator instance. The current page is determined # by the params[:page] variable. # # ==== Pagination for a single action # # def list # @person_pages, @people = # paginate :people, :order => 'last_name, first_name' # end # # Like the previous example, but explicitly creates @person_pages # and @people for a single action, and uses the default of 10 items # per page. # # ==== Custom/"classic" pagination # # def list # @person_pages = Paginator.new self, Person.count, 10, params[:page] # @people = Person.find :all, :order => 'last_name, first_name', # :limit => @person_pages.items_per_page, # :offset => @person_pages.current.offset # end # # Explicitly creates the paginator from the previous example and uses # Paginator#to_sql to retrieve @people from the model. # module Pagination unless const_defined?(:OPTIONS) # A hash holding options for controllers using macro-style pagination OPTIONS = Hash.new # The default options for pagination DEFAULT_OPTIONS = { :class_name => nil, :singular_name => nil, :per_page => 10, :conditions => nil, :order_by => nil, :order => nil, :join => nil, :joins => nil, :count => nil, :include => nil, :select => nil, :parameter => 'page' } end def self.included(base) #:nodoc: super base.extend(ClassMethods) end def self.validate_options!(collection_id, options, in_action) #:nodoc: options.merge!(DEFAULT_OPTIONS) {|key, old, new| old} valid_options = DEFAULT_OPTIONS.keys valid_options << :actions unless in_action unknown_option_keys = options.keys - valid_options raise ActionController::ActionControllerError, "Unknown options: #{unknown_option_keys.join(', ')}" unless unknown_option_keys.empty? options[:singular_name] ||= Inflector.singularize(collection_id.to_s) options[:class_name] ||= Inflector.camelize(options[:singular_name]) end # Returns a paginator and a collection of Active Record model instances # for the paginator's current page. This is designed to be used in a # single action; to automatically paginate multiple actions, consider # ClassMethods#paginate. # # +options+ are: # :singular_name:: the singular name to use, if it can't be inferred by # singularizing the collection name # :class_name:: the class name to use, if it can't be inferred by # camelizing the singular name # :per_page:: the maximum number of items to include in a # single page. Defaults to 10 # :conditions:: optional conditions passed to Model.find(:all, *params) and # Model.count # :order:: optional order parameter passed to Model.find(:all, *params) # :order_by:: (deprecated, used :order) optional order parameter passed to Model.find(:all, *params) # :joins:: optional joins parameter passed to Model.find(:all, *params) # and Model.count # :join:: (deprecated, used :joins or :include) optional join parameter passed to Model.find(:all, *params) # and Model.count # :include:: optional eager loading parameter passed to Model.find(:all, *params) # and Model.count # :select:: :select parameter passed to Model.find(:all, *params) # # :count:: parameter passed as :select option to Model.count(*params) # def paginate(collection_id, options={}) Pagination.validate_options!(collection_id, options, true) paginator_and_collection_for(collection_id, options) end # These methods become class methods on any controller module ClassMethods # Creates a +before_filter+ which automatically paginates an Active # Record model for all actions in a controller (or certain actions if # specified with the :actions option). # # +options+ are the same as PaginationHelper#paginate, with the addition # of: # :actions:: an array of actions for which the pagination is # active. Defaults to +nil+ (i.e., every action) def paginate(collection_id, options={}) Pagination.validate_options!(collection_id, options, false) module_eval do before_filter :create_paginators_and_retrieve_collections OPTIONS[self] ||= Hash.new OPTIONS[self][collection_id] = options end end end def create_paginators_and_retrieve_collections #:nodoc: Pagination::OPTIONS[self.class].each do |collection_id, options| next unless options[:actions].include? action_name if options[:actions] paginator, collection = paginator_and_collection_for(collection_id, options) paginator_name = "@#{options[:singular_name]}_pages" self.instance_variable_set(paginator_name, paginator) collection_name = "@#{collection_id.to_s}" self.instance_variable_set(collection_name, collection) end end # Returns the total number of items in the collection to be paginated for # the +model+ and given +conditions+. Override this method to implement a # custom counter. def count_collection_for_pagination(model, options) model.count(:conditions => options[:conditions], :joins => options[:join] || options[:joins], :include => options[:include], :select => options[:count]) end # Returns a collection of items for the given +model+ and +options[conditions]+, # ordered by +options[order]+, for the current page in the given +paginator+. # Override this method to implement a custom finder. def find_collection_for_pagination(model, options, paginator) model.find(:all, :conditions => options[:conditions], :order => options[:order_by] || options[:order], :joins => options[:join] || options[:joins], :include => options[:include], :select => options[:select], :limit => options[:per_page], :offset => paginator.current.offset) end protected :create_paginators_and_retrieve_collections, :count_collection_for_pagination, :find_collection_for_pagination def paginator_and_collection_for(collection_id, options) #:nodoc: klass = options[:class_name].constantize page = @params[options[:parameter]] count = count_collection_for_pagination(klass, options) paginator = Paginator.new(self, count, options[:per_page], page) collection = find_collection_for_pagination(klass, options, paginator) return paginator, collection end private :paginator_and_collection_for # A class representing a paginator for an Active Record collection. class Paginator include Enumerable # Creates a new Paginator on the given +controller+ for a set of items # of size +item_count+ and having +items_per_page+ items per page. # Raises ArgumentError if items_per_page is out of bounds (i.e., less # than or equal to zero). The page CGI parameter for links defaults to # "page" and can be overridden with +page_parameter+. def initialize(controller, item_count, items_per_page, current_page=1) raise ArgumentError, 'must have at least one item per page' if items_per_page <= 0 @controller = controller @item_count = item_count || 0 @items_per_page = items_per_page @pages = {} self.current_page = current_page end attr_reader :controller, :item_count, :items_per_page # Sets the current page number of this paginator. If +page+ is a Page # object, its +number+ attribute is used as the value; if the page does # not belong to this Paginator, an ArgumentError is raised. def current_page=(page) if page.is_a? Page raise ArgumentError, 'Page/Paginator mismatch' unless page.paginator == self end page = page.to_i @current_page_number = has_page_number?(page) ? page : 1 end # Returns a Page object representing this paginator's current page. def current_page @current_page ||= self[@current_page_number] end alias current :current_page # Returns a new Page representing the first page in this paginator. def first_page @first_page ||= self[1] end alias first :first_page # Returns a new Page representing the last page in this paginator. def last_page @last_page ||= self[page_count] end alias last :last_page # Returns the number of pages in this paginator. def page_count @page_count ||= @item_count.zero? ? 1 : (q,r=@item_count.divmod(@items_per_page); r==0? q : q+1) end alias length :page_count # Returns true if this paginator contains the page of index +number+. def has_page_number?(number) number >= 1 and number <= page_count end # Returns a new Page representing the page with the given index # +number+. def [](number) @pages[number] ||= Page.new(self, number) end # Successively yields all the paginator's pages to the given block. def each(&block) page_count.times do |n| yield self[n+1] end end # A class representing a single page in a paginator. class Page include Comparable # Creates a new Page for the given +paginator+ with the index # +number+. If +number+ is not in the range of valid page numbers or # is not a number at all, it defaults to 1. def initialize(paginator, number) @paginator = paginator @number = number.to_i @number = 1 unless @paginator.has_page_number? @number end attr_reader :paginator, :number alias to_i :number # Compares two Page objects and returns true when they represent the # same page (i.e., their paginators are the same and they have the # same page number). def ==(page) return false if page.nil? @paginator == page.paginator and @number == page.number end # Compares two Page objects and returns -1 if the left-hand page comes # before the right-hand page, 0 if the pages are equal, and 1 if the # left-hand page comes after the right-hand page. Raises ArgumentError # if the pages do not belong to the same Paginator object. def <=>(page) raise ArgumentError unless @paginator == page.paginator @number <=> page.number end # Returns the item offset for the first item in this page. def offset @paginator.items_per_page * (@number - 1) end # Returns the number of the first item displayed. def first_item offset + 1 end # Returns the number of the last item displayed. def last_item [@paginator.items_per_page * @number, @paginator.item_count].min end # Returns true if this page is the first page in the paginator. def first? self == @paginator.first end # Returns true if this page is the last page in the paginator. def last? self == @paginator.last end # Returns a new Page object representing the page just before this # page, or nil if this is the first page. def previous if first? then nil else @paginator[@number - 1] end end # Returns a new Page object representing the page just after this # page, or nil if this is the last page. def next if last? then nil else @paginator[@number + 1] end end # Returns a new Window object for this page with the specified # +padding+. def window(padding=2) Window.new(self, padding) end # Returns the limit/offset array for this page. def to_sql [@paginator.items_per_page, offset] end def to_param #:nodoc: @number.to_s end end # A class for representing ranges around a given page. class Window # Creates a new Window object for the given +page+ with the specified # +padding+. def initialize(page, padding=2) @paginator = page.paginator @page = page self.padding = padding end attr_reader :paginator, :page # Sets the window's padding (the number of pages on either side of the # window page). def padding=(padding) @padding = padding < 0 ? 0 : padding # Find the beginning and end pages of the window @first = @paginator.has_page_number?(@page.number - @padding) ? @paginator[@page.number - @padding] : @paginator.first @last = @paginator.has_page_number?(@page.number + @padding) ? @paginator[@page.number + @padding] : @paginator.last end attr_reader :padding, :first, :last # Returns an array of Page objects in the current window. def pages (@first.number..@last.number).to_a.collect! {|n| @paginator[n]} end alias to_a :pages end end end end module ActionController # Subclassing AbstractRequest makes these methods available to the request objects used in production and testing, # CgiRequest and TestRequest class AbstractRequest cattr_accessor :relative_url_root # Returns the hash of environment variables for this request, # such as { 'RAILS_ENV' => 'production' }. attr_reader :env # Returns both GET and POST parameters in a single hash. def parameters @parameters ||= request_parameters.update(query_parameters).update(path_parameters).with_indifferent_access end # Returns the HTTP request method as a lowercase symbol (:get, for example) def method @request_method ||= @env['REQUEST_METHOD'].downcase.to_sym end # Is this a GET request? Equivalent to request.method == :get def get? method == :get end # Is this a POST request? Equivalent to request.method == :post def post? method == :post end # Is this a PUT request? Equivalent to request.method == :put def put? method == :put end # Is this a DELETE request? Equivalent to request.method == :delete def delete? method == :delete end # Is this a HEAD request? Equivalent to request.method == :head def head? method == :head end # 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 content_type @content_type ||= begin content_type = @env['CONTENT_TYPE'].to_s.downcase if x_post_format = @env['HTTP_X_POST_DATA_FORMAT'] case x_post_format.to_s.downcase when 'yaml' content_type = 'application/x-yaml' when 'xml' content_type = 'application/xml' end end Mime::Type.lookup(content_type) end end # Returns the accepted MIME type for the request def accepts @accepts ||= if @env['HTTP_ACCEPT'].to_s.strip.empty? [ content_type, Mime::ALL ] else Mime::Type.parse(@env['HTTP_ACCEPT']) end end # Returns true if the request's "X-Requested-With" header contains # "XMLHttpRequest". (The Prototype Javascript library sends this header with # every Ajax request.) def xml_http_request? not /XMLHttpRequest/i.match(@env['HTTP_X_REQUESTED_WITH']).nil? end alias xhr? :xml_http_request? # Determine originating IP address. REMOTE_ADDR is the standard # but will fail if the user is behind a proxy. HTTP_CLIENT_IP and/or # HTTP_X_FORWARDED_FOR are set by proxies so check for these before # falling back to REMOTE_ADDR. HTTP_X_FORWARDED_FOR may be a comma- # delimited list in the case of multiple chained proxies; the first is # the originating IP. def remote_ip return @env['HTTP_CLIENT_IP'] if @env.include? 'HTTP_CLIENT_IP' if @env.include? 'HTTP_X_FORWARDED_FOR' then remote_ips = @env['HTTP_X_FORWARDED_FOR'].split(',').reject do |ip| ip =~ /^unknown$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\./i end return remote_ips.first.strip unless remote_ips.empty? end @env['REMOTE_ADDR'] end # Returns the domain part of a host, such as rubyonrails.org in "www.rubyonrails.org". You can specify # a different tld_length, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk". def domain(tld_length = 1) return nil if !/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/.match(host).nil? or host.nil? host.split('.').last(1 + tld_length).join('.') end # Returns all the subdomains as an array, so ["dev", "www"] would be returned for "dev.www.rubyonrails.org". # You can specify a different tld_length, such as 2 to catch ["www"] instead of ["www", "rubyonrails"] # in "www.rubyonrails.co.uk". def subdomains(tld_length = 1) return [] unless host parts = host.split('.') parts[0..-(tld_length+2)] end # Receive the raw post data. # This is useful for services such as REST, XMLRPC and SOAP # which communicate over HTTP POST but don't use the traditional parameter format. def raw_post @env['RAW_POST_DATA'] end # Returns the request URI correctly, taking into account the idiosyncracies # of the various servers. def request_uri if uri = @env['REQUEST_URI'] (%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri # Remove domain, which webrick puts into the request_uri. else # REQUEST_URI is blank under IIS - get this from PATH_INFO and SCRIPT_NAME script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$}) uri = @env['PATH_INFO'] uri = uri.sub(/#{script_filename}\//, '') unless script_filename.nil? unless (env_qs = @env['QUERY_STRING']).nil? || env_qs.empty? uri << '?' << env_qs end uri end end # Return 'https://' if this is an SSL request and 'http://' otherwise. def protocol ssl? ? 'https://' : 'http://' end # Is this an SSL request? def ssl? @env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https' end # Returns the interpreted path to requested resource after all the installation directory of this application was taken into account def path path = (uri = request_uri) ? uri.split('?').first : '' # Cut off the path to the installation directory if given root = relative_url_root path[0, root.length] = '' if root path || '' end # Returns the path minus the web server relative installation directory. # This can be set with the environment variable RAILS_RELATIVE_URL_ROOT. # It can be automatically extracted for Apache setups. If the server is not # Apache, this method returns an empty string. def relative_url_root @@relative_url_root ||= case when @env["RAILS_RELATIVE_URL_ROOT"] @env["RAILS_RELATIVE_URL_ROOT"] when server_software == 'apache' @env["SCRIPT_NAME"].to_s.sub(/\/dispatch\.(fcgi|rb|cgi)$/, '') else '' end end # Returns the port number of this request as an integer. def port @port_as_int ||= @env['SERVER_PORT'].to_i end # Returns the standard port number for this request's protocol def standard_port case protocol when 'https://' then 443 else 80 end end # Returns a port suffix like ":8080" if the port number of this request # is not the default HTTP port 80 or HTTPS port 443. def port_string (port == standard_port) ? '' : ":#{port}" end # Returns a host:port string for this request, such as example.com or # example.com:8080. def host_with_port host + port_string end def path_parameters=(parameters) #:nodoc: @path_parameters = parameters @symbolized_path_parameters = @parameters = nil end # The same as path_parameters with explicitly symbolized keys def symbolized_path_parameters @symbolized_path_parameters ||= path_parameters.symbolize_keys end # Returns a hash with the parameters used to form the path of the request # # Example: # # {:action => 'my_action', :controller => 'my_controller'} def path_parameters @path_parameters ||= {} end # Returns the lowercase name of the HTTP server software. def server_software (@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil end #-- # Must be implemented in the concrete request #++ def query_parameters #:nodoc: end def request_parameters #:nodoc: end # Returns the host for this request, such as example.com. def host end def cookies #:nodoc: end def session #:nodoc: end def session=(session) #:nodoc: @session = session end def reset_session #:nodoc: end end end module ActionController #:nodoc: # Actions that fail to perform as expected throw exceptions. These exceptions can either be rescued for the public view # (with a nice user-friendly explanation) or for the developers view (with tons of debugging information). The developers view # is already implemented by the Action Controller, but the public view should be tailored to your specific application. So too # could the decision on whether something is a public or a developer request. # # You can tailor the rescuing behavior and appearance by overwriting the following two stub methods. module Rescue def self.append_features(base) #:nodoc: super base.extend(ClassMethods) base.class_eval do alias_method :perform_action_without_rescue, :perform_action alias_method :perform_action, :perform_action_with_rescue end end module ClassMethods #:nodoc: def process_with_exception(request, response, exception) new.process(request, response, :rescue_action, exception) end end protected # Exception handler called when the performance of an action raises an exception. def rescue_action(exception) log_error(exception) if logger erase_results if performed? if consider_all_requests_local || local_request? rescue_action_locally(exception) else rescue_action_in_public(exception) end end # Overwrite to implement custom logging of errors. By default logs as fatal. def log_error(exception) #:doc: if ActionView::TemplateError === exception logger.fatal(exception.to_s) else logger.fatal( "\n\n#{exception.class} (#{exception.message}):\n " + clean_backtrace(exception).join("\n ") + "\n\n" ) end end # Overwrite to implement public exception handling (for requests answering false to local_request?). def rescue_action_in_public(exception) #:doc: case exception when RoutingError, UnknownAction then render_text(IO.read(File.join(RAILS_ROOT, 'public', '404.html')), "404 Not Found") else render_text "

Application error (Rails)

" end end # Overwrite to expand the meaning of a local request in order to show local rescues on other occurrences than # the remote IP being 127.0.0.1. For example, this could include the IP of the developer machine when debugging # remotely. def local_request? #:doc: [@request.remote_addr, @request.remote_ip] == ["127.0.0.1"] * 2 end # Renders a detailed diagnostics screen on action exceptions. def rescue_action_locally(exception) add_variables_to_assigns @template.instance_variable_set("@exception", exception) @template.instance_variable_set("@rescues_path", File.dirname(__FILE__) + "/templates/rescues/") @template.send(:assign_variables_from_controller) @template.instance_variable_set("@contents", @template.render_file(template_path_for_local_rescue(exception), false)) @headers["Content-Type"] = "text/html" render_file(rescues_path("layout"), response_code_for_rescue(exception)) end private def perform_action_with_rescue #:nodoc: begin perform_action_without_rescue rescue Object => exception if defined?(Breakpoint) && @params["BP-RETRY"] msg = exception.backtrace.first if md = /^(.+?):(\d+)(?::in `(.+)')?$/.match(msg) then origin_file, origin_line = md[1], md[2].to_i set_trace_func(lambda do |type, file, line, method, context, klass| if file == origin_file and line == origin_line then set_trace_func(nil) @params["BP-RETRY"] = false callstack = caller callstack.slice!(0) if callstack.first["rescue.rb"] file, line, method = *callstack.first.match(/^(.+?):(\d+)(?::in `(.*?)')?/).captures message = "Exception at #{file}:#{line}#{" in `#{method}'" if method}." # `´ ( for ruby-mode) Breakpoint.handle_breakpoint(context, message, file, line) end end) retry end end rescue_action(exception) end end def rescues_path(template_name) File.dirname(__FILE__) + "/templates/rescues/#{template_name}.rhtml" end def template_path_for_local_rescue(exception) rescues_path( case exception when MissingTemplate then "missing_template" when RoutingError then "routing_error" when UnknownAction then "unknown_action" when ActionView::TemplateError then "template_error" else "diagnostics" end ) end def response_code_for_rescue(exception) case exception when UnknownAction, RoutingError then "404 Page Not Found" else "500 Internal Error" end end def clean_backtrace(exception) exception.backtrace.collect { |line| Object.const_defined?(:RAILS_ROOT) ? line.gsub(RAILS_ROOT, "") : line } end end end module ActionController class AbstractResponse #:nodoc: DEFAULT_HEADERS = { "Cache-Control" => "no-cache" } attr_accessor :body, :headers, :session, :cookies, :assigns, :template, :redirected_to, :redirected_to_method_params def initialize @body, @headers, @session, @assigns = "", DEFAULT_HEADERS.merge("cookie" => []), [], [] end def redirect(to_url, permanently = false) @headers["Status"] = "302 Found" unless @headers["Status"] == "301 Moved Permanently" @headers["location"] = to_url @body = "You are being redirected." end end endmodule ActionController module Routing #:nodoc: class << self def expiry_hash(options, recall) k = v = nil expire_on = {} options.each {|k, v| expire_on[k] = ((rcv = recall[k]) && (rcv != v))} expire_on end def extract_parameter_value(parameter) #:nodoc: CGI.escape((parameter.respond_to?(:to_param) ? parameter.to_param : parameter).to_s) end def controller_relative_to(controller, previous) if controller.nil? then previous elsif controller[0] == ?/ then controller[1..-1] elsif %r{^(.*)/} =~ previous then "#{$1}/#{controller}" else controller end end def treat_hash(hash, keys_to_delete = []) k = v = nil hash.each do |k, v| if v then hash[k] = (v.respond_to? :to_param) ? v.to_param.to_s : v.to_s else hash.delete k keys_to_delete << k end end hash end def test_condition(expression, condition) case condition when String then "(#{expression} == #{condition.inspect})" when Regexp then condition = Regexp.new("^#{condition.source}$") unless /^\^.*\$$/ =~ condition.source "(#{condition.inspect} =~ #{expression})" when Array then conds = condition.collect do |condition| cond = test_condition(expression, condition) (cond[0, 1] == '(' && cond[-1, 1] == ')') ? cond : "(#{cond})" end "(#{conds.join(' || ')})" when true then expression when nil then "! #{expression}" else raise ArgumentError, "Valid criteria are strings, regular expressions, true, or nil" end end end class Component #:nodoc: def dynamic?() false end def optional?() false end def key() nil end def self.new(string, *args) return super(string, *args) unless self == Component case string when ':controller' then ControllerComponent.new(:controller, *args) when /^:(\w+)$/ then DynamicComponent.new($1, *args) when /^\*(\w+)$/ then PathComponent.new($1, *args) else StaticComponent.new(string, *args) end end end class StaticComponent < Component #:nodoc: attr_reader :value def initialize(value) @value = value end def write_recognition(g) g.if_next_matches(value) do |gp| gp.move_forward {|gpp| gpp.continue} end end def write_generation(g) g.add_segment(value) {|gp| gp.continue } end end class DynamicComponent < Component #:nodoc: attr_reader :key, :default attr_accessor :condition def dynamic?() true end def optional?() @optional end def default=(default) @optional = true @default = default end def initialize(key, options = {}) @key = key.to_sym @optional = false default, @condition = options[:default], options[:condition] self.default = default if options.key?(:default) end def default_check(g) presence = "#{g.hash_value(key, !! default)}" if default "!(#{presence} && #{g.hash_value(key, false)} != #{default.to_s.inspect})" else "! #{presence}" end end def write_generation(g) wrote_dropout = write_dropout_generation(g) write_continue_generation(g, wrote_dropout) end def write_dropout_generation(g) return false unless optional? && g.after.all? {|c| c.optional?} check = [default_check(g)] gp = g.dup # Use another generator to write the conditions after the first && # We do this to ensure that the generator will not assume x_value is set. It will # not be set if it follows a false condition -- for example, false && (x = 2) check += gp.after.map {|c| c.default_check gp} gp.if(check.join(' && ')) { gp.finish } # If this condition is met, we stop here true end def write_continue_generation(g, use_else) test = Routing.test_condition(g.hash_value(key, true, default), condition || true) check = (use_else && condition.nil? && default) ? [:else] : [use_else ? :elsif : :if, test] g.send(*check) do |gp| gp.expire_for_keys(key) unless gp.after.empty? add_segments_to(gp) {|gpp| gpp.continue} end end def add_segments_to(g) g.add_segment(%(\#{CGI.escape(#{g.hash_value(key, true, default)})})) {|gp| yield gp} end def recognition_check(g) test_type = [true, nil].include?(condition) ? :presence : :constraint prefix = condition.is_a?(Regexp) ? "#{g.next_segment(true)} && " : '' check = prefix + Routing.test_condition(g.next_segment(true), condition || true) g.if(check) {|gp| yield gp, test_type} end def write_recognition(g) test_type = nil recognition_check(g) do |gp, test_type| assign_result(gp) {|gpp| gpp.continue} end if optional? && g.after.all? {|c| c.optional?} call = (test_type == :presence) ? [:else] : [:elsif, "! #{g.next_segment(true)}"] g.send(*call) do |gp| assign_default(gp) gp.after.each {|c| c.assign_default(gp)} gp.finish(false) end end end def assign_result(g, with_default = false) g.result key, "CGI.unescape(#{g.next_segment(true, with_default ? default : nil)})" g.move_forward {|gp| yield gp} end def assign_default(g) g.constant_result key, default unless default.nil? end end class ControllerComponent < DynamicComponent #:nodoc: def key() :controller end def add_segments_to(g) g.add_segment(%(\#{#{g.hash_value(key, true, default)}})) {|gp| yield gp} end def recognition_check(g) g << "controller_result = ::ActionController::Routing::ControllerComponent.traverse_to_controller(#{g.path_name}, #{g.index_name})" g.if('controller_result') do |gp| gp << 'controller_value, segments_to_controller = controller_result' if condition gp << "controller_path = #{gp.path_name}[#{gp.index_name},segments_to_controller].join('/')" gp.if(Routing.test_condition("controller_path", condition)) do |gpp| gpp.move_forward('segments_to_controller') {|gppp| yield gppp, :constraint} end else gp.move_forward('segments_to_controller') {|gpp| yield gpp, :constraint} end end end def assign_result(g) g.result key, 'controller_value' yield g end def assign_default(g) ControllerComponent.assign_controller(g, default) end class << self def assign_controller(g, controller) expr = "::#{controller.split('/').collect {|c| c.camelize}.join('::')}Controller" g.result :controller, expr, true end def traverse_to_controller(segments, start_at = 0) mod = ::Object length = segments.length index = start_at mod_name = controller_name = segment = nil while index < length return nil unless /\A[A-Za-z][A-Za-z\d_]*\Z/ =~ (segment = segments[index]) index += 1 mod_name = segment.camelize controller_name = "#{mod_name}Controller" path_suffix = File.join(segments[start_at..(index - 1)]) next_mod = nil # If the controller is already present, or if we load it, return it. if mod.const_defined?(controller_name) || attempt_load(mod, controller_name, path_suffix + "_controller") == :defined controller = mod.const_get(controller_name) return nil unless controller.is_a?(Class) && controller.ancestors.include?(ActionController::Base) # it's not really a controller? return [controller, (index - start_at)] end # No controller? Look for the module if mod.const_defined? mod_name next_mod = mod.send(:const_get, mod_name) next_mod = nil unless next_mod.is_a?(Module) else # Try to load a file that defines the module we want. case attempt_load(mod, mod_name, path_suffix) when :defined then next_mod = mod.const_get mod_name when :dir then # We didn't find a file, but there's a dir. next_mod = Module.new # So create a module for the directory mod.send :const_set, mod_name, next_mod else return nil end end mod = next_mod return nil unless mod && mod.is_a?(Module) end nil end protected def safe_load_paths #:nodoc: if defined?(RAILS_ROOT) $LOAD_PATH.select do |base| base = File.expand_path(base) extended_root = File.expand_path(RAILS_ROOT) # Exclude all paths that are not nested within app, lib, or components. base.match(/\A#{Regexp.escape(extended_root)}\/*(app|lib|components)\/[a-z]/) || base =~ %r{rails-[\d.]+/builtin} end else $LOAD_PATH end end def attempt_load(mod, const_name, path) has_dir = false safe_load_paths.each do |load_path| full_path = File.join(load_path, path) file_path = full_path + '.rb' if File.file?(file_path) # Found a .rb file? Load it up require_dependency(file_path) return :defined if mod.const_defined? const_name else has_dir ||= File.directory?(full_path) end end return (has_dir ? :dir : nil) end end end class PathComponent < DynamicComponent #:nodoc: def optional?() true end def default() [] end def condition() nil end def default=(value) raise RoutingError, "All path components have an implicit default of []" unless value == [] end def write_generation(g) raise RoutingError, 'Path components must occur last' unless g.after.empty? g.if("#{g.hash_value(key, true)} && ! #{g.hash_value(key, true)}.empty?") do g << "#{g.hash_value(key, true)} = #{g.hash_value(key, true)}.join('/') unless #{g.hash_value(key, true)}.is_a?(String)" g.add_segment("\#{CGI.escape_skipping_slashes(#{g.hash_value(key, true)})}") {|gp| gp.finish } end g.else { g.finish } end def write_recognition(g) raise RoutingError, "Path components must occur last" unless g.after.empty? start = g.index_name start = "(#{start})" unless /^\w+$/ =~ start value_expr = "#{g.path_name}[#{start}..-1] || []" g.result key, "ActionController::Routing::PathComponent::Result.new_escaped(#{value_expr})" g.finish(false) end class Result < ::Array #:nodoc: def to_s() join '/' end def self.new_escaped(strings) new strings.collect {|str| CGI.unescape str} end end end class Route #:nodoc: attr_accessor :components, :known attr_reader :path, :options, :keys, :defaults def initialize(path, options = {}) @path, @options = path, options initialize_components path defaults, conditions = initialize_hashes options.dup @defaults = defaults.dup configure_components(defaults, conditions) add_default_requirements initialize_keys end def inspect "<#{self.class} #{path.inspect}, #{options.inspect[1..-1]}>" end def write_generation(generator = CodeGeneration::GenerationGenerator.new) generator.before, generator.current, generator.after = [], components.first, (components[1..-1] || []) if known.empty? then generator.go else # Alter the conditions to allow :action => 'index' to also catch :action => nil altered_known = known.collect do |k, v| if k == :action && v== 'index' then [k, [nil, 'index']] else [k, v] end end generator.if(generator.check_conditions(altered_known)) {|gp| gp.go } end generator end def write_recognition(generator = CodeGeneration::RecognitionGenerator.new) g = generator.dup g.share_locals_with generator g.before, g.current, g.after = [], components.first, (components[1..-1] || []) known.each do |key, value| if key == :controller then ControllerComponent.assign_controller(g, value) else g.constant_result(key, value) end end g.go generator end def initialize_keys @keys = (components.collect {|c| c.key} + known.keys).compact @keys.freeze end def extra_keys(options) options.keys - @keys end def matches_controller?(controller) if known[:controller] then known[:controller] == controller else c = components.find {|c| c.key == :controller} return false unless c return c.condition.nil? || eval(Routing.test_condition('controller', c.condition)) end end protected def initialize_components(path) path = path.split('/') if path.is_a? String path.shift if path.first.blank? self.components = path.collect {|str| Component.new str} end def initialize_hashes(options) path_keys = components.collect {|c| c.key }.compact self.known = {} defaults = options.delete(:defaults) || {} conditions = options.delete(:require) || {} conditions.update(options.delete(:requirements) || {}) options.each do |k, v| if path_keys.include?(k) then (v.is_a?(Regexp) ? conditions : defaults)[k] = v else known[k] = v end end [defaults, conditions] end def configure_components(defaults, conditions) components.each do |component| if defaults.key?(component.key) then component.default = defaults[component.key] elsif component.key == :action then component.default = 'index' elsif component.key == :id then component.default = nil end component.condition = conditions[component.key] if conditions.key?(component.key) end end def add_default_requirements component_keys = components.collect {|c| c.key} known[:action] ||= 'index' unless component_keys.include? :action end end class RouteSet #:nodoc: attr_reader :routes, :categories, :controller_to_selector def initialize @routes = [] @generation_methods = Hash.new(:generate_default_path) end def generate(options, request_or_recall_hash = {}) recall = request_or_recall_hash.is_a?(Hash) ? request_or_recall_hash : request_or_recall_hash.symbolized_path_parameters use_recall = true controller = options[:controller] options[:action] ||= 'index' if controller recall_controller = recall[:controller] if (recall_controller && recall_controller.include?(?/)) || (controller && controller.include?(?/)) recall = {} if controller && controller[0] == ?/ options[:controller] = Routing.controller_relative_to(controller, recall_controller) end options = recall.dup if options.empty? # XXX move to url_rewriter? keys_to_delete = [] Routing.treat_hash(options, keys_to_delete) merged = recall.merge(options) keys_to_delete.each {|key| merged.delete key} expire_on = Routing.expiry_hash(options, recall) generate_path(merged, options, expire_on) end def generate_path(merged, options, expire_on) send @generation_methods[merged[:controller]], merged, options, expire_on end def generate_default_path(*args) write_generation generate_default_path(*args) end def write_generation method_sources = [] @generation_methods = Hash.new(:generate_default_path) categorize_routes.each do |controller, routes| next unless routes.length < @routes.length ivar = controller.gsub('/', '__') method_name = "generate_path_for_#{ivar}".to_sym instance_variable_set "@#{ivar}", routes code = generation_code_for(ivar, method_name).to_s method_sources << code filename = "generated_code/routing/generation_for_controller_#{controller}.rb" eval(code, nil, filename) @generation_methods[controller.to_s] = method_name @generation_methods[controller.to_sym] = method_name end code = generation_code_for('routes', 'generate_default_path').to_s eval(code, nil, 'generated_code/routing/generation.rb') return (method_sources << code) end def recognize(request) string_path = request.path string_path.chomp! if string_path[0] == ?/ path = string_path.split '/' path.shift hash = recognize_path(path) return recognition_failed(request) unless hash && hash['controller'] controller = hash['controller'] hash['controller'] = controller.controller_path request.path_parameters = hash controller.new end alias :recognize! :recognize def recognition_failed(request) raise ActionController::RoutingError, "Recognition failed for #{request.path.inspect}" end def write_recognition g = generator = CodeGeneration::RecognitionGenerator.new g.finish_statement = Proc.new {|hash_expr| "return #{hash_expr}"} g.def "self.recognize_path(path)" do each do |route| g << 'index = 0' route.write_recognition(g) end end eval g.to_s, nil, 'generated/routing/recognition.rb' return g.to_s end def generation_code_for(ivar = 'routes', method_name = nil) routes = instance_variable_get('@' + ivar) key_ivar = "@keys_for_#{ivar}" instance_variable_set(key_ivar, routes.collect {|route| route.keys}) g = generator = CodeGeneration::GenerationGenerator.new g.def "self.#{method_name}(merged, options, expire_on)" do g << 'unused_count = options.length + 1' g << "unused_keys = keys = options.keys" g << 'path = nil' routes.each_with_index do |route, index| g << "new_unused_keys = keys - #{key_ivar}[#{index}]" g << 'new_path = (' g.source.indent do if index.zero? g << "new_unused_count = new_unused_keys.length" g << "hash = merged; not_expired = true" route.write_generation(g.dup) else g.if "(new_unused_count = new_unused_keys.length) < unused_count" do |gp| gp << "hash = merged; not_expired = true" route.write_generation(gp) end end end g.source.lines.last << ' )' # Add the closing brace to the end line g.if 'new_path' do g << 'return new_path, [] if new_unused_count.zero?' g << 'path = new_path; unused_keys = new_unused_keys; unused_count = new_unused_count' end end g << "raise RoutingError, \"No url can be generated for the hash \#{options.inspect}\" unless path" g << "return path, unused_keys" end return g end def categorize_routes @categorized_routes = by_controller = Hash.new(self) known_controllers.each do |name| set = by_controller[name] = [] each do |route| set << route if route.matches_controller? name end end @categorized_routes end def known_controllers @routes.inject([]) do |known, route| if (controller = route.known[:controller]) if controller.is_a?(Regexp) known << controller.source.scan(%r{[\w\d/]+}).select {|word| controller =~ word} else known << controller end end known end.uniq end def reload NamedRoutes.clear if defined?(RAILS_ROOT) then load(File.join(RAILS_ROOT, 'config', 'routes.rb')) else connect(':controller/:action/:id', :action => 'index', :id => nil) end NamedRoutes.install end def connect(*args) new_route = Route.new(*args) @routes << new_route return new_route end def draw old_routes = @routes @routes = [] begin yield self rescue @routes = old_routes raise end write_generation write_recognition end def empty?() @routes.empty? end def each(&block) @routes.each(&block) end # Defines a new named route with the provided name and arguments. # This method need only be used when you wish to use a name that a RouteSet instance # method exists for, such as categories. # # For example, map.categories '/categories', :controller => 'categories' will not work # due to RouteSet#categories. def named_route(name, path, hash = {}) route = connect(path, hash) NamedRoutes.name_route(route, name) route end def method_missing(name, *args) (1..2).include?(args.length) ? named_route(name, *args) : super(name, *args) end def extra_keys(options, recall = {}) generate(options.dup, recall).last end end module NamedRoutes #:nodoc: Helpers = [] class << self def clear() Helpers.clear end def hash_access_name(name) "hash_for_#{name}_url" end def url_helper_name(name) "#{name}_url" end def known_hash_for_route(route) hash = route.known.symbolize_keys route.defaults.each do |key, value| hash[key.to_sym] ||= value if value end hash[:controller] = "/#{hash[:controller]}" hash end def define_hash_access_method(route, name) hash = known_hash_for_route(route) define_method(hash_access_name(name)) do |*args| args.first ? hash.merge(args.first) : hash end end def name_route(route, name) define_hash_access_method(route, name) module_eval(%{def #{url_helper_name name}(options = {}) url_for(#{hash_access_name(name)}.merge(options)) end}, "generated/routing/named_routes/#{name}.rb") protected url_helper_name(name), hash_access_name(name) Helpers << url_helper_name(name).to_sym Helpers << hash_access_name(name).to_sym Helpers.uniq! end def install(cls = ActionController::Base) cls.send :include, self if cls.respond_to? :helper_method Helpers.each do |helper_name| cls.send :helper_method, helper_name end end end end end Routes = RouteSet.new end end module ActionController module Scaffolding # :nodoc: def self.append_features(base) super base.extend(ClassMethods) end # Scaffolding is a way to quickly put an Active Record class online by providing a series of standardized actions # for listing, showing, creating, updating, and destroying objects of the class. These standardized actions come # with both controller logic and default templates that through introspection already know which fields to display # and which input types to use. Example: # # class WeblogController < ActionController::Base # scaffold :entry # end # # This tiny piece of code will add all of the following methods to the controller: # # class WeblogController < ActionController::Base # verify :method => :post, :only => [ :destroy, :create, :update ], # :redirect_to => { :action => :list } # # def index # list # end # # def list # @entries = Entry.find_all # render_scaffold "list" # end # # def show # @entry = Entry.find(params[:id]) # render_scaffold # end # # def destroy # Entry.find(params[:id]).destroy # redirect_to :action => "list" # end # # def new # @entry = Entry.new # render_scaffold # end # # def create # @entry = Entry.new(params[:entry]) # if @entry.save # flash[:notice] = "Entry was successfully created" # redirect_to :action => "list" # else # render_scaffold('new') # end # end # # def edit # @entry = Entry.find(params[:id]) # render_scaffold # end # # def update # @entry = Entry.find(params[:id]) # @entry.attributes = params[:entry] # # if @entry.save # flash[:notice] = "Entry was successfully updated" # redirect_to :action => "show", :id => @entry # else # render_scaffold('edit') # end # end # end # # The render_scaffold method will first check to see if you've made your own template (like "weblog/show.rhtml" for # the show action) and if not, then render the generic template for that action. This gives you the possibility of using the # scaffold while you're building your specific application. Start out with a totally generic setup, then replace one template # and one action at a time while relying on the rest of the scaffolded templates and actions. module ClassMethods # Adds a swath of generic CRUD actions to the controller. The +model_id+ is automatically converted into a class name unless # one is specifically provide through options[:class_name]. So scaffold :post would use Post as the class # and @post/@posts for the instance variables. # # It's possible to use more than one scaffold in a single controller by specifying options[:suffix] = true. This will # make scaffold :post, :suffix => true use method names like list_post, show_post, and create_post # instead of just list, show, and post. If suffix is used, then no index method is added. def scaffold(model_id, options = {}) options.assert_valid_keys(:class_name, :suffix) singular_name = model_id.to_s class_name = options[:class_name] || singular_name.camelize plural_name = singular_name.pluralize suffix = options[:suffix] ? "_#{singular_name}" : "" unless options[:suffix] module_eval <<-"end_eval", __FILE__, __LINE__ def index list end end_eval end module_eval <<-"end_eval", __FILE__, __LINE__ verify :method => :post, :only => [ :destroy#{suffix}, :create#{suffix}, :update#{suffix} ], :redirect_to => { :action => :list#{suffix} } def list#{suffix} @#{singular_name}_pages, @#{plural_name} = paginate :#{plural_name}, :per_page => 10 render#{suffix}_scaffold "list#{suffix}" end def show#{suffix} @#{singular_name} = #{class_name}.find(params[:id]) render#{suffix}_scaffold end def destroy#{suffix} #{class_name}.find(params[:id]).destroy redirect_to :action => "list#{suffix}" end def new#{suffix} @#{singular_name} = #{class_name}.new render#{suffix}_scaffold end def create#{suffix} @#{singular_name} = #{class_name}.new(params[:#{singular_name}]) if @#{singular_name}.save flash[:notice] = "#{class_name} was successfully created" redirect_to :action => "list#{suffix}" else render#{suffix}_scaffold('new') end end def edit#{suffix} @#{singular_name} = #{class_name}.find(params[:id]) render#{suffix}_scaffold end def update#{suffix} @#{singular_name} = #{class_name}.find(params[:id]) @#{singular_name}.attributes = params[:#{singular_name}] if @#{singular_name}.save flash[:notice] = "#{class_name} was successfully updated" redirect_to :action => "show#{suffix}", :id => @#{singular_name} else render#{suffix}_scaffold('edit') end end private def render#{suffix}_scaffold(action=nil) action ||= caller_method_name(caller) # logger.info ("testing template:" + "\#{self.class.controller_path}/\#{action}") if logger if template_exists?("\#{self.class.controller_path}/\#{action}") render_action(action) else @scaffold_class = #{class_name} @scaffold_singular_name, @scaffold_plural_name = "#{singular_name}", "#{plural_name}" @scaffold_suffix = "#{suffix}" add_instance_variables_to_assigns @template.instance_variable_set("@content_for_layout", @template.render_file(scaffold_path(action.sub(/#{suffix}$/, "")), false)) if !active_layout.nil? render_file(active_layout, nil, true) else render_file(scaffold_path("layout")) end end end def scaffold_path(template_name) File.dirname(__FILE__) + "/templates/scaffolds/" + template_name + ".rhtml" end def caller_method_name(caller) caller.first.scan(/`(.*)'/).first.first # ' ruby-mode end end_eval end end end end require 'cgi' require 'cgi/session' require 'digest/md5' require 'base64' class CGI class Session # Return this session's underlying Session instance. Useful for the DB-backed session stores. def model @dbman.model if @dbman end # A session store backed by an Active Record class. A default class is # provided, but any object duck-typing to an Active Record +Session+ class # with text +session_id+ and +data+ attributes is sufficient. # # The default assumes a +sessions+ tables with columns: # +id+ (numeric primary key), # +session_id+ (text, or longtext if your session data exceeds 65K), and # +data+ (text or longtext; careful if your session data exceeds 65KB). # The +session_id+ column should always be indexed for speedy lookups. # Session data is marshaled to the +data+ column in Base64 format. # If the data you write is larger than the column's size limit, # ActionController::SessionOverflowError will be raised. # # You may configure the table name, primary key, and data column. # For example, at the end of config/environment.rb: # CGI::Session::ActiveRecordStore::Session.table_name = 'legacy_session_table' # CGI::Session::ActiveRecordStore::Session.primary_key = 'session_id' # CGI::Session::ActiveRecordStore::Session.data_column_name = 'legacy_session_data' # Note that setting the primary key to the session_id frees you from # having a separate id column if you don't want it. However, you must # set session.model.id = session.session_id by hand! A before_filter # on ApplicationController is a good place. # # Since the default class is a simple Active Record, you get timestamps # for free if you add +created_at+ and +updated_at+ datetime columns to # the +sessions+ table, making periodic session expiration a snap. # # You may provide your own session class implementation, whether a # feature-packed Active Record or a bare-metal high-performance SQL # store, by setting # +CGI::Session::ActiveRecordStore.session_class = MySessionClass+ # You must implement these methods: # self.find_by_session_id(session_id) # initialize(hash_of_session_id_and_data) # attr_reader :session_id # attr_accessor :data # save # destroy # # The example SqlBypass class is a generic SQL session store. You may # use it as a basis for high-performance database-specific stores. class ActiveRecordStore # The default Active Record class. class Session < ActiveRecord::Base # Customizable data column name. Defaults to 'data'. cattr_accessor :data_column_name self.data_column_name = 'data' before_save :marshal_data! before_save :raise_on_session_data_overflow! class << self # Don't try to reload ARStore::Session in dev mode. def reloadable? #:nodoc: false end def data_column_size_limit @data_column_size_limit ||= columns_hash[@@data_column_name].limit end # Hook to set up sessid compatibility. def find_by_session_id(session_id) setup_sessid_compatibility! find_by_session_id(session_id) end def marshal(data) Base64.encode64(Marshal.dump(data)) if data end def unmarshal(data) Marshal.load(Base64.decode64(data)) if data end def create_table! connection.execute <<-end_sql CREATE TABLE #{table_name} ( id INTEGER PRIMARY KEY, #{connection.quote_column_name('session_id')} TEXT UNIQUE, #{connection.quote_column_name(@@data_column_name)} TEXT(255) ) end_sql end def drop_table! connection.execute "DROP TABLE #{table_name}" end private # Compatibility with tables using sessid instead of session_id. def setup_sessid_compatibility! # Reset column info since it may be stale. reset_column_information if columns_hash['sessid'] def self.find_by_session_id(*args) find_by_sessid(*args) end define_method(:session_id) { sessid } define_method(:session_id=) { |session_id| self.sessid = session_id } else def self.find_by_session_id(session_id) find :first, :conditions => ["session_id #{attribute_condition(session_id)}", session_id] end end end end # Lazy-unmarshal session state. def data @data ||= self.class.unmarshal(read_attribute(@@data_column_name)) || {} end # Has the session been loaded yet? def loaded? !! @data end private attr_writer :data def marshal_data! return false if !loaded? write_attribute(@@data_column_name, self.class.marshal(self.data)) end # Ensures that the data about to be stored in the database is not # larger than the data storage column. Raises # ActionController::SessionOverflowError. def raise_on_session_data_overflow! return false if !loaded? limit = self.class.data_column_size_limit if loaded? and limit and read_attribute(@@data_column_name).size > limit raise ActionController::SessionOverflowError end end end # A barebones session store which duck-types with the default session # store but bypasses Active Record and issues SQL directly. This is # an example session model class meant as a basis for your own classes. # # The database connection, table name, and session id and data columns # are configurable class attributes. Marshaling and unmarshaling # are implemented as class methods that you may override. By default, # marshaling data is +Base64.encode64(Marshal.dump(data))+ and # unmarshaling data is +Marshal.load(Base64.decode64(data))+. # # This marshaling behavior is intended to store the widest range of # binary session data in a +text+ column. For higher performance, # store in a +blob+ column instead and forgo the Base64 encoding. class SqlBypass # Use the ActiveRecord::Base.connection by default. cattr_accessor :connection # The table name defaults to 'sessions'. cattr_accessor :table_name @@table_name = 'sessions' # The session id field defaults to 'session_id'. cattr_accessor :session_id_column @@session_id_column = 'session_id' # The data field defaults to 'data'. cattr_accessor :data_column @@data_column = 'data' class << self def connection @@connection ||= ActiveRecord::Base.connection end # Look up a session by id and unmarshal its data if found. def find_by_session_id(session_id) if record = @@connection.select_one("SELECT * FROM #{@@table_name} WHERE #{@@session_id_column}=#{@@connection.quote(session_id)}") new(:session_id => session_id, :marshaled_data => record['data']) end end def marshal(data) Base64.encode64(Marshal.dump(data)) if data end def unmarshal(data) Marshal.load(Base64.decode64(data)) if data end def create_table! @@connection.execute <<-end_sql CREATE TABLE #{table_name} ( id INTEGER PRIMARY KEY, #{@@connection.quote_column_name(session_id_column)} TEXT UNIQUE, #{@@connection.quote_column_name(data_column)} TEXT ) end_sql end def drop_table! @@connection.execute "DROP TABLE #{table_name}" end end attr_reader :session_id attr_writer :data # Look for normal and marshaled data, self.find_by_session_id's way of # telling us to postpone unmarshaling until the data is requested. # We need to handle a normal data attribute in case of a new record. def initialize(attributes) @session_id, @data, @marshaled_data = attributes[:session_id], attributes[:data], attributes[:marshaled_data] @new_record = @marshaled_data.nil? end def new_record? @new_record end # Lazy-unmarshal session state. def data unless @data if @marshaled_data @data, @marshaled_data = self.class.unmarshal(@marshaled_data) || {}, nil else @data = {} end end @data end def loaded? !! @data end def save return false if !loaded? marshaled_data = self.class.marshal(data) if @new_record @new_record = false @@connection.update <<-end_sql, 'Create session' INSERT INTO #{@@table_name} ( #{@@connection.quote_column_name(@@session_id_column)}, #{@@connection.quote_column_name(@@data_column)} ) VALUES ( #{@@connection.quote(session_id)}, #{@@connection.quote(marshaled_data)} ) end_sql else @@connection.update <<-end_sql, 'Update session' UPDATE #{@@table_name} SET #{@@connection.quote_column_name(@@data_column)}=#{@@connection.quote(marshaled_data)} WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)} end_sql end end def destroy unless @new_record @@connection.delete <<-end_sql, 'Destroy session' DELETE FROM #{@@table_name} WHERE #{@@connection.quote_column_name(@@session_id_column)}=#{@@connection.quote(session_id)} end_sql end end end # The class used for session storage. Defaults to # CGI::Session::ActiveRecordStore::Session. cattr_accessor :session_class self.session_class = Session # Find or instantiate a session given a CGI::Session. def initialize(session, option = nil) session_id = session.session_id unless @session = ActiveRecord::Base.silence { @@session_class.find_by_session_id(session_id) } unless session.new_session raise CGI::Session::NoSession, 'uninitialized session' end @session = @@session_class.new(:session_id => session_id, :data => {}) # session saving can be lazy again, because of improved component implementation # therefore next line gets commented out: # @session.save end end # Access the underlying session model. def model @session end # Restore session state. The session model handles unmarshaling. def restore if @session @session.data end end # Save session store. def update if @session ActiveRecord::Base.silence { @session.save } end end # Save and close the session store. def close if @session update @session = nil end end # Delete and close the session store. def delete if @session ActiveRecord::Base.silence { @session.destroy } @session = nil end end protected def logger ActionController::Base.logger rescue nil end end end end #!/usr/local/bin/ruby -w # This is a really simple session storage daemon, basically just a hash, # which is enabled for DRb access. require 'drb' session_hash = Hash.new session_hash.instance_eval { @mutex = Mutex.new } class <:only and # :except clauses to restrict the subset, otherwise options # apply to all actions on this controller. # # The session options are inheritable, as well, so if you specify them in # a parent controller, they apply to controllers that extend the parent. # # Usage: # # # turn off session management for all actions. # session :off # # # turn off session management for all actions _except_ foo and bar. # session :off, :except => %w(foo bar) # # # turn off session management for only the foo and bar actions. # session :off, :only => %w(foo bar) # # # the session will only work over HTTPS, but only for the foo action # session :only => :foo, :session_secure => true # # # the session will only be disabled for 'foo', and only if it is # # requested as a web service # session :off, :only => :foo, # :if => Proc.new { |req| req.parameters[:ws] } # # All session options described for ActionController::Base.process_cgi # are valid arguments. def session(*args) options = Hash === args.last ? args.pop : {} options[:disabled] = true if !args.empty? options[:only] = [*options[:only]].map { |o| o.to_s } if options[:only] options[:except] = [*options[:except]].map { |o| o.to_s } if options[:except] if options[:only] && options[:except] raise ArgumentError, "only one of either :only or :except are allowed" end write_inheritable_array("session_options", [options]) end def cached_session_options #:nodoc: @session_options ||= read_inheritable_attribute("session_options") || [] end def session_options_for(request, action) #:nodoc: if (session_options = cached_session_options).empty? {} else options = {} action = action.to_s session_options.each do |opts| next if opts[:if] && !opts[:if].call(request) if opts[:only] && opts[:only].include?(action) options.merge!(opts) elsif opts[:except] && !opts[:except].include?(action) options.merge!(opts) elsif !opts[:only] && !opts[:except] options.merge!(opts) end end if options.empty? then options else options.delete :only options.delete :except options.delete :if options[:disabled] ? false : options end end end end def process_with_session_management_support(request, response, method = :perform_action, *arguments) #:nodoc: set_session_options(request) process_without_session_management_support(request, response, method, *arguments) end private def set_session_options(request) request.session_options = self.class.session_options_for(request, request.parameters["action"] || "index") end def process_cleanup_with_session_management_support process_cleanup_without_session_management_support clear_persistent_model_associations end # Clear cached associations in session data so they don't overflow # the database field. Only applies to ActiveRecordStore since there # is not a standard way to iterate over session data. def clear_persistent_model_associations #:doc: if defined?(@session) && @session.instance_variables.include?('@data') session_data = @session.instance_variable_get('@data') if session_data && session_data.respond_to?(:each_value) session_data.each_value do |obj| obj.clear_association_cache if obj.respond_to?(:clear_association_cache) end end end end end end module ActionController #:nodoc: # Methods for sending files and streams to the browser instead of rendering. module Streaming DEFAULT_SEND_FILE_OPTIONS = { :type => 'application/octet-stream'.freeze, :disposition => 'attachment'.freeze, :stream => true, :buffer_size => 4096 }.freeze protected # Sends the file by streaming it 4096 bytes at a time. This way the # whole file doesn't need to be read into memory at once. This makes # it feasible to send even large files. # # Be careful to sanitize the path parameter if it coming from a web # page. send_file(params[:path]) allows a malicious user to # download any file on your server. # # Options: # * :filename - suggests a filename for the browser to use. # Defaults to File.basename(path). # * :type - specifies an HTTP content type. # Defaults to 'application/octet-stream'. # * :disposition - specifies whether the file will be shown inline or downloaded. # Valid values are 'inline' and 'attachment' (default). # * :stream - whether to send the file to the user agent as it is read (true) # or to read the entire file before sending (false). Defaults to true. # * :buffer_size - specifies size (in bytes) of the buffer used to stream the file. # Defaults to 4096. # * :status - specifies the status code to send with the response. Defaults to '200 OK'. # # The default Content-Type and Content-Disposition headers are # set to download arbitrary binary files in as many browsers as # possible. IE versions 4, 5, 5.5, and 6 are all known to have # a variety of quirks (especially when downloading over SSL). # # Simple download: # send_file '/path/to.zip' # # Show a JPEG in the browser: # send_file '/path/to.jpeg', :type => 'image/jpeg', :disposition => 'inline' # # Show a 404 page in the browser: # send_file '/path/to/404.html, :type => 'text/html; charset=utf-8', :status => 404 # # Read about the other Content-* HTTP headers if you'd like to # provide the user with more information (such as Content-Description). # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11 # # Also be aware that the document may be cached by proxies and browsers. # The Pragma and Cache-Control headers declare how the file may be cached # by intermediaries. They default to require clients to validate with # the server before releasing cached responses. See # http://www.mnot.net/cache_docs/ for an overview of web caching and # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9 # for the Cache-Control header spec. def send_file(path, options = {}) #:doc: raise MissingFile, "Cannot read file #{path}" unless File.file?(path) and File.readable?(path) options[:length] ||= File.size(path) options[:filename] ||= File.basename(path) send_file_headers! options @performed_render = false if options[:stream] render :status => options[:status], :text => Proc.new { |response, output| logger.info "Streaming file #{path}" unless logger.nil? len = options[:buffer_size] || 4096 File.open(path, 'rb') do |file| if output.respond_to?(:syswrite) begin while true output.syswrite(file.sysread(len)) end rescue EOFError end else while buf = file.read(len) output.write(buf) end end end } else logger.info "Sending file #{path}" unless logger.nil? File.open(path, 'rb') { |file| render :status => options[:status], :text => file.read } end end # Send binary data to the user as a file download. May set content type, apparent file name, # and specify whether to show data inline or download as an attachment. # # Options: # * :filename - Suggests a filename for the browser to use. # * :type - specifies an HTTP content type. # Defaults to 'application/octet-stream'. # * :disposition - specifies whether the file will be shown inline or downloaded. # * :status - specifies the status code to send with the response. Defaults to '200 OK'. # Valid values are 'inline' and 'attachment' (default). # # Generic data download: # send_data buffer # # Download a dynamically-generated tarball: # send_data generate_tgz('dir'), :filename => 'dir.tgz' # # Display an image Active Record in the browser: # send_data image.data, :type => image.content_type, :disposition => 'inline' # # See +send_file+ for more information on HTTP Content-* headers and caching. def send_data(data, options = {}) #:doc: logger.info "Sending data #{options[:filename]}" unless logger.nil? send_file_headers! options.merge(:length => data.size) @performed_render = false render :status => options[:status], :text => data end private def send_file_headers!(options) options.update(DEFAULT_SEND_FILE_OPTIONS.merge(options)) [:length, :type, :disposition].each do |arg| raise ArgumentError, ":#{arg} option required" if options[arg].nil? end disposition = options[:disposition].dup || 'attachment' disposition <<= %(; filename="#{options[:filename]}") if options[:filename] @headers.update( 'Content-Length' => options[:length], 'Content-Type' => options[:type].strip, # fixes a problem with extra '\r' with some browsers 'Content-Disposition' => disposition, 'Content-Transfer-Encoding' => 'binary' ) # Fix a problem with IE 6.0 on opening downloaded files: # If Cache-Control: no-cache is set (which Rails does by default), # IE removes the file it just downloaded from its cache immediately # after it displays the "open/save" dialog, which means that if you # hit "open" the file isn't there anymore when the application that # is called for handling the download is run, so let's workaround that @headers['Cache-Control'] = 'private' if @headers['Cache-Control'] == 'no-cache' end end end require File.dirname(__FILE__) + '/assertions' require File.dirname(__FILE__) + '/deprecated_assertions' module ActionController #:nodoc: class Base # Process a test request called with a +TestRequest+ object. def self.process_test(request) new.process_test(request) end def process_test(request) #:nodoc: process(request, TestResponse.new) end def process_with_test(*args) returning process_without_test(*args) do add_variables_to_assigns end end alias_method :process_without_test, :process alias_method :process, :process_with_test end class TestRequest < AbstractRequest #:nodoc: attr_accessor :cookies, :session_options attr_accessor :query_parameters, :request_parameters, :path, :session, :env attr_accessor :host def initialize(query_parameters = nil, request_parameters = nil, session = nil) @query_parameters = query_parameters || {} @request_parameters = request_parameters || {} @session = session || TestSession.new initialize_containers initialize_default_values super() end def reset_session @session = {} end def raw_post if raw_post = env['RAW_POST_DATA'] raw_post else params = self.request_parameters.dup %w(controller action only_path).each do |k| params.delete(k) params.delete(k.to_sym) end params.map { |k,v| [ CGI.escape(k.to_s), CGI.escape(v.to_s) ].join('=') }.sort.join('&') end end def port=(number) @env["SERVER_PORT"] = number.to_i @port_as_int = nil end def action=(action_name) @query_parameters.update({ "action" => action_name }) @parameters = nil end # Used to check AbstractRequest's request_uri functionality. # Disables the use of @path and @request_uri so superclass can handle those. def set_REQUEST_URI(value) @env["REQUEST_URI"] = value @request_uri = nil @path = nil end def request_uri=(uri) @request_uri = uri @path = uri.split("?").first end def remote_addr=(addr) @env['REMOTE_ADDR'] = addr end def remote_addr @env['REMOTE_ADDR'] end def request_uri @request_uri || super() end def path @path || super() end def assign_parameters(controller_path, action, parameters) parameters = parameters.symbolize_keys.merge(:controller => controller_path, :action => action) extra_keys = ActionController::Routing::Routes.extra_keys(parameters) non_path_parameters = get? ? query_parameters : request_parameters parameters.each do |key, value| if value.is_a? Fixnum value = value.to_s elsif value.is_a? Array value = ActionController::Routing::PathComponent::Result.new(value) end if extra_keys.include?(key.to_sym) non_path_parameters[key] = value else path_parameters[key.to_s] = value end end end def recycle! self.request_parameters = {} self.query_parameters = {} self.path_parameters = {} @request_method, @accepts, @content_type = nil, nil, nil end private def initialize_containers @env, @cookies = {}, {} end def initialize_default_values @host = "test.host" @request_uri = "/" self.remote_addr = "0.0.0.0" @env["SERVER_PORT"] = 80 @env['REQUEST_METHOD'] = "GET" end end # A refactoring of TestResponse to allow the same behavior to be applied # to the "real" CgiResponse class in integration tests. module TestResponseBehavior #:nodoc: # the response code of the request def response_code headers['Status'][0,3].to_i rescue 0 end # returns a String to ensure compatibility with Net::HTTPResponse def code headers['Status'].to_s.split(' ')[0] end def message headers['Status'].to_s.split(' ',2)[1] end # was the response successful? def success? response_code == 200 end # was the URL not found? def missing? response_code == 404 end # were we redirected? def redirect? (300..399).include?(response_code) end # was there a server-side error? def error? (500..599).include?(response_code) end alias_method :server_error?, :error? # returns the redirection location or nil def redirect_url redirect? ? headers['location'] : nil end # does the redirect location match this regexp pattern? def redirect_url_match?( pattern ) return false if redirect_url.nil? p = Regexp.new(pattern) if pattern.class == String p = pattern if pattern.class == Regexp return false if p.nil? p.match(redirect_url) != nil end # returns the template path of the file which was used to # render this response (or nil) def rendered_file(with_controller=false) unless template.first_render.nil? unless with_controller template.first_render else template.first_render.split('/').last || template.first_render end end end # was this template rendered by a file? def rendered_with_file? !rendered_file.nil? end # a shortcut to the flash (or an empty hash if no flash.. hey! that rhymes!) def flash session['flash'] || {} end # do we have a flash? def has_flash? !session['flash'].empty? end # do we have a flash that has contents? def has_flash_with_contents? !flash.empty? end # does the specified flash object exist? def has_flash_object?(name=nil) !flash[name].nil? end # does the specified object exist in the session? def has_session_object?(name=nil) !session[name].nil? end # a shortcut to the template.assigns def template_objects template.assigns || {} end # does the specified template object exist? def has_template_object?(name=nil) !template_objects[name].nil? end # Returns the response cookies, converted to a Hash of (name => CGI::Cookie) pairs # Example: # # assert_equal ['AuthorOfNewPage'], r.cookies['author'].value def cookies headers['cookie'].inject({}) { |hash, cookie| hash[cookie.name] = cookie; hash } end # Returns binary content (downloadable file), converted to a String def binary_content raise "Response body is not a Proc: #{body.inspect}" unless body.kind_of?(Proc) require 'stringio' sio = StringIO.new begin $stdout = sio body.call ensure $stdout = STDOUT end sio.rewind sio.read end end class TestResponse < AbstractResponse #:nodoc: include TestResponseBehavior end class TestSession #:nodoc: def initialize(attributes = {}) @attributes = attributes end def [](key) @attributes[key] end def []=(key, value) @attributes[key] = value end def session_id "" end def update() end def close() end def delete() @attributes = {} end end # Essentially generates a modified Tempfile object similar to the object # you'd get from the standard library CGI module in a multipart # request. This means you can use an ActionController::TestUploadedFile # object in the params of a test request in order to simulate # a file upload. # # Usage example, within a functional test: # post :change_avatar, :avatar => ActionController::TestUploadedFile.new(Test::Unit::TestCase.fixture_path + '/files/spongebob.png', 'image/png') class TestUploadedFile # The filename, *not* including the path, of the "uploaded" file attr_reader :original_filename # The content type of the "uploaded" file attr_reader :content_type def initialize(path, content_type = 'text/plain') raise "file does not exist" unless File.exist?(path) @content_type = content_type @original_filename = path.sub(/^.*#{File::SEPARATOR}([^#{File::SEPARATOR}]+)$/) { $1 } @tempfile = Tempfile.new(@original_filename) FileUtils.copy_file(path, @tempfile.path) end def path #:nodoc: @tempfile.path end alias local_path path def method_missing(method_name, *args, &block) #:nodoc: @tempfile.send(method_name, *args, &block) end end module TestProcess def self.included(base) # execute the request simulating a specific http method and set/volley the response %w( get post put delete head ).each do |method| base.class_eval <<-EOV, __FILE__, __LINE__ def #{method}(action, parameters = nil, session = nil, flash = nil) @request.env['REQUEST_METHOD'] = "#{method.upcase}" if @request process(action, parameters, session, flash) end EOV end end # execute the request and set/volley the response def process(action, parameters = nil, session = nil, flash = nil) # Sanity check for required instance variables so we can give an # understandable error message. %w(controller request response).each do |iv_name| raise "@#{iv_name} is nil: make sure you set it in your test's setup method." if instance_variable_get("@#{iv_name}").nil? end @request.recycle! @html_document = nil @request.env['REQUEST_METHOD'] ||= "GET" @request.action = action.to_s parameters ||= {} @request.assign_parameters(@controller.class.controller_path, action.to_s, parameters) @request.session = ActionController::TestSession.new(session) unless session.nil? @request.session["flash"] = ActionController::Flash::FlashHash.new.update(flash) if flash build_request_uri(action, parameters) @controller.process(@request, @response) end def xml_http_request(request_method, action, parameters = nil, session = nil, flash = nil) @request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' @request.env['HTTP_ACCEPT'] = 'text/javascript, text/html, application/xml, text/xml, */*' returning self.send(request_method, action, parameters, session, flash) do @request.env.delete 'HTTP_X_REQUESTED_WITH' @request.env.delete 'HTTP_ACCEPT' end end alias xhr :xml_http_request def follow_redirect if @response.redirected_to[:controller] raise "Can't follow redirects outside of current controller (#{@response.redirected_to[:controller]})" end get(@response.redirected_to.delete(:action), @response.redirected_to.stringify_keys) end def assigns(key = nil) if key.nil? @response.template.assigns else @response.template.assigns[key.to_s] end end def session @response.session end def flash @response.flash end def cookies @response.cookies end def redirect_to_url @response.redirect_url end def build_request_uri(action, parameters) unless @request.env['REQUEST_URI'] options = @controller.send(:rewrite_options, parameters) options.update(:only_path => true, :action => action) url = ActionController::UrlRewriter.new(@request, parameters) @request.set_REQUEST_URI(url.rewrite(options)) end end def html_document @html_document ||= HTML::Document.new(@response.body) end def find_tag(conditions) html_document.find(conditions) end def find_all_tag(conditions) html_document.find_all(conditions) end def method_missing(selector, *args) return @controller.send(selector, *args) if ActionController::Routing::NamedRoutes::Helpers.include?(selector) return super end # Shortcut for ActionController::TestUploadedFile.new(Test::Unit::TestCase.fixture_path + path, type). Example: # post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png') def fixture_file_upload(path, mime_type = nil) ActionController::TestUploadedFile.new( Test::Unit::TestCase.respond_to?(:fixture_path) ? Test::Unit::TestCase.fixture_path + path : path, mime_type ) end # A helper to make it easier to test different route configurations. # This method temporarily replaces ActionController::Routing::Routes # with a new RouteSet instance. # # The new instance is yielded to the passed block. Typically the block # will create some routes using map.draw { map.connect ... }: # # with_routing do |set| # set.draw { set.connect ':controller/:id/:action' } # assert_equal( # ['/content/10/show', {}], # set.generate(:controller => 'content', :id => 10, :action => 'show') # ) # end # def with_routing real_routes = ActionController::Routing::Routes ActionController::Routing.send :remove_const, :Routes temporary_routes = ActionController::Routing::RouteSet.new ActionController::Routing.send :const_set, :Routes, temporary_routes yield temporary_routes ensure if ActionController::Routing.const_defined? :Routes ActionController::Routing.send(:remove_const, :Routes) end ActionController::Routing.const_set(:Routes, real_routes) if real_routes end end end module Test module Unit class TestCase #:nodoc: include ActionController::TestProcess end end end module ActionController # Rewrites URLs for Base.redirect_to and Base.url_for in the controller. class UrlRewriter #:nodoc: RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :trailing_slash, :skip_relative_url_root] def initialize(request, parameters) @request, @parameters = request, parameters end def rewrite(options = {}) rewrite_url(rewrite_path(options), options) end def to_str "#{@request.protocol}, #{@request.host_with_port}, #{@request.path}, #{@parameters[:controller]}, #{@parameters[:action]}, #{@request.parameters.inspect}" end alias_method :to_s, :to_str private def rewrite_url(path, options) rewritten_url = "" unless options[:only_path] rewritten_url << (options[:protocol] || @request.protocol) rewritten_url << (options[:host] || @request.host_with_port) end rewritten_url << @request.relative_url_root.to_s unless options[:skip_relative_url_root] rewritten_url << path rewritten_url << '/' if options[:trailing_slash] rewritten_url << "##{options[:anchor]}" if options[:anchor] rewritten_url end def rewrite_path(options) options = options.symbolize_keys options.update(options[:params].symbolize_keys) if options[:params] if (overwrite = options.delete(:overwrite_params)) options.update(@parameters.symbolize_keys) options.update(overwrite) end RESERVED_OPTIONS.each {|k| options.delete k} path, extra_keys = Routing::Routes.generate(options.dup, @request) # Warning: Routes will mutate and violate the options hash path << build_query_string(options, extra_keys) unless extra_keys.empty? path end # Returns a query string with escaped keys and values from the passed hash. If the passed hash contains an "id" it'll # be added as a path element instead of a regular parameter pair. def build_query_string(hash, only_keys = nil) elements = [] query_string = "" only_keys ||= hash.keys only_keys.each do |key| value = hash[key] key = CGI.escape key.to_s if value.class == Array key << '[]' else value = [ value ] end value.each { |val| elements << "#{key}=#{Routing.extract_parameter_value(val)}" } end query_string << ("?" + elements.join("&")) unless elements.empty? query_string end end end require File.dirname(__FILE__) + '/tokenizer' require File.dirname(__FILE__) + '/node' module HTML #:nodoc: # A top-level HTMl document. You give it a body of text, and it will parse that # text into a tree of nodes. class Document #:nodoc: # The root of the parsed document. attr_reader :root # Create a new Document from the given text. def initialize(text, strict=false, xml=false) tokenizer = Tokenizer.new(text) @root = Node.new(nil) node_stack = [ @root ] while token = tokenizer.next node = Node.parse(node_stack.last, tokenizer.line, tokenizer.position, token) node_stack.last.children << node unless node.tag? && node.closing == :close if node.tag? if node_stack.length > 1 && node.closing == :close if node_stack.last.name == node.name node_stack.pop else open_start = node_stack.last.position - 20 open_start = 0 if open_start < 0 close_start = node.position - 20 close_start = 0 if close_start < 0 msg = < hash } unless Hash === hash hash = keys_to_symbols(hash) hash.each do |k,v| case k when :tag, :content then # keys are valid, and require no further processing when :attributes then hash[k] = keys_to_strings(v) when :parent, :child, :ancestor, :descendant, :sibling, :before, :after hash[k] = Conditions.new(v) when :children hash[k] = v = keys_to_symbols(v) v.each do |k,v2| case k when :count, :greater_than, :less_than # keys are valid, and require no further processing when :only v[k] = Conditions.new(v2) else raise "illegal key #{k.inspect} => #{v2.inspect}" end end else raise "illegal key #{k.inspect} => #{v.inspect}" end end update hash end private def keys_to_strings(hash) hash.keys.inject({}) do |h,k| h[k.to_s] = hash[k] h end end def keys_to_symbols(hash) hash.keys.inject({}) do |h,k| raise "illegal key #{k.inspect}" unless k.respond_to?(:to_sym) h[k.to_sym] = hash[k] h end end end # The base class of all nodes, textual and otherwise, in an HTML document. class Node #:nodoc: # The array of children of this node. Not all nodes have children. attr_reader :children # The parent node of this node. All nodes have a parent, except for the # root node. attr_reader :parent # The line number of the input where this node was begun attr_reader :line # The byte position in the input where this node was begun attr_reader :position # Create a new node as a child of the given parent. def initialize(parent, line=0, pos=0) @parent = parent @children = [] @line, @position = line, pos end # Return a textual representation of the node. def to_s s = "" @children.each { |child| s << child.to_s } s end # Return false (subclasses must override this to provide specific matching # behavior.) +conditions+ may be of any type. def match(conditions) false end # Search the children of this node for the first node for which #find # returns non +nil+. Returns the result of the #find call that succeeded. def find(conditions) conditions = validate_conditions(conditions) @children.each do |child| node = child.find(conditions) return node if node end nil end # Search for all nodes that match the given conditions, and return them # as an array. def find_all(conditions) conditions = validate_conditions(conditions) matches = [] matches << self if match(conditions) @children.each do |child| matches.concat child.find_all(conditions) end matches end # Returns +false+. Subclasses may override this if they define a kind of # tag. def tag? false end def validate_conditions(conditions) Conditions === conditions ? conditions : Conditions.new(conditions) end def ==(node) return false unless self.class == node.class && children.size == node.children.size equivalent = true children.size.times do |i| equivalent &&= children[i] == node.children[i] end equivalent end class </) return CDATA.new(parent, line, pos, scanner.pre_match) end closing = ( scanner.scan(/\//) ? :close : nil ) return Text.new(parent, line, pos, content) unless name = scanner.scan(/[\w:]+/) name.downcase! unless closing scanner.skip(/\s*/) attributes = {} while attr = scanner.scan(/[-\w:]+/) value = true if scanner.scan(/\s*=\s*/) if delim = scanner.scan(/['"]/) value = "" while text = scanner.scan(/[^#{delim}\\]+|./) case text when "\\" then value << text value << scanner.getch when delim break else value << text end end else value = scanner.scan(/[^\s>\/]+/) end end attributes[attr.downcase] = value scanner.skip(/\s*/) end closing = ( scanner.scan(/\//) ? :self : nil ) end unless scanner.scan(/\s*>/) if strict raise "expected > (got #{scanner.rest.inspect} for #{content}, #{attributes.inspect})" else # throw away all text until we find what we're looking for scanner.skip_until(/>/) or scanner.terminate end end Tag.new(parent, line, pos, name, attributes, closing) end end end end # A node that represents text, rather than markup. class Text < Node #:nodoc: attr_reader :content # Creates a new text node as a child of the given parent, with the given # content. def initialize(parent, line, pos, content) super(parent, line, pos) @content = content end # Returns the content of this node. def to_s @content end # Returns +self+ if this node meets the given conditions. Text nodes support # conditions of the following kinds: # # * if +conditions+ is a string, it must be a substring of the node's # content # * if +conditions+ is a regular expression, it must match the node's # content # * if +conditions+ is a hash, it must contain a :content key that # is either a string or a regexp, and which is interpreted as described # above. def find(conditions) match(conditions) && self end # Returns non-+nil+ if this node meets the given conditions, or +nil+ # otherwise. See the discussion of #find for the valid conditions. def match(conditions) case conditions when String @content.index(conditions) when Regexp @content =~ conditions when Hash conditions = validate_conditions(conditions) # Text nodes only have :content, :parent, :ancestor unless (conditions.keys - [:content, :parent, :ancestor]).empty? return false end match(conditions[:content]) else nil end end def ==(node) return false unless super content == node.content end end # A CDATA node is simply a text node with a specialized way of displaying # itself. class CDATA < Text #:nodoc: def to_s "" end end # A Tag is any node that represents markup. It may be an opening tag, a # closing tag, or a self-closing tag. It has a name, and may have a hash of # attributes. class Tag < Node #:nodoc: # Either +nil+, :close, or :self attr_reader :closing # Either +nil+, or a hash of attributes for this node. attr_reader :attributes # The name of this tag. attr_reader :name # Create a new node as a child of the given parent, using the given content # to describe the node. It will be parsed and the node name, attributes and # closing status extracted. def initialize(parent, line, pos, name, attributes, closing) super(parent, line, pos) @name = name @attributes = attributes @closing = closing end # A convenience for obtaining an attribute of the node. Returns +nil+ if # the node has no attributes. def [](attr) @attributes ? @attributes[attr] : nil end # Returns non-+nil+ if this tag can contain child nodes. def childless?(xml = false) return false if xml && @closing.nil? !@closing.nil? || @name =~ /^(img|br|hr|link|meta|area|base|basefont| col|frame|input|isindex|param)$/ox end # Returns a textual representation of the node def to_s if @closing == :close "" else s = "<#{@name}" @attributes.each do |k,v| s << " #{k}" s << "='#{v.gsub(/'/,"\\\\'")}'" if String === v end s << " /" if @closing == :self s << ">" @children.each { |child| s << child.to_s } s << "" if @closing != :self && !@children.empty? s end end # If either the node or any of its children meet the given conditions, the # matching node is returned. Otherwise, +nil+ is returned. (See the # description of the valid conditions in the +match+ method.) def find(conditions) match(conditions) && self || super end # Returns +true+, indicating that this node represents an HTML tag. def tag? true end # Returns +true+ if the node meets any of the given conditions. The # +conditions+ parameter must be a hash of any of the following keys # (all are optional): # # * :tag: the node name must match the corresponding value # * :attributes: a hash. The node's values must match the # corresponding values in the hash. # * :parent: a hash. The node's parent must match the # corresponding hash. # * :child: a hash. At least one of the node's immediate children # must meet the criteria described by the hash. # * :ancestor: a hash. At least one of the node's ancestors must # meet the criteria described by the hash. # * :descendant: a hash. At least one of the node's descendants # must meet the criteria described by the hash. # * :sibling: a hash. At least one of the node's siblings must # meet the criteria described by the hash. # * :after: a hash. The node must be after any sibling meeting # the criteria described by the hash, and at least one sibling must match. # * :before: a hash. The node must be before any sibling meeting # the criteria described by the hash, and at least one sibling must match. # * :children: a hash, for counting children of a node. Accepts the # keys: # ** :count: either a number or a range which must equal (or # include) the number of children that match. # ** :less_than: the number of matching children must be less than # this number. # ** :greater_than: the number of matching children must be # greater than this number. # ** :only: another hash consisting of the keys to use # to match on the children, and only matching children will be # counted. # # 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: # # # test if the node is a "span" tag # node.match :tag => "span" # # # test if the node's parent is a "div" # node.match :parent => { :tag => "div" } # # # test if any of the node's ancestors are "table" tags # node.match :ancestor => { :tag => "table" } # # # test if any of the node's immediate children are "em" tags # node.match :child => { :tag => "em" } # # # test if any of the node's descendants are "strong" tags # node.match :descendant => { :tag => "strong" } # # # test if the node has between 2 and 4 span tags as immediate children # node.match :children => { :count => 2..4, :only => { :tag => "span" } } # # # get funky: test to see if the node is a "div", has a "ul" ancestor # # and an "li" parent (with "class" = "enum"), and whether or not it has # # a "span" descendant that contains # text matching /hello world/: # node.match :tag => "div", # :ancestor => { :tag => "ul" }, # :parent => { :tag => "li", # :attributes => { :class => "enum" } }, # :descendant => { :tag => "span", # :child => /hello world/ } def match(conditions) conditions = validate_conditions(conditions) # check content of child nodes if conditions[:content] if children.empty? return false unless match_condition("", conditions[:content]) else return false unless children.find { |child| child.match(conditions[:content]) } end end # test the name return false unless match_condition(@name, conditions[:tag]) if conditions[:tag] # test attributes (conditions[:attributes] || {}).each do |key, value| return false unless match_condition(self[key], value) end # test parent return false unless parent.match(conditions[:parent]) if conditions[:parent] # test children return false unless children.find { |child| child.match(conditions[:child]) } if conditions[:child] # test ancestors if conditions[:ancestor] return false unless catch :found do p = self throw :found, true if p.match(conditions[:ancestor]) while p = p.parent end end # test descendants if conditions[:descendant] return false unless children.find do |child| # test the child child.match(conditions[:descendant]) || # test the child's descendants child.match(:descendant => conditions[:descendant]) end end # count children if opts = conditions[:children] matches = children.select do |c| c.match(/./) or (c.kind_of?(HTML::Tag) and (c.closing == :self or ! c.childless?)) end matches = matches.select { |c| c.match(opts[:only]) } if opts[:only] opts.each do |key, value| next if key == :only case key when :count if Integer === value return false if matches.length != value else return false unless value.include?(matches.length) end when :less_than return false unless matches.length < value when :greater_than return false unless matches.length > value else raise "unknown count condition #{key}" end end end # test siblings if conditions[:sibling] || conditions[:before] || conditions[:after] siblings = parent ? parent.children : [] self_index = siblings.index(self) if conditions[:sibling] return false unless siblings.detect do |s| s != self && s.match(conditions[:sibling]) end end if conditions[:before] return false unless siblings[self_index+1..-1].detect do |s| s != self && s.match(conditions[:before]) end end if conditions[:after] return false unless siblings[0,self_index].detect do |s| s != self && s.match(conditions[:after]) end end end true end def ==(node) return false unless super return false unless closing == node.closing && self.name == node.name attributes == node.attributes end private # Match the given value to the given condition. def match_condition(value, condition) case condition when String value && value == condition when Regexp value && value.match(condition) when Numeric value == condition.to_s when true !value.nil? when false, nil value.nil? else false end end end end require 'strscan' module HTML #:nodoc: # A simple HTML tokenizer. It simply breaks a stream of text into tokens, where each # token is a string. Each string represents either "text", or an HTML element. # # This currently assumes valid XHTML, which means no free < or > characters. # # Usage: # # tokenizer = HTML::Tokenizer.new(text) # while token = tokenizer.next # p token # end class Tokenizer #:nodoc: # The current (byte) position in the text attr_reader :position # The current line number attr_reader :line # Create a new Tokenizer for the given text. def initialize(text) @scanner = StringScanner.new(text) @position = 0 @line = 0 @current_line = 1 end # Return the next token in the sequence, or +nil+ if there are no more tokens in # the stream. def next return nil if @scanner.eos? @position = @scanner.pos @line = @current_line if @scanner.check(/<\S/) update_current_line(scan_tag) else update_current_line(scan_text) end end private # Treat the text at the current position as a tag, and scan it. Supports # comments, doctype tags, and regular tags, and ignores less-than and # greater-than characters within quoted strings. def scan_tag tag = @scanner.getch if @scanner.scan(/!--/) # comment tag << @scanner.matched tag << (@scanner.scan_until(/--\s*>/) || @scanner.scan_until(/\Z/)) elsif @scanner.scan(/!\[CDATA\[/) tag << @scanner.matched tag << @scanner.scan_until(/\]\]>/) elsif @scanner.scan(/!/) # doctype tag << @scanner.matched tag << consume_quoted_regions else tag << consume_quoted_regions end tag end # Scan all text up to the next < character and return it. def scan_text "#{@scanner.getch}#{@scanner.scan(/[^<]*/)}" end # Counts the number of newlines in the text and updates the current line # accordingly. def update_current_line(text) text.scan(/\r?\n/) { @current_line += 1 } end # Skips over quoted strings, so that less-than and greater-than characters # within the strings are ignored. def consume_quoted_regions text = "" loop do match = @scanner.scan_until(/['"<>]/) or break delim = @scanner.matched if delim == "<" match = match.chop @scanner.pos -= 1 end text << match break if delim == "<" || delim == ">" # consume the quoted region while match = @scanner.scan_until(/[\\#{delim}]/) text << match break if @scanner.matched == delim text << @scanner.getch # skip the escaped character end end text end end end module HTML #:nodoc: module Version #:nodoc: MAJOR = 0 MINOR = 5 TINY = 3 STRING = [ MAJOR, MINOR, TINY ].join(".") end end require 'rexml/document' # SimpleXML like xml parser. Written by leon breet from the ruby on rails Mailing list class XmlNode #:nodoc: attr :node def initialize(node, options = {}) @node = node @children = {} @raise_errors = options[:raise_errors] end def self.from_xml(xml_or_io) document = REXML::Document.new(xml_or_io) if document.root XmlNode.new(document.root) else XmlNode.new(document) end end def node_encoding @node.encoding end def node_name @node.name end def node_value @node.text end def node_value=(value) @node.text = value end def xpath(expr) matches = nil REXML::XPath.each(@node, expr) do |element| matches ||= XmlNodeList.new matches << (@children[element] ||= XmlNode.new(element)) end matches end def method_missing(name, *args) name = name.to_s nodes = nil @node.each_element(name) do |element| nodes ||= XmlNodeList.new nodes << (@children[element] ||= XmlNode.new(element)) end nodes end def <<(node) if node.is_a? REXML::Node child = node elsif node.respond_to? :node child = node.node end @node.add_element child @children[child] ||= XmlNode.new(child) end def [](name) @node.attributes[name.to_s] end def []=(name, value) @node.attributes[name.to_s] = value end def to_s @node.to_s end def to_i to_s.to_i end end class XmlNodeList < Array #:nodoc: def [](i) i.is_a?(String) ? super(0)[i] : super(i) end def []=(i, value) i.is_a?(String) ? self[0][i] = value : super(i, value) end def method_missing(name, *args) name = name.to_s self[0].__send__(name, *args) end end# = XmlSimple # # Author:: Maik Schmidt # Copyright:: Copyright (c) 2003 Maik Schmidt # License:: Distributes under the same terms as Ruby. # require 'rexml/document' # Easy API to maintain XML (especially configuration files). class XmlSimple #:nodoc: include REXML @@VERSION = '1.0.2' # A simple cache for XML documents that were already transformed # by xml_in. class Cache #:nodoc: # Creates and initializes a new Cache object. def initialize @mem_share_cache = {} @mem_copy_cache = {} end # Saves a data structure into a file. # # data:: # Data structure to be saved. # filename:: # Name of the file belonging to the data structure. def save_storable(data, filename) cache_file = get_cache_filename(filename) File.open(cache_file, "w+") { |f| Marshal.dump(data, f) } end # Restores a data structure from a file. If restoring the data # structure failed for any reason, nil will be returned. # # filename:: # Name of the file belonging to the data structure. def restore_storable(filename) cache_file = get_cache_filename(filename) return nil unless File::exist?(cache_file) return nil unless File::mtime(cache_file).to_i > File::mtime(filename).to_i data = nil File.open(cache_file) { |f| data = Marshal.load(f) } data end # Saves a data structure in a shared memory cache. # # data:: # Data structure to be saved. # filename:: # Name of the file belonging to the data structure. def save_mem_share(data, filename) @mem_share_cache[filename] = [Time::now.to_i, data] end # Restores a data structure from a shared memory cache. You # should consider these elements as "read only". If restoring # the data structure failed for any reason, nil will be # returned. # # filename:: # Name of the file belonging to the data structure. def restore_mem_share(filename) get_from_memory_cache(filename, @mem_share_cache) end # Copies a data structure to a memory cache. # # data:: # Data structure to be copied. # filename:: # Name of the file belonging to the data structure. def save_mem_copy(data, filename) @mem_share_cache[filename] = [Time::now.to_i, Marshal.dump(data)] end # Restores a data structure from a memory cache. If restoring # the data structure failed for any reason, nil will be # returned. # # filename:: # Name of the file belonging to the data structure. def restore_mem_copy(filename) data = get_from_memory_cache(filename, @mem_share_cache) data = Marshal.load(data) unless data.nil? data end private # Returns the "cache filename" belonging to a filename, i.e. # the extension '.xml' in the original filename will be replaced # by '.stor'. If filename does not have this extension, '.stor' # will be appended. # # filename:: # Filename to get "cache filename" for. def get_cache_filename(filename) filename.sub(/(\.xml)?$/, '.stor') end # Returns a cache entry from a memory cache belonging to a # certain filename. If no entry could be found for any reason, # nil will be returned. # # filename:: # Name of the file the cache entry belongs to. # cache:: # Memory cache to get entry from. def get_from_memory_cache(filename, cache) return nil unless cache[filename] return nil unless cache[filename][0] > File::mtime(filename).to_i return cache[filename][1] end end # Create a "global" cache. @@cache = Cache.new # Creates and intializes a new XmlSimple object. # # defaults:: # Default values for options. def initialize(defaults = nil) unless defaults.nil? || defaults.instance_of?(Hash) raise ArgumentError, "Options have to be a Hash." end @default_options = normalize_option_names(defaults, KNOWN_OPTIONS['in'] & KNOWN_OPTIONS['out']) @options = Hash.new @_var_values = nil end # Converts an XML document in the same way as the Perl module XML::Simple. # # string:: # XML source. Could be one of the following: # # - nil: Tries to load and parse '.xml'. # - filename: Tries to load and parse filename. # - IO object: Reads from object until EOF is detected and parses result. # - XML string: Parses string. # # options:: # Options to be used. def xml_in(string = nil, options = nil) handle_options('in', options) # If no XML string or filename was supplied look for scriptname.xml. if string.nil? string = File::basename($0) string.sub!(/\.[^.]+$/, '') string += '.xml' directory = File::dirname($0) @options['searchpath'].unshift(directory) unless directory.nil? end if string.instance_of?(String) if string =~ /<.*?>/m @doc = parse(string) elsif string == '-' @doc = parse($stdin.readlines.to_s) else filename = find_xml_file(string, @options['searchpath']) if @options.has_key?('cache') @options['cache'].each { |scheme| case(scheme) when 'storable' content = @@cache.restore_storable(filename) when 'mem_share' content = @@cache.restore_mem_share(filename) when 'mem_copy' content = @@cache.restore_mem_copy(filename) else raise ArgumentError, "Unsupported caching scheme: <#{scheme}>." end return content if content } end @doc = load_xml_file(filename) end elsif string.kind_of?(IO) @doc = parse(string.readlines.to_s) else raise ArgumentError, "Could not parse object of type: <#{string.type}>." end result = collapse(@doc.root) result = @options['keeproot'] ? merge({}, @doc.root.name, result) : result put_into_cache(result, filename) result end # This is the functional version of the instance method xml_in. def XmlSimple.xml_in(string = nil, options = nil) xml_simple = XmlSimple.new xml_simple.xml_in(string, options) end # Converts a data structure into an XML document. # # ref:: # Reference to data structure to be converted into XML. # options:: # Options to be used. def xml_out(ref, options = nil) handle_options('out', options) if ref.instance_of?(Array) ref = { @options['anonymoustag'] => ref } end if @options['keeproot'] keys = ref.keys if keys.size == 1 ref = ref[keys[0]] @options['rootname'] = keys[0] end elsif @options['rootname'] == '' if ref.instance_of?(Hash) refsave = ref ref = {} refsave.each { |key, value| if !scalar(value) ref[key] = value else ref[key] = [ value.to_s ] end } end end @ancestors = [] xml = value_to_xml(ref, @options['rootname'], '') @ancestors = nil if @options['xmldeclaration'] xml = @options['xmldeclaration'] + "\n" + xml end if @options.has_key?('outputfile') if @options['outputfile'].kind_of?(IO) return @options['outputfile'].write(xml) else File.open(@options['outputfile'], "w") { |file| file.write(xml) } end end xml end # This is the functional version of the instance method xml_out. def XmlSimple.xml_out(hash, options = nil) xml_simple = XmlSimple.new xml_simple.xml_out(hash, options) end private # Declare options that are valid for xml_in and xml_out. KNOWN_OPTIONS = { 'in' => %w( keyattr keeproot forcecontent contentkey noattr searchpath forcearray suppressempty anonymoustag cache grouptags normalisespace normalizespace variables varattr ), 'out' => %w( keyattr keeproot contentkey noattr rootname xmldeclaration outputfile noescape suppressempty anonymoustag indent grouptags noindent ) } # Define some reasonable defaults. DEF_KEY_ATTRIBUTES = [] DEF_ROOT_NAME = 'opt' DEF_CONTENT_KEY = 'content' DEF_XML_DECLARATION = "" DEF_ANONYMOUS_TAG = 'anon' DEF_FORCE_ARRAY = true DEF_INDENTATION = ' ' # Normalizes option names in a hash, i.e., turns all # characters to lower case and removes all underscores. # Additionally, this method checks, if an unknown option # was used and raises an according exception. # # options:: # Hash to be normalized. # known_options:: # List of known options. def normalize_option_names(options, known_options) return nil if options.nil? result = Hash.new options.each { |key, value| lkey = key.downcase lkey.gsub!(/_/, '') if !known_options.member?(lkey) raise ArgumentError, "Unrecognised option: #{lkey}." end result[lkey] = value } result end # Merges a set of options with the default options. # # direction:: # 'in': If options should be handled for xml_in. # 'out': If options should be handled for xml_out. # options:: # Options to be merged with the default options. def handle_options(direction, options) @options = options || Hash.new raise ArgumentError, "Options must be a Hash!" unless @options.instance_of?(Hash) unless KNOWN_OPTIONS.has_key?(direction) raise ArgumentError, "Unknown direction: <#{direction}>." end known_options = KNOWN_OPTIONS[direction] @options = normalize_option_names(@options, known_options) unless @default_options.nil? known_options.each { |option| unless @options.has_key?(option) if @default_options.has_key?(option) @options[option] = @default_options[option] end end } end unless @options.has_key?('noattr') @options['noattr'] = false end if @options.has_key?('rootname') @options['rootname'] = '' if @options['rootname'].nil? else @options['rootname'] = DEF_ROOT_NAME end if @options.has_key?('xmldeclaration') && @options['xmldeclaration'] == true @options['xmldeclaration'] = DEF_XML_DECLARATION end if @options.has_key?('contentkey') if @options['contentkey'] =~ /^-(.*)$/ @options['contentkey'] = $1 @options['collapseagain'] = true end else @options['contentkey'] = DEF_CONTENT_KEY end unless @options.has_key?('normalisespace') @options['normalisespace'] = @options['normalizespace'] end @options['normalisespace'] = 0 if @options['normalisespace'].nil? if @options.has_key?('searchpath') unless @options['searchpath'].instance_of?(Array) @options['searchpath'] = [ @options['searchpath'] ] end else @options['searchpath'] = [] end if @options.has_key?('cache') && scalar(@options['cache']) @options['cache'] = [ @options['cache'] ] end @options['anonymoustag'] = DEF_ANONYMOUS_TAG unless @options.has_key?('anonymoustag') if !@options.has_key?('indent') || @options['indent'].nil? @options['indent'] = DEF_INDENTATION end @options['indent'] = '' if @options.has_key?('noindent') # Special cleanup for 'keyattr' which could be an array or # a hash or left to default to array. if @options.has_key?('keyattr') if !scalar(@options['keyattr']) # Convert keyattr => { elem => '+attr' } # to keyattr => { elem => ['attr', '+'] } if @options['keyattr'].instance_of?(Hash) @options['keyattr'].each { |key, value| if value =~ /^([-+])?(.*)$/ @options['keyattr'][key] = [$2, $1 ? $1 : ''] end } elsif !@options['keyattr'].instance_of?(Array) raise ArgumentError, "'keyattr' must be String, Hash, or Array!" end else @options['keyattr'] = [ @options['keyattr'] ] end else @options['keyattr'] = DEF_KEY_ATTRIBUTES end if @options.has_key?('forcearray') if @options['forcearray'].instance_of?(Regexp) @options['forcearray'] = [ @options['forcearray'] ] end if @options['forcearray'].instance_of?(Array) force_list = @options['forcearray'] unless force_list.empty? @options['forcearray'] = {} force_list.each { |tag| if tag.instance_of?(Regexp) unless @options['forcearray']['_regex'].instance_of?(Array) @options['forcearray']['_regex'] = [] end @options['forcearray']['_regex'] << tag else @options['forcearray'][tag] = true end } else @options['forcearray'] = false end else @options['forcearray'] = @options['forcearray'] ? true : false end else @options['forcearray'] = DEF_FORCE_ARRAY end if @options.has_key?('grouptags') && !@options['grouptags'].instance_of?(Hash) raise ArgumentError, "Illegal value for 'GroupTags' option - expected a Hash." end if @options.has_key?('variables') && !@options['variables'].instance_of?(Hash) raise ArgumentError, "Illegal value for 'Variables' option - expected a Hash." end if @options.has_key?('variables') @_var_values = @options['variables'] elsif @options.has_key?('varattr') @_var_values = {} end end # Actually converts an XML document element into a data structure. # # element:: # The document element to be collapsed. def collapse(element) result = @options['noattr'] ? {} : get_attributes(element) if @options['normalisespace'] == 2 result.each { |k, v| result[k] = normalise_space(v) } end if element.has_elements? element.each_element { |child| value = collapse(child) if empty(value) && (element.attributes.empty? || @options['noattr']) next if @options.has_key?('suppressempty') && @options['suppressempty'] == true end result = merge(result, child.name, value) } if has_mixed_content?(element) # normalisespace? content = element.texts.map { |x| x.to_s } content = content[0] if content.size == 1 result[@options['contentkey']] = content end elsif element.has_text? # i.e. it has only text. return collapse_text_node(result, element) end # Turn Arrays into Hashes if key fields present. count = fold_arrays(result) # Disintermediate grouped tags. if @options.has_key?('grouptags') result.each { |key, value| next unless (value.instance_of?(Hash) && (value.size == 1)) child_key, child_value = value.to_a[0] if @options['grouptags'][key] == child_key result[key] = child_value end } end # Fold Hases containing a single anonymous Array up into just the Array. if count == 1 anonymoustag = @options['anonymoustag'] if result.has_key?(anonymoustag) && result[anonymoustag].instance_of?(Array) return result[anonymoustag] end end if result.empty? && @options.has_key?('suppressempty') return @options['suppressempty'] == '' ? '' : nil end result end # Collapses a text node and merges it with an existing Hash, if # possible. # Thanks to Curtis Schofield for reporting a subtle bug. # # hash:: # Hash to merge text node value with, if possible. # element:: # Text node to be collapsed. def collapse_text_node(hash, element) value = node_to_text(element) if empty(value) && !element.has_attributes? return {} end if element.has_attributes? && !@options['noattr'] return merge(hash, @options['contentkey'], value) else if @options['forcecontent'] return merge(hash, @options['contentkey'], value) else return value end end end # Folds all arrays in a Hash. # # hash:: # Hash to be folded. def fold_arrays(hash) fold_amount = 0 keyattr = @options['keyattr'] if (keyattr.instance_of?(Array) || keyattr.instance_of?(Hash)) hash.each { |key, value| if value.instance_of?(Array) if keyattr.instance_of?(Array) hash[key] = fold_array(value) else hash[key] = fold_array_by_name(key, value) end fold_amount += 1 end } end fold_amount end # Folds an Array to a Hash, if possible. Folding happens # according to the content of keyattr, which has to be # an array. # # array:: # Array to be folded. def fold_array(array) hash = Hash.new array.each { |x| return array unless x.instance_of?(Hash) key_matched = false @options['keyattr'].each { |key| if x.has_key?(key) key_matched = true value = x[key] return array if value.instance_of?(Hash) || value.instance_of?(Array) value = normalise_space(value) if @options['normalisespace'] == 1 x.delete(key) hash[value] = x break end } return array unless key_matched } hash = collapse_content(hash) if @options['collapseagain'] hash end # Folds an Array to a Hash, if possible. Folding happens # according to the content of keyattr, which has to be # a Hash. # # name:: # Name of the attribute to be folded upon. # array:: # Array to be folded. def fold_array_by_name(name, array) return array unless @options['keyattr'].has_key?(name) key, flag = @options['keyattr'][name] hash = Hash.new array.each { |x| if x.instance_of?(Hash) && x.has_key?(key) value = x[key] return array if value.instance_of?(Hash) || value.instance_of?(Array) value = normalise_space(value) if @options['normalisespace'] == 1 hash[value] = x hash[value]["-#{key}"] = hash[value][key] if flag == '-' hash[value].delete(key) unless flag == '+' else $stderr.puts("Warning: <#{name}> element has no '#{key}' attribute.") return array end } hash = collapse_content(hash) if @options['collapseagain'] hash end # Tries to collapse a Hash even more ;-) # # hash:: # Hash to be collapsed again. def collapse_content(hash) content_key = @options['contentkey'] hash.each_value { |value| return hash unless value.instance_of?(Hash) && value.size == 1 && value.has_key?(content_key) hash.each_key { |key| hash[key] = hash[key][content_key] } } hash end # Adds a new key/value pair to an existing Hash. If the key to be added # does already exist and the existing value associated with key is not # an Array, it will be converted into an Array. Then the new value is # appended to that Array. # # hash:: # Hash to add key/value pair to. # key:: # Key to be added. # value:: # Value to be associated with key. def merge(hash, key, value) if value.instance_of?(String) value = normalise_space(value) if @options['normalisespace'] == 2 # do variable substitutions unless @_var_values.nil? || @_var_values.empty? value.gsub!(/\$\{(\w+)\}/) { |x| get_var($1) } end # look for variable definitions if @options.has_key?('varattr') varattr = @options['varattr'] if hash.has_key?(varattr) set_var(hash[varattr], value) end end end if hash.has_key?(key) if hash[key].instance_of?(Array) hash[key] << value else hash[key] = [ hash[key], value ] end elsif value.instance_of?(Array) # Handle anonymous arrays. hash[key] = [ value ] else if force_array?(key) hash[key] = [ value ] else hash[key] = value end end hash end # Checks, if the 'forcearray' option has to be used for # a certain key. def force_array?(key) return false if key == @options['contentkey'] return true if @options['forcearray'] == true forcearray = @options['forcearray'] if forcearray.instance_of?(Hash) return true if forcearray.has_key?(key) return false unless forcearray.has_key?('_regex') forcearray['_regex'].each { |x| return true if key =~ x } end return false end # Converts the attributes array of a document node into a Hash. # Returns an empty Hash, if node has no attributes. # # node:: # Document node to extract attributes from. def get_attributes(node) attributes = {} node.attributes.each { |n,v| attributes[n] = v } attributes end # Determines, if a document element has mixed content. # # element:: # Document element to be checked. def has_mixed_content?(element) if element.has_text? && element.has_elements? return true if element.texts.join('') !~ /^\s*$/s end false end # Called when a variable definition is encountered in the XML. # A variable definition looks like # value # where attrname matches the varattr setting. def set_var(name, value) @_var_values[name] = value end # Called during variable substitution to get the value for the # named variable. def get_var(name) if @_var_values.has_key?(name) return @_var_values[name] else return "${#{name}}" end end # Recurses through a data structure building up and returning an # XML representation of that structure as a string. # # ref:: # Reference to the data structure to be encoded. # name:: # The XML tag name to be used for this item. # indent:: # A string of spaces for use as the current indent level. def value_to_xml(ref, name, indent) named = !name.nil? && name != '' nl = @options.has_key?('noindent') ? '' : "\n" if !scalar(ref) if @ancestors.member?(ref) raise ArgumentError, "Circular data structures not supported!" end @ancestors << ref else if named return [indent, '<', name, '>', @options['noescape'] ? ref.to_s : escape_value(ref.to_s), '', nl].join('') else return ref.to_s + nl end end # Unfold hash to array if possible. if ref.instance_of?(Hash) && !ref.empty? && !@options['keyattr'].empty? && indent != '' ref = hash_to_array(name, ref) end result = [] if ref.instance_of?(Hash) # Reintermediate grouped values if applicable. if @options.has_key?('grouptags') ref.each { |key, value| if @options['grouptags'].has_key?(key) ref[key] = { @options['grouptags'][key] => value } end } end nested = [] text_content = nil if named result << indent << '<' << name end if !ref.empty? ref.each { |key, value| next if !key.nil? && key[0, 1] == '-' if value.nil? unless @options.has_key?('suppressempty') && @options['suppressempty'].nil? raise ArgumentError, "Use of uninitialized value!" end value = {} end if !scalar(value) || @options['noattr'] nested << value_to_xml(value, key, indent + @options['indent']) else value = value.to_s value = escape_value(value) unless @options['noescape'] if key == @options['contentkey'] text_content = value else result << ' ' << key << '="' << value << '"' end end } else text_content = '' end if !nested.empty? || !text_content.nil? if named result << '>' if !text_content.nil? result << text_content nested[0].sub!(/^\s+/, '') if !nested.empty? else result << nl end if !nested.empty? result << nested << indent end result << '' << nl else result << nested end else result << ' />' << nl end elsif ref.instance_of?(Array) ref.each { |value| if scalar(value) result << indent << '<' << name << '>' result << (@options['noescape'] ? value.to_s : escape_value(value.to_s)) result << '' << nl elsif value.instance_of?(Hash) result << value_to_xml(value, name, indent) else result << indent << '<' << name << '>' << nl result << value_to_xml(value, @options['anonymoustag'], indent + @options['indent']) result << indent << '' << nl end } else # Probably, this is obsolete. raise ArgumentError, "Can't encode a value of type: #{ref.type}." end @ancestors.pop if !scalar(ref) result.join('') end # Checks, if a certain value is a "scalar" value. Whatever # that will be in Ruby ... ;-) # # value:: # Value to be checked. def scalar(value) return false if value.instance_of?(Hash) || value.instance_of?(Array) return true end # Attempts to unfold a hash of hashes into an array of hashes. Returns # a reference to th array on success or the original hash, if unfolding # is not possible. # # parent:: # # hashref:: # Reference to the hash to be unfolded. def hash_to_array(parent, hashref) arrayref = [] hashref.each { |key, value| return hashref unless value.instance_of?(Hash) if @options['keyattr'].instance_of?(Hash) return hashref unless @options['keyattr'].has_key?(parent) arrayref << { @options['keyattr'][parent][0] => key }.update(value) else arrayref << { @options['keyattr'][0] => key }.update(value) end } arrayref end # Replaces XML markup characters by their external entities. # # data:: # The string to be escaped. def escape_value(data) return data if data.nil? || data == '' result = data.dup result.gsub!('&', '&') result.gsub!('<', '<') result.gsub!('>', '>') result.gsub!('"', '"') result.gsub!("'", ''') result end # Removes leading and trailing whitespace and sequences of # whitespaces from a string. # # text:: # String to be normalised. def normalise_space(text) text.sub!(/^\s+/, '') text.sub!(/\s+$/, '') text.gsub!(/\s\s+/, ' ') text end # Checks, if an object is nil, an empty String or an empty Hash. # Thanks to Norbert Gawor for a bugfix. # # value:: # Value to be checked for emptyness. def empty(value) case value when Hash return value.empty? when String return value !~ /\S/m else return value.nil? end end # Converts a document node into a String. # If the node could not be converted into a String # for any reason, default will be returned. # # node:: # Document node to be converted. # default:: # Value to be returned, if node could not be converted. def node_to_text(node, default = nil) if node.instance_of?(Element) return node.texts.join('') elsif node.instance_of?(Attribute) return node.value.nil? ? default : node.value.strip elsif node.instance_of?(Text) return node.to_s.strip else return default end end # Parses an XML string and returns the according document. # # xml_string:: # XML string to be parsed. # # The following exception may be raised: # # REXML::ParseException:: # If the specified file is not wellformed. def parse(xml_string) Document.new(xml_string) end # Searches in a list of paths for a certain file. Returns # the full path to the file, if it could be found. Otherwise, # an exception will be raised. # # filename:: # Name of the file to search for. # searchpath:: # List of paths to search in. def find_xml_file(file, searchpath) filename = File::basename(file) if filename != file return file if File::file?(file) else searchpath.each { |path| full_path = File::join(path, filename) return full_path if File::file?(full_path) } end if searchpath.empty? return file if File::file?(file) raise ArgumentError, "File does not exist: #{file}." end raise ArgumentError, "Could not find <#{filename}> in <#{searchpath.join(':')}>" end # Loads and parses an XML configuration file. # # filename:: # Name of the configuration file to be loaded. # # The following exceptions may be raised: # # Errno::ENOENT:: # If the specified file does not exist. # REXML::ParseException:: # If the specified file is not wellformed. def load_xml_file(filename) parse(File.readlines(filename).to_s) end # Caches the data belonging to a certain file. # # data:: # Data to be cached. # filename:: # Name of file the data was read from. def put_into_cache(data, filename) if @options.has_key?('cache') @options['cache'].each { |scheme| case(scheme) when 'storable' @@cache.save_storable(data, filename) when 'mem_share' @@cache.save_mem_share(data, filename) when 'mem_copy' @@cache.save_mem_copy(data, filename) else raise ArgumentError, "Unsupported caching scheme: <#{scheme}>." end } end end end # vim:sw=2 module ActionController #:nodoc: module Verification #:nodoc: def self.append_features(base) #:nodoc: super base.extend(ClassMethods) end # This module provides a class-level method for specifying that certain # actions are guarded against being called without certain prerequisites # being met. This is essentially a special kind of before_filter. # # An action may be guarded against being invoked without certain request # parameters being set, or without certain session values existing. # # When a verification is violated, values may be inserted into the flash, and # a specified redirection is triggered. # # Usage: # # class GlobalController < ActionController::Base # # prevent the #update_settings action from being invoked unless # # the 'admin_privileges' request parameter exists. # verify :params => "admin_privileges", :only => :update_post, # :redirect_to => { :action => "settings" } # # # disallow a post from being updated if there was no information # # submitted with the post, and if there is no active post in the # # session, and if there is no "note" key in the flash. # verify :params => "post", :session => "post", "flash" => "note", # :only => :update_post, # :add_flash => { "alert" => "Failed to create your message" }, # :redirect_to => :category_url # module ClassMethods # Verify the given actions so that if certain prerequisites are not met, # the user is redirected to a different action. The +options+ parameter # is a hash consisting of the following key/value pairs: # # * :params: a single key or an array of keys that must # be in the params hash in order for the action(s) to be safely # called. # * :session: a single key or an array of keys that must # be in the @session in order for the action(s) to be safely called. # * :flash: a single key or an array of keys that must # be in the flash in order for the action(s) to be safely called. # * :method: a single key or an array of keys--any one of which # must match the current request method in order for the action(s) to # be safely called. (The key should be a symbol: :get or # :post, for example.) # * :xhr: true/false option to ensure that the request is coming # from an Ajax call or not. # * :add_flash: a hash of name/value pairs that should be merged # into the session's flash if the prerequisites cannot be satisfied. # * :redirect_to: the redirection parameters to be used when # redirecting if the prerequisites cannot be satisfied. # * :render: the render parameters to be used when # the prerequisites cannot be satisfied. # * :only: only apply this verification to the actions specified # in the associated array (may also be a single value). # * :except: do not apply this verification to the actions # specified in the associated array (may also be a single value). def verify(options={}) filter_opts = { :only => options[:only], :except => options[:except] } before_filter(filter_opts) do |c| c.send :verify_action, options end end end def verify_action(options) #:nodoc: prereqs_invalid = [*options[:params] ].find { |v| @params[v].nil? } || [*options[:session]].find { |v| @session[v].nil? } || [*options[:flash] ].find { |v| flash[v].nil? } if !prereqs_invalid && options[:method] prereqs_invalid ||= [*options[:method]].all? { |v| @request.method != v.to_sym } end prereqs_invalid ||= (request.xhr? != options[:xhr]) unless options[:xhr].nil? if prereqs_invalid flash.update(options[:add_flash]) if options[:add_flash] unless performed? render(options[:render]) if options[:render] redirect_to(options[:redirect_to]) if options[:redirect_to] end return false end true end private :verify_action 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. #++ $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__))) unless defined?(ActiveSupport) begin $:.unshift(File.dirname(__FILE__) + "/../../activesupport/lib") require 'active_support' rescue LoadError require 'rubygems' require_gem 'activesupport' end end require 'action_controller/base' require 'action_controller/deprecated_redirects' require 'action_controller/request' require 'action_controller/deprecated_request_methods' require 'action_controller/rescue' require 'action_controller/benchmarking' require 'action_controller/flash' require 'action_controller/filters' require 'action_controller/layout' require 'action_controller/dependencies' require 'action_controller/mime_responds' require 'action_controller/pagination' require 'action_controller/scaffolding' require 'action_controller/helpers' require 'action_controller/cookies' require 'action_controller/cgi_process' require 'action_controller/caching' require 'action_controller/verification' require 'action_controller/streaming' require 'action_controller/session_management' require 'action_controller/components' require 'action_controller/macros/auto_complete' require 'action_controller/macros/in_place_editing' require 'action_view' ActionController::Base.template_class = ActionView::Base ActionController::Base.class_eval do include ActionController::Flash include ActionController::Filters include ActionController::Layout include ActionController::Benchmarking include ActionController::Rescue include ActionController::Dependencies include ActionController::MimeResponds include ActionController::Pagination include ActionController::Scaffolding include ActionController::Helpers include ActionController::Cookies include ActionController::Caching include ActionController::Verification include ActionController::Streaming include ActionController::SessionManagement include ActionController::Components include ActionController::Macros::AutoComplete include ActionController::Macros::InPlaceEditing end module ActionPack #:nodoc: module VERSION #:nodoc: MAJOR = 1 MINOR = 12 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. #++ require 'action_pack/version' require 'erb' module ActionView #:nodoc: class ActionViewError < StandardError #:nodoc: end # Action View templates can be written in three ways. If the template file has a +.rhtml+ extension then it uses a mixture of ERb # (included in Ruby) and HTML. If the template file has a +.rxml+ extension then Jim Weirich's Builder::XmlMarkup library is used. # If the template file has a +.rjs+ extension then it will use ActionView::Helpers::PrototypeHelper::JavaScriptGenerator. # # = ERb # # You trigger ERb by using embeddings such as <% %>, <% -%>, and <%= %>. The <%= %> tag set is used when you want output. Consider the # following loop for names: # # Names of all the people # <% for person in @people %> # Name: <%= person.name %>
# <% end %> # # The loop is setup in regular embedding tags <% %> and the name is written using the output embedding tag <%= %>. Note that this # is not just a usage suggestion. Regular output functions like print or puts won't work with ERb templates. So this would be wrong: # # Hi, Mr. <% puts "Frodo" %> # # If you absolutely must write from within a function, you can use the TextHelper#concat # # <%- and -%> suppress leading and trailing whitespace, including the trailing newline, and can be used interchangeably with <% and %>. # # == Using sub templates # # Using sub templates allows you to sidestep tedious replication and extract common display structures in shared templates. The # classic example is the use of a header and footer (even though the Action Pack-way would be to use Layouts): # # <%= render "shared/header" %> # Something really specific and terrific # <%= render "shared/footer" %> # # As you see, we use the output embeddings for the render methods. The render call itself will just return a string holding the # result of the rendering. The output embedding writes it to the current template. # # But you don't have to restrict yourself to static includes. Templates can share variables amongst themselves by using instance # variables defined using the regular embedding tags. Like this: # # <% @page_title = "A Wonderful Hello" %> # <%= render "shared/header" %> # # Now the header can pick up on the @page_title variable and use it for outputting a title tag: # # <%= @page_title %> # # == Passing local variables to sub templates # # You can pass local variables to sub templates by using a hash with the variable names as keys and the objects as values: # # <%= render "shared/header", { "headline" => "Welcome", "person" => person } %> # # These can now be accessed in shared/header with: # # Headline: <%= headline %> # First name: <%= person.first_name %> # # == Template caching # # By default, Rails will compile each template to a method in order to render it. When you alter a template, Rails will # check the file's modification time and recompile it. # # == Builder # # Builder templates are a more programmatic alternative to ERb. They are especially useful for generating XML content. An +XmlMarkup+ object # named +xml+ is automatically made available to templates with a +.rxml+ extension. # # Here are some basic examples: # # xml.em("emphasized") # => emphasized # xml.em { xml.b("emp & bold") } # => emph & bold # xml.a("A Link", "href"=>"http://onestepback.org") # => A Link # xml.target("name"=>"compile", "option"=>"fast") # => # # NOTE: order of attributes is not specified. # # Any method with a block will be treated as an XML markup tag with nested markup in the block. For example, the following: # # xml.div { # xml.h1(@person.name) # xml.p(@person.bio) # } # # would produce something like: # #
#

David Heinemeier Hansson

#

A product of Danish Design during the Winter of '79...

#
# # A full-length RSS example actually used on Basecamp: # # xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do # xml.channel do # xml.title(@feed_title) # xml.link(@url) # xml.description "Basecamp: Recent items" # xml.language "en-us" # xml.ttl "40" # # for item in @recent_items # xml.item do # xml.title(item_title(item)) # xml.description(item_description(item)) if item_description(item) # xml.pubDate(item_pubDate(item)) # xml.guid(@person.firm.account.url + @recent_items.url(item)) # xml.link(@person.firm.account.url + @recent_items.url(item)) # # xml.tag!("dc:creator", item.author_name) if item_has_creator?(item) # end # end # end # end # # More builder documentation can be found at http://builder.rubyforge.org. # # == JavaScriptGenerator # # JavaScriptGenerator templates end in +.rjs+. Unlike conventional templates which are used to # render the results of an action, these templates generate instructions on how to modify an already rendered page. This makes it easy to # modify multiple elements on your page in one declarative Ajax response. Actions with these templates are called in the background with Ajax # and make updates to the page where the request originated from. # # An instance of the JavaScriptGenerator object named +page+ is automatically made available to your template, which is implicitly wrapped in an ActionView::Helpers::PrototypeHelper#update_page block. # # When an .rjs action is called with +link_to_remote+, the generated JavaScript is automatically evaluated. Example: # # link_to_remote :url => {:action => 'delete'} # # The subsequently rendered +delete.rjs+ might look like: # # page.replace_html 'sidebar', :partial => 'sidebar' # page.remove "person-#{@person.id}" # page.visual_effect :highlight, 'user-list' # # This refreshes the sidebar, removes a person element and highlights the user list. # # See the ActionView::Helpers::PrototypeHelper::JavaScriptGenerator documentation for more details. class Base include ERB::Util attr_reader :first_render attr_accessor :base_path, :assigns, :template_extension attr_accessor :controller attr_reader :logger, :params, :request, :response, :session, :headers, :flash # Specify trim mode for the ERB compiler. Defaults to '-'. # See ERB documentation for suitable values. @@erb_trim_mode = '-' cattr_accessor :erb_trim_mode # Specify whether file modification times should be checked to see if a template needs recompilation @@cache_template_loading = false cattr_accessor :cache_template_loading # Specify whether file extension lookup should be cached. # Should be +false+ for development environments. Defaults to +true+. @@cache_template_extensions = true cattr_accessor :cache_template_extensions # Specify whether local_assigns should be able to use string keys. # Defaults to +true+. String keys are deprecated and will be removed # shortly. @@local_assigns_support_string_keys = true cattr_accessor :local_assigns_support_string_keys # Specify whether RJS responses should be wrapped in a try/catch block # that alert()s the caught exception (and then re-raises it). @@debug_rjs = false cattr_accessor :debug_rjs @@template_handlers = HashWithIndifferentAccess.new module CompiledTemplates #:nodoc: # holds compiled template code end include CompiledTemplates # maps inline templates to their method names @@method_names = {} # map method names to their compile time @@compile_time = {} # map method names to the names passed in local assigns so far @@template_args = {} # count the number of inline templates @@inline_template_count = 0 # maps template paths without extension to their file extension returned by pick_template_extension. # if for a given path, path.ext1 and path.ext2 exist on the file system, the order of extensions # used by pick_template_extension determines whether ext1 or ext2 will be stored @@cached_template_extension = {} class ObjectWrapper < Struct.new(:value) #:nodoc: end def self.load_helpers(helper_dir)#:nodoc: Dir.foreach(helper_dir) do |helper_file| next unless helper_file =~ /^([a-z][a-z_]*_helper).rb$/ require File.join(helper_dir, $1) helper_module_name = $1.camelize class_eval("include ActionView::Helpers::#{helper_module_name}") if Helpers.const_defined?(helper_module_name) end end # Register a class that knows how to handle template files with the given # extension. This can be used to implement new template types. # The constructor for the class must take the ActiveView::Base instance # as a parameter, and the class must implement a #render method that # takes the contents of the template to render as well as the Hash of # local assigns available to the template. The #render method ought to # return the rendered template as a string. def self.register_template_handler(extension, klass) @@template_handlers[extension] = klass end def initialize(base_path = nil, assigns_for_first_render = {}, controller = nil)#:nodoc: @base_path, @assigns = base_path, assigns_for_first_render @assigns_added = nil @controller = controller @logger = controller && controller.logger end # Renders the template present at template_path. If use_full_path is set to true, # it's relative to the template_root, otherwise it's absolute. The hash in local_assigns # is made available as local variables. def render_file(template_path, use_full_path = true, local_assigns = {}) #:nodoc: @first_render ||= template_path if use_full_path template_path_without_extension, template_extension = path_and_extension(template_path) if template_extension template_file_name = full_template_path(template_path_without_extension, template_extension) else template_extension = pick_template_extension(template_path).to_s template_file_name = full_template_path(template_path, template_extension) end else template_file_name = template_path template_extension = template_path.split('.').last end template_source = nil # Don't read the source until we know that it is required begin render_template(template_extension, template_source, template_file_name, local_assigns) rescue Exception => e if TemplateError === e e.sub_template_of(template_file_name) raise e else raise TemplateError.new(@base_path, template_file_name, @assigns, template_source, e) end end end # Renders the template present at template_path (relative to the template_root). # The hash in local_assigns is made available as local variables. def render(options = {}, old_local_assigns = {}, &block) #:nodoc: if options.is_a?(String) render_file(options, true, old_local_assigns) elsif options == :update update_page(&block) elsif options.is_a?(Hash) options[:locals] ||= {} options[:use_full_path] = options[:use_full_path].nil? ? true : options[:use_full_path] if options[:file] render_file(options[:file], options[:use_full_path], options[:locals]) elsif options[:partial] && options[:collection] render_partial_collection(options[:partial], options[:collection], options[:spacer_template], options[:locals]) elsif options[:partial] render_partial(options[:partial], ActionView::Base::ObjectWrapper.new(options[:object]), options[:locals]) elsif options[:inline] render_template(options[:type] || :rhtml, options[:inline], nil, options[:locals] || {}) end end end # Renders the +template+ which is given as a string as either rhtml or rxml depending on template_extension. # The hash in local_assigns is made available as local variables. def render_template(template_extension, template, file_path = nil, local_assigns = {}) #:nodoc: if handler = @@template_handlers[template_extension] template ||= read_template_file(file_path, template_extension) # Make sure that a lazyily-read template is loaded. delegate_render(handler, template, local_assigns) else compile_and_render_template(template_extension, template, file_path, local_assigns) end end # Render the provided template with the given local assigns. If the template has not been rendered with the provided # local assigns yet, or if the template has been updated on disk, then the template will be compiled to a method. # # Either, but not both, of template and file_path may be nil. If file_path is given, the template # will only be read if it has to be compiled. # def compile_and_render_template(extension, template = nil, file_path = nil, local_assigns = {}) #:nodoc: # compile the given template, if necessary if compile_template?(template, file_path, local_assigns) template ||= read_template_file(file_path, extension) compile_template(extension, template, file_path, local_assigns) end # Get the method name for this template and run it method_name = @@method_names[file_path || template] evaluate_assigns local_assigns = local_assigns.symbolize_keys if @@local_assigns_support_string_keys send(method_name, local_assigns) do |*name| instance_variable_get "@content_for_#{name.first || 'layout'}" end end def pick_template_extension(template_path)#:nodoc: if @@cache_template_extensions @@cached_template_extension[template_path] ||= find_template_extension_for(template_path) else find_template_extension_for(template_path) end end def delegate_template_exists?(template_path)#:nodoc: @@template_handlers.find { |k,| template_exists?(template_path, k) } end def erb_template_exists?(template_path)#:nodoc: template_exists?(template_path, :rhtml) end def builder_template_exists?(template_path)#:nodoc: template_exists?(template_path, :rxml) end def javascript_template_exists?(template_path)#:nodoc: template_exists?(template_path, :rjs) end def file_exists?(template_path)#:nodoc: template_file_name, template_file_extension = path_and_extension(template_path) if template_file_extension template_exists?(template_file_name, template_file_extension) else cached_template_extension(template_path) || %w(erb builder javascript delegate).any? do |template_type| send("#{template_type}_template_exists?", template_path) end end end # Returns true is the file may be rendered implicitly. def file_public?(template_path)#:nodoc: template_path.split('/').last[0,1] != '_' end private def full_template_path(template_path, extension) "#{@base_path}/#{template_path}.#{extension}" end def template_exists?(template_path, extension) file_path = full_template_path(template_path, extension) @@method_names.has_key?(file_path) || FileTest.exists?(file_path) end def path_and_extension(template_path) template_path_without_extension = template_path.sub(/\.(\w+)$/, '') [ template_path_without_extension, $1 ] end def cached_template_extension(template_path) @@cache_template_extensions && @@cached_template_extension[template_path] end def find_template_extension_for(template_path) if match = delegate_template_exists?(template_path) match.first.to_sym elsif erb_template_exists?(template_path): :rhtml elsif builder_template_exists?(template_path): :rxml elsif javascript_template_exists?(template_path): :rjs else raise ActionViewError, "No rhtml, rxml, rjs or delegate template found for #{template_path}" end end # This method reads a template file. def read_template_file(template_path, extension) File.read(template_path) end def evaluate_assigns unless @assigns_added assign_variables_from_controller @assigns_added = true end end def delegate_render(handler, template, local_assigns) handler.new(self).render(template, local_assigns) end def assign_variables_from_controller @assigns.each { |key, value| instance_variable_set("@#{key}", value) } end # Return true if the given template was compiled for a superset of the keys in local_assigns def supports_local_assigns?(render_symbol, local_assigns) local_assigns.empty? || ((args = @@template_args[render_symbol]) && local_assigns.all? { |k,_| args.has_key?(k) }) end # Check whether compilation is necessary. # Compile if the inline template or file has not been compiled yet. # Or if local_assigns has a new key, which isn't supported by the compiled code yet. # Or if the file has changed on disk and checking file mods hasn't been disabled. def compile_template?(template, file_name, local_assigns) method_key = file_name || template render_symbol = @@method_names[method_key] if @@compile_time[render_symbol] && supports_local_assigns?(render_symbol, local_assigns) if file_name && !@@cache_template_loading @@compile_time[render_symbol] < File.mtime(file_name) || (File.symlink?(file_name) ? @@compile_time[render_symbol] < File.lstat(file_name).mtime : false) end else true end end # Create source code for given template def create_template_source(extension, template, render_symbol, locals) if template_requires_setup?(extension) body = case extension.to_sym when :rxml "xml = Builder::XmlMarkup.new(:indent => 2)\n" + "@controller.headers['Content-Type'] ||= 'application/xml'\n" + template when :rjs "@controller.headers['Content-Type'] ||= 'text/javascript'\n" + "update_page do |page|\n#{template}\nend" end else body = ERB.new(template, nil, @@erb_trim_mode).src end @@template_args[render_symbol] ||= {} locals_keys = @@template_args[render_symbol].keys | locals @@template_args[render_symbol] = locals_keys.inject({}) { |h, k| h[k] = true; h } locals_code = "" locals_keys.each do |key| locals_code << "#{key} = local_assigns[:#{key}] if local_assigns.has_key?(:#{key})\n" end "def #{render_symbol}(local_assigns)\n#{locals_code}#{body}\nend" end def template_requires_setup?(extension) templates_requiring_setup.include? extension.to_s end def templates_requiring_setup %w(rxml rjs) end def assign_method_name(extension, template, file_name) method_name = '_run_' method_name << "#{extension}_" if extension if file_name file_path = File.expand_path(file_name) base_path = File.expand_path(@base_path) i = file_path.index(base_path) l = base_path.length method_name_file_part = i ? file_path[i+l+1,file_path.length-l-1] : file_path.clone method_name_file_part.sub!(/\.r(html|xml|js)$/,'') method_name_file_part.tr!('/:-', '_') method_name_file_part.gsub!(/[^a-zA-Z0-9_]/){|s| s[0].to_s} method_name += method_name_file_part else @@inline_template_count += 1 method_name << @@inline_template_count.to_s end @@method_names[file_name || template] = method_name.intern end def compile_template(extension, template, file_name, local_assigns) method_key = file_name || template render_symbol = @@method_names[method_key] || assign_method_name(extension, template, file_name) render_source = create_template_source(extension, template, render_symbol, local_assigns.keys) line_offset = @@template_args[render_symbol].size if extension case extension.to_sym when :rxml, :rjs line_offset += 2 end end begin unless file_name.blank? CompiledTemplates.module_eval(render_source, file_name, -line_offset) else CompiledTemplates.module_eval(render_source, 'compiled-template', -line_offset) end rescue Object => e if logger logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}" logger.debug "Function body: #{render_source}" logger.debug "Backtrace: #{e.backtrace.join("\n")}" end raise TemplateError.new(@base_path, method_key, @assigns, template, e) end @@compile_time[render_symbol] = Time.now # logger.debug "Compiled template #{method_key}\n ==> #{render_symbol}" if logger end end end require 'action_view/template_error' module ActionView # CompiledTemplates modules hold methods that have been compiled. # Templates are compiled into these methods so that they do not need to be # re-read and re-parsed each request. # # Each template may be compiled into one or more methods. Each method accepts a given # set of parameters which is used to implement local assigns passing. # # To use a compiled template module, create a new instance and include it into the class # in which you want the template to be rendered. class CompiledTemplates < Module #:nodoc: attr_reader :method_names def initialize @method_names = Hash.new do |hash, key| hash[key] = "__compiled_method_#{(hash.length + 1)}" end @mtimes = {} end # Return the full key for the given identifier and argument names def full_key(identifier, arg_names) [identifier, arg_names] end # Return the selector for this method or nil if it has not been compiled def selector(identifier, arg_names) key = full_key(identifier, arg_names) method_names.key?(key) ? method_names[key] : nil end alias :compiled? :selector # Return the time at which the method for the given identifier and argument names was compiled. def mtime(identifier, arg_names) @mtimes[full_key(identifier, arg_names)] end # Compile the provided source code for the given argument names and with the given initial line number. # The identifier should be unique to this source. # # The file_name, if provided will appear in backtraces. If not provded, the file_name defaults # to the identifier. # # This method will return the selector for the compiled version of this method. def compile_source(identifier, arg_names, source, initial_line_number = 0, file_name = nil) file_name ||= identifier name = method_names[full_key(identifier, arg_names)] arg_desc = arg_names.empty? ? '' : "(#{arg_names * ', '})" fake_file_name = "#{file_name}#{arg_desc}" # Include the arguments for this version (for now) method_def = wrap_source(name, arg_names, source) begin module_eval(method_def, fake_file_name, initial_line_number) @mtimes[full_key(identifier, arg_names)] = Time.now rescue Object => e e.blame_file! identifier raise end name end # Wrap the provided source in a def ... end block. def wrap_source(name, arg_names, source) "def #{name}(#{arg_names * ', '})\n#{source}\nend" end end end require 'cgi' require File.dirname(__FILE__) + '/form_helper' module ActionView class Base @@field_error_proc = Proc.new{ |html_tag, instance| "
#{html_tag}
" } cattr_accessor :field_error_proc end module Helpers # The Active Record Helper makes it easier to create forms for records kept in instance variables. The most far-reaching is the form # method that creates a complete form for all the basic content types of the record (not associations or aggregations, though). This # is a great of making the record quickly available for editing, but likely to prove lackluster for a complicated real-world form. # In that case, it's better to use the input method and the specialized form methods in link:classes/ActionView/Helpers/FormHelper.html module ActiveRecordHelper # Returns a default input tag for the type of object returned by the method. Example # (title is a VARCHAR column and holds "Hello World"): # input("post", "title") => # def input(record_name, method, options = {}) InstanceTag.new(record_name, method, self).to_tag(options) end # Returns an entire form with input tags and everything for a specified Active Record object. Example # (post is a new record that has a title using VARCHAR and a body using TEXT): # form("post") => #
#

#
# #

#

#
# #

# #
# # It's possible to specialize the form builder by using a different action name and by supplying another # block renderer. Example (entry is a new record that has a message attribute using VARCHAR): # # form("entry", :action => "sign", :input_block => # Proc.new { |record, column| "#{column.human_name}: #{input(record, column.name)}
" }) => # #
# Message: #
# #
# # It's also possible to add additional content to the form by giving it a block, such as: # # form("entry", :action => "sign") do |form| # form << content_tag("b", "Department") # form << collection_select("department", "id", @departments, "id", "name") # end def form(record_name, options = {}) record = instance_variable_get("@#{record_name}") options = options.symbolize_keys options[:action] ||= record.new_record? ? "create" : "update" action = url_for(:action => options[:action], :id => record) submit_value = options[:submit_value] || options[:action].gsub(/[^\w]/, '').capitalize contents = '' contents << hidden_field(record_name, :id) unless record.new_record? contents << all_input_tags(record, record_name, options) yield contents if block_given? contents << submit_tag(submit_value) content_tag('form', contents, :action => action, :method => 'post', :enctype => options[:multipart] ? 'multipart/form-data': nil) end # Returns a string containing the error message attached to the +method+ on the +object+, if one exists. # This error message is wrapped in a DIV tag, which can be specialized to include both a +prepend_text+ and +append_text+ # to properly introduce the error and a +css_class+ to style it accordingly. Examples (post has an error message # "can't be empty" on the title attribute): # # <%= error_message_on "post", "title" %> => #
can't be empty
# # <%= error_message_on "post", "title", "Title simply ", " (or it won't work)", "inputError" %> => #
Title simply can't be empty (or it won't work)
def error_message_on(object, method, prepend_text = "", append_text = "", css_class = "formError") if errors = instance_variable_get("@#{object}").errors.on(method) content_tag("div", "#{prepend_text}#{errors.is_a?(Array) ? errors.first : errors}#{append_text}", :class => css_class) end end # Returns a string with a div containing all the error messages for the object located as an instance variable by the name # of object_name. This div can be tailored by the following options: # # * header_tag - Used for the header of the error div (default: h2) # * id - The id of the error div (default: errorExplanation) # * class - The class of the error div (default: errorExplanation) # # NOTE: This is a pre-packaged presentation of the errors with embedded strings and a certain HTML structure. If what # you need is significantly different from the default presentation, it makes plenty of sense to access the object.errors # instance yourself and set it up. View the source of this method to see how easy it is. def error_messages_for(object_name, options = {}) options = options.symbolize_keys object = instance_variable_get("@#{object_name}") if object && !object.errors.empty? content_tag("div", content_tag( options[:header_tag] || "h2", "#{pluralize(object.errors.count, "error")} prohibited this #{object_name.to_s.gsub("_", " ")} from being saved" ) + content_tag("p", "There were problems with the following fields:") + content_tag("ul", object.errors.full_messages.collect { |msg| content_tag("li", msg) }), "id" => options[:id] || "errorExplanation", "class" => options[:class] || "errorExplanation" ) else "" end end private def all_input_tags(record, record_name, options) input_block = options[:input_block] || default_input_block record.class.content_columns.collect{ |column| input_block.call(record_name, column) }.join("\n") end def default_input_block Proc.new { |record, column| %(


#{input(record, column.name)}

) } end end class InstanceTag #:nodoc: def to_tag(options = {}) case column_type when :string field_type = @method_name.include?("password") ? "password" : "text" to_input_field_tag(field_type, options) when :text to_text_area_tag(options) when :integer, :float to_input_field_tag("text", options) when :date to_date_select_tag(options) when :datetime, :timestamp to_datetime_select_tag(options) when :boolean to_boolean_select_tag(options) end end alias_method :tag_without_error_wrapping, :tag def tag(name, options) if object.respond_to?("errors") && object.errors.respond_to?("on") error_wrapping(tag_without_error_wrapping(name, options), object.errors.on(@method_name)) else tag_without_error_wrapping(name, options) end end alias_method :content_tag_without_error_wrapping, :content_tag def content_tag(name, value, options) if object.respond_to?("errors") && object.errors.respond_to?("on") error_wrapping(content_tag_without_error_wrapping(name, value, options), object.errors.on(@method_name)) else content_tag_without_error_wrapping(name, value, options) end end alias_method :to_date_select_tag_without_error_wrapping, :to_date_select_tag def to_date_select_tag(options = {}) if object.respond_to?("errors") && object.errors.respond_to?("on") error_wrapping(to_date_select_tag_without_error_wrapping(options), object.errors.on(@method_name)) else to_date_select_tag_without_error_wrapping(options) end end alias_method :to_datetime_select_tag_without_error_wrapping, :to_datetime_select_tag def to_datetime_select_tag(options = {}) if object.respond_to?("errors") && object.errors.respond_to?("on") error_wrapping(to_datetime_select_tag_without_error_wrapping(options), object.errors.on(@method_name)) else to_datetime_select_tag_without_error_wrapping(options) end end def error_wrapping(html_tag, has_error) has_error ? Base.field_error_proc.call(html_tag, self) : html_tag end def error_message object.errors.on(@method_name) end def column_type object.send("column_for_attribute", @method_name).type end end end end require 'cgi' require File.dirname(__FILE__) + '/url_helper' require File.dirname(__FILE__) + '/tag_helper' module ActionView module Helpers # Provides methods for linking a HTML page together with other assets, such as javascripts, stylesheets, and feeds. module AssetTagHelper # Returns a link tag that browsers and news readers can use to auto-detect a RSS or ATOM feed for this page. The +type+ can # either be :rss (default) or :atom and the +options+ follow the url_for style of declaring a link target. # # Examples: # auto_discovery_link_tag # => # # auto_discovery_link_tag(:atom) # => # # auto_discovery_link_tag(:rss, {:action => "feed"}) # => # # auto_discovery_link_tag(:rss, {:action => "feed"}, {:title => "My RSS"}) # => # def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {}) tag( "link", "rel" => tag_options[:rel] || "alternate", "type" => tag_options[:type] || "application/#{type}+xml", "title" => tag_options[:title] || type.to_s.upcase, "href" => url_options.is_a?(Hash) ? url_for(url_options.merge(:only_path => false)) : url_options ) end # Returns path to a javascript asset. Example: # # javascript_path "xmlhr" # => /javascripts/xmlhr.js def javascript_path(source) compute_public_path(source, 'javascripts', 'js') end JAVASCRIPT_DEFAULT_SOURCES = ['prototype', 'effects', 'dragdrop', 'controls'] unless const_defined?(:JAVASCRIPT_DEFAULT_SOURCES) @@javascript_default_sources = JAVASCRIPT_DEFAULT_SOURCES.dup # Returns a script include tag per source given as argument. Examples: # # javascript_include_tag "xmlhr" # => # # # javascript_include_tag "common.javascript", "/elsewhere/cools" # => # # # # javascript_include_tag :defaults # => # # # ... # *see below # # If there's an application.js file in your public/javascripts directory, # javascript_include_tag :defaults will automatically include it. This file # facilitates the inclusion of small snippets of JavaScript code, along the lines of # controllers/application.rb and helpers/application_helper.rb. def javascript_include_tag(*sources) options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { } if sources.include?(:defaults) sources = sources[0..(sources.index(:defaults))] + @@javascript_default_sources.dup + sources[(sources.index(:defaults) + 1)..sources.length] sources.delete(:defaults) sources << "application" if defined?(RAILS_ROOT) && File.exists?("#{RAILS_ROOT}/public/javascripts/application.js") end sources.collect { |source| source = javascript_path(source) content_tag("script", "", { "type" => "text/javascript", "src" => source }.merge(options)) }.join("\n") end # Register one or more additional JavaScript files to be included when # # javascript_include_tag :defaults # # is called. This method is intended to be called only from plugin initialization # to register extra .js files the plugin installed in public/javascripts. def self.register_javascript_include_default(*sources) @@javascript_default_sources.concat(sources) end def self.reset_javascript_include_default #:nodoc: @@javascript_default_sources = JAVASCRIPT_DEFAULT_SOURCES.dup end # Returns path to a stylesheet asset. Example: # # stylesheet_path "style" # => /stylesheets/style.css def stylesheet_path(source) compute_public_path(source, 'stylesheets', 'css') end # Returns a css link tag per source given as argument. Examples: # # stylesheet_link_tag "style" # => # # # stylesheet_link_tag "style", :media => "all" # => # # # stylesheet_link_tag "random.styles", "/css/stylish" # => # # def stylesheet_link_tag(*sources) options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { } sources.collect { |source| source = stylesheet_path(source) tag("link", { "rel" => "Stylesheet", "type" => "text/css", "media" => "screen", "href" => source }.merge(options)) }.join("\n") end # Returns path to an image asset. Example: # # The +src+ can be supplied as a... # * full path, like "/my_images/image.gif" # * file name, like "rss.gif", that gets expanded to "/images/rss.gif" # * file name without extension, like "logo", that gets expanded to "/images/logo.png" def image_path(source) compute_public_path(source, 'images', 'png') end # Returns an image tag converting the +options+ into html options on the tag, but with these special cases: # # * :alt - If no alt text is given, the file name part of the +src+ is used (capitalized and without the extension) # * :size - Supplied as "XxY", so "30x45" becomes width="30" and height="45" # # The +src+ can be supplied as a... # * full path, like "/my_images/image.gif" # * file name, like "rss.gif", that gets expanded to "/images/rss.gif" # * file name without extension, like "logo", that gets expanded to "/images/logo.png" def image_tag(source, options = {}) options.symbolize_keys! options[:src] = image_path(source) options[:alt] ||= File.basename(options[:src], '.*').split('.').first.capitalize if options[:size] options[:width], options[:height] = options[:size].split("x") options.delete :size end tag("img", options) end private def compute_public_path(source, dir, ext) source = "/#{dir}/#{source}" unless source.first == "/" || source.include?(":") source << ".#{ext}" unless source.split("/").last.include?(".") source << '?' + rails_asset_id(source) if defined?(RAILS_ROOT) && %r{^[-a-z]+://} !~ source source = "#{@controller.request.relative_url_root}#{source}" unless %r{^[-a-z]+://} =~ source source = ActionController::Base.asset_host + source unless source.include?(":") source end def rails_asset_id(source) ENV["RAILS_ASSET_ID"] || File.mtime("#{RAILS_ROOT}/public/#{source}").to_i.to_s rescue "" end end end end require 'benchmark' module ActionView module Helpers module BenchmarkHelper # Measures the execution time of a block in a template and reports the result to the log. Example: # # <% benchmark "Notes section" do %> # <%= expensive_notes_operation %> # <% end %> # # Will add something like "Notes section (0.34523)" to the log. # # You may give an optional logger level as the second argument # (:debug, :info, :warn, :error). The default is :info. def benchmark(message = "Benchmarking", level = :info) if @logger real = Benchmark.realtime { yield } @logger.send level, "#{message} (#{'%.5f' % real})" end end end end end module ActionView module Helpers # See ActionController::Caching::Fragments for usage instructions. module CacheHelper def cache(name = {}, &block) @controller.cache_erb_fragment(block, name) end end end end module ActionView module Helpers # Capture lets you extract parts of code which # can be used in other points of the template or even layout file. # # == Capturing a block into an instance variable # # <% @script = capture do %> # [some html...] # <% end %> # # == Add javascript to header using content_for # # content_for("name") is a wrapper for capture which will # make the fragment available by name to a yielding layout or template. # # layout.rhtml: # # # # layout with js # # # # <%= yield %> # # # # view.rhtml # # This page shows an alert box! # # <% content_for("script") do %> # alert('hello world') # <% end %> # # Normal view text module CaptureHelper # Capture allows you to extract a part of the template into an # instance variable. You can use this instance variable anywhere # in your templates and even in your layout. # # Example of capture being used in a .rhtml page: # # <% @greeting = capture do %> # Welcome To my shiny new web page! # <% end %> # # Example of capture being used in a .rxml page: # # @greeting = capture do # 'Welcome To my shiny new web page!' # end def capture(*args, &block) # execute the block begin buffer = eval("_erbout", block.binding) rescue buffer = nil end if buffer.nil? capture_block(*args, &block) else capture_erb_with_buffer(buffer, *args, &block) end end # Calling content_for stores the block of markup for later use. # Subsequently, you can make calls to it by name with yield # in another template or in the layout. # # Example: # # <% content_for("header") do %> # alert('hello world') # <% end %> # # You can use yield :header anywhere in your templates. # # <%= yield :header %> # # NOTE: Beware that content_for is ignored in caches. So you shouldn't use it # for elements that are going to be fragment cached. # # The deprecated way of accessing a content_for block was to use a instance variable # named @@content_for_#{name_of_the_content_block}@. So <%= content_for('footer') %> # would be avaiable as <%= @content_for_footer %>. The preferred notation now is # <%= yield :footer %>. def content_for(name, &block) eval "@content_for_#{name} = (@content_for_#{name} || '') + capture(&block)" end private def capture_block(*args, &block) block.call(*args) end def capture_erb(*args, &block) buffer = eval("_erbout", block.binding) capture_erb_with_buffer(buffer, *args, &block) end def capture_erb_with_buffer(buffer, *args, &block) pos = buffer.length block.call(*args) # extract the block data = buffer[pos..-1] # replace it in the original with empty string buffer[pos..-1] = '' data end def erb_content_for(name, &block) eval "@content_for_#{name} = (@content_for_#{name} || '') + capture_erb(&block)" end def block_content_for(name, &block) eval "@content_for_#{name} = (@content_for_#{name} || '') + capture_block(&block)" end end end end require "date" module ActionView module Helpers # The Date Helper primarily creates select/option tags for different kinds of dates and date elements. All of the select-type methods # share a number of common options that are as follows: # # * :prefix - overwrites the default prefix of "date" used for the select names. So specifying "birthday" would give # birthday[month] instead of date[month] if passed to the select_month method. # * :include_blank - set to true if it should be possible to set an empty date. # * :discard_type - set to true if you want to discard the type part of the select name. If set to true, the select_month # method would use simply "date" (which can be overwritten using :prefix) instead of "date[month]". module DateHelper DEFAULT_PREFIX = 'date' unless const_defined?('DEFAULT_PREFIX') # Reports the approximate distance in time between two Time objects or integers. # For example, if the distance is 47 minutes, it'll return # "about 1 hour". See the source for the complete wording list. # # Integers are interpreted as seconds. So, # distance_of_time_in_words(50) returns "less than a minute". # # Set include_seconds to true if you want more detailed approximations if distance < 1 minute def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false) from_time = from_time.to_time if from_time.respond_to?(:to_time) to_time = to_time.to_time if to_time.respond_to?(:to_time) distance_in_minutes = (((to_time - from_time).abs)/60).round distance_in_seconds = ((to_time - from_time).abs).round case distance_in_minutes when 0..1 return (distance_in_minutes==0) ? 'less than a minute' : '1 minute' unless include_seconds case distance_in_seconds when 0..5 then 'less than 5 seconds' when 6..10 then 'less than 10 seconds' when 11..20 then 'less than 20 seconds' when 21..40 then 'half a minute' when 41..59 then 'less than a minute' else '1 minute' end when 2..45 then "#{distance_in_minutes} minutes" when 46..90 then 'about 1 hour' when 90..1440 then "about #{(distance_in_minutes.to_f / 60.0).round} hours" when 1441..2880 then '1 day' else "#{(distance_in_minutes / 1440).round} days" end end # Like distance_of_time_in_words, but where to_time is fixed to Time.now. def time_ago_in_words(from_time, include_seconds = false) distance_of_time_in_words(from_time, Time.now, include_seconds) end alias_method :distance_of_time_in_words_to_now, :time_ago_in_words # Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based attribute (identified by # +method+) on an object assigned to the template (identified by +object+). It's possible to tailor the selects through the +options+ hash, # which accepts all the keys that each of the individual select builders do (like :use_month_numbers for select_month) as well as a range of # discard options. The discard options are :discard_year, :discard_month and :discard_day. Set to true, they'll # drop the respective select. Discarding the month select will also automatically discard the day select. It's also possible to explicitly # set the order of the tags using the :order option with an array of symbols :year, :month and :day in # the desired order. Symbols may be omitted and the respective select is not included. # # Passing :disabled => true as part of the +options+ will make elements inaccessible for change. # # NOTE: Discarded selects will default to 1. So if no month select is available, January will be assumed. # # Examples: # # date_select("post", "written_on") # date_select("post", "written_on", :start_year => 1995) # date_select("post", "written_on", :start_year => 1995, :use_month_numbers => true, # :discard_day => true, :include_blank => true) # date_select("post", "written_on", :order => [:day, :month, :year]) # date_select("user", "birthday", :order => [:month, :day]) # # The selects are prepared for multi-parameter assignment to an Active Record object. def date_select(object_name, method, options = {}) InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_date_select_tag(options) end # Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a specified datetime-based # attribute (identified by +method+) on an object assigned to the template (identified by +object+). Examples: # # datetime_select("post", "written_on") # datetime_select("post", "written_on", :start_year => 1995) # # The selects are prepared for multi-parameter assignment to an Active Record object. def datetime_select(object_name, method, options = {}) InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_datetime_select_tag(options) end # Returns a set of html select-tags (one for year, month, and day) pre-selected with the +date+. def select_date(date = Date.today, options = {}) select_year(date, options) + select_month(date, options) + select_day(date, options) end # Returns a set of html select-tags (one for year, month, day, hour, and minute) pre-selected with the +datetime+. def select_datetime(datetime = Time.now, options = {}) select_year(datetime, options) + select_month(datetime, options) + select_day(datetime, options) + select_hour(datetime, options) + select_minute(datetime, options) end # Returns a set of html select-tags (one for hour and minute) def select_time(datetime = Time.now, options = {}) h = select_hour(datetime, options) + select_minute(datetime, options) + (options[:include_seconds] ? select_second(datetime, options) : '') end # Returns a select tag with options for each of the seconds 0 through 59 with the current second selected. # The second can also be substituted for a second number. # Override the field name using the :field_name option, 'second' by default. def select_second(datetime, options = {}) second_options = [] 0.upto(59) do |second| second_options << ((datetime && (datetime.kind_of?(Fixnum) ? datetime : datetime.sec) == second) ? %(\n) : %(\n) ) end select_html(options[:field_name] || 'second', second_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) end # Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected. # Also can return a select tag with options by minute_step from 0 through 59 with the 00 minute selected # The minute can also be substituted for a minute number. # Override the field name using the :field_name option, 'minute' by default. def select_minute(datetime, options = {}) minute_options = [] 0.step(59, options[:minute_step] || 1) do |minute| minute_options << ((datetime && (datetime.kind_of?(Fixnum) ? datetime : datetime.min) == minute) ? %(\n) : %(\n) ) end select_html(options[:field_name] || 'minute', minute_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) end # Returns a select tag with options for each of the hours 0 through 23 with the current hour selected. # The hour can also be substituted for a hour number. # Override the field name using the :field_name option, 'hour' by default. def select_hour(datetime, options = {}) hour_options = [] 0.upto(23) do |hour| hour_options << ((datetime && (datetime.kind_of?(Fixnum) ? datetime : datetime.hour) == hour) ? %(\n) : %(\n) ) end select_html(options[:field_name] || 'hour', hour_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) end # Returns a select tag with options for each of the days 1 through 31 with the current day selected. # The date can also be substituted for a hour number. # Override the field name using the :field_name option, 'day' by default. def select_day(date, options = {}) day_options = [] 1.upto(31) do |day| day_options << ((date && (date.kind_of?(Fixnum) ? date : date.day) == day) ? %(\n) : %(\n) ) end select_html(options[:field_name] || 'day', day_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) end # Returns a select tag with options for each of the months January through December with the current month selected. # The month names are presented as keys (what's shown to the user) and the month numbers (1-12) are used as values # (what's submitted to the server). It's also possible to use month numbers for the presentation instead of names -- # set the :use_month_numbers key in +options+ to true for this to happen. If you want both numbers and names, # set the :add_month_numbers key in +options+ to true. Examples: # # select_month(Date.today) # Will use keys like "January", "March" # select_month(Date.today, :use_month_numbers => true) # Will use keys like "1", "3" # select_month(Date.today, :add_month_numbers => true) # Will use keys like "1 - January", "3 - March" # # Override the field name using the :field_name option, 'month' by default. # # If you would prefer to show month names as abbreviations, set the # :use_short_month key in +options+ to true. def select_month(date, options = {}) month_options = [] month_names = options[:use_short_month] ? Date::ABBR_MONTHNAMES : Date::MONTHNAMES 1.upto(12) do |month_number| month_name = if options[:use_month_numbers] month_number elsif options[:add_month_numbers] month_number.to_s + ' - ' + month_names[month_number] else month_names[month_number] end month_options << ((date && (date.kind_of?(Fixnum) ? date : date.month) == month_number) ? %(\n) : %(\n) ) end select_html(options[:field_name] || 'month', month_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) end # Returns a select tag with options for each of the five years on each side of the current, which is selected. The five year radius # can be changed using the :start_year and :end_year keys in the +options+. Both ascending and descending year # lists are supported by making :start_year less than or greater than :end_year. The date can also be # substituted for a year given as a number. Example: # # select_year(Date.today, :start_year => 1992, :end_year => 2007) # ascending year values # select_year(Date.today, :start_year => 2005, :end_year => 1900) # descending year values # # Override the field name using the :field_name option, 'year' by default. def select_year(date, options = {}) year_options = [] y = date ? (date.kind_of?(Fixnum) ? (y = (date == 0) ? Date.today.year : date) : date.year) : Date.today.year start_year, end_year = (options[:start_year] || y-5), (options[:end_year] || y+5) step_val = start_year < end_year ? 1 : -1 start_year.step(end_year, step_val) do |year| year_options << ((date && (date.kind_of?(Fixnum) ? date : date.year) == year) ? %(\n) : %(\n) ) end select_html(options[:field_name] || 'year', year_options, options[:prefix], options[:include_blank], options[:discard_type], options[:disabled]) end private def select_html(type, options, prefix = nil, include_blank = false, discard_type = false, disabled = false) select_html = %(\n" end def leading_zero_on_single_digits(number) number > 9 ? number : "0#{number}" end end class InstanceTag #:nodoc: include DateHelper def to_date_select_tag(options = {}) defaults = { :discard_type => true } options = defaults.merge(options) options_with_prefix = Proc.new { |position| options.merge(:prefix => "#{@object_name}[#{@method_name}(#{position}i)]") } date = options[:include_blank] ? (value || 0) : (value || Date.today) date_select = '' options[:order] = [:month, :year, :day] if options[:month_before_year] # For backwards compatibility options[:order] ||= [:year, :month, :day] position = {:year => 1, :month => 2, :day => 3} discard = {} discard[:year] = true if options[:discard_year] discard[:month] = true if options[:discard_month] discard[:day] = true if options[:discard_day] or options[:discard_month] options[:order].each do |param| date_select << self.send("select_#{param}", date, options_with_prefix.call(position[param])) unless discard[param] end date_select end def to_datetime_select_tag(options = {}) defaults = { :discard_type => true } options = defaults.merge(options) options_with_prefix = Proc.new { |position| options.merge(:prefix => "#{@object_name}[#{@method_name}(#{position}i)]") } datetime = options[:include_blank] ? (value || nil) : (value || Time.now) datetime_select = select_year(datetime, options_with_prefix.call(1)) datetime_select << select_month(datetime, options_with_prefix.call(2)) unless options[:discard_month] datetime_select << select_day(datetime, options_with_prefix.call(3)) unless options[:discard_day] || options[:discard_month] datetime_select << ' — ' + select_hour(datetime, options_with_prefix.call(4)) unless options[:discard_hour] datetime_select << ' : ' + select_minute(datetime, options_with_prefix.call(5)) unless options[:discard_minute] || options[:discard_hour] datetime_select end end class FormBuilder def date_select(method, options = {}) @template.date_select(@object_name, method, options.merge(:object => @object)) end def datetime_select(method, options = {}) @template.datetime_select(@object_name, method, options.merge(:object => @object)) end end end end module ActionView module Helpers # Provides a set of methods for making it easier to locate problems. module DebugHelper # Returns a
-tag set with the +object+ dumped by YAML. Very readable way to inspect an object.
      def debug(object)
        begin
          Marshal::dump(object)
          "
#{h(object.to_yaml).gsub("  ", "  ")}
" rescue Object => e # Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback "#{h(object.inspect)}" end end end end endrequire 'cgi' require File.dirname(__FILE__) + '/date_helper' require File.dirname(__FILE__) + '/tag_helper' module ActionView module Helpers # Provides a set of methods for working with forms and especially forms related to objects assigned to the template. # The following is an example of a complete form for a person object that works for both creates and updates built # with all the form helpers. The @person object was assigned by an action on the controller: #
# Name: # <%= text_field "person", "name", "size" => 20 %> # # Password: # <%= password_field "person", "password", "maxsize" => 20 %> # # Single?: # <%= check_box "person", "single" %> # # Description: # <%= text_area "person", "description", "cols" => 20 %> # # #
# # ...is compiled to: # #
# Name: # # # Password: # # # Single?: # # # Description: # # # #
# # If the object name contains square brackets the id for the object will be inserted. Example: # # <%= text_field "person[]", "name" %> # # ...becomes: # # # # If the helper is being used to generate a repetitive sequence of similar form elements, for example in a partial # used by render_collection_of_partials, the "index" option may come in handy. Example: # # <%= text_field "person", "name", "index" => 1 %> # # becomes # # # # There's also methods for helping to build form tags in link:classes/ActionView/Helpers/FormOptionsHelper.html, # link:classes/ActionView/Helpers/DateHelper.html, and link:classes/ActionView/Helpers/ActiveRecordHelper.html module FormHelper # Creates a form and a scope around a specific model object, which is then used as a base for questioning about # values for the fields. Examples: # # <% form_for :person, @person, :url => { :action => "update" } do |f| %> # First name: <%= f.text_field :first_name %> # Last name : <%= f.text_field :last_name %> # Biography : <%= f.text_area :biography %> # Admin? : <%= f.check_box :admin %> # <% end %> # # Worth noting is that the form_for tag is called in a ERb evaluation block, not a ERb output block. So that's <% %>, # not <%= %>. Also worth noting is that the form_for yields a form_builder object, in this example as f, which emulates # the API for the stand-alone FormHelper methods, but without the object name. So instead of text_field :person, :name, # you get away with f.text_field :name. # # That in itself is a modest increase in comfort. The big news is that form_for allows us to more easily escape the instance # variable convention, so while the stand-alone approach would require text_field :person, :name, :object => person # to work with local variables instead of instance ones, the form_for calls remain the same. You simply declare once with # :person, person and all subsequent field calls save :person and :object => person. # # Also note that form_for doesn't create an exclusive scope. It's still possible to use both the stand-alone FormHelper methods # and methods from FormTagHelper. Example: # # <% form_for :person, @person, :url => { :action => "update" } do |f| %> # First name: <%= f.text_field :first_name %> # Last name : <%= f.text_field :last_name %> # Biography : <%= text_area :person, :biography %> # Admin? : <%= check_box_tag "person[admin]", @person.company.admin? %> # <% end %> # # Note: This also works for the methods in FormOptionHelper and DateHelper that are designed to work with an object as base. # Like collection_select and datetime_select. # # Html attributes for the form tag can be given as :html => {...}. Example: # # <% form_for :person, @person, :html => {:id => 'person_form'} do |f| %> # ... # <% end %> # # You can also build forms using a customized FormBuilder class. Subclass FormBuilder and override or define some more helpers, # then use your custom builder like so: # # <% form_for :person, @person, :url => { :action => "update" }, :builder => LabellingFormBuilder do |f| %> # <%= f.text_field :first_name %> # <%= f.text_field :last_name %> # <%= text_area :person, :biography %> # <%= check_box_tag "person[admin]", @person.company.admin? %> # <% end %> # # In many cases you will want to wrap the above in another helper, such as: # # def labelled_form_for(name, object, options, &proc) # form_for(name, object, options.merge(:builder => LabellingFormBuiler), &proc) # end # def form_for(object_name, *args, &proc) raise ArgumentError, "Missing block" unless block_given? options = args.last.is_a?(Hash) ? args.pop : {} concat(form_tag(options.delete(:url) || {}, options.delete(:html) || {}), proc.binding) fields_for(object_name, *(args << options), &proc) concat('', proc.binding) end # Creates a scope around a specific model object like form_for, but doesn't create the form tags themselves. This makes # fields_for suitable for specifying additional model objects in the same form. Example: # # <% form_for :person, @person, :url => { :action => "update" } do |person_form| %> # First name: <%= person_form.text_field :first_name %> # Last name : <%= person_form.text_field :last_name %> # # <% fields_for :permission, @person.permission do |permission_fields| %> # Admin? : <%= permission_fields.check_box :admin %> # <% end %> # <% end %> # # Note: This also works for the methods in FormOptionHelper and DateHelper that are designed to work with an object as base. # Like collection_select and datetime_select. def fields_for(object_name, *args, &proc) raise ArgumentError, "Missing block" unless block_given? options = args.last.is_a?(Hash) ? args.pop : {} object = args.first yield((options[:builder] || FormBuilder).new(object_name, object, self, options, proc)) end # Returns an input tag of the "text" type tailored for accessing a specified attribute (identified by +method+) on an object # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a # hash with +options+. # # Examples (call, result): # text_field("post", "title", "size" => 20) # def text_field(object_name, method, options = {}) InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("text", options) end # Works just like text_field, but returns an input tag of the "password" type instead. def password_field(object_name, method, options = {}) InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("password", options) end # Works just like text_field, but returns an input tag of the "hidden" type instead. def hidden_field(object_name, method, options = {}) InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("hidden", options) end # Works just like text_field, but returns an input tag of the "file" type instead, which won't have a default value. def file_field(object_name, method, options = {}) InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_input_field_tag("file", options) end # Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+) # on an object assigned to the template (identified by +object+). Additional options on the input tag can be passed as a # hash with +options+. # # Example (call, result): # text_area("post", "body", "cols" => 20, "rows" => 40) # def text_area(object_name, method, options = {}) InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_text_area_tag(options) end # Returns a checkbox tag tailored for accessing a specified attribute (identified by +method+) on an object # assigned to the template (identified by +object+). It's intended that +method+ returns an integer and if that # integer is above zero, then the checkbox is checked. Additional options on the input tag can be passed as a # hash with +options+. The +checked_value+ defaults to 1 while the default +unchecked_value+ # is set to 0 which is convenient for boolean values. Usually unchecked checkboxes don't post anything. # We work around this problem by adding a hidden value with the same name as the checkbox. # # Example (call, result). Imagine that @post.validated? returns 1: # check_box("post", "validated") # # # # Example (call, result). Imagine that @puppy.gooddog returns no: # check_box("puppy", "gooddog", {}, "yes", "no") # # def check_box(object_name, method, options = {}, checked_value = "1", unchecked_value = "0") InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_check_box_tag(options, checked_value, unchecked_value) end # Returns a radio button tag for accessing a specified attribute (identified by +method+) on an object # assigned to the template (identified by +object+). If the current value of +method+ is +tag_value+ the # radio button will be checked. Additional options on the input tag can be passed as a # hash with +options+. # Example (call, result). Imagine that @post.category returns "rails": # radio_button("post", "category", "rails") # radio_button("post", "category", "java") # # # def radio_button(object_name, method, tag_value, options = {}) InstanceTag.new(object_name, method, self, nil, options.delete(:object)).to_radio_button_tag(tag_value, options) end end class InstanceTag #:nodoc: include Helpers::TagHelper attr_reader :method_name, :object_name DEFAULT_FIELD_OPTIONS = { "size" => 30 }.freeze unless const_defined?(:DEFAULT_FIELD_OPTIONS) DEFAULT_RADIO_OPTIONS = { }.freeze unless const_defined?(:DEFAULT_RADIO_OPTIONS) DEFAULT_TEXT_AREA_OPTIONS = { "cols" => 40, "rows" => 20 }.freeze unless const_defined?(:DEFAULT_TEXT_AREA_OPTIONS) DEFAULT_DATE_OPTIONS = { :discard_type => true }.freeze unless const_defined?(:DEFAULT_DATE_OPTIONS) def initialize(object_name, method_name, template_object, local_binding = nil, object = nil) @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup @template_object, @local_binding = template_object, local_binding @object = object if @object_name.sub!(/\[\]$/,"") @auto_index = @template_object.instance_variable_get("@#{Regexp.last_match.pre_match}").id_before_type_cast end end def to_input_field_tag(field_type, options = {}) options = options.stringify_keys options["size"] ||= options["maxlength"] || DEFAULT_FIELD_OPTIONS["size"] options = DEFAULT_FIELD_OPTIONS.merge(options) if field_type == "hidden" options.delete("size") end options["type"] = field_type options["value"] ||= value_before_type_cast unless field_type == "file" add_default_name_and_id(options) tag("input", options) end def to_radio_button_tag(tag_value, options = {}) options = DEFAULT_RADIO_OPTIONS.merge(options.stringify_keys) options["type"] = "radio" options["value"] = tag_value options["checked"] = "checked" if value.to_s == tag_value.to_s pretty_tag_value = tag_value.to_s.gsub(/\s/, "_").gsub(/\W/, "").downcase options["id"] = @auto_index ? "#{@object_name}_#{@auto_index}_#{@method_name}_#{pretty_tag_value}" : "#{@object_name}_#{@method_name}_#{pretty_tag_value}" add_default_name_and_id(options) tag("input", options) end def to_text_area_tag(options = {}) options = DEFAULT_TEXT_AREA_OPTIONS.merge(options.stringify_keys) add_default_name_and_id(options) content_tag("textarea", html_escape(options.delete('value') || value_before_type_cast), options) end def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0") options = options.stringify_keys options["type"] = "checkbox" options["value"] = checked_value checked = case value when TrueClass, FalseClass value when NilClass false when Integer value != 0 when String value == checked_value else value.to_i != 0 end if checked || options["checked"] == "checked" options["checked"] = "checked" else options.delete("checked") end add_default_name_and_id(options) tag("input", options) << tag("input", "name" => options["name"], "type" => "hidden", "value" => unchecked_value) end def to_date_tag() defaults = DEFAULT_DATE_OPTIONS.dup date = value || Date.today options = Proc.new { |position| defaults.merge(:prefix => "#{@object_name}[#{@method_name}(#{position}i)]") } html_day_select(date, options.call(3)) + html_month_select(date, options.call(2)) + html_year_select(date, options.call(1)) end def to_boolean_select_tag(options = {}) options = options.stringify_keys add_default_name_and_id(options) tag_text = "" end def to_content_tag(tag_name, options = {}) content_tag(tag_name, value, options) end def object @object || @template_object.instance_variable_get("@#{@object_name}") end def value unless object.nil? object.send(@method_name) end end def value_before_type_cast unless object.nil? object.respond_to?(@method_name + "_before_type_cast") ? object.send(@method_name + "_before_type_cast") : object.send(@method_name) end end private def add_default_name_and_id(options) if options.has_key?("index") options["name"] ||= tag_name_with_index(options["index"]) options["id"] ||= tag_id_with_index(options["index"]) options.delete("index") elsif @auto_index options["name"] ||= tag_name_with_index(@auto_index) options["id"] ||= tag_id_with_index(@auto_index) else options["name"] ||= tag_name options["id"] ||= tag_id end end def tag_name "#{@object_name}[#{@method_name}]" end def tag_name_with_index(index) "#{@object_name}[#{index}][#{@method_name}]" end def tag_id "#{@object_name}_#{@method_name}" end def tag_id_with_index(index) "#{@object_name}_#{index}_#{@method_name}" end end class FormBuilder #:nodoc: # The methods which wrap a form helper call. class_inheritable_accessor :field_helpers self.field_helpers = (FormHelper.instance_methods - ['form_for']) attr_accessor :object_name, :object def initialize(object_name, object, template, options, proc) @object_name, @object, @template, @options, @proc = object_name, object, template, options, proc end (field_helpers - %w(check_box radio_button)).each do |selector| src = <<-end_src def #{selector}(method, options = {}) @template.send(#{selector.inspect}, @object_name, method, options.merge(:object => @object)) end end_src class_eval src, __FILE__, __LINE__ end def check_box(method, options = {}, checked_value = "1", unchecked_value = "0") @template.check_box(@object_name, method, options.merge(:object => @object), checked_value, unchecked_value) end def radio_button(method, tag_value, options = {}) @template.radio_button(@object_name, method, tag_value, options.merge(:object => @object)) end end end end require 'cgi' require 'erb' require File.dirname(__FILE__) + '/form_helper' module ActionView module Helpers # Provides a number of methods for turning different kinds of containers into a set of option tags. # == Options # The collection_select, country_select, select, # and time_zone_select methods take an options parameter, # a hash. # # * :include_blank - set to true if the first option element of the select element is a blank. Useful if there is not a default value required for the select element. For example, # # select("post", "category", Post::CATEGORIES, {:include_blank => true}) # # could become: # # # # * :prompt - set to true or a prompt string. When the select element doesn't have a value yet, this prepends an option with a generic prompt -- "Please select" -- or the given prompt string. # # Another common case is a select tag for an belongs_to-associated object. For example, # # select("post", "person_id", Person.find_all.collect {|p| [ p.name, p.id ] }) # # could become: # # module FormOptionsHelper include ERB::Util # Create a select tag and a series of contained option tags for the provided object and method. # The option currently held by the object will be selected, provided that the object is available. # See options_for_select for the required format of the choices parameter. # # Example with @post.person_id => 1: # select("post", "person_id", Person.find_all.collect {|p| [ p.name, p.id ] }, { :include_blank => true }) # # could become: # # # # This can be used to provide a default set of options in the standard way: before rendering the create form, a # new model instance is assigned the default options and bound to @model_name. Usually this model is not saved # to the database. Instead, a second model object is created when the create request is received. # This allows the user to submit a form page more than once with the expected results of creating multiple records. # In addition, this allows a single partial to be used to generate form inputs for both edit and create forms. # # By default, post.person_id is the selected option. Specify :selected => value to use a different selection # or :selected => nil to leave all options unselected. def select(object, method, choices, options = {}, html_options = {}) InstanceTag.new(object, method, self, nil, options.delete(:object)).to_select_tag(choices, options, html_options) end # Return select and option tags for the given object and method using options_from_collection_for_select to generate the list of option tags. def collection_select(object, method, collection, value_method, text_method, options = {}, html_options = {}) InstanceTag.new(object, method, self, nil, options.delete(:object)).to_collection_select_tag(collection, value_method, text_method, options, html_options) end # Return select and option tags for the given object and method, using country_options_for_select to generate the list of option tags. def country_select(object, method, priority_countries = nil, options = {}, html_options = {}) InstanceTag.new(object, method, self, nil, options.delete(:object)).to_country_select_tag(priority_countries, options, html_options) end # Return select and option tags for the given object and method, using # #time_zone_options_for_select to generate the list of option tags. # # In addition to the :include_blank option documented above, # this method also supports a :model option, which defaults # to TimeZone. This may be used by users to specify a different time # zone model object. (See #time_zone_options_for_select for more # information.) def time_zone_select(object, method, priority_zones = nil, options = {}, html_options = {}) InstanceTag.new(object, method, self, nil, options.delete(:object)).to_time_zone_select_tag(priority_zones, options, html_options) end # Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container # where the elements respond to first and last (such as a two-element array), the "lasts" serve as option values and # the "firsts" as option text. Hashes are turned into this form automatically, so the keys become "firsts" and values # become lasts. If +selected+ is specified, the matching "last" or element will get the selected option-tag. +Selected+ # may also be an array of values to be selected when using a multiple select. # # Examples (call, result): # options_for_select([["Dollar", "$"], ["Kroner", "DKK"]]) # \n # # options_for_select([ "VISA", "MasterCard" ], "MasterCard") # \n # # options_for_select({ "Basic" => "$20", "Plus" => "$40" }, "$40") # \n # # options_for_select([ "VISA", "MasterCard", "Discover" ], ["VISA", "Discover"]) # \n\n # # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag. def options_for_select(container, selected = nil) container = container.to_a if Hash === container options_for_select = container.inject([]) do |options, element| if !element.is_a?(String) and element.respond_to?(:first) and element.respond_to?(:last) is_selected = ( (selected.respond_to?(:include?) ? selected.include?(element.last) : element.last == selected) ) is_selected = ( (selected.respond_to?(:include?) && !selected.is_a?(String) ? selected.include?(element.last) : element.last == selected) ) if is_selected options << "" else options << "" end else is_selected = ( (selected.respond_to?(:include?) ? selected.include?(element) : element == selected) ) is_selected = ( (selected.respond_to?(:include?) && !selected.is_a?(String) ? selected.include?(element) : element == selected) ) options << ((is_selected) ? "" : "") end end options_for_select.join("\n") end # Returns a string of option tags that have been compiled by iterating over the +collection+ and assigning the # the result of a call to the +value_method+ as the option value and the +text_method+ as the option text. # If +selected_value+ is specified, the element returning a match on +value_method+ will get the selected option tag. # # Example (call, result). Imagine a loop iterating over each +person+ in @project.people to generate an input tag: # options_from_collection_for_select(@project.people, "id", "name") # # # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag. def options_from_collection_for_select(collection, value_method, text_method, selected_value = nil) options_for_select( collection.inject([]) { |options, object| options << [ object.send(text_method), object.send(value_method) ] }, selected_value ) end # Returns a string of option tags, like options_from_collection_for_select, but surrounds them with tags. # # An array of group objects are passed. Each group should return an array of options when calling group_method # Each group should return its name when calling group_label_method. # # html_option_groups_from_collection(@continents, "countries", "continent_name", "country_id", "country_name", @selected_country.id) # # Could become: # # # # ... # # # # # # ... # # # with objects of the following classes: # class Continent # def initialize(p_name, p_countries) @continent_name = p_name; @countries = p_countries; end # def continent_name() @continent_name; end # def countries() @countries; end # end # class Country # def initialize(id, name) @id = id; @name = name end # def country_id() @id; end # def country_name() @name; end # end # # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag. def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil) collection.inject("") do |options_for_select, group| group_label_string = eval("group.#{group_label_method}") options_for_select += "" options_for_select += options_from_collection_for_select(eval("group.#{group_method}"), option_key_method, option_value_method, selected_key) options_for_select += '' end end # Returns a string of option tags for pretty much any country in the world. Supply a country name as +selected+ to # have it marked as the selected option tag. You can also supply an array of countries as +priority_countries+, so # that they will be listed above the rest of the (long) list. # # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag. def country_options_for_select(selected = nil, priority_countries = nil) country_options = "" if priority_countries country_options += options_for_select(priority_countries, selected) country_options += "\n" end if priority_countries && priority_countries.include?(selected) country_options += options_for_select(COUNTRIES - priority_countries, selected) else country_options += options_for_select(COUNTRIES, selected) end return country_options end # Returns a string of option tags for pretty much any time zone in the # world. Supply a TimeZone name as +selected+ to have it marked as the # selected option tag. You can also supply an array of TimeZone objects # as +priority_zones+, so that they will be listed above the rest of the # (long) list. (You can use TimeZone.us_zones as a convenience for # obtaining a list of the US time zones.) # # The +selected+ parameter must be either +nil+, or a string that names # a TimeZone. # # By default, +model+ is the TimeZone constant (which can be obtained # in ActiveRecord as a value object). The only requirement is that the # +model+ parameter be an object that responds to #all, and returns # an array of objects that represent time zones. # # NOTE: Only the option tags are returned, you have to wrap this call in # a regular HTML select tag. def time_zone_options_for_select(selected = nil, priority_zones = nil, model = TimeZone) zone_options = "" zones = model.all convert_zones = lambda { |list| list.map { |z| [ z.to_s, z.name ] } } if priority_zones zone_options += options_for_select(convert_zones[priority_zones], selected) zone_options += "\n" zones = zones.reject { |z| priority_zones.include?( z ) } end zone_options += options_for_select(convert_zones[zones], selected) zone_options end private # All the countries included in the country_options output. COUNTRIES = [ "Afghanistan", "Albania", "Algeria", "American Samoa", "Andorra", "Angola", "Anguilla", "Antarctica", "Antigua And Barbuda", "Argentina", "Armenia", "Aruba", "Australia", "Austria", "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus", "Belgium", "Belize", "Benin", "Bermuda", "Bhutan", "Bolivia", "Bosnia and Herzegowina", "Botswana", "Bouvet Island", "Brazil", "British Indian Ocean Territory", "Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burma", "Burundi", "Cambodia", "Cameroon", "Canada", "Cape Verde", "Cayman Islands", "Central African Republic", "Chad", "Chile", "China", "Christmas Island", "Cocos (Keeling) Islands", "Colombia", "Comoros", "Congo", "Congo, the Democratic Republic of the", "Cook Islands", "Costa Rica", "Cote d'Ivoire", "Croatia", "Cuba", "Cyprus", "Czech Republic", "Denmark", "Djibouti", "Dominica", "Dominican Republic", "East Timor", "Ecuador", "Egypt", "El Salvador", "England", "Equatorial Guinea", "Eritrea", "Espana", "Estonia", "Ethiopia", "Falkland Islands", "Faroe Islands", "Fiji", "Finland", "France", "French Guiana", "French Polynesia", "French Southern Territories", "Gabon", "Gambia", "Georgia", "Germany", "Ghana", "Gibraltar", "Great Britain", "Greece", "Greenland", "Grenada", "Guadeloupe", "Guam", "Guatemala", "Guinea", "Guinea-Bissau", "Guyana", "Haiti", "Heard and Mc Donald Islands", "Honduras", "Hong Kong", "Hungary", "Iceland", "India", "Indonesia", "Ireland", "Israel", "Italy", "Iran", "Iraq", "Jamaica", "Japan", "Jordan", "Kazakhstan", "Kenya", "Kiribati", "Korea, Republic of", "Korea (South)", "Kuwait", "Kyrgyzstan", "Lao People's Democratic Republic", "Latvia", "Lebanon", "Lesotho", "Liberia", "Liechtenstein", "Lithuania", "Luxembourg", "Macau", "Macedonia", "Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands", "Martinique", "Mauritania", "Mauritius", "Mayotte", "Mexico", "Micronesia, Federated States of", "Moldova, Republic of", "Monaco", "Mongolia", "Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", "Nepal", "Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", "Nicaragua", "Niger", "Nigeria", "Niue", "Norfolk Island", "Northern Ireland", "Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Pitcairn", "Poland", "Portugal", "Puerto Rico", "Qatar", "Reunion", "Romania", "Russia", "Rwanda", "Saint Kitts and Nevis", "Saint Lucia", "Saint Vincent and the Grenadines", "Samoa (Independent)", "San Marino", "Sao Tome and Principe", "Saudi Arabia", "Scotland", "Senegal", "Serbia and Montenegro", "Seychelles", "Sierra Leone", "Singapore", "Slovakia", "Slovenia", "Solomon Islands", "Somalia", "South Africa", "South Georgia and the South Sandwich Islands", "South Korea", "Spain", "Sri Lanka", "St. Helena", "St. Pierre and Miquelon", "Suriname", "Svalbard and Jan Mayen Islands", "Swaziland", "Sweden", "Switzerland", "Taiwan", "Tajikistan", "Tanzania", "Thailand", "Togo", "Tokelau", "Tonga", "Trinidad", "Trinidad and Tobago", "Tunisia", "Turkey", "Turkmenistan", "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", "United States", "United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu", "Vatican City State (Holy See)", "Venezuela", "Viet Nam", "Virgin Islands (British)", "Virgin Islands (U.S.)", "Wales", "Wallis and Futuna Islands", "Western Sahara", "Yemen", "Zambia", "Zimbabwe" ] unless const_defined?("COUNTRIES") end class InstanceTag #:nodoc: include FormOptionsHelper def to_select_tag(choices, options, html_options) html_options = html_options.stringify_keys add_default_name_and_id(html_options) selected_value = options.has_key?(:selected) ? options[:selected] : value content_tag("select", add_options(options_for_select(choices, selected_value), options, value), html_options) end def to_collection_select_tag(collection, value_method, text_method, options, html_options) html_options = html_options.stringify_keys add_default_name_and_id(html_options) content_tag( "select", add_options(options_from_collection_for_select(collection, value_method, text_method, value), options, value), html_options ) end def to_country_select_tag(priority_countries, options, html_options) html_options = html_options.stringify_keys add_default_name_and_id(html_options) content_tag("select", add_options(country_options_for_select(value, priority_countries), options, value), html_options) end def to_time_zone_select_tag(priority_zones, options, html_options) html_options = html_options.stringify_keys add_default_name_and_id(html_options) content_tag("select", add_options( time_zone_options_for_select(value, priority_zones, options[:model] || TimeZone), options, value ), html_options ) end private def add_options(option_tags, options, value = nil) option_tags = "\n" + option_tags if options[:include_blank] if value.blank? && options[:prompt] ("\n") + option_tags else option_tags end end end class FormBuilder def select(method, choices, options = {}, html_options = {}) @template.select(@object_name, method, choices, options.merge(:object => @object), html_options) end def collection_select(method, collection, value_method, text_method, options = {}, html_options = {}) @template.collection_select(@object_name, method, collection, value_method, text_method, options.merge(:object => @object), html_options) end def country_select(method, priority_countries = nil, options = {}, html_options = {}) @template.country_select(@object_name, method, priority_countries, options.merge(:object => @object), html_options) end def time_zone_select(method, priority_zones = nil, options = {}, html_options = {}) @template.time_zone_select(@object_name, method, priority_zones, options.merge(:object => @object), html_options) end end end end require 'cgi' require File.dirname(__FILE__) + '/tag_helper' module ActionView module Helpers # Provides a number of methods for creating form tags that doesn't rely on conventions with an object assigned to the template like # FormHelper does. With the FormTagHelper, you provide the names and values yourself. # # NOTE: The html options disabled, readonly, and multiple can all be treated as booleans. So specifying :disabled => true # will give disabled="disabled". module FormTagHelper # Starts a form tag that points the action to an url configured with url_for_options just like # ActionController::Base#url_for. The method for the form defaults to POST. # # Options: # * :multipart - If set to true, the enctype is set to "multipart/form-data". # * :method - The method to use when submitting the form, usually either "get" or "post". def form_tag(url_for_options = {}, options = {}, *parameters_for_url, &proc) html_options = { "method" => "post" }.merge(options.stringify_keys) html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart") html_options["action"] = url_for(url_for_options, *parameters_for_url) tag :form, html_options, true end alias_method :start_form_tag, :form_tag # Outputs "" def end_form_tag "" end # Creates a dropdown selection box, or if the :multiple option is set to true, a multiple # choice selection box. # # Helpers::FormOptions can be used to create common select boxes such as countries, time zones, or # associated records. # # option_tags is a string containing the option tags for the select box: # # Outputs # select_tag "people", "" # # Options: # * :multiple - If set to true the selection will allow multiple choices. def select_tag(name, option_tags = nil, options = {}) content_tag :select, option_tags, { "name" => name, "id" => name }.update(options.stringify_keys) end # Creates a standard text field. # # Options: # * :disabled - If set to true, the user will not be able to use this input. # * :size - The number of visible characters that will fit in the input. # * :maxlength - The maximum number of characters that the browser will allow the user to enter. # # A hash of standard HTML options for the tag. def text_field_tag(name, value = nil, options = {}) tag :input, { "type" => "text", "name" => name, "id" => name, "value" => value }.update(options.stringify_keys) end # Creates a hidden field. # # Takes the same options as text_field_tag def hidden_field_tag(name, value = nil, options = {}) text_field_tag(name, value, options.stringify_keys.update("type" => "hidden")) end # Creates a file upload field. # # If you are using file uploads then you will also need to set the multipart option for the form: # <%= form_tag { :action => "post" }, { :multipart => true } %> # <%= file_field_tag "file" %> # <%= submit_tag %> # <%= end_form_tag %> # # The specified URL will then be passed a File object containing the selected file, or if the field # was left blank, a StringIO object. def file_field_tag(name, options = {}) text_field_tag(name, nil, options.update("type" => "file")) end # Creates a password field. # # Takes the same options as text_field_tag def password_field_tag(name = "password", value = nil, options = {}) text_field_tag(name, value, options.update("type" => "password")) end # Creates a text input area. # # Options: # * :size - A string specifying the dimensions of the textarea. # # Outputs # <%= text_area_tag "body", nil, :size => "25x10" %> def text_area_tag(name, content = nil, options = {}) options.stringify_keys! if size = options.delete("size") options["cols"], options["rows"] = size.split("x") end content_tag :textarea, content, { "name" => name, "id" => name }.update(options.stringify_keys) end # Creates a check box. def check_box_tag(name, value = "1", checked = false, options = {}) html_options = { "type" => "checkbox", "name" => name, "id" => name, "value" => value }.update(options.stringify_keys) html_options["checked"] = "checked" if checked tag :input, html_options end # Creates a radio button. def radio_button_tag(name, value, checked = false, options = {}) html_options = { "type" => "radio", "name" => name, "id" => name, "value" => value }.update(options.stringify_keys) html_options["checked"] = "checked" if checked tag :input, html_options end # Creates a submit button with the text value as the caption. If options contains a pair with the key of "disable_with", # then the value will be used to rename a disabled version of the submit button. def submit_tag(value = "Save changes", options = {}) options.stringify_keys! if disable_with = options.delete("disable_with") options["onclick"] = "this.disabled=true;this.value='#{disable_with}';this.form.submit();#{options["onclick"]}" end tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(options.stringify_keys) end # Displays an image which when clicked will submit the form. # # source is passed to AssetTagHelper#image_path def image_submit_tag(source, options = {}) tag :input, { "type" => "image", "src" => image_path(source) }.update(options.stringify_keys) end end end end require File.dirname(__FILE__) + '/tag_helper' module ActionView module Helpers # Provides a set of helpers for creating JavaScript macros that rely on and often bundle methods from JavaScriptHelper into # larger units. These macros also rely on counterparts in the controller that provide them with their backing. The in-place # editing relies on ActionController::Base.in_place_edit_for and the autocompletion relies on # ActionController::Base.auto_complete_for. module JavaScriptMacrosHelper # Makes an HTML element specified by the DOM ID +field_id+ become an in-place # editor of a property. # # A form is automatically created and displayed when the user clicks the element, # something like this: #
# # # cancel #
# # The form is serialized and sent to the server using an AJAX call, the action on # the server should process the value and return the updated value in the body of # the reponse. The element will automatically be updated with the changed value # (as returned from the server). # # Required +options+ are: # :url:: Specifies the url where the updated value should # be sent after the user presses "ok". # # # Addtional +options+ are: # :rows:: Number of rows (more than 1 will use a TEXTAREA) # :cols:: Number of characters the text input should span (works for both INPUT and TEXTAREA) # :size:: Synonym for :cols when using a single line text input. # :cancel_text:: The text on the cancel link. (default: "cancel") # :save_text:: The text on the save link. (default: "ok") # :loading_text:: The text to display when submitting to the server (default: "Saving...") # :external_control:: The id of an external control used to enter edit mode. # :load_text_url:: URL where initial value of editor (content) is retrieved. # :options:: Pass through options to the AJAX call (see prototype's Ajax.Updater) # :with:: JavaScript snippet that should return what is to be sent # in the AJAX call, +form+ is an implicit parameter # :script:: Instructs the in-place editor to evaluate the remote JavaScript response (default: false) def in_place_editor(field_id, options = {}) function = "new Ajax.InPlaceEditor(" function << "'#{field_id}', " function << "'#{url_for(options[:url])}'" js_options = {} js_options['cancelText'] = %('#{options[:cancel_text]}') if options[:cancel_text] js_options['okText'] = %('#{options[:save_text]}') if options[:save_text] js_options['loadingText'] = %('#{options[:loading_text]}') if options[:loading_text] js_options['rows'] = options[:rows] if options[:rows] js_options['cols'] = options[:cols] if options[:cols] js_options['size'] = options[:size] if options[:size] js_options['externalControl'] = "'#{options[:external_control]}'" if options[:external_control] js_options['loadTextURL'] = "'#{url_for(options[:load_text_url])}'" if options[:load_text_url] js_options['ajaxOptions'] = options[:options] if options[:options] js_options['evalScripts'] = options[:script] if options[:script] js_options['callback'] = "function(form) { return #{options[:with]} }" if options[:with] function << (', ' + options_for_javascript(js_options)) unless js_options.empty? function << ')' javascript_tag(function) end # Renders the value of the specified object and method with in-place editing capabilities. # # See the RDoc on ActionController::InPlaceEditing to learn more about this. def in_place_editor_field(object, method, tag_options = {}, in_place_editor_options = {}) tag = ::ActionView::Helpers::InstanceTag.new(object, method, self) tag_options = {:tag => "span", :id => "#{object}_#{method}_#{tag.object.id}_in_place_editor", :class => "in_place_editor_field"}.merge!(tag_options) in_place_editor_options[:url] = in_place_editor_options[:url] || url_for({ :action => "set_#{object}_#{method}", :id => tag.object.id }) tag.to_content_tag(tag_options.delete(:tag), tag_options) + in_place_editor(tag_options[:id], in_place_editor_options) end # Adds AJAX autocomplete functionality to the text input field with the # DOM ID specified by +field_id+. # # This function expects that the called action returns a HTML
    list, # or nothing if no entries should be displayed for autocompletion. # # You'll probably want to turn the browser's built-in autocompletion off, # so be sure to include a autocomplete="off" attribute with your text # input field. # # The autocompleter object is assigned to a Javascript variable named field_id_auto_completer. # This object is useful if you for example want to trigger the auto-complete suggestions through # other means than user input (for that specific case, call the activate method on that object). # # Required +options+ are: # :url:: URL to call for autocompletion results # in url_for format. # # Addtional +options+ are: # :update:: Specifies the DOM ID of the element whose # innerHTML should be updated with the autocomplete # entries returned by the AJAX request. # Defaults to field_id + '_auto_complete' # :with:: A JavaScript expression specifying the # parameters for the XMLHttpRequest. This defaults # to 'fieldname=value'. # :frequency:: Determines the time to wait after the last keystroke # for the AJAX request to be initiated. # :indicator:: Specifies the DOM ID of an element which will be # displayed while autocomplete is running. # :tokens:: A string or an array of strings containing # separator tokens for tokenized incremental # autocompletion. Example: :tokens => ',' would # allow multiple autocompletion entries, separated # by commas. # :min_chars:: The minimum number of characters that should be # in the input field before an Ajax call is made # to the server. # :on_hide:: A Javascript expression that is called when the # autocompletion div is hidden. The expression # should take two variables: element and update. # Element is a DOM element for the field, update # is a DOM element for the div from which the # innerHTML is replaced. # :on_show:: Like on_hide, only now the expression is called # then the div is shown. # :after_update_element:: A Javascript expression that is called when the # user has selected one of the proposed values. # The expression should take two variables: element and value. # Element is a DOM element for the field, value # is the value selected by the user. # :select:: Pick the class of the element from which the value for # insertion should be extracted. If this is not specified, # the entire element is used. def auto_complete_field(field_id, options = {}) function = "var #{field_id}_auto_completer = new Ajax.Autocompleter(" function << "'#{field_id}', " function << "'" + (options[:update] || "#{field_id}_auto_complete") + "', " function << "'#{url_for(options[:url])}'" js_options = {} js_options[:tokens] = array_or_string_for_javascript(options[:tokens]) if options[:tokens] js_options[:callback] = "function(element, value) { return #{options[:with]} }" if options[:with] js_options[:indicator] = "'#{options[:indicator]}'" if options[:indicator] js_options[:select] = "'#{options[:select]}'" if options[:select] js_options[:frequency] = "#{options[:frequency]}" if options[:frequency] { :after_update_element => :afterUpdateElement, :on_show => :onShow, :on_hide => :onHide, :min_chars => :minChars }.each do |k,v| js_options[v] = options[k] if options[k] end function << (', ' + options_for_javascript(js_options) + ')') javascript_tag(function) end # Use this method in your view to generate a return for the AJAX autocomplete requests. # # Example action: # # def auto_complete_for_item_title # @items = Item.find(:all, # :conditions => [ 'LOWER(description) LIKE ?', # '%' + request.raw_post.downcase + '%' ]) # render :inline => '<%= auto_complete_result(@items, 'description') %>' # end # # The auto_complete_result can of course also be called from a view belonging to the # auto_complete action if you need to decorate it further. def auto_complete_result(entries, field, phrase = nil) return unless entries items = entries.map { |entry| content_tag("li", phrase ? highlight(entry[field], phrase) : h(entry[field])) } content_tag("ul", items.uniq) end # Wrapper for text_field with added AJAX autocompletion functionality. # # In your controller, you'll need to define an action called # auto_complete_for_object_method to respond the AJAX calls, # # See the RDoc on ActionController::AutoComplete to learn more about this. def text_field_with_auto_complete(object, method, tag_options = {}, completion_options = {}) (completion_options[:skip_style] ? "" : auto_complete_stylesheet) + text_field(object, method, tag_options) + content_tag("div", "", :id => "#{object}_#{method}_auto_complete", :class => "auto_complete") + auto_complete_field("#{object}_#{method}", { :url => { :action => "auto_complete_for_#{object}_#{method}" } }.update(completion_options)) end private def auto_complete_stylesheet content_tag("style", <<-EOT div.auto_complete { width: 350px; background: #fff; } div.auto_complete ul { border:1px solid #888; margin:0; padding:0; width:100%; list-style-type:none; } div.auto_complete ul li { margin:0; padding:3px; } div.auto_complete ul li.selected { background-color: #ffb; } div.auto_complete ul strong.highlight { color: #800; margin:0; padding:0; } EOT ) end end end end require File.dirname(__FILE__) + '/tag_helper' module ActionView module Helpers # Provides functionality for working with JavaScript in your views. # # == Ajax, controls and visual effects # # * For information on using Ajax, see # ActionView::Helpers::PrototypeHelper. # * For information on using controls and visual effects, see # ActionView::Helpers::ScriptaculousHelper. # # == Including the JavaScript libraries into your pages # # Rails includes the Prototype JavaScript framework and the Scriptaculous # JavaScript controls and visual effects library. If you wish to use # these libraries and their helpers (ActionView::Helpers::PrototypeHelper # and ActionView::Helpers::ScriptaculousHelper), you must do one of the # following: # # * Use <%= javascript_include_tag :defaults %> in the HEAD # section of your page (recommended): This function will return # references to the JavaScript files created by the +rails+ command in # your public/javascripts directory. Using it is recommended as # the browser can then cache the libraries instead of fetching all the # functions anew on every request. # * Use <%= javascript_include_tag 'prototype' %>: As above, but # will only include the Prototype core library, which means you are able # to use all basic AJAX functionality. For the Scriptaculous-based # JavaScript helpers, like visual effects, autocompletion, drag and drop # and so on, you should use the method described above. # * Use <%= define_javascript_functions %>: this will copy all the # JavaScript support functions within a single script block. Not # recommended. # # For documentation on +javascript_include_tag+ see # ActionView::Helpers::AssetTagHelper. module JavaScriptHelper unless const_defined? :JAVASCRIPT_PATH JAVASCRIPT_PATH = File.join(File.dirname(__FILE__), 'javascripts') end # Returns a link that'll trigger a JavaScript +function+ using the # onclick handler and return false after the fact. # # Examples: # link_to_function "Greeting", "alert('Hello world!')" # link_to_function(image_tag("delete"), "if confirm('Really?'){ do_delete(); }") def link_to_function(name, function, html_options = {}) html_options.symbolize_keys! content_tag( "a", name, html_options.merge({ :href => html_options[:href] || "#", :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function}; return false;" }) ) end # Returns a link that'll trigger a JavaScript +function+ using the # onclick handler. # # Examples: # button_to_function "Greeting", "alert('Hello world!')" # button_to_function "Delete", "if confirm('Really?'){ do_delete(); }") def button_to_function(name, function, html_options = {}) html_options.symbolize_keys! tag(:input, html_options.merge({ :type => "button", :value => name, :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};" })) end # Includes the Action Pack JavaScript libraries inside a single ' end # Escape carrier returns and single and double quotes for JavaScript segments. def escape_javascript(javascript) (javascript || '').gsub(/\r\n|\n|\r/, "\\n").gsub(/["']/) { |m| "\\#{m}" } end # Returns a JavaScript tag with the +content+ inside. Example: # javascript_tag "alert('All is good')" # => def javascript_tag(content) content_tag("script", javascript_cdata_section(content), :type => "text/javascript") end def javascript_cdata_section(content) #:nodoc: "\n//#{cdata_section("\n#{content}\n//")}\n" end protected def options_for_javascript(options) '{' + options.map {|k, v| "#{k}:#{v}"}.sort.join(', ') + '}' end def array_or_string_for_javascript(option) js_option = if option.kind_of?(Array) "['#{option.join('\',\'')}']" elsif !option.nil? "'#{option}'" end js_option end end JavascriptHelper = JavaScriptHelper unless const_defined? :JavascriptHelper end end module ActionView module Helpers # Provides methods for converting a number into a formatted string that currently represents # one of the following forms: phone number, percentage, money, or precision level. module NumberHelper # Formats a +number+ into a US phone number string. The +options+ can be a hash used to customize the format of the output. # The area code can be surrounded by parentheses by setting +:area_code+ to true; default is false # The delimiter can be set using +:delimiter+; default is "-" # Examples: # number_to_phone(1235551234) => 123-555-1234 # number_to_phone(1235551234, {:area_code => true}) => (123) 555-1234 # number_to_phone(1235551234, {:delimiter => " "}) => 123 555 1234 # number_to_phone(1235551234, {:area_code => true, :extension => 555}) => (123) 555-1234 x 555 def number_to_phone(number, options = {}) options = options.stringify_keys area_code = options.delete("area_code") { false } delimiter = options.delete("delimiter") { "-" } extension = options.delete("extension") { "" } begin str = area_code == true ? number.to_s.gsub(/([0-9]{3})([0-9]{3})([0-9]{4})/,"(\\1) \\2#{delimiter}\\3") : number.to_s.gsub(/([0-9]{3})([0-9]{3})([0-9]{4})/,"\\1#{delimiter}\\2#{delimiter}\\3") extension.to_s.strip.empty? ? str : "#{str} x #{extension.to_s.strip}" rescue number end end # Formats a +number+ into a currency string. The +options+ hash can be used to customize the format of the output. # The +number+ can contain a level of precision using the +precision+ key; default is 2 # The currency type can be set using the +unit+ key; default is "$" # The unit separator can be set using the +separator+ key; default is "." # The delimiter can be set using the +delimiter+ key; default is "," # Examples: # number_to_currency(1234567890.50) => $1,234,567,890.50 # number_to_currency(1234567890.506) => $1,234,567,890.51 # number_to_currency(1234567890.50, {:unit => "£", :separator => ",", :delimiter => ""}) => £1234567890,50 def number_to_currency(number, options = {}) options = options.stringify_keys precision, unit, separator, delimiter = options.delete("precision") { 2 }, options.delete("unit") { "$" }, options.delete("separator") { "." }, options.delete("delimiter") { "," } separator = "" unless precision > 0 begin parts = number_with_precision(number, precision).split('.') unit + number_with_delimiter(parts[0], delimiter) + separator + parts[1].to_s rescue number end end # Formats a +number+ as into a percentage string. The +options+ hash can be used to customize the format of the output. # The +number+ can contain a level of precision using the +precision+ key; default is 3 # The unit separator can be set using the +separator+ key; default is "." # Examples: # number_to_percentage(100) => 100.000% # number_to_percentage(100, {:precision => 0}) => 100% # number_to_percentage(302.0574, {:precision => 2}) => 302.06% def number_to_percentage(number, options = {}) options = options.stringify_keys precision, separator = options.delete("precision") { 3 }, options.delete("separator") { "." } begin number = number_with_precision(number, precision) parts = number.split('.') if parts.at(1).nil? parts[0] + "%" else parts[0] + separator + parts[1].to_s + "%" end rescue number end end # Formats a +number+ with a +delimiter+. # Example: # number_with_delimiter(12345678) => 12,345,678 def number_with_delimiter(number, delimiter=",") number.to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{delimiter}") end # Returns a formatted-for-humans file size. # # Examples: # human_size(123) => 123 Bytes # human_size(1234) => 1.2 KB # human_size(12345) => 12.1 KB # human_size(1234567) => 1.2 MB # human_size(1234567890) => 1.1 GB def number_to_human_size(size) case when size < 1.kilobyte: '%d Bytes' % size when size < 1.megabyte: '%.1f KB' % (size / 1.0.kilobyte) when size < 1.gigabyte: '%.1f MB' % (size / 1.0.megabyte) when size < 1.terabyte: '%.1f GB' % (size / 1.0.gigabyte) else '%.1f TB' % (size / 1.0.terabyte) end.sub('.0', '') rescue nil end alias_method :human_size, :number_to_human_size # deprecated alias # Formats a +number+ with a level of +precision+. # Example: # number_with_precision(111.2345) => 111.235 def number_with_precision(number, precision=3) sprintf("%01.#{precision}f", number) end end end end module ActionView module Helpers # Provides methods for linking to ActionController::Pagination objects. # # You can also build your links manually, like in this example: # # <%= link_to "Previous page", { :page => paginator.current.previous } if paginator.current.previous %> # # <%= link_to "Next page", { :page => paginator.current.next } if paginator.current.next %> module PaginationHelper unless const_defined?(:DEFAULT_OPTIONS) DEFAULT_OPTIONS = { :name => :page, :window_size => 2, :always_show_anchors => true, :link_to_current_page => false, :params => {} } end # Creates a basic HTML link bar for the given +paginator+. # +html_options+ are passed to +link_to+. # # +options+ are: # :name:: the routing name for this paginator # (defaults to +page+) # :window_size:: the number of pages to show around # the current page (defaults to +2+) # :always_show_anchors:: whether or not the first and last # pages should always be shown # (defaults to +true+) # :link_to_current_page:: whether or not the current page # should be linked to (defaults to # +false+) # :params:: any additional routing parameters # for page URLs def pagination_links(paginator, options={}, html_options={}) name = options[:name] || DEFAULT_OPTIONS[:name] params = (options[:params] || DEFAULT_OPTIONS[:params]).clone pagination_links_each(paginator, options) do |n| params[name] = n link_to(n.to_s, params, html_options) end end # Iterate through the pages of a given +paginator+, invoking a # block for each page number that needs to be rendered as a link. def pagination_links_each(paginator, options) options = DEFAULT_OPTIONS.merge(options) link_to_current_page = options[:link_to_current_page] always_show_anchors = options[:always_show_anchors] current_page = paginator.current_page window_pages = current_page.window(options[:window_size]).pages return if window_pages.length <= 1 unless link_to_current_page first, last = paginator.first, paginator.last html = '' if always_show_anchors and not (wp_first = window_pages[0]).first? html << yield(first.number) html << ' ... ' if wp_first.number - first.number > 1 html << ' ' end window_pages.each do |page| if current_page == page && !link_to_current_page html << page.number.to_s else html << yield(page.number) end html << ' ' end if always_show_anchors and not (wp_last = window_pages[-1]).last? html << ' ... ' if last.number - wp_last.number > 1 html << yield(last.number) end html end end # PaginationHelper end # Helpers end # ActionView require File.dirname(__FILE__) + '/javascript_helper' require 'set' module ActionView module Helpers # Provides a set of helpers for calling Prototype JavaScript functions, # including functionality to call remote methods using # Ajax[http://www.adaptivepath.com/publications/essays/archives/000385.php]. # This means that you can call actions in your controllers without # reloading the page, but still update certain parts of it using # injections into the DOM. The common use case is having a form that adds # a new element to a list without reloading the page. # # To be able to use these helpers, you must include the Prototype # JavaScript framework in your pages. See the documentation for # ActionView::Helpers::JavaScriptHelper for more information on including # the necessary JavaScript. # # See link_to_remote for documentation of options common to all Ajax # helpers. # # See also ActionView::Helpers::ScriptaculousHelper for helpers which work # with the Scriptaculous controls and visual effects library. # # See JavaScriptGenerator for information on updating multiple elements # on the page in an Ajax response. module PrototypeHelper unless const_defined? :CALLBACKS CALLBACKS = Set.new([ :uninitialized, :loading, :loaded, :interactive, :complete, :failure, :success ] + (100..599).to_a) AJAX_OPTIONS = Set.new([ :before, :after, :condition, :url, :asynchronous, :method, :insertion, :position, :form, :with, :update, :script ]).merge(CALLBACKS) end # Returns a link to a remote action defined by options[:url] # (using the url_for format) that's called in the background using # XMLHttpRequest. The result of that request can then be inserted into a # DOM object whose id can be specified with options[:update]. # Usually, the result would be a partial prepared by the controller with # either render_partial or render_partial_collection. # # Examples: # link_to_remote "Delete this post", :update => "posts", # :url => { :action => "destroy", :id => post.id } # link_to_remote(image_tag("refresh"), :update => "emails", # :url => { :action => "list_emails" }) # # You can also specify a hash for options[:update] to allow for # easy redirection of output to an other DOM element if a server-side # error occurs: # # Example: # link_to_remote "Delete this post", # :url => { :action => "destroy", :id => post.id }, # :update => { :success => "posts", :failure => "error" } # # Optionally, you can use the options[:position] parameter to # influence how the target DOM element is updated. It must be one of # :before, :top, :bottom, or :after. # # By default, these remote requests are processed asynchronous during # which various JavaScript callbacks can be triggered (for progress # indicators and the likes). All callbacks get access to the # request object, which holds the underlying XMLHttpRequest. # # To access the server response, use request.responseText, to # find out the HTTP status, use request.status. # # Example: # link_to_remote word, # :url => { :action => "undo", :n => word_counter }, # :complete => "undoRequestCompleted(request)" # # The callbacks that may be specified are (in order): # # :loading:: Called when the remote document is being # loaded with data by the browser. # :loaded:: Called when the browser has finished loading # the remote document. # :interactive:: Called when the user can interact with the # remote document, even though it has not # finished loading. # :success:: Called when the XMLHttpRequest is completed, # and the HTTP status code is in the 2XX range. # :failure:: Called when the XMLHttpRequest is completed, # and the HTTP status code is not in the 2XX # range. # :complete:: Called when the XMLHttpRequest is complete # (fires after success/failure if they are # present). # # You can further refine :success and :failure by # adding additional callbacks for specific status codes. # # Example: # link_to_remote word, # :url => { :action => "action" }, # 404 => "alert('Not found...? Wrong URL...?')", # :failure => "alert('HTTP Error ' + request.status + '!')" # # A status code callback overrides the success/failure handlers if # present. # # If you for some reason or another need synchronous processing (that'll # block the browser while the request is happening), you can specify # options[:type] = :synchronous. # # You can customize further browser side call logic by passing in # JavaScript code snippets via some optional parameters. In their order # of use these are: # # :confirm:: Adds confirmation dialog. # :condition:: Perform remote request conditionally # by this expression. Use this to # describe browser-side conditions when # request should not be initiated. # :before:: Called before request is initiated. # :after:: Called immediately after request was # initiated and before :loading. # :submit:: Specifies the DOM element ID that's used # as the parent of the form elements. By # default this is the current form, but # it could just as well be the ID of a # table row or any other DOM element. def link_to_remote(name, options = {}, html_options = {}) link_to_function(name, remote_function(options), html_options) end # Periodically calls the specified url (options[:url]) every # options[:frequency] seconds (default is 10). Usually used to # update a specified div (options[:update]) with the results # of the remote call. The options for specifying the target with :url # and defining callbacks is the same as link_to_remote. def periodically_call_remote(options = {}) frequency = options[:frequency] || 10 # every ten seconds by default code = "new PeriodicalExecuter(function() {#{remote_function(options)}}, #{frequency})" javascript_tag(code) end # Returns a form tag that will submit using XMLHttpRequest in the # background instead of the regular reloading POST arrangement. Even # though it's using JavaScript to serialize the form elements, the form # submission will work just like a regular submission as viewed by the # receiving side (all elements available in params). The options for # specifying the target with :url and defining callbacks is the same as # link_to_remote. # # A "fall-through" target for browsers that doesn't do JavaScript can be # specified with the :action/:method options on :html. # # Example: # form_remote_tag :html => { :action => # url_for(:controller => "some", :action => "place") } # # The Hash passed to the :html key is equivalent to the options (2nd) # argument in the FormTagHelper.form_tag method. # # By default the fall-through action is the same as the one specified in # the :url (and the default method is :post). def form_remote_tag(options = {}) options[:form] = true options[:html] ||= {} options[:html][:onsubmit] = "#{remote_function(options)}; return false;" options[:html][:action] = options[:html][:action] || url_for(options[:url]) options[:html][:method] = options[:html][:method] || "post" tag("form", options[:html], true) end # Works like form_remote_tag, but uses form_for semantics. def remote_form_for(object_name, *args, &proc) options = args.last.is_a?(Hash) ? args.pop : {} concat(form_remote_tag(options), proc.binding) fields_for(object_name, *(args << options), &proc) concat('', proc.binding) end alias_method :form_remote_for, :remote_form_for # Returns a button input tag that will submit form using XMLHttpRequest # in the background instead of regular reloading POST arrangement. # options argument is the same as in form_remote_tag. def submit_to_remote(name, value, options = {}) options[:with] ||= 'Form.serialize(this.form)' options[:html] ||= {} options[:html][:type] = 'button' options[:html][:onclick] = "#{remote_function(options)}; return false;" options[:html][:name] = name options[:html][:value] = value tag("input", options[:html], false) end # Returns a JavaScript function (or expression) that'll update a DOM # element according to the options passed. # # * :content: The content to use for updating. Can be left out # if using block, see example. # * :action: Valid options are :update (assumed by default), # :empty, :remove # * :position If the :action is :update, you can optionally # specify one of the following positions: :before, :top, :bottom, # :after. # # Examples: # <%= javascript_tag(update_element_function("products", # :position => :bottom, :content => "

    New product!

    ")) %> # # <% replacement_function = update_element_function("products") do %> #

    Product 1

    #

    Product 2

    # <% end %> # <%= javascript_tag(replacement_function) %> # # This method can also be used in combination with remote method call # where the result is evaluated afterwards to cause multiple updates on # a page. Example: # # # Calling view # <%= form_remote_tag :url => { :action => "buy" }, # :complete => evaluate_remote_response %> # all the inputs here... # # # Controller action # def buy # @product = Product.find(1) # end # # # Returning view # <%= update_element_function( # "cart", :action => :update, :position => :bottom, # :content => "

    New Product: #{@product.name}

    ")) %> # <% update_element_function("status", :binding => binding) do %> # You've bought a new product! # <% end %> # # Notice how the second call doesn't need to be in an ERb output block # since it uses a block and passes in the binding to render directly. # This trick will however only work in ERb (not Builder or other # template forms). # # See also JavaScriptGenerator and update_page. def update_element_function(element_id, options = {}, &block) content = escape_javascript(options[:content] || '') content = escape_javascript(capture(&block)) if block javascript_function = case (options[:action] || :update) when :update if options[:position] "new Insertion.#{options[:position].to_s.camelize}('#{element_id}','#{content}')" else "$('#{element_id}').innerHTML = '#{content}'" end when :empty "$('#{element_id}').innerHTML = ''" when :remove "Element.remove('#{element_id}')" else raise ArgumentError, "Invalid action, choose one of :update, :remove, :empty" end javascript_function << ";\n" options[:binding] ? concat(javascript_function, options[:binding]) : javascript_function end # Returns 'eval(request.responseText)' which is the JavaScript function # that form_remote_tag can call in :complete to evaluate a multiple # update return document using update_element_function calls. def evaluate_remote_response "eval(request.responseText)" end # Returns the JavaScript needed for a remote function. # Takes the same arguments as link_to_remote. # # Example: # def remote_function(options) javascript_options = options_for_ajax(options) update = '' if options[:update] and options[:update].is_a?Hash update = [] update << "success:'#{options[:update][:success]}'" if options[:update][:success] update << "failure:'#{options[:update][:failure]}'" if options[:update][:failure] update = '{' + update.join(',') + '}' elsif options[:update] update << "'#{options[:update]}'" end function = update.empty? ? "new Ajax.Request(" : "new Ajax.Updater(#{update}, " url_options = options[:url] url_options = url_options.merge(:escape => false) if url_options.is_a? Hash function << "'#{url_for(url_options)}'" function << ", #{javascript_options})" function = "#{options[:before]}; #{function}" if options[:before] function = "#{function}; #{options[:after]}" if options[:after] function = "if (#{options[:condition]}) { #{function}; }" if options[:condition] function = "if (confirm('#{escape_javascript(options[:confirm])}')) { #{function}; }" if options[:confirm] return function end # Observes the field with the DOM ID specified by +field_id+ and makes # an Ajax call when its contents have changed. # # Required +options+ are either of: # :url:: +url_for+-style options for the action to call # when the field has changed. # :function:: Instead of making a remote call to a URL, you # can specify a function to be called instead. # # Additional options are: # :frequency:: The frequency (in seconds) at which changes to # this field will be detected. Not setting this # option at all or to a value equal to or less than # zero will use event based observation instead of # time based observation. # :update:: Specifies the DOM ID of the element whose # innerHTML should be updated with the # XMLHttpRequest response text. # :with:: A JavaScript expression specifying the # parameters for the XMLHttpRequest. This defaults # to 'value', which in the evaluated context # refers to the new field value. If you specify a # string without a "=", it'll be extended to mean # the form key that the value should be assigned to. # So :with => "term" gives "'term'=value". If a "=" is # present, no extension will happen. # :on:: Specifies which event handler to observe. By default, # it's set to "changed" for text fields and areas and # "click" for radio buttons and checkboxes. With this, # you can specify it instead to be "blur" or "focus" or # any other event. # # Additionally, you may specify any of the options documented in # link_to_remote. def observe_field(field_id, options = {}) if options[:frequency] && options[:frequency] > 0 build_observer('Form.Element.Observer', field_id, options) else build_observer('Form.Element.EventObserver', field_id, options) end end # Like +observe_field+, but operates on an entire form identified by the # DOM ID +form_id+. +options+ are the same as +observe_field+, except # the default value of the :with option evaluates to the # serialized (request string) value of the form. def observe_form(form_id, options = {}) if options[:frequency] build_observer('Form.Observer', form_id, options) else build_observer('Form.EventObserver', form_id, options) end end # All the methods were moved to GeneratorMethods so that # #include_helpers_from_context has nothing to overwrite. class JavaScriptGenerator #:nodoc: def initialize(context, &block) #:nodoc: @context, @lines = context, [] include_helpers_from_context @context.instance_exec(self, &block) end private def include_helpers_from_context @context.extended_by.each do |mod| extend mod unless mod.name =~ /^ActionView::Helpers/ end extend GeneratorMethods end # JavaScriptGenerator generates blocks of JavaScript code that allow you # to change the content and presentation of multiple DOM elements. Use # this in your Ajax response bodies, either in a # # mail_to "me@domain.com", "My email", :encode => "hex" # => # My email # # You can also specify the cc address, bcc address, subject, and body parts of the message header to create a complex e-mail using the # corresponding +cc+, +bcc+, +subject+, and +body+ html_options keys. Each of these options are URI escaped and then appended to # the email_address before being output. Be aware that javascript keywords will not be escaped and may break this feature # when encoding with javascript. # Examples: # mail_to "me@domain.com", "My email", :cc => "ccaddress@domain.com", :bcc => "bccaddress@domain.com", :subject => "This is an example email", :body => "This is the body of the message." # => # My email def mail_to(email_address, name = nil, html_options = {}) html_options = html_options.stringify_keys encode = html_options.delete("encode") cc, bcc, subject, body = html_options.delete("cc"), html_options.delete("bcc"), html_options.delete("subject"), html_options.delete("body") string = '' extras = '' extras << "cc=#{CGI.escape(cc).gsub("+", "%20")}&" unless cc.nil? extras << "bcc=#{CGI.escape(bcc).gsub("+", "%20")}&" unless bcc.nil? extras << "body=#{CGI.escape(body).gsub("+", "%20")}&" unless body.nil? extras << "subject=#{CGI.escape(subject).gsub("+", "%20")}&" unless subject.nil? extras = "?" << extras.gsub!(/&?$/,"") unless extras.empty? email_address_obfuscated = email_address.dup email_address_obfuscated.gsub!(/@/, html_options.delete("replace_at")) if html_options.has_key?("replace_at") email_address_obfuscated.gsub!(/\./, html_options.delete("replace_dot")) if html_options.has_key?("replace_dot") if encode == 'javascript' tmp = "document.write('#{content_tag("a", name || email_address, html_options.merge({ "href" => "mailto:"+email_address.to_s+extras }))}');" for i in 0...tmp.length string << sprintf("%%%x",tmp[i]) end "" elsif encode == 'hex' for i in 0...email_address.length if email_address[i,1] =~ /\w/ string << sprintf("%%%x",email_address[i]) else string << email_address[i,1] end end content_tag "a", name || email_address_obfuscated, html_options.merge({ "href" => "mailto:#{string}#{extras}" }) else content_tag "a", name || email_address_obfuscated, html_options.merge({ "href" => "mailto:#{email_address}#{extras}" }) end end # Returns true if the current page uri is generated by the options passed (in url_for format). def current_page?(options) CGI.escapeHTML(url_for(options)) == @controller.request.request_uri end private def convert_options_to_javascript!(html_options) confirm, popup, post = html_options.delete("confirm"), html_options.delete("popup"), html_options.delete("post") html_options["onclick"] = case when popup && post raise ActionView::ActionViewError, "You can't use :popup and :post in the same link" when confirm && popup "if (#{confirm_javascript_function(confirm)}) { #{popup_javascript_function(popup)} };return false;" when confirm && post "if (#{confirm_javascript_function(confirm)}) { #{post_javascript_function} };return false;" when confirm "return #{confirm_javascript_function(confirm)};" when post "#{post_javascript_function}return false;" when popup popup_javascript_function(popup) + 'return false;' else html_options["onclick"] end end def confirm_javascript_function(confirm) "confirm('#{escape_javascript(confirm)}')" end def popup_javascript_function(popup) popup.is_a?(Array) ? "window.open(this.href,'#{popup.first}','#{popup.last}');" : "window.open(this.href);" end def post_javascript_function "var f = document.createElement('form'); this.parentNode.appendChild(f); f.method = 'POST'; f.action = this.href; f.submit();" end # Processes the _html_options_ hash, converting the boolean # attributes from true/false form into the form required by # HTML/XHTML. (An attribute is considered to be boolean if # its name is listed in the given _bool_attrs_ array.) # # More specifically, for each boolean attribute in _html_options_ # given as: # # "attr" => bool_value # # if the associated _bool_value_ evaluates to true, it is # replaced with the attribute's name; otherwise the attribute is # removed from the _html_options_ hash. (See the XHTML 1.0 spec, # section 4.5 "Attribute Minimization" for more: # http://www.w3.org/TR/xhtml1/#h-4.5) # # Returns the updated _html_options_ hash, which is also modified # in place. # # Example: # # convert_boolean_attributes!( html_options, # %w( checked disabled readonly ) ) def convert_boolean_attributes!(html_options, bool_attrs) bool_attrs.each { |x| html_options[x] = x if html_options.delete(x) } html_options end end end end module ActionView # There's also a convenience method for rendering sub templates within the current controller that depends on a single object # (we call this kind of sub templates for partials). It relies on the fact that partials should follow the naming convention of being # prefixed with an underscore -- as to separate them from regular templates that could be rendered on their own. # # In a template for Advertiser#account: # # <%= render :partial => "account" %> # # This would render "advertiser/_account.rhtml" and pass the instance variable @account in as a local variable +account+ to # the template for display. # # In another template for Advertiser#buy, we could have: # # <%= render :partial => "account", :locals => { :account => @buyer } %> # # <% for ad in @advertisements %> # <%= render :partial => "ad", :locals => { :ad => ad } %> # <% end %> # # This would first render "advertiser/_account.rhtml" with @buyer passed in as the local variable +account+, then render # "advertiser/_ad.rhtml" and pass the local variable +ad+ to the template for display. # # == Rendering a collection of partials # # The example of partial use describes a familiar pattern where a template needs to iterate over an array and render a sub # template for each of the elements. This pattern has been implemented as a single method that accepts an array and renders # a partial by the same name as the elements contained within. So the three-lined example in "Using partials" can be rewritten # with a single line: # # <%= render :partial => "ad", :collection => @advertisements %> # # This will render "advertiser/_ad.rhtml" and pass the local variable +ad+ to the template for display. An iteration counter # will automatically be made available to the template with a name of the form +partial_name_counter+. In the case of the # example above, the template would be fed +ad_counter+. # # NOTE: Due to backwards compatibility concerns, the collection can't be one of hashes. Normally you'd also just keep domain objects, # like Active Records, in there. # # == Rendering shared partials # # Two controllers can share a set of partials and render them like this: # # <%= render :partial => "advertisement/ad", :locals => { :ad => @advertisement } %> # # This will render the partial "advertisement/_ad.rhtml" regardless of which controller this is being called from. module Partials # Deprecated, use render :partial def render_partial(partial_path, local_assigns = nil, deprecated_local_assigns = nil) #:nodoc: path, partial_name = partial_pieces(partial_path) object = extracting_object(partial_name, local_assigns, deprecated_local_assigns) local_assigns = extract_local_assigns(local_assigns, deprecated_local_assigns) local_assigns = local_assigns ? local_assigns.clone : {} add_counter_to_local_assigns!(partial_name, local_assigns) add_object_to_local_assigns!(partial_name, local_assigns, object) if logger ActionController::Base.benchmark("Rendered #{path}/_#{partial_name}", Logger::DEBUG, false) do render("#{path}/_#{partial_name}", local_assigns) end else render("#{path}/_#{partial_name}", local_assigns) end end # Deprecated, use render :partial, :collection def render_partial_collection(partial_name, collection, partial_spacer_template = nil, local_assigns = nil) #:nodoc: collection_of_partials = Array.new counter_name = partial_counter_name(partial_name) local_assigns = local_assigns ? local_assigns.clone : {} collection.each_with_index do |element, counter| local_assigns[counter_name] = counter collection_of_partials.push(render_partial(partial_name, element, local_assigns)) end return " " if collection_of_partials.empty? if partial_spacer_template spacer_path, spacer_name = partial_pieces(partial_spacer_template) collection_of_partials.join(render("#{spacer_path}/_#{spacer_name}")) else collection_of_partials.join end end alias_method :render_collection_of_partials, :render_partial_collection private def partial_pieces(partial_path) if partial_path.include?('/') return File.dirname(partial_path), File.basename(partial_path) else return controller.class.controller_path, partial_path end end def partial_counter_name(partial_name) "#{partial_name.split('/').last}_counter".intern end def extracting_object(partial_name, local_assigns, deprecated_local_assigns) if local_assigns.is_a?(Hash) || local_assigns.nil? controller.instance_variable_get("@#{partial_name}") else # deprecated form where object could be passed in as second parameter local_assigns end end def extract_local_assigns(local_assigns, deprecated_local_assigns) local_assigns.is_a?(Hash) ? local_assigns : deprecated_local_assigns end def add_counter_to_local_assigns!(partial_name, local_assigns) counter_name = partial_counter_name(partial_name) local_assigns[counter_name] = 1 unless local_assigns.has_key?(counter_name) end def add_object_to_local_assigns!(partial_name, local_assigns, object) local_assigns[partial_name.intern] ||= if object.is_a?(ActionView::Base::ObjectWrapper) object.value else object end || controller.instance_variable_get("@#{partial_name}") end end end module ActionView # The TemplateError exception is raised when the compilation of the template fails. This exception then gathers a # bunch of intimate details and uses it to report a very precise exception message. class TemplateError < ActionViewError #:nodoc: SOURCE_CODE_RADIUS = 3 attr_reader :original_exception def initialize(base_path, file_name, assigns, source, original_exception) @base_path, @assigns, @source, @original_exception = base_path, assigns, source, original_exception @file_name = file_name end def message original_exception.message end def sub_template_message if @sub_templates "Trace of template inclusion: " + @sub_templates.collect { |template| strip_base_path(template) }.join(", ") else "" end end def source_extract(indention = 0) source_code = IO.readlines(@file_name) start_on_line = [ line_number - SOURCE_CODE_RADIUS - 1, 0 ].max end_on_line = [ line_number + SOURCE_CODE_RADIUS - 1, source_code.length].min line_counter = start_on_line extract = source_code[start_on_line..end_on_line].collect do |line| line_counter += 1 "#{' ' * indention}#{line_counter}: " + line end extract.join end def sub_template_of(file_name) @sub_templates ||= [] @sub_templates << file_name end def line_number if file_name regexp = /#{Regexp.escape File.basename(file_name)}:(\d+)/ [@original_exception.message, @original_exception.clean_backtrace].flatten.each do |line| return $1.to_i if regexp =~ line end end 0 end def file_name stripped = strip_base_path(@file_name) stripped[0] == ?/ ? stripped[1..-1] : stripped end def to_s "\n\n#{self.class} (#{message}) on line ##{line_number} of #{file_name}:\n" + source_extract + "\n " + original_exception.clean_backtrace.join("\n ") + "\n\n" end def backtrace [ "On line ##{line_number} of #{file_name}\n\n#{source_extract(4)}\n " + original_exception.clean_backtrace.join("\n ") ] end private def strip_base_path(file_name) file_name = File.expand_path(file_name).gsub(/^#{Regexp.escape File.expand_path(RAILS_ROOT)}/, '') file_name.gsub(@base_path, "") end end end Exception::TraceSubstitutions << [/:in\s+`_run_(html|xml).*'\s*$/, ''] if defined?(Exception::TraceSubstitutions) Exception::TraceSubstitutions << [%r{^\s*#{Regexp.escape RAILS_ROOT}}, '#{RAILS_ROOT}'] if defined?(RAILS_ROOT) #-- # Copyright (c) 2004 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ $:.unshift(File.dirname(__FILE__) + "/action_view/vendor") require 'action_view/base' require 'action_view/partials' ActionView::Base.class_eval do include ActionView::Partials end ActionView::Base.load_helpers(File.dirname(__FILE__) + "/action_view/helpers/") $:.unshift(File.dirname(__FILE__) + '/../lib') $:.unshift(File.dirname(__FILE__) + '/../../activesupport/lib/active_support') $:.unshift(File.dirname(__FILE__) + '/fixtures/helpers') require 'yaml' require 'test/unit' require 'action_controller' require 'breakpoint' require 'action_controller/test_process' ActionController::Base.logger = nil ActionController::Base.ignore_missing_templates = false ActionController::Routing::Routes.reload rescue nilrequire File.dirname(__FILE__) + '/abstract_unit' # Define the essentials class ActiveRecordTestConnector cattr_accessor :able_to_connect cattr_accessor :connected # Set our defaults self.connected = false self.able_to_connect = true end # Try to grab AR begin PATH_TO_AR = File.dirname(__FILE__) + '/../../activerecord' require "#{PATH_TO_AR}/lib/active_record" unless Object.const_defined?(:ActiveRecord) require "#{PATH_TO_AR}/lib/active_record/fixtures" unless Object.const_defined?(:Fixtures) rescue Object => e $stderr.puts "\nSkipping ActiveRecord assertion tests: #{e}" ActiveRecordTestConnector.able_to_connect = false end # Define the rest of the connector class ActiveRecordTestConnector def self.setup unless self.connected || !self.able_to_connect setup_connection load_schema self.connected = true end rescue Object => e $stderr.puts "\nSkipping ActiveRecord assertion tests: #{e}" #$stderr.puts " #{e.backtrace.join("\n ")}\n" self.able_to_connect = false end private def self.setup_connection if Object.const_defined?(:ActiveRecord) begin ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :dbfile => ':memory:') ActiveRecord::Base.connection rescue Object $stderr.puts 'SQLite 3 unavailable; falling to SQLite 2.' ActiveRecord::Base.establish_connection(:adapter => 'sqlite', :dbfile => ':memory:') ActiveRecord::Base.connection end Object.send(:const_set, :QUOTED_TYPE, ActiveRecord::Base.connection.quote_column_name('type')) unless Object.const_defined?(:QUOTED_TYPE) else raise "Couldn't locate ActiveRecord." end end # Load actionpack sqlite tables def self.load_schema File.read(File.dirname(__FILE__) + "/fixtures/db_definitions/sqlite.sql").split(';').each do |sql| ActiveRecord::Base.connection.execute(sql) unless sql.blank? end end end # Test case for inheiritance class ActiveRecordTestCase < Test::Unit::TestCase # Set our fixture path self.fixture_path = "#{File.dirname(__FILE__)}/fixtures/" def setup abort_tests unless ActiveRecordTestConnector.connected = true end # Default so Test::Unit::TestCase doesn't complain def test_truth end private # If things go wrong, we don't want to run our test cases. We'll just define them to test nothing. def abort_tests self.class.public_instance_methods.grep(/^test./).each do |method| self.class.class_eval { define_method(method.to_sym){} } end end end ActiveRecordTestConnector.setuprequire "#{File.dirname(__FILE__)}/../active_record_unit" require 'fixtures/company' class ActiveRecordAssertionsController < ActionController::Base self.template_root = "#{File.dirname(__FILE__)}/../fixtures/" # fail with 1 bad column def nasty_columns_1 @company = Company.new @company.name = "B" @company.rating = 2 render :inline => "snicker...." end # fail with 2 bad columns def nasty_columns_2 @company = Company.new @company.name = "" @company.rating = 2 render :inline => "double snicker...." end # this will pass validation def good_company @company = Company.new @company.name = "A" @company.rating = 69 render :inline => "Goodness Gracious!" end # this will fail validation def bad_company @company = Company.new render :inline => "Who's Bad?" end # the safety dance...... def rescue_action(e) raise; end end class ActiveRecordAssertionsControllerTest < ActiveRecordTestCase fixtures :companies def setup @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new @controller = ActiveRecordAssertionsController.new super end # test for 1 bad apple column def test_some_invalid_columns process :nasty_columns_1 assert_success assert_invalid_record 'company' assert_invalid_column_on_record 'company', 'rating' assert_valid_column_on_record 'company', 'name' assert_valid_column_on_record 'company', %w(name id) end # test for 2 bad apples columns def test_all_invalid_columns process :nasty_columns_2 assert_success assert_invalid_record 'company' assert_invalid_column_on_record 'company', 'rating' assert_invalid_column_on_record 'company', 'name' assert_invalid_column_on_record 'company', %w(name rating) end # ensure we have no problems with an ActiveRecord def test_valid_record process :good_company assert_success assert_valid_record 'company' end # ensure we have problems with an ActiveRecord def test_invalid_record process :bad_company assert_success assert_invalid_record 'company' end end# Unfurl the safety net. path_to_ar = File.dirname(__FILE__) + '/../../../activerecord' if Object.const_defined?(:ActiveRecord) or File.exist?(path_to_ar) begin # These tests exercise CGI::Session::ActiveRecordStore, so you're going to # need AR in a sibling directory to AP and have SQLite installed. unless Object.const_defined?(:ActiveRecord) require File.join(path_to_ar, 'lib', 'active_record') end require File.dirname(__FILE__) + '/../abstract_unit' require 'action_controller/session/active_record_store' #ActiveRecord::Base.logger = Logger.new($stdout) begin CGI::Session::ActiveRecordStore::Session.establish_connection(:adapter => 'sqlite3', :database => ':memory:') CGI::Session::ActiveRecordStore::Session.connection rescue Object $stderr.puts 'SQLite 3 unavailable; falling back to SQLite 2.' begin CGI::Session::ActiveRecordStore::Session.establish_connection(:adapter => 'sqlite', :database => ':memory:') CGI::Session::ActiveRecordStore::Session.connection rescue Object $stderr.puts 'SQLite 2 unavailable; skipping ActiveRecordStore test suite.' raise SystemExit end end module CommonActiveRecordStoreTests def test_basics s = session_class.new(:session_id => '1234', :data => { 'foo' => 'bar' }) assert_equal 'bar', s.data['foo'] assert s.save assert_equal 'bar', s.data['foo'] assert_not_nil t = session_class.find_by_session_id('1234') assert_not_nil t.data assert_equal 'bar', t.data['foo'] end def test_reload_same_session @new_session.update reloaded = CGI::Session.new(CGI.new, 'session_id' => @new_session.session_id, 'database_manager' => CGI::Session::ActiveRecordStore) assert_equal 'bar', reloaded['foo'] end def test_tolerates_close_close assert_nothing_raised do @new_session.close @new_session.close end end end class ActiveRecordStoreTest < Test::Unit::TestCase include CommonActiveRecordStoreTests def session_class CGI::Session::ActiveRecordStore::Session end def session_id_column "session_id" end def setup session_class.create_table! ENV['REQUEST_METHOD'] = 'GET' CGI::Session::ActiveRecordStore.session_class = session_class @cgi = CGI.new @new_session = CGI::Session.new(@cgi, 'database_manager' => CGI::Session::ActiveRecordStore, 'new_session' => true) @new_session['foo'] = 'bar' end # this test only applies for eager sesssion saving # def test_another_instance # @another = CGI::Session.new(@cgi, 'session_id' => @new_session.session_id, 'database_manager' => CGI::Session::ActiveRecordStore) # assert_equal @new_session.session_id, @another.session_id # end def test_model_attribute assert_kind_of CGI::Session::ActiveRecordStore::Session, @new_session.model assert_equal({ 'foo' => 'bar' }, @new_session.model.data) end def test_save_unloaded_session c = session_class.connection bogus_class = c.quote(Base64.encode64("\004\010o:\vBlammo\000")) c.insert("INSERT INTO #{session_class.table_name} ('#{session_id_column}', 'data') VALUES ('abcdefghijklmnop', #{bogus_class})") sess = session_class.find_by_session_id('abcdefghijklmnop') assert_not_nil sess assert !sess.loaded? # because the session is not loaded, the save should be a no-op. If it # isn't, this'll try and unmarshall the bogus class, and should get an error. assert_nothing_raised { sess.save } end def teardown session_class.drop_table! end end class ColumnLimitTest < Test::Unit::TestCase def setup @session_class = CGI::Session::ActiveRecordStore::Session @session_class.create_table! end def teardown @session_class.drop_table! end def test_protection_from_data_larger_than_column # Can't test this unless there is a limit return unless limit = @session_class.data_column_size_limit too_big = ':(' * limit s = @session_class.new(:session_id => '666', :data => {'foo' => too_big}) s.data assert_raise(ActionController::SessionOverflowError) { s.save } end end class DeprecatedActiveRecordStoreTest < ActiveRecordStoreTest def session_id_column "sessid" end def setup session_class.connection.execute 'create table old_sessions (id integer primary key, sessid text unique, data text)' session_class.table_name = 'old_sessions' session_class.send :setup_sessid_compatibility! ENV['REQUEST_METHOD'] = 'GET' CGI::Session::ActiveRecordStore.session_class = session_class @new_session = CGI::Session.new(CGI.new, 'database_manager' => CGI::Session::ActiveRecordStore, 'new_session' => true) @new_session['foo'] = 'bar' end def teardown session_class.connection.execute 'drop table old_sessions' session_class.table_name = 'sessions' end end class SqlBypassActiveRecordStoreTest < ActiveRecordStoreTest def session_class unless @session_class @session_class = CGI::Session::ActiveRecordStore::SqlBypass @session_class.connection = CGI::Session::ActiveRecordStore::Session.connection end @session_class end def test_model_attribute assert_kind_of CGI::Session::ActiveRecordStore::SqlBypass, @new_session.model assert_equal({ 'foo' => 'bar' }, @new_session.model.data) end end # End of safety net. rescue Object => e $stderr.puts "Skipping CGI::Session::ActiveRecordStore tests: #{e}" #$stderr.puts " #{e.backtrace.join("\n ")}" end end require File.dirname(__FILE__) + '/../active_record_unit' require 'fixtures/topic' require 'fixtures/reply' require 'fixtures/developer' require 'fixtures/project' class PaginationTest < ActiveRecordTestCase fixtures :topics, :replies, :developers, :projects, :developers_projects class PaginationController < ActionController::Base self.template_root = "#{File.dirname(__FILE__)}/../fixtures/" def simple_paginate @topic_pages, @topics = paginate(:topics) render :nothing => true end def paginate_with_per_page @topic_pages, @topics = paginate(:topics, :per_page => 1) render :nothing => true end def paginate_with_order @topic_pages, @topics = paginate(:topics, :order => 'created_at asc') render :nothing => true end def paginate_with_order_by @topic_pages, @topics = paginate(:topics, :order_by => 'created_at asc') render :nothing => true end def paginate_with_include_and_order @topic_pages, @topics = paginate(:topics, :include => :replies, :order => 'replies.created_at asc, topics.created_at asc') render :nothing => true end def paginate_with_conditions @topic_pages, @topics = paginate(:topics, :conditions => ["created_at > ?", 30.minutes.ago]) render :nothing => true end def paginate_with_class_name @developer_pages, @developers = paginate(:developers, :class_name => "DeVeLoPeR") render :nothing => true end def paginate_with_singular_name @developer_pages, @developers = paginate() render :nothing => true end def paginate_with_joins @developer_pages, @developers = paginate(:developers, :joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id', :conditions => 'project_id=1') render :nothing => true end def paginate_with_join @developer_pages, @developers = paginate(:developers, :join => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id', :conditions => 'project_id=1') render :nothing => true end def paginate_with_join_and_count @developer_pages, @developers = paginate(:developers, :join => 'd LEFT JOIN developers_projects ON d.id = developers_projects.developer_id', :conditions => 'project_id=1', :count => "d.id") render :nothing => true end def rescue_errors(e) raise e end def rescue_action(e) raise end end def setup @controller = PaginationController.new @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new super end # Single Action Pagination Tests def test_simple_paginate get :simple_paginate assert_equal 1, assigns(:topic_pages).page_count assert_equal 3, assigns(:topics).size end def test_paginate_with_per_page get :paginate_with_per_page assert_equal 1, assigns(:topics).size assert_equal 3, assigns(:topic_pages).page_count end def test_paginate_with_order get :paginate_with_order expected = [topics(:futurama), topics(:harvey_birdman), topics(:rails)] assert_equal expected, assigns(:topics) assert_equal 1, assigns(:topic_pages).page_count end def test_paginate_with_order_by get :paginate_with_order expected = assigns(:topics) get :paginate_with_order_by assert_equal expected, assigns(:topics) assert_equal 1, assigns(:topic_pages).page_count end def test_paginate_with_conditions get :paginate_with_conditions expected = [topics(:rails)] assert_equal expected, assigns(:topics) assert_equal 1, assigns(:topic_pages).page_count end def test_paginate_with_class_name get :paginate_with_class_name assert assigns(:developers).size > 0 assert_equal DeVeLoPeR, assigns(:developers).first.class end def test_paginate_with_joins get :paginate_with_joins assert_equal 2, assigns(:developers).size developer_names = assigns(:developers).map { |d| d.name } assert developer_names.include?('David') assert developer_names.include?('Jamis') end def test_paginate_with_join_and_conditions get :paginate_with_joins expected = assigns(:developers) get :paginate_with_join assert_equal expected, assigns(:developers) end def test_paginate_with_join_and_count get :paginate_with_joins expected = assigns(:developers) get :paginate_with_join_and_count assert_equal expected, assigns(:developers) end def test_paginate_with_include_and_order get :paginate_with_include_and_order expected = Topic.find(:all, :include => 'replies', :order => 'replies.created_at asc, topics.created_at asc', :limit => 10) assert_equal expected, assigns(:topics) end end require File.dirname(__FILE__) + '/../abstract_unit' # a controller class to facilitate the tests class ActionPackAssertionsController < ActionController::Base # this does absolutely nothing def nothing() render_text ""; end # a standard template def hello_world() render "test/hello_world"; end # a standard template def hello_xml_world() render "test/hello_xml_world"; end # a redirect to an internal location def redirect_internal() redirect_to "/nothing"; end def redirect_to_action() redirect_to :action => "flash_me", :id => 1, :params => { "panda" => "fun" }; end def redirect_to_controller() redirect_to :controller => "elsewhere", :action => "flash_me"; end def redirect_to_path() redirect_to '/some/path' end def redirect_to_named_route() redirect_to route_one_url end # a redirect to an external location def redirect_external() redirect_to_url "http://www.rubyonrails.org"; end # a 404 def response404() render_text "", "404 AWOL"; end # a 500 def response500() render_text "", "500 Sorry"; end # a fictional 599 def response599() render_text "", "599 Whoah!"; end # putting stuff in the flash def flash_me flash['hello'] = 'my name is inigo montoya...' render_text "Inconceivable!" end # we have a flash, but nothing is in it def flash_me_naked flash.clear render_text "wow!" end # assign some template instance variables def assign_this @howdy = "ho" render :inline => "Mr. Henke" end def render_based_on_parameters render_text "Mr. #{@params["name"]}" end def render_url render_text "
    #{url_for(:action => 'flash_me', :only_path => true)}
    " end # puts something in the session def session_stuffing session['xmas'] = 'turkey' render_text "ho ho ho" end # raises exception on get requests def raise_on_get raise "get" if @request.get? render_text "request method: #{@request.env['REQUEST_METHOD']}" end # raises exception on post requests def raise_on_post raise "post" if @request.post? render_text "request method: #{@request.env['REQUEST_METHOD']}" end def get_valid_record @record = Class.new do def valid? true end def errors Class.new do def full_messages; []; end end.new end end.new render :nothing => true end def get_invalid_record @record = Class.new do def valid? false end def errors Class.new do def full_messages; ['...stuff...']; end end.new end end.new render :nothing => true end # 911 def rescue_action(e) raise; end end module Admin class InnerModuleController < ActionController::Base def redirect_to_absolute_controller redirect_to :controller => '/content' end def redirect_to_fellow_controller redirect_to :controller => 'user' end end end # --------------------------------------------------------------------------- # tell the controller where to find its templates but start from parent # directory of test_request_response to simulate the behaviour of a # production environment ActionPackAssertionsController.template_root = File.dirname(__FILE__) + "/../fixtures/" # a test case to exercise the new capabilities TestRequest & TestResponse class ActionPackAssertionsControllerTest < Test::Unit::TestCase # let's get this party started def setup @controller = ActionPackAssertionsController.new @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new end # -- assertion-based testing ------------------------------------------------ def test_assert_tag_and_url_for get :render_url assert_tag :content => "/action_pack_assertions/flash_me" end # test the session assertion to make sure something is there. def test_assert_session_has process :session_stuffing assert_session_has 'xmas' assert_session_has_no 'halloween' end # test the get method, make sure the request really was a get def test_get assert_raise(RuntimeError) { get :raise_on_get } get :raise_on_post assert_equal @response.body, 'request method: GET' end # test the get method, make sure the request really was a get def test_post assert_raise(RuntimeError) { post :raise_on_post } post :raise_on_get assert_equal @response.body, 'request method: POST' end # the following test fails because the request_method is now cached on the request instance # test the get/post switch within one test action # def test_get_post_switch # post :raise_on_get # assert_equal @response.body, 'request method: POST' # get :raise_on_post # assert_equal @response.body, 'request method: GET' # post :raise_on_get # assert_equal @response.body, 'request method: POST' # get :raise_on_post # assert_equal @response.body, 'request method: GET' # end # test the assertion of goodies in the template def test_assert_template_has process :assign_this assert_template_has 'howdy' end # test the assertion for goodies that shouldn't exist in the template def test_assert_template_has_no process :nothing assert_template_has_no 'maple syrup' assert_template_has_no 'howdy' end # test the redirection assertions def test_assert_redirect process :redirect_internal assert_redirect end # test the redirect url string def test_assert_redirect_url process :redirect_external assert_redirect_url 'http://www.rubyonrails.org' end # test the redirection pattern matching on a string def test_assert_redirect_url_match_string process :redirect_external assert_redirect_url_match 'rails.org' end # test the redirection pattern matching on a pattern def test_assert_redirect_url_match_pattern process :redirect_external assert_redirect_url_match /ruby/ end # test the redirection to a named route def test_assert_redirect_to_named_route process :redirect_to_named_route assert_raise(Test::Unit::AssertionFailedError) do assert_redirected_to 'http://test.host/route_two' end end # test the flash-based assertions with something is in the flash def test_flash_assertions_full process :flash_me assert @response.has_flash_with_contents? assert_flash_exists assert_flash_not_empty assert_flash_has 'hello' assert_flash_has_no 'stds' end # test the flash-based assertions with no flash at all def test_flash_assertions_negative process :nothing assert_flash_empty assert_flash_has_no 'hello' assert_flash_has_no 'qwerty' end # test the assert_rendered_file def test_assert_rendered_file process :hello_world assert_rendered_file 'test/hello_world' assert_rendered_file 'hello_world' end # test the assert_success assertion def test_assert_success process :nothing assert_success assert_rendered_file end # -- standard request/response object testing -------------------------------- # ensure our session is working properly def test_session_objects process :session_stuffing assert @response.has_session_object?('xmas') assert_session_equal 'turkey', 'xmas' assert !@response.has_session_object?('easter') end # make sure that the template objects exist def test_template_objects_alive process :assign_this assert !@response.has_template_object?('hi') assert @response.has_template_object?('howdy') end # make sure we don't have template objects when we shouldn't def test_template_object_missing process :nothing assert_nil @response.template_objects['howdy'] end def test_assigned_equal process :assign_this assert_assigned_equal "ho", :howdy end # check the empty flashing def test_flash_me_naked process :flash_me_naked assert !@response.has_flash? assert !@response.has_flash_with_contents? end # check if we have flash objects def test_flash_haves process :flash_me assert @response.has_flash? assert @response.has_flash_with_contents? assert @response.has_flash_object?('hello') end # ensure we don't have flash objects def test_flash_have_nots process :nothing assert !@response.has_flash? assert !@response.has_flash_with_contents? assert_nil @response.flash['hello'] end # examine that the flash objects are what we expect def test_flash_equals process :flash_me assert_flash_equal 'my name is inigo montoya...', 'hello' end # check if we were rendered by a file-based template? def test_rendered_action process :nothing assert !@response.rendered_with_file? process :hello_world assert @response.rendered_with_file? assert 'hello_world', @response.rendered_file end # check the redirection location def test_redirection_location process :redirect_internal assert_equal 'http://test.host/nothing', @response.redirect_url process :redirect_external assert_equal 'http://www.rubyonrails.org', @response.redirect_url process :nothing assert_nil @response.redirect_url end # check server errors def test_server_error_response_code process :response500 assert @response.server_error? process :response599 assert @response.server_error? process :response404 assert !@response.server_error? end # check a 404 response code def test_missing_response_code process :response404 assert @response.missing? end # check to see if our redirection matches a pattern def test_redirect_url_match process :redirect_external assert @response.redirect? assert @response.redirect_url_match?("rubyonrails") assert @response.redirect_url_match?(/rubyonrails/) assert !@response.redirect_url_match?("phpoffrails") assert !@response.redirect_url_match?(/perloffrails/) end # check for a redirection def test_redirection process :redirect_internal assert @response.redirect? process :redirect_external assert @response.redirect? process :nothing assert !@response.redirect? end # check a successful response code def test_successful_response_code process :nothing assert @response.success? end # a basic check to make sure we have a TestResponse object def test_has_response process :nothing assert_kind_of ActionController::TestResponse, @response end def test_render_based_on_parameters process :render_based_on_parameters, "name" => "David" assert_equal "Mr. David", @response.body end def test_assert_template_xpath_match_no_matches process :hello_xml_world assert_raises Test::Unit::AssertionFailedError do assert_template_xpath_match('/no/such/node/in/document') end end def test_simple_one_element_xpath_match process :hello_xml_world assert_template_xpath_match('//title', "Hello World") end def test_array_of_elements_in_xpath_match process :hello_xml_world assert_template_xpath_match('//p', %w( abes monks wiseguys )) end def test_follow_redirect process :redirect_to_action assert_redirected_to :action => "flash_me" follow_redirect assert_equal 1, @request.parameters["id"].to_i assert "Inconceivable!", @response.body end def test_follow_redirect_outside_current_action process :redirect_to_controller assert_redirected_to :controller => "elsewhere", :action => "flash_me" assert_raises(RuntimeError, "Can't follow redirects outside of current controller (elsewhere)") { follow_redirect } end def test_redirected_to_url_leadling_slash process :redirect_to_path assert_redirected_to '/some/path' end def test_redirected_to_url_no_leadling_slash process :redirect_to_path assert_redirected_to 'some/path' end def test_redirected_to_url_full_url process :redirect_to_path assert_redirected_to 'http://test.host/some/path' end def test_redirected_to_with_nested_controller @controller = Admin::InnerModuleController.new get :redirect_to_absolute_controller assert_redirected_to :controller => 'content' get :redirect_to_fellow_controller assert_redirected_to :controller => 'admin/user' end def test_assert_valid get :get_valid_record assert_valid assigns('record') end def test_assert_valid_failing get :get_invalid_record begin assert_valid assigns('record') assert false rescue Test::Unit::AssertionFailedError => e end end end class ActionPackHeaderTest < Test::Unit::TestCase def setup @controller = ActionPackAssertionsController.new @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new end def test_rendering_xml_sets_content_type process :hello_xml_world assert_equal('application/xml', @controller.headers['Content-Type']) end def test_rendering_xml_respects_content_type @response.headers['Content-Type'] = 'application/pdf' process :hello_xml_world assert_equal('application/pdf', @controller.headers['Content-Type']) end end require File.dirname(__FILE__) + '/../abstract_unit' class Address def Address.count(conditions = nil, join = nil) nil end def Address.find_all(arg1, arg2, arg3, arg4) [] end def self.find(*args) [] end end class AddressesTestController < ActionController::Base scaffold :address def self.controller_name; "addresses"; end def self.controller_path; "addresses"; end end AddressesTestController.template_root = File.dirname(__FILE__) + "/../fixtures/" class AddressesTest < Test::Unit::TestCase def setup @controller = AddressesTestController.new # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get # a more accurate simulation of what happens in "real life". @controller.logger = Logger.new(nil) @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new @request.host = "www.nextangle.com" end def test_list get :list assert_equal "We only need to get this far!", @response.body.chomp end end require File.dirname(__FILE__) + '/../abstract_unit' require 'test/unit' require 'pp' # require 'pp' early to prevent hidden_methods from not picking up the pretty-print methods until too late # Provide some controller to run the tests on. module Submodule class ContainedEmptyController < ActionController::Base end class ContainedNonEmptyController < ActionController::Base def public_action end hide_action :hidden_action def hidden_action end def another_hidden_action end hide_action :another_hidden_action end class SubclassedController < ContainedNonEmptyController hide_action :public_action # Hiding it here should not affect the superclass. end end class EmptyController < ActionController::Base include ActionController::Caching end class NonEmptyController < ActionController::Base def public_action end hide_action :hidden_action def hidden_action end end class ControllerClassTests < Test::Unit::TestCase def test_controller_path assert_equal 'empty', EmptyController.controller_path assert_equal 'submodule/contained_empty', Submodule::ContainedEmptyController.controller_path end def test_controller_name assert_equal 'empty', EmptyController.controller_name assert_equal 'contained_empty', Submodule::ContainedEmptyController.controller_name end end class ControllerInstanceTests < Test::Unit::TestCase def setup @empty = EmptyController.new @contained = Submodule::ContainedEmptyController.new @empty_controllers = [@empty, @contained, Submodule::SubclassedController.new] @non_empty_controllers = [NonEmptyController.new, Submodule::ContainedNonEmptyController.new] end def test_action_methods @empty_controllers.each do |c| assert_equal Set.new, c.send(:action_methods), "#{c.class.controller_path} should be empty!" end @non_empty_controllers.each do |c| assert_equal Set.new('public_action'), c.send(:action_methods), "#{c.class.controller_path} should not be empty!" end end end require File.dirname(__FILE__) + '/../abstract_unit' require 'test/unit' # Provide some static controllers. class BenchmarkedController < ActionController::Base def public_action render :nothing => true end def rescue_action(e) raise e end end class BenchmarkTest < Test::Unit::TestCase class MockLogger def method_missing(*args) end end def setup @controller = BenchmarkedController.new # benchmark doesn't do anything unless a logger is set @controller.logger = MockLogger.new @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new @request.host = "test.actioncontroller.i" end def test_with_http_1_0_request @request.host = nil assert_nothing_raised { get :public_action } end end require 'fileutils' require File.dirname(__FILE__) + '/../abstract_unit' class TestLogDevice < Logger::LogDevice attr :last_message, true def initialize @last_message=String.new end def write(message) @last_message << message end def clear @last_message = String.new end end #setup our really sophisticated logger TestLog = TestLogDevice.new RAILS_DEFAULT_LOGGER = Logger.new(TestLog) ActionController::Base.logger = RAILS_DEFAULT_LOGGER def use_store #generate a random key to ensure the cache is always in a different location RANDOM_KEY = rand(99999999).to_s FILE_STORE_PATH = File.dirname(__FILE__) + '/../temp/' + RANDOM_KEY ActionController::Base.perform_caching = true ActionController::Base.fragment_cache_store = :file_store, FILE_STORE_PATH end class TestController < ActionController::Base caches_action :render_to_cache, :index def render_to_cache render_text "Render Cached" end alias :index :render_to_cache end class FileStoreTest < Test::Unit::TestCase def setup @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new @controller = TestController.new @request.host = "hostname.com" end def teardown FileUtils.rm_rf(FILE_STORE_PATH) end def test_render_cached assert_fragment_cached { get :render_to_cache } assert_fragment_hit { get :render_to_cache } end private def assert_fragment_cached yield assert(TestLog.last_message.include?("Cached fragment:"), "--ERROR-- FileStore write failed ----") assert(!TestLog.last_message.include?("Couldn't create cache directory:"), "--ERROR-- FileStore create directory failed ----") TestLog.clear end def assert_fragment_hit yield assert(TestLog.last_message.include?("Fragment read:"), "--ERROR-- Fragment not found in FileStore ----") assert(!TestLog.last_message.include?("Cached fragment:"), "--ERROR-- Did cache ----") TestLog.clear end endrequire File.dirname(__FILE__) + '/../abstract_unit' class CaptureController < ActionController::Base def self.controller_name; "test"; end def self.controller_path; "test"; end def content_for render :layout => "talk_from_action" end def erb_content_for render :layout => "talk_from_action" end def block_content_for render :layout => "talk_from_action" end def non_erb_block_content_for render :layout => "talk_from_action" end def rescue_action(e) raise end end CaptureController.template_root = File.dirname(__FILE__) + "/../fixtures/" class CaptureTest < Test::Unit::TestCase def setup @controller = CaptureController.new # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get # a more accurate simulation of what happens in "real life". @controller.logger = Logger.new(nil) @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new @request.host = "www.nextangle.com" end def test_simple_capture get :capturing assert_equal "Dreamy days", @response.body.strip end def test_content_for get :content_for assert_equal expected_content_for_output, @response.body end def test_erb_content_for get :content_for assert_equal expected_content_for_output, @response.body end def test_block_content_for get :block_content_for assert_equal expected_content_for_output, @response.body end def test_non_erb_block_content_for get :non_erb_block_content_for assert_equal expected_content_for_output, @response.body end def test_update_element_with_capture get :update_element_with_capture assert_equal( "" + "\n\n$('status').innerHTML = '\\n You bought something!\\n';", @response.body.strip ) end private def expected_content_for_output "Putting stuff in the title!\n\nGreat stuff!" end end require File.dirname(__FILE__) + '/../abstract_unit' require 'action_controller/cgi_process' require 'action_controller/cgi_ext/cgi_ext' require 'stringio' class CGITest < Test::Unit::TestCase def setup @query_string = "action=create_customer&full_name=David%20Heinemeier%20Hansson&customerId=1" @query_string_with_nil = "action=create_customer&full_name=" @query_string_with_array = "action=create_customer&selected[]=1&selected[]=2&selected[]=3" @query_string_with_amps = "action=create_customer&name=Don%27t+%26+Does" @query_string_with_multiple_of_same_name = "action=update_order&full_name=Lau%20Taarnskov&products=4&products=2&products=3" @query_string_with_many_equal = "action=create_customer&full_name=abc=def=ghi" @query_string_without_equal = "action" @query_string_with_many_ampersands = "&action=create_customer&&&full_name=David%20Heinemeier%20Hansson" end def test_query_string assert_equal( { "action" => "create_customer", "full_name" => "David Heinemeier Hansson", "customerId" => "1"}, CGIMethods.parse_query_parameters(@query_string) ) end def test_deep_query_string assert_equal({'x' => {'y' => {'z' => '10'}}}, CGIMethods.parse_query_parameters('x[y][z]=10')) end def test_deep_query_string_with_array assert_equal({'x' => {'y' => {'z' => ['10']}}}, CGIMethods.parse_query_parameters('x[y][z][]=10')) assert_equal({'x' => {'y' => {'z' => ['10', '5']}}}, CGIMethods.parse_query_parameters('x[y][z][]=10&x[y][z][]=5')) end def test_query_string_with_nil assert_equal( { "action" => "create_customer", "full_name" => nil}, CGIMethods.parse_query_parameters(@query_string_with_nil) ) end def test_query_string_with_array assert_equal( { "action" => "create_customer", "selected" => ["1", "2", "3"]}, CGIMethods.parse_query_parameters(@query_string_with_array) ) end def test_query_string_with_amps assert_equal( { "action" => "create_customer", "name" => "Don't & Does"}, CGIMethods.parse_query_parameters(@query_string_with_amps) ) end def test_query_string_with_many_equal assert_equal( { "action" => "create_customer", "full_name" => "abc=def=ghi"}, CGIMethods.parse_query_parameters(@query_string_with_many_equal) ) end def test_query_string_without_equal assert_equal( { "action" => nil }, CGIMethods.parse_query_parameters(@query_string_without_equal) ) end def test_query_string_with_many_ampersands assert_equal( { "action" => "create_customer", "full_name" => "David Heinemeier Hansson"}, CGIMethods.parse_query_parameters(@query_string_with_many_ampersands) ) end def test_parse_params input = { "customers[boston][first][name]" => [ "David" ], "customers[boston][first][url]" => [ "http://David" ], "customers[boston][second][name]" => [ "Allan" ], "customers[boston][second][url]" => [ "http://Allan" ], "something_else" => [ "blah" ], "something_nil" => [ nil ], "something_empty" => [ "" ], "products[first]" => [ "Apple Computer" ], "products[second]" => [ "Pc" ] } expected_output = { "customers" => { "boston" => { "first" => { "name" => "David", "url" => "http://David" }, "second" => { "name" => "Allan", "url" => "http://Allan" } } }, "something_else" => "blah", "something_empty" => "", "something_nil" => "", "products" => { "first" => "Apple Computer", "second" => "Pc" } } assert_equal expected_output, CGIMethods.parse_request_parameters(input) end def test_parse_params_from_multipart_upload mockup = Struct.new(:content_type, :original_filename) file = mockup.new('img/jpeg', 'foo.jpg') ie_file = mockup.new('img/jpeg', 'c:\\Documents and Settings\\foo\\Desktop\\bar.jpg') input = { "something" => [ StringIO.new("") ], "array_of_stringios" => [[ StringIO.new("One"), StringIO.new("Two") ]], "mixed_types_array" => [[ StringIO.new("Three"), "NotStringIO" ]], "mixed_types_as_checkboxes[strings][nested]" => [[ file, "String", StringIO.new("StringIO")]], "ie_mixed_types_as_checkboxes[strings][nested]" => [[ ie_file, "String", StringIO.new("StringIO")]], "products[string]" => [ StringIO.new("Apple Computer") ], "products[file]" => [ file ], "ie_products[string]" => [ StringIO.new("Microsoft") ], "ie_products[file]" => [ ie_file ] } expected_output = { "something" => "", "array_of_stringios" => ["One", "Two"], "mixed_types_array" => [ "Three", "NotStringIO" ], "mixed_types_as_checkboxes" => { "strings" => { "nested" => [ file, "String", "StringIO" ] }, }, "ie_mixed_types_as_checkboxes" => { "strings" => { "nested" => [ ie_file, "String", "StringIO" ] }, }, "products" => { "string" => "Apple Computer", "file" => file }, "ie_products" => { "string" => "Microsoft", "file" => ie_file } } params = CGIMethods.parse_request_parameters(input) assert_equal expected_output, params # Lone filenames are preserved. assert_equal 'foo.jpg', params['mixed_types_as_checkboxes']['strings']['nested'].first.original_filename assert_equal 'foo.jpg', params['products']['file'].original_filename # But full Windows paths are reduced to their basename. assert_equal 'bar.jpg', params['ie_mixed_types_as_checkboxes']['strings']['nested'].first.original_filename assert_equal 'bar.jpg', params['ie_products']['file'].original_filename end def test_parse_params_with_file input = { "customers[boston][first][name]" => [ "David" ], "something_else" => [ "blah" ], "logo" => [ File.new(File.dirname(__FILE__) + "/cgi_test.rb").path ] } expected_output = { "customers" => { "boston" => { "first" => { "name" => "David" } } }, "something_else" => "blah", "logo" => File.new(File.dirname(__FILE__) + "/cgi_test.rb").path, } assert_equal expected_output, CGIMethods.parse_request_parameters(input) end def test_parse_params_with_array input = { "selected[]" => [ "1", "2", "3" ] } expected_output = { "selected" => [ "1", "2", "3" ] } assert_equal expected_output, CGIMethods.parse_request_parameters(input) end def test_parse_params_with_non_alphanumeric_name input = { "a/b[c]" => %w(d) } expected = { "a/b" => { "c" => "d" }} assert_equal expected, CGIMethods.parse_request_parameters(input) end def test_parse_params_with_single_brackets_in_middle input = { "a/b[c]d" => %w(e) } expected = { "a/b[c]d" => "e" } assert_equal expected, CGIMethods.parse_request_parameters(input) end def test_parse_params_with_separated_brackets input = { "a/b@[c]d[e]" => %w(f) } expected = { "a/b@" => { "c]d[e" => "f" }} assert_equal expected, CGIMethods.parse_request_parameters(input) end def test_parse_params_with_separated_brackets_and_array input = { "a/b@[c]d[e][]" => %w(f) } expected = { "a/b@" => { "c]d[e" => ["f"] }} assert_equal expected , CGIMethods.parse_request_parameters(input) end def test_parse_params_with_unmatched_brackets_and_array input = { "a/b@[c][d[e][]" => %w(f) } expected = { "a/b@" => { "c" => { "d[e" => ["f"] }}} assert_equal expected, CGIMethods.parse_request_parameters(input) end end class MultipartCGITest < Test::Unit::TestCase FIXTURE_PATH = File.dirname(__FILE__) + '/../fixtures/multipart' def setup ENV['REQUEST_METHOD'] = 'POST' ENV['CONTENT_LENGTH'] = '0' ENV['CONTENT_TYPE'] = 'multipart/form-data, boundary=AaB03x' end def test_single_parameter params = process('single_parameter') assert_equal({ 'foo' => 'bar' }, params) end def test_text_file params = process('text_file') assert_equal %w(file foo), params.keys.sort assert_equal 'bar', params['foo'] file = params['file'] assert_kind_of StringIO, file assert_equal 'file.txt', file.original_filename assert_equal "text/plain\r", file.content_type assert_equal 'contents', file.read end def test_large_text_file params = process('large_text_file') assert_equal %w(file foo), params.keys.sort assert_equal 'bar', params['foo'] file = params['file'] assert_kind_of Tempfile, file assert_equal 'file.txt', file.original_filename assert_equal "text/plain\r", file.content_type assert ('a' * 20480) == file.read end def test_binary_file params = process('binary_file') assert_equal %w(file flowers foo), params.keys.sort assert_equal 'bar', params['foo'] file = params['file'] assert_kind_of StringIO, file assert_equal 'file.txt', file.original_filename assert_equal "text/plain\r", file.content_type assert_equal 'contents', file.read file = params['flowers'] assert_kind_of StringIO, file assert_equal 'flowers.jpg', file.original_filename assert_equal "image/jpeg\r", file.content_type assert_equal 19512, file.size #assert_equal File.read(File.dirname(__FILE__) + '/../../../activerecord/test/fixtures/flowers.jpg'), file.read end def test_mixed_files params = process('mixed_files') assert_equal %w(files foo), params.keys.sort assert_equal 'bar', params['foo'] # Ruby CGI doesn't handle multipart/mixed for us. assert_kind_of StringIO, params['files'] assert_equal 19756, params['files'].size end private def process(name) old_stdin = $stdin File.open(File.join(FIXTURE_PATH, name), 'rb') do |file| ENV['CONTENT_LENGTH'] = file.stat.size.to_s $stdin = file CGIMethods.parse_request_parameters CGI.new.params end ensure $stdin = old_stdin end end class CGIRequestTest < Test::Unit::TestCase def setup @request_hash = {"HTTP_MAX_FORWARDS"=>"10", "SERVER_NAME"=>"glu.ttono.us:8007", "FCGI_ROLE"=>"RESPONDER", "HTTP_X_FORWARDED_HOST"=>"glu.ttono.us", "HTTP_ACCEPT_ENCODING"=>"gzip, deflate", "HTTP_USER_AGENT"=>"Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/312.5.1 (KHTML, like Gecko) Safari/312.3.1", "PATH_INFO"=>"", "HTTP_ACCEPT_LANGUAGE"=>"en", "HTTP_HOST"=>"glu.ttono.us:8007", "SERVER_PROTOCOL"=>"HTTP/1.1", "REDIRECT_URI"=>"/dispatch.fcgi", "SCRIPT_NAME"=>"/dispatch.fcgi", "SERVER_ADDR"=>"207.7.108.53", "REMOTE_ADDR"=>"207.7.108.53", "SERVER_SOFTWARE"=>"lighttpd/1.4.5", "HTTP_COOKIE"=>"_session_id=c84ace84796670c052c6ceb2451fb0f2; is_admin=yes", "HTTP_X_FORWARDED_SERVER"=>"glu.ttono.us", "REQUEST_URI"=>"/admin", "DOCUMENT_ROOT"=>"/home/kevinc/sites/typo/public", "SERVER_PORT"=>"8007", "QUERY_STRING"=>"", "REMOTE_PORT"=>"63137", "GATEWAY_INTERFACE"=>"CGI/1.1", "HTTP_X_FORWARDED_FOR"=>"65.88.180.234", "HTTP_ACCEPT"=>"*/*", "SCRIPT_FILENAME"=>"/home/kevinc/sites/typo/public/dispatch.fcgi", "REDIRECT_STATUS"=>"200", "REQUEST_METHOD"=>"GET"} # cookie as returned by some Nokia phone browsers (no space after semicolon separator) @alt_cookie_fmt_request_hash = {"HTTP_COOKIE"=>"_session_id=c84ace84796670c052c6ceb2451fb0f2;is_admin=yes"} @fake_cgi = Struct.new(:env_table).new(@request_hash) @request = ActionController::CgiRequest.new(@fake_cgi) end def test_proxy_request assert_equal 'glu.ttono.us', @request.host_with_port end def test_http_host @request_hash.delete "HTTP_X_FORWARDED_HOST" @request_hash['HTTP_HOST'] = "rubyonrails.org:8080" assert_equal "rubyonrails.org:8080", @request.host_with_port @request_hash['HTTP_X_FORWARDED_HOST'] = "www.firsthost.org, www.secondhost.org" assert_equal "www.secondhost.org", @request.host end def test_http_host_with_default_port_overrides_server_port @request_hash.delete "HTTP_X_FORWARDED_HOST" @request_hash['HTTP_HOST'] = "rubyonrails.org" assert_equal "rubyonrails.org", @request.host_with_port end def test_host_with_port_defaults_to_server_name_if_no_host_headers @request_hash.delete "HTTP_X_FORWARDED_HOST" @request_hash.delete "HTTP_HOST" assert_equal "glu.ttono.us:8007", @request.host_with_port end def test_host_with_port_falls_back_to_server_addr_if_necessary @request_hash.delete "HTTP_X_FORWARDED_HOST" @request_hash.delete "HTTP_HOST" @request_hash.delete "SERVER_NAME" assert_equal "207.7.108.53:8007", @request.host_with_port end def test_cookie_syntax_resilience cookies = CGI::Cookie::parse(@request_hash["HTTP_COOKIE"]); assert_equal ["c84ace84796670c052c6ceb2451fb0f2"], cookies["_session_id"] assert_equal ["yes"], cookies["is_admin"] alt_cookies = CGI::Cookie::parse(@alt_cookie_fmt_request_hash["HTTP_COOKIE"]); assert_equal ["c84ace84796670c052c6ceb2451fb0f2"], alt_cookies["_session_id"] assert_equal ["yes"], alt_cookies["is_admin"] end end require File.dirname(__FILE__) + '/../abstract_unit' class CallerController < ActionController::Base def calling_from_controller render_component(:controller => "callee", :action => "being_called") end def calling_from_controller_with_params render_component(:controller => "callee", :action => "being_called", :params => { "name" => "David" }) end def calling_from_controller_with_different_status_code render_component(:controller => "callee", :action => "blowing_up") end def calling_from_template render_template "Ring, ring: <%= render_component(:controller => 'callee', :action => 'being_called') %>" end def internal_caller render_template "Are you there? <%= render_component(:action => 'internal_callee') %>" end def internal_callee render_text "Yes, ma'am" end def set_flash render_component(:controller => "callee", :action => "set_flash") end def use_flash render_component(:controller => "callee", :action => "use_flash") end def calling_redirected render_component(:controller => "callee", :action => "redirected") end def calling_redirected_as_string render_template "<%= render_component(:controller => 'callee', :action => 'redirected') %>" end def rescue_action(e) raise end end class CalleeController < ActionController::Base def being_called render_text "#{@params["name"] || "Lady"} of the House, speaking" end def blowing_up render_text "It's game over, man, just game over, man!", "500 Internal Server Error" end def set_flash flash[:notice] = 'My stoney baby' render :text => 'flash is set' end def use_flash render :text => flash[:notice] || 'no flash' end def redirected redirect_to :controller => "callee", :action => "being_called" end def rescue_action(e) raise end end class ComponentsTest < Test::Unit::TestCase def setup @controller = CallerController.new @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new end def test_calling_from_controller get :calling_from_controller assert_equal "Lady of the House, speaking", @response.body end def test_calling_from_controller_with_params get :calling_from_controller_with_params assert_equal "David of the House, speaking", @response.body end def test_calling_from_controller_with_different_status_code get :calling_from_controller_with_different_status_code assert_equal 500, @response.response_code end def test_calling_from_template get :calling_from_template assert_equal "Ring, ring: Lady of the House, speaking", @response.body end def test_internal_calling get :internal_caller assert_equal "Are you there? Yes, ma'am", @response.body end def test_flash get :set_flash assert_equal 'My stoney baby', flash[:notice] get :use_flash assert_equal 'My stoney baby', @response.body get :use_flash assert_equal 'no flash', @response.body end def test_component_redirect_redirects get :calling_redirected assert_redirected_to :action => "being_called" end def test_component_multiple_redirect_redirects test_component_redirect_redirects test_internal_calling end def test_component_as_string_redirect_renders_redirecte_action get :calling_redirected_as_string assert_equal "Lady of the House, speaking", @response.body end endrequire File.dirname(__FILE__) + '/../abstract_unit' class CookieTest < Test::Unit::TestCase class TestController < ActionController::Base def authenticate_with_deprecated_writer cookie "name" => "user_name", "value" => "david" render_text "hello world" end def authenticate cookies["user_name"] = "david" render_text "hello world" end def authenticate_for_fourten_days cookies["user_name"] = { "value" => "david", "expires" => Time.local(2005, 10, 10) } render_text "hello world" end def authenticate_for_fourten_days_with_symbols cookies[:user_name] = { :value => "david", :expires => Time.local(2005, 10, 10) } render_text "hello world" end def set_multiple_cookies cookies["user_name"] = { "value" => "david", "expires" => Time.local(2005, 10, 10) } cookies["login"] = "XJ-122" render_text "hello world" end def access_frozen_cookies @cookies["will"] = "work" render_text "hello world" end def rescue_action(e) raise end end def setup @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new @request.host = "www.nextangle.com" end def test_setting_cookie_with_deprecated_writer @request.action = "authenticate_with_deprecated_writer" assert_equal [ CGI::Cookie::new("name" => "user_name", "value" => "david") ], process_request.headers["cookie"] end def test_setting_cookie @request.action = "authenticate" assert_equal [ CGI::Cookie::new("name" => "user_name", "value" => "david") ], process_request.headers["cookie"] end def test_setting_cookie_for_fourteen_days @request.action = "authenticate_for_fourten_days" assert_equal [ CGI::Cookie::new("name" => "user_name", "value" => "david", "expires" => Time.local(2005, 10, 10)) ], process_request.headers["cookie"] end def test_setting_cookie_for_fourteen_days_with_symbols @request.action = "authenticate_for_fourten_days" assert_equal [ CGI::Cookie::new("name" => "user_name", "value" => "david", "expires" => Time.local(2005, 10, 10)) ], process_request.headers["cookie"] end def test_multiple_cookies @request.action = "set_multiple_cookies" assert_equal 2, process_request.headers["cookie"].size end def test_setting_test_cookie @request.action = "access_frozen_cookies" assert_nothing_raised { process_request } end private def process_request TestController.process(@request, @response) end end require File.dirname(__FILE__) + '/../abstract_unit' class CustomHandler def initialize( view ) @view = view end def render( template, local_assigns ) [ template, local_assigns, @view ] end end class CustomHandlerTest < Test::Unit::TestCase def setup ActionView::Base.register_template_handler "foo", CustomHandler ActionView::Base.register_template_handler :foo2, CustomHandler @view = ActionView::Base.new end def test_custom_render result = @view.render_template( "foo", "hello <%= one %>", nil, :one => "two" ) assert_equal( [ "hello <%= one %>", { :one => "two" }, @view ], result ) end def test_custom_render2 result = @view.render_template( "foo2", "hello <%= one %>", nil, :one => "two" ) assert_equal( [ "hello <%= one %>", { :one => "two" }, @view ], result ) end def test_unhandled_extension # uses the ERb handler by default if the extension isn't recognized result = @view.render_template( "bar", "hello <%= one %>", nil, :one => "two" ) assert_equal "hello two", result end end class << Object; alias_method :const_available?, :const_defined?; end class ContentController < Class.new(ActionController::Base) end class NotAController end module Admin class << self; alias_method :const_available?, :const_defined?; end SomeConstant = 10 class UserController < Class.new(ActionController::Base); end class NewsFeedController < Class.new(ActionController::Base); end end ActionController::Routing::Routes.draw do |map| map.route_one 'route_one', :controller => 'elsewhere', :action => 'flash_me' map.connect ':controller/:action/:id' end require File.dirname(__FILE__) + '/../abstract_unit' class FilterParamController < ActionController::Base end class FilterParamTest < Test::Unit::TestCase def setup @controller = FilterParamController.new end def test_filter_parameters assert FilterParamController.respond_to?(:filter_parameter_logging) assert !@controller.respond_to?(:filter_parameters) FilterParamController.filter_parameter_logging assert @controller.respond_to?(:filter_parameters) test_hashes = [[{},{},[]], [{'foo'=>'bar'},{'foo'=>'bar'},[]], [{'foo'=>'bar'},{'foo'=>'bar'},%w'food'], [{'foo'=>'bar'},{'foo'=>'[FILTERED]'},%w'foo'], [{'foo'=>'bar', 'bar'=>'foo'},{'foo'=>'[FILTERED]', 'bar'=>'foo'},%w'foo baz'], [{'foo'=>'bar', 'baz'=>'foo'},{'foo'=>'[FILTERED]', 'baz'=>'[FILTERED]'},%w'foo baz'], [{'bar'=>{'foo'=>'bar','bar'=>'foo'}},{'bar'=>{'foo'=>'[FILTERED]','bar'=>'foo'}},%w'fo'], [{'foo'=>{'foo'=>'bar','bar'=>'foo'}},{'foo'=>'[FILTERED]'},%w'f banana']] test_hashes.each do |before_filter, after_filter, filter_words| FilterParamController.filter_parameter_logging(*filter_words) assert_equal after_filter, @controller.filter_parameters(before_filter) filter_words.push('blah') FilterParamController.filter_parameter_logging(*filter_words) do |key, value| value.reverse! if key =~ /bargain/ end before_filter['barg'] = {'bargain'=>'gain', 'blah'=>'bar', 'bar'=>{'bargain'=>{'blah'=>'foo'}}} after_filter['barg'] = {'bargain'=>'niag', 'blah'=>'[FILTERED]', 'bar'=>{'bargain'=>{'blah'=>'[FILTERED]'}}} assert_equal after_filter, @controller.filter_parameters(before_filter) end end end require File.dirname(__FILE__) + '/../abstract_unit' class FilterTest < Test::Unit::TestCase class TestController < ActionController::Base before_filter :ensure_login after_filter :clean_up def show render :inline => "ran action" end private def ensure_login @ran_filter ||= [] @ran_filter << "ensure_login" end def clean_up @ran_after_filter ||= [] @ran_after_filter << "clean_up" end end class RenderingController < ActionController::Base before_filter :render_something_else def show @ran_action = true render :inline => "ran action" end private def render_something_else render :inline => "something else" end end class ConditionalFilterController < ActionController::Base def show render :inline => "ran action" end def another_action render :inline => "ran action" end def show_without_filter render :inline => "ran action without filter" end private def ensure_login @ran_filter ||= [] @ran_filter << "ensure_login" end def clean_up_tmp @ran_filter ||= [] @ran_filter << "clean_up_tmp" end def rescue_action(e) raise(e) end end class ConditionalCollectionFilterController < ConditionalFilterController before_filter :ensure_login, :except => [ :show_without_filter, :another_action ] end class OnlyConditionSymController < ConditionalFilterController before_filter :ensure_login, :only => :show end class ExceptConditionSymController < ConditionalFilterController before_filter :ensure_login, :except => :show_without_filter end class BeforeAndAfterConditionController < ConditionalFilterController before_filter :ensure_login, :only => :show after_filter :clean_up_tmp, :only => :show end class OnlyConditionProcController < ConditionalFilterController before_filter(:only => :show) {|c| c.assigns["ran_proc_filter"] = true } end class ExceptConditionProcController < ConditionalFilterController before_filter(:except => :show_without_filter) {|c| c.assigns["ran_proc_filter"] = true } end class ConditionalClassFilter def self.filter(controller) controller.assigns["ran_class_filter"] = true end end class OnlyConditionClassController < ConditionalFilterController before_filter ConditionalClassFilter, :only => :show end class ExceptConditionClassController < ConditionalFilterController before_filter ConditionalClassFilter, :except => :show_without_filter end class AnomolousYetValidConditionController < ConditionalFilterController before_filter(ConditionalClassFilter, :ensure_login, Proc.new {|c| c.assigns["ran_proc_filter1"] = true }, :except => :show_without_filter) { |c| c.assigns["ran_proc_filter2"] = true} end class PrependingController < TestController prepend_before_filter :wonderful_life # skip_before_filter :fire_flash private def wonderful_life @ran_filter ||= [] @ran_filter << "wonderful_life" end end class ConditionalSkippingController < TestController skip_before_filter :ensure_login, :only => [ :login ] skip_after_filter :clean_up, :only => [ :login ] before_filter :find_user, :only => [ :change_password ] def login render :inline => "ran action" end def change_password render :inline => "ran action" end protected def find_user @ran_filter ||= [] @ran_filter << "find_user" end end class ConditionalParentOfConditionalSkippingController < ConditionalFilterController before_filter :conditional_in_parent, :only => [:show, :another_action] after_filter :conditional_in_parent, :only => [:show, :another_action] private def conditional_in_parent @ran_filter ||= [] @ran_filter << 'conditional_in_parent' end end class ChildOfConditionalParentController < ConditionalParentOfConditionalSkippingController skip_before_filter :conditional_in_parent, :only => :another_action skip_after_filter :conditional_in_parent, :only => :another_action end class ProcController < PrependingController before_filter(proc { |c| c.assigns["ran_proc_filter"] = true }) end class ImplicitProcController < PrependingController before_filter { |c| c.assigns["ran_proc_filter"] = true } end class AuditFilter def self.filter(controller) controller.assigns["was_audited"] = true end end class AroundFilter def before(controller) @execution_log = "before" controller.class.execution_log << " before aroundfilter " if controller.respond_to? :execution_log controller.assigns["before_ran"] = true end def after(controller) controller.assigns["execution_log"] = @execution_log + " and after" controller.assigns["after_ran"] = true controller.class.execution_log << " after aroundfilter " if controller.respond_to? :execution_log end end class AppendedAroundFilter def before(controller) controller.class.execution_log << " before appended aroundfilter " end def after(controller) controller.class.execution_log << " after appended aroundfilter " end end class AuditController < ActionController::Base before_filter(AuditFilter) def show render_text "hello" end end class BadFilterController < ActionController::Base before_filter 2 def show() "show" end protected def rescue_action(e) raise(e) end end class AroundFilterController < PrependingController around_filter AroundFilter.new end class MixedFilterController < PrependingController cattr_accessor :execution_log def initialize @@execution_log = "" end before_filter { |c| c.class.execution_log << " before procfilter " } prepend_around_filter AroundFilter.new after_filter { |c| c.class.execution_log << " after procfilter " } append_around_filter AppendedAroundFilter.new end class MixedSpecializationController < ActionController::Base class OutOfOrder < StandardError; end before_filter :first before_filter :second, :only => :foo def foo render_text 'foo' end def bar render_text 'bar' end protected def first @first = true end def second raise OutOfOrder unless @first end end class DynamicDispatchController < ActionController::Base before_filter :choose %w(foo bar baz).each do |action| define_method(action) { render :text => action } end private def choose self.action_name = params[:choose] end end def test_added_filter_to_inheritance_graph assert_equal [ :ensure_login ], TestController.before_filters end def test_base_class_in_isolation assert_equal [ ], ActionController::Base.before_filters end def test_prepending_filter assert_equal [ :wonderful_life, :ensure_login ], PrependingController.before_filters end def test_running_filters assert_equal %w( wonderful_life ensure_login ), test_process(PrependingController).template.assigns["ran_filter"] end def test_running_filters_with_proc assert test_process(ProcController).template.assigns["ran_proc_filter"] end def test_running_filters_with_implicit_proc assert test_process(ImplicitProcController).template.assigns["ran_proc_filter"] end def test_running_filters_with_class assert test_process(AuditController).template.assigns["was_audited"] end def test_running_anomolous_yet_valid_condition_filters response = test_process(AnomolousYetValidConditionController) assert_equal %w( ensure_login ), response.template.assigns["ran_filter"] assert response.template.assigns["ran_class_filter"] assert response.template.assigns["ran_proc_filter1"] assert response.template.assigns["ran_proc_filter2"] response = test_process(AnomolousYetValidConditionController, "show_without_filter") assert_equal nil, response.template.assigns["ran_filter"] assert !response.template.assigns["ran_class_filter"] assert !response.template.assigns["ran_proc_filter1"] assert !response.template.assigns["ran_proc_filter2"] end def test_running_collection_condition_filters assert_equal %w( ensure_login ), test_process(ConditionalCollectionFilterController).template.assigns["ran_filter"] assert_equal nil, test_process(ConditionalCollectionFilterController, "show_without_filter").template.assigns["ran_filter"] assert_equal nil, test_process(ConditionalCollectionFilterController, "another_action").template.assigns["ran_filter"] end def test_running_only_condition_filters assert_equal %w( ensure_login ), test_process(OnlyConditionSymController).template.assigns["ran_filter"] assert_equal nil, test_process(OnlyConditionSymController, "show_without_filter").template.assigns["ran_filter"] assert test_process(OnlyConditionProcController).template.assigns["ran_proc_filter"] assert !test_process(OnlyConditionProcController, "show_without_filter").template.assigns["ran_proc_filter"] assert test_process(OnlyConditionClassController).template.assigns["ran_class_filter"] assert !test_process(OnlyConditionClassController, "show_without_filter").template.assigns["ran_class_filter"] end def test_running_except_condition_filters assert_equal %w( ensure_login ), test_process(ExceptConditionSymController).template.assigns["ran_filter"] assert_equal nil, test_process(ExceptConditionSymController, "show_without_filter").template.assigns["ran_filter"] assert test_process(ExceptConditionProcController).template.assigns["ran_proc_filter"] assert !test_process(ExceptConditionProcController, "show_without_filter").template.assigns["ran_proc_filter"] assert test_process(ExceptConditionClassController).template.assigns["ran_class_filter"] assert !test_process(ExceptConditionClassController, "show_without_filter").template.assigns["ran_class_filter"] end def test_running_before_and_after_condition_filters assert_equal %w( ensure_login clean_up_tmp), test_process(BeforeAndAfterConditionController).template.assigns["ran_filter"] assert_equal nil, test_process(BeforeAndAfterConditionController, "show_without_filter").template.assigns["ran_filter"] end def test_bad_filter assert_raises(ActionController::ActionControllerError) { test_process(BadFilterController) } end def test_around_filter controller = test_process(AroundFilterController) assert controller.template.assigns["before_ran"] assert controller.template.assigns["after_ran"] end def test_having_properties_in_around_filter controller = test_process(AroundFilterController) assert_equal "before and after", controller.template.assigns["execution_log"] end def test_prepending_and_appending_around_filter controller = test_process(MixedFilterController) assert_equal " before aroundfilter before procfilter before appended aroundfilter " + " after appended aroundfilter after aroundfilter after procfilter ", MixedFilterController.execution_log end def test_rendering_breaks_filtering_chain response = test_process(RenderingController) assert_equal "something else", response.body assert !response.template.assigns["ran_action"] end def test_filters_with_mixed_specialization_run_in_order assert_nothing_raised do response = test_process(MixedSpecializationController, 'bar') assert_equal 'bar', response.body end assert_nothing_raised do response = test_process(MixedSpecializationController, 'foo') assert_equal 'foo', response.body end end def test_dynamic_dispatch %w(foo bar baz).each do |action| request = ActionController::TestRequest.new request.query_parameters[:choose] = action response = DynamicDispatchController.process(request, ActionController::TestResponse.new) assert_equal action, response.body end end def test_conditional_skipping_of_filters assert_nil test_process(ConditionalSkippingController, "login").template.assigns["ran_filter"] assert_equal %w( ensure_login find_user ), test_process(ConditionalSkippingController, "change_password").template.assigns["ran_filter"] assert_nil test_process(ConditionalSkippingController, "login").template.controller.instance_variable_get("@ran_after_filter") assert_equal %w( clean_up ), test_process(ConditionalSkippingController, "change_password").template.controller.instance_variable_get("@ran_after_filter") end def test_conditional_skipping_of_filters_when_parent_filter_is_also_conditional assert_equal %w( conditional_in_parent conditional_in_parent ), test_process(ChildOfConditionalParentController).template.assigns['ran_filter'] assert_nil test_process(ChildOfConditionalParentController, 'another_action').template.assigns['ran_filter'] end private def test_process(controller, action = "show") request = ActionController::TestRequest.new request.action = action controller.process(request, ActionController::TestResponse.new) end end require File.dirname(__FILE__) + '/../abstract_unit' class FlashTest < Test::Unit::TestCase class TestController < ActionController::Base def set_flash flash["that"] = "hello" render :inline => "hello" end def set_flash_now flash.now["that"] = "hello" flash.now["foo"] ||= "bar" flash.now["foo"] ||= "err" @flashy = flash.now["that"] @flash_copy = {}.update flash render :inline => "hello" end def attempt_to_use_flash_now @flash_copy = {}.update flash @flashy = flash["that"] render :inline => "hello" end def use_flash @flash_copy = {}.update flash @flashy = flash["that"] render :inline => "hello" end def use_flash_and_keep_it @flash_copy = {}.update flash @flashy = flash["that"] silence_warnings { keep_flash } render :inline => "hello" end def rescue_action(e) raise unless ActionController::MissingTemplate === e end end def setup @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new @controller = TestController.new end def test_flash get :set_flash get :use_flash assert_equal "hello", @response.template.assigns["flash_copy"]["that"] assert_equal "hello", @response.template.assigns["flashy"] get :use_flash assert_nil @response.template.assigns["flash_copy"]["that"], "On second flash" end def test_keep_flash get :set_flash get :use_flash_and_keep_it assert_equal "hello", @response.template.assigns["flash_copy"]["that"] assert_equal "hello", @response.template.assigns["flashy"] get :use_flash assert_equal "hello", @response.template.assigns["flash_copy"]["that"], "On second flash" get :use_flash assert_nil @response.template.assigns["flash_copy"]["that"], "On third flash" end def test_flash_now get :set_flash_now assert_equal "hello", @response.template.assigns["flash_copy"]["that"] assert_equal "bar" , @response.template.assigns["flash_copy"]["foo"] assert_equal "hello", @response.template.assigns["flashy"] get :attempt_to_use_flash_now assert_nil @response.template.assigns["flash_copy"]["that"] assert_nil @response.template.assigns["flash_copy"]["foo"] assert_nil @response.template.assigns["flashy"] end endrequire File.dirname(__FILE__) + '/../abstract_unit' MemCache = Struct.new(:MemCache, :address) unless Object.const_defined?(:MemCache) class FragmentCacheStoreSettingTest < Test::Unit::TestCase def teardown ActionController::Base.fragment_cache_store = ActionController::Caching::Fragments::MemoryStore.new end def test_file_fragment_cache_store ActionController::Base.fragment_cache_store = :file_store, "/path/to/cache/directory" assert_kind_of( ActionController::Caching::Fragments::FileStore, ActionController::Base.fragment_cache_store ) assert_equal "/path/to/cache/directory", ActionController::Base.fragment_cache_store.cache_path end def test_drb_fragment_cache_store ActionController::Base.fragment_cache_store = :drb_store, "druby://localhost:9192" assert_kind_of( ActionController::Caching::Fragments::DRbStore, ActionController::Base.fragment_cache_store ) assert_equal "druby://localhost:9192", ActionController::Base.fragment_cache_store.address end def test_mem_cache_fragment_cache_store ActionController::Base.fragment_cache_store = :mem_cache_store, "localhost" assert_kind_of( ActionController::Caching::Fragments::MemCacheStore, ActionController::Base.fragment_cache_store ) assert_equal %w(localhost), ActionController::Base.fragment_cache_store.addresses end def test_object_assigned_fragment_cache_store ActionController::Base.fragment_cache_store = ActionController::Caching::Fragments::FileStore.new("/path/to/cache/directory") assert_kind_of( ActionController::Caching::Fragments::FileStore, ActionController::Base.fragment_cache_store ) assert_equal "/path/to/cache/directory", ActionController::Base.fragment_cache_store.cache_path end end require File.dirname(__FILE__) + '/../abstract_unit' class TestController < ActionController::Base attr_accessor :delegate_attr def delegate_method() end def rescue_action(e) raise end end module Fun class GamesController < ActionController::Base def render_hello_world render :inline => "hello: <%= stratego %>" end def rescue_action(e) raise end end class PDFController < ActionController::Base def test render :inline => "test: <%= foobar %>" end def rescue_action(e) raise end end end module LocalAbcHelper def a() end def b() end def c() end end class HelperTest < Test::Unit::TestCase def setup # Increment symbol counter. @symbol = (@@counter ||= 'A0').succ!.dup # Generate new controller class. controller_class_name = "Helper#{@symbol}Controller" eval("class #{controller_class_name} < TestController; end") @controller_class = self.class.const_get(controller_class_name) # Generate new template class and assign to controller. template_class_name = "Test#{@symbol}View" eval("class #{template_class_name} < ActionView::Base; end") @template_class = self.class.const_get(template_class_name) @controller_class.template_class = @template_class # Set default test helper. self.test_helper = LocalAbcHelper end def teardown # Reset template class. #ActionController::Base.template_class = ActionView::Base end def test_deprecated_helper assert_equal expected_helper_methods, missing_methods assert_nothing_raised { @controller_class.helper TestHelper } assert_equal [], missing_methods end def test_declare_helper require 'abc_helper' self.test_helper = AbcHelper assert_equal expected_helper_methods, missing_methods assert_nothing_raised { @controller_class.helper :abc } assert_equal [], missing_methods end def test_declare_missing_helper assert_equal expected_helper_methods, missing_methods assert_raise(MissingSourceFile) { @controller_class.helper :missing } end def test_declare_missing_file_from_helper require 'broken_helper' rescue LoadError => e assert_nil /\bbroken_helper\b/.match(e.to_s)[1] end def test_helper_block assert_nothing_raised { @controller_class.helper { def block_helper_method; end } } assert master_helper_methods.include?('block_helper_method') end def test_helper_block_include assert_equal expected_helper_methods, missing_methods assert_nothing_raised { @controller_class.helper { include TestHelper } } assert [], missing_methods end def test_helper_method assert_nothing_raised { @controller_class.helper_method :delegate_method } assert master_helper_methods.include?('delegate_method') end def test_helper_attr assert_nothing_raised { @controller_class.helper_attr :delegate_attr } assert master_helper_methods.include?('delegate_attr') assert master_helper_methods.include?('delegate_attr=') end def test_helper_for_nested_controller request = ActionController::TestRequest.new response = ActionController::TestResponse.new request.action = 'render_hello_world' assert_equal 'hello: Iz guuut!', Fun::GamesController.process(request, response).body end def test_helper_for_acronym_controller request = ActionController::TestRequest.new response = ActionController::TestResponse.new request.action = 'test' assert_equal 'test: baz', Fun::PDFController.process(request, response).body end private def expected_helper_methods TestHelper.instance_methods end def master_helper_methods @controller_class.master_helper_module.instance_methods end def missing_methods expected_helper_methods - master_helper_methods end def test_helper=(helper_module) silence_warnings { self.class.const_set('TestHelper', helper_module) } end end class IsolatedHelpersTest < Test::Unit::TestCase class A < ActionController::Base def index render :inline => '<%= shout %>' end def rescue_action(e) raise end end class B < A helper { def shout; 'B' end } def index render :inline => '<%= shout %>' end end class C < A helper { def shout; 'C' end } def index render :inline => '<%= shout %>' end end def setup @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new @request.action = 'index' end def test_helper_in_a assert_raise(NameError) { A.process(@request, @response) } end def test_helper_in_b assert_equal 'B', B.process(@request, @response).body end def test_helper_in_c assert_equal 'C', C.process(@request, @response).body end end require File.dirname(__FILE__) + '/../abstract_unit' # The template_root must be set on Base and not LayoutTest so that LayoutTest's inherited method has access to # the template_root when looking for a layout ActionController::Base.template_root = File.dirname(__FILE__) + '/../fixtures/layout_tests/' class LayoutTest < ActionController::Base def self.controller_path; 'views' end end # Restore template root to be unset ActionController::Base.template_root = nil class ProductController < LayoutTest end class ItemController < LayoutTest end class ThirdPartyTemplateLibraryController < LayoutTest end module ControllerNameSpace end class ControllerNameSpace::NestedController < LayoutTest end class MabView def initialize(view) end def render(text, locals = {}) text end end ActionView::Base::register_template_handler :mab, MabView class LayoutAutoDiscoveryTest < Test::Unit::TestCase def setup @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new @request.host = "www.nextangle.com" end def test_application_layout_is_default_when_no_controller_match @controller = ProductController.new get :hello assert_equal 'layout_test.rhtml hello.rhtml', @response.body end def test_controller_name_layout_name_match @controller = ItemController.new get :hello assert_equal 'item.rhtml hello.rhtml', @response.body end def test_third_party_template_library_auto_discovers_layout @controller = ThirdPartyTemplateLibraryController.new get :hello assert_equal 'layouts/third_party_template_library', @controller.active_layout assert_equal 'Mab', @response.body end def test_namespaced_controllers_auto_detect_layouts @controller = ControllerNameSpace::NestedController.new get :hello assert_equal 'layouts/controller_name_space/nested', @controller.active_layout assert_equal 'controller_name_space/nested.rhtml hello.rhtml', @response.body end endrequire File.dirname(__FILE__) + '/../abstract_unit' class RespondToController < ActionController::Base layout :set_layout def html_xml_or_rss respond_to do |type| type.html { render :text => "HTML" } type.xml { render :text => "XML" } type.rss { render :text => "RSS" } type.all { render :text => "Nothing" } end end def js_or_html respond_to do |type| type.html { render :text => "HTML" } type.js { render :text => "JS" } type.all { render :text => "Nothing" } end end def html_or_xml respond_to do |type| type.html { render :text => "HTML" } type.xml { render :text => "XML" } type.all { render :text => "Nothing" } end end def just_xml respond_to do |type| type.xml { render :text => "XML" } end end def using_defaults respond_to do |type| type.html type.js type.xml end end def using_defaults_with_type_list respond_to(:html, :js, :xml) end def made_for_content_type respond_to do |type| type.rss { render :text => "RSS" } type.atom { render :text => "ATOM" } type.all { render :text => "Nothing" } end end def custom_type_handling respond_to do |type| type.html { render :text => "HTML" } type.custom("application/crazy-xml") { render :text => "Crazy XML" } type.all { render :text => "Nothing" } end end def handle_any respond_to do |type| type.html { render :text => "HTML" } type.any(:js, :xml) { render :text => "Either JS or XML" } end end def all_types_with_layout respond_to do |type| type.html type.js end end def rescue_action(e) raise end protected def set_layout if action_name == "all_types_with_layout" "standard" end end end RespondToController.template_root = File.dirname(__FILE__) + "/../fixtures/" class MimeControllerTest < Test::Unit::TestCase def setup @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new @controller = RespondToController.new @request.host = "www.example.com" end def test_html @request.env["HTTP_ACCEPT"] = "text/html" get :js_or_html assert_equal 'HTML', @response.body get :html_or_xml assert_equal 'HTML', @response.body get :just_xml assert_response 406 end def test_all @request.env["HTTP_ACCEPT"] = "*/*" get :js_or_html assert_equal 'HTML', @response.body # js is not part of all get :html_or_xml assert_equal 'HTML', @response.body get :just_xml assert_equal 'XML', @response.body end def test_xml @request.env["HTTP_ACCEPT"] = "application/xml" get :html_xml_or_rss assert_equal 'XML', @response.body end def test_js_or_html @request.env["HTTP_ACCEPT"] = "text/javascript, text/html" get :js_or_html assert_equal 'JS', @response.body get :html_or_xml assert_equal 'HTML', @response.body get :just_xml assert_response 406 end def test_js_or_anything @request.env["HTTP_ACCEPT"] = "text/javascript, */*" get :js_or_html assert_equal 'JS', @response.body get :html_or_xml assert_equal 'HTML', @response.body get :just_xml assert_equal 'XML', @response.body end def test_using_defaults @request.env["HTTP_ACCEPT"] = "*/*" get :using_defaults assert_equal 'Hello world!', @response.body @request.env["HTTP_ACCEPT"] = "text/javascript" get :using_defaults assert_equal '$("body").visualEffect("highlight");', @response.body @request.env["HTTP_ACCEPT"] = "application/xml" get :using_defaults assert_equal "

    Hello world!

    \n", @response.body end def test_using_defaults_with_type_list @request.env["HTTP_ACCEPT"] = "*/*" get :using_defaults_with_type_list assert_equal 'Hello world!', @response.body @request.env["HTTP_ACCEPT"] = "text/javascript" get :using_defaults_with_type_list assert_equal '$("body").visualEffect("highlight");', @response.body @request.env["HTTP_ACCEPT"] = "application/xml" get :using_defaults_with_type_list assert_equal "

    Hello world!

    \n", @response.body end def test_with_content_type @request.env["CONTENT_TYPE"] = "application/atom+xml" get :made_for_content_type assert_equal "ATOM", @response.body @request.env["CONTENT_TYPE"] = "application/rss+xml" get :made_for_content_type assert_equal "RSS", @response.body end def test_synonyms @request.env["HTTP_ACCEPT"] = "application/javascript" get :js_or_html assert_equal 'JS', @response.body @request.env["HTTP_ACCEPT"] = "application/x-xml" get :html_xml_or_rss assert_equal "XML", @response.body end def test_custom_types @request.env["HTTP_ACCEPT"] = "application/crazy-xml" get :custom_type_handling assert_equal 'Crazy XML', @response.body @request.env["HTTP_ACCEPT"] = "text/html" get :custom_type_handling assert_equal 'HTML', @response.body end def test_xhtml_alias @request.env["HTTP_ACCEPT"] = "application/xhtml+xml,application/xml" get :html_or_xml assert_equal 'HTML', @response.body end def test_firefox_simulation @request.env["HTTP_ACCEPT"] = "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5" get :html_or_xml assert_equal 'HTML', @response.body end def test_handle_any @request.env["HTTP_ACCEPT"] = "*/*" get :handle_any assert_equal 'HTML', @response.body @request.env["HTTP_ACCEPT"] = "text/javascript" get :handle_any assert_equal 'Either JS or XML', @response.body @request.env["HTTP_ACCEPT"] = "text/xml" get :handle_any assert_equal 'Either JS or XML', @response.body end def test_all_types_with_layout @request.env["HTTP_ACCEPT"] = "text/javascript" get :all_types_with_layout assert_equal 'RJS for all_types_with_layout', @response.body @request.env["HTTP_ACCEPT"] = "text/html" get :all_types_with_layout assert_equal 'HTML for all_types_with_layout', @response.body end def test_xhr xhr :get, :js_or_html assert_equal 'JS', @response.body xhr :get, :using_defaults assert_equal '$("body").visualEffect("highlight");', @response.body end end require File.dirname(__FILE__) + '/../abstract_unit' class MimeTypeTest < Test::Unit::TestCase Mime::PNG = Mime::Type.new("image/png") Mime::PLAIN = Mime::Type.new("text/plain") def test_parse_single Mime::LOOKUP.keys.each do |mime_type| assert_equal [Mime::Type.lookup(mime_type)], Mime::Type.parse(mime_type) end end def test_parse_without_q accept = "text/xml,application/xhtml+xml,text/yaml,application/xml,text/html,image/png,text/plain,*/*" expect = [Mime::HTML, Mime::XML, Mime::YAML, Mime::PNG, Mime::PLAIN, Mime::ALL] assert_equal expect, Mime::Type.parse(accept) end def test_parse_with_q accept = "text/xml,application/xhtml+xml,text/yaml; q=0.3,application/xml,text/html; q=0.8,image/png,text/plain; q=0.5,*/*; q=0.2" expect = [Mime::HTML, Mime::XML, Mime::PNG, Mime::PLAIN, Mime::YAML, Mime::ALL] assert_equal expect, Mime::Type.parse(accept) end endrequire File.dirname(__FILE__) + '/../abstract_unit' silence_warnings { Customer = Struct.new("Customer", :name) } module Fun class GamesController < ActionController::Base def hello_world end end end module NewRenderTestHelper def rjs_helper_method_from_module page.visual_effect :highlight end end class NewRenderTestController < ActionController::Base layout :determine_layout def self.controller_name; "test"; end def self.controller_path; "test"; end def hello_world end def render_hello_world render :template => "test/hello_world" end def render_hello_world_from_variable @person = "david" render :text => "hello #{@person}" end def render_action_hello_world render :action => "hello_world" end def render_action_hello_world_as_symbol render :action => :hello_world end def render_text_hello_world render :text => "hello world" end def render_text_hello_world_with_layout @variable_for_layout = ", I'm here!" render :text => "hello world", :layout => true end def hello_world_with_layout_false render :layout => false end def render_custom_code render :text => "hello world", :status => "404 Moved" end def render_file_with_instance_variables @secret = 'in the sauce' path = File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_ivar.rhtml') render :file => path end def render_file_with_locals path = File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_locals.rhtml') render :file => path, :locals => {:secret => 'in the sauce'} end def render_file_not_using_full_path @secret = 'in the sauce' render :file => 'test/render_file_with_ivar', :use_full_path => true end def render_file_not_using_full_path_with_relative_path @secret = 'in the sauce' render :file => 'test/../test/render_file_with_ivar', :use_full_path => true end def render_file_not_using_full_path_with_dot_in_path @secret = 'in the sauce' render :file => 'test/dot.directory/render_file_with_ivar', :use_full_path => true end def render_xml_hello @name = "David" render :template => "test/hello" end def greeting # let's just rely on the template end def layout_test render :action => "hello_world" end def layout_test_with_different_layout render :action => "hello_world", :layout => "standard" end def rendering_without_layout render :action => "hello_world", :layout => false end def layout_overriding_layout render :action => "hello_world", :layout => "standard" end def rendering_nothing_on_layout render :nothing => true end def builder_layout_test render :action => "hello" end def partials_list @test_unchanged = 'hello' @customers = [ Customer.new("david"), Customer.new("mary") ] render :action => "list" end def partial_only render :partial => true end def partial_only_with_layout render :partial => "partial_only", :layout => true end def partial_with_locals render :partial => "customer", :locals => { :customer => Customer.new("david") } end def partial_collection render :partial => "customer", :collection => [ Customer.new("david"), Customer.new("mary") ] end def partial_collection_with_locals render :partial => "customer_greeting", :collection => [ Customer.new("david"), Customer.new("mary") ], :locals => { :greeting => "Bonjour" } end def empty_partial_collection render :partial => "customer", :collection => [] end def partial_with_hash_object render :partial => "hash_object", :object => {:first_name => "Sam"} end def partial_with_implicit_local_assignment @customer = Customer.new("Marcel") render :partial => "customer" end def hello_in_a_string @customers = [ Customer.new("david"), Customer.new("mary") ] render :text => "How's there? #{render_to_string("test/list")}" end def accessing_params_in_template render :inline => "Hello: <%= params[:name] %>" end def accessing_params_in_template_with_layout render :layout => nil, :inline => "Hello: <%= params[:name] %>" end def render_with_explicit_template render "test/hello_world" end def double_render render :text => "hello" render :text => "world" end def double_redirect redirect_to :action => "double_render" redirect_to :action => "double_render" end def render_and_redirect render :text => "hello" redirect_to :action => "double_render" end def rendering_with_conflicting_local_vars @name = "David" def @template.name() nil end render :action => "potential_conflicts" end def hello_world_from_rxml_using_action render :action => "hello_world.rxml" end def hello_world_from_rxml_using_template render :template => "test/hello_world.rxml" end helper NewRenderTestHelper helper do def rjs_helper_method(value) page.visual_effect :highlight, value end end def enum_rjs_test render :update do |page| page.select('.product').each do |value| page.rjs_helper_method_from_module page.rjs_helper_method(value) page.sortable(value, :url => { :action => "order" }) page.draggable(value) end end end def delete_with_js @project_id = 4 end def render_js_with_explicit_template @project_id = 4 render :template => 'test/delete_with_js' end def render_js_with_explicit_action_template @project_id = 4 render :action => 'delete_with_js' end def update_page render :update do |page| page.replace_html 'balance', '$37,000,000.00' page.visual_effect :highlight, 'balance' end end def update_page_with_instance_variables @money = '$37,000,000.00' @div_id = 'balance' render :update do |page| page.replace_html @div_id, @money page.visual_effect :highlight, @div_id end end def action_talk_to_layout # Action template sets variable that's picked up by layout end def render_text_with_assigns @hello = "world" render :text => "foo" end def yield_content_for render :action => "content_for", :layout => "yield" end def rescue_action(e) raise end private def determine_layout case action_name when "hello_world", "layout_test", "rendering_without_layout", "rendering_nothing_on_layout", "render_text_hello_world", "render_text_hello_world_with_layout", "hello_world_with_layout_false", "partial_only", "partial_only_with_layout", "accessing_params_in_template", "accessing_params_in_template_with_layout", "render_with_explicit_template", "render_js_with_explicit_template", "render_js_with_explicit_action_template", "delete_with_js", "update_page", "update_page_with_instance_variables" "layouts/standard" when "builder_layout_test" "layouts/builder" when "action_talk_to_layout", "layout_overriding_layout" "layouts/talk_from_action" end end end NewRenderTestController.template_root = File.dirname(__FILE__) + "/../fixtures/" Fun::GamesController.template_root = File.dirname(__FILE__) + "/../fixtures/" class NewRenderTest < Test::Unit::TestCase def setup @controller = NewRenderTestController.new # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get # a more accurate simulation of what happens in "real life". @controller.logger = Logger.new(nil) @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new @request.host = "www.nextangle.com" end def test_simple_show get :hello_world assert_response :success assert_template "test/hello_world" assert_equal "Hello world!", @response.body end def test_do_with_render get :render_hello_world assert_template "test/hello_world" end def test_do_with_render_from_variable get :render_hello_world_from_variable assert_equal "hello david", @response.body end def test_do_with_render_action get :render_action_hello_world assert_template "test/hello_world" end def test_do_with_render_action_as_symbol get :render_action_hello_world_as_symbol assert_template "test/hello_world" end def test_do_with_render_text get :render_text_hello_world assert_equal "hello world", @response.body end def test_do_with_render_text_and_layout get :render_text_hello_world_with_layout assert_equal "hello world, I'm here!", @response.body end def test_do_with_render_action_and_layout_false get :hello_world_with_layout_false assert_equal 'Hello world!', @response.body end def test_do_with_render_custom_code get :render_custom_code assert_response :missing end def test_render_file_with_instance_variables get :render_file_with_instance_variables assert_equal "The secret is in the sauce\n", @response.body end def test_render_file_not_using_full_path get :render_file_not_using_full_path assert_equal "The secret is in the sauce\n", @response.body end def test_render_file_not_using_full_path_with_relative_path get :render_file_not_using_full_path_with_relative_path assert_equal "The secret is in the sauce\n", @response.body end def test_render_file_not_using_full_path_with_dot_in_path get :render_file_not_using_full_path_with_dot_in_path assert_equal "The secret is in the sauce\n", @response.body end def test_render_file_with_locals get :render_file_with_locals assert_equal "The secret is in the sauce\n", @response.body end def test_attempt_to_access_object_method assert_raises(ActionController::UnknownAction, "No action responded to [clone]") { get :clone } end def test_private_methods assert_raises(ActionController::UnknownAction, "No action responded to [determine_layout]") { get :determine_layout } end def test_access_to_request_in_view view_internals_old_value = ActionController::Base.view_controller_internals ActionController::Base.view_controller_internals = false ActionController::Base.protected_variables_cache = nil get :hello_world assert_nil(assigns["request"]) ActionController::Base.view_controller_internals = true ActionController::Base.protected_variables_cache = nil get :hello_world assert_kind_of ActionController::AbstractRequest, assigns["request"] ActionController::Base.view_controller_internals = view_internals_old_value ActionController::Base.protected_variables_cache = nil end def test_render_xml get :render_xml_hello assert_equal "\n

    Hello David

    \n

    This is grand!

    \n\n", @response.body end def test_enum_rjs_test get :enum_rjs_test assert_equal <<-EOS.strip, @response.body $$(".product").each(function(value, index) { new Effect.Highlight(element,{}); new Effect.Highlight(value,{}); Sortable.create(value, {onUpdate:function(){new Ajax.Request('/test/order', {asynchronous:true, evalScripts:true, parameters:Sortable.serialize(value)})}}); new Draggable(value, {}); }); EOS end def test_render_xml_with_default get :greeting assert_equal "

    This is grand!

    \n", @response.body end def test_render_rjs_with_default get :delete_with_js assert_equal %!["person"].each(Element.remove);\nnew Effect.Highlight(\"project-4\",{});!, @response.body end def test_render_rjs_template_explicitly get :render_js_with_explicit_template assert_equal %!["person"].each(Element.remove);\nnew Effect.Highlight(\"project-4\",{});!, @response.body end def test_rendering_rjs_action_explicitly get :render_js_with_explicit_action_template assert_equal %!["person"].each(Element.remove);\nnew Effect.Highlight(\"project-4\",{});!, @response.body end def test_layout_rendering get :layout_test assert_equal "Hello world!", @response.body end def test_layout_test_with_different_layout get :layout_test_with_different_layout assert_equal "Hello world!", @response.body end def test_rendering_without_layout get :rendering_without_layout assert_equal "Hello world!", @response.body end def test_layout_overriding_layout get :layout_overriding_layout assert_no_match %r{}, @response.body end def test_rendering_nothing_on_layout get :rendering_nothing_on_layout assert_equal " ", @response.body end def test_render_xml_with_layouts get :builder_layout_test assert_equal "<wrapper>\n<html>\n <p>Hello </p>\n<p>This is grand!</p>\n</html>\n</wrapper>\n", @response.body end def test_partial_only get :partial_only assert_equal "only partial", @response.body end def test_partial_only_with_layout get :partial_only_with_layout assert_equal "<html>only partial</html>", @response.body end def test_render_to_string get :hello_in_a_string assert_equal "How's there? goodbyeHello: davidHello: marygoodbye\n", @response.body end def test_nested_rendering get :hello_world assert_equal "Living in a nested world", Fun::GamesController.process(@request, @response).body end def test_accessing_params_in_template get :accessing_params_in_template, :name => "David" assert_equal "Hello: David", @response.body end def test_accessing_params_in_template_with_layout get :accessing_params_in_template_with_layout, :name => "David" assert_equal "<html>Hello: David</html>", @response.body end def test_render_with_explicit_template get :render_with_explicit_template assert_response :success end def test_double_render assert_raises(ActionController::DoubleRenderError) { get :double_render } end def test_double_redirect assert_raises(ActionController::DoubleRenderError) { get :double_redirect } end def test_render_and_redirect assert_raises(ActionController::DoubleRenderError) { get :render_and_redirect } end def test_rendering_with_conflicting_local_vars get :rendering_with_conflicting_local_vars assert_equal("First: David\nSecond: Stephan\nThird: David\nFourth: David\nFifth: ", @response.body) end def test_action_talk_to_layout get :action_talk_to_layout assert_equal "<title>Talking to the layout\nAction was here!", @response.body end def test_partials_list get :partials_list assert_equal "goodbyeHello: davidHello: marygoodbye\n", @response.body end def test_partial_with_locals get :partial_with_locals assert_equal "Hello: david", @response.body end def test_partial_collection get :partial_collection assert_equal "Hello: davidHello: mary", @response.body end def test_partial_collection_with_locals get :partial_collection_with_locals assert_equal "Bonjour: davidBonjour: mary", @response.body end def test_empty_partial_collection get :empty_partial_collection assert_equal " ", @response.body end def test_partial_with_hash_object get :partial_with_hash_object assert_equal "Sam", @response.body end def test_partial_with_implicit_local_assignment get :partial_with_implicit_local_assignment assert_equal "Hello: Marcel", @response.body end def test_render_text_with_assigns get :render_text_with_assigns assert_equal "world", assigns["hello"] end def test_update_page get :update_page assert_template nil assert_equal 'text/javascript; charset=UTF-8', @response.headers['Content-Type'] assert_equal 2, @response.body.split($/).length end def test_update_page_with_instance_variables get :update_page_with_instance_variables assert_template nil assert_equal 'text/javascript; charset=UTF-8', @response.headers['Content-Type'] assert_match /balance/, @response.body assert_match /\$37/, @response.body end def test_yield_content_for get :yield_content_for assert_equal "Putting stuff in the title!\n\nGreat stuff!\n", @response.body end def test_overwritting_rendering_relative_file_with_extension get :hello_world_from_rxml_using_template assert_equal "\n

    Hello

    \n\n", @response.body get :hello_world_from_rxml_using_action assert_equal "\n

    Hello

    \n\n", @response.body end end require 'test/unit' require 'cgi' require 'stringio' require File.dirname(__FILE__) + '/../../lib/action_controller/cgi_ext/raw_post_data_fix' class RawPostDataTest < Test::Unit::TestCase def setup ENV['REQUEST_METHOD'] = 'POST' ENV['CONTENT_TYPE'] = '' ENV['CONTENT_LENGTH'] = '0' end def test_raw_post_data process_raw "action=create_customer&full_name=David%20Heinemeier%20Hansson&customerId=1" end private def process_raw(query_string) old_stdin = $stdin begin $stdin = StringIO.new(query_string.dup) ENV['CONTENT_LENGTH'] = $stdin.size.to_s CGI.new assert_not_nil ENV['RAW_POST_DATA'] assert ENV['RAW_POST_DATA'].frozen? assert_equal query_string, ENV['RAW_POST_DATA'] ensure $stdin = old_stdin end end end require File.dirname(__FILE__) + '/../abstract_unit' class RedirectController < ActionController::Base def simple_redirect redirect_to :action => "hello_world" end def method_redirect redirect_to :dashbord_url, 1, "hello" end def host_redirect redirect_to :action => "other_host", :only_path => false, :host => 'other.test.host' end def module_redirect redirect_to :controller => 'module_test/module_redirect', :action => "hello_world" end def redirect_with_assigns @hello = "world" redirect_to :action => "hello_world" end def redirect_to_back redirect_to :back end def rescue_errors(e) raise e end def rescue_action(e) raise end protected def dashbord_url(id, message) url_for :action => "dashboard", :params => { "id" => id, "message" => message } end end class RedirectTest < Test::Unit::TestCase def setup @controller = RedirectController.new @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new end def test_simple_redirect get :simple_redirect assert_redirect_url "http://test.host/redirect/hello_world" end def test_redirect_with_method_reference_and_parameters get :method_redirect assert_redirect_url "http://test.host/redirect/dashboard/1?message=hello" end def test_simple_redirect_using_options get :host_redirect assert_redirected_to :action => "other_host", :only_path => false, :host => 'other.test.host' end def test_redirect_error_with_pretty_diff get :host_redirect begin assert_redirected_to :action => "other_host", :only_path => true rescue Test::Unit::AssertionFailedError => err redirection_msg, diff_msg = err.message.scan(/<\{[^\}]+\}>/).collect { |s| s[2..-3] } assert_match %r(:only_path=>false), redirection_msg assert_match %r(:host=>"other.test.host"), redirection_msg assert_match %r(:action=>"other_host"), redirection_msg assert_match %r(:only_path=>true), diff_msg assert_match %r(:host=>"other.test.host"), diff_msg end end def test_module_redirect get :module_redirect assert_redirect_url "http://test.host/module_test/module_redirect/hello_world" end def test_module_redirect_using_options get :module_redirect assert_redirected_to :controller => 'module_test/module_redirect', :action => 'hello_world' end def test_redirect_with_assigns get :redirect_with_assigns assert_equal "world", assigns["hello"] end def test_redirect_to_back @request.env["HTTP_REFERER"] = "http://www.example.com/coming/from" get :redirect_to_back assert_redirect_url "http://www.example.com/coming/from" end def test_redirect_to_back_with_no_referer assert_raises(ActionController::RedirectBackError) { @request.env["HTTP_REFERER"] = nil get :redirect_to_back } end end module ModuleTest class ModuleRedirectController < ::RedirectController def module_redirect redirect_to :controller => '/redirect', :action => "hello_world" end end class ModuleRedirectTest < Test::Unit::TestCase def setup @controller = ModuleRedirectController.new @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new end def test_simple_redirect get :simple_redirect assert_redirect_url "http://test.host/module_test/module_redirect/hello_world" end def test_redirect_with_method_reference_and_parameters get :method_redirect assert_redirect_url "http://test.host/module_test/module_redirect/dashboard/1?message=hello" end def test_simple_redirect_using_options get :host_redirect assert_redirected_to :action => "other_host", :only_path => false, :host => 'other.test.host' end def test_module_redirect get :module_redirect assert_redirect_url "http://test.host/redirect/hello_world" end def test_module_redirect_using_options get :module_redirect assert_redirected_to :controller => 'redirect', :action => "hello_world" end end end require File.dirname(__FILE__) + '/../abstract_unit' unless defined?(Customer) Customer = Struct.new("Customer", :name) end module Fun class GamesController < ActionController::Base def hello_world end end end class TestController < ActionController::Base layout :determine_layout def hello_world end def render_hello_world render "test/hello_world" end def render_hello_world_from_variable @person = "david" render_text "hello #{@person}" end def render_action_hello_world render_action "hello_world" end def render_action_hello_world_with_symbol render_action :hello_world end def render_text_hello_world render_text "hello world" end def render_custom_code render_text "hello world", "404 Moved" end def render_xml_hello @name = "David" render "test/hello" end def greeting # let's just rely on the template end def layout_test render_action "hello_world" end def builder_layout_test render_action "hello" end def partials_list @test_unchanged = 'hello' @customers = [ Customer.new("david"), Customer.new("mary") ] render_action "list" end def partial_only render_partial end def hello_in_a_string @customers = [ Customer.new("david"), Customer.new("mary") ] render_text "How's there? #{render_to_string("test/list")}" end def accessing_params_in_template render_template "Hello: <%= params[:name] %>" end def accessing_local_assigns_in_inline_template name = params[:local_name] render :inline => "<%= 'Goodbye, ' + local_name %>", :locals => { :local_name => name } end def accessing_local_assigns_in_inline_template_with_string_keys name = params[:local_name] ActionView::Base.local_assigns_support_string_keys = true render :inline => "<%= 'Goodbye, ' + local_name %>", :locals => { "local_name" => name } ActionView::Base.local_assigns_support_string_keys = false end def render_to_string_test @foo = render_to_string :inline => "this is a test" end def rescue_action(e) raise end private def determine_layout case action_name when "layout_test": "layouts/standard" when "builder_layout_test": "layouts/builder" end end end TestController.template_root = File.dirname(__FILE__) + "/../fixtures/" Fun::GamesController.template_root = File.dirname(__FILE__) + "/../fixtures/" class RenderTest < Test::Unit::TestCase def setup @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new @controller = TestController.new @request.host = "www.nextangle.com" end def test_simple_show get :hello_world assert_response 200 assert_template "test/hello_world" end def test_do_with_render get :render_hello_world assert_template "test/hello_world" end def test_do_with_render_from_variable get :render_hello_world_from_variable assert_equal "hello david", @response.body end def test_do_with_render_action get :render_action_hello_world assert_template "test/hello_world" end def test_do_with_render_action_with_symbol get :render_action_hello_world_with_symbol assert_template "test/hello_world" end def test_do_with_render_text get :render_text_hello_world assert_equal "hello world", @response.body end def test_do_with_render_custom_code get :render_custom_code assert_response 404 end def test_attempt_to_access_object_method assert_raises(ActionController::UnknownAction, "No action responded to [clone]") { get :clone } end def test_private_methods assert_raises(ActionController::UnknownAction, "No action responded to [determine_layout]") { get :determine_layout } end def test_access_to_request_in_view view_internals_old_value = ActionController::Base.view_controller_internals ActionController::Base.view_controller_internals = false ActionController::Base.protected_variables_cache = nil get :hello_world assert_nil assigns["request"] ActionController::Base.view_controller_internals = true ActionController::Base.protected_variables_cache = nil get :hello_world assert_kind_of ActionController::AbstractRequest, assigns["request"] ActionController::Base.view_controller_internals = view_internals_old_value ActionController::Base.protected_variables_cache = nil end def test_render_xml get :render_xml_hello assert_equal "\n

    Hello David

    \n

    This is grand!

    \n\n", @response.body end def test_render_xml_with_default get :greeting assert_equal "

    This is grand!

    \n", @response.body end def test_layout_rendering get :layout_test assert_equal "Hello world!", @response.body end def test_render_xml_with_layouts get :builder_layout_test assert_equal "\n\n

    Hello

    \n

    This is grand!

    \n\n
    \n", @response.body end # def test_partials_list # get :partials_list # assert_equal "goodbyeHello: davidHello: marygoodbye\n", process_request.body # end def test_partial_only get :partial_only assert_equal "only partial", @response.body end def test_render_to_string get :hello_in_a_string assert_equal "How's there? goodbyeHello: davidHello: marygoodbye\n", @response.body end def test_render_to_string_resets_assigns get :render_to_string_test assert_equal "The value of foo is: ::this is a test::\n", @response.body end def test_nested_rendering @controller = Fun::GamesController.new get :hello_world assert_equal "Living in a nested world", @response.body end def test_accessing_params_in_template get :accessing_params_in_template, :name => "David" assert_equal "Hello: David", @response.body end def test_accessing_local_assigns_in_inline_template get :accessing_local_assigns_in_inline_template, :local_name => "Local David" assert_equal "Goodbye, Local David", @response.body end def test_accessing_local_assigns_in_inline_template_with_string_keys get :accessing_local_assigns_in_inline_template_with_string_keys, :local_name => "Local David" assert_equal "Goodbye, Local David", @response.body end end require File.dirname(__FILE__) + '/../abstract_unit' class RequestTest < Test::Unit::TestCase def setup @request = ActionController::TestRequest.new end def test_remote_ip assert_equal '0.0.0.0', @request.remote_ip @request.remote_addr = '1.2.3.4' assert_equal '1.2.3.4', @request.remote_ip @request.env['HTTP_CLIENT_IP'] = '2.3.4.5' assert_equal '2.3.4.5', @request.remote_ip @request.env.delete 'HTTP_CLIENT_IP' @request.env['HTTP_X_FORWARDED_FOR'] = '3.4.5.6' assert_equal '3.4.5.6', @request.remote_ip @request.env['HTTP_X_FORWARDED_FOR'] = 'unknown,3.4.5.6' assert_equal '3.4.5.6', @request.remote_ip @request.env['HTTP_X_FORWARDED_FOR'] = '172.16.0.1,3.4.5.6' assert_equal '3.4.5.6', @request.remote_ip @request.env['HTTP_X_FORWARDED_FOR'] = '192.168.0.1,3.4.5.6' assert_equal '3.4.5.6', @request.remote_ip @request.env['HTTP_X_FORWARDED_FOR'] = '10.0.0.1,3.4.5.6' assert_equal '3.4.5.6', @request.remote_ip @request.env['HTTP_X_FORWARDED_FOR'] = '127.0.0.1,3.4.5.6' assert_equal '127.0.0.1', @request.remote_ip @request.env['HTTP_X_FORWARDED_FOR'] = 'unknown,192.168.0.1' assert_equal '1.2.3.4', @request.remote_ip @request.env.delete 'HTTP_X_FORWARDED_FOR' end def test_domains @request.host = "www.rubyonrails.org" assert_equal "rubyonrails.org", @request.domain @request.host = "www.rubyonrails.co.uk" assert_equal "rubyonrails.co.uk", @request.domain(2) @request.host = "192.168.1.200" assert_nil @request.domain @request.host = nil assert_nil @request.domain end def test_subdomains @request.host = "www.rubyonrails.org" assert_equal %w( www ), @request.subdomains @request.host = "www.rubyonrails.co.uk" assert_equal %w( www ), @request.subdomains(2) @request.host = "dev.www.rubyonrails.co.uk" assert_equal %w( dev www ), @request.subdomains(2) @request.host = "foobar.foobar.com" assert_equal %w( foobar ), @request.subdomains @request.host = nil assert_equal [], @request.subdomains end def test_port_string @request.port = 80 assert_equal "", @request.port_string @request.port = 8080 assert_equal ":8080", @request.port_string end def test_relative_url_root @request.env['SCRIPT_NAME'] = "/hieraki/dispatch.cgi" @request.env['SERVER_SOFTWARE'] = 'lighttpd/1.2.3' assert_equal '', @request.relative_url_root, "relative_url_root should be disabled on lighttpd" @request.env['SERVER_SOFTWARE'] = 'apache/1.2.3 some random text' @request.env['SCRIPT_NAME'] = nil assert_equal "", @request.relative_url_root @request.env['SCRIPT_NAME'] = "/dispatch.cgi" assert_equal "", @request.relative_url_root @request.env['SCRIPT_NAME'] = "/myapp.rb" assert_equal "", @request.relative_url_root @request.relative_url_root = nil @request.env['SCRIPT_NAME'] = "/hieraki/dispatch.cgi" assert_equal "/hieraki", @request.relative_url_root @request.relative_url_root = nil @request.env['SCRIPT_NAME'] = "/collaboration/hieraki/dispatch.cgi" assert_equal "/collaboration/hieraki", @request.relative_url_root # apache/scgi case @request.relative_url_root = nil @request.env['SCRIPT_NAME'] = "/collaboration/hieraki" assert_equal "/collaboration/hieraki", @request.relative_url_root @request.relative_url_root = nil @request.env['SCRIPT_NAME'] = "/hieraki/dispatch.cgi" @request.env['SERVER_SOFTWARE'] = 'lighttpd/1.2.3' @request.env['RAILS_RELATIVE_URL_ROOT'] = "/hieraki" assert_equal "/hieraki", @request.relative_url_root # @env overrides path guess @request.relative_url_root = nil @request.env['SCRIPT_NAME'] = "/hieraki/dispatch.cgi" @request.env['SERVER_SOFTWARE'] = 'apache/1.2.3 some random text' @request.env['RAILS_RELATIVE_URL_ROOT'] = "/real_url" assert_equal "/real_url", @request.relative_url_root end def test_request_uri @request.env['SERVER_SOFTWARE'] = 'Apache 42.342.3432' @request.relative_url_root = nil @request.set_REQUEST_URI "http://www.rubyonrails.org/path/of/some/uri?mapped=1" assert_equal "/path/of/some/uri?mapped=1", @request.request_uri assert_equal "/path/of/some/uri", @request.path @request.relative_url_root = nil @request.set_REQUEST_URI "http://www.rubyonrails.org/path/of/some/uri" assert_equal "/path/of/some/uri", @request.request_uri assert_equal "/path/of/some/uri", @request.path @request.relative_url_root = nil @request.set_REQUEST_URI "/path/of/some/uri" assert_equal "/path/of/some/uri", @request.request_uri assert_equal "/path/of/some/uri", @request.path @request.relative_url_root = nil @request.set_REQUEST_URI "/" assert_equal "/", @request.request_uri assert_equal "/", @request.path @request.relative_url_root = nil @request.set_REQUEST_URI "/?m=b" assert_equal "/?m=b", @request.request_uri assert_equal "/", @request.path @request.relative_url_root = nil @request.set_REQUEST_URI "/" @request.env['SCRIPT_NAME'] = "/dispatch.cgi" assert_equal "/", @request.request_uri assert_equal "/", @request.path @request.relative_url_root = nil @request.set_REQUEST_URI "/hieraki/" @request.env['SCRIPT_NAME'] = "/hieraki/dispatch.cgi" assert_equal "/hieraki/", @request.request_uri assert_equal "/", @request.path @request.relative_url_root = nil @request.set_REQUEST_URI "/collaboration/hieraki/books/edit/2" @request.env['SCRIPT_NAME'] = "/collaboration/hieraki/dispatch.cgi" assert_equal "/collaboration/hieraki/books/edit/2", @request.request_uri assert_equal "/books/edit/2", @request.path # The following tests are for when REQUEST_URI is not supplied (as in IIS) @request.relative_url_root = nil @request.set_REQUEST_URI nil @request.env['PATH_INFO'] = "/path/of/some/uri?mapped=1" @request.env['SCRIPT_NAME'] = nil #"/path/dispatch.rb" assert_equal "/path/of/some/uri?mapped=1", @request.request_uri assert_equal "/path/of/some/uri", @request.path @request.relative_url_root = nil @request.env['PATH_INFO'] = "/path/of/some/uri?mapped=1" @request.env['SCRIPT_NAME'] = "/path/dispatch.rb" assert_equal "/path/of/some/uri?mapped=1", @request.request_uri assert_equal "/of/some/uri", @request.path @request.relative_url_root = nil @request.env['PATH_INFO'] = "/path/of/some/uri" @request.env['SCRIPT_NAME'] = nil assert_equal "/path/of/some/uri", @request.request_uri assert_equal "/path/of/some/uri", @request.path @request.relative_url_root = nil @request.env['PATH_INFO'] = "/" assert_equal "/", @request.request_uri assert_equal "/", @request.path @request.relative_url_root = nil @request.env['PATH_INFO'] = "/?m=b" assert_equal "/?m=b", @request.request_uri assert_equal "/", @request.path @request.relative_url_root = nil @request.env['PATH_INFO'] = "/" @request.env['SCRIPT_NAME'] = "/dispatch.cgi" assert_equal "/", @request.request_uri assert_equal "/", @request.path @request.relative_url_root = nil @request.env['PATH_INFO'] = "/hieraki/" @request.env['SCRIPT_NAME'] = "/hieraki/dispatch.cgi" assert_equal "/hieraki/", @request.request_uri assert_equal "/", @request.path # This test ensures that Rails uses REQUEST_URI over PATH_INFO @request.relative_url_root = nil @request.env['REQUEST_URI'] = "/some/path" @request.env['PATH_INFO'] = "/another/path" @request.env['SCRIPT_NAME'] = "/dispatch.cgi" assert_equal "/some/path", @request.request_uri assert_equal "/some/path", @request.path end def test_host_with_port @request.host = "rubyonrails.org" @request.port = 80 assert_equal "rubyonrails.org", @request.host_with_port @request.host = "rubyonrails.org" @request.port = 81 assert_equal "rubyonrails.org:81", @request.host_with_port end def test_server_software assert_equal nil, @request.server_software @request.env['SERVER_SOFTWARE'] = 'Apache3.422' assert_equal 'apache', @request.server_software @request.env['SERVER_SOFTWARE'] = 'lighttpd(1.1.4)' assert_equal 'lighttpd', @request.server_software end def test_xml_http_request assert !@request.xml_http_request? assert !@request.xhr? @request.env['HTTP_X_REQUESTED_WITH'] = "DefinitelyNotAjax1.0" assert !@request.xml_http_request? assert !@request.xhr? @request.env['HTTP_X_REQUESTED_WITH'] = "XMLHttpRequest" assert @request.xml_http_request? assert @request.xhr? end def test_reports_ssl assert !@request.ssl? @request.env['HTTPS'] = 'on' assert @request.ssl? end def test_reports_ssl_when_proxied_via_lighttpd assert !@request.ssl? @request.env['HTTP_X_FORWARDED_PROTO'] = 'https' assert @request.ssl? end end require File.dirname(__FILE__) + '/../abstract_unit' require 'test/unit' require File.dirname(__FILE__) + '/fake_controllers' require 'stringio' RunTimeTests = ARGV.include? 'time' module ActionController::CodeGeneration class SourceTests < Test::Unit::TestCase attr_accessor :source def setup @source = Source.new end def test_initial_state assert_equal [], source.lines assert_equal 0, source.indentation_level end def test_trivial_operations source << "puts 'Hello World'" assert_equal ["puts 'Hello World'"], source.lines assert_equal "puts 'Hello World'", source.to_s source.line "puts 'Goodbye World'" assert_equal ["puts 'Hello World'", "puts 'Goodbye World'"], source.lines assert_equal "puts 'Hello World'\nputs 'Goodbye World'", source.to_s end def test_indentation source << "x = gets.to_i" source << 'if x.odd?' source.indent { source << "puts 'x is odd!'" } source << 'else' source.indent { source << "puts 'x is even!'" } source << 'end' assert_equal ["x = gets.to_i", "if x.odd?", " puts 'x is odd!'", 'else', " puts 'x is even!'", 'end'], source.lines text = "x = gets.to_i if x.odd? puts 'x is odd!' else puts 'x is even!' end" assert_equal text, source.to_s end end class CodeGeneratorTests < Test::Unit::TestCase attr_accessor :generator def setup @generator = CodeGenerator.new end def test_initial_state assert_equal [], generator.source.lines assert_equal [], generator.locals end def test_trivial_operations ["puts 'Hello World'", "puts 'Goodbye World'"].each {|l| generator << l} assert_equal ["puts 'Hello World'", "puts 'Goodbye World'"], generator.source.lines assert_equal "puts 'Hello World'\nputs 'Goodbye World'", generator.to_s end def test_if generator << "x = gets.to_i" generator.if("x.odd?") { generator << "puts 'x is odd!'" } assert_equal "x = gets.to_i\nif x.odd?\n puts 'x is odd!'\nend", generator.to_s end def test_else test_if generator.else { generator << "puts 'x is even!'" } assert_equal "x = gets.to_i\nif x.odd?\n puts 'x is odd!'\nelse \n puts 'x is even!'\nend", generator.to_s end def test_dup generator << 'x = 2' generator.locals << :x g = generator.dup assert_equal generator.source, g.source assert_equal generator.locals, g.locals g << 'y = 3' g.locals << :y assert_equal [:x, :y], g.locals # Make sure they don't share the same array. assert_equal [:x], generator.locals end end class RecognitionTests < Test::Unit::TestCase attr_accessor :generator alias :g :generator def setup @generator = RecognitionGenerator.new end def go(components) g.current = components.first g.after = components[1..-1] || [] g.go end def execute(path, show = false) path = path.split('/') if path.is_a? String source = "index, path = 0, #{path.inspect}\n#{g.to_s}" puts source if show r = eval source r ? r.symbolize_keys : nil end Static = ::ActionController::Routing::StaticComponent Dynamic = ::ActionController::Routing::DynamicComponent Path = ::ActionController::Routing::PathComponent Controller = ::ActionController::Routing::ControllerComponent def test_all_static c = %w(hello world how are you).collect {|str| Static.new(str)} g.result :controller, "::ContentController", true g.constant_result :action, 'index' go c assert_nil execute('x') assert_nil execute('hello/world/how') assert_nil execute('hello/world/how/are') assert_nil execute('hello/world/how/are/you/today') assert_equal({:controller => ::ContentController, :action => 'index'}, execute('hello/world/how/are/you')) end def test_basic_dynamic c = [Static.new("hi"), Dynamic.new(:action)] g.result :controller, "::ContentController", true go c assert_nil execute('boo') assert_nil execute('boo/blah') assert_nil execute('hi') assert_nil execute('hi/dude/what') assert_equal({:controller => ::ContentController, :action => 'dude'}, execute('hi/dude')) end def test_basic_dynamic_backwards c = [Dynamic.new(:action), Static.new("hi")] go c assert_nil execute('') assert_nil execute('boo') assert_nil execute('boo/blah') assert_nil execute('hi') assert_equal({:action => 'index'}, execute('index/hi')) assert_equal({:action => 'show'}, execute('show/hi')) assert_nil execute('hi/dude') end def test_dynamic_with_default c = [Static.new("hi"), Dynamic.new(:action, :default => 'index')] g.result :controller, "::ContentController", true go c assert_nil execute('boo') assert_nil execute('boo/blah') assert_nil execute('hi/dude/what') assert_equal({:controller => ::ContentController, :action => 'index'}, execute('hi')) assert_equal({:controller => ::ContentController, :action => 'index'}, execute('hi/index')) assert_equal({:controller => ::ContentController, :action => 'dude'}, execute('hi/dude')) end def test_dynamic_with_string_condition c = [Static.new("hi"), Dynamic.new(:action, :condition => 'index')] g.result :controller, "::ContentController", true go c assert_nil execute('boo') assert_nil execute('boo/blah') assert_nil execute('hi') assert_nil execute('hi/dude/what') assert_equal({:controller => ::ContentController, :action => 'index'}, execute('hi/index')) assert_nil execute('hi/dude') end def test_dynamic_with_string_condition_backwards c = [Dynamic.new(:action, :condition => 'index'), Static.new("hi")] g.result :controller, "::ContentController", true go c assert_nil execute('boo') assert_nil execute('boo/blah') assert_nil execute('hi') assert_nil execute('dude/what/hi') assert_nil execute('index/what') assert_equal({:controller => ::ContentController, :action => 'index'}, execute('index/hi')) assert_nil execute('dude/hi') end def test_dynamic_with_regexp_condition c = [Static.new("hi"), Dynamic.new(:action, :condition => /^[a-z]+$/)] g.result :controller, "::ContentController", true go c assert_nil execute('boo') assert_nil execute('boo/blah') assert_nil execute('hi') assert_nil execute('hi/FOXY') assert_nil execute('hi/138708jkhdf') assert_nil execute('hi/dkjfl8792343dfsf') assert_nil execute('hi/dude/what') assert_equal({:controller => ::ContentController, :action => 'index'}, execute('hi/index')) assert_equal({:controller => ::ContentController, :action => 'dude'}, execute('hi/dude')) end def test_dynamic_with_regexp_and_default c = [Static.new("hi"), Dynamic.new(:action, :condition => /^[a-z]+$/, :default => 'index')] g.result :controller, "::ContentController", true go c assert_nil execute('boo') assert_nil execute('boo/blah') assert_nil execute('hi/FOXY') assert_nil execute('hi/138708jkhdf') assert_nil execute('hi/dkjfl8792343dfsf') assert_equal({:controller => ::ContentController, :action => 'index'}, execute('hi')) assert_equal({:controller => ::ContentController, :action => 'index'}, execute('hi/index')) assert_equal({:controller => ::ContentController, :action => 'dude'}, execute('hi/dude')) assert_nil execute('hi/dude/what') end def test_path c = [Static.new("hi"), Path.new(:file)] g.result :controller, "::ContentController", true g.constant_result :action, "download" go c assert_nil execute('boo') assert_nil execute('boo/blah') assert_equal({:controller => ::ContentController, :action => 'download', :file => []}, execute('hi')) assert_equal({:controller => ::ContentController, :action => 'download', :file => %w(books agile_rails_dev.pdf)}, execute('hi/books/agile_rails_dev.pdf')) assert_equal({:controller => ::ContentController, :action => 'download', :file => ['dude']}, execute('hi/dude')) assert_equal 'dude/what', execute('hi/dude/what')[:file].to_s end def test_path_with_dynamic c = [Dynamic.new(:action), Path.new(:file)] g.result :controller, "::ContentController", true go c assert_nil execute('') assert_equal({:controller => ::ContentController, :action => 'download', :file => []}, execute('download')) assert_equal({:controller => ::ContentController, :action => 'download', :file => %w(books agile_rails_dev.pdf)}, execute('download/books/agile_rails_dev.pdf')) assert_equal({:controller => ::ContentController, :action => 'download', :file => ['dude']}, execute('download/dude')) assert_equal 'dude/what', execute('hi/dude/what')[:file].to_s end def test_path_with_dynamic_and_default c = [Dynamic.new(:action, :default => 'index'), Path.new(:file)] go c assert_equal({:action => 'index', :file => []}, execute('')) assert_equal({:action => 'index', :file => []}, execute('index')) assert_equal({:action => 'blarg', :file => []}, execute('blarg')) assert_equal({:action => 'index', :file => ['content']}, execute('index/content')) assert_equal({:action => 'show', :file => ['rails_dev.pdf']}, execute('show/rails_dev.pdf')) end def test_controller c = [Static.new("hi"), Controller.new(:controller)] g.constant_result :action, "hi" go c assert_nil execute('boo') assert_nil execute('boo/blah') assert_nil execute('hi/x') assert_nil execute('hi/13870948') assert_nil execute('hi/content/dog') assert_nil execute('hi/admin/user/foo') assert_equal({:controller => ::ContentController, :action => 'hi'}, execute('hi/content')) assert_equal({:controller => ::Admin::UserController, :action => 'hi'}, execute('hi/admin/user')) end def test_controller_with_regexp c = [Static.new("hi"), Controller.new(:controller, :condition => /^admin\/.+$/)] g.constant_result :action, "hi" go c assert_nil execute('hi') assert_nil execute('hi/x') assert_nil execute('hi/content') assert_equal({:controller => ::Admin::UserController, :action => 'hi'}, execute('hi/admin/user')) assert_equal({:controller => ::Admin::NewsFeedController, :action => 'hi'}, execute('hi/admin/news_feed')) assert_nil execute('hi/admin/user/foo') end def test_standard_route(time = ::RunTimeTests) c = [Controller.new(:controller), Dynamic.new(:action, :default => 'index'), Dynamic.new(:id, :default => nil)] go c # Make sure we get the right answers assert_equal({:controller => ::ContentController, :action => 'index'}, execute('content')) assert_equal({:controller => ::ContentController, :action => 'list'}, execute('content/list')) assert_equal({:controller => ::ContentController, :action => 'show', :id => '10'}, execute('content/show/10')) assert_equal({:controller => ::Admin::UserController, :action => 'index'}, execute('admin/user')) assert_equal({:controller => ::Admin::UserController, :action => 'list'}, execute('admin/user/list')) assert_equal({:controller => ::Admin::UserController, :action => 'show', :id => 'nseckar'}, execute('admin/user/show/nseckar')) assert_nil execute('content/show/10/20') assert_nil execute('food') if time source = "def self.execute(path) path = path.split('/') if path.is_a? String index = 0 r = #{g.to_s} end" eval(source) GC.start n = 1000 time = Benchmark.realtime do n.times { execute('content') execute('content/list') execute('content/show/10') execute('admin/user') execute('admin/user/list') execute('admin/user/show/nseckar') execute('admin/user/show/nseckar/dude') execute('admin/why/show/nseckar') execute('content/show/10/20') execute('food') } end time -= Benchmark.realtime do n.times { } end puts "\n\nRecognition:" per_url = time / (n * 10) puts "#{per_url * 1000} ms/url" puts "#{1 / per_url} urls/s\n\n" end end def test_default_route g.result :controller, "::ContentController", true g.constant_result :action, 'index' go [] assert_nil execute('x') assert_nil execute('hello/world/how') assert_nil execute('hello/world/how/are') assert_nil execute('hello/world/how/are/you/today') assert_equal({:controller => ::ContentController, :action => 'index'}, execute([])) end end class GenerationTests < Test::Unit::TestCase attr_accessor :generator alias :g :generator def setup @generator = GenerationGenerator.new # ha! end def go(components) g.current = components.first g.after = components[1..-1] || [] g.go end def execute(options, recall, show = false) source = "\n expire_on = ::ActionController::Routing.expiry_hash(options, recall) hash = merged = recall.merge(options) not_expired = true #{g.to_s}\n\n" puts source if show eval(source) end Static = ::ActionController::Routing::StaticComponent Dynamic = ::ActionController::Routing::DynamicComponent Path = ::ActionController::Routing::PathComponent Controller = ::ActionController::Routing::ControllerComponent def test_all_static_no_requirements c = [Static.new("hello"), Static.new("world")] go c assert_equal "/hello/world", execute({}, {}) end def test_basic_dynamic c = [Static.new("hi"), Dynamic.new(:action)] go c assert_equal '/hi/index', execute({:action => 'index'}, {:action => 'index'}) assert_equal '/hi/show', execute({:action => 'show'}, {:action => 'index'}) assert_equal '/hi/list+people', execute({}, {:action => 'list people'}) assert_nil execute({},{}) end def test_dynamic_with_default c = [Static.new("hi"), Dynamic.new(:action, :default => 'index')] go c assert_equal '/hi', execute({:action => 'index'}, {:action => 'index'}) assert_equal '/hi/show', execute({:action => 'show'}, {:action => 'index'}) assert_equal '/hi/list+people', execute({}, {:action => 'list people'}) assert_equal '/hi', execute({}, {}) end def test_dynamic_with_regexp_condition c = [Static.new("hi"), Dynamic.new(:action, :condition => /^[a-z]+$/)] go c assert_equal '/hi/index', execute({:action => 'index'}, {:action => 'index'}) assert_nil execute({:action => 'fox5'}, {:action => 'index'}) assert_nil execute({:action => 'something_is_up'}, {:action => 'index'}) assert_nil execute({}, {:action => 'list people'}) assert_equal '/hi/abunchofcharacter', execute({:action => 'abunchofcharacter'}, {}) assert_nil execute({}, {}) end def test_dynamic_with_default_and_regexp_condition c = [Static.new("hi"), Dynamic.new(:action, :default => 'index', :condition => /^[a-z]+$/)] go c assert_equal '/hi', execute({:action => 'index'}, {:action => 'index'}) assert_nil execute({:action => 'fox5'}, {:action => 'index'}) assert_nil execute({:action => 'something_is_up'}, {:action => 'index'}) assert_nil execute({}, {:action => 'list people'}) assert_equal '/hi/abunchofcharacter', execute({:action => 'abunchofcharacter'}, {}) assert_equal '/hi', execute({}, {}) end def test_path c = [Static.new("hi"), Path.new(:file)] go c assert_equal '/hi', execute({:file => []}, {}) assert_equal '/hi/books/agile_rails_dev.pdf', execute({:file => %w(books agile_rails_dev.pdf)}, {}) assert_equal '/hi/books/development%26whatever/agile_rails_dev.pdf', execute({:file => %w(books development&whatever agile_rails_dev.pdf)}, {}) assert_equal '/hi', execute({:file => ''}, {}) assert_equal '/hi/books/agile_rails_dev.pdf', execute({:file => 'books/agile_rails_dev.pdf'}, {}) assert_equal '/hi/books/development%26whatever/agile_rails_dev.pdf', execute({:file => 'books/development&whatever/agile_rails_dev.pdf'}, {}) end def test_controller c = [Static.new("hi"), Controller.new(:controller)] go c assert_nil execute({}, {}) assert_equal '/hi/content', execute({:controller => 'content'}, {}) assert_equal '/hi/admin/user', execute({:controller => 'admin/user'}, {}) assert_equal '/hi/content', execute({}, {:controller => 'content'}) assert_equal '/hi/admin/user', execute({}, {:controller => 'admin/user'}) end def test_controller_with_regexp c = [Static.new("hi"), Controller.new(:controller, :condition => /^admin\/.+$/)] go c assert_nil execute({}, {}) assert_nil execute({:controller => 'content'}, {}) assert_equal '/hi/admin/user', execute({:controller => 'admin/user'}, {}) assert_nil execute({}, {:controller => 'content'}) assert_equal '/hi/admin/user', execute({}, {:controller => 'admin/user'}) end def test_standard_route(time = ::RunTimeTests) c = [Controller.new(:controller), Dynamic.new(:action, :default => 'index'), Dynamic.new(:id, :default => nil)] go c # Make sure we get the right answers assert_equal('/content', execute({:action => 'index'}, {:controller => 'content', :action => 'list'})) assert_equal('/content/list', execute({:action => 'list'}, {:controller => 'content', :action => 'index'})) assert_equal('/content/show/10', execute({:action => 'show', :id => '10'}, {:controller => 'content', :action => 'list'})) assert_equal('/admin/user', execute({:action => 'index'}, {:controller => 'admin/user', :action => 'list'})) assert_equal('/admin/user/list', execute({:action => 'list'}, {:controller => 'admin/user', :action => 'index'})) assert_equal('/admin/user/show/10', execute({:action => 'show', :id => '10'}, {:controller => 'admin/user', :action => 'list'})) if time GC.start n = 1000 time = Benchmark.realtime do n.times { execute({:action => 'index'}, {:controller => 'content', :action => 'list'}) execute({:action => 'list'}, {:controller => 'content', :action => 'index'}) execute({:action => 'show', :id => '10'}, {:controller => 'content', :action => 'list'}) execute({:action => 'index'}, {:controller => 'admin/user', :action => 'list'}) execute({:action => 'list'}, {:controller => 'admin/user', :action => 'index'}) execute({:action => 'show', :id => '10'}, {:controller => 'admin/user', :action => 'list'}) } end time -= Benchmark.realtime do n.times { } end puts "\n\nGeneration:" per_url = time / (n * 6) puts "#{per_url * 1000} ms/url" puts "#{1 / per_url} urls/s\n\n" end end def test_default_route g.if(g.check_conditions(:controller => 'content', :action => 'welcome')) { go [] } assert_nil execute({:controller => 'foo', :action => 'welcome'}, {}) assert_nil execute({:controller => 'content', :action => 'elcome'}, {}) assert_nil execute({:action => 'elcome'}, {:controller => 'content'}) assert_equal '/', execute({:controller => 'content', :action => 'welcome'}, {}) assert_equal '/', execute({:action => 'welcome'}, {:controller => 'content'}) assert_equal '/', execute({:action => 'welcome', :id => '10'}, {:controller => 'content'}) end end class RouteTests < Test::Unit::TestCase def route(*args) @route = ::ActionController::Routing::Route.new(*args) unless args.empty? return @route end def rec(path, show = false) path = path.split('/') if path.is_a? String index = 0 source = route.write_recognition.to_s puts "\n\n#{source}\n\n" if show r = eval(source) r ? r.symbolize_keys : r end def gen(options, recall = nil, show = false) recall ||= options.dup expire_on = ::ActionController::Routing.expiry_hash(options, recall) hash = merged = recall.merge(options) not_expired = true source = route.write_generation.to_s puts "\n\n#{source}\n\n" if show eval(source) end def test_static route 'hello/world', :known => 'known_value', :controller => 'content', :action => 'index' assert_nil rec('hello/turn') assert_nil rec('turn/world') assert_equal( {:known => 'known_value', :controller => ::ContentController, :action => 'index'}, rec('hello/world') ) assert_nil gen(:known => 'foo') assert_nil gen({}) assert_equal '/hello/world', gen(:known => 'known_value', :controller => 'content', :action => 'index') assert_equal '/hello/world', gen(:known => 'known_value', :extra => 'hi', :controller => 'content', :action => 'index') assert_equal [:extra], route.extra_keys(:known => 'known_value', :extra => 'hi') end def test_dynamic route 'hello/:name', :controller => 'content', :action => 'show_person' assert_nil rec('hello') assert_nil rec('foo/bar') assert_equal({:controller => ::ContentController, :action => 'show_person', :name => 'rails'}, rec('hello/rails')) assert_equal({:controller => ::ContentController, :action => 'show_person', :name => 'Nicholas Seckar'}, rec('hello/Nicholas+Seckar')) assert_nil gen(:controller => 'content', :action => 'show_dude', :name => 'rails') assert_nil gen(:controller => 'content', :action => 'show_person') assert_nil gen(:controller => 'admin/user', :action => 'show_person', :name => 'rails') assert_equal '/hello/rails', gen(:controller => 'content', :action => 'show_person', :name => 'rails') assert_equal '/hello/Nicholas+Seckar', gen(:controller => 'content', :action => 'show_person', :name => 'Nicholas Seckar') end def test_typical route ':controller/:action/:id', :action => 'index', :id => nil assert_nil rec('hello') assert_nil rec('foo bar') assert_equal({:controller => ::ContentController, :action => 'index'}, rec('content')) assert_equal({:controller => ::Admin::UserController, :action => 'index'}, rec('admin/user')) assert_equal({:controller => ::Admin::UserController, :action => 'index'}, rec('admin/user/index')) assert_equal({:controller => ::Admin::UserController, :action => 'list'}, rec('admin/user/list')) assert_equal({:controller => ::Admin::UserController, :action => 'show', :id => '10'}, rec('admin/user/show/10')) assert_equal({:controller => ::ContentController, :action => 'list'}, rec('content/list')) assert_equal({:controller => ::ContentController, :action => 'show', :id => '10'}, rec('content/show/10')) assert_equal '/content', gen(:controller => 'content', :action => 'index') assert_equal '/content/list', gen(:controller => 'content', :action => 'list') assert_equal '/content/show/10', gen(:controller => 'content', :action => 'show', :id => '10') assert_equal '/admin/user', gen(:controller => 'admin/user', :action => 'index') assert_equal '/admin/user', gen(:controller => 'admin/user') assert_equal '/admin/user', gen({:controller => 'admin/user'}, {:controller => 'content', :action => 'list', :id => '10'}) assert_equal '/admin/user/show/10', gen(:controller => 'admin/user', :action => 'show', :id => '10') end end class RouteSetTests < Test::Unit::TestCase attr_reader :rs def setup @rs = ::ActionController::Routing::RouteSet.new @rs.draw {|m| m.connect ':controller/:action/:id' } ::ActionController::Routing::NamedRoutes.clear end def test_default_setup assert_equal({:controller => ::ContentController, :action => 'index'}.stringify_keys, rs.recognize_path(%w(content))) assert_equal({:controller => ::ContentController, :action => 'list'}.stringify_keys, rs.recognize_path(%w(content list))) assert_equal({:controller => ::ContentController, :action => 'show', :id => '10'}.stringify_keys, rs.recognize_path(%w(content show 10))) assert_equal({:controller => ::Admin::UserController, :action => 'show', :id => '10'}.stringify_keys, rs.recognize_path(%w(admin user show 10))) assert_equal ['/admin/user/show/10', []], rs.generate({:controller => 'admin/user', :action => 'show', :id => 10}) assert_equal ['/admin/user/show', []], rs.generate({:action => 'show'}, {:controller => 'admin/user', :action => 'list', :id => '10'}) assert_equal ['/admin/user/list/10', []], rs.generate({}, {:controller => 'admin/user', :action => 'list', :id => '10'}) assert_equal ['/admin/stuff', []], rs.generate({:controller => 'stuff'}, {:controller => 'admin/user', :action => 'list', :id => '10'}) assert_equal ['/stuff', []], rs.generate({:controller => '/stuff'}, {:controller => 'admin/user', :action => 'list', :id => '10'}) end def test_ignores_leading_slash @rs.draw {|m| m.connect '/:controller/:action/:id'} test_default_setup end def test_time_recognition n = 10000 if RunTimeTests GC.start rectime = Benchmark.realtime do n.times do rs.recognize_path(%w(content)) rs.recognize_path(%w(content list)) rs.recognize_path(%w(content show 10)) rs.recognize_path(%w(admin user)) rs.recognize_path(%w(admin user list)) rs.recognize_path(%w(admin user show 10)) end end puts "\n\nRecognition (RouteSet):" per_url = rectime / (n * 6) puts "#{per_url * 1000} ms/url" puts "#{1 / per_url} url/s\n\n" end end def test_time_generation n = 5000 if RunTimeTests GC.start pairs = [ [{:controller => 'content', :action => 'index'}, {:controller => 'content', :action => 'show'}], [{:controller => 'content'}, {:controller => 'content', :action => 'index'}], [{:controller => 'content', :action => 'list'}, {:controller => 'content', :action => 'index'}], [{:controller => 'content', :action => 'show', :id => '10'}, {:controller => 'content', :action => 'list'}], [{:controller => 'admin/user', :action => 'index'}, {:controller => 'admin/user', :action => 'show'}], [{:controller => 'admin/user'}, {:controller => 'admin/user', :action => 'index'}], [{:controller => 'admin/user', :action => 'list'}, {:controller => 'admin/user', :action => 'index'}], [{:controller => 'admin/user', :action => 'show', :id => '10'}, {:controller => 'admin/user', :action => 'list'}], ] p = nil gentime = Benchmark.realtime do n.times do pairs.each {|(a, b)| rs.generate(a, b)} end end puts "\n\nGeneration (RouteSet): (#{(n * 8)} urls)" per_url = gentime / (n * 8) puts "#{per_url * 1000} ms/url" puts "#{1 / per_url} url/s\n\n" end end def test_route_with_colon_first rs.draw do |map| map.connect '/:controller/:action/:id', :action => 'index', :id => nil map.connect ':url', :controller => 'tiny_url', :action => 'translate' end end def test_route_generating_string_literal_in_comparison_warning old_stderr = $stderr $stderr = StringIO.new rs.draw do |map| map.connect 'subscriptions/:action/:subscription_type', :controller => "subscriptions" end assert_equal "", $stderr.string ensure $stderr = old_stderr end def test_route_with_regexp_for_controller rs.draw do |map| map.connect ':controller/:admintoken/:action/:id', :controller => /admin\/.+/ map.connect ':controller/:action/:id' end assert_equal({:controller => ::Admin::UserController, :admintoken => "foo", :action => "index"}.stringify_keys, rs.recognize_path(%w(admin user foo))) assert_equal({:controller => ::ContentController, :action => "foo"}.stringify_keys, rs.recognize_path(%w(content foo))) assert_equal ['/admin/user/foo', []], rs.generate(:controller => "admin/user", :admintoken => "foo", :action => "index") assert_equal ['/content/foo',[]], rs.generate(:controller => "content", :action => "foo") end def test_basic_named_route rs.home '', :controller => 'content', :action => 'list' x = setup_for_named_route assert_equal({:controller => '/content', :action => 'list'}, x.new.send(:home_url)) end def test_named_route_with_option rs.page 'page/:title', :controller => 'content', :action => 'show_page' x = setup_for_named_route assert_equal({:controller => '/content', :action => 'show_page', :title => 'new stuff'}, x.new.send(:page_url, :title => 'new stuff')) end def test_named_route_with_default rs.page 'page/:title', :controller => 'content', :action => 'show_page', :title => 'AboutPage' x = setup_for_named_route assert_equal({:controller => '/content', :action => 'show_page', :title => 'AboutPage'}, x.new.send(:page_url)) assert_equal({:controller => '/content', :action => 'show_page', :title => 'AboutRails'}, x.new.send(:page_url, :title => "AboutRails")) end def setup_for_named_route x = Class.new x.send(:define_method, :url_for) {|x| x} x.send :include, ::ActionController::Routing::NamedRoutes x end def test_named_route_without_hash rs.draw do |map| rs.normal ':controller/:action/:id' end end def test_named_route_with_regexps rs.draw do |map| rs.article 'page/:year/:month/:day/:title', :controller => 'page', :action => 'show', :year => /^\d+$/, :month => /^\d+$/, :day => /^\d+$/ rs.connect ':controller/:action/:id' end x = setup_for_named_route assert_equal( {:controller => '/page', :action => 'show', :title => 'hi'}, x.new.send(:article_url, :title => 'hi') ) assert_equal( {:controller => '/page', :action => 'show', :title => 'hi', :day => 10, :year => 2005, :month => 6}, x.new.send(:article_url, :title => 'hi', :day => 10, :year => 2005, :month => 6) ) end def test_changing_controller assert_equal ['/admin/stuff/show/10', []], rs.generate( {:controller => 'stuff', :action => 'show', :id => 10}, {:controller => 'admin/user', :action => 'index'} ) end def test_paths_escaped rs.draw do |map| rs.path 'file/*path', :controller => 'content', :action => 'show_file' rs.connect ':controller/:action/:id' end results = rs.recognize_path %w(file hello+world how+are+you%3F) assert results, "Recognition should have succeeded" assert_equal ['hello world', 'how are you?'], results['path'] results = rs.recognize_path %w(file) assert results, "Recognition should have succeeded" assert_equal [], results['path'] end def test_non_controllers_cannot_be_matched rs.draw do rs.connect ':controller/:action/:id' end assert_nil rs.recognize_path(%w(not_a show 10)), "Shouldn't recognize non-controllers as controllers!" end def test_paths_do_not_accept_defaults assert_raises(ActionController::RoutingError) do rs.draw do |map| rs.path 'file/*path', :controller => 'content', :action => 'show_file', :path => %w(fake default) rs.connect ':controller/:action/:id' end end rs.draw do |map| rs.path 'file/*path', :controller => 'content', :action => 'show_file', :path => [] rs.connect ':controller/:action/:id' end end def test_backwards rs.draw do |map| rs.connect 'page/:id/:action', :controller => 'pages', :action => 'show' rs.connect ':controller/:action/:id' end assert_equal ['/page/20', []], rs.generate({:id => 20}, {:controller => 'pages'}) assert_equal ['/page/20', []], rs.generate(:controller => 'pages', :id => 20, :action => 'show') assert_equal ['/pages/boo', []], rs.generate(:controller => 'pages', :action => 'boo') end def test_route_with_fixnum_default rs.draw do |map| rs.connect 'page/:id', :controller => 'content', :action => 'show_page', :id => 1 rs.connect ':controller/:action/:id' end assert_equal ['/page', []], rs.generate(:controller => 'content', :action => 'show_page') assert_equal ['/page', []], rs.generate(:controller => 'content', :action => 'show_page', :id => 1) assert_equal ['/page', []], rs.generate(:controller => 'content', :action => 'show_page', :id => '1') assert_equal ['/page/10', []], rs.generate(:controller => 'content', :action => 'show_page', :id => 10) ctrl = ::ContentController assert_equal({'controller' => ctrl, 'action' => 'show_page', 'id' => 1}, rs.recognize_path(%w(page))) assert_equal({'controller' => ctrl, 'action' => 'show_page', 'id' => '1'}, rs.recognize_path(%w(page 1))) assert_equal({'controller' => ctrl, 'action' => 'show_page', 'id' => '10'}, rs.recognize_path(%w(page 10))) end def test_action_expiry assert_equal ['/content', []], rs.generate({:controller => 'content'}, {:controller => 'content', :action => 'show'}) end def test_recognition_with_uppercase_controller_name assert_equal({'controller' => ::ContentController, 'action' => 'index'}, rs.recognize_path(%w(Content))) assert_equal({'controller' => ::ContentController, 'action' => 'list'}, rs.recognize_path(%w(Content list))) assert_equal({'controller' => ::ContentController, 'action' => 'show', 'id' => '10'}, rs.recognize_path(%w(Content show 10))) assert_equal({'controller' => ::Admin::NewsFeedController, 'action' => 'index'}, rs.recognize_path(%w(Admin NewsFeed))) assert_equal({'controller' => ::Admin::NewsFeedController, 'action' => 'index'}, rs.recognize_path(%w(Admin News_Feed))) end def test_both_requirement_and_optional rs.draw do rs.blog('test/:year', :controller => 'post', :action => 'show', :defaults => { :year => nil }, :requirements => { :year => /\d{4}/ } ) rs.connect ':controller/:action/:id' end assert_equal ['/test', []], rs.generate(:controller => 'post', :action => 'show') assert_equal ['/test', []], rs.generate(:controller => 'post', :action => 'show', :year => nil) x = setup_for_named_route assert_equal({:controller => '/post', :action => 'show'}, x.new.send(:blog_url)) end def test_set_to_nil_forgets rs.draw do rs.connect 'pages/:year/:month/:day', :controller => 'content', :action => 'list_pages', :month => nil, :day => nil rs.connect ':controller/:action/:id' end assert_equal ['/pages/2005', []], rs.generate(:controller => 'content', :action => 'list_pages', :year => 2005) assert_equal ['/pages/2005/6', []], rs.generate(:controller => 'content', :action => 'list_pages', :year => 2005, :month => 6) assert_equal ['/pages/2005/6/12', []], rs.generate(:controller => 'content', :action => 'list_pages', :year => 2005, :month => 6, :day => 12) assert_equal ['/pages/2005/6/4', []], rs.generate({:day => 4}, {:controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12'}) assert_equal ['/pages/2005/6', []], rs.generate({:day => nil}, {:controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12'}) assert_equal ['/pages/2005', []], rs.generate({:day => nil, :month => nil}, {:controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12'}) end def test_url_with_no_action_specified rs.draw do rs.connect '', :controller => 'content' rs.connect ':controller/:action/:id' end assert_equal ['/', []], rs.generate(:controller => 'content', :action => 'index') assert_equal ['/', []], rs.generate(:controller => 'content') end def test_named_url_with_no_action_specified rs.draw do rs.root '', :controller => 'content' rs.connect ':controller/:action/:id' end assert_equal ['/', []], rs.generate(:controller => 'content', :action => 'index') assert_equal ['/', []], rs.generate(:controller => 'content') x = setup_for_named_route assert_equal({:controller => '/content', :action => 'index'}, x.new.send(:root_url)) end def test_url_generated_when_forgetting_action [{:controller => 'content', :action => 'index'}, {:controller => 'content'}].each do |hash| rs.draw do rs.root '', hash rs.connect ':controller/:action/:id' end assert_equal ['/', []], rs.generate({:action => nil}, {:controller => 'content', :action => 'hello'}) assert_equal ['/', []], rs.generate({:controller => 'content'}) assert_equal ['/content/hi', []], rs.generate({:controller => 'content', :action => 'hi'}) end end def test_named_route_method rs.draw do assert_raises(ArgumentError) { rs.categories 'categories', :controller => 'content', :action => 'categories' } rs.named_route :categories, 'categories', :controller => 'content', :action => 'categories' rs.connect ':controller/:action/:id' end assert_equal ['/categories', []], rs.generate(:controller => 'content', :action => 'categories') assert_equal ['/content/hi', []], rs.generate({:controller => 'content', :action => 'hi'}) end def test_named_route_helper_array test_named_route_method assert_equal [:categories_url, :hash_for_categories_url], ::ActionController::Routing::NamedRoutes::Helpers end def test_nil_defaults rs.draw do rs.connect 'journal', :controller => 'content', :action => 'list_journal', :date => nil, :user_id => nil rs.connect ':controller/:action/:id' end assert_equal ['/journal', []], rs.generate(:controller => 'content', :action => 'list_journal', :date => nil, :user_id => nil) end end class ControllerComponentTest < Test::Unit::TestCase def test_traverse_to_controller_should_not_load_arbitrary_files load_path = $:.dup base = File.dirname(File.dirname(File.expand_path(__FILE__))) $: << File.join(base, 'fixtures') Object.send :const_set, :RAILS_ROOT, File.join(base, 'fixtures/application_root') assert_equal nil, ActionController::Routing::ControllerComponent.traverse_to_controller(%w(dont_load pretty please)) ensure $:[0..-1] = load_path Object.send :remove_const, :RAILS_ROOT end def test_traverse_should_not_trip_on_non_module_constants assert_equal nil, ActionController::Routing::ControllerComponent.traverse_to_controller(%w(admin some_constant a)) end # This is evil, but people do it. def test_traverse_to_controller_should_pass_thru_classes load_path = $:.dup base = File.dirname(File.dirname(File.expand_path(__FILE__))) $: << File.join(base, 'fixtures') $: << File.join(base, 'fixtures/application_root/app/controllers') $: << File.join(base, 'fixtures/application_root/app/models') Object.send :const_set, :RAILS_ROOT, File.join(base, 'fixtures/application_root') pair = ActionController::Routing::ControllerComponent.traverse_to_controller(%w(a_class_that_contains_a_controller poorly_placed)) # Make sure the container class was loaded properly assert defined?(AClassThatContainsAController) assert_kind_of Class, AClassThatContainsAController assert_equal :you_know_it, AClassThatContainsAController.is_special? # Make sure the controller was too assert_kind_of Array, pair assert_equal 2, pair[1] klass = pair.first assert_kind_of Class, klass assert_equal :decidedly_so, klass.is_evil? assert klass.ancestors.include?(ActionController::Base) assert defined?(AClassThatContainsAController::PoorlyPlacedController) assert_equal klass, AClassThatContainsAController::PoorlyPlacedController ensure $:[0..-1] = load_path Object.send :remove_const, :RAILS_ROOT end def test_traverse_to_nested_controller load_path = $:.dup base = File.dirname(File.dirname(File.expand_path(__FILE__))) $: << File.join(base, 'fixtures') $: << File.join(base, 'fixtures/application_root/app/controllers') Object.send :const_set, :RAILS_ROOT, File.join(base, 'fixtures/application_root') pair = ActionController::Routing::ControllerComponent.traverse_to_controller(%w(module_that_holds_controllers nested)) assert_not_equal nil, pair # Make sure that we created a module for the dir assert defined?(ModuleThatHoldsControllers) assert_kind_of Module, ModuleThatHoldsControllers # Make sure the controller is ok assert_kind_of Array, pair assert_equal 2, pair[1] klass = pair.first assert_kind_of Class, klass assert klass.ancestors.include?(ActionController::Base) assert defined?(ModuleThatHoldsControllers::NestedController) assert_equal klass, ModuleThatHoldsControllers::NestedController ensure $:[0..-1] = load_path Object.send :remove_const, :RAILS_ROOT end end end require File.join(File.dirname(__FILE__), '..', 'abstract_unit') module TestFileUtils def file_name() File.basename(__FILE__) end def file_path() File.expand_path(__FILE__) end def file_data() File.open(file_path, 'rb') { |f| f.read } end end class SendFileController < ActionController::Base include TestFileUtils layout "layouts/standard" # to make sure layouts don't interfere attr_writer :options def options() @options ||= {} end def file() send_file(file_path, options) end def data() send_data(file_data, options) end def rescue_action(e) raise end end SendFileController.template_root = File.dirname(__FILE__) + "/../fixtures/" class SendFileTest < Test::Unit::TestCase include TestFileUtils def setup @controller = SendFileController.new @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new end def test_file_nostream @controller.options = { :stream => false } response = nil assert_nothing_raised { response = process('file') } assert_not_nil response assert_kind_of String, response.body assert_equal file_data, response.body end def test_file_stream response = nil assert_nothing_raised { response = process('file') } assert_not_nil response assert_kind_of Proc, response.body require 'stringio' output = StringIO.new output.binmode assert_nothing_raised { response.body.call(response, output) } assert_equal file_data, output.string end def test_data response = nil assert_nothing_raised { response = process('data') } assert_not_nil response assert_kind_of String, response.body assert_equal file_data, response.body end # Test that send_file_headers! is setting the correct HTTP headers. def test_send_file_headers! options = { :length => 1, :type => 'type', :disposition => 'disposition', :filename => 'filename' } # Do it a few times: the resulting headers should be identical # no matter how many times you send with the same options. # Test resolving Ticket #458. @controller.headers = {} @controller.send(:send_file_headers!, options) @controller.send(:send_file_headers!, options) @controller.send(:send_file_headers!, options) h = @controller.headers assert_equal 1, h['Content-Length'] assert_equal 'type', h['Content-Type'] assert_equal 'disposition; filename="filename"', h['Content-Disposition'] assert_equal 'binary', h['Content-Transfer-Encoding'] # test overriding Cache-Control: no-cache header to fix IE open/save dialog @controller.headers = { 'Cache-Control' => 'no-cache' } @controller.send(:send_file_headers!, options) h = @controller.headers assert_equal 'private', h['Cache-Control'] end %w(file data).each do |method| define_method "test_send_#{method}_status" do @controller.options = { :stream => false, :status => 500 } assert_nothing_raised { assert_not_nil process(method) } assert_equal '500', @controller.headers['Status'] end define_method "test_default_send_#{method}_status" do @controller.options = { :stream => false } assert_nothing_raised { assert_not_nil process(method) } assert_equal ActionController::Base::DEFAULT_RENDER_STATUS_CODE, @controller.headers['Status'] end end end require File.dirname(__FILE__) + '/../abstract_unit' class SessionManagementTest < Test::Unit::TestCase class SessionOffController < ActionController::Base session :off def show render_text "done" end def tell render_text "done" end end class TestController < ActionController::Base session :off, :only => :show session :session_secure => true, :except => :show session :off, :only => :conditional, :if => Proc.new { |r| r.parameters[:ws] } def show render_text "done" end def tell render_text "done" end def conditional render_text ">>>#{params[:ws]}<<<" end end class SpecializedController < SessionOffController session :disabled => false, :only => :something def something render_text "done" end def another render_text "done" end end def setup @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new end def test_session_off_globally @controller = SessionOffController.new get :show assert_equal false, @request.session_options get :tell assert_equal false, @request.session_options end def test_session_off_conditionally @controller = TestController.new get :show assert_equal false, @request.session_options get :tell assert_instance_of Hash, @request.session_options assert @request.session_options[:session_secure] end def test_controller_specialization_overrides_settings @controller = SpecializedController.new get :something assert_instance_of Hash, @request.session_options get :another assert_equal false, @request.session_options end def test_session_off_with_if @controller = TestController.new get :conditional assert_instance_of Hash, @request.session_options get :conditional, :ws => "ws" assert_equal false, @request.session_options end def test_session_store_setting ActionController::Base.session_store = :drb_store assert_equal CGI::Session::DRbStore, ActionController::Base.session_store if Object.const_defined?(:ActiveRecord) ActionController::Base.session_store = :active_record_store assert_equal CGI::Session::ActiveRecordStore, ActionController::Base.session_store end end end require File.dirname(__FILE__) + '/../abstract_unit' require File.dirname(__FILE__) + '/fake_controllers' class TestTest < Test::Unit::TestCase class TestController < ActionController::Base def set_flash flash["test"] = ">#{flash["test"]}<" render :text => 'ignore me' end def render_raw_post raise Test::Unit::AssertionFailedError, "#raw_post is blank" if request.raw_post.blank? render :text => request.raw_post end def test_params render :text => params.inspect end def test_uri render :text => request.request_uri end def test_html_output render :text => <
    • hello
    • goodbye
    Name:
    HTML end def test_only_one_param render :text => (params[:left] && params[:right]) ? "EEP, Both here!" : "OK" end def test_remote_addr render :text => (request.remote_addr || "not specified") end def test_file_upload render :text => params[:file].size end def redirect_to_symbol redirect_to :generate_url, :id => 5 end private def rescue_action(e) raise e end def generate_url(opts) url_for(opts.merge(:action => "test_uri")) end end def setup @controller = TestController.new @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new ActionController::Routing::Routes.reload end def teardown ActionController::Routing::Routes.reload end def test_raw_post_handling params = {:page => {:name => 'page name'}, 'some key' => 123} get :render_raw_post, params.dup raw_post = params.map {|k,v| [CGI::escape(k.to_s), CGI::escape(v.to_s)].join('=')}.sort.join('&') assert_equal raw_post, @response.body end def test_process_without_flash process :set_flash assert_equal '><', flash['test'] end def test_process_with_flash process :set_flash, nil, nil, { "test" => "value" } assert_equal '>value<', flash['test'] end def test_process_with_request_uri_with_no_params process :test_uri assert_equal "/test_test/test/test_uri", @response.body end def test_process_with_request_uri_with_params process :test_uri, :id => 7 assert_equal "/test_test/test/test_uri/7", @response.body end def test_process_with_request_uri_with_params_with_explicit_uri @request.set_REQUEST_URI "/explicit/uri" process :test_uri, :id => 7 assert_equal "/explicit/uri", @response.body end def test_multiple_calls process :test_only_one_param, :left => true assert_equal "OK", @response.body process :test_only_one_param, :right => true assert_equal "OK", @response.body end def test_assert_tag_tag process :test_html_output # there is a 'form' tag assert_tag :tag => 'form' # there is not an 'hr' tag assert_no_tag :tag => 'hr' end def test_assert_tag_attributes process :test_html_output # there is a tag with an 'id' of 'bar' assert_tag :attributes => { :id => "bar" } # there is no tag with a 'name' of 'baz' assert_no_tag :attributes => { :name => "baz" } end def test_assert_tag_parent process :test_html_output # there is a tag with a parent 'form' tag assert_tag :parent => { :tag => "form" } # there is no tag with a parent of 'input' assert_no_tag :parent => { :tag => "input" } end def test_assert_tag_child process :test_html_output # there is a tag with a child 'input' tag assert_tag :child => { :tag => "input" } # there is no tag with a child 'strong' tag assert_no_tag :child => { :tag => "strong" } end def test_assert_tag_ancestor process :test_html_output # there is a 'li' tag with an ancestor having an id of 'foo' assert_tag :ancestor => { :attributes => { :id => "foo" } }, :tag => "li" # there is no tag of any kind with an ancestor having an href matching 'foo' assert_no_tag :ancestor => { :attributes => { :href => /foo/ } } end def test_assert_tag_descendant process :test_html_output # there is a tag with a decendant 'li' tag assert_tag :descendant => { :tag => "li" } # there is no tag with a descendant 'html' tag assert_no_tag :descendant => { :tag => "html" } end def test_assert_tag_sibling process :test_html_output # there is a tag with a sibling of class 'item' assert_tag :sibling => { :attributes => { :class => "item" } } # there is no tag with a sibling 'ul' tag assert_no_tag :sibling => { :tag => "ul" } end def test_assert_tag_after process :test_html_output # there is a tag following a sibling 'div' tag assert_tag :after => { :tag => "div" } # there is no tag following a sibling tag with id 'bar' assert_no_tag :after => { :attributes => { :id => "bar" } } end def test_assert_tag_before process :test_html_output # there is a tag preceeding a tag with id 'bar' assert_tag :before => { :attributes => { :id => "bar" } } # there is no tag preceeding a 'form' tag assert_no_tag :before => { :tag => "form" } end def test_assert_tag_children_count process :test_html_output # there is a tag with 2 children assert_tag :children => { :count => 2 } # there is no tag with 4 children assert_no_tag :children => { :count => 4 } end def test_assert_tag_children_less_than process :test_html_output # there is a tag with less than 5 children assert_tag :children => { :less_than => 5 } # there is no 'ul' tag with less than 2 children assert_no_tag :children => { :less_than => 2 }, :tag => "ul" end def test_assert_tag_children_greater_than process :test_html_output # there is a 'body' tag with more than 1 children assert_tag :children => { :greater_than => 1 }, :tag => "body" # there is no tag with more than 10 children assert_no_tag :children => { :greater_than => 10 } end def test_assert_tag_children_only process :test_html_output # there is a tag containing only one child with an id of 'foo' assert_tag :children => { :count => 1, :only => { :attributes => { :id => "foo" } } } # there is no tag containing only one 'li' child assert_no_tag :children => { :count => 1, :only => { :tag => "li" } } end def test_assert_tag_content process :test_html_output # the output contains the string "Name" assert_tag :content => "Name" # the output does not contain the string "test" assert_no_tag :content => "test" end def test_assert_tag_multiple process :test_html_output # there is a 'div', id='bar', with an immediate child whose 'action' # attribute matches the regexp /somewhere/. assert_tag :tag => "div", :attributes => { :id => "bar" }, :child => { :attributes => { :action => /somewhere/ } } # there is no 'div', id='foo', with a 'ul' child with more than # 2 "li" children. assert_no_tag :tag => "div", :attributes => { :id => "foo" }, :child => { :tag => "ul", :children => { :greater_than => 2, :only => { :tag => "li" } } } end def test_assert_tag_children_without_content process :test_html_output # there is a form tag with an 'input' child which is a self closing tag assert_tag :tag => "form", :children => { :count => 1, :only => { :tag => "input" } } # the body tag has an 'a' child which in turn has an 'img' child assert_tag :tag => "body", :children => { :count => 1, :only => { :tag => "a", :children => { :count => 1, :only => { :tag => "img" } } } } end def test_assert_generates assert_generates 'controller/action/5', :controller => 'controller', :action => 'action', :id => '5' end def test_assert_routing assert_routing 'content', :controller => 'content', :action => 'index' end def test_assert_routing_in_module assert_routing 'admin/user', :controller => 'admin/user', :action => 'index' end def test_params_passing get :test_params, :page => {:name => "Page name", :month => '4', :year => '2004', :day => '6'} parsed_params = eval(@response.body) assert_equal( {'controller' => 'test_test/test', 'action' => 'test_params', 'page' => {'name' => "Page name", 'month' => '4', 'year' => '2004', 'day' => '6'}}, parsed_params ) end def test_id_converted_to_string get :test_params, :id => 20, :foo => Object.new assert_kind_of String, @request.path_parameters['id'] end def test_array_path_parameter_handled_properly with_routing do |set| set.draw do set.connect 'file/*path', :controller => 'test_test/test', :action => 'test_params' set.connect ':controller/:action/:id' end get :test_params, :path => ['hello', 'world'] assert_equal ['hello', 'world'], @request.path_parameters['path'] assert_equal 'hello/world', @request.path_parameters['path'].to_s end end def test_assert_realistic_path_parameters get :test_params, :id => 20, :foo => Object.new # All elements of path_parameters should use string keys @request.path_parameters.keys.each do |key| assert_kind_of String, key end end def test_with_routing_places_routes_back assert ActionController::Routing::Routes routes_id = ActionController::Routing::Routes.object_id begin with_routing { raise 'fail' } fail 'Should not be here.' rescue RuntimeError end assert ActionController::Routing::Routes assert_equal routes_id, ActionController::Routing::Routes.object_id end def test_remote_addr get :test_remote_addr assert_equal "0.0.0.0", @response.body @request.remote_addr = "192.0.0.1" get :test_remote_addr assert_equal "192.0.0.1", @response.body end def test_header_properly_reset_after_remote_http_request xhr :get, :test_params assert_nil @request.env['HTTP_X_REQUESTED_WITH'] end def test_header_properly_reset_after_get_request get :test_params @request.recycle! assert_nil @request.instance_variable_get("@request_method") end %w(controller response request).each do |variable| %w(get post put delete head process).each do |method| define_method("test_#{variable}_missing_for_#{method}_raises_error") do remove_instance_variable "@#{variable}" begin send(method, :test_remote_addr) assert false, "expected RuntimeError, got nothing" rescue RuntimeError => error assert true assert_match %r{@#{variable} is nil}, error.message rescue => error assert false, "expected RuntimeError, got #{error.class}" end end end end FILES_DIR = File.dirname(__FILE__) + '/../fixtures/multipart' def test_test_uploaded_file filename = 'mona_lisa.jpg' path = "#{FILES_DIR}/#{filename}" content_type = 'image/png' file = ActionController::TestUploadedFile.new(path, content_type) assert_equal filename, file.original_filename assert_equal content_type, file.content_type assert_equal file.path, file.local_path assert_equal File.read(path), file.read end def test_fixture_file_upload post :test_file_upload, :file => fixture_file_upload(FILES_DIR + "/mona_lisa.jpg", "image/jpg") assert_equal 159528, @response.body end def test_test_uploaded_file_exception_when_file_doesnt_exist assert_raise(RuntimeError) { ActionController::TestUploadedFile.new('non_existent_file') } end def test_assert_redirected_to_symbol get :redirect_to_symbol assert_redirected_to :generate_url end end require File.dirname(__FILE__) + '/../abstract_unit' class UrlRewriterTests < Test::Unit::TestCase def setup @request = ActionController::TestRequest.new @params = {} @rewriter = ActionController::UrlRewriter.new(@request, @params) end def test_simple_build_query_string assert_query_equal '?x=1&y=2', @rewriter.send(:build_query_string, :x => '1', :y => '2') end def test_convert_ints_build_query_string assert_query_equal '?x=1&y=2', @rewriter.send(:build_query_string, :x => 1, :y => 2) end def test_escape_spaces_build_query_string assert_query_equal '?x=hello+world&y=goodbye+world', @rewriter.send(:build_query_string, :x => 'hello world', :y => 'goodbye world') end def test_expand_array_build_query_string assert_query_equal '?x[]=1&x[]=2', @rewriter.send(:build_query_string, :x => [1, 2]) end def test_escape_spaces_build_query_string_selected_keys assert_query_equal '?x=hello+world', @rewriter.send(:build_query_string, {:x => 'hello world', :y => 'goodbye world'}, [:x]) end def test_overwrite_params @params[:controller] = 'hi' @params[:action] = 'bye' @params[:id] = '2' assert_equal '/hi/hi/2', @rewriter.rewrite(:only_path => true, :overwrite_params => {:action => 'hi'}) u = @rewriter.rewrite(:only_path => false, :overwrite_params => {:action => 'hi'}) assert_match %r(/hi/hi/2$), u end private def split_query_string(str) [str[0].chr] + str[1..-1].split(/&/).sort end def assert_query_equal(q1, q2) assert_equal(split_query_string(q1), split_query_string(q2)) end end require File.dirname(__FILE__) + '/../abstract_unit' class VerificationTest < Test::Unit::TestCase class TestController < ActionController::Base verify :only => :guarded_one, :params => "one", :redirect_to => { :action => "unguarded" } verify :only => :guarded_two, :params => %w( one two ), :redirect_to => { :action => "unguarded" } verify :only => :guarded_with_flash, :params => "one", :add_flash => { "notice" => "prereqs failed" }, :redirect_to => { :action => "unguarded" } verify :only => :guarded_in_session, :session => "one", :redirect_to => { :action => "unguarded" } verify :only => [:multi_one, :multi_two], :session => %w( one two ), :redirect_to => { :action => "unguarded" } verify :only => :guarded_by_method, :method => :post, :redirect_to => { :action => "unguarded" } verify :only => :guarded_by_xhr, :xhr => true, :redirect_to => { :action => "unguarded" } verify :only => :guarded_by_not_xhr, :xhr => false, :redirect_to => { :action => "unguarded" } before_filter :unconditional_redirect, :only => :two_redirects verify :only => :two_redirects, :method => :post, :redirect_to => { :action => "unguarded" } verify :only => :must_be_post, :method => :post, :render => { :status => 500, :text => "Must be post"} def guarded_one render :text => "#{@params["one"]}" end def guarded_with_flash render :text => "#{@params["one"]}" end def guarded_two render :text => "#{@params["one"]}:#{@params["two"]}" end def guarded_in_session render :text => "#{@session["one"]}" end def multi_one render :text => "#{@session["one"]}:#{@session["two"]}" end def multi_two render :text => "#{@session["two"]}:#{@session["one"]}" end def guarded_by_method render :text => "#{@request.method}" end def guarded_by_xhr render :text => "#{@request.xhr?}" end def guarded_by_not_xhr render :text => "#{@request.xhr?}" end def unguarded render :text => "#{@params["one"]}" end def two_redirects render :nothing => true end def must_be_post render :text => "Was a post!" end protected def rescue_action(e) raise end def unconditional_redirect redirect_to :action => "unguarded" end end def setup @controller = TestController.new @request = ActionController::TestRequest.new @response = ActionController::TestResponse.new end def test_guarded_one_with_prereqs get :guarded_one, :one => "here" assert_equal "here", @response.body end def test_guarded_one_without_prereqs get :guarded_one assert_redirected_to :action => "unguarded" end def test_guarded_with_flash_with_prereqs get :guarded_with_flash, :one => "here" assert_equal "here", @response.body assert_flash_empty end def test_guarded_with_flash_without_prereqs get :guarded_with_flash assert_redirected_to :action => "unguarded" assert_flash_equal "prereqs failed", "notice" end def test_guarded_two_with_prereqs get :guarded_two, :one => "here", :two => "there" assert_equal "here:there", @response.body end def test_guarded_two_without_prereqs_one get :guarded_two, :two => "there" assert_redirected_to :action => "unguarded" end def test_guarded_two_without_prereqs_two get :guarded_two, :one => "here" assert_redirected_to :action => "unguarded" end def test_guarded_two_without_prereqs_both get :guarded_two assert_redirected_to :action => "unguarded" end def test_unguarded_with_params get :unguarded, :one => "here" assert_equal "here", @response.body end def test_unguarded_without_params get :unguarded assert_equal "", @response.body end def test_guarded_in_session_with_prereqs get :guarded_in_session, {}, "one" => "here" assert_equal "here", @response.body end def test_guarded_in_session_without_prereqs get :guarded_in_session assert_redirected_to :action => "unguarded" end def test_multi_one_with_prereqs get :multi_one, {}, "one" => "here", "two" => "there" assert_equal "here:there", @response.body end def test_multi_one_without_prereqs get :multi_one assert_redirected_to :action => "unguarded" end def test_multi_two_with_prereqs get :multi_two, {}, "one" => "here", "two" => "there" assert_equal "there:here", @response.body end def test_multi_two_without_prereqs get :multi_two assert_redirected_to :action => "unguarded" end def test_guarded_by_method_with_prereqs post :guarded_by_method assert_equal "post", @response.body end def test_guarded_by_method_without_prereqs get :guarded_by_method assert_redirected_to :action => "unguarded" end def test_guarded_by_xhr_with_prereqs xhr :post, :guarded_by_xhr assert_equal "true", @response.body end def test_guarded_by_xhr_without_prereqs get :guarded_by_xhr assert_redirected_to :action => "unguarded" end def test_guarded_by_not_xhr_with_prereqs get :guarded_by_not_xhr assert_equal "false", @response.body end def test_guarded_by_not_xhr_without_prereqs xhr :post, :guarded_by_not_xhr assert_redirected_to :action => "unguarded" end def test_guarded_post_and_calls_render_succeeds post :must_be_post assert_equal "Was a post!", @response.body end def test_guarded_post_and_calls_render_fails get :must_be_post assert_response 500 assert_equal "Must be post", @response.body end def test_second_redirect assert_nothing_raised { get :two_redirects } end end require File.dirname(__FILE__) + '/../abstract_unit' require 'stringio' class WebServiceTest < Test::Unit::TestCase class MockCGI < CGI #:nodoc: attr_accessor :stdinput, :stdoutput, :env_table def initialize(env, data = '') self.env_table = env self.stdinput = StringIO.new(data) self.stdoutput = StringIO.new super() end end class TestController < ActionController::Base session :off def assign_parameters if params[:full] render :text => dump_params_keys else render :text => (params.keys - ['controller', 'action']).sort.join(", ") end end def dump_params_keys(hash=params) hash.keys.sort.inject("") do |s, k| value = hash[k] value = Hash === value ? "(#{dump_params_keys(value)})" : "" s << ", " unless s.empty? s << "#{k}#{value}" end end def rescue_action(e) raise end end def setup @controller = TestController.new ActionController::Base.param_parsers.clear ActionController::Base.param_parsers[Mime::XML] = :xml_node end def test_check_parameters process('GET') assert_equal '', @controller.response.body end def test_post_xml process('POST', 'application/xml', 'content...') assert_equal 'entry', @controller.response.body assert @controller.params.has_key?(:entry) assert_equal 'content...', @controller.params["entry"].summary.node_value assert_equal 'true', @controller.params["entry"]['attributed'] end def test_put_xml process('PUT', 'application/xml', 'content...') assert_equal 'entry', @controller.response.body assert @controller.params.has_key?(:entry) assert_equal 'content...', @controller.params["entry"].summary.node_value assert_equal 'true', @controller.params["entry"]['attributed'] end def test_register_and_use_yaml ActionController::Base.param_parsers[Mime::YAML] = Proc.new { |d| YAML.load(d) } process('POST', 'application/x-yaml', {"entry" => "loaded from yaml"}.to_yaml) assert_equal 'entry', @controller.response.body assert @controller.params.has_key?(:entry) assert_equal 'loaded from yaml', @controller.params["entry"] end def test_register_and_use_yaml_as_symbol ActionController::Base.param_parsers[Mime::YAML] = :yaml process('POST', 'application/x-yaml', {"entry" => "loaded from yaml"}.to_yaml) assert_equal 'entry', @controller.response.body assert @controller.params.has_key?(:entry) assert_equal 'loaded from yaml', @controller.params["entry"] end def test_register_and_use_xml_simple ActionController::Base.param_parsers[Mime::XML] = Proc.new { |data| XmlSimple.xml_in(data, 'ForceArray' => false) } process('POST', 'application/xml', 'content...SimpleXml' ) assert_equal 'summary, title', @controller.response.body assert @controller.params.has_key?(:summary) assert @controller.params.has_key?(:title) assert_equal 'content...', @controller.params["summary"] assert_equal 'SimpleXml', @controller.params["title"] end def test_use_xml_ximple_with_empty_request ActionController::Base.param_parsers[Mime::XML] = :xml_simple assert_nothing_raised { process('POST', 'application/xml', "") } assert_equal "", @controller.response.body end def test_deprecated_request_methods process('POST', 'application/x-yaml') assert_equal Mime::YAML, @controller.request.content_type assert_equal true, @controller.request.post? assert_equal :yaml, @controller.request.post_format assert_equal true, @controller.request.yaml_post? assert_equal false, @controller.request.xml_post? end def test_dasherized_keys_as_xml ActionController::Base.param_parsers[Mime::XML] = :xml_simple process('POST', 'application/xml', "\n...\n", true) assert_equal 'action, controller, first_key(sub_key), full', @controller.response.body assert_equal "...", @controller.params[:first_key][:sub_key] end def test_typecast_as_xml ActionController::Base.param_parsers[Mime::XML] = :xml_simple process('POST', 'application/xml', <<-XML) 15 false true 2005-03-17 2005-03-17T21:41:07Z unparsed 1 hello 1974-07-25 XML params = @controller.params assert_equal 15, params[:data][:a] assert_equal false, params[:data][:b] assert_equal true, params[:data][:c] assert_equal Date.new(2005,3,17), params[:data][:d] assert_equal Time.utc(2005,3,17,21,41,7), params[:data][:e] assert_equal "unparsed", params[:data][:f] assert_equal [1, "hello", Date.new(1974,7,25)], params[:data][:g] end def test_entities_unescaped_as_xml_simple ActionController::Base.param_parsers[Mime::XML] = :xml_simple process('POST', 'application/xml', <<-XML) <foo "bar's" & friends> XML assert_equal %(), @controller.params[:data] end def test_dasherized_keys_as_yaml ActionController::Base.param_parsers[Mime::YAML] = :yaml process('POST', 'application/x-yaml', "---\nfirst-key:\n sub-key: ...\n", true) assert_equal 'action, controller, first_key(sub_key), full', @controller.response.body assert_equal "...", @controller.params[:first_key][:sub_key] end def test_typecast_as_yaml ActionController::Base.param_parsers[Mime::YAML] = :yaml process('POST', 'application/x-yaml', <<-YAML) --- data: a: 15 b: false c: true d: 2005-03-17 e: 2005-03-17T21:41:07Z f: unparsed g: - 1 - hello - 1974-07-25 YAML params = @controller.params assert_equal 15, params[:data][:a] assert_equal false, params[:data][:b] assert_equal true, params[:data][:c] assert_equal Date.new(2005,3,17), params[:data][:d] assert_equal Time.utc(2005,3,17,21,41,7), params[:data][:e] assert_equal "unparsed", params[:data][:f] assert_equal [1, "hello", Date.new(1974,7,25)], params[:data][:g] end private def process(verb, content_type = 'application/x-www-form-urlencoded', data = '', full=false) cgi = MockCGI.new({ 'REQUEST_METHOD' => verb, 'CONTENT_TYPE' => content_type, 'QUERY_STRING' => "action=assign_parameters&controller=webservicetest/test#{"&full=1" if full}", "REQUEST_URI" => "/", "HTTP_HOST" => 'testdomain.com', "CONTENT_LENGTH" => data.size, "SERVER_PORT" => "80", "HTTPS" => "off"}, data) @controller.send(:process, ActionController::CgiRequest.new(cgi, {}), ActionController::CgiResponse.new(cgi)) end end class XmlNodeTest < Test::Unit::TestCase def test_all xn = XmlNode.from_xml(%{ With O'Reilly and Adaptive Path Staying at the Savoy } ) assert_equal 'UTF-8', xn.node.document.encoding assert_equal '1.0', xn.node.document.version assert_equal 'true', xn['success'] assert_equal 'response', xn.node_name assert_equal 'Ajax Summit', xn.page['title'] assert_equal '1133', xn.page['id'] assert_equal "With O'Reilly and Adaptive Path", xn.page.description.node_value assert_equal nil, xn.nonexistent assert_equal "Staying at the Savoy", xn.page.notes.note.node_value.strip assert_equal 'Technology', xn.page.tags.tag[0]['name'] assert_equal 'Travel', xn.page.tags.tag[1][:name] matches = xn.xpath('//@id').map{ |id| id.to_i } assert_equal [4, 5, 1020, 1133], matches.sort matches = xn.xpath('//tag').map{ |tag| tag['name'] } assert_equal ['Technology', 'Travel'], matches.sort assert_equal "Ajax Summit", xn.page['title'] xn.page['title'] = 'Ajax Summit V2' assert_equal "Ajax Summit V2", xn.page['title'] assert_equal "Staying at the Savoy", xn.page.notes.note.node_value.strip xn.page.notes.note.node_value = "Staying at the Ritz" assert_equal "Staying at the Ritz", xn.page.notes.note.node_value.strip assert_equal '5', xn.page.tags.tag[1][:id] xn.page.tags.tag[1]['id'] = '7' assert_equal '7', xn.page.tags.tag[1]['id'] end def test_small_entry node = XmlNode.from_xml('hi') assert_equal 'hi', node.node_value end end class AClassThatContainsAController::PoorlyPlacedController < ActionController::Base def self.is_evil? :decidedly_so end endclass ModuleThatHoldsControllers::NestedController < ActionController::Base endclass AClassThatContainsAController #often < ActiveRecord::Base def self.is_special? :you_know_it end endclass Company < ActiveRecord::Base attr_protected :rating set_sequence_name :companies_nonstd_seq validates_presence_of :name def validate errors.add('rating', 'rating should not be 2') if rating == 2 end endclass Developer < ActiveRecord::Base has_and_belongs_to_many :projects end class DeVeLoPeR < ActiveRecord::Base set_table_name "developers" end # see routing/controller component tests raise Exception, "I should never be loaded"module AbcHelper def bare_a() end def bare_b() end def bare_c() end end module Fun::GamesHelper def stratego() "Iz guuut!" end endmodule Fun::PDFHelper def foobar() 'baz' end end class Project < ActiveRecord::Base has_and_belongs_to_many :developers, :uniq => true end class Reply < ActiveRecord::Base belongs_to :topic, :include => [:replies] validates_presence_of :content end class Topic < ActiveRecord::Base has_many :replies, :include => [:user], :dependent => true endrequire 'test/unit' require File.dirname(__FILE__) + '/../../lib/action_view/helpers/date_helper' require File.dirname(__FILE__) + '/../../lib/action_view/helpers/form_helper' require File.dirname(__FILE__) + '/../../lib/action_view/helpers/text_helper' require File.dirname(__FILE__) + '/../../lib/action_view/helpers/tag_helper' require File.dirname(__FILE__) + '/../../lib/action_view/helpers/url_helper' require File.dirname(__FILE__) + '/../../lib/action_view/helpers/form_tag_helper' # require File.dirname(__FILE__) + '/../../lib/action_view/helpers/active_record_helper' class ActiveRecordHelperTest < Test::Unit::TestCase include ActionView::Helpers::FormHelper include ActionView::Helpers::ActiveRecordHelper include ActionView::Helpers::TextHelper include ActionView::Helpers::TagHelper include ActionView::Helpers::UrlHelper include ActionView::Helpers::FormTagHelper silence_warnings do Post = Struct.new("Post", :title, :author_name, :body, :secret, :written_on) Post.class_eval do alias_method :title_before_type_cast, :title unless respond_to?(:title_before_type_cast) alias_method :body_before_type_cast, :body unless respond_to?(:body_before_type_cast) alias_method :author_name_before_type_cast, :author_name unless respond_to?(:author_name_before_type_cast) end Column = Struct.new("Column", :type, :name, :human_name) end def setup @post = Post.new def @post.errors Class.new { def on(field) field == "author_name" || field == "body" end def empty?() false end def count() 1 end def full_messages() [ "Author name can't be empty" ] end }.new end def @post.new_record?() true end def @post.to_param() nil end def @post.column_for_attribute(attr_name) Post.content_columns.select { |column| column.name == attr_name }.first end def Post.content_columns() [ Column.new(:string, "title", "Title"), Column.new(:text, "body", "Body") ] end @post.title = "Hello World" @post.author_name = "" @post.body = "Back to the hill and over it again!" @post.secret = 1 @post.written_on = Date.new(2004, 6, 15) @controller = Object.new def @controller.url_for(options, *parameters_for_method_reference) options = options.symbolize_keys [options[:action], options[:id].to_param].compact.join('/') end end def test_generic_input_tag assert_dom_equal( %(), input("post", "title") ) end def test_text_area_with_errors assert_dom_equal( %(
    ), text_area("post", "body") ) end def test_text_field_with_errors assert_dom_equal( %(
    ), text_field("post", "author_name") ) end def test_form_with_string assert_dom_equal( %(


    \n


    ), form("post") ) class << @post def new_record?() false end def to_param() id end def id() 1 end end assert_dom_equal( %(


    \n


    ), form("post") ) end def test_form_with_date def Post.content_columns() [ Column.new(:date, "written_on", "Written on") ] end assert_dom_equal( %(


    \n\n\n

    ), form("post") ) end def test_form_with_datetime def Post.content_columns() [ Column.new(:datetime, "written_on", "Written on") ] end @post.written_on = Time.gm(2004, 6, 15, 16, 30) assert_dom_equal( %(


    \n\n\n — \n : \n

    ), form("post") ) end def test_error_for_block assert_dom_equal %(

    1 error prohibited this post from being saved

    There were problems with the following fields:

    • Author name can't be empty
    ), error_messages_for("post") assert_equal %(

    1 error prohibited this post from being saved

    There were problems with the following fields:

    • Author name can't be empty
    ), error_messages_for("post", :class => "errorDeathByClass", :id => "errorDeathById", :header_tag => "h1") end def test_error_messages_for_handles_nil assert_equal "", error_messages_for("notthere") end def test_form_with_string_multipart assert_dom_equal( %(


    \n


    ), form("post", :multipart => true) ) end end require File.dirname(__FILE__) + '/../abstract_unit' class AssetTagHelperTest < Test::Unit::TestCase include ActionView::Helpers::TagHelper include ActionView::Helpers::UrlHelper include ActionView::Helpers::AssetTagHelper def setup @controller = Class.new do attr_accessor :request def url_for(options, *parameters_for_method_reference) "http://www.example.com" end end.new @request = Class.new do def relative_url_root "" end end.new @controller.request = @request ActionView::Helpers::AssetTagHelper::reset_javascript_include_default end def teardown Object.send(:remove_const, :RAILS_ROOT) if defined?(RAILS_ROOT) ENV["RAILS_ASSET_ID"] = nil end AutoDiscoveryToTag = { %(auto_discovery_link_tag) => %(), %(auto_discovery_link_tag(:atom)) => %(), %(auto_discovery_link_tag(:rss, :action => "feed")) => %(), %(auto_discovery_link_tag(:rss, "http://localhost/feed")) => %(), %(auto_discovery_link_tag(:rss, {:action => "feed"}, {:title => "My RSS"})) => %(), %(auto_discovery_link_tag(:rss, {}, {:title => "My RSS"})) => %(), %(auto_discovery_link_tag(nil, {}, {:type => "text/html"})) => %(), %(auto_discovery_link_tag(nil, {}, {:title => "No stream.. really", :type => "text/html"})) => %(), %(auto_discovery_link_tag(:rss, {}, {:title => "My RSS", :type => "text/html"})) => %(), %(auto_discovery_link_tag(:atom, {}, {:rel => "Not so alternate"})) => %(), } JavascriptPathToTag = { %(javascript_path("xmlhr")) => %(/javascripts/xmlhr.js), %(javascript_path("super/xmlhr")) => %(/javascripts/super/xmlhr.js) } JavascriptIncludeToTag = { %(javascript_include_tag("xmlhr")) => %(), %(javascript_include_tag("xmlhr", :lang => "vbscript")) => %(), %(javascript_include_tag("common.javascript", "/elsewhere/cools")) => %(\n), %(javascript_include_tag(:defaults)) => %(\n\n\n), %(javascript_include_tag(:defaults, "test")) => %(\n\n\n\n), %(javascript_include_tag("test", :defaults)) => %(\n\n\n\n) } StylePathToTag = { %(stylesheet_path("style")) => %(/stylesheets/style.css), %(stylesheet_path('dir/file')) => %(/stylesheets/dir/file.css), %(stylesheet_path('/dir/file')) => %(/dir/file.css) } StyleLinkToTag = { %(stylesheet_link_tag("style")) => %(), %(stylesheet_link_tag("/dir/file")) => %(), %(stylesheet_link_tag("dir/file")) => %(), %(stylesheet_link_tag("style", :media => "all")) => %(), %(stylesheet_link_tag("random.styles", "/css/stylish")) => %(\n) } ImagePathToTag = { %(image_path("xml")) => %(/images/xml.png), } ImageLinkToTag = { %(image_tag("xml")) => %(Xml), %(image_tag("rss", :alt => "rss syndication")) => %(rss syndication), %(image_tag("gold", :size => "45x70")) => %(Gold), %(image_tag("symbolize", "size" => "45x70")) => %(Symbolize), %(image_tag("http://www.rubyonrails.com/images/rails")) => %(Rails) } def test_auto_discovery AutoDiscoveryToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end def test_javascript_path JavascriptPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end def test_javascript_include JavascriptIncludeToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end def test_register_javascript_include_default ActionView::Helpers::AssetTagHelper::register_javascript_include_default 'slider' assert_dom_equal %(\n\n\n\n), javascript_include_tag(:defaults) ActionView::Helpers::AssetTagHelper::register_javascript_include_default 'lib1', '/elsewhere/blub/lib2' assert_dom_equal %(\n\n\n\n\n\n), javascript_include_tag(:defaults) end def test_style_path StylePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end def test_style_link StyleLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end def test_image_path ImagePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end def test_image_tag ImageLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end def test_timebased_asset_id Object.send(:const_set, :RAILS_ROOT, File.dirname(__FILE__) + "/../fixtures/") expected_time = File.stat(File.expand_path(File.dirname(__FILE__) + "/../fixtures/public/images/rails.png")).mtime.to_i.to_s assert_equal %(Rails), image_tag("rails.png") end def test_skipping_asset_id_on_complete_url Object.send(:const_set, :RAILS_ROOT, File.dirname(__FILE__) + "/../fixtures/") assert_equal %(Rails), image_tag("http://www.example.com/rails.png") end def test_preset_asset_id Object.send(:const_set, :RAILS_ROOT, File.dirname(__FILE__) + "/../fixtures/") ENV["RAILS_ASSET_ID"] = "4500" assert_equal %(Rails), image_tag("rails.png") end end class AssetTagHelperNonVhostTest < Test::Unit::TestCase include ActionView::Helpers::TagHelper include ActionView::Helpers::UrlHelper include ActionView::Helpers::AssetTagHelper def setup @controller = Class.new do attr_accessor :request def url_for(options, *parameters_for_method_reference) "http://www.example.com/calloboration/hieraki" end end.new @request = Class.new do def relative_url_root "/calloboration/hieraki" end end.new @controller.request = @request ActionView::Helpers::AssetTagHelper::reset_javascript_include_default end AutoDiscoveryToTag = { %(auto_discovery_link_tag(:rss, :action => "feed")) => %(), %(auto_discovery_link_tag(:atom)) => %(), %(auto_discovery_link_tag) => %(), } JavascriptPathToTag = { %(javascript_path("xmlhr")) => %(/calloboration/hieraki/javascripts/xmlhr.js), } JavascriptIncludeToTag = { %(javascript_include_tag("xmlhr")) => %(), %(javascript_include_tag("common.javascript", "/elsewhere/cools")) => %(\n), %(javascript_include_tag(:defaults)) => %(\n\n\n) } StylePathToTag = { %(stylesheet_path("style")) => %(/calloboration/hieraki/stylesheets/style.css), } StyleLinkToTag = { %(stylesheet_link_tag("style")) => %(), %(stylesheet_link_tag("random.styles", "/css/stylish")) => %(\n) } ImagePathToTag = { %(image_path("xml")) => %(/calloboration/hieraki/images/xml.png), } ImageLinkToTag = { %(image_tag("xml")) => %(Xml), %(image_tag("rss", :alt => "rss syndication")) => %(rss syndication), %(image_tag("gold", :size => "45x70")) => %(Gold), %(image_tag("http://www.example.com/images/icon.gif")) => %(Icon), %(image_tag("symbolize", "size" => "45x70")) => %(Symbolize) } def test_auto_discovery AutoDiscoveryToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end def test_javascript_path JavascriptPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end def test_javascript_include JavascriptIncludeToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end def test_register_javascript_include_default ActionView::Helpers::AssetTagHelper::register_javascript_include_default 'slider' assert_dom_equal %(\n\n\n\n), javascript_include_tag(:defaults) ActionView::Helpers::AssetTagHelper::register_javascript_include_default 'lib1', '/elsewhere/blub/lib2' assert_dom_equal %(\n\n\n\n\n\n), javascript_include_tag(:defaults) end def test_style_path StylePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end def test_style_link StyleLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end def test_image_path ImagePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end def test_image_tag ImageLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } # Assigning a default alt tag should not cause an exception to be raised assert_nothing_raised { image_tag('') } end def test_stylesheet_with_asset_host_already_encoded ActionController::Base.asset_host = "http://foo.example.com" result = stylesheet_link_tag("http://bar.example.com/stylesheets/style.css") assert_dom_equal( %(), result) ensure ActionController::Base.asset_host = "" end end require 'test/unit' require File.dirname(__FILE__) + '/../../lib/action_view/helpers/benchmark_helper' class BenchmarkHelperTest < Test::Unit::TestCase include ActionView::Helpers::BenchmarkHelper class MockLogger attr_reader :logged def initialize @logged = [] end def method_missing(method, *args) @logged << [method, args] end end def setup @logger = MockLogger.new end def test_without_logger_or_block @logger = nil assert_nothing_raised { benchmark } end def test_without_block assert_raise(LocalJumpError) { benchmark } assert @logger.logged.empty? end def test_without_logger @logger = nil i_was_run = false benchmark { i_was_run = true } assert !i_was_run end def test_defaults i_was_run = false benchmark { i_was_run = true } assert i_was_run assert 1, @logger.logged.size assert_last_logged end def test_with_message i_was_run = false benchmark('test_run') { i_was_run = true } assert i_was_run assert 1, @logger.logged.size assert_last_logged 'test_run' end def test_with_message_and_level i_was_run = false benchmark('debug_run', :debug) { i_was_run = true } assert i_was_run assert 1, @logger.logged.size assert_last_logged 'debug_run', :debug end private def assert_last_logged(message = 'Benchmarking', level = :info) last = @logger.logged.last assert 2, last.size assert_equal level, last.first assert 1, last[1].size assert last[1][0] =~ /^#{message} \(.*\)$/ end end require 'test/unit' require File.dirname(__FILE__) + '/../../lib/action_view/helpers/date_helper' require File.dirname(__FILE__) + "/../abstract_unit" class CompiledTemplateTests < Test::Unit::TestCase def setup @ct = ActionView::CompiledTemplates.new @v = Class.new @v.send :include, @ct @a = './test_compile_template_a.rhtml' @b = './test_compile_template_b.rhtml' @s = './test_compile_template_link.rhtml' end def teardown [@a, @b, @s].each do |f| `rm #{f}` if File.exist?(f) || File.symlink?(f) end end attr_reader :ct, :v def test_name_allocation hi_world = ct.method_names['hi world'] hi_sexy = ct.method_names['hi sexy'] wish_upon_a_star = ct.method_names['I love seeing decent error messages'] assert_equal hi_world, ct.method_names['hi world'] assert_equal hi_sexy, ct.method_names['hi sexy'] assert_equal wish_upon_a_star, ct.method_names['I love seeing decent error messages'] assert_equal 3, [hi_world, hi_sexy, wish_upon_a_star].uniq.length end def test_wrap_source assert_equal( "def aliased_assignment(value)\nself.value = value\nend", @ct.wrap_source(:aliased_assignment, [:value], 'self.value = value') ) assert_equal( "def simple()\nnil\nend", @ct.wrap_source(:simple, [], 'nil') ) end def test_compile_source_single_method selector = ct.compile_source('doubling method', [:a], 'a + a') assert_equal 2, @v.new.send(selector, 1) assert_equal 4, @v.new.send(selector, 2) assert_equal -4, @v.new.send(selector, -2) assert_equal 0, @v.new.send(selector, 0) selector end def test_compile_source_two_method sel1 = test_compile_source_single_method # compile the method in the other test sel2 = ct.compile_source('doubling method', [:a, :b], 'a + b + a + b') assert_not_equal sel1, sel2 assert_equal 2, @v.new.send(sel1, 1) assert_equal 4, @v.new.send(sel1, 2) assert_equal 6, @v.new.send(sel2, 1, 2) assert_equal 32, @v.new.send(sel2, 15, 1) end def test_mtime t1 = Time.now test_compile_source_single_method assert (t1..Time.now).include?(ct.mtime('doubling method', [:a])) end def test_compile_time `echo '#{@a}' > #{@a}; echo '#{@b}' > #{@b}; ln -s #{@a} #{@s}` v = ActionView::Base.new v.base_path = '.' v.cache_template_loading = false; sleep 1 t = Time.now v.compile_and_render_template(:rhtml, '', @a) v.compile_and_render_template(:rhtml, '', @b) v.compile_and_render_template(:rhtml, '', @s) a_n = v.method_names[@a] b_n = v.method_names[@b] s_n = v.method_names[@s] # all of the files have changed since last compile assert v.compile_time[a_n] > t assert v.compile_time[b_n] > t assert v.compile_time[s_n] > t sleep 1 t = Time.now v.compile_and_render_template(:rhtml, '', @a) v.compile_and_render_template(:rhtml, '', @b) v.compile_and_render_template(:rhtml, '', @s) # none of the files have changed since last compile assert v.compile_time[a_n] < t assert v.compile_time[b_n] < t assert v.compile_time[s_n] < t `rm #{@s}; ln -s #{@b} #{@s}` v.compile_and_render_template(:rhtml, '', @a) v.compile_and_render_template(:rhtml, '', @b) v.compile_and_render_template(:rhtml, '', @s) # the symlink has changed since last compile assert v.compile_time[a_n] < t assert v.compile_time[b_n] < t assert v.compile_time[s_n] > t sleep 1 `touch #{@b}` t = Time.now v.compile_and_render_template(:rhtml, '', @a) v.compile_and_render_template(:rhtml, '', @b) v.compile_and_render_template(:rhtml, '', @s) # the file at the end of the symlink has changed since last compile # both the symlink and the file at the end of it should be recompiled assert v.compile_time[a_n] < t assert v.compile_time[b_n] > t assert v.compile_time[s_n] > t end end module ActionView class Base def compile_time @@compile_time end def method_names @@method_names end end end require 'test/unit' require File.dirname(__FILE__) + "/../abstract_unit" class DateHelperTest < Test::Unit::TestCase include ActionView::Helpers::DateHelper include ActionView::Helpers::FormHelper silence_warnings do Post = Struct.new("Post", :written_on, :updated_at) end def test_distance_in_words from = Time.mktime(2004, 3, 6, 21, 41, 18) assert_equal "less than a minute", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 41, 25)) assert_equal "5 minutes", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 46, 25)) assert_equal "about 1 hour", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 22, 47, 25)) assert_equal "about 3 hours", distance_of_time_in_words(from, Time.mktime(2004, 3, 7, 0, 41)) assert_equal "about 4 hours", distance_of_time_in_words(from, Time.mktime(2004, 3, 7, 1, 20)) assert_equal "2 days", distance_of_time_in_words(from, Time.mktime(2004, 3, 9, 15, 40)) # include seconds assert_equal "less than a minute", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 41, 19), false) assert_equal "less than 5 seconds", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 41, 19), true) assert_equal "less than 10 seconds", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 41, 28), true) assert_equal "less than 20 seconds", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 41, 38), true) assert_equal "half a minute", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 41, 48), true) assert_equal "less than a minute", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 42, 17), true) assert_equal "1 minute", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 42, 18), true) assert_equal "1 minute", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 42, 28), true) assert_equal "2 minutes", distance_of_time_in_words(from, Time.mktime(2004, 3, 6, 21, 42, 48), true) # test to < from assert_equal "about 4 hours", distance_of_time_in_words(Time.mktime(2004, 3, 7, 1, 20), from) assert_equal "less than 20 seconds", distance_of_time_in_words(Time.mktime(2004, 3, 6, 21, 41, 38), from, true) # test with integers assert_equal "less than a minute", distance_of_time_in_words(50) assert_equal "about 1 hour", distance_of_time_in_words(60*60) # more cumbersome test with integers assert_equal "less than a minute", distance_of_time_in_words(0, 50) assert_equal "about 1 hour", distance_of_time_in_words(60*60, 0) end def test_distance_in_words_date start_date = Date.new 1975, 1, 31 end_date = Date.new 1977, 4, 17 assert_not_equal("13 minutes", distance_of_time_in_words(start_date, end_date)) end def test_select_day expected = %(\n" assert_equal expected, select_day(Time.mktime(2003, 8, 16)) assert_equal expected, select_day(16) end def test_select_day_with_blank expected = %(\n" assert_equal expected, select_day(Time.mktime(2003, 8, 16), :include_blank => true) assert_equal expected, select_day(16, :include_blank => true) end def test_select_day_nil_with_blank expected = %(\n" assert_equal expected, select_day(nil, :include_blank => true) end def test_select_month expected = %(\n" assert_equal expected, select_month(Time.mktime(2003, 8, 16)) assert_equal expected, select_month(8) end def test_select_month_with_disabled expected = %(\n" assert_equal expected, select_month(Time.mktime(2003, 8, 16), :disabled => true) assert_equal expected, select_month(8, :disabled => true) end def test_select_month_with_field_name_override expected = %(\n" assert_equal expected, select_month(Time.mktime(2003, 8, 16), :field_name => 'mois') assert_equal expected, select_month(8, :field_name => 'mois') end def test_select_month_with_blank expected = %(\n" assert_equal expected, select_month(Time.mktime(2003, 8, 16), :include_blank => true) assert_equal expected, select_month(8, :include_blank => true) end def test_select_month_nil_with_blank expected = %(\n" assert_equal expected, select_month(nil, :include_blank => true) end def test_select_month_with_numbers expected = %(\n" assert_equal expected, select_month(Time.mktime(2003, 8, 16), :use_month_numbers => true) assert_equal expected, select_month(8, :use_month_numbers => true) end def test_select_month_with_numbers_and_names expected = %(\n" assert_equal expected, select_month(Time.mktime(2003, 8, 16), :add_month_numbers => true) assert_equal expected, select_month(8, :add_month_numbers => true) end def test_select_month_with_numbers_and_names_with_abbv expected = %(\n" assert_equal expected, select_month(Time.mktime(2003, 8, 16), :add_month_numbers => true, :use_short_month => true) assert_equal expected, select_month(8, :add_month_numbers => true, :use_short_month => true) end def test_select_month_with_abbv expected = %(\n" assert_equal expected, select_month(Time.mktime(2003, 8, 16), :use_short_month => true) assert_equal expected, select_month(8, :use_short_month => true) end def test_select_year expected = %(\n" assert_equal expected, select_year(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005) assert_equal expected, select_year(2003, :start_year => 2003, :end_year => 2005) end def test_select_year_with_disabled expected = %(\n" assert_equal expected, select_year(Time.mktime(2003, 8, 16), :disabled => true, :start_year => 2003, :end_year => 2005) assert_equal expected, select_year(2003, :disabled => true, :start_year => 2003, :end_year => 2005) end def test_select_year_with_field_name_override expected = %(\n" assert_equal expected, select_year(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :field_name => 'annee') assert_equal expected, select_year(2003, :start_year => 2003, :end_year => 2005, :field_name => 'annee') end def test_select_year_with_type_discarding expected = %(\n" assert_equal expected, select_year( Time.mktime(2003, 8, 16), :prefix => "date_year", :discard_type => true, :start_year => 2003, :end_year => 2005) assert_equal expected, select_year( 2003, :prefix => "date_year", :discard_type => true, :start_year => 2003, :end_year => 2005) end def test_select_year_descending expected = %(\n" assert_equal expected, select_year(Time.mktime(2005, 8, 16), :start_year => 2005, :end_year => 2003) assert_equal expected, select_year(2005, :start_year => 2005, :end_year => 2003) end def test_select_hour expected = %(\n" assert_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18)) end def test_select_hour_with_disabled expected = %(\n" assert_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :disabled => true) end def test_select_hour_with_field_name_override expected = %(\n" assert_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :field_name => 'heure') end def test_select_hour_with_blank expected = %(\n" assert_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :include_blank => true) end def test_select_hour_nil_with_blank expected = %(\n" assert_equal expected, select_hour(nil, :include_blank => true) end def test_select_minute expected = %(\n" assert_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18)) end def test_select_minute_with_disabled expected = %(\n" assert_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :disabled => true) end def test_select_minute_with_field_name_override expected = %(\n" assert_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :field_name => 'minuto') end def test_select_minute_with_blank expected = %(\n" assert_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :include_blank => true) end def test_select_minute_with_blank_and_step expected = %(\n" assert_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), { :include_blank => true , :minute_step => 15 }) end def test_select_minute_nil_with_blank expected = %(\n" assert_equal expected, select_minute(nil, :include_blank => true) end def test_select_minute_nil_with_blank_and_step expected = %(\n" assert_equal expected, select_minute(nil, { :include_blank => true , :minute_step => 15 }) end def test_select_second expected = %(\n" assert_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18)) end def test_select_second_with_disabled expected = %(\n" assert_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :disabled => true) end def test_select_second_with_field_name_override expected = %(\n" assert_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :field_name => 'segundo') end def test_select_second_with_blank expected = %(\n" assert_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :include_blank => true) end def test_select_second_nil_with_blank expected = %(\n" assert_equal expected, select_second(nil, :include_blank => true) end def test_select_date expected = %(\n" expected << %(\n" expected << %(\n" assert_equal expected, select_date( Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]" ) end def test_select_date_with_disabled expected = %(\n" expected << %(\n" expected << %(\n" assert_equal expected, select_date(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :disabled => true) end def test_select_date_with_no_start_year expected = %(\n" expected << %(\n" expected << %(\n" assert_equal expected, select_date( Time.mktime(Date.today.year, 8, 16), :end_year => Date.today.year+1, :prefix => "date[first]" ) end def test_select_date_with_no_end_year expected = %(\n" expected << %(\n" expected << %(\n" assert_equal expected, select_date( Time.mktime(2003, 8, 16), :start_year => 2003, :prefix => "date[first]" ) end def test_select_date_with_no_start_or_end_year expected = %(\n" expected << %(\n" expected << %(\n" assert_equal expected, select_date( Time.mktime(Date.today.year, 8, 16), :prefix => "date[first]" ) end def test_select_time_with_seconds expected = %(\n" expected << %(\n" expected << %(\n" assert_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :include_seconds => true) end def test_select_time_without_seconds expected = %(\n" expected << %(\n" assert_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18)) assert_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :include_seconds => false) end def test_date_select_with_zero_value expected = %(\n" expected << %(\n" expected << %(\n" assert_equal expected, select_date(0, :start_year => 2003, :end_year => 2005, :prefix => "date[first]") end def test_date_select_within_fields_for @post = Post.new @post.written_on = Date.new(2004, 6, 15) _erbout = '' fields_for :post, @post do |f| _erbout.concat f.date_select(:written_on) end expected = "\n" + "\n" + "\n" assert_dom_equal(expected, _erbout) end def test_datetime_select_within_fields_for @post = Post.new @post.updated_at = Time.local(2004, 6, 15, 16, 35) _erbout = '' fields_for :post, @post do |f| _erbout.concat f.datetime_select(:updated_at) end expected = "\n\n\n — \n : \n" assert_dom_equal(expected, _erbout) end def test_date_select_with_zero_value_and_no_start_year expected = %(\n" expected << %(\n" expected << %(\n" assert_equal expected, select_date(0, :end_year => Date.today.year+1, :prefix => "date[first]") end def test_date_select_with_zero_value_and_no_end_year expected = %(\n" expected << %(\n" expected << %(\n" assert_equal expected, select_date(0, :start_year => 2003, :prefix => "date[first]") end def test_date_select_with_zero_value_and_no_start_and_end_year expected = %(\n" expected << %(\n" expected << %(\n" assert_equal expected, select_date(0, :prefix => "date[first]") end def test_date_select_with_nil_value_and_no_start_and_end_year expected = %(\n" expected << %(\n" expected << %(\n" assert_equal expected, select_date(nil, :prefix => "date[first]") end def test_datetime_select_with_nil_value_and_no_start_and_end_year expected = %(\n" expected << %(\n" expected << %(\n" expected << %(\n" expected << %(\n" assert_equal expected, select_datetime(nil, :prefix => "date[first]") end end require File.dirname(__FILE__) + '/../abstract_unit' class FormHelperTest < Test::Unit::TestCase include ActionView::Helpers::FormHelper include ActionView::Helpers::FormTagHelper include ActionView::Helpers::UrlHelper include ActionView::Helpers::TagHelper include ActionView::Helpers::TextHelper silence_warnings do Post = Struct.new("Post", :title, :author_name, :body, :secret, :written_on, :cost) Post.class_eval do alias_method :title_before_type_cast, :title unless respond_to?(:title_before_type_cast) alias_method :body_before_type_cast, :body unless respond_to?(:body_before_type_cast) alias_method :author_name_before_type_cast, :author_name unless respond_to?(:author_name_before_type_cast) end end def setup @post = Post.new def @post.errors() Class.new{ def on(field) field == "author_name" end }.new end def @post.id; 123; end def @post.id_before_type_cast; 123; end @post.title = "Hello World" @post.author_name = "" @post.body = "Back to the hill and over it again!" @post.secret = 1 @post.written_on = Date.new(2004, 6, 15) @controller = Class.new do attr_reader :url_for_options def url_for(options, *parameters_for_method_reference) @url_for_options = options "http://www.example.com" end end @controller = @controller.new end def test_text_field assert_dom_equal( '', text_field("post", "title") ) assert_dom_equal( '', password_field("post", "title") ) assert_dom_equal( '', password_field("person", "name") ) end def test_text_field_with_escapes @post.title = "Hello World" assert_dom_equal( '', text_field("post", "title") ) end def test_text_field_with_options expected = '' assert_dom_equal expected, text_field("post", "title", "size" => 35) assert_dom_equal expected, text_field("post", "title", :size => 35) end def test_text_field_assuming_size expected = '' assert_dom_equal expected, text_field("post", "title", "maxlength" => 35) assert_dom_equal expected, text_field("post", "title", :maxlength => 35) end def test_text_field_doesnt_change_param_values object_name = 'post[]' expected = '' assert_equal expected, text_field(object_name, "title") assert_equal object_name, "post[]" end def test_check_box assert_dom_equal( '', check_box("post", "secret") ) @post.secret = 0 assert_dom_equal( '', check_box("post", "secret") ) assert_dom_equal( '', check_box("post", "secret" ,{"checked"=>"checked"}) ) @post.secret = true assert_dom_equal( '', check_box("post", "secret") ) end def test_check_box_with_explicit_checked_and_unchecked_values @post.secret = "on" assert_dom_equal( '', check_box("post", "secret", {}, "on", "off") ) end def test_radio_button assert_dom_equal('', radio_button("post", "title", "Hello World") ) assert_dom_equal('', radio_button("post", "title", "Goodbye World") ) end def test_radio_button_is_checked_with_integers assert_dom_equal('', radio_button("post", "secret", "1") ) end def test_text_area assert_dom_equal( '', text_area("post", "body") ) end def test_text_area_with_escapes @post.body = "Back to the hill and over it again!" assert_dom_equal( '', text_area("post", "body") ) end def test_text_area_with_alternate_value assert_dom_equal( '', text_area("post", "body", :value => 'Testing alternate values.') ) end def test_date_selects assert_dom_equal( '', text_area("post", "body") ) end def test_explicit_name assert_dom_equal( '', text_field("post", "title", "name" => "dont guess") ) assert_dom_equal( '', text_area("post", "body", "name" => "really!") ) assert_dom_equal( '', check_box("post", "secret", "name" => "i mean it") ) assert_dom_equal text_field("post", "title", "name" => "dont guess"), text_field("post", "title", :name => "dont guess") assert_dom_equal text_area("post", "body", "name" => "really!"), text_area("post", "body", :name => "really!") assert_dom_equal check_box("post", "secret", "name" => "i mean it"), check_box("post", "secret", :name => "i mean it") end def test_explicit_id assert_dom_equal( '', text_field("post", "title", "id" => "dont guess") ) assert_dom_equal( '', text_area("post", "body", "id" => "really!") ) assert_dom_equal( '', check_box("post", "secret", "id" => "i mean it") ) assert_dom_equal text_field("post", "title", "id" => "dont guess"), text_field("post", "title", :id => "dont guess") assert_dom_equal text_area("post", "body", "id" => "really!"), text_area("post", "body", :id => "really!") assert_dom_equal check_box("post", "secret", "id" => "i mean it"), check_box("post", "secret", :id => "i mean it") end def test_auto_index pid = @post.id assert_dom_equal( "", text_field("post[]","title") ) assert_dom_equal( "", text_area("post[]", "body") ) assert_dom_equal( "", check_box("post[]", "secret") ) assert_dom_equal( "", radio_button("post[]", "title", "Hello World") ) assert_dom_equal("", radio_button("post[]", "title", "Goodbye World") ) end def test_form_for _erbout = '' form_for(:post, @post, :html => { :id => 'create-post' }) do |f| _erbout.concat f.text_field(:title) _erbout.concat f.text_area(:body) _erbout.concat f.check_box(:secret) end expected = "
    " + "" + "" + "" + "" + "
    " assert_dom_equal expected, _erbout end def test_form_for_without_object _erbout = '' form_for(:post, :html => { :id => 'create-post' }) do |f| _erbout.concat f.text_field(:title) _erbout.concat f.text_area(:body) _erbout.concat f.check_box(:secret) end expected = "
    " + "" + "" + "" + "" + "
    " assert_dom_equal expected, _erbout end def test_fields_for _erbout = '' fields_for(:post, @post) do |f| _erbout.concat f.text_field(:title) _erbout.concat f.text_area(:body) _erbout.concat f.check_box(:secret) end expected = "" + "" + "" + "" assert_dom_equal expected, _erbout end def test_fields_for_without_object _erbout = '' fields_for(:post) do |f| _erbout.concat f.text_field(:title) _erbout.concat f.text_area(:body) _erbout.concat f.check_box(:secret) end expected = "" + "" + "" + "" assert_dom_equal expected, _erbout end def test_form_builder_does_not_have_form_for_method assert ! ActionView::Helpers::FormBuilder.instance_methods.include?('form_for') end def test_form_for_and_fields_for _erbout = '' form_for(:post, @post, :html => { :id => 'create-post' }) do |post_form| _erbout.concat post_form.text_field(:title) _erbout.concat post_form.text_area(:body) fields_for(:parent_post, @post) do |parent_fields| _erbout.concat parent_fields.check_box(:secret) end end expected = "
    " + "" + "" + "" + "" + "
    " assert_dom_equal expected, _erbout end class LabelledFormBuilder < ActionView::Helpers::FormBuilder (field_helpers - %w(hidden_field)).each do |selector| src = <<-END_SRC def #{selector}(field, *args, &proc) " " + super + "
    " end END_SRC class_eval src, __FILE__, __LINE__ end end def test_form_for_with_labelled_builder _erbout = '' form_for(:post, @post, :builder => LabelledFormBuilder) do |f| _erbout.concat f.text_field(:title) _erbout.concat f.text_area(:body) _erbout.concat f.check_box(:secret) end expected = "
    " + "
    " + "
    " + " " + "
    " + "
    " assert_dom_equal expected, _erbout end # Perhaps this test should be moved to prototype helper tests. def test_remote_form_for_with_labelled_builder self.extend ActionView::Helpers::PrototypeHelper _erbout = '' remote_form_for(:post, @post, :builder => LabelledFormBuilder) do |f| _erbout.concat f.text_field(:title) _erbout.concat f.text_area(:body) _erbout.concat f.check_box(:secret) end expected = %(
    ) + "
    " + "
    " + " " + "
    " + "
    " assert_dom_equal expected, _erbout end def test_fields_for_with_labelled_builder _erbout = '' fields_for(:post, @post, :builder => LabelledFormBuilder) do |f| _erbout.concat f.text_field(:title) _erbout.concat f.text_area(:body) _erbout.concat f.check_box(:secret) end expected = "
    " + "
    " + " " + "
    " assert_dom_equal expected, _erbout end def test_form_for_with_html_options_adds_options_to_form_tag _erbout = '' form_for(:post, @post, :html => {:id => 'some_form', :class => 'some_class'}) do |f| end expected = "
    " assert_dom_equal expected, _erbout end def test_form_for_with_string_url_option _erbout = '' form_for(:post, @post, :url => 'http://www.otherdomain.com') do |f| end assert_equal 'http://www.otherdomain.com', @controller.url_for_options end def test_form_for_with_hash_url_option _erbout = '' form_for(:post, @post, :url => {:controller => 'controller', :action => 'action'}) do |f| end assert_equal 'controller', @controller.url_for_options[:controller] assert_equal 'action', @controller.url_for_options[:action] end def test_remote_form_for_with_html_options_adds_options_to_form_tag self.extend ActionView::Helpers::PrototypeHelper _erbout = '' remote_form_for(:post, @post, :html => {:id => 'some_form', :class => 'some_class'}) do |f| end expected = "
    " assert_dom_equal expected, _erbout end end require File.dirname(__FILE__) + '/../abstract_unit' class MockTimeZone attr_reader :name def initialize( name ) @name = name end def self.all [ "A", "B", "C", "D", "E" ].map { |s| new s } end def ==( z ) z && @name == z.name end def to_s @name end end ActionView::Helpers::FormOptionsHelper::TimeZone = MockTimeZone class FormOptionsHelperTest < Test::Unit::TestCase include ActionView::Helpers::FormHelper include ActionView::Helpers::FormOptionsHelper silence_warnings do Post = Struct.new('Post', :title, :author_name, :body, :secret, :written_on, :category, :origin) Continent = Struct.new('Continent', :continent_name, :countries) Country = Struct.new('Country', :country_id, :country_name) Firm = Struct.new('Firm', :time_zone) end def test_collection_options @posts = [ Post.new(" went home", "", "To a little house", "shh!"), Post.new("Babe went home", "Babe", "To a little house", "shh!"), Post.new("Cabe went home", "Cabe", "To a little house", "shh!") ] assert_dom_equal( "\n\n", options_from_collection_for_select(@posts, "author_name", "title") ) end def test_collection_options_with_preselected_value @posts = [ Post.new(" went home", "", "To a little house", "shh!"), Post.new("Babe went home", "Babe", "To a little house", "shh!"), Post.new("Cabe went home", "Cabe", "To a little house", "shh!") ] assert_dom_equal( "\n\n", options_from_collection_for_select(@posts, "author_name", "title", "Babe") ) end def test_collection_options_with_preselected_value_array @posts = [ Post.new(" went home", "", "To a little house", "shh!"), Post.new("Babe went home", "Babe", "To a little house", "shh!"), Post.new("Cabe went home", "Cabe", "To a little house", "shh!") ] assert_dom_equal( "\n\n", options_from_collection_for_select(@posts, "author_name", "title", [ "Babe", "Cabe" ]) ) end def test_array_options_for_select assert_dom_equal( "\n\n", options_for_select([ "", "USA", "Sweden" ]) ) end def test_array_options_for_select_with_selection assert_dom_equal( "\n\n", options_for_select([ "Denmark", "", "Sweden" ], "") ) end def test_array_options_for_select_with_selection_array assert_dom_equal( "\n\n", options_for_select([ "Denmark", "", "Sweden" ], [ "", "Sweden" ]) ) end def test_array_options_for_string_include_in_other_string_bug_fix assert_dom_equal( "\n", options_for_select([ "ruby", "rubyonrails" ], "rubyonrails") ) assert_dom_equal( "\n", options_for_select([ "ruby", "rubyonrails" ], "ruby") ) end def test_hash_options_for_select assert_dom_equal( "\n", options_for_select({ "$" => "Dollar", "" => "" }) ) assert_dom_equal( "\n", options_for_select({ "$" => "Dollar", "" => "" }, "Dollar") ) assert_dom_equal( "\n", options_for_select({ "$" => "Dollar", "" => "" }, [ "Dollar", "" ]) ) end def test_ducktyped_options_for_select quack = Struct.new(:first, :last) assert_dom_equal( "\n", options_for_select([quack.new("", ""), quack.new("$", "Dollar")]) ) assert_dom_equal( "\n", options_for_select([quack.new("", ""), quack.new("$", "Dollar")], "Dollar") ) assert_dom_equal( "\n", options_for_select([quack.new("", ""), quack.new("$", "Dollar")], ["Dollar", ""]) ) end def test_html_option_groups_from_collection @continents = [ Continent.new("", [Country.new("", ""), Country.new("so", "Somalia")] ), Continent.new("Europe", [Country.new("dk", "Denmark"), Country.new("ie", "Ireland")] ) ] assert_dom_equal( "\n\n", option_groups_from_collection_for_select(@continents, "countries", "continent_name", "country_id", "country_name", "dk") ) end def test_time_zone_options_no_parms opts = time_zone_options_for_select assert_dom_equal "\n" + "\n" + "\n" + "\n" + "", opts end def test_time_zone_options_with_selected opts = time_zone_options_for_select( "D" ) assert_dom_equal "\n" + "\n" + "\n" + "\n" + "", opts end def test_time_zone_options_with_unknown_selected opts = time_zone_options_for_select( "K" ) assert_dom_equal "\n" + "\n" + "\n" + "\n" + "", opts end def test_time_zone_options_with_priority_zones zones = [ TimeZone.new( "B" ), TimeZone.new( "E" ) ] opts = time_zone_options_for_select( nil, zones ) assert_dom_equal "\n" + "" + "\n" + "\n" + "\n" + "", opts end def test_time_zone_options_with_selected_priority_zones zones = [ TimeZone.new( "B" ), TimeZone.new( "E" ) ] opts = time_zone_options_for_select( "E", zones ) assert_dom_equal "\n" + "" + "\n" + "\n" + "\n" + "", opts end def test_time_zone_options_with_unselected_priority_zones zones = [ TimeZone.new( "B" ), TimeZone.new( "E" ) ] opts = time_zone_options_for_select( "C", zones ) assert_dom_equal "\n" + "" + "\n" + "\n" + "\n" + "", opts end def test_select @post = Post.new @post.category = "" assert_dom_equal( "", select("post", "category", %w( abe hest)) ) end def test_select_under_fields_for @post = Post.new @post.category = "" _erbout = '' fields_for :post, @post do |f| _erbout.concat f.select(:category, %w( abe hest)) end assert_dom_equal( "", _erbout ) end def test_select_with_blank @post = Post.new @post.category = "" assert_dom_equal( "", select("post", "category", %w( abe hest), :include_blank => true) ) end def test_select_with_default_prompt @post = Post.new @post.category = "" assert_dom_equal( "", select("post", "category", %w( abe hest), :prompt => true) ) end def test_select_no_prompt_when_select_has_value @post = Post.new @post.category = "" assert_dom_equal( "", select("post", "category", %w( abe hest), :prompt => true) ) end def test_select_with_given_prompt @post = Post.new @post.category = "" assert_dom_equal( "", select("post", "category", %w( abe hest), :prompt => 'The prompt') ) end def test_select_with_prompt_and_blank @post = Post.new @post.category = "" assert_dom_equal( "", select("post", "category", %w( abe hest), :prompt => true, :include_blank => true) ) end def test_select_with_selected_value @post = Post.new @post.category = "" assert_dom_equal( "", select("post", "category", %w( abe hest ), :selected => 'abe') ) end def test_select_with_selected_nil @post = Post.new @post.category = "" assert_dom_equal( "", select("post", "category", %w( abe hest ), :selected => nil) ) end def test_collection_select @posts = [ Post.new(" went home", "", "To a little house", "shh!"), Post.new("Babe went home", "Babe", "To a little house", "shh!"), Post.new("Cabe went home", "Cabe", "To a little house", "shh!") ] @post = Post.new @post.author_name = "Babe" assert_dom_equal( "", collection_select("post", "author_name", @posts, "author_name", "author_name") ) end def test_collection_select_under_fields_for @posts = [ Post.new(" went home", "", "To a little house", "shh!"), Post.new("Babe went home", "Babe", "To a little house", "shh!"), Post.new("Cabe went home", "Cabe", "To a little house", "shh!") ] @post = Post.new @post.author_name = "Babe" _erbout = '' fields_for :post, @post do |f| _erbout.concat f.collection_select(:author_name, @posts, :author_name, :author_name) end assert_dom_equal( "", _erbout ) end def test_collection_select_with_blank_and_style @posts = [ Post.new(" went home", "", "To a little house", "shh!"), Post.new("Babe went home", "Babe", "To a little house", "shh!"), Post.new("Cabe went home", "Cabe", "To a little house", "shh!") ] @post = Post.new @post.author_name = "Babe" assert_dom_equal( "", collection_select("post", "author_name", @posts, "author_name", "author_name", { :include_blank => true }, "style" => "width: 200px") ) end def test_country_select @post = Post.new @post.origin = "Denmark" assert_dom_equal( "", country_select("post", "origin") ) end def test_time_zone_select @firm = Firm.new("D") html = time_zone_select( "firm", "time_zone" ) assert_dom_equal "", html end def test_time_zone_select_under_fields_for @firm = Firm.new("D") _erbout = '' fields_for :firm, @firm do |f| _erbout.concat f.time_zone_select(:time_zone) end assert_dom_equal( "", _erbout ) end def test_time_zone_select_with_blank @firm = Firm.new("D") html = time_zone_select("firm", "time_zone", nil, :include_blank => true) assert_dom_equal "", html end def test_time_zone_select_with_style @firm = Firm.new("D") html = time_zone_select("firm", "time_zone", nil, {}, "style" => "color: red") assert_dom_equal "", html assert_dom_equal html, time_zone_select("firm", "time_zone", nil, {}, :style => "color: red") end def test_time_zone_select_with_blank_and_style @firm = Firm.new("D") html = time_zone_select("firm", "time_zone", nil, { :include_blank => true }, "style" => "color: red") assert_dom_equal "", html assert_dom_equal html, time_zone_select("firm", "time_zone", nil, { :include_blank => true }, :style => "color: red") end def test_time_zone_select_with_priority_zones @firm = Firm.new("D") zones = [ TimeZone.new("A"), TimeZone.new("D") ] html = time_zone_select("firm", "time_zone", zones ) assert_dom_equal "", html end end require File.dirname(__FILE__) + '/../abstract_unit' class FormTagHelperTest < Test::Unit::TestCase include ActionView::Helpers::UrlHelper include ActionView::Helpers::TagHelper include ActionView::Helpers::FormTagHelper def setup @controller = Class.new do def url_for(options, *parameters_for_method_reference) "http://www.example.com" end end @controller = @controller.new end def test_check_box_tag actual = check_box_tag "admin" expected = %() assert_dom_equal expected, actual end def test_form_tag actual = form_tag expected = %(
    ) assert_dom_equal expected, actual end def test_form_tag_multipart actual = form_tag({}, { 'multipart' => true }) expected = %() assert_dom_equal expected, actual end def test_hidden_field_tag actual = hidden_field_tag "id", 3 expected = %() assert_dom_equal expected, actual end def test_password_field_tag actual = password_field_tag expected = %() assert_dom_equal expected, actual end def test_radio_button_tag actual = radio_button_tag "people", "david" expected = %() assert_dom_equal expected, actual end def test_select_tag actual = select_tag "people", "" expected = %() assert_dom_equal expected, actual end def test_text_area_tag_size_string actual = text_area_tag "body", "hello world", "size" => "20x40" expected = %() assert_dom_equal expected, actual end def test_text_area_tag_size_symbol actual = text_area_tag "body", "hello world", :size => "20x40" expected = %() assert_dom_equal expected, actual end def test_text_field_tag actual = text_field_tag "title", "Hello!" expected = %() assert_dom_equal expected, actual end def test_text_field_tag_class_string actual = text_field_tag "title", "Hello!", "class" => "admin" expected = %() assert_dom_equal expected, actual end def test_boolean_optios assert_dom_equal %(), check_box_tag("admin", 1, true, 'disabled' => true, :readonly => "yes") assert_dom_equal %(), check_box_tag("admin", 1, true, :disabled => false, :readonly => nil) assert_dom_equal %(), select_tag("people", "", :multiple => true) assert_dom_equal %(), select_tag("people", "", :multiple => nil) end def test_stringify_symbol_keys actual = text_field_tag "title", "Hello!", :id => "admin" expected = %() assert_dom_equal expected, actual end def test_submit_tag assert_dom_equal( %(), submit_tag("Save", :disable_with => "Saving...", :onclick => "alert('hello!')") ) end def test_pass assert_equal 1, 1 end end require File.dirname(__FILE__) + '/../abstract_unit' class JavaScriptMacrosHelperTest < Test::Unit::TestCase include ActionView::Helpers::JavaScriptHelper include ActionView::Helpers::JavaScriptMacrosHelper include ActionView::Helpers::UrlHelper include ActionView::Helpers::TagHelper include ActionView::Helpers::TextHelper include ActionView::Helpers::FormHelper include ActionView::Helpers::CaptureHelper def setup @controller = Class.new do def url_for(options, *parameters_for_method_reference) url = "http://www.example.com/" url << options[:action].to_s if options and options[:action] url end end @controller = @controller.new end def test_auto_complete_field assert_dom_equal %(), auto_complete_field("some_input", :url => { :action => "autocomplete" }); assert_dom_equal %(), auto_complete_field("some_input", :url => { :action => "autocomplete" }, :tokens => ','); assert_dom_equal %(), auto_complete_field("some_input", :url => { :action => "autocomplete" }, :tokens => [',']); assert_dom_equal %(), auto_complete_field("some_input", :url => { :action => "autocomplete" }, :min_chars => 3); assert_dom_equal %(), auto_complete_field("some_input", :url => { :action => "autocomplete" }, :on_hide => "function(element, update){alert('me');}"); assert_dom_equal %(), auto_complete_field("some_input", :url => { :action => "autocomplete" }, :frequency => 2); assert_dom_equal %(), auto_complete_field("some_input", :url => { :action => "autocomplete" }, :after_update_element => "function(element,value){alert('You have chosen: '+value)}"); end def test_auto_complete_result result = [ { :title => 'test1' }, { :title => 'test2' } ] assert_equal %(
    • test1
    • test2
    ), auto_complete_result(result, :title) assert_equal %(
    • test1
    • test2
    ), auto_complete_result(result, :title, "est") resultuniq = [ { :title => 'test1' }, { :title => 'test1' } ] assert_equal %(
    • test1
    ), auto_complete_result(resultuniq, :title, "est") end def test_text_field_with_auto_complete assert_match "