Skip to content

Commit 5eae6de

Browse files
Use structured request paths.
Assisted-By: devx/cbdeae41-9308-4071-ad53-58cd92db2946
1 parent de519b5 commit 5eae6de

39 files changed

Lines changed: 661 additions & 246 deletions

lib/utopia/content/document.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def initialize(request, attributes = {}, localization: request&.localization)
5555

5656
# @returns [Path] The original request path, if known.
5757
def request_path
58-
Path[request.request_path]
58+
request.original_path
5959
end
6060

6161
protected def current_base_uri_path

lib/utopia/content/link.rb

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# Copyright, 2020, by Michael Adams.
77

88
require "yaml"
9+
require "protocol/url"
910
require "xrb/builder"
1011

1112
require "xrb/strings"
@@ -51,11 +52,7 @@ def full_path(root, extension = XNODE_EXTENSION)
5152
# Resolve this link's target URI.
5253
# @returns [String | Nil] The explicit target URI or one derived from the content path.
5354
def href
54-
@href ||= @info.fetch(:uri) do
55-
@info.fetch(:href) do
56-
(@path.dirname + @path.basename).to_s if @path
57-
end
58-
end
55+
target_url&.to_s
5956
end
6057

6158
# Look up from the `links.yaml` metadata with a given symbolic key.
@@ -91,10 +88,32 @@ def virtual?
9188
# @parameter base [Path | String | Nil] The source path.
9289
# @returns [Path | String | Nil] The relative or unchanged target.
9390
def relative_href(base = nil)
94-
if base and href.start_with? "/"
95-
Path.shortest_path(href, base)
96-
else
97-
href
91+
target = target_url
92+
return unless target
93+
94+
case target
95+
when Protocol::URL::Absolute
96+
return target.to_s
97+
when Protocol::URL::Relative
98+
if base && target.path.absolute?
99+
# Convert root-relative targets to application paths before finding the shortest path:
100+
path = Path.new(target.path.components(Protocol::URL::Encoding::System))
101+
relative_path = Path.shortest_path(path, base).to_url_path
102+
103+
return Protocol::URL::Relative.new(relative_path, target.query, target.fragment).to_s
104+
end
105+
end
106+
107+
return target.to_s
108+
end
109+
110+
private def target_url
111+
if @info.key?(:uri)
112+
Protocol::URL[@info[:uri]]
113+
elsif @info.key?(:href)
114+
Protocol::URL[@info[:href]]
115+
elsif @path
116+
Protocol::URL::Relative.new(@path.to_url_path)
98117
end
99118
end
100119

lib/utopia/content/middleware.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def respond(link, request, localization: request.localization)
123123
# @parameter request [Utopia::Request] The request.
124124
# @returns [Protocol::HTTP::Response] The content, redirect, or downstream response.
125125
def call(request)
126-
path = Path.create(request.path_info)
126+
path = request.path
127127

128128
# Check if the request is to a non-specific index. This only works for requests with a given name:
129129
basename = path.basename
@@ -132,8 +132,9 @@ def call(request)
132132
# If the request for /foo/bar is actually a directory, rewrite it to /foo/bar/index:
133133
if File.directory? directory_path
134134
index_path = [basename, INDEX]
135+
location = path.dirname.join(index_path).to_url_path.encoded
135136

136-
return Utopia::Response[307, {HTTP::LOCATION => path.dirname.join(index_path).to_s}, []]
137+
return Utopia::Response[307, {HTTP::LOCATION => location}, []]
137138
end
138139

139140
response = resolve_localized(request) do |localization|

lib/utopia/controller/actions.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ on "new" do |request|
3232
end
3333

3434
on "edit" do |request|
35-
@user = User.find(request.query_arguments["id"])
35+
@user = User.find(request.query_parameters["id"])
3636

3737
if request.post?
3838
@user.update_attributes(parse_body(request)["user"])
@@ -42,7 +42,7 @@ on "edit" do |request|
4242
end
4343

4444
on "delete" do |request|
45-
User.find(request.query_arguments["id"]).destroy
45+
User.find(request.query_parameters["id"]).destroy
4646

4747
redirect! "index"
4848
end

lib/utopia/controller/base.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# Copyright, 2014-2025, by Samuel Williams.
55

66
require_relative "../http"
7+
require_relative "../path"
78
require_relative "../response"
89
require_relative "result"
910

@@ -149,7 +150,12 @@ def ignore!
149150
# Request relative redirect. Respond with a redirect to the given target.
150151
def redirect!(target, status = 302)
151152
status = HTTP::Status.new(status, 300...400)
152-
location = target.to_s
153+
154+
if target.is_a?(Utopia::Path)
155+
location = target.to_url_path.encoded
156+
else
157+
location = target.to_s
158+
end
153159

154160
respond! Utopia::Response[status.to_i, {HTTP::LOCATION => location}, [status.to_s]]
155161
end

lib/utopia/controller/middleware.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def load_controller_file(uri_path)
8686

8787
# Invoke the controller layer for a given request. The request path may be rewritten.
8888
def invoke_controllers(request)
89-
request_path = Path.from_string(request.path_info)
89+
request_path = request.path
9090

9191
# The request path must be absolute. We could handle this internally but it is probably better for this to be an error:
9292
raise ArgumentError.new("Invalid request path #{request_path}") unless request_path.absolute?
@@ -114,8 +114,8 @@ def invoke_controllers(request)
114114
end
115115
end
116116

117-
# Controllers can directly modify relative_path, which is copied into controller_path. The controllers may have rewriten the path so we update the path info:
118-
request.path_info = controller_path.to_s
117+
# Controllers can directly modify the remaining path. Preserve those rewrites at the request boundary:
118+
request.path = controller_path
119119

120120
# No controller gave a useful result:
121121
return nil

lib/utopia/exceptions/handler.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def call(request)
4444
# We do an internal redirection to the error location:
4545
error_request = request.with(
4646
method: "GET",
47-
path_info: @location
47+
path: @location
4848
)
4949
error_request.exception = exception
5050

lib/utopia/exceptions/mailer.rb

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,6 @@ def call(request)
7979
:referrer,
8080
:path,
8181
:request_path,
82-
:path_info,
83-
:query,
8482
:user_agent,
8583
]
8684

@@ -113,8 +111,8 @@ def generate_body(exception, request)
113111
io.puts "request.#{key}: #{value.inspect}"
114112
end
115113

116-
request.query_arguments.each do |key, value|
117-
io.puts "request.query_arguments.#{key}: #{value.inspect}"
114+
request.query_parameters.each do |key, value|
115+
io.puts "request.query_parameters.#{key}: #{value.inspect}"
118116
end
119117

120118
io.puts

lib/utopia/import_map.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ def scope(scope_prefix, imports)
257257
# Create a new import map with paths relative to the given page path.
258258
# This is useful for creating page-specific import maps from a global one.
259259
#
260-
# @parameter path [String] The absolute page path to make imports relative to.
260+
# @parameter path [String | Protocol::URL::Path] The absolute page path to make imports relative to.
261261
# @returns [ImportMap] A new import map with a relative base.
262262
#
263263
# @example Creating page-specific import maps.
@@ -270,8 +270,8 @@ def scope(scope_prefix, imports)
270270
def relative_to(path)
271271
if @base
272272
# Calculate the relative path from the page to the base
273-
relative_base = Protocol::URL::Path.relative(@base.path, path)
274-
resolved_base = Protocol::URL[relative_base]
273+
relative_path = @base.path.relative(path)
274+
resolved_base = Protocol::URL::Relative.new(relative_path)
275275
else
276276
resolved_base = nil
277277
end

lib/utopia/localization/middleware.rb

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -92,16 +92,16 @@ def preferred_locales(request, path_locale = nil)
9292
return locales.to_a
9393
end
9494

95-
# Infer preferred locales from the request host.
95+
# Infer preferred locales from the request authority.
9696
# @parameter request [Utopia::Request] The application request.
97-
# @yields {|locale| ...} Each locale whose host pattern matches the request host.
97+
# @yields {|locale| ...} Each locale whose host pattern matches the request authority.
9898
# @returns [Hash] The configured host mappings.
9999
def host_preferred_locales(request)
100-
http_host = request.host.to_s
100+
authority = request.authority.to_s
101101

102-
# Yield all hosts which match the incoming http_host:
102+
# Yield all hosts which match the incoming authority:
103103
@hosts.each do |pattern, locale|
104-
if http_host[pattern]
104+
if authority[pattern]
105105
yield locale
106106
end
107107
end
@@ -111,16 +111,34 @@ def host_preferred_locales(request)
111111
# @parameter request [Utopia::Request] The application request.
112112
# @returns [Array(Utopia::Request, String | Nil)] The request and extracted locale.
113113
def extract_path_locale(request)
114-
path = Path[request.path_info]
114+
path = request.url.path
115115

116-
if request_locale = @all_locales.patterns[path.first]
117-
# Remove the localization prefix:
118-
path.delete_at(0)
119-
120-
return request.with(path_info: path.to_s), request_locale
121-
else
116+
# Localization prefixes only apply to absolute application paths:
117+
unless path.absolute?
122118
return request, nil
123119
end
120+
121+
if segment = path.segments[1]
122+
# Decode only the component which may contain the locale:
123+
component = Protocol::URL::Encoding::System.unescape(segment)
124+
125+
if request_locale = @all_locales.patterns[component]
126+
# Remove the locale while preserving all other encoded segments:
127+
segments = path.segments.dup
128+
segments.delete_at(1)
129+
130+
# Preserve the absolute root when the locale was the only component:
131+
if segments == [""]
132+
segments << ""
133+
end
134+
135+
path = Protocol::URL::Path.new(nil, segments)
136+
137+
return request.with(path: path), request_locale
138+
end
139+
end
140+
141+
return request, nil
124142
end
125143

126144
# Parse the locales preferred by the browser.
@@ -147,8 +165,8 @@ def browser_preferred_locales(request)
147165
# @returns [Boolean] Whether the path is eligible for localization.
148166
def localized?(request)
149167
# Ignore requests which match the ignored paths:
150-
path_info = request.path_info
151-
return false if @ignore.any?{|pattern| path_info[pattern] != nil}
168+
path = request.url.path.encoded
169+
return false if @ignore.any?{|pattern| path[pattern] != nil}
152170

153171
return true
154172
end

0 commit comments

Comments
 (0)