url_helper.rb 17.8 KB
Newer Older
1
require 'action_view/helpers/javascript_helper'
2

D
Initial  
David Heinemeier Hansson 已提交
3
module ActionView
4
  module Helpers #:nodoc:
5 6
    # Provides a set of methods for making easy links and getting urls that
    # depend on the controller and action. This means that you can use the
7
    # same format for links in the views that you do in the controller.
D
Initial  
David Heinemeier Hansson 已提交
8
    module UrlHelper
9
      include JavaScriptHelper
10 11 12 13 14

      # Returns the URL for the set of +options+ provided. This takes the
      # same options as url_for in action controller. For a list, see the
      # documentation for ActionController::Base#url_for. Note that it'll
      # set :only_path => true so you'll get the relative /controller/action
15
      # instead of the fully qualified http://example.com/controller/action.
16 17
      #
      # When called from a view, url_for returns an HTML escaped url. If you
18
      # need an unescaped url, pass :escape => false in the +options+.
D
Initial  
David Heinemeier Hansson 已提交
19
      def url_for(options = {}, *parameters_for_method_reference)
20 21 22 23 24 25
        if options.kind_of? Hash
          options = { :only_path => true }.update(options.symbolize_keys)
          escape = options.key?(:escape) ? options.delete(:escape) : true
        else
          escape = true
        end
26

27 28
        url = @controller.send(:url_for, options, *parameters_for_method_reference)
        escape ? html_escape(url) : url
D
Initial  
David Heinemeier Hansson 已提交
29 30
      end

31 32 33 34 35
      # Creates a link tag of the given +name+ using a URL created by the set
      # of +options+. See the valid options in the documentation for
      # ActionController::Base#url_for. It's also possible to pass a string instead
      # of an options hash to get a link tag that uses the value of the string as the
      # href for the link. If nil is passed as a name, the link itself will become
36
      # the name.
37
      #
38
      # The +html_options+ will accept a hash of html attributes for the link tag.
39
      # It also accepts 3 modifiers that specialize the link behavior.
40
      #
41 42
      # * <tt>:confirm => 'question?'</tt>: This will add a JavaScript confirm
      #   prompt with the question specified. If the user accepts, the link is
43
      #   processed normally, otherwise no action is taken.
44 45 46
      # * <tt>:popup => true || array of window options</tt>: This will force the
      #   link to open in a popup window. By passing true, a default browser window
      #   will be opened with the URL. You can also specify an array of options
47 48
      #   that are passed-thru to JavaScripts window.open method.
      # * <tt>:method => symbol of HTTP verb</tt>: This modifier will dynamically
49
      #   create an HTML form and immediately submit the form for processing using
50 51 52
      #   the HTTP verb specified. Useful for having links perform a POST operation
      #   in dangerous actions like deleting a record (which search bots can follow
      #   while spidering your site). Supported verbs are :post, :delete and :put.
53
      #   Note that if the user has JavaScript disabled, the request will fall back
54 55 56
      #   to using GET. If you are relying on the POST behavior, your should check
      #   for it in your controllers action by using the request objects methods
      #   for post?, delete? or put?.
57
      #
58 59 60
      # You can mix and match the +html_options+ with the exception of
      # :popup and :method which will raise an ActionView::ActionViewError
      # exception.
61
      #
62
      #   link_to "Visit Other Site", "http://www.rubyonrails.org/", :confirm => "Are you sure?"
63
      #   link_to "Help", { :action => "help" }, :popup => true
64 65
      #   link_to "View Image", { :action => "view" }, :popup => ['new_window_name', 'height=300,width=600']
      #   link_to "Delete Image", { :action => "delete", :id => @image.id }, :confirm => "Are you sure?", :method => :delete
66
      def link_to(name, options = {}, html_options = nil, *parameters_for_method_reference)
67 68 69 70
        if html_options
          html_options = html_options.stringify_keys
          convert_options_to_javascript!(html_options)
          tag_options = tag_options(html_options)
D
Initial  
David Heinemeier Hansson 已提交
71
        else
72
          tag_options = nil
D
Initial  
David Heinemeier Hansson 已提交
73
        end
74

75 76
        url = options.is_a?(String) ? options : self.url_for(options, *parameters_for_method_reference)
        "<a href=\"#{url}\"#{tag_options}>#{name || url}</a>"
D
Initial  
David Heinemeier Hansson 已提交
77 78
      end

79 80 81 82 83 84
      # Generates a form containing a single button that submits to the URL created
      # by the set of +options+. This is the safest method to ensure links that
      # cause changes to your data are not triggered by search bots or accelerators.
      # If the HTML button does not work with your layout, you can also consider
      # using the link_to method with the <tt>:method</tt> modifier as described in
      # the link_to documentation.
85
      #
86 87 88 89 90
      # The generated FORM element has a class name of <tt>button-to</tt>
      # to allow styling of the form itself and its children. You can control
      # the form submission and input element behavior using +html_options+.
      # This method accepts the <tt>:method</tt> and <tt>:confirm</tt> modifiers
      # described in the link_to documentation. If no <tt>:method</tt> modifier
91
      # is given, it will default to performing a POST operation. You can also
92
      # disable the button by passing <tt>:disabled => true</tt> in +html_options+.
93
      #
94
      #   button_to "New", :action => "new"
95
      #
96
      # Generates the following HTML:
97
      #
98 99
      #   <form method="post" action="/controller/new" class="button-to">
      #     <div><input value="New" type="submit" /></div>
100 101
      #   </form>
      #
102 103
      # If you are using RESTful routes, you can pass the <tt>:method</tt>
      # to change the HTTP verb used to submit the form.
104
      #
105
      #   button_to "Delete Image", { :action => "delete", :id => @image.id },
106
      #             :confirm => "Are you sure?", :method => :delete
107
      #
108
      # Which generates the following HTML:
109
      #
110
      #   <form method="post" action="/images/delete/1" class="button-to">
111 112 113
      #     <div>
      #       <input type="hidden" name="_method" value="delete" />
      #       <input onclick="return confirm('Are you sure?');"
114
      #                 value="Delete" type="submit" />
115 116
      #     </div>
      #   </form>
117 118
      def button_to(name, options = {}, html_options = {})
        html_options = html_options.stringify_keys
119
        convert_boolean_attributes!(html_options, %w( disabled ))
120 121 122 123 124 125 126 127

        method_tag = ''
        if (method = html_options.delete('method')) && %w{put delete}.include?(method.to_s)
          method_tag = tag('input', :type => 'hidden', :name => '_method', :value => method.to_s)
        end

        form_method = method.to_s == 'get' ? 'get' : 'post'

128 129 130
        if confirm = html_options.delete("confirm")
          html_options["onclick"] = "return #{confirm_javascript_function(confirm)};"
        end
131

132
        url = options.is_a?(String) ? options : self.url_for(options)
133
        name ||= url
134

135
        html_options.merge!("type" => "submit", "value" => name)
136

137 138
        "<form method=\"#{form_method}\" action=\"#{escape_once url}\" class=\"button-to\"><div>" +
          method_tag + tag("input", html_options) + "</div></form>"
139 140
      end

141

142
      # Creates a link tag of the given +name+ using a URL created by the set of
143
      # +options+ unless the current request uri is the same as the links, in
144 145 146 147 148 149 150 151 152 153 154 155 156 157
      # which case only the name is returned (or the given block is yielded, if
      # one exists). Refer to the documentation for link_to_unless for block usage.
      #
      #   <ul id="navbar">
      #     <li><%= link_to_unless_current("Home", { :action => "index" }) %></li>
      #     <li><%= link_to_unless_current("About Us", { :action => "about" }) %></li>
      #   </ul>
      #
      # This will render the following HTML when on the about us page:
      #
      #   <ul id="navbar">
      #     <li><a href="/controller/index">Home</a></li>
      #     <li>About Us</li>
      #   </ul>
158 159 160 161
      def link_to_unless_current(name, options = {}, html_options = {}, *parameters_for_method_reference, &block)
        link_to_unless current_page?(options), name, options, html_options, *parameters_for_method_reference, &block
      end

162
      # Creates a link tag of the given +name+ using a URL created by the set of
163
      # +options+ unless +condition+ is true, in which case only the name is
164 165 166 167 168 169 170 171 172
      # returned. To specialize the default behavior, you can pass a block that
      # accepts the name or the full argument list for link_to_unless (see the example).
      #
      #   <%= link_to_unless(@current_user.nil?, "Reply", { :action => "reply" }) %>
      #
      # This example uses a block to modify the link if the condition isn't met.
      #
      #   <%= link_to_unless(@current_user.nil?, "Reply", { :action => "reply" }) do |name|
      #      link_to(name, { :controller => "accounts", :action => "signup" })
173
      #    end %>
174 175 176 177 178
      def link_to_unless(condition, name, options = {}, html_options = {}, *parameters_for_method_reference, &block)
        if condition
          if block_given?
            block.arity <= 1 ? yield(name) : yield(name, options, html_options, *parameters_for_method_reference)
          else
179
            name
180
          end
D
Initial  
David Heinemeier Hansson 已提交
181
        else
182
          link_to(name, options, html_options, *parameters_for_method_reference)
183
        end
184
      end
185

186
      # Creates a link tag of the given +name+ using a URL created by the set of
187
      # +options+ if +condition+ is true, in which case only the name is
188 189 190
      # returned. To specialize the default behavior, you can pass a block that
      # accepts the name or the full argument list for link_to_unless (see the examples
      # in link_to_unless).
191 192
      def link_to_if(condition, name, options = {}, html_options = {}, *parameters_for_method_reference, &block)
        link_to_unless !condition, name, options, html_options, *parameters_for_method_reference, &block
D
Initial  
David Heinemeier Hansson 已提交
193 194
      end

195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
      # Creates a mailto link tag to the specified +email_address+, which is
      # also used as the name of the link unless +name+ is specified. Additional
      # html attributes for the link can be passed in +html_options+.
      #
      # mail_to has several methods for hindering email harvestors and customizing
      # the email itself by passing special keys to +html_options+.
      #
      # Special HTML Options:
      #
      # * <tt>:encode</tt>  - This key will accept the strings "javascript" or "hex".
      #   Passing "javascript" will dynamically create and encode the mailto: link then
      #   eval it into the DOM of the page. This method will not show the link on
      #   the page if the user has JavaScript disabled. Passing "hex" will hex
      #   encode the +email_address+ before outputting the mailto: link.
      # * <tt>:replace_at</tt>  - When the link +name+ isn't provided, the
      #   +email_address+ is used for the link label. You can use this option to
      #   obfuscate the +email_address+ by substituting the @ sign with the string
      #   given as the value.
      # * <tt>:replace_dot</tt>  - When the link +name+ isn't provided, the
      #   +email_address+ is used for the link label. You can use this option to
      #   obfuscate the +email_address+ by substituting the . in the email with the
      #   string given as the value.
      # * <tt>:subject</tt>  - Preset the subject line of the email.
      # * <tt>:body</tt> - Preset the body of the email.
      # * <tt>:cc</tt>  - Carbon Copy addition recipients on the email.
      # * <tt>:bcc</tt>  - Blind Carbon Copy additional recipients on the email.
221
      #
222
      # Examples:
223
      #   mail_to "me@domain.com"  # => <a href="mailto:me@domain.com">me@domain.com</a>
224
      #   mail_to "me@domain.com", "My email", :encode => "javascript"  # =>
225
      #     <script type="text/javascript">eval(unescape('%64%6f%63...%6d%65%6e'))</script>
226 227 228
      #
      #   mail_to "me@domain.com", "My email", :encode => "hex"  # =>
      #     <a href="mailto:%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d">My email</a>
229
      #
230 231 232
      #   mail_to "me@domain.com", nil, :replace_at => "_at_", :replace_dot => "_dot_", :class => "email"  # =>
      #     <a href="mailto:me@domain.com" class="email">me_at_domain_dot_com</a>
      #
233
      #   mail_to "me@domain.com", "My email", :cc => "ccaddress@domain.com",
234 235
      #            :subject => "This is an example email"  # =>
      #     <a href="mailto:me@domain.com?cc=ccaddress@domain.com&subject=This%20is%20an%20example%20email">My email</a>
D
Initial  
David Heinemeier Hansson 已提交
236
      def mail_to(email_address, name = nil, html_options = {})
237
        html_options = html_options.stringify_keys
238
        encode = html_options.delete("encode").to_s
239 240
        cc, bcc, subject, body = html_options.delete("cc"), html_options.delete("bcc"), html_options.delete("subject"), html_options.delete("body")

241
        string = ''
242 243 244 245 246 247 248
        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?

249 250
        email_address = email_address.to_s

251 252 253 254
        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")

255 256
        if encode == "javascript"
          tmp = "document.write('#{content_tag("a", name || email_address, html_options.merge({ "href" => "mailto:"+email_address+extras }))}');"
257 258 259
          for i in 0...tmp.length
            string << sprintf("%%%x",tmp[i])
          end
260
          "<script type=\"#{Mime::JS}\">eval(unescape('#{string}'))</script>"
261
        elsif encode == "hex"
262 263 264 265 266 267 268 269
          email_address_encoded = ''
          email_address_obfuscated.each_byte do |c|
            email_address_encoded << sprintf("&#%d;", c)
          end

          protocol = 'mailto:'
          protocol.each_byte { |c| string << sprintf("&#%d;", c) }

270 271 272 273 274 275 276
          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
277
          content_tag "a", name || email_address_encoded, html_options.merge({ "href" => "#{string}#{extras}" })
278
        else
279
          content_tag "a", name || email_address_obfuscated, html_options.merge({ "href" => "mailto:#{email_address}#{extras}" })
280
        end
D
Initial  
David Heinemeier Hansson 已提交
281 282
      end

283
      # True if the current request uri was generated by the given +options+.
284
      def current_page?(options)
285 286 287 288 289 290 291
        url_string = CGI.escapeHTML(url_for(options))
        request = @controller.request
        if url_string =~ /^\w+:\/\//
          url_string == "#{request.protocol}#{request.host_with_port}#{request.request_uri}"
        else
          url_string == request.request_uri
        end
292 293
      end

D
Initial  
David Heinemeier Hansson 已提交
294
      private
295
        def convert_options_to_javascript!(html_options)
296 297
          confirm, popup = html_options.delete("confirm"), html_options.delete("popup")

298 299
          method = html_options.delete("method")

300
          html_options["onclick"] = case
301
            when popup && method
302
              raise ActionView::ActionViewError, "You can't use :popup and :method in the same link"
303 304
            when confirm && popup
              "if (#{confirm_javascript_function(confirm)}) { #{popup_javascript_function(popup)} };return false;"
305 306
            when confirm && method
              "if (#{confirm_javascript_function(confirm)}) { #{method_javascript_function(method)} };return false;"
307 308
            when confirm
              "return #{confirm_javascript_function(confirm)};"
309 310
            when method
              "#{method_javascript_function(method)}return false;"
311 312
            when popup
              popup_javascript_function(popup) + 'return false;'
313 314
            else
              html_options["onclick"]
D
Initial  
David Heinemeier Hansson 已提交
315 316
          end
        end
317

318 319
        def confirm_javascript_function(confirm)
          "confirm('#{escape_javascript(confirm)}')"
320
        end
321

322 323
        def popup_javascript_function(popup)
          popup.is_a?(Array) ? "window.open(this.href,'#{popup.first}','#{popup.last}');" : "window.open(this.href);"
324
        end
325

326
        def method_javascript_function(method)
327
          submit_function =
328 329
            "var f = document.createElement('form'); f.style.display = 'none'; " +
            "this.parentNode.appendChild(f); f.method = 'POST'; f.action = this.href;"
330

331 332 333 334
          unless method == :post
            submit_function << "var m = document.createElement('input'); m.setAttribute('type', 'hidden'); "
            submit_function << "m.setAttribute('name', '_method'); m.setAttribute('value', '#{method}'); f.appendChild(m);"
          end
335

336
          submit_function << "f.submit();"
337 338
        end

339 340 341 342 343 344 345 346 347 348
        # 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
        #
D
David Heinemeier Hansson 已提交
349
        # if the associated _bool_value_ evaluates to true, it is
350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365
        # 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
D
Initial  
David Heinemeier Hansson 已提交
366 367
    end
  end
368
end