Skip to content

Commit 590123b

Browse files
committed
Expose the asset integrity in the Propshaft helper
And allow it to be added to the asset tags when the `integrity: true` option is passed.
1 parent 5c39270 commit 590123b

File tree

4 files changed

+145
-9
lines changed

4 files changed

+145
-9
lines changed

lib/propshaft/helper.rb

Lines changed: 133 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,102 @@
11
module Propshaft
2+
# Helper module that provides asset path resolution and integrity support for Rails applications.
3+
#
4+
# This module extends Rails' built-in asset helpers with additional functionality:
5+
# - Subresource Integrity (SRI) support for enhanced security
6+
# - Bulk stylesheet inclusion with :all and :app options
7+
# - Asset path resolution with proper error handling
8+
#
9+
# == Subresource Integrity (SRI) Support
10+
#
11+
# SRI helps protect against malicious modifications of assets by ensuring that
12+
# resources fetched from CDNs or other sources haven't been tampered with.
13+
#
14+
# SRI is automatically enabled in secure contexts (HTTPS or local development)
15+
# when the 'integrity' option is set to true:
16+
#
17+
# <%= stylesheet_link_tag "application", integrity: true %>
18+
# <%= javascript_include_tag "application", integrity: true %>
19+
#
20+
# This will generate integrity hashes and include them in the HTML:
21+
#
22+
# <link rel="stylesheet" href="/assets/application-abc123.css"
23+
# integrity="sha256-xyz789...">
24+
# <script src="/assets/application-def456.js"
25+
# integrity="sha256-uvw012..."></script>
26+
#
27+
# == Bulk Stylesheet Inclusion
28+
#
29+
# The stylesheet_link_tag helper supports special symbols for bulk inclusion:
30+
# - :all - includes all CSS files found in the load path
31+
# - :app - includes only CSS files from app/assets/**/*.css
32+
#
33+
# <%= stylesheet_link_tag :all %> # All stylesheets
34+
# <%= stylesheet_link_tag :app %> # Only app stylesheets
235
module Helper
36+
# Computes the Subresource Integrity (SRI) hash for the given asset path.
37+
#
38+
# This method generates a cryptographic hash of the asset content that can be used
39+
# to verify the integrity of the resource when it's loaded by the browser.
40+
#
41+
# asset_integrity("application.css")
42+
# # => "sha256-xyz789abcdef..."
43+
def asset_integrity(path, options = {})
44+
path = _path_with_extname(path, options)
45+
Rails.application.assets.resolver.integrity(path)
46+
end
47+
48+
# Resolves the full path for an asset, raising an error if not found.
349
def compute_asset_path(path, options = {})
450
Rails.application.assets.resolver.resolve(path) || raise(MissingAssetError.new(path))
551
end
652

7-
# Add an option to call `stylesheet_link_tag` with `:all` to include every css file found on the load path
8-
# or `:app` to include css files found in `Rails.root("app/assets/**/*.css")`, which will exclude lib/ and plugins.
53+
# Enhanced +stylesheet_link_tag+ with integrity support and bulk inclusion options.
54+
#
55+
# In addition to the standard Rails functionality, this method supports:
56+
# * Automatic SRI (Subresource Integrity) hash generation in secure contexts
57+
# * Add an option to call +stylesheet_link_tag+ with +:all+ to include every css
58+
# file found on the load path or +:app+ to include css files found in
59+
# <tt>Rails.root("app/assets/**/*.css")</tt>, which will exclude lib/ and plugins.
60+
#
61+
# ==== Options
62+
#
63+
# * <tt>:integrity</tt> - Enable SRI hash generation
64+
#
65+
# ==== Examples
66+
#
67+
# stylesheet_link_tag "application", integrity: true
68+
# # => <link rel="stylesheet" href="/assets/application-abc123.css"
69+
# # integrity="sha256-xyz789...">
70+
#
71+
# stylesheet_link_tag :all # All stylesheets in load path
72+
# stylesheet_link_tag :app # Only app/assets stylesheets
973
def stylesheet_link_tag(*sources, **options)
1074
case sources.first
1175
when :all
12-
super(*all_stylesheets_paths , **options)
76+
sources = all_stylesheets_paths
1377
when :app
14-
super(*app_stylesheets_paths , **options)
15-
else
16-
super
78+
sources = app_stylesheets_paths
1779
end
80+
81+
_build_asset_tags(sources, options, :stylesheet) { |source, opts| super(source, opts) }
82+
end
83+
84+
# Enhanced +javascript_include_tag+ with automatic SRI (Subresource Integrity) support.
85+
#
86+
# This method extends Rails' built-in +javascript_include_tag+ to automatically
87+
# generate and include integrity hashes when running in secure contexts.
88+
#
89+
# ==== Options
90+
#
91+
# * <tt>:integrity</tt> - Enable SRI hash generation
92+
#
93+
# ==== Examples
94+
#
95+
# javascript_include_tag "application", integrity: true
96+
# # => <script src="/assets/application-abc123.js"
97+
# # integrity="sha256-xyz789..."></script>
98+
def javascript_include_tag(*sources, **options)
99+
_build_asset_tags(sources, options, :javascript) { |source, opts| super(source, opts) }
18100
end
19101

20102
# Returns a sorted and unique array of logical paths for all stylesheets in the load path.
@@ -26,5 +108,50 @@ def all_stylesheets_paths
26108
def app_stylesheets_paths
27109
Rails.application.assets.load_path.asset_paths_by_glob("#{Rails.root.join("app/assets")}/**/*.css")
28110
end
111+
112+
private
113+
# Core method that builds asset tags with optional integrity support.
114+
#
115+
# This method handles the common logic for both +stylesheet_link_tag+ and
116+
# +javascript_include_tag+, including SRI hash generation and HTML tag creation.
117+
def _build_asset_tags(sources, options, asset_type)
118+
options = options.stringify_keys
119+
integrity = _compute_integrity?(options)
120+
121+
sources.map { |source|
122+
opts = integrity ? options.merge!('integrity' => asset_integrity(source, type: asset_type)) : options
123+
yield(source, opts)
124+
}.join("\n").html_safe
125+
end
126+
127+
# Determines whether integrity hashes should be computed for assets.
128+
#
129+
# Integrity is only computed in secure contexts (HTTPS or local development)
130+
# and when explicitly requested via the +integrity+ option.
131+
def _compute_integrity?(options)
132+
if _secure_subresource_integrity_context?
133+
case options['integrity']
134+
when nil, false, true
135+
options.delete('integrity') == true
136+
end
137+
else
138+
options.delete 'integrity'
139+
false
140+
end
141+
end
142+
143+
# Checks if the current context is secure enough for Subresource Integrity.
144+
#
145+
# SRI is only beneficial in secure contexts. Returns true when:
146+
# * The request is made over HTTPS (SSL), OR
147+
# * The request is local (development environment)
148+
def _secure_subresource_integrity_context?
149+
respond_to?(:request) && self.request && (self.request.local? || self.request.ssl?)
150+
end
151+
152+
# Ensures the asset path includes the appropriate file extension.
153+
def _path_with_extname(path, options)
154+
"#{path}#{compute_asset_extname(path, options)}"
155+
end
29156
end
30157
end
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
<%= stylesheet_link_tag "hello_world" %>
1+
<%= stylesheet_link_tag "hello_world", integrity: true %>
22

33
<h1>Sample#load_real_assets</h1>
44
<p>Find me in app/views/sample/load_real_assets.html.erb</p>
55

6-
<%= javascript_include_tag "hello_world" %>
6+
<%= javascript_include_tag "hello_world", integrity: true %>
77
<%= javascript_include_tag "actioncable" %>

test/dummy/config/application.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,7 @@ class Application < Rails::Application
3131
#
3232
# config.time_zone = "Central Time (US & Canada)"
3333
# config.eager_load_paths << Rails.root.join("extras")
34+
35+
config.assets.integrity_hash_algorithm = "sha384"
3436
end
3537
end

test/propshaft_integration_test.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@ class PropshaftIntegrationTest < ActionDispatch::IntegrationTest
1010
assert_select 'link[href="/assets/goodbye-b1dc9940.css"][data-custom-attribute="true"]'
1111
assert_select 'link[href="/assets/library-86a3b7a9.css"][data-custom-attribute="true"]'
1212

13-
assert_select 'script[src="/assets/hello_world-888761f8.js"]'
13+
hello_css_link = css_select('link[href="/assets/hello_world-4137140a.css"][integrity]').first
14+
assert(hello_css_link)
15+
assert_equal "stylesheet", hello_css_link["rel"]
16+
assert_equal "sha384-ZSAt6UaTZ1OYvSB1fr2WXE8izMW4qnd17BZ1zaZ3TpAdIw3VEUmyupHd/k/cMCqM", hello_css_link["integrity"]
17+
18+
hello_js_script = css_select('script[src="/assets/hello_world-888761f8.js"]').first
19+
assert(hello_js_script)
20+
assert_equal "sha384-BIr0kyMRq2sfytK/T0XlGjfav9ZZrWkSBC2yHVunCchnkpP83H28/UtHw+m9iNHO", hello_js_script["integrity"]
1421
end
1522

1623
test "should prioritize app assets over engine assets" do

0 commit comments

Comments
 (0)