utopia 3.0.4 → 3.0.6
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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/integrating-with-javascript.md +39 -13
- data/context/middleware.md +2 -2
- data/lib/traces/provider/utopia/content/middleware.rb +20 -0
- data/lib/traces/provider/utopia/static/middleware.rb +19 -0
- data/lib/traces/provider/utopia.rb +7 -0
- data/lib/utopia/content/builder.rb +1 -5
- data/lib/utopia/content/links.rb +5 -3
- data/lib/utopia/content/markup.rb +3 -3
- data/lib/utopia/content/middleware.rb +16 -22
- data/lib/utopia/content/node.rb +7 -9
- data/lib/utopia/controller/rewrite.rb +3 -1
- data/lib/utopia/exceptions/application_errors.rb +11 -0
- data/lib/utopia/exceptions/handler.rb +4 -3
- data/lib/utopia/exceptions/mailer.rb +81 -19
- data/lib/utopia/import_map.rb +14 -0
- data/lib/utopia/path.rb +11 -16
- data/lib/utopia/request.rb +5 -20
- data/lib/utopia/session/lazy_hash.rb +28 -9
- data/lib/utopia/session/middleware.rb +74 -49
- data/lib/utopia/static/local_file.rb +7 -3
- data/lib/utopia/static/middleware.rb +0 -12
- data/lib/utopia/version.rb +1 -1
- data/readme.md +13 -0
- data/releases.md +17 -0
- data.tar.gz.sig +0 -0
- metadata +21 -5
- metadata.gz.sig +0 -0
- data/bake/utopia/components.rb +0 -44
- data/lib/utopia/components.rb +0 -179
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 29332f03b21ff72e75b4f372deb3b317469ff3a5f2991ad363214b144d68ee4d
|
|
4
|
+
data.tar.gz: 3e3405b9bc409eeaf96e575bbb4f61fceb16298630b63f6069c2f443c2264025
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4f376ca1d057b6a91002e1abcab1ef98c0754772203c8d44392eca14e41df72b59e79bdb37a81e29624a6e0dd696e71ae63623a3ede9afec76b400825a8063fd
|
|
7
|
+
data.tar.gz: 4c20f0613d97496fd0fe1c4584384a1022677378b84504173817cdb6f6eb1a5405c440319ee482222e7c1fe7c8eea9401e1f7df89871607be8eeb023c9bf6487
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -8,20 +8,45 @@ Import maps provide a modern way to manage JavaScript module dependencies. Utopi
|
|
|
8
8
|
|
|
9
9
|
### Installing JavaScript Libraries
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Declare browser libraries as production dependencies in `package.json`. The `bake-node.packages` section selects the files that should be served and assigns their browser import names:
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"private": true,
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@socketry/syntax": "^0.6.1"
|
|
18
|
+
},
|
|
19
|
+
"bake-node": {
|
|
20
|
+
"packages": {
|
|
21
|
+
"@socketry/syntax": {
|
|
22
|
+
"include": [
|
|
23
|
+
"Syntax.js"
|
|
24
|
+
],
|
|
25
|
+
"imports": {
|
|
26
|
+
"@socketry/syntax": "Syntax.js"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Install the dependencies using the configured package manager, then generate the browser-facing package projection:
|
|
12
35
|
|
|
13
36
|
```bash
|
|
14
|
-
$
|
|
37
|
+
$ bundle exec bake node:install
|
|
38
|
+
$ bundle exec bake node:packages:static
|
|
15
39
|
```
|
|
16
40
|
|
|
17
|
-
|
|
41
|
+
This installs dependencies into `node_modules/` and copies only the selected browser files into `public/_components/`. Treat both directories as generated projections rather than authored source.
|
|
42
|
+
|
|
43
|
+
Use an immutable installation and verify the checked-in projection in CI:
|
|
18
44
|
|
|
19
45
|
```bash
|
|
20
|
-
$ bundle exec bake
|
|
46
|
+
$ bundle exec bake node:install frozen=true
|
|
47
|
+
$ bundle exec bake node:packages:check
|
|
21
48
|
```
|
|
22
49
|
|
|
23
|
-
This will copy the library's distribution files (typically from `node_modules/*/dist/`) to your `public/_components/` directory, making them available for local serving.
|
|
24
|
-
|
|
25
50
|
### Creating the Import Map
|
|
26
51
|
|
|
27
52
|
Create a global import map in `lib/my_website/import_map.rb`:
|
|
@@ -30,12 +55,12 @@ Create a global import map in `lib/my_website/import_map.rb`:
|
|
|
30
55
|
require "utopia/import_map"
|
|
31
56
|
|
|
32
57
|
module MyWebsite
|
|
33
|
-
IMPORT_MAP = Utopia::ImportMap.
|
|
34
|
-
map.import("jquery", "./jquery/jquery.js")
|
|
35
|
-
end
|
|
58
|
+
IMPORT_MAP = Utopia::ImportMap.load_manifest("public/_components")
|
|
36
59
|
end
|
|
37
60
|
```
|
|
38
61
|
|
|
62
|
+
This loads the generated package mappings directly, so the browser import map remains synchronized with `package.json`.
|
|
63
|
+
|
|
39
64
|
Then load this in `lib/my_website.rb`:
|
|
40
65
|
|
|
41
66
|
```ruby
|
|
@@ -64,15 +89,16 @@ Once the import map is set up, you can import and use the library in your script
|
|
|
64
89
|
```xrb
|
|
65
90
|
<script type="module">
|
|
66
91
|
// <![CDATA[
|
|
67
|
-
import
|
|
92
|
+
import Syntax from '@socketry/syntax';
|
|
68
93
|
|
|
69
|
-
|
|
70
|
-
console.log("jQuery is ready!");
|
|
71
|
-
});
|
|
94
|
+
await Syntax.highlight();
|
|
72
95
|
// ]]>
|
|
73
96
|
</script>
|
|
74
97
|
```
|
|
75
98
|
|
|
99
|
+
Inspect the generated import map with `bundle exec bake node:importmap:show` when debugging package resolution.
|
|
100
|
+
|
|
101
|
+
See the [Bake Node documentation](https://socketry.github.io/bake-node/) for workspace packages, package selection, and alternative package managers.
|
|
76
102
|
|
|
77
103
|
### Advanced Import Map Features
|
|
78
104
|
|
data/context/middleware.md
CHANGED
|
@@ -167,7 +167,7 @@ This template would typically be designed with supporting `_page.xnode` and `_he
|
|
|
167
167
|
|
|
168
168
|
## Session
|
|
169
169
|
|
|
170
|
-
The {ruby Utopia::Session} middleware provides session storage using encrypted client-side cookies. The session management uses symmetric private key encryption to store data on the client and
|
|
170
|
+
The {ruby Utopia::Session} middleware provides session storage using authenticated, encrypted client-side cookies. The session management uses symmetric private key encryption to store data on the client and prevent tampering.
|
|
171
171
|
|
|
172
172
|
```ruby
|
|
173
173
|
use Utopia::Session,
|
|
@@ -177,7 +177,7 @@ use Utopia::Session,
|
|
|
177
177
|
secure: true
|
|
178
178
|
```
|
|
179
179
|
|
|
180
|
-
All session data is stored on the client, but it
|
|
180
|
+
All session data is stored on the client, but it is encrypted and authenticated with a key derived from the secret. The client cannot read or modify the data without the secret stored on the server.
|
|
181
181
|
|
|
182
182
|
When the middleware is installed, the session is available on the request:
|
|
183
183
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "../../../../utopia/content/middleware"
|
|
7
|
+
|
|
8
|
+
require "traces/provider"
|
|
9
|
+
|
|
10
|
+
Traces::Provider(Utopia::Content::Middleware) do
|
|
11
|
+
def respond(link, request, localization: request.localization)
|
|
12
|
+
attributes = {
|
|
13
|
+
"link.key" => link.key,
|
|
14
|
+
"link.href" => link.href,
|
|
15
|
+
"link.locale" => localization&.locale,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Traces.trace("utopia.content.respond", attributes: attributes){super}
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "../../../../utopia/static/middleware"
|
|
7
|
+
|
|
8
|
+
require "traces/provider"
|
|
9
|
+
|
|
10
|
+
Traces::Provider(Utopia::Static::Middleware) do
|
|
11
|
+
def respond(request, path, extension, content_type, localization: request.localization)
|
|
12
|
+
attributes = {
|
|
13
|
+
path: path,
|
|
14
|
+
locale: localization&.locale,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
Traces.trace("utopia.static.respond", attributes: attributes){super}
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -82,11 +82,7 @@ module Utopia
|
|
|
82
82
|
def text(content)
|
|
83
83
|
return unless content
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
content.build_markup(self)
|
|
87
|
-
else
|
|
88
|
-
XRB::Markup.append(@output, content)
|
|
89
|
-
end
|
|
85
|
+
content.build_markup(self)
|
|
90
86
|
end
|
|
91
87
|
|
|
92
88
|
# Write a complete tag to the output.
|
data/lib/utopia/content/links.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2015-
|
|
4
|
+
# Copyright, 2015-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require_relative "link"
|
|
7
7
|
|
|
@@ -204,8 +204,10 @@ module Utopia
|
|
|
204
204
|
def each(locale)
|
|
205
205
|
return to_enum(:each, locale) unless block_given?
|
|
206
206
|
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
@named.each_key do |name|
|
|
208
|
+
if link = lookup(name, locale)
|
|
209
|
+
yield link
|
|
210
|
+
end
|
|
209
211
|
end
|
|
210
212
|
end
|
|
211
213
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2009-
|
|
4
|
+
# Copyright, 2009-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require "xrb/parsers"
|
|
7
7
|
require "xrb/entities"
|
|
@@ -108,9 +108,9 @@ module Utopia
|
|
|
108
108
|
# @returns [String] The resulting string.
|
|
109
109
|
def to_s
|
|
110
110
|
if @closing_tag
|
|
111
|
-
"#{start_location}: #{@opening_tag} was not closed!"
|
|
112
|
-
else
|
|
113
111
|
"#{start_location}: #{@opening_tag} was closed by #{@closing_tag}!"
|
|
112
|
+
else
|
|
113
|
+
"#{start_location}: #{@opening_tag} was not closed!"
|
|
114
114
|
end
|
|
115
115
|
end
|
|
116
116
|
end
|
|
@@ -16,7 +16,6 @@ require_relative "tags"
|
|
|
16
16
|
|
|
17
17
|
require "xrb/template"
|
|
18
18
|
require "concurrent/map"
|
|
19
|
-
require "traces/provider"
|
|
20
19
|
|
|
21
20
|
module Utopia
|
|
22
21
|
module Content
|
|
@@ -66,11 +65,7 @@ module Utopia
|
|
|
66
65
|
end
|
|
67
66
|
|
|
68
67
|
attr :root
|
|
69
|
-
|
|
70
|
-
# TODO we should remove this method and expose `@links` directly.
|
|
71
|
-
def links(path, **options)
|
|
72
|
-
@links.index(path, **options)
|
|
73
|
-
end
|
|
68
|
+
attr :links
|
|
74
69
|
|
|
75
70
|
# Load and cache a content template.
|
|
76
71
|
# @parameter path [Utopia::Path | String] The path.
|
|
@@ -159,12 +154,7 @@ module Utopia
|
|
|
159
154
|
private
|
|
160
155
|
|
|
161
156
|
def lookup_content(name, parent_path)
|
|
162
|
-
if String === name && name.index("/")
|
|
163
|
-
name = Path.create(name)
|
|
164
|
-
end
|
|
165
|
-
|
|
166
157
|
if Path === name
|
|
167
|
-
name = parent_path + name
|
|
168
158
|
name_path = name.components.dup
|
|
169
159
|
name_path[-1] += XNODE_EXTENSION
|
|
170
160
|
else
|
|
@@ -195,6 +185,21 @@ module Utopia
|
|
|
195
185
|
end
|
|
196
186
|
|
|
197
187
|
def content_tag(name, node, parent_path: node.parent_path)
|
|
188
|
+
# Preserve nested names while searching the physical content hierarchy:
|
|
189
|
+
if String === name
|
|
190
|
+
if name.index("/")
|
|
191
|
+
name = Path.create(name)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
if Path === name
|
|
196
|
+
cache_key = parent_path + name
|
|
197
|
+
|
|
198
|
+
return @node_cache.fetch_or_store(cache_key) do
|
|
199
|
+
lookup_content(name, parent_path)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
198
203
|
full_path = parent_path + name
|
|
199
204
|
|
|
200
205
|
name = full_path.pop
|
|
@@ -216,16 +221,5 @@ module Utopia
|
|
|
216
221
|
end
|
|
217
222
|
end
|
|
218
223
|
|
|
219
|
-
Traces::Provider(Middleware) do
|
|
220
|
-
def respond(link, request, localization: request.localization)
|
|
221
|
-
attributes = {
|
|
222
|
-
"link.key" => link.key,
|
|
223
|
-
"link.href" => link.href,
|
|
224
|
-
"link.locale" => localization&.locale,
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
Traces.trace("utopia.content.middleware.respond", attributes: attributes){super}
|
|
228
|
-
end
|
|
229
|
-
end
|
|
230
224
|
end
|
|
231
225
|
end
|
data/lib/utopia/content/node.rb
CHANGED
|
@@ -51,14 +51,12 @@ module Utopia
|
|
|
51
51
|
def local_path(path = ".", base = nil)
|
|
52
52
|
path = Path[path]
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if path.absolute?
|
|
57
|
-
return root.join(*path.components)
|
|
58
|
-
else
|
|
54
|
+
if path.relative?
|
|
59
55
|
base ||= uri_path.dirname
|
|
60
|
-
|
|
56
|
+
path = base + path
|
|
61
57
|
end
|
|
58
|
+
|
|
59
|
+
return Pathname.new(path.to_url_path.local_path(@controller.root))
|
|
62
60
|
end
|
|
63
61
|
|
|
64
62
|
# Resolve a path relative to this node's containing URI path.
|
|
@@ -84,7 +82,7 @@ module Utopia
|
|
|
84
82
|
def links(path = ".", **options, &block)
|
|
85
83
|
path = uri_path.dirname + Path[path]
|
|
86
84
|
|
|
87
|
-
links = @controller.links(path, **options)
|
|
85
|
+
links = @controller.links.index(path, **options)
|
|
88
86
|
|
|
89
87
|
if block_given?
|
|
90
88
|
links.each(&block)
|
|
@@ -96,7 +94,7 @@ module Utopia
|
|
|
96
94
|
# Return localized and indexed variants related to this node.
|
|
97
95
|
# @returns [Array(Link)] The related links.
|
|
98
96
|
def related_links
|
|
99
|
-
@controller.links(@uri_path.dirname, name: @uri_path.basename, indices: true)
|
|
97
|
+
@controller.links.index(@uri_path.dirname, name: @uri_path.basename, indices: true)
|
|
100
98
|
end
|
|
101
99
|
|
|
102
100
|
# Return the directory whose links are siblings of this node.
|
|
@@ -113,7 +111,7 @@ module Utopia
|
|
|
113
111
|
# @parameter options [Hash] The options.
|
|
114
112
|
# @returns [Array(Link)] The sibling links.
|
|
115
113
|
def sibling_links(**options)
|
|
116
|
-
return @controller.links(siblings_path, **options)
|
|
114
|
+
return @controller.links.index(siblings_path, **options)
|
|
117
115
|
end
|
|
118
116
|
|
|
119
117
|
# Lookup the given tag which is being rendered within the given node. Invoked by {Document}.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2015-
|
|
4
|
+
# Copyright, 2015-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require_relative "../http"
|
|
7
7
|
require_relative "../path/matcher"
|
|
@@ -48,6 +48,8 @@ module Utopia
|
|
|
48
48
|
# Freeze this object and its internal state.
|
|
49
49
|
# @returns [self] This object.
|
|
50
50
|
def freeze
|
|
51
|
+
return self if frozen?
|
|
52
|
+
|
|
51
53
|
@matcher.freeze
|
|
52
54
|
@block.freeze
|
|
53
55
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Utopia
|
|
7
|
+
module Exceptions
|
|
8
|
+
# Exceptions raised by application code which can be safely handled and reported.
|
|
9
|
+
APPLICATION_ERRORS = [StandardError, ScriptError].freeze
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2014-
|
|
4
|
+
# Copyright, 2014-2026, by Samuel Williams.
|
|
5
5
|
# Copyright, 2025, by Olle Jonsson.
|
|
6
6
|
|
|
7
7
|
require "console"
|
|
@@ -9,6 +9,7 @@ require "console"
|
|
|
9
9
|
require_relative "../middleware"
|
|
10
10
|
require_relative "../request"
|
|
11
11
|
require_relative "../response"
|
|
12
|
+
require_relative "application_errors"
|
|
12
13
|
|
|
13
14
|
module Utopia
|
|
14
15
|
module Exceptions
|
|
@@ -37,7 +38,7 @@ module Utopia
|
|
|
37
38
|
def call(request)
|
|
38
39
|
begin
|
|
39
40
|
return @delegate.call(request)
|
|
40
|
-
rescue
|
|
41
|
+
rescue *APPLICATION_ERRORS => exception
|
|
41
42
|
Console.warn(self, "An error occurred while processing the request.", error: exception)
|
|
42
43
|
|
|
43
44
|
begin
|
|
@@ -52,7 +53,7 @@ module Utopia
|
|
|
52
53
|
error_response.status = 500
|
|
53
54
|
|
|
54
55
|
return error_response
|
|
55
|
-
rescue
|
|
56
|
+
rescue *APPLICATION_ERRORS => exception
|
|
56
57
|
# If redirection fails, we also finish with a fatal error:
|
|
57
58
|
Console.error(self, "An error occurred while invoking the error handler.", error: exception)
|
|
58
59
|
return Response[500, {"content-type" => "text/plain"}, ["An error occurred while processing the request."]]
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2016-
|
|
4
|
+
# Copyright, 2016-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require "net/smtp"
|
|
7
7
|
require "mail"
|
|
8
|
+
require "console"
|
|
9
|
+
require "stringio"
|
|
10
|
+
require "yaml"
|
|
8
11
|
|
|
9
12
|
require_relative "../middleware"
|
|
10
13
|
require_relative "../request"
|
|
11
|
-
require_relative "
|
|
14
|
+
require_relative "application_errors"
|
|
12
15
|
|
|
13
16
|
module Utopia
|
|
14
17
|
module Exceptions
|
|
@@ -23,20 +26,33 @@ module Utopia
|
|
|
23
26
|
|
|
24
27
|
DEFAULT_FROM = (ENV["USER"] || "utopia").freeze
|
|
25
28
|
DEFAULT_SUBJECT = "%{exception} [PID %{pid} : %{cwd}]".freeze
|
|
29
|
+
ATTACHMENT_SIZE_LIMIT = 64*1024
|
|
30
|
+
SENSITIVE_FIELD = /authorization|cookie|credential|password|private[_-]?key|referer|referrer|secret|session|token|variables|api[_-]?key/i
|
|
31
|
+
REDACTED = "[REDACTED]".freeze
|
|
26
32
|
|
|
27
33
|
# @param to [String] The address to email error reports to.
|
|
28
34
|
# @param from [String] The from address for error reports.
|
|
29
35
|
# @param subject [String] The subject template which can access attributes defined by `#attributes_for`.
|
|
30
36
|
# @param delivery_method [Object] The delivery method as required by the mail gem.
|
|
31
|
-
# @param
|
|
32
|
-
|
|
37
|
+
# @param dump_body [Boolean] Attach a bounded rewindable request body to the error report.
|
|
38
|
+
# @param dump_environment [Boolean] Include application state and attach it as `state.yaml`.
|
|
39
|
+
# @param attachment_size_limit [Integer] The maximum size of each attachment.
|
|
40
|
+
# @param redact [Regexp | Nil] A pattern matching structured field names whose values should be redacted.
|
|
41
|
+
def initialize(app, to: "postmaster", from: DEFAULT_FROM, subject: DEFAULT_SUBJECT, delivery_method: LOCAL_SMTP, dump_body: false, dump_environment: false, attachment_size_limit: ATTACHMENT_SIZE_LIMIT, redact: SENSITIVE_FIELD)
|
|
33
42
|
super(app)
|
|
34
43
|
|
|
35
44
|
@to = to
|
|
36
45
|
@from = from
|
|
37
46
|
@subject = subject
|
|
38
47
|
@delivery_method = delivery_method
|
|
48
|
+
@dump_body = dump_body
|
|
39
49
|
@dump_environment = dump_environment
|
|
50
|
+
@attachment_size_limit = Integer(attachment_size_limit)
|
|
51
|
+
@redact = redact
|
|
52
|
+
|
|
53
|
+
if @attachment_size_limit < 0
|
|
54
|
+
raise ArgumentError, "attachment_size_limit must not be negative!"
|
|
55
|
+
end
|
|
40
56
|
end
|
|
41
57
|
|
|
42
58
|
# Freeze this object and its internal state.
|
|
@@ -48,7 +64,10 @@ module Utopia
|
|
|
48
64
|
@from.freeze
|
|
49
65
|
@subject.freeze
|
|
50
66
|
@delivery_method.freeze
|
|
67
|
+
@dump_body.freeze
|
|
51
68
|
@dump_environment.freeze
|
|
69
|
+
@attachment_size_limit.freeze
|
|
70
|
+
@redact.freeze
|
|
52
71
|
|
|
53
72
|
super
|
|
54
73
|
end
|
|
@@ -59,7 +78,7 @@ module Utopia
|
|
|
59
78
|
def call(request)
|
|
60
79
|
begin
|
|
61
80
|
return @delegate.call(request)
|
|
62
|
-
rescue => exception
|
|
81
|
+
rescue *APPLICATION_ERRORS => exception
|
|
63
82
|
request.exception = exception
|
|
64
83
|
send_notification exception, request
|
|
65
84
|
|
|
@@ -99,30 +118,32 @@ module Utopia
|
|
|
99
118
|
def generate_body(exception, request)
|
|
100
119
|
io = StringIO.new
|
|
101
120
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
# TODO embed the request body if it's textual?
|
|
105
|
-
# TODO dump and embed `utopia.variables`?
|
|
121
|
+
# Do not include the raw query string, as it may contain sensitive values:
|
|
122
|
+
io.puts "#{request.method} #{request.url.path.encoded}"
|
|
106
123
|
|
|
107
124
|
io.puts
|
|
108
125
|
|
|
109
126
|
REQUEST_ATTRIBUTES.each do |key|
|
|
110
|
-
value = request.send(key)
|
|
127
|
+
value = redact(key, request.send(key))
|
|
111
128
|
io.puts "request.#{key}: #{value.inspect}"
|
|
112
129
|
end
|
|
113
130
|
|
|
114
131
|
request.query_parameters.each do |key, value|
|
|
132
|
+
value = redact(key, value)
|
|
115
133
|
io.puts "request.query_parameters.#{key}: #{value.inspect}"
|
|
116
134
|
end
|
|
117
135
|
|
|
118
136
|
io.puts
|
|
119
137
|
|
|
120
138
|
request.headers.each do |key, value|
|
|
139
|
+
value = redact(key, value)
|
|
121
140
|
io.puts "header[#{key.inspect}]: #{value.inspect}"
|
|
122
141
|
end
|
|
123
142
|
|
|
124
|
-
|
|
125
|
-
|
|
143
|
+
if @dump_environment
|
|
144
|
+
filtered_state(request).each do |key, value|
|
|
145
|
+
io.puts "state.#{key}: #{value.inspect}"
|
|
146
|
+
end
|
|
126
147
|
end
|
|
127
148
|
|
|
128
149
|
io.puts
|
|
@@ -150,12 +171,14 @@ module Utopia
|
|
|
150
171
|
mail.text_part = Mail::Part.new
|
|
151
172
|
mail.text_part.body = generate_body(exception, request)
|
|
152
173
|
|
|
153
|
-
if
|
|
154
|
-
|
|
174
|
+
if @dump_body
|
|
175
|
+
if body = extract_body(request, @attachment_size_limit)
|
|
176
|
+
mail.attachments["body.bin"] = body
|
|
177
|
+
end
|
|
155
178
|
end
|
|
156
179
|
|
|
157
180
|
if @dump_environment
|
|
158
|
-
mail
|
|
181
|
+
attach(mail, "state.yaml", YAML.dump(filtered_state(request)))
|
|
159
182
|
end
|
|
160
183
|
|
|
161
184
|
return mail
|
|
@@ -168,8 +191,7 @@ module Utopia
|
|
|
168
191
|
|
|
169
192
|
mail.deliver
|
|
170
193
|
rescue => mail_exception
|
|
171
|
-
|
|
172
|
-
$stderr.puts mail_exception.backtrace
|
|
194
|
+
Console.warn(self, "Failed to deliver exception notification.", error: mail_exception)
|
|
173
195
|
end
|
|
174
196
|
|
|
175
197
|
def current_state(request)
|
|
@@ -181,11 +203,51 @@ module Utopia
|
|
|
181
203
|
}
|
|
182
204
|
end
|
|
183
205
|
|
|
184
|
-
def
|
|
206
|
+
def filtered_state(request)
|
|
207
|
+
redact(nil, current_state(request))
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def redact(name, value)
|
|
211
|
+
if @redact && name
|
|
212
|
+
if @redact.match?(name.to_s)
|
|
213
|
+
return REDACTED
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
case value
|
|
218
|
+
when Hash
|
|
219
|
+
return value.to_h do |key, item|
|
|
220
|
+
[key, redact(key, item)]
|
|
221
|
+
end
|
|
222
|
+
when Array
|
|
223
|
+
return value.map{|item| redact(nil, item)}
|
|
224
|
+
else
|
|
225
|
+
return value
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def attach(mail, name, content)
|
|
230
|
+
if content.bytesize <= @attachment_size_limit
|
|
231
|
+
mail.attachments[name] = content
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def extract_body(request, size_limit)
|
|
185
236
|
body = request.body
|
|
186
237
|
|
|
187
238
|
if body&.rewindable? && body.rewind
|
|
188
|
-
|
|
239
|
+
buffer = String.new.b
|
|
240
|
+
|
|
241
|
+
body.each do |chunk|
|
|
242
|
+
# Do not retain a partial body when the complete attachment would exceed the limit:
|
|
243
|
+
if chunk.bytesize > size_limit - buffer.bytesize
|
|
244
|
+
return nil
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
buffer << chunk
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
return buffer unless buffer.empty?
|
|
189
251
|
end
|
|
190
252
|
end
|
|
191
253
|
end
|
data/lib/utopia/import_map.rb
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
require "json"
|
|
7
7
|
require "xrb"
|
|
8
8
|
require "protocol/url"
|
|
9
|
+
require "bake/node/manifest"
|
|
9
10
|
|
|
10
11
|
module Utopia
|
|
11
12
|
# Represents an import map for JavaScript modules with support for URI and relative path resolution.
|
|
@@ -55,6 +56,19 @@ module Utopia
|
|
|
55
56
|
#
|
|
56
57
|
# puts page_map.to_html
|
|
57
58
|
class ImportMap
|
|
59
|
+
# Load the import mappings from a Bake Node static package manifest.
|
|
60
|
+
# @parameter root [String | Pathname] The static package output directory.
|
|
61
|
+
# @returns [ImportMap] A frozen import map using the manifest's public base URL.
|
|
62
|
+
def self.load_manifest(root)
|
|
63
|
+
manifest = Bake::Node::Manifest.load(root)
|
|
64
|
+
base = Protocol::URL[manifest.data.fetch("base")]
|
|
65
|
+
imports = manifest.import_map.fetch("imports").transform_values do |value|
|
|
66
|
+
Protocol::URL[value].relative_to(base).to_s
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
return self.new(imports, base: base).freeze
|
|
70
|
+
end
|
|
71
|
+
|
|
58
72
|
# Builder class for constructing import maps with scoped base URIs.
|
|
59
73
|
#
|
|
60
74
|
# The builder supports nested `with(base:)` blocks where each base is resolved
|