asset_url_helper.rb 19.9 KB
Newer Older
1
require 'zlib'
2 3 4

module ActionView
  # = Action View Asset URL Helpers
A
Alvaro Pereyra 已提交
5
  module Helpers
6 7 8 9
    # This module provides methods for generating asset paths and
    # urls.
    #
    #   image_path("rails.png")
10
    #   # => "/assets/rails.png"
11 12
    #
    #   image_url("rails.png")
13
    #   # => "http://www.example.com/assets/rails.png"
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
    #
    # === Using asset hosts
    #
    # By default, Rails links to these assets on the current host in the public
    # folder, but you can direct Rails to link to assets from a dedicated asset
    # server by setting <tt>ActionController::Base.asset_host</tt> in the application
    # configuration, typically in <tt>config/environments/production.rb</tt>.
    # For example, you'd define <tt>assets.example.com</tt> to be your asset
    # host this way, inside the <tt>configure</tt> block of your environment-specific
    # configuration files or <tt>config/application.rb</tt>:
    #
    #   config.action_controller.asset_host = "assets.example.com"
    #
    # Helpers take that into account:
    #
    #   image_tag("rails.png")
    #   # => <img alt="Rails" src="http://assets.example.com/assets/rails.png" />
    #   stylesheet_link_tag("application")
    #   # => <link href="http://assets.example.com/assets/application.css" media="screen" rel="stylesheet" />
    #
    # Browsers typically open at most two simultaneous connections to a single
    # host, which means your assets often have to wait for other assets to finish
    # downloading. You can alleviate this by using a <tt>%d</tt> wildcard in the
    # +asset_host+. For example, "assets%d.example.com". If that wildcard is
    # present Rails distributes asset requests among the corresponding four hosts
    # "assets0.example.com", ..., "assets3.example.com". With this trick browsers
    # will open eight simultaneous connections rather than two.
    #
    #   image_tag("rails.png")
    #   # => <img alt="Rails" src="http://assets0.example.com/assets/rails.png" />
    #   stylesheet_link_tag("application")
    #   # => <link href="http://assets2.example.com/assets/application.css" media="screen" rel="stylesheet" />
    #
    # To do this, you can either setup four actual hosts, or you can use wildcard
    # DNS to CNAME the wildcard to a single asset host. You can read more about
    # setting up your DNS CNAME records from your ISP.
    #
    # Note: This is purely a browser performance optimization and is not meant
    # for server load balancing. See http://www.die.net/musings/page_load_time/
    # for background.
    #
    # Alternatively, you can exert more control over the asset host by setting
    # +asset_host+ to a proc like this:
    #
    #   ActionController::Base.asset_host = Proc.new { |source|
    #     "http://assets#{Digest::MD5.hexdigest(source).to_i(16) % 2 + 1}.example.com"
    #   }
    #   image_tag("rails.png")
    #   # => <img alt="Rails" src="http://assets1.example.com/assets/rails.png" />
    #   stylesheet_link_tag("application")
    #   # => <link href="http://assets2.example.com/assets/application.css" media="screen" rel="stylesheet" />
    #
    # The example above generates "http://assets1.example.com" and
    # "http://assets2.example.com". This option is useful for example if
    # you need fewer/more than four hosts, custom host names, etc.
    #
    # As you see the proc takes a +source+ parameter. That's a string with the
    # absolute path of the asset, for example "/assets/rails.png".
    #
    #    ActionController::Base.asset_host = Proc.new { |source|
    #      if source.ends_with?('.css')
    #        "http://stylesheets.example.com"
    #      else
    #        "http://assets.example.com"
    #      end
    #    }
    #   image_tag("rails.png")
    #   # => <img alt="Rails" src="http://assets.example.com/assets/rails.png" />
    #   stylesheet_link_tag("application")
    #   # => <link href="http://stylesheets.example.com/assets/application.css" media="screen" rel="stylesheet" />
    #
    # Alternatively you may ask for a second parameter +request+. That one is
    # particularly useful for serving assets from an SSL-protected page. The
    # example proc below disables asset hosting for HTTPS connections, while
    # still sending assets for plain HTTP requests from asset hosts. If you don't
    # have SSL certificates for each of the asset hosts this technique allows you
    # to avoid warnings in the client about mixed media.
91 92 93
    # Note that the request parameter might not be supplied, e.g. when the assets
    # are precompiled via a Rake task. Make sure to use a Proc instead of a lambda,
    # since a Proc allows missing parameters and sets them to nil.
94 95
    #
    #   config.action_controller.asset_host = Proc.new { |source, request|
96
    #     if request && request.ssl?
97 98 99 100 101 102 103 104 105 106 107 108 109 110
    #       "#{request.protocol}#{request.host_with_port}"
    #     else
    #       "#{request.protocol}assets.example.com"
    #     end
    #   }
    #
    # You can also implement a custom asset host object that responds to +call+
    # and takes either one or two parameters just like the proc.
    #
    #   config.action_controller.asset_host = AssetHostingWithMinimumSsl.new(
    #     "http://asset%d.example.com", "https://asset1.example.com"
    #   )
    #
    module AssetUrlHelper
111
      URI_REGEXP = %r{^[-a-z]+://|^(?:cid|data):|^//}i
J
Joshua Peek 已提交
112 113 114 115 116 117 118

      # Computes the path to asset in public directory. If :type
      # options is set, a file extension will be appended and scoped
      # to the corresponding public directory.
      #
      # All other asset *_path helpers delegate through this method.
      #
119 120 121
      #   asset_path "application.js"                     # => /assets/application.js
      #   asset_path "application", type: :javascript     # => /assets/application.js
      #   asset_path "application", type: :stylesheet     # => /assets/application.css
J
Joshua Peek 已提交
122 123
      #   asset_path "http://www.example.com/js/xmlhr.js" # => http://www.example.com/js/xmlhr.js
      def asset_path(source, options = {})
A
Anton Kolomiychuk 已提交
124 125
        raise ArgumentError, "Cannot pass nil as asset source" if source.nil?

126
        source = source.to_s
127
        return "" unless source.present?
128 129
        return source if source =~ URI_REGEXP

130 131
        tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, '')

132 133
        if extname = compute_asset_extname(source, options)
          source = "#{source}#{extname}"
134 135 136 137 138 139
        end

        if source[0] != ?/
          source = compute_asset_path(source, options)
        end

140
        relative_url_root = defined?(config.relative_url_root) && config.relative_url_root
141
        if relative_url_root
P
phoet 已提交
142
          source = File.join(relative_url_root, source) unless source.starts_with?("#{relative_url_root}/")
143 144 145
        end

        if host = compute_asset_host(source, options)
P
phoet 已提交
146
          source = File.join(host, source)
147 148
        end

149
        "#{source}#{tail}"
J
Joshua Peek 已提交
150
      end
N
namusyaka 已提交
151
      alias_method :path_to_asset, :asset_path # aliased to avoid conflicts with an asset_path named route
J
Joshua Peek 已提交
152

N
namusyaka 已提交
153
      # Computes the full URL to an asset in the public directory. This
J
Joshua Peek 已提交
154
      # will use +asset_path+ internally, so most of their behaviors
155 156 157 158 159
      # will be the same. If :host options is set, it overwrites global
      # +config.action_controller.asset_host+ setting.
      #
      # All other options provided are forwarded to +asset_path+ call.
      #
160
      #   asset_url "application.js"                                 # => http://example.com/assets/application.js
161
      #   asset_url "application.js", host: "http://cdn.example.com" # => http://cdn.example.com/assets/application.js
162
      #
J
Joshua Peek 已提交
163
      def asset_url(source, options = {})
164
        path_to_asset(source, options.merge(:protocol => :request))
J
Joshua Peek 已提交
165 166 167
      end
      alias_method :url_to_asset, :asset_url # aliased to avoid conflicts with an asset_url named route

168 169 170 171 172 173 174 175 176 177 178 179 180
      ASSET_EXTENSIONS = {
        javascript: '.js',
        stylesheet: '.css'
      }

      # Compute extname to append to asset path. Returns nil if
      # nothing should be added.
      def compute_asset_extname(source, options = {})
        return if options[:extname] == false
        extname = options[:extname] || ASSET_EXTENSIONS[options[:type]]
        extname if extname && File.extname(source) != extname
      end

181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
      # Maps asset types to public directory.
      ASSET_PUBLIC_DIRECTORIES = {
        audio:      '/audios',
        font:       '/fonts',
        image:      '/images',
        javascript: '/javascripts',
        stylesheet: '/stylesheets',
        video:      '/videos'
      }

      # Computes asset path to public directory. Plugins and
      # extensions can override this method to point to custom assets
      # or generate digested paths or query strings.
      def compute_asset_path(source, options = {})
        dir = ASSET_PUBLIC_DIRECTORIES[options[:type]] || ""
        File.join(dir, source)
      end

      # Pick an asset host for this source. Returns +nil+ if no host is set,
      # the host if no wildcard is set, the host interpolated with the
      # numbers 0-3 if it contains <tt>%d</tt> (the number is the source hash mod 4),
      # or the value returned from invoking call on an object responding to call
      # (proc or otherwise).
      def compute_asset_host(source = "", options = {})
205
        request = self.request if respond_to?(:request)
206 207
        host = options[:host]
        host ||= config.asset_host if defined? config.asset_host
208 209 210 211 212 213 214 215 216 217

        if host.respond_to?(:call)
          arity = host.respond_to?(:arity) ? host.arity : host.method(:call).arity
          args = [source]
          args << request if request && (arity > 1 || arity < 0)
          host = host.call(*args)
        elsif host =~ /%d/
          host = host % (Zlib.crc32(source) % 4)
        end

218
        host ||= request.base_url if request && options[:protocol] == :request
219 220
        return unless host

221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
        if host =~ URI_REGEXP
          host
        else
          protocol = options[:protocol] || config.default_asset_host_protocol || (request ? :request : :relative)
          case protocol
          when :relative
            "//#{host}"
          when :request
            "#{request.protocol}#{host}"
          else
            "#{protocol}://#{host}"
          end
        end
      end

236
      # Computes the path to a JavaScript asset in the public javascripts directory.
237 238
      # If the +source+ filename has no extension, .js will be appended (except for explicit URIs)
      # Full paths from the document root will be passed through.
239
      # Used internally by +javascript_include_tag+ to build the script path.
240
      #
241 242
      #   javascript_path "xmlhr"                              # => /assets/xmlhr.js
      #   javascript_path "dir/xmlhr.js"                       # => /assets/dir/xmlhr.js
243 244 245
      #   javascript_path "/dir/xmlhr"                         # => /dir/xmlhr.js
      #   javascript_path "http://www.example.com/js/xmlhr"    # => http://www.example.com/js/xmlhr
      #   javascript_path "http://www.example.com/js/xmlhr.js" # => http://www.example.com/js/xmlhr.js
246
      def javascript_path(source, options = {})
J
Joshua Peek 已提交
247
        path_to_asset(source, {type: :javascript}.merge!(options))
248 249 250
      end
      alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route

251
      # Computes the full URL to a JavaScript asset in the public javascripts directory.
252
      # This will use +javascript_path+ internally, so most of their behaviors will be the same.
253
      # Since +javascript_url+ is based on +asset_url+ method you can set :host options. If :host
254 255 256 257
      # options is set, it overwrites global +config.action_controller.asset_host+ setting.
      #
      #   javascript_url "js/xmlhr.js", host: "http://stage.example.com" # => http://stage.example.com/assets/dir/xmlhr.js
      #
258
      def javascript_url(source, options = {})
J
Joshua Peek 已提交
259
        url_to_asset(source, {type: :javascript}.merge!(options))
260 261 262 263
      end
      alias_method :url_to_javascript, :javascript_url # aliased to avoid conflicts with a javascript_url named route

      # Computes the path to a stylesheet asset in the public stylesheets directory.
264
      # If the +source+ filename has no extension, .css will be appended (except for explicit URIs).
265 266 267
      # Full paths from the document root will be passed through.
      # Used internally by +stylesheet_link_tag+ to build the stylesheet path.
      #
268 269
      #   stylesheet_path "style"                                  # => /assets/style.css
      #   stylesheet_path "dir/style.css"                          # => /assets/dir/style.css
270 271 272
      #   stylesheet_path "/dir/style.css"                         # => /dir/style.css
      #   stylesheet_path "http://www.example.com/css/style"       # => http://www.example.com/css/style
      #   stylesheet_path "http://www.example.com/css/style.css"   # => http://www.example.com/css/style.css
273
      def stylesheet_path(source, options = {})
J
Joshua Peek 已提交
274
        path_to_asset(source, {type: :stylesheet}.merge!(options))
275 276 277 278 279
      end
      alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route

      # Computes the full URL to a stylesheet asset in the public stylesheets directory.
      # This will use +stylesheet_path+ internally, so most of their behaviors will be the same.
280
      # Since +stylesheet_url+ is based on +asset_url+ method you can set :host options. If :host
281 282 283 284
      # options is set, it overwrites global +config.action_controller.asset_host+ setting.
      #
      #   stylesheet_url "css/style.css", host: "http://stage.example.com" # => http://stage.example.com/css/style.css
      #
285
      def stylesheet_url(source, options = {})
J
Joshua Peek 已提交
286
        url_to_asset(source, {type: :stylesheet}.merge!(options))
287 288 289 290 291 292 293
      end
      alias_method :url_to_stylesheet, :stylesheet_url # aliased to avoid conflicts with a stylesheet_url named route

      # Computes the path to an image asset.
      # Full paths from the document root will be passed through.
      # Used internally by +image_tag+ to build the image path:
      #
294 295 296
      #   image_path("edit")                                         # => "/assets/edit"
      #   image_path("edit.png")                                     # => "/assets/edit.png"
      #   image_path("icons/edit.png")                               # => "/assets/icons/edit.png"
297 298 299 300 301 302
      #   image_path("/icons/edit.png")                              # => "/icons/edit.png"
      #   image_path("http://www.example.com/img/edit.png")          # => "http://www.example.com/img/edit.png"
      #
      # If you have images as application resources this method may conflict with their named routes.
      # The alias +path_to_image+ is provided to avoid that. Rails uses the alias internally, and
      # plugin authors are encouraged to do so.
303
      def image_path(source, options = {})
J
Joshua Peek 已提交
304
        path_to_asset(source, {type: :image}.merge!(options))
305 306 307 308 309
      end
      alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route

      # Computes the full URL to an image asset.
      # This will use +image_path+ internally, so most of their behaviors will be the same.
310
      # Since +image_url+ is based on +asset_url+ method you can set :host options. If :host
311 312 313 314
      # options is set, it overwrites global +config.action_controller.asset_host+ setting.
      #
      #   image_url "edit.png", host: "http://stage.example.com" # => http://stage.example.com/edit.png
      #
315
      def image_url(source, options = {})
J
Joshua Peek 已提交
316
        url_to_asset(source, {type: :image}.merge!(options))
317 318 319 320 321 322 323 324 325 326 327 328
      end
      alias_method :url_to_image, :image_url # aliased to avoid conflicts with an image_url named route

      # Computes the path to a video asset in the public videos directory.
      # Full paths from the document root will be passed through.
      # Used internally by +video_tag+ to build the video path.
      #
      #   video_path("hd")                                            # => /videos/hd
      #   video_path("hd.avi")                                        # => /videos/hd.avi
      #   video_path("trailers/hd.avi")                               # => /videos/trailers/hd.avi
      #   video_path("/trailers/hd.avi")                              # => /trailers/hd.avi
      #   video_path("http://www.example.com/vid/hd.avi")             # => http://www.example.com/vid/hd.avi
329
      def video_path(source, options = {})
J
Joshua Peek 已提交
330
        path_to_asset(source, {type: :video}.merge!(options))
331 332 333 334 335
      end
      alias_method :path_to_video, :video_path # aliased to avoid conflicts with a video_path named route

      # Computes the full URL to a video asset in the public videos directory.
      # This will use +video_path+ internally, so most of their behaviors will be the same.
336
      # Since +video_url+ is based on +asset_url+ method you can set :host options. If :host
337 338 339 340
      # options is set, it overwrites global +config.action_controller.asset_host+ setting.
      #
      #   video_url "hd.avi", host: "http://stage.example.com" # => http://stage.example.com/hd.avi
      #
341
      def video_url(source, options = {})
J
Joshua Peek 已提交
342
        url_to_asset(source, {type: :video}.merge!(options))
343 344 345 346 347 348 349 350 351 352 353 354
      end
      alias_method :url_to_video, :video_url # aliased to avoid conflicts with an video_url named route

      # Computes the path to an audio asset in the public audios directory.
      # Full paths from the document root will be passed through.
      # Used internally by +audio_tag+ to build the audio path.
      #
      #   audio_path("horse")                                            # => /audios/horse
      #   audio_path("horse.wav")                                        # => /audios/horse.wav
      #   audio_path("sounds/horse.wav")                                 # => /audios/sounds/horse.wav
      #   audio_path("/sounds/horse.wav")                                # => /sounds/horse.wav
      #   audio_path("http://www.example.com/sounds/horse.wav")          # => http://www.example.com/sounds/horse.wav
355
      def audio_path(source, options = {})
J
Joshua Peek 已提交
356
        path_to_asset(source, {type: :audio}.merge!(options))
357 358 359 360 361
      end
      alias_method :path_to_audio, :audio_path # aliased to avoid conflicts with an audio_path named route

      # Computes the full URL to an audio asset in the public audios directory.
      # This will use +audio_path+ internally, so most of their behaviors will be the same.
362
      # Since +audio_url+ is based on +asset_url+ method you can set :host options. If :host
363 364 365 366
      # options is set, it overwrites global +config.action_controller.asset_host+ setting.
      #
      #   audio_url "horse.wav", host: "http://stage.example.com" # => http://stage.example.com/horse.wav
      #
367
      def audio_url(source, options = {})
J
Joshua Peek 已提交
368
        url_to_asset(source, {type: :audio}.merge!(options))
369 370 371 372 373 374
      end
      alias_method :url_to_audio, :audio_url # aliased to avoid conflicts with an audio_url named route

      # Computes the path to a font asset.
      # Full paths from the document root will be passed through.
      #
375 376 377
      #   font_path("font")                                           # => /fonts/font
      #   font_path("font.ttf")                                       # => /fonts/font.ttf
      #   font_path("dir/font.ttf")                                   # => /fonts/dir/font.ttf
378 379
      #   font_path("/dir/font.ttf")                                  # => /dir/font.ttf
      #   font_path("http://www.example.com/dir/font.ttf")            # => http://www.example.com/dir/font.ttf
380
      def font_path(source, options = {})
J
Joshua Peek 已提交
381
        path_to_asset(source, {type: :font}.merge!(options))
382 383 384 385 386
      end
      alias_method :path_to_font, :font_path # aliased to avoid conflicts with an font_path named route

      # Computes the full URL to a font asset.
      # This will use +font_path+ internally, so most of their behaviors will be the same.
387
      # Since +font_url+ is based on +asset_url+ method you can set :host options. If :host
388 389 390 391
      # options is set, it overwrites global +config.action_controller.asset_host+ setting.
      #
      #   font_url "font.ttf", host: "http://stage.example.com" # => http://stage.example.com/font.ttf
      #
392
      def font_url(source, options = {})
J
Joshua Peek 已提交
393
        url_to_asset(source, {type: :font}.merge!(options))
394 395 396 397 398
      end
      alias_method :url_to_font, :font_url # aliased to avoid conflicts with an font_url named route
    end
  end
end