hanami 2.2.1 → 2.3.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -1
  3. data/README.md +20 -35
  4. data/hanami.gemspec +3 -2
  5. data/lib/hanami/app.rb +2 -0
  6. data/lib/hanami/config/actions/content_security_policy.rb +23 -0
  7. data/lib/hanami/config/actions.rb +21 -0
  8. data/lib/hanami/config/console.rb +79 -0
  9. data/lib/hanami/config/logger.rb +1 -1
  10. data/lib/hanami/config.rb +13 -0
  11. data/lib/hanami/constants.rb +3 -0
  12. data/lib/hanami/extensions/db/repo.rb +11 -6
  13. data/lib/hanami/extensions/view/context.rb +10 -0
  14. data/lib/hanami/helpers/assets_helper.rb +92 -25
  15. data/lib/hanami/middleware/content_security_policy_nonce.rb +53 -0
  16. data/lib/hanami/slice.rb +22 -6
  17. data/lib/hanami/slice_registrar.rb +1 -1
  18. data/lib/hanami/version.rb +1 -1
  19. data/lib/hanami.rb +10 -2
  20. data/spec/integration/assets/cross_slice_assets_helpers_spec.rb +0 -1
  21. data/spec/integration/assets/serve_static_assets_spec.rb +1 -1
  22. data/spec/integration/container/autoloader_spec.rb +2 -0
  23. data/spec/integration/db/db_spec.rb +1 -1
  24. data/spec/integration/db/logging_spec.rb +63 -0
  25. data/spec/integration/db/repo_spec.rb +87 -2
  26. data/spec/integration/logging/exception_logging_spec.rb +6 -1
  27. data/spec/integration/rack_app/middleware_spec.rb +4 -11
  28. data/spec/integration/view/helpers/form_helper_spec.rb +1 -1
  29. data/spec/integration/web/content_security_policy_nonce_spec.rb +251 -0
  30. data/spec/support/app_integration.rb +2 -1
  31. data/spec/unit/hanami/config/actions/content_security_policy_spec.rb +7 -0
  32. data/spec/unit/hanami/config/console_spec.rb +22 -0
  33. data/spec/unit/hanami/env_spec.rb +10 -13
  34. data/spec/unit/hanami/slice_spec.rb +18 -0
  35. data/spec/unit/hanami/version_spec.rb +1 -1
  36. data/spec/unit/hanami/web/rack_logger_spec.rb +11 -4
  37. metadata +27 -18
  38. data/spec/support/shared_examples/cli/generate/app.rb +0 -494
  39. data/spec/support/shared_examples/cli/generate/migration.rb +0 -32
  40. data/spec/support/shared_examples/cli/generate/model.rb +0 -81
  41. data/spec/support/shared_examples/cli/new.rb +0 -97
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f8538bd01b6c70d005b10ba4683326798d74af43f6e2999a98228176e9c4faeb
4
- data.tar.gz: ebcb99c071cb38326509d1132fb06a3a5e2ed310fa77fdaf1fae020798293fca
3
+ metadata.gz: a8faef478dec45e673c25c67704b0fd7a363e784061412c64e96735040f2d07f
4
+ data.tar.gz: b88eac54b8ba6241645b616ba9eafd20d54edb888e7fca8abc9e310f414dabe5
5
5
  SHA512:
6
- metadata.gz: 9f9c67c9648cd127a6f18781f1609869571b10ef6ca62e829cc300d6b4b0105e467656c08f59729edf519e8d92670131f53b3349e3093b47550683689199166c
7
- data.tar.gz: aa13d4ab655a9d3a225e85b6c8c17c4cc2371a12dad72fe31eb08b1c27c709d0efa6abd9a020a5275af1cebd56b54e821409fe572ca51f1130fd9b579c18c35b
6
+ metadata.gz: 8233b2e453669ae1528c3cadd8032cbff3a7337c590bb0962d307ddd71bb8b18e28f56cc19a5644225efaf0ad0bfa2f5a008dd8fbae2615a9bc819906e47e1cc
7
+ data.tar.gz: 07551dada781667f44be90593599a0f505e9580d82d98c70df4e18e4288651e68fb26130d3504bf228dd2b7d2237af7494d7cfac86cd06ece733920657adfa83
data/CHANGELOG.md CHANGED
@@ -1,6 +1,38 @@
1
1
  # Hanami
2
2
 
3
- The web, with simplicity.
3
+ ## [Unreleased]
4
+
5
+ ### Added
6
+
7
+ ### Changed
8
+
9
+ ### Deprecated
10
+
11
+ ### Removed
12
+
13
+ ### Fixed
14
+
15
+ ### Security
16
+
17
+ ## [v2.3.0.beta1] - 2025-10-03
18
+
19
+ ### Added
20
+
21
+ - Add `config.console` settings to app. Set an alternative engine with e.g. `config.console.engine = :pry` (`:irb` is default). Add your own methods to the console with `config.console.include MyModule, AnotherModule`. (@alassek in #1540)
22
+ - Support optional nonce in Rack requests, CSP header rules and view helpers (@svoop in #1500)
23
+ - Check `ENV["APP_ENV"]` for the Hanami env if `ENV["HANAMI_ENV"]` is not set. The order of environment variable checks is now `HANAMI_ENV`->`APP_ENV`->`RACK_ENV` (@svoop in #1487).
24
+
25
+ ### Changed
26
+
27
+ - Support both Rack v2 and v3. (@kyleplump in #1493)
28
+ - Support single-character slice names. (@aaronmallen in #1528)
29
+
30
+ ### Fixed
31
+
32
+ - Allow `include Deps` to be used in `Hanami::DB::Repo` subclasses. (@wuarmin in #1523)
33
+ - Properly infer root relations for deeper `Hanami::DB::Repo` subclasses, such as in slices. (@wuarmin in #1478)
34
+ - Delay loading `config/routes.rb` until after autoloading is setup, which means you can access your constants there. (@timriley in #1539)
35
+ - Avoid warning from referencing deprecated `URI::DEFAULT_PARSER`. (@wuarmin in #1518)
4
36
 
5
37
  ## v2.2.1 - 2024-11-12
6
38
 
@@ -1478,3 +1510,7 @@ end
1478
1510
  - [Luca Guidi] Introduced `Lotus::Configuration`
1479
1511
  - [Luca Guidi] Introduced `Lotus::Application`
1480
1512
  - [Luca Guidi] Official support for MRI 2.0
1513
+
1514
+
1515
+ [unreleased]: https://github.com/hanami/hanami/compare/v2.3.0.beta1...HEAD
1516
+ [v2.3.0.beta1] https://github.com/hanami/hanami/compare/v2.2.1...v2.3.0.beta1
data/README.md CHANGED
@@ -1,12 +1,6 @@
1
1
  # Hanami :cherry_blossom:
2
2
 
3
- The web, with simplicity.
4
-
5
- ## Version
6
-
7
- **This branch contains the code for `hanami`: 2.2**
8
-
9
- ## Frameworks
3
+ **A flexible framework for maintainable Ruby apps.**
10
4
 
11
5
  Hanami is a **full-stack** Ruby web framework. It's made up of smaller, single-purpose libraries.
12
6
 
@@ -14,8 +8,8 @@ This repository is for the full-stack framework, which provides the glue that ti
14
8
 
15
9
  * [**Hanami::Router**](https://github.com/hanami/router) - Rack compatible HTTP router for Ruby
16
10
  * [**Hanami::Controller**](https://github.com/hanami/controller) - Full featured, fast and testable actions for Rack
17
- * [**Hanami::Validations**](https://github.com/hanami/validations) - Parameter validations & coercion for actions
18
11
  * [**Hanami::View**](https://github.com/hanami/view) - Presentation with a separation between views and templates
12
+ * [**Hanami::DB**](https://github.com/hanami/db) - Database integration, complete with migrations, repositories, relations, and structs
19
13
  * [**Hanami::Assets**](https://github.com/hanami/assets) - Assets management for Ruby
20
14
 
21
15
  These components are designed to be used independently or together in a Hanami application.
@@ -24,12 +18,10 @@ These components are designed to be used independently or together in a Hanami a
24
18
 
25
19
  [![Gem Version](https://badge.fury.io/rb/hanami.svg)](https://badge.fury.io/rb/hanami)
26
20
  [![CI](https://github.com/hanami/hanami/actions/workflows/ci.yml/badge.svg)](https://github.com/hanami/hanami/actions?query=workflow%3Aci+branch%3Amain)
27
- [![Test Coverage](https://codecov.io/gh/hanami/hanami/branch/main/graph/badge.svg)](https://codecov.io/gh/hanami/hanami)
28
- [![Depfu](https://badges.depfu.com/badges/ba000e0f69e6ef1c44cd3038caaa1841/overview.svg)](https://depfu.com/github/hanami/hanami?project=Bundler)
29
21
 
30
22
  ## Installation
31
23
 
32
- __Hanami__ supports Ruby (MRI) 3.1+.
24
+ Hanami supports Ruby (MRI) 3.1+.
33
25
 
34
26
  ```shell
35
27
  gem install hanami
@@ -40,7 +32,8 @@ gem install hanami
40
32
  ```shell
41
33
  hanami new bookshelf
42
34
  cd bookshelf && bundle
43
- bundle exec hanami server # visit http://localhost:2300
35
+ bundle exec hanami dev
36
+ # Now visit http://localhost:2300
44
37
  ```
45
38
 
46
39
  Please follow along with the [Getting Started guide](https://guides.hanamirb.org/getting-started/).
@@ -49,30 +42,22 @@ Please follow along with the [Getting Started guide](https://guides.hanamirb.org
49
42
 
50
43
  You can give back to Open Source, by supporting Hanami development via [GitHub Sponsors](https://github.com/sponsors/hanami).
51
44
 
52
- ### Supporters
53
-
54
- * [Trung Lê](https://github.com/runlevel5)
55
- * [James Carlson](https://github.com/jxxcarlson)
56
- * [Creditas](https://www.creditas.com.br/)
57
-
58
45
  ## Contact
59
46
 
60
- * Home page: http://hanamirb.org
61
- * Community: http://hanamirb.org/community
62
- * Guides: https://guides.hanamirb.org
63
- * Snippets: https://snippets.hanamirb.org
64
- * Mailing List: http://hanamirb.org/mailing-list
65
- * API Doc: https://gemdocs.org/gems/hanami/latest
66
- * Bugs/Issues: https://github.com/hanami/hanami/issues
67
- * Stack Overflow: http://stackoverflow.com/questions/tagged/hanami
68
- * Forum: https://discourse.hanamirb.org
69
- * **Chat**: http://chat.hanamirb.org
47
+ * [Home page](http://hanamirb.org)
48
+ * [Community](http://hanamirb.org/community)
49
+ * [Guides](https://guides.hanamirb.org)
50
+ * [Issues](https://github.com/hanami/hanami/issues)
51
+ * [Forum](https://discourse.hanamirb.org)
52
+ * [Chat](https://discord.gg/KFCxDmk3JQ)
70
53
 
71
54
  ## Community
72
55
 
73
- We strive for an inclusive and helpful community. We have a [Code of Conduct](http://hanamirb.org/community/#code-of-conduct) to handle controversial cases. In general, we expect **you** to be **nice** with other people. Our hope is for a great software and a great Community.
56
+ We care about building a friendly, inclusive and helpful community. We welcome people of all backgrounds, genders and experience levels, and respect you all equally.
57
+
58
+ We do not tolerate nazis, transphobes, racists, or any kind of bigotry. See our [code of conduct](http://hanamirb.org/community/#code-of-conduct) for more.
74
59
 
75
- ## Contributing [![Open Source Helpers](https://www.codetriage.com/hanami/hanami/badges/users.svg)](https://www.codetriage.com/hanami/hanami)
60
+ ## Contributing
76
61
 
77
62
  1. Fork it ( https://github.com/hanami/hanami/fork )
78
63
  2. Create your feature branch (`git checkout -b my-new-feature`)
@@ -110,14 +95,14 @@ $ bundle exec rspec path/to/spec.rb
110
95
 
111
96
  ### Development Requirements
112
97
 
113
- * Ruby >= 3.1
114
- * Bundler
115
- * Node.js (MacOS)
98
+ * Ruby >= 3.1
99
+ * Bundler
100
+ * Node.js
116
101
 
117
102
  ## Versioning
118
103
 
119
- __Hanami__ uses [Semantic Versioning 2.0.0](http://semver.org)
104
+ Hanami uses [Semantic Versioning 2.0.0](http://semver.org).
120
105
 
121
106
  ## Copyright
122
107
 
123
- Copyright © 2014–2024 Hanami Team – Released under MIT License.
108
+ Copyright © 2014–2025 Hanami Team – Released under MIT License.
data/hanami.gemspec CHANGED
@@ -38,12 +38,13 @@ Gem::Specification.new do |spec|
38
38
  spec.add_dependency "dry-monitor", "~> 1.0", ">= 1.0.1", "< 2"
39
39
  spec.add_dependency "dry-system", "~> 1.1"
40
40
  spec.add_dependency "dry-logger", "~> 1.0", "< 2"
41
- spec.add_dependency "hanami-cli", "~> 2.2.1"
41
+ spec.add_dependency "hanami-cli", "~> 2.3.0.beta1"
42
42
  spec.add_dependency "hanami-utils", "~> 2.2"
43
43
  spec.add_dependency "json", ">= 2.7.2"
44
44
  spec.add_dependency "zeitwerk", "~> 2.6"
45
+ spec.add_dependency "rack-session"
45
46
 
46
47
  spec.add_development_dependency "rspec", "~> 3.8"
47
- spec.add_development_dependency "rack-test", "~> 1.1"
48
+ spec.add_development_dependency "rack-test", "~> 2.0"
48
49
  spec.add_development_dependency "rake", "~> 13.0"
49
50
  end
data/lib/hanami/app.rb CHANGED
@@ -168,6 +168,8 @@ module Hanami
168
168
  end
169
169
 
170
170
  def prepare_autoloader
171
+ autoloader.tag = "hanami.app.#{slice_name.name}"
172
+
171
173
  # Component dirs are automatically pushed to the autoloader by dry-system's zeitwerk plugin.
172
174
  # This method adds other dirs that are not otherwise configured as component dirs.
173
175
 
@@ -96,6 +96,29 @@ module Hanami
96
96
  @policy.delete(key)
97
97
  end
98
98
 
99
+ # Returns true if 'nonce' is used in any of the policies.
100
+ #
101
+ # @return [Boolean]
102
+ #
103
+ # @api public
104
+ # @since x.x.x
105
+ def nonce?
106
+ @policy.any? { _2.match?(/'nonce'/) }
107
+ end
108
+
109
+ # Returns an array of middleware name to support 'nonce' in
110
+ # policies, or an empty array if 'nonce' is not used.
111
+ #
112
+ # @return [Array<(Symbol, Array)>]
113
+ #
114
+ # @api public
115
+ # @since x.x.x
116
+ def middleware
117
+ return [] unless nonce?
118
+
119
+ [Hanami::Middleware::ContentSecurityPolicyNonce]
120
+ end
121
+
99
122
  # @since 2.0.0
100
123
  # @api private
101
124
  def to_s
@@ -74,6 +74,23 @@ module Hanami
74
74
  # @since 2.0.0
75
75
  attr_accessor :content_security_policy
76
76
 
77
+ # Returns the proc to generate Content Security Policy nonce values.
78
+ #
79
+ # The current Rack request object is provided as an optional argument
80
+ # to the proc, enabling the generation of nonces based on session IDs.
81
+ #
82
+ # @example Independent random nonce (default)
83
+ # -> { SecureRandom.urlsafe_base64(16) }
84
+ #
85
+ # @example Session dependent nonce
86
+ # ->(request) { Digest::SHA256.base64digest(request.session[:uuid])[0, 16] }
87
+ #
88
+ # @return [Proc]
89
+ #
90
+ # @api public
91
+ # @since x.x.x
92
+ setting :content_security_policy_nonce_generator, default: -> { SecureRandom.urlsafe_base64(16) }
93
+
77
94
  # @!attribute [rw] method_override
78
95
  # Sets or returns whether HTTP method override should be enabled for action classes.
79
96
  #
@@ -138,6 +155,10 @@ module Hanami
138
155
  end
139
156
  end
140
157
 
158
+ # @api public
159
+ # @since x.x.x
160
+ def content_security_policy? = !!@content_security_policy
161
+
141
162
  private
142
163
 
143
164
  # Apply defaults for base config
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/configurable"
4
+
5
+ module Hanami
6
+ class Config
7
+ # Hanami console config
8
+ #
9
+ # @since 2.3.0
10
+ # @api public
11
+ class Console
12
+ include Dry::Configurable
13
+
14
+ # @!attribute [rw] engine
15
+ # Sets or returns the interactive console engine to be used by `hanami console`.
16
+ # Supported values are `:irb` (default) and `:pry`.
17
+ #
18
+ # @example
19
+ # config.console.engine = :pry
20
+ #
21
+ # @return [Symbol]
22
+ #
23
+ # @api public
24
+ # @since 2.3.0
25
+ setting :engine, default: :irb
26
+
27
+ # Returns the complete list of extensions to be used in the console
28
+ #
29
+ # @example
30
+ # config.console.include MyExtension, OtherExtension
31
+ # config.console.include ThirdExtension
32
+ #
33
+ # config.console.extensions
34
+ # # => [MyExtension, OtherExtension, ThirdExtension]
35
+ #
36
+ # @return [Array<Module>]
37
+ #
38
+ # @api public
39
+ # @since 2.3.0
40
+ def extensions = @extensions.dup.freeze
41
+
42
+ # Define a module extension to be included in the console
43
+ #
44
+ # @param mod [Module] one or more modules to be included in the console
45
+ # @return [void]
46
+ #
47
+ # @api public
48
+ # @since 2.3.0
49
+ def include(*mod)
50
+ @extensions.concat(mod).uniq!
51
+ end
52
+
53
+ # @api private
54
+ def initialize
55
+ @extensions = []
56
+ end
57
+
58
+ private
59
+
60
+ # @api private
61
+ def initialize_copy(source)
62
+ super
63
+ @extensions = [*source.extensions]
64
+ end
65
+
66
+ def method_missing(name, *args, &block)
67
+ if config.respond_to?(name)
68
+ config.public_send(name, *args, &block)
69
+ else
70
+ super
71
+ end
72
+ end
73
+
74
+ def respond_to_missing?(name, _include_all = false)
75
+ config.respond_to?(name) || super
76
+ end
77
+ end
78
+ end
79
+ end
@@ -101,7 +101,7 @@ module Hanami
101
101
  # Sets or returns a hash of options to pass to the {logger_constructor} when initializing
102
102
  # the logger.
103
103
  #
104
- # Defaults to `[]`
104
+ # Defaults to `{}`
105
105
  #
106
106
  # @return [Hash]
107
107
  #
data/lib/hanami/config.rb CHANGED
@@ -6,6 +6,7 @@ require "dry/configurable"
6
6
  require "dry/inflector"
7
7
 
8
8
  require_relative "constants"
9
+ require_relative "config/console"
9
10
 
10
11
  module Hanami
11
12
  # Hanami app config
@@ -184,6 +185,18 @@ module Hanami
184
185
  "Hanami::Router::NotFoundError" => :not_found,
185
186
  )
186
187
 
188
+ # @!attribute [rw] console
189
+ # Returns the app's console config
190
+ #
191
+ # @example
192
+ # config.console.engine # => :irb
193
+ #
194
+ # @return [Hanami::Config::Console]
195
+ #
196
+ # @api public
197
+ # @since 2.3.0
198
+ setting :console, default: Hanami::Config::Console.new
199
+
187
200
  # Returns the app or slice's {Hanami::SliceName slice_name}.
188
201
  #
189
202
  # This is useful for default config values that depend on this name.
@@ -56,4 +56,7 @@ module Hanami
56
56
  # @api private
57
57
  RB_EXT_REGEXP = %r{.rb$}
58
58
  private_constant :RB_EXT_REGEXP
59
+
60
+ # @api private
61
+ CONTENT_SECURITY_POLICY_NONCE_REQUEST_KEY = "hanami.content_security_policy_nonce"
59
62
  end
@@ -69,7 +69,8 @@ module Hanami
69
69
  resolve_rom = method(:resolve_rom)
70
70
 
71
71
  define_method(:new) do |**kwargs|
72
- super(container: kwargs.fetch(:container) { resolve_rom.() })
72
+ container = kwargs.delete(:container) || resolve_rom.()
73
+ super(container: container, **kwargs)
73
74
  end
74
75
  end
75
76
 
@@ -77,14 +78,18 @@ module Hanami
77
78
  slice["db.rom"]
78
79
  end
79
80
 
80
- def root_for_repo_class(repo_class)
81
- return unless repo_class.to_s.end_with?("Repo")
81
+ REPO_CLASS_NAME_REGEX = /^(?<name>.+)_(repo|repository)$/
82
82
 
83
- slice.inflector.demodulize(repo_class)
83
+ def root_for_repo_class(repo_class)
84
+ repo_class_name = slice.inflector.demodulize(repo_class)
84
85
  .then { slice.inflector.underscore(_1) }
85
- .then { _1.gsub(/_repo$/, "") }
86
+
87
+ repo_class_match = repo_class_name.match(REPO_CLASS_NAME_REGEX)
88
+ return unless repo_class_match
89
+
90
+ repo_class_match[:name]
86
91
  .then { slice.inflector.pluralize(_1) }
87
- .then { _1.to_sym }
92
+ .then(&:to_sym)
88
93
  end
89
94
 
90
95
  def struct_namespace
@@ -172,6 +172,16 @@ module Hanami
172
172
  @request
173
173
  end
174
174
 
175
+ # Returns true if the view is rendered from within an action and a request is available.
176
+ #
177
+ # @return [Boolean]
178
+ #
179
+ # @api public
180
+ # @since x.x.x
181
+ def request?
182
+ !!@request
183
+ end
184
+
175
185
  # Returns the app's routes helper.
176
186
  #
177
187
  # @return [Hanami::Slice::RoutesHelper] the routes helper
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "uri"
4
4
  require "hanami/view"
5
+ require_relative "../constants"
5
6
 
6
7
  # rubocop:disable Metrics/ModuleLength
7
8
 
@@ -61,7 +62,10 @@ module Hanami
61
62
 
62
63
  # @since 0.3.0
63
64
  # @api private
64
- ABSOLUTE_URL_MATCHER = URI::DEFAULT_PARSER.make_regexp
65
+ # TODO: we can drop the defined?-check and fallback once Ruby 3.3 becomes our minimum required version
66
+ ABSOLUTE_URL_MATCHER = (
67
+ defined?(URI::RFC2396_PARSER) ? URI::RFC2396_PARSER : URI::DEFAULT_PARSER
68
+ ).make_regexp
65
69
 
66
70
  # @since 1.1.0
67
71
  # @api private
@@ -85,7 +89,12 @@ module Hanami
85
89
  # name of the algorithm, then a hyphen, then the hash value of the file.
86
90
  # If more than one algorithm is used, they"ll be separated by a space.
87
91
  #
88
- # @param source_paths [Array<String, #url>] one or more assets by name or absolute URL
92
+ # If the Content Security Policy uses 'nonce' and the source is not
93
+ # absolute, the nonce value of the current request is automatically added
94
+ # as an attribute. You can override this with the `nonce: false` option.
95
+ # See {#content_security_policy_nonce} for more.
96
+ #
97
+ # @param sources [Array<String, #url>] one or more assets by name or absolute URL
89
98
  #
90
99
  # @return [Hanami::View::HTML::SafeString] the markup
91
100
  #
@@ -136,6 +145,10 @@ module Hanami
136
145
  #
137
146
  # # <script src="/assets/application.js" type="text/javascript" defer="defer"></script>
138
147
  #
148
+ # @example Disable nonce
149
+ #
150
+ # <%= javascript_tag "application", nonce: false %>
151
+ #
139
152
  # @example Absolute URL
140
153
  #
141
154
  # <%= javascript_tag "https://code.jquery.com/jquery-2.1.4.min.js" %>
@@ -154,13 +167,15 @@ module Hanami
154
167
  #
155
168
  # # <script src="https://assets.bookshelf.org/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.js"
156
169
  # # type="text/javascript"></script>
157
- def javascript_tag(*source_paths, **options)
170
+ def javascript_tag(*sources, **options)
158
171
  options = options.reject { |k, _| k.to_sym == :src }
172
+ nonce_option = options.delete(:nonce)
159
173
 
160
- _safe_tags(*source_paths) do |source|
174
+ _safe_tags(*sources) do |source|
161
175
  attributes = {
162
- src: _typed_path(source, JAVASCRIPT_EXT),
163
- type: JAVASCRIPT_MIME_TYPE
176
+ src: _typed_url(source, JAVASCRIPT_EXT),
177
+ type: JAVASCRIPT_MIME_TYPE,
178
+ nonce: _nonce(source, nonce_option)
164
179
  }
165
180
  attributes.merge!(options)
166
181
 
@@ -189,7 +204,12 @@ module Hanami
189
204
  # name of the algorithm, then a hyphen, then the hashed value of the file.
190
205
  # If more than one algorithm is used, they"ll be separated by a space.
191
206
  #
192
- # @param source_paths [Array<String, #url>] one or more assets by name or absolute URL
207
+ # If the Content Security Policy uses 'nonce' and the source is not
208
+ # absolute, the nonce value of the current request is automatically added
209
+ # as an attribute. You can override this with the `nonce: false` option.
210
+ # See {#content_security_policy_nonce} for more.
211
+ #
212
+ # @param sources [Array<String, #url>] one or more assets by name or absolute URL
193
213
  #
194
214
  # @return [Hanami::View::HTML::SafeString] the markup
195
215
  #
@@ -214,6 +234,10 @@ module Hanami
214
234
  # # <link href="/assets/application.css" type="text/css" rel="stylesheet">
215
235
  # # <link href="/assets/dashboard.css" type="text/css" rel="stylesheet">
216
236
  #
237
+ # @example Disable nonce
238
+ #
239
+ # <%= stylesheet_tag "application", nonce: false %>
240
+ #
217
241
  # @example Subresource Integrity
218
242
  #
219
243
  # <%= stylesheet_tag "application" %>
@@ -247,19 +271,21 @@ module Hanami
247
271
  #
248
272
  # # <link href="https://assets.bookshelf.org/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.css"
249
273
  # # type="text/css" rel="stylesheet">
250
- def stylesheet_tag(*source_paths, **options)
274
+ def stylesheet_tag(*sources, **options)
251
275
  options = options.reject { |k, _| k.to_sym == :href }
276
+ nonce_option = options.delete(:nonce)
252
277
 
253
- _safe_tags(*source_paths) do |source_path|
278
+ _safe_tags(*sources) do |source|
254
279
  attributes = {
255
- href: _typed_path(source_path, STYLESHEET_EXT),
280
+ href: _typed_url(source, STYLESHEET_EXT),
256
281
  type: STYLESHEET_MIME_TYPE,
257
- rel: STYLESHEET_REL
282
+ rel: STYLESHEET_REL,
283
+ nonce: _nonce(source, nonce_option)
258
284
  }
259
285
  attributes.merge!(options)
260
286
 
261
287
  if _context.assets.subresource_integrity? || attributes.include?(:integrity)
262
- attributes[:integrity] ||= _subresource_integrity_value(source_path, STYLESHEET_EXT)
288
+ attributes[:integrity] ||= _subresource_integrity_value(source, STYLESHEET_EXT)
263
289
  attributes[:crossorigin] ||= CROSSORIGIN_ANONYMOUS
264
290
  end
265
291
 
@@ -626,7 +652,7 @@ module Hanami
626
652
  #
627
653
  # If CDN mode is on, it returns the absolute URL of the asset.
628
654
  #
629
- # @param source_path [String, #url] the asset name or asset object
655
+ # @param source [String, #url] the asset name or asset object
630
656
  #
631
657
  # @return [String] the asset path
632
658
  #
@@ -665,42 +691,71 @@ module Hanami
665
691
  # <%= asset_url "application.js" %>
666
692
  #
667
693
  # # "https://assets.bookshelf.org/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.js"
668
- def asset_url(source_path)
669
- return source_path.url if source_path.respond_to?(:url)
670
- return source_path if _absolute_url?(source_path)
694
+ def asset_url(source)
695
+ return source.url if source.respond_to?(:url)
696
+ return source if _absolute_url?(source)
671
697
 
672
- _context.assets[source_path].url
698
+ _context.assets[source].url
699
+ end
700
+
701
+ # Random per request nonce value for Content Security Policy (CSP) rules.
702
+ #
703
+ # If the `Hanami::Middleware::ContentSecurityPolicyNonce` middleware is
704
+ # in use, this helper returns the nonce value for the current request
705
+ # or `nil` otherwise.
706
+ #
707
+ # For this policy to work in the browser, you have to add the `'nonce'`
708
+ # placeholder to the script and/or style source policy rule. It will be
709
+ # substituted by the current nonce value like `'nonce-A12OggyZ'.
710
+ #
711
+ # @return [String, nil] nonce value of the current request
712
+ #
713
+ # @since x.x.x
714
+ #
715
+ # @example App configuration
716
+ #
717
+ # config.middleware.use Hanami::Middleware::ContentSecurityPolicyNonce
718
+ # config.actions.content_security_policy[:script_src] = "'self' 'nonce'"
719
+ # config.actions.content_security_policy[:style_src] = "'self' 'nonce'"
720
+ #
721
+ # @example View helper
722
+ #
723
+ # <script nonce="<%= content_security_policy_nonce %>">
724
+ def content_security_policy_nonce
725
+ return unless _context.request?
726
+
727
+ _context.request.env[CONTENT_SECURITY_POLICY_NONCE_REQUEST_KEY]
673
728
  end
674
729
 
675
730
  private
676
731
 
677
732
  # @since 2.1.0
678
733
  # @api private
679
- def _safe_tags(*source_paths, &blk)
734
+ def _safe_tags(*sources, &blk)
680
735
  ::Hanami::View::HTML::SafeString.new(
681
- source_paths.map(&blk).join(NEW_LINE_SEPARATOR)
736
+ sources.map(&blk).join(NEW_LINE_SEPARATOR)
682
737
  )
683
738
  end
684
739
 
685
740
  # @since 2.1.0
686
741
  # @api private
687
- def _typed_path(source, ext)
742
+ def _typed_url(source, ext)
688
743
  source = "#{source}#{ext}" if source.is_a?(String) && _append_extension?(source, ext)
689
744
  asset_url(source)
690
745
  end
691
746
 
692
747
  # @api private
693
- def _subresource_integrity_value(source_path, ext)
694
- return if _absolute_url?(source_path)
748
+ def _subresource_integrity_value(source, ext)
749
+ return if _absolute_url?(source)
695
750
 
696
- source_path = "#{source_path}#{ext}" unless /#{Regexp.escape(ext)}\z/.match?(source_path)
697
- _context.assets[source_path].sri
751
+ source = "#{source}#{ext}" unless /#{Regexp.escape(ext)}\z/.match?(source)
752
+ _context.assets[source].sri
698
753
  end
699
754
 
700
755
  # @since 2.1.0
701
756
  # @api private
702
757
  def _absolute_url?(source)
703
- ABSOLUTE_URL_MATCHER.match(source)
758
+ ABSOLUTE_URL_MATCHER.match?(source.respond_to?(:url) ? source.url : source)
704
759
  end
705
760
 
706
761
  # @since 1.2.0
@@ -711,6 +766,18 @@ module Hanami
711
766
  _context.assets.crossorigin?(source)
712
767
  end
713
768
 
769
+ # @since x.x.x
770
+ # @api private
771
+ def _nonce(source, nonce_option)
772
+ if nonce_option == false
773
+ nil
774
+ elsif nonce_option == true || (nonce_option.nil? && !_absolute_url?(source))
775
+ content_security_policy_nonce
776
+ else
777
+ nonce_option
778
+ end
779
+ end
780
+
714
781
  # @since 2.1.0
715
782
  # @api private
716
783
  def _source_options(src, options, &blk)