Class: Dommy::Rack::Navigation

Inherits:
Object
  • Object
show all
Defined in:
lib/dommy/rack/navigation.rb

Overview

URL resolution, redirect following, same-origin enforcement, and browser-tab-style history. Reads policy from the frozen Config and drives the Session only through its request seam (raw_request / apply_navigation_response / current_url).

Constant Summary collapse

KEEP_METHOD_STATUSES =
[307, 308].freeze

Instance Method Summary collapse

Constructor Details

#initialize(session, config) ⇒ Navigation

Returns a new instance of Navigation.



14
15
16
17
# File 'lib/dommy/rack/navigation.rb', line 14

def initialize(session, config)
  @session = session
  @config = config
end

Instance Method Details

#check_same_origin!(url) ⇒ Object

Raises:



27
28
29
30
31
32
# File 'lib/dommy/rack/navigation.rb', line 27

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) ⇒ Object

Fetch-style request: resolves and enforces origin, runs the redirect loop per mode, and returns the Response without touching session state.



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/dommy/rack/navigation.rb', line 48

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)

  args = {method: verb, url: target, params: params, body: body, headers: headers}
  case redirect
  when :follow
    run(**args, follow: true).first
  when :manual
    run(**args, follow: false).first
  when :error
    response = run(**args, follow: false).first
    raise Error, "redirect encountered with redirect: :error" if response.redirect?

    response
  else
    raise ArgumentError, "unsupported redirect mode: #{redirect.inspect}"
  end
end

Perform a navigation, following redirects per session policy, then apply the final response to the session (updating document + history).



36
37
38
39
40
41
42
43
44
# File 'lib/dommy/rack/navigation.rb', line 36

def navigate(method:, url:, params: nil, body: nil, headers: {})
  verb = method.to_s.upcase
  target = resolve_url(url, @session.current_url)
  check_same_origin!(target)

  response, final_url = run(method: verb, url: target, params: params, body: body, headers: headers)
  @session.apply_navigation_response(response, final_url)
  maybe_follow_meta_refresh(response) || response
end

#resolve_url(url_or_path, base_url) ⇒ Object

Resolve a possibly-relative URL against a base (current URL or host).



20
21
22
23
24
25
# File 'lib/dommy/rack/navigation.rb', line 20

def resolve_url(url_or_path, base_url)
  base = base_url || @config.default_host
  URI.join(base, url_or_path.to_s).to_s
rescue URI::InvalidURIError
  url_or_path.to_s
end

#revisit(url) ⇒ Object

Re-navigate to an already-resolved URL without pushing a new history entry (used by Session#back / #forward).



71
72
73
74
75
# File 'lib/dommy/rack/navigation.rb', line 71

def revisit(url)
  response, final_url = run(method: "GET", url: url)
  @session.apply_navigation_response(response, final_url, push_history: false)
  response
end

#run(method:, url:, params: nil, body: nil, headers: {}, follow: true) ⇒ Object

Run the request/redirect loop. Returns [response, final_url]. Public so Session#fetch can reuse it without applying navigation state.



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/dommy/rack/navigation.rb', line 79

def run(method:, url:, params: nil, body: nil, headers: {}, follow: true)
  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
    response = @session.raw_request(
      verb, target, params: params, body: body, headers: headers
    )

    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