utopia 2.32.0 → 3.0.0
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/bake/utopia/environment.rb +6 -6
- data/bake/utopia/server.rb +4 -1
- data/bake/utopia/site.rb +14 -14
- data/bake/utopia/static.rb +26 -13
- data/bake/utopia.rb +1 -1
- data/context/getting-started.md +7 -5
- data/context/index.yaml +1 -2
- data/context/middleware.md +42 -13
- data/lib/utopia/application.rb +83 -0
- data/lib/utopia/content/builder.rb +25 -0
- data/lib/utopia/content/document.rb +64 -6
- data/lib/utopia/content/link.rb +50 -5
- data/lib/utopia/content/links.rb +44 -7
- data/lib/utopia/content/markup.rb +74 -0
- data/lib/utopia/content/middleware.rb +48 -20
- data/lib/utopia/content/namespace.rb +10 -1
- data/lib/utopia/content/node.rb +66 -4
- data/lib/utopia/content/response.rb +12 -3
- data/lib/utopia/content.rb +3 -0
- data/lib/utopia/controller/actions.md +4 -4
- data/lib/utopia/controller/actions.rb +51 -3
- data/lib/utopia/controller/base.rb +72 -29
- data/lib/utopia/controller/middleware.rb +20 -15
- data/lib/utopia/controller/respond.rb +36 -32
- data/lib/utopia/controller/responder.rb +106 -45
- data/lib/utopia/controller/result.rb +11 -0
- data/lib/utopia/controller/rewrite.rb +39 -0
- data/lib/utopia/controller/variables.rb +15 -3
- data/lib/utopia/controller.rb +2 -0
- data/lib/utopia/exceptions/handler.rb +20 -11
- data/lib/utopia/exceptions/mailer.rb +56 -51
- data/lib/utopia/extensions/array_split.rb +6 -0
- data/lib/utopia/extensions/date_comparisons.rb +6 -0
- data/lib/utopia/http.rb +11 -48
- data/lib/utopia/import_map.rb +19 -11
- data/lib/utopia/localization/locales.rb +60 -0
- data/lib/utopia/localization/middleware.rb +107 -73
- data/lib/utopia/localization/preferences.rb +76 -0
- data/lib/utopia/localization/resolver.rb +47 -0
- data/lib/utopia/localization.rb +6 -0
- data/lib/utopia/middleware.rb +3 -4
- data/lib/utopia/path/matcher.rb +15 -0
- data/lib/utopia/path.rb +146 -10
- data/lib/utopia/redirection/client_redirect.rb +87 -0
- data/lib/utopia/redirection/directory_index.rb +41 -0
- data/lib/utopia/redirection/errors.rb +78 -0
- data/lib/utopia/redirection/moved.rb +52 -0
- data/lib/utopia/redirection/request_failure.rb +26 -0
- data/lib/utopia/redirection/rewrite.rb +42 -0
- data/lib/utopia/redirection.rb +7 -165
- data/lib/utopia/request.rb +202 -0
- data/lib/utopia/response.rb +73 -0
- data/lib/utopia/session/lazy_hash.rb +27 -1
- data/lib/utopia/session/middleware.rb +103 -30
- data/lib/utopia/session/serialization.rb +8 -0
- data/lib/utopia/session.rb +5 -0
- data/lib/utopia/setup.rb +23 -3
- data/lib/utopia/shell.rb +28 -7
- data/lib/utopia/static/local_file.rb +84 -61
- data/lib/utopia/static/middleware.rb +65 -33
- data/lib/utopia/static/mime_types.rb +38 -29
- data/lib/utopia/static.rb +2 -0
- data/lib/utopia/version.rb +3 -2
- data/lib/utopia.rb +1 -1
- data/license.md +1 -1
- data/readme.md +33 -4
- data/releases.md +15 -0
- data/setup/site/bake.rb +1 -1
- data/setup/site/config/application.rb +51 -0
- data/setup/site/config/serve.rb +8 -0
- data/setup/site/falcon.rb +17 -4
- data/setup/site/fixtures/website.rb +27 -11
- data/setup/site/gems.rb +1 -3
- data/setup/site/lib/readme.txt +1 -1
- data/setup/site/pages/welcome/index.xnode +3 -3
- data/setup/site/readme.md +0 -3
- data/setup/site/test/website.rb +2 -2
- data.tar.gz.sig +0 -0
- metadata +60 -34
- metadata.gz.sig +0 -0
- data/lib/utopia/localization/wrapper.rb +0 -52
- data/setup/site/Guardfile +0 -12
- data/setup/site/config.ru +0 -49
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright,
|
|
4
|
+
# Copyright, 2025-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require_relative "../middleware"
|
|
7
|
-
require_relative "../
|
|
7
|
+
require_relative "../request"
|
|
8
|
+
require_relative "../response"
|
|
8
9
|
|
|
9
10
|
require_relative "local_file"
|
|
10
11
|
require_relative "mime_types"
|
|
12
|
+
require_relative "../localization/resolver"
|
|
11
13
|
|
|
12
14
|
require "traces/provider"
|
|
13
15
|
|
|
@@ -16,19 +18,24 @@ module Utopia
|
|
|
16
18
|
DEFAULT_CACHE_CONTROL = "public, max-age=3600".freeze
|
|
17
19
|
|
|
18
20
|
# A middleware which serves static files from the specified root directory.
|
|
19
|
-
class Middleware
|
|
21
|
+
class Middleware < Protocol::HTTP::Middleware
|
|
22
|
+
include Localization::Resolver
|
|
23
|
+
|
|
20
24
|
# @param root [String] The root directory to serve files from.
|
|
21
|
-
# @param types [Array] The
|
|
25
|
+
# @param types [Array] The file extensions and named groups to recognize/serve.
|
|
22
26
|
# @param cache_control [String] The cache-control header to set for static content.
|
|
23
27
|
def initialize(app, root: Utopia::default_root, types: MIME_TYPES[:default], cache_control: DEFAULT_CACHE_CONTROL)
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
super(app)
|
|
29
|
+
|
|
30
|
+
@root = File.expand_path(root)
|
|
26
31
|
|
|
27
32
|
@extensions = MimeTypeLoader.extensions_for(types)
|
|
28
33
|
|
|
29
34
|
@cache_control = cache_control
|
|
30
35
|
end
|
|
31
36
|
|
|
37
|
+
# Freeze this object and its internal state.
|
|
38
|
+
# @returns [self] This object.
|
|
32
39
|
def freeze
|
|
33
40
|
return self if frozen?
|
|
34
41
|
|
|
@@ -39,12 +46,12 @@ module Utopia
|
|
|
39
46
|
super
|
|
40
47
|
end
|
|
41
48
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if File.
|
|
47
|
-
return LocalFile.new(
|
|
49
|
+
# Open metadata for an existing file under the static root.
|
|
50
|
+
# @parameter local_path [String] The resolved filesystem path.
|
|
51
|
+
# @returns [LocalFile | Nil] The local file, or `nil` when it does not exist.
|
|
52
|
+
private def fetch_file(local_path)
|
|
53
|
+
if File.file?(local_path)
|
|
54
|
+
return LocalFile.new(local_path)
|
|
48
55
|
else
|
|
49
56
|
return nil
|
|
50
57
|
end
|
|
@@ -52,12 +59,16 @@ module Utopia
|
|
|
52
59
|
|
|
53
60
|
attr :extensions
|
|
54
61
|
|
|
55
|
-
LAST_MODIFIED = "
|
|
62
|
+
LAST_MODIFIED = "last-modified".freeze
|
|
56
63
|
CONTENT_TYPE = HTTP::CONTENT_TYPE
|
|
57
64
|
CACHE_CONTROL = HTTP::CACHE_CONTROL
|
|
58
|
-
ETAG = "
|
|
59
|
-
ACCEPT_RANGES = "
|
|
65
|
+
ETAG = "etag".freeze
|
|
66
|
+
ACCEPT_RANGES = "accept-ranges".freeze
|
|
60
67
|
|
|
68
|
+
# Build response headers for the given file.
|
|
69
|
+
# @parameter file [LocalFile] The file.
|
|
70
|
+
# @parameter content_type [String] The content type.
|
|
71
|
+
# @returns [Hash(String, String)] The response headers.
|
|
61
72
|
def response_headers_for(file, content_type)
|
|
62
73
|
if @cache_control.respond_to?(:call)
|
|
63
74
|
cache_control = @cache_control.call(file)
|
|
@@ -74,46 +85,67 @@ module Utopia
|
|
|
74
85
|
}
|
|
75
86
|
end
|
|
76
87
|
|
|
77
|
-
|
|
78
|
-
|
|
88
|
+
# Respond.
|
|
89
|
+
# @parameter request [Utopia::Request] The request.
|
|
90
|
+
# @parameter path [Protocol::URL::Path] The request path to serve.
|
|
91
|
+
# @parameter extension [String] The file extension.
|
|
92
|
+
# @parameter content_type [String] The file media type.
|
|
93
|
+
# @parameter localization [Utopia::Localization::Preferences | Nil] The selected localization.
|
|
94
|
+
# @returns [Protocol::HTTP::Response] The response.
|
|
95
|
+
def respond(request, path, extension, content_type, localization: request.localization)
|
|
96
|
+
local_path = path.local_path(@root)
|
|
79
97
|
|
|
80
|
-
if locale =
|
|
81
|
-
|
|
98
|
+
if locale = localization&.locale
|
|
99
|
+
local_path = local_path.delete_suffix(extension) + ".#{locale}#{extension}"
|
|
82
100
|
end
|
|
83
101
|
|
|
84
|
-
if file = fetch_file(
|
|
85
|
-
response_headers = self.response_headers_for(file,
|
|
102
|
+
if file = fetch_file(local_path)
|
|
103
|
+
response_headers = self.response_headers_for(file, content_type)
|
|
86
104
|
|
|
87
|
-
if file.modified?(
|
|
88
|
-
return file.serve(
|
|
105
|
+
if file.modified?(request)
|
|
106
|
+
return file.serve(request, response_headers)
|
|
89
107
|
else
|
|
90
|
-
return [304, response_headers, []]
|
|
108
|
+
return Response[304, response_headers, []]
|
|
91
109
|
end
|
|
92
110
|
end
|
|
93
111
|
end
|
|
94
112
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
113
|
+
# Serve a recognized static file or pass the request to the next middleware.
|
|
114
|
+
# @parameter request [Utopia::Request] The request.
|
|
115
|
+
# @returns [Protocol::HTTP::Response] The static-file or downstream response.
|
|
116
|
+
def call(request)
|
|
117
|
+
case request.method
|
|
118
|
+
when Protocol::HTTP::Methods::GET, Protocol::HTTP::Methods::HEAD
|
|
119
|
+
else
|
|
120
|
+
return @delegate.call(request)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
path = request.url.path
|
|
124
|
+
extension = File.extname(path.basename)
|
|
98
125
|
|
|
99
|
-
if @extensions
|
|
100
|
-
|
|
126
|
+
if content_type = @extensions[extension.downcase]
|
|
127
|
+
response = resolve_localized(request) do |localization|
|
|
128
|
+
self.respond(request, path, extension, content_type, localization: localization)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
if response
|
|
101
132
|
return response
|
|
102
133
|
end
|
|
103
134
|
end
|
|
104
135
|
|
|
105
136
|
# else if no file was found:
|
|
106
|
-
return @
|
|
137
|
+
return @delegate.call(request)
|
|
107
138
|
end
|
|
108
139
|
end
|
|
109
140
|
|
|
110
141
|
Traces::Provider(Static) do
|
|
111
|
-
def respond(
|
|
142
|
+
def respond(request, path, extension, content_type, localization: request.localization)
|
|
112
143
|
attributes = {
|
|
113
|
-
|
|
144
|
+
path: path,
|
|
145
|
+
locale: localization&.locale,
|
|
114
146
|
}
|
|
115
147
|
|
|
116
|
-
Traces.trace("utopia.static.respond", attributes: attributes)
|
|
148
|
+
Traces.trace("utopia.static.respond", attributes: attributes){super}
|
|
117
149
|
end
|
|
118
150
|
end
|
|
119
151
|
end
|
|
@@ -1,38 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2017-
|
|
4
|
+
# Copyright, 2017-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
|
-
require "
|
|
6
|
+
require "protocol/media/registry"
|
|
7
7
|
|
|
8
8
|
module Utopia
|
|
9
9
|
# A middleware which serves static files from the specified root directory.
|
|
10
10
|
module Static
|
|
11
11
|
# Default mime-types which are common for files served over HTTP:
|
|
12
12
|
MIME_TYPES = {
|
|
13
|
-
:xiph =>
|
|
14
|
-
"ogx"
|
|
15
|
-
|
|
16
|
-
"oga" => "audio/ogg",
|
|
17
|
-
"ogg" => "audio/ogg",
|
|
18
|
-
"spx" => "audio/ogg",
|
|
19
|
-
"flac" => "audio/flac",
|
|
20
|
-
"anx" => "application/annodex",
|
|
21
|
-
"axa" => "audio/annodex",
|
|
22
|
-
"xspf" => "application/xspf+xml",
|
|
23
|
-
},
|
|
13
|
+
:xiph => [
|
|
14
|
+
"ogx", "ogv", "oga", "ogg", "spx", "flac", "xspf"
|
|
15
|
+
],
|
|
24
16
|
:media => [
|
|
25
|
-
:xiph, "mp3", "mp4", "wav", "aiff", "aac", "webm", "mov", "avi", "wmv", "mpg", "m3u8", "ts"
|
|
17
|
+
:xiph, "mp3", "mp4", "wav", "aiff", "aac", "webm", "weba", "mov", "avi", "wmv", "mpg", "m3u8", "ts"
|
|
26
18
|
],
|
|
27
19
|
:text => [
|
|
28
|
-
"html", "css", "js",
|
|
20
|
+
"html", "css", "js", "map", "txt", "rtf", "xml", "pdf"
|
|
29
21
|
],
|
|
30
22
|
:fonts => [
|
|
31
|
-
"otf",
|
|
23
|
+
"otf", "eot", "ttf", "woff", "woff2"
|
|
32
24
|
],
|
|
33
25
|
:archive => [
|
|
34
|
-
"zip", "tar", "tgz", "
|
|
35
|
-
["torrent", "application/x-bittorrent"]
|
|
26
|
+
"zip", "tar", "tgz", "gz", "bz2", "dmg", "torrent"
|
|
36
27
|
],
|
|
37
28
|
:images => [
|
|
38
29
|
"png", "gif", "jpeg", "tiff", "svg", "webp"
|
|
@@ -44,6 +35,8 @@ module Utopia
|
|
|
44
35
|
|
|
45
36
|
# A class to assist with loading mime-type metadata.
|
|
46
37
|
class MimeTypeLoader
|
|
38
|
+
# Initialize an empty extension map backed by named MIME type groups.
|
|
39
|
+
# @parameter library [Object] The library.
|
|
47
40
|
def initialize(library)
|
|
48
41
|
@extensions = {}
|
|
49
42
|
@library = library
|
|
@@ -51,37 +44,53 @@ module Utopia
|
|
|
51
44
|
|
|
52
45
|
attr :extensions
|
|
53
46
|
|
|
47
|
+
# Find extensions associated with the given MIME types.
|
|
48
|
+
# @parameter types [Array] The MIME type definitions to expand.
|
|
49
|
+
# @parameter library [Hash] The named MIME type groups.
|
|
50
|
+
# @returns [Hash(String, String)] A mapping from file extensions to content types.
|
|
54
51
|
def self.extensions_for(types, library = MIME_TYPES)
|
|
55
52
|
loader = self.new(library)
|
|
56
53
|
loader.expand(types)
|
|
57
54
|
return loader.extensions
|
|
58
55
|
end
|
|
59
56
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
# Add an extension using its registered media type.
|
|
58
|
+
# @parameter extension [String] The file extension.
|
|
59
|
+
# @returns [Protocol::Media::Registry::Record] The registered media type record.
|
|
60
|
+
def add_extension(extension)
|
|
61
|
+
# Normalize optional leading dots and case before constructing the File.extname-style key:
|
|
62
|
+
extension = extension.delete_prefix(".").downcase
|
|
63
|
+
|
|
64
|
+
if record = Protocol::Media::Registry.for_extension(extension)
|
|
65
|
+
@extensions["." + extension] = record.type.to_s
|
|
66
|
+
return record
|
|
65
67
|
end
|
|
68
|
+
|
|
69
|
+
raise ExpansionError.new("Unknown file extension: #{extension.inspect}")
|
|
66
70
|
end
|
|
67
71
|
|
|
72
|
+
# Raised when expansion processing fails.
|
|
68
73
|
class ExpansionError < ArgumentError
|
|
69
74
|
end
|
|
70
75
|
|
|
76
|
+
# Expand named groups, file extensions, and explicit extension pairs into extension mappings.
|
|
77
|
+
# @parameter types [Array] The MIME type definitions.
|
|
78
|
+
# @returns [Array] The supplied definitions.
|
|
79
|
+
# @raises [ExpansionError] If a definition cannot be expanded.
|
|
71
80
|
def expand(types)
|
|
72
81
|
types.each do |type|
|
|
73
82
|
case type
|
|
74
83
|
when Symbol
|
|
75
|
-
self.expand(
|
|
84
|
+
self.expand(@library.fetch(type))
|
|
76
85
|
when Array
|
|
77
86
|
@extensions["." + type[0]] = type[1]
|
|
78
87
|
when String
|
|
79
|
-
self.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
when MIME::Type
|
|
83
|
-
self.extract_extensions.call([type])
|
|
88
|
+
self.add_extension(type)
|
|
89
|
+
else
|
|
90
|
+
raise ExpansionError.new("Unsupported MIME type definition: #{type.inspect}")
|
|
84
91
|
end
|
|
92
|
+
rescue ExpansionError
|
|
93
|
+
raise
|
|
85
94
|
rescue
|
|
86
95
|
raise ExpansionError.new("#{self.class.name}: Error while processing #{type.inspect}!")
|
|
87
96
|
end
|
data/lib/utopia/static.rb
CHANGED
data/lib/utopia/version.rb
CHANGED
data/lib/utopia.rb
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
require_relative "utopia/version"
|
|
7
7
|
|
|
8
|
+
require_relative "utopia/application"
|
|
8
9
|
require_relative "utopia/import_map"
|
|
9
10
|
require_relative "utopia/content"
|
|
10
11
|
require_relative "utopia/controller"
|
|
@@ -12,6 +13,5 @@ require_relative "utopia/exceptions"
|
|
|
12
13
|
require_relative "utopia/redirection"
|
|
13
14
|
require_relative "utopia/static"
|
|
14
15
|
|
|
15
|
-
# Utopia is a web application framework built on top of Rack.
|
|
16
16
|
module Utopia
|
|
17
17
|
end
|
data/license.md
CHANGED
data/readme.md
CHANGED
|
@@ -8,7 +8,7 @@ Utopia is a website generation framework which provides a robust set of tools to
|
|
|
8
8
|
|
|
9
9
|
- Designed for both content-based websites and applications. Does not depend on a database.
|
|
10
10
|
- Supports flexible content localization based on industry recommendations.
|
|
11
|
-
-
|
|
11
|
+
- Built directly on `protocol-http` with a small application and middleware interface.
|
|
12
12
|
- Low latency and high throughput. Capable of 10,000+ requests/second out of the box.
|
|
13
13
|
|
|
14
14
|
## Usage
|
|
@@ -17,7 +17,7 @@ Please see the [project documentation](https://socketry.github.io/utopia/) for m
|
|
|
17
17
|
|
|
18
18
|
- [Getting Started](https://socketry.github.io/utopia/guides/getting-started/index) - This guide explains how to set up a `utopia` website for local development and deployment.
|
|
19
19
|
|
|
20
|
-
- [Middleware](https://socketry.github.io/utopia/guides/middleware/index) - This guide gives an overview of the different
|
|
20
|
+
- [Middleware](https://socketry.github.io/utopia/guides/middleware/index) - This guide gives an overview of the different middleware used by Utopia.
|
|
21
21
|
|
|
22
22
|
- [Server Setup](https://socketry.github.io/utopia/guides/server-setup/index) - This guide explains how to deploy a `utopia` web application.
|
|
23
23
|
|
|
@@ -31,6 +31,21 @@ Please see the [project documentation](https://socketry.github.io/utopia/) for m
|
|
|
31
31
|
|
|
32
32
|
Please see the [project releases](https://socketry.github.io/utopia/releases/index) for all releases.
|
|
33
33
|
|
|
34
|
+
### v3.0.0
|
|
35
|
+
|
|
36
|
+
The 3.0.x series is considered a development release while the protocol HTTP application and controller interfaces stabilize.
|
|
37
|
+
|
|
38
|
+
- **Breaking** Require Ruby 3.3 or later.
|
|
39
|
+
- **Breaking** Replace the Rack-centric application boundary with <code class="language-ruby">Utopia::Application</code> and <code class="language-ruby">Protocol::HTTP::Middleware</code>. Applications now use `config/application.rb` and `config/serve.rb` instead of `config.ru`.
|
|
40
|
+
- **Breaking** Separate complete protocol responses from semantic controller results. Use `respond!` with a <code class="language-ruby">Protocol::HTTP::Response</code>, or `succeed!` with a value serialized by <code class="language-ruby">Utopia::Controller::Respond</code>.
|
|
41
|
+
- Parse request bodies at the controller layer using `protocol-content`, independently of URL query parameters.
|
|
42
|
+
- Resolve localization through immutable request preferences, avoiding repeated internal controller requests.
|
|
43
|
+
- Split redirection handling into focused middleware for rewrites, moved resources, directory indexes, and error documents.
|
|
44
|
+
- **Security** Fix handling of redirects that start with `//` to prevent open redirect vulnerabilities.
|
|
45
|
+
- Normalize request URLs using `Protocol::URL` and use structured paths throughout the middleware stack.
|
|
46
|
+
- Resolve static files through encoded URL paths, with improved `HEAD`, range, and validator handling.
|
|
47
|
+
- Use `protocol-media` and `protocol-http` for response and language negotiation, removing the `http-accept` dependency.
|
|
48
|
+
|
|
34
49
|
### v2.31.0
|
|
35
50
|
|
|
36
51
|
- Add agent context.
|
|
@@ -53,10 +68,8 @@ Please see the [project releases](https://socketry.github.io/utopia/releases/ind
|
|
|
53
68
|
- [Utopia::Gallery](https://github.com/ioquatix/utopia-gallery) — A fast photo gallery based on [libvips](https://github.com/jcupitt/libvips).
|
|
54
69
|
- [Utopia::Project](https://github.com/socketry/utopia-project) — A Ruby project documentation tool.
|
|
55
70
|
- [Utopia::Analytics](https://github.com/ioquatix/utopia-analytics) — Simple integration with Google Analytics.
|
|
56
|
-
- [HTTP::Accept](https://github.com/ioquatix/http-accept) — RFC compliant header parser.
|
|
57
71
|
- [Samovar](https://github.com/ioquatix/samovar) — Command line parser used by Utopia.
|
|
58
72
|
- [Mapping](https://github.com/ioquatix/mapping) — Provide structured conversions for web interfaces.
|
|
59
|
-
- [Rack::Test::Body](https://github.com/ioquatix/rack-test-body) — Provide convenient helpers for testing web interfaces.
|
|
60
73
|
|
|
61
74
|
### Examples
|
|
62
75
|
|
|
@@ -74,6 +87,22 @@ We welcome contributions to this project.
|
|
|
74
87
|
4. Push to the branch (`git push origin my-new-feature`).
|
|
75
88
|
5. Create new Pull Request.
|
|
76
89
|
|
|
90
|
+
### Running Tests
|
|
91
|
+
|
|
92
|
+
To run the test suite:
|
|
93
|
+
|
|
94
|
+
``` shell
|
|
95
|
+
bundle exec sus
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Making Releases
|
|
99
|
+
|
|
100
|
+
To make a new release:
|
|
101
|
+
|
|
102
|
+
``` shell
|
|
103
|
+
bundle exec bake gem:release:patch # or minor or major
|
|
104
|
+
```
|
|
105
|
+
|
|
77
106
|
### Developer Certificate of Origin
|
|
78
107
|
|
|
79
108
|
In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v3.0.0
|
|
4
|
+
|
|
5
|
+
The 3.0.x series is considered a development release while the protocol HTTP application and controller interfaces stabilize.
|
|
6
|
+
|
|
7
|
+
- **Breaking** Require Ruby 3.3 or later.
|
|
8
|
+
- **Breaking** Replace the Rack-centric application boundary with {ruby Utopia::Application} and {ruby Protocol::HTTP::Middleware}. Applications now use `config/application.rb` and `config/serve.rb` instead of `config.ru`.
|
|
9
|
+
- **Breaking** Separate complete protocol responses from semantic controller results. Use `respond!` with a {ruby Protocol::HTTP::Response}, or `succeed!` with a value serialized by {ruby Utopia::Controller::Respond}.
|
|
10
|
+
- Parse request bodies at the controller layer using `protocol-content`, independently of URL query parameters.
|
|
11
|
+
- Resolve localization through immutable request preferences, avoiding repeated internal controller requests.
|
|
12
|
+
- Split redirection handling into focused middleware for rewrites, moved resources, directory indexes, and error documents.
|
|
13
|
+
- **Security** Fix handling of redirects that start with `//` to prevent open redirect vulnerabilities.
|
|
14
|
+
- Normalize request URLs using `Protocol::URL` and use structured paths throughout the middleware stack.
|
|
15
|
+
- Resolve static files through encoded URL paths, with improved `HEAD`, range, and validator handling.
|
|
16
|
+
- Use `protocol-media` and `protocol-http` for response and language negotiation, removing the `http-accept` dependency.
|
|
17
|
+
|
|
3
18
|
## v2.31.0
|
|
4
19
|
|
|
5
20
|
- Add agent context.
|
data/setup/site/bake.rb
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "environment"
|
|
4
|
+
|
|
5
|
+
require "utopia/application"
|
|
6
|
+
require "utopia/controller"
|
|
7
|
+
require "utopia/content"
|
|
8
|
+
require "utopia/exceptions"
|
|
9
|
+
require "utopia/localization"
|
|
10
|
+
require "utopia/redirection"
|
|
11
|
+
require "utopia/session"
|
|
12
|
+
require "utopia/static"
|
|
13
|
+
|
|
14
|
+
Application = Utopia::Application.build do
|
|
15
|
+
if UTOPIA.production?
|
|
16
|
+
# Handle exceptions in production with an error page and send an email notification:
|
|
17
|
+
use Utopia::Exceptions::Handler
|
|
18
|
+
use Utopia::Exceptions::Mailer
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
use Utopia::Localization,
|
|
22
|
+
default_locale: "en",
|
|
23
|
+
locales: ["en", "de", "ja", "zh"]
|
|
24
|
+
|
|
25
|
+
# Serve static files from "public" directory:
|
|
26
|
+
use Utopia::Static, root: "public"
|
|
27
|
+
|
|
28
|
+
use Utopia::Redirection::Rewrite, {
|
|
29
|
+
"/" => "/welcome/index"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
use Utopia::Redirection::DirectoryIndex
|
|
33
|
+
|
|
34
|
+
# Handle error documents after client redirects so internal requests bypass them:
|
|
35
|
+
use Utopia::Redirection::Errors, {
|
|
36
|
+
404 => "/errors/file-not-found"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
use Utopia::Session,
|
|
40
|
+
expires_after: 3600 * 24,
|
|
41
|
+
secret: UTOPIA.secret_for(:session),
|
|
42
|
+
secure: true
|
|
43
|
+
|
|
44
|
+
use Utopia::Controller
|
|
45
|
+
|
|
46
|
+
# Serve static files from "pages" directory:
|
|
47
|
+
use Utopia::Static
|
|
48
|
+
|
|
49
|
+
# Serve dynamic content:
|
|
50
|
+
use Utopia::Content
|
|
51
|
+
end
|
data/setup/site/falcon.rb
CHANGED
|
@@ -2,11 +2,24 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
# Released under the MIT License.
|
|
5
|
-
# Copyright, 2019-
|
|
5
|
+
# Copyright, 2019-2026, by Samuel Williams.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
require "async/service/supervisor"
|
|
8
|
+
require "falcon/environment/application"
|
|
9
|
+
require "falcon/environment/lets_encrypt_tls"
|
|
10
|
+
require "utopia/application"
|
|
8
11
|
|
|
9
12
|
hostname = File.basename(__dir__)
|
|
10
|
-
rack hostname, :lets_encrypt_tls
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
service hostname do
|
|
15
|
+
include Falcon::Environment::Application
|
|
16
|
+
include Falcon::Environment::LetsEncryptTLS
|
|
17
|
+
|
|
18
|
+
def middleware
|
|
19
|
+
Utopia::Application.load
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
service "supervisor" do
|
|
24
|
+
include Async::Service::Supervisor::Environment
|
|
25
|
+
end
|
|
@@ -3,24 +3,41 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2016-2025, by Samuel Williams.
|
|
5
5
|
|
|
6
|
-
require "
|
|
6
|
+
require "protocol/http/request"
|
|
7
7
|
require "sus/fixtures/async/http"
|
|
8
|
-
require "
|
|
8
|
+
require "utopia/application"
|
|
9
|
+
require "uri"
|
|
9
10
|
|
|
10
11
|
AWebsite = Sus::Shared("a website") do
|
|
11
|
-
|
|
12
|
+
let(:application_path) {File.expand_path("../config/application.rb", __dir__)}
|
|
13
|
+
let(:application_directory) {File.dirname(application_path)}
|
|
12
14
|
|
|
13
|
-
let(:
|
|
14
|
-
let(:rackup_directory) {File.dirname(rackup_path)}
|
|
15
|
+
let(:app) {Utopia::Application.load(application_path)}
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
def get(path)
|
|
18
|
+
@last_request = Protocol::HTTP::Request["GET", path]
|
|
19
|
+
@last_response = app.call(@last_request)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
attr :last_request
|
|
23
|
+
attr :last_response
|
|
24
|
+
|
|
25
|
+
def follow_redirect!
|
|
26
|
+
location = last_response.headers["location"]
|
|
27
|
+
raise "Cannot follow redirect without a location header!" unless location
|
|
28
|
+
|
|
29
|
+
current = URI.join("http://localhost", last_request.path)
|
|
30
|
+
target = URI.join(current, location.to_s)
|
|
31
|
+
|
|
32
|
+
get(target.request_uri)
|
|
33
|
+
end
|
|
17
34
|
end
|
|
18
35
|
|
|
19
36
|
AValidPage = Sus::Shared("a valid page") do |path|
|
|
20
37
|
it "can access #{path}" do
|
|
21
38
|
get path
|
|
22
39
|
|
|
23
|
-
while last_response.
|
|
40
|
+
while last_response.redirection?
|
|
24
41
|
follow_redirect!
|
|
25
42
|
end
|
|
26
43
|
|
|
@@ -31,9 +48,8 @@ end
|
|
|
31
48
|
AServer = Sus::Shared("a server") do
|
|
32
49
|
include Sus::Fixtures::Async::HTTP::ServerContext
|
|
33
50
|
|
|
34
|
-
let(:
|
|
35
|
-
let(:
|
|
51
|
+
let(:application_path) {File.expand_path("../config/application.rb", __dir__)}
|
|
52
|
+
let(:application_directory) {File.dirname(application_path)}
|
|
36
53
|
|
|
37
|
-
let(:
|
|
38
|
-
let(:app) {Protocol::Rack::Adapter.new(rack_app)}
|
|
54
|
+
let(:app) {Utopia::Application.load(application_path)}
|
|
39
55
|
end
|
data/setup/site/gems.rb
CHANGED
|
@@ -17,8 +17,6 @@ gem "net-smtp"
|
|
|
17
17
|
|
|
18
18
|
group :development do
|
|
19
19
|
gem "bake-test"
|
|
20
|
-
gem "rack-test"
|
|
21
|
-
gem "guard-falcon", require: false
|
|
22
20
|
|
|
23
21
|
gem "sus"
|
|
24
22
|
gem "sus-fixtures-async-http"
|
|
@@ -29,5 +27,5 @@ group :development do
|
|
|
29
27
|
end
|
|
30
28
|
|
|
31
29
|
group :production do
|
|
32
|
-
gem "falcon"
|
|
30
|
+
gem "falcon", "~> 0.57"
|
|
33
31
|
end
|
data/setup/site/lib/readme.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
You can add additional code for your application in this directory, and require it directly from
|
|
1
|
+
You can add additional code for your application in this directory, and require it directly from config/application.rb.
|
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
<div>
|
|
12
12
|
<i class="fa fa-cubes"></i>
|
|
13
13
|
<h2>Modular code and structure</h2>
|
|
14
|
-
<p>Utopia provides independently useful
|
|
14
|
+
<p>Utopia provides independently useful HTTP middleware and has been designed with simplicity in mind. Several fully-featured webapps and a ton of commercial websites have guided the development of the Utopia stack. It is capable of handling a diverse range of requirements.</p>
|
|
15
15
|
</div>
|
|
16
16
|
|
|
17
17
|
<div>
|
|
18
18
|
<i class="fa fa-code"></i>
|
|
19
19
|
<h2>Well tested and maintained</h2>
|
|
20
|
-
<p>Utopia comprises a <a href="https://github.com/ioquatix/utopia">core gem</a> and several supporting libraries,
|
|
20
|
+
<p>Utopia comprises a <a href="https://github.com/ioquatix/utopia">core gem</a> and several supporting libraries, including <a href="https://github.com/socketry/xrb">XRB</a> for templates and markup parsing and <a href="https://github.com/socketry/protocol-http">Protocol HTTP</a> for HTTP protocol handling.</p>
|
|
21
21
|
</div>
|
|
22
22
|
|
|
23
23
|
<div>
|
|
@@ -32,4 +32,4 @@
|
|
|
32
32
|
<p>Utopia supports the <code>Accept-Language</code> header and transparently selects the correct view to render. Build multi-lingual websites and webapps easily: translate content incrementally as required, or not at all.</p>
|
|
33
33
|
</div>
|
|
34
34
|
</section>
|
|
35
|
-
</content:page>
|
|
35
|
+
</content:page>
|
data/setup/site/readme.md
CHANGED
|
@@ -9,8 +9,5 @@ To start the development server, simply execute
|
|
|
9
9
|
> bake
|
|
10
10
|
Generating transient session key for development...
|
|
11
11
|
20:57:36 - INFO - Starting Falcon HTTP server on localhost:9292
|
|
12
|
-
20:57:36 - INFO - Guard::RSpec is running
|
|
13
|
-
20:57:36 - INFO - Guard is now watching at '...'
|
|
14
|
-
[1] guard(main)>
|
|
15
12
|
|
|
16
13
|
Then browse https://localhost:9292 (or as specified) to see your new site.
|