Class: Dommy::Rack::Navigation
- Inherits:
-
Object
- Object
- Dommy::Rack::Navigation
- Defined in:
- lib/dommy/rack/navigation.rb,
sig/dommy/rack.rbs
Overview
URL resolution, redirect following, same-origin enforcement, meta refresh.
Constant Summary collapse
- KEEP_METHOD_STATUSES =
[307, 308].freeze
- QUERY_METHODS =
Verbs whose params belong in the URL query (vs a request body).
%w[GET HEAD].freeze
Instance Method Summary collapse
-
#append_query(url, params) ⇒ Object
Merge ordered [name, value] params into
url's query, preserving any existing query and keeping a fragment last (browser address-bar form). - #check_same_origin!(url) ⇒ void
-
#fetch(url, method: "GET", params: nil, body: nil, headers: {}, redirect: :follow) ⇒ Response
Fetch-style request: resolves and enforces origin, runs the redirect loop per mode, and returns the Response without touching session state.
-
#fetch_resolved(exchange, method, target, params: nil, body: nil, headers: {}, redirect: :follow) ⇒ Object
Worker-safe variant of #fetch:
targetis already absolute and origin- checked (the page thread did both before handing off), and every request in the redirect loop is issued throughexchange(which touches only thread-safe state) instead of the session. -
#initialize(session, config) ⇒ Navigation
constructor
A new instance of Navigation.
-
#navigate(method:, url:, params: nil, body: nil, headers: {}, replace: false) ⇒ Response
Perform a navigation, following redirects per session policy, then apply the final response to the session (updating document + history).
-
#resolve_url(url_or_path, base_url) ⇒ String
Resolve a possibly-relative URL against a base (current URL or host).
-
#revisit(url) ⇒ Response
Re-navigate to an already-resolved URL without pushing a new history entry (used by Session#back / #forward).
-
#run(method:, url:, params: nil, body: nil, headers: {}, follow: true, exchange: nil) ⇒ [ Response, String ]
Run the request/redirect loop.
Constructor Details
#initialize(session, config) ⇒ Navigation
Returns a new instance of Navigation.
16 17 18 19 |
# File 'lib/dommy/rack/navigation.rb', line 16 def initialize(session, config) @session = session @config = config end |
Instance Method Details
#append_query(url, params) ⇒ Object
Merge ordered [name, value] params into url's query, preserving any
existing query and keeping a fragment last (browser address-bar form).
35 36 37 38 39 40 41 42 |
# File 'lib/dommy/rack/navigation.rb', line 35 def append_query(url, params) encoded = URI.encode_www_form(params) return url if encoded.empty? base, hash, fragment = url.to_s.partition("#") sep = base.include?("?") ? "&" : "?" "#{base}#{sep}#{encoded}#{hash}#{fragment}" end |
#check_same_origin!(url) ⇒ void
This method returns an undefined value.
44 45 46 47 48 49 |
# File 'lib/dommy/rack/navigation.rb', line 44 def check_same_origin!(url) return unless @config.enforce_same_origin return if same_origin?(url, @config.default_host) raise CrossOriginError, "cross-origin request to #{url} is not allowed" end |
#fetch(url, method: "GET", params: nil, body: nil, headers: {}, redirect: :follow) ⇒ Response
Fetch-style request: resolves and enforces origin, runs the redirect loop per mode, and returns the Response without touching session state.
78 79 80 81 82 83 |
# File 'lib/dommy/rack/navigation.rb', line 78 def fetch(url, method: "GET", params: nil, body: nil, headers: {}, redirect: :follow) verb = method.to_s.upcase target = resolve_url(url, @session.current_url) check_same_origin!(target) run_fetch(verb, target, params: params, body: body, headers: headers, redirect: redirect) end |
#fetch_resolved(exchange, method, target, params: nil, body: nil, headers: {}, redirect: :follow) ⇒ Object
Worker-safe variant of #fetch: target is already absolute and origin-
checked (the page thread did both before handing off), and every request
in the redirect loop is issued through exchange (which touches only
thread-safe state) instead of the session. Returns the Response. This is
the primitive a network worker runs for the async-network path.
90 91 92 93 |
# File 'lib/dommy/rack/navigation.rb', line 90 def fetch_resolved(exchange, method, target, params: nil, body: nil, headers: {}, redirect: :follow) run_fetch(method.to_s.upcase, target, params: params, body: body, headers: headers, redirect: redirect, exchange: exchange) end |
#navigate(method:, url:, params: nil, body: nil, headers: {}, replace: false) ⇒ Response
Perform a navigation, following redirects per session policy, then apply the final response to the session (updating document + history).
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/dommy/rack/navigation.rb', line 53 def navigate(method:, url:, params: nil, body: nil, headers: {}, replace: false) return navigate_about(url.to_s) if url.to_s.start_with?("about:") verb = method.to_s.upcase target = resolve_url(url, @session.current_url) # A GET-style navigation carries its data in the URL: fold params into the # query so current_url (the address the user sees, and reloads) reflects # what was submitted — exactly what a browser shows after a GET form. POST # keeps params as the request body. (params and body are mutually # exclusive, so a GET never has a body to conflict with.) if params && QUERY_METHODS.include?(verb) target = append_query(target, params) params = nil end check_same_origin!(target) response, final_url = run(method: verb, url: target, params: params, body: body, headers: headers) # replace: a location.replace() / reload() / redirect updates the current # history entry in place rather than pushing a new one. @session.(response, final_url, replace: replace) (response) || response end |
#resolve_url(url_or_path, base_url) ⇒ String
Resolve a possibly-relative URL against a base (current URL or host).
22 23 24 25 26 27 28 29 30 31 |
# File 'lib/dommy/rack/navigation.rb', line 22 def resolve_url(url_or_path, base_url) base = base_url || @config.default_host # A link/redirect target may carry raw UTF-8 (e.g. /hashtag/応援); the # ASCII-only URI parser needs it percent-encoded first (what a browser # does), or URI.join raises and the rescue would leak a non-ASCII URL # that crashes downstream (cookie matching, request building). URI.join(base, Url.encode_iri(url_or_path)).to_s rescue URI::InvalidURIError url_or_path.to_s end |
#revisit(url) ⇒ Response
Re-navigate to an already-resolved URL without pushing a new history entry (used by Session#back / #forward).
97 98 99 100 101 |
# File 'lib/dommy/rack/navigation.rb', line 97 def revisit(url) response, final_url = run(method: "GET", url: url) @session.(response, final_url, push_history: false) response end |
#run(method:, url:, params: nil, body: nil, headers: {}, follow: true, exchange: nil) ⇒ [ Response, String ]
Run the request/redirect loop. Returns [response, final_url]. Public so Session#fetch can reuse it without applying navigation state.
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/dommy/rack/navigation.rb', line 105 def run(method:, url:, params: nil, body: nil, headers: {}, follow: true, exchange: nil) verb = method target = url # Carry the fragment across redirects: a redirect Location without its # own fragment preserves the previous one (browser behavior). fragment = uri_fragment(target) redirect_count = 0 chain = [] loop do # Default path issues through the session (page thread). When an # `exchange` is injected, the very same loop runs on a network worker. response = if exchange exchange.request(verb, target, params: params, body: body, headers: headers) else @session.raw_request(verb, target, params: params, body: body, headers: headers) end unless follow && redirect_to_follow?(response) response.redirects = chain return [response, with_fragment(target, fragment)] end chain << {status: response.status, url: target, location: response.location_header} redirect_count += 1 if redirect_count > @config.max_redirects raise TooManyRedirectsError, "exceeded #{@config.max_redirects} redirects" end target = resolve_url(response.location_header, target) check_same_origin!(target) location_fragment = uri_fragment(target) fragment = location_fragment unless location_fragment.nil? || location_fragment.empty? unless KEEP_METHOD_STATUSES.include?(response.status) params = nil body = nil end verb = redirect_method(response.status, verb) end end |