# frozen_string_literal: true require "zlib" module ActionView # = Action View Asset URL Helpers module Helpers # :nodoc: # This module provides methods for generating asset paths and # URLs. # # image_path("rails.png") # # => "/assets/rails.png" # # image_url("rails.png") # # => "http://www.example.com/assets/rails.png" # # === 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 ActionController::Base.asset_host in the application # configuration, typically in config/environments/production.rb. # For example, you'd define assets.example.com to be your asset # host this way, inside the configure block of your environment-specific # configuration files or config/application.rb: # # config.action_controller.asset_host = "assets.example.com" # # Helpers take that into account: # # image_tag("rails.png") # # => # stylesheet_link_tag("application") # # => # # Browsers open a limited number of simultaneous connections to a single # host. The exact number varies by browser and version. This limit may cause # some asset downloads to wait for previous assets to finish before they can # begin. You can use the %d wildcard in the +asset_host+ to # distribute the requests over four hosts. For example, # assets%d.example.com will spread the asset requests over # "assets0.example.com", ..., "assets3.example.com". # # image_tag("rails.png") # # => # stylesheet_link_tag("application") # # => # # This may improve the asset loading performance of your application. # It is also possible the combination of additional connection overhead # (DNS, SSL) and the overall browser connection limits may result in this # solution being slower. You should be sure to measure your actual # performance across targeted browsers both before and after this change. # # To implement the corresponding hosts you can either set up four actual # hosts or 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 https://www.die.net/musings/page_load_time/ # for background and https://www.browserscope.org/?category=network for # connection limit data. # # 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#{OpenSSL::Digest::SHA256.hexdigest(source).to_i(16) % 2 + 1}.example.com" # } # image_tag("rails.png") # # => # stylesheet_link_tag("application") # # => # # 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.end_with?('.css') # "http://stylesheets.example.com" # else # "http://assets.example.com" # end # } # image_tag("rails.png") # # => # stylesheet_link_tag("application") # # => # # 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. # Note that the +request+ parameter might not be supplied, e.g. when the assets # are precompiled with the command bin/rails assets:precompile. Make sure to use a # +Proc+ instead of a lambda, since a +Proc+ allows missing parameters and sets them # to +nil+. # # config.action_controller.asset_host = Proc.new { |source, request| # if request && request.ssl? # "#{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 URI_REGEXP = %r{^[-a-z]+://|^(?:cid|data):|^//}i # This is the entry point for all assets. # When using an asset pipeline gem (e.g. propshaft or sprockets-rails), the # behavior is "enhanced". You can bypass the asset pipeline by passing in # skip_pipeline: true to the options. # # All other asset *_path helpers delegate through this method. # # === With the asset pipeline # # All options passed to +asset_path+ will be passed to +compute_asset_path+ # which is implemented by asset pipeline gems. # # asset_path("application.js") # => "/assets/application-60aa4fdc5cea14baf5400fba1abf4f2a46a5166bad4772b1effe341570f07de9.js" # asset_path('application.js', host: 'example.com') # => "//example.com/assets/application.js" # asset_path("application.js", host: 'example.com', protocol: 'https') # => "https://example.com/assets/application.js" # # === Without the asset pipeline (skip_pipeline: true) # # Accepts a type option that can specify the asset's extension. No error # checking is done to verify the source passed into +asset_path+ is valid # and that the file exists on disk. # # asset_path("application.js", skip_pipeline: true) # => "application.js" # asset_path("filedoesnotexist.png", skip_pipeline: true) # => "filedoesnotexist.png" # asset_path("application", type: :javascript, skip_pipeline: true) # => "/javascripts/application.js" # asset_path("application", type: :stylesheet, skip_pipeline: true) # => "/stylesheets/application.css" # # === Options applying to all assets # # Below lists scenarios that apply to +asset_path+ whether or not you're # using the asset pipeline. # # - All fully qualified URLs are returned immediately. This bypasses the # asset pipeline and all other behavior described. # # asset_path("http://www.example.com/js/xmlhr.js") # => "http://www.example.com/js/xmlhr.js" # # - All assets that begin with a forward slash are assumed to be full # URLs and will not be expanded. This will bypass the asset pipeline. # # asset_path("/foo.png") # => "/foo.png" # # - All blank strings will be returned immediately. This bypasses the # asset pipeline and all other behavior described. # # asset_path("") # => "" # # - If config.relative_url_root is specified, all assets will have that # root prepended. # # Rails.application.config.relative_url_root = "bar" # asset_path("foo.js", skip_pipeline: true) # => "bar/foo.js" # # - A different asset host can be specified via config.action_controller.asset_host # this is commonly used in conjunction with a CDN. # # Rails.application.config.action_controller.asset_host = "assets.example.com" # asset_path("foo.js", skip_pipeline: true) # => "http://assets.example.com/foo.js" # # - An extension name can be specified manually with extname. # # asset_path("foo", skip_pipeline: true, extname: ".js") # => "/foo.js" # asset_path("foo.css", skip_pipeline: true, extname: ".js") # => "/foo.css.js" def asset_path(source, options = {}) raise ArgumentError, "nil is not a valid asset source" if source.nil? source = source.to_s return "" if source.blank? return source if URI_REGEXP.match?(source) tail, source = source[/([?#].+)$/], source.sub(/([?#].+)$/, "") if extname = compute_asset_extname(source, options) source = "#{source}#{extname}" end if source[0] != ?/ if options[:skip_pipeline] source = public_compute_asset_path(source, options) else source = compute_asset_path(source, options) end end relative_url_root = defined?(config.relative_url_root) && config.relative_url_root if relative_url_root source = File.join(relative_url_root, source) unless source.start_with?("#{relative_url_root}/") end if host = compute_asset_host(source, options) source = File.join(host, source) end "#{source}#{tail}" end alias_method :path_to_asset, :asset_path # aliased to avoid conflicts with an asset_path named route # Computes the full URL to an asset in the public directory. This # will use +asset_path+ internally, so most of their behaviors # 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. # # asset_url "application.js" # => http://example.com/assets/application.js # asset_url "application.js", host: "http://cdn.example.com" # => http://cdn.example.com/assets/application.js # def asset_url(source, options = {}) path_to_asset(source, options.merge(protocol: :request)) end alias_method :url_to_asset, :asset_url # aliased to avoid conflicts with an asset_url named route 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]] if extname && File.extname(source) != extname extname else nil end end # 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 alias :public_compute_asset_path :compute_asset_path # 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 %d (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 = {}) request = self.request if respond_to?(:request) host = options[:host] host ||= config.asset_host if defined? config.asset_host if host 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.include?("%d") host = host % (Zlib.crc32(source) % 4) end end host ||= request.base_url if request && options[:protocol] == :request return unless host if URI_REGEXP.match?(host) 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 # Computes the path to a JavaScript asset in the public javascripts directory. # 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. # Used internally by +javascript_include_tag+ to build the script path. # # javascript_path "xmlhr" # => /assets/xmlhr.js # javascript_path "dir/xmlhr.js" # => /assets/dir/xmlhr.js # 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 def javascript_path(source, options = {}) path_to_asset(source, { type: :javascript }.merge!(options)) end alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route # Computes the full URL to a JavaScript asset in the public javascripts directory. # This will use +javascript_path+ internally, so most of their behaviors will be the same. # Since +javascript_url+ is based on +asset_url+ method you can set :host options. If :host # 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/js/xmlhr.js # def javascript_url(source, options = {}) url_to_asset(source, { type: :javascript }.merge!(options)) 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. # If the +source+ filename has no extension, .css will be appended (except for explicit URIs). # Full paths from the document root will be passed through. # Used internally by +stylesheet_link_tag+ to build the stylesheet path. # # stylesheet_path "style" # => /assets/style.css # stylesheet_path "dir/style.css" # => /assets/dir/style.css # 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 def stylesheet_path(source, options = {}) path_to_asset(source, { type: :stylesheet }.merge!(options)) 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. # Since +stylesheet_url+ is based on +asset_url+ method you can set :host options. If :host # 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/assets/css/style.css # def stylesheet_url(source, options = {}) url_to_asset(source, { type: :stylesheet }.merge!(options)) 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: # # image_path("edit") # => "/assets/edit" # image_path("edit.png") # => "/assets/edit.png" # image_path("icons/edit.png") # => "/assets/icons/edit.png" # 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. def image_path(source, options = {}) path_to_asset(source, { type: :image }.merge!(options)) 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. # Since +image_url+ is based on +asset_url+ method you can set :host options. If :host # 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/assets/edit.png # def image_url(source, options = {}) url_to_asset(source, { type: :image }.merge!(options)) 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 def video_path(source, options = {}) path_to_asset(source, { type: :video }.merge!(options)) 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. # Since +video_url+ is based on +asset_url+ method you can set :host options. If :host # 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/videos/hd.avi # def video_url(source, options = {}) url_to_asset(source, { type: :video }.merge!(options)) end alias_method :url_to_video, :video_url # aliased to avoid conflicts with a 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 def audio_path(source, options = {}) path_to_asset(source, { type: :audio }.merge!(options)) 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. # Since +audio_url+ is based on +asset_url+ method you can set :host options. If :host # 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/audios/horse.wav # def audio_url(source, options = {}) url_to_asset(source, { type: :audio }.merge!(options)) 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. # # font_path("font") # => /fonts/font # font_path("font.ttf") # => /fonts/font.ttf # font_path("dir/font.ttf") # => /fonts/dir/font.ttf # font_path("/dir/font.ttf") # => /dir/font.ttf # font_path("http://www.example.com/dir/font.ttf") # => http://www.example.com/dir/font.ttf def font_path(source, options = {}) path_to_asset(source, { type: :font }.merge!(options)) end alias_method :path_to_font, :font_path # aliased to avoid conflicts with a 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. # Since +font_url+ is based on +asset_url+ method you can set :host options. If :host # 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/fonts/font.ttf # def font_url(source, options = {}) url_to_asset(source, { type: :font }.merge!(options)) end alias_method :url_to_font, :font_url # aliased to avoid conflicts with a font_url named route end end end