fetch_util 0.5.2 → 0.6.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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -0
  3. data/README.md +3 -1
  4. data/Rakefile +0 -2
  5. data/lib/fetch_util/assets/extract.js +1 -1
  6. data/lib/fetch_util/browser/interaction_helpers/consent_helpers.rb +12 -17
  7. data/lib/fetch_util/browser/interaction_helpers/timing_helpers.rb +42 -15
  8. data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_commit_details_script.rb +48 -0
  9. data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_product_state_script.rb +80 -0
  10. data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_request_helpers_script.rb +53 -0
  11. data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_request_state_script.rb +118 -0
  12. data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_stabilization.rb +54 -0
  13. data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_state.rb +27 -0
  14. data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_diff_product_state.rb +55 -0
  15. data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_diff_request_state.rb +100 -0
  16. data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_diff_state.rb +59 -0
  17. data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_resource_modules.rb +26 -0
  18. data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_resource_state.rb +93 -0
  19. data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_resources.rb +73 -0
  20. data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_thread_state.rb +92 -0
  21. data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_threads.rb +71 -0
  22. data/lib/fetch_util/browser/site_stabilization/community_and_marketplace.rb +9 -10
  23. data/lib/fetch_util/browser/site_stabilization/facebook_stabilization.rb +56 -0
  24. data/lib/fetch_util/browser/site_stabilization/gerrit_change_product_state_script.rb +97 -0
  25. data/lib/fetch_util/browser/site_stabilization/gerrit_change_request_state_script.rb +75 -0
  26. data/lib/fetch_util/browser/site_stabilization/gerrit_change_stabilization.rb +37 -0
  27. data/lib/fetch_util/browser/site_stabilization/gerrit_change_state.rb +39 -0
  28. data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_fetch_state_script.rb +31 -0
  29. data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_product_state_script.rb +76 -0
  30. data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_request_state_script.rb +111 -0
  31. data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_route_state_script.rb +71 -0
  32. data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_stabilization.rb +37 -0
  33. data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_state.rb +42 -0
  34. data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_validation_state_script.rb +49 -0
  35. data/lib/fetch_util/browser/site_stabilization/gitea_family_pull_resource_product_state.rb +88 -0
  36. data/lib/fetch_util/browser/site_stabilization/gitea_family_pull_resource_state.rb +109 -0
  37. data/lib/fetch_util/browser/site_stabilization/gitea_family_pull_resources.rb +30 -0
  38. data/lib/fetch_util/browser/site_stabilization/gitea_family_stabilization.rb +60 -0
  39. data/lib/fetch_util/browser/site_stabilization/gitea_family_thread_product_state.rb +74 -0
  40. data/lib/fetch_util/browser/site_stabilization/gitea_family_thread_timeline_state.rb +69 -0
  41. data/lib/fetch_util/browser/site_stabilization/gitea_family_threads.rb +28 -0
  42. data/lib/fetch_util/browser/site_stabilization/gitea_family_visibility_script.rb +28 -0
  43. data/lib/fetch_util/browser/site_stabilization/github_pull_resource_stabilization.rb +31 -0
  44. data/lib/fetch_util/browser/site_stabilization/github_pull_resources.rb +105 -0
  45. data/lib/fetch_util/browser/site_stabilization/github_threads.rb +82 -0
  46. data/lib/fetch_util/browser/site_stabilization/gitlab_merge_request_resource_state.rb +17 -0
  47. data/lib/fetch_util/browser/site_stabilization/gitlab_merge_request_resource_state_script.rb +98 -0
  48. data/lib/fetch_util/browser/site_stabilization/gitlab_merge_request_resource_visibility_script.rb +43 -0
  49. data/lib/fetch_util/browser/site_stabilization/gitlab_merge_request_resources.rb +28 -0
  50. data/lib/fetch_util/browser/site_stabilization/gitlab_repo.rb +3 -11
  51. data/lib/fetch_util/browser/site_stabilization/gitlab_stabilization.rb +33 -0
  52. data/lib/fetch_util/browser/site_stabilization/gitlab_threads.rb +92 -0
  53. data/lib/fetch_util/browser/site_stabilization/social_platforms.rb +19 -38
  54. data/lib/fetch_util/browser/site_stabilization/travel_and_lodging.rb +3 -3
  55. data/lib/fetch_util/browser/site_stabilization.rb +89 -0
  56. data/lib/fetch_util/browser/stabilization/anubis.rb +4 -5
  57. data/lib/fetch_util/browser/stabilization/page_flow.rb +71 -27
  58. data/lib/fetch_util/browser/stabilization/spa_hydration.rb +7 -5
  59. data/lib/fetch_util/browser.rb +88 -17
  60. data/lib/fetch_util/cli.rb +69 -23
  61. data/lib/fetch_util/extractor.rb +8 -2
  62. data/lib/fetch_util/fetcher.rb +144 -43
  63. data/lib/fetch_util/http_redirect_client.rb +161 -0
  64. data/lib/fetch_util/parallel_fetcher.rb +125 -28
  65. data/lib/fetch_util/raw_docs_fallback.rb +158 -21
  66. data/lib/fetch_util/regulatory/cache_store.rb +47 -14
  67. data/lib/fetch_util/regulatory/fetch_records.rb +10 -7
  68. data/lib/fetch_util/regulatory/headers.rb +8 -1
  69. data/lib/fetch_util/regulatory/http_client.rb +2 -97
  70. data/lib/fetch_util/regulatory/orchestration.rb +10 -6
  71. data/lib/fetch_util/regulatory/page.rb +8 -11
  72. data/lib/fetch_util/regulatory/robots.rb +2 -10
  73. data/lib/fetch_util/regulatory/source_selection.rb +13 -3
  74. data/lib/fetch_util/regulatory/tdm_page.rb +9 -4
  75. data/lib/fetch_util/regulatory/tdm_policy.rb +16 -9
  76. data/lib/fetch_util/regulatory/tdm_rep.rb +4 -3
  77. data/lib/fetch_util/regulatory/tdm_support.rb +10 -6
  78. data/lib/fetch_util/regulatory.rb +5 -1
  79. data/lib/fetch_util/request_log.rb +24 -3
  80. data/lib/fetch_util/result.rb +37 -89
  81. data/lib/fetch_util/search_transport.rb +102 -35
  82. data/lib/fetch_util/searcher.rb +15 -13
  83. data/lib/fetch_util/version.rb +1 -1
  84. data/lib/fetch_util.rb +4 -1
  85. metadata +66 -2
@@ -1,103 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "net/http"
4
- require "uri"
3
+ require_relative "../http_redirect_client"
5
4
 
6
5
  module FetchUtil
7
- class HttpRedirectClient
8
- REDIRECT_LIMIT = 5
9
- TRANSIENT_ERRORS = [EOFError, IOError, SocketError, SystemCallError, Timeout::Error].freeze
10
- Response = Struct.new(:url, :status, :headers, :body, :redirects, keyword_init: true)
11
-
12
- def initialize(timeout:, headers: {})
13
- @timeout = timeout.to_f
14
- @headers = headers.reject { |_key, value| value.to_s.empty? }
15
- end
16
-
17
- def get(url, limit: REDIRECT_LIMIT)
18
- @connections = {}
19
- fetch(parse_http_uri(url), limit, [])
20
- ensure
21
- close_connections
22
- end
23
-
24
- private
25
-
26
- attr_reader :timeout, :headers
27
-
28
- def fetch(uri, limit, redirects)
29
- response = request(uri)
30
- return build_response(uri, response, redirects: redirects) unless response.is_a?(Net::HTTPRedirection)
31
-
32
- raise FetchUtil::Error, "too many redirects for #{uri}" if limit <= 0
33
-
34
- location = response["location"].to_s.strip
35
- return build_response(uri, response, redirects: redirects) if location.empty?
36
-
37
- redirect_response = build_response(uri, response)
38
- fetch(uri.merge(location), limit - 1, redirects + [redirect_response])
39
- end
40
-
41
- def request(uri)
42
- attempts = 0
43
- begin
44
- http = connection_for(uri)
45
- request = Net::HTTP::Get.new(uri.request_uri.empty? ? "/" : uri.request_uri)
46
- headers.each { |key, value| request[key] = value }
47
- http.request(request)
48
- rescue *TRANSIENT_ERRORS
49
- close_connection(uri)
50
- attempts += 1
51
- retry if attempts <= 1
52
- raise
53
- end
54
- end
55
-
56
- def connection_for(uri)
57
- key = [uri.scheme, uri.host, uri.port]
58
- @connections[key] ||= Net::HTTP.start(
59
- uri.host,
60
- uri.port,
61
- use_ssl: uri.scheme == "https",
62
- open_timeout: timeout,
63
- read_timeout: timeout
64
- )
65
- end
66
-
67
- def close_connection(uri)
68
- key = [uri.scheme, uri.host, uri.port]
69
- @connections.delete(key)&.finish
70
- rescue IOError
71
- nil
72
- end
73
-
74
- def close_connections
75
- @connections&.each_value do |http|
76
- http.finish if http.started?
77
- rescue IOError
78
- nil
79
- end
80
- @connections = nil
81
- end
82
-
83
- def build_response(uri, response, redirects: [])
84
- Response.new(
85
- url: uri.to_s,
86
- status: response.code.to_i,
87
- headers: response.to_hash.transform_keys(&:downcase),
88
- body: response.body.to_s,
89
- redirects: redirects
90
- )
91
- end
92
-
93
- def parse_http_uri(url)
94
- uri = URI.parse(url.to_s)
95
- raise URI::InvalidURIError, "unsupported url: #{url}" unless uri.is_a?(URI::HTTP) && uri.host
96
-
97
- uri
98
- end
99
- end
100
-
101
6
  class Regulatory
102
7
  class HttpClient
103
8
  DEFAULT_ACCEPT = "text/html,application/json,text/plain,*/*"
@@ -110,7 +15,7 @@ module FetchUtil
110
15
  def get(url, limit: FetchUtil::HttpRedirectClient::REDIRECT_LIMIT)
111
16
  build_response(redirect_client.get(url, limit: limit))
112
17
  rescue URI::InvalidURIError
113
- raise ArgumentError, "unsupported url: #{url}"
18
+ raise InputError, "unsupported url: #{url}"
114
19
  end
115
20
 
116
21
  private
@@ -5,8 +5,8 @@ module FetchUtil
5
5
  module Orchestration
6
6
  def initialize(client: nil, cache_path: DEFAULT_CACHE_PATH, sources: nil, timeout: 20, user_agent: nil)
7
7
  @client = client || HttpClient.new(timeout: timeout, user_agent: user_agent || default_user_agent)
8
- @cache_path = cache_path || DEFAULT_CACHE_PATH
9
- @source_tokens = sources
8
+ @cache_path = (cache_path || DEFAULT_CACHE_PATH).dup.freeze
9
+ @source_tokens = normalized_source_selection(sources)
10
10
  end
11
11
 
12
12
  def call(url)
@@ -14,7 +14,7 @@ module FetchUtil
14
14
  origin_query = origin_query?(requested_uri)
15
15
  query_target = request_target(requested_uri)
16
16
  effective_query_target = query_target
17
- selected_sources = resolve_sources(@source_tokens)
17
+ selected_sources = source_selection
18
18
  result = {}
19
19
  policy_refs = []
20
20
 
@@ -33,7 +33,7 @@ module FetchUtil
33
33
 
34
34
  if needs_robots_fetch?(selected_sources)
35
35
  record = robots_record(requested_uri)
36
- %w[robotstxt contentsignal contentusagerobots].each do |source|
36
+ ROBOTS_RECORD_SOURCES.each do |source|
37
37
  next unless selected_sources.include?(source)
38
38
 
39
39
  add_source_payload(
@@ -47,7 +47,7 @@ module FetchUtil
47
47
  if needs_page_fetch?(selected_sources)
48
48
  record = page_record(requested_uri)
49
49
  effective_query_target = page_query_target(record, fallback: query_target)
50
- %w[xrobotstag metarobots tdmheaders tdmmeta contentusageheader human].each do |source|
50
+ PAGE_RECORD_SOURCES.each do |source|
51
51
  next unless selected_sources.include?(source)
52
52
 
53
53
  add_source_payload(
@@ -63,7 +63,11 @@ module FetchUtil
63
63
  add_source_payload(
64
64
  result,
65
65
  "tdmpolicy",
66
- scoped_signals(expanded_tdm_policy_signals(policy_refs), origin_query: origin_query, query_target: effective_query_target)
66
+ scoped_signals(
67
+ expanded_tdm_policy_signals(policy_refs, target_origin: requested_uri),
68
+ origin_query: origin_query,
69
+ query_target: effective_query_target
70
+ )
67
71
  )
68
72
  end
69
73
 
@@ -13,13 +13,17 @@ module FetchUtil
13
13
  header_policies = []
14
14
 
15
15
  response_chain(response).each do |step|
16
- path = request_target(parse_http_uri(step.url))
16
+ step_uri = parse_http_uri(step.url)
17
+ next unless origin_key(step_uri).casecmp?(origin_key(final_uri))
18
+
19
+ path = request_target(step_uri)
17
20
  xrobotstag.concat(extract_x_robot_signals(step.headers, path: path))
18
21
  contentusageheader.concat(extract_content_usage_header_signals(step.headers, path: path))
19
22
  step_tdmheaders, step_policies = extract_tdm_value_signals(
20
23
  reservation: first_header_value(step.headers, "tdm-reservation"),
21
24
  policy_url: first_header_value(step.headers, "tdm-policy"),
22
- path: path
25
+ path: path,
26
+ target_origin: step_uri
23
27
  )
24
28
  tdmheaders.concat(step_tdmheaders)
25
29
  header_policies.concat(step_policies)
@@ -32,7 +36,7 @@ module FetchUtil
32
36
  if html_content?(response.headers, response.body)
33
37
  meta_tags = parse_meta_tags(response.body)
34
38
  metarobots = sort_generic_signals(extract_meta_robot_signals(meta_tags, path: final_path))
35
- tdmmeta, meta_policies = extract_tdm_meta_signals(meta_tags, path: final_path)
39
+ tdmmeta, meta_policies = extract_tdm_meta_signals(meta_tags, path: final_path, target_origin: final_uri)
36
40
  human = sort_generic_signals(extract_human_signals(response.body, path: final_path))
37
41
  end
38
42
 
@@ -54,14 +58,7 @@ module FetchUtil
54
58
  def empty_page_record
55
59
  {
56
60
  "final_url" => nil,
57
- "signals" => {
58
- "xrobotstag" => [],
59
- "metarobots" => [],
60
- "tdmheaders" => [],
61
- "tdmmeta" => [],
62
- "contentusageheader" => [],
63
- "human" => []
64
- },
61
+ "signals" => PAGE_RECORD_SOURCES.to_h { |source| [source, []] },
65
62
  "policies" => []
66
63
  }
67
64
  end
@@ -37,11 +37,7 @@ module FetchUtil
37
37
  end
38
38
 
39
39
  def extract_robots_source_signals(body)
40
- signals = {
41
- "robotstxt" => [],
42
- "contentsignal" => [],
43
- "contentusagerobots" => []
44
- }
40
+ signals = ROBOTS_RECORD_SOURCES.to_h { |source| [source, []] }
45
41
  user_agents = []
46
42
  in_rules = false
47
43
 
@@ -91,11 +87,7 @@ module FetchUtil
91
87
 
92
88
  def empty_robots_record
93
89
  {
94
- "signals" => {
95
- "robotstxt" => [],
96
- "contentsignal" => [],
97
- "contentusagerobots" => []
98
- }
90
+ "signals" => ROBOTS_RECORD_SOURCES.to_h { |source| [source, []] }
99
91
  }
100
92
  end
101
93
 
@@ -32,14 +32,24 @@ module FetchUtil
32
32
  selected
33
33
  end
34
34
 
35
+ def source_selection
36
+ @source_tokens ? @source_tokens.dup : resolve_sources(nil)
37
+ end
38
+
39
+ def normalized_source_selection(sources)
40
+ return if sources.nil?
41
+
42
+ resolve_sources(sources).map { |source| source.dup.freeze }.freeze
43
+ end
44
+
35
45
  def validate_source!(source)
36
46
  return if all_sources.include?(source)
37
47
 
38
- raise ArgumentError, "unsupported regulatory source: #{source}"
48
+ raise InputError, "unsupported regulatory source: #{source}"
39
49
  end
40
50
 
41
51
  def needs_page_fetch?(selected_sources)
42
- (selected_sources & (HUMAN_SOURCES + %w[xrobotstag metarobots tdmheaders tdmmeta contentusageheader tdmpolicy])).any?
52
+ (selected_sources & PAGE_RECORD_SOURCES).any? || selected_sources.include?("tdmpolicy")
43
53
  end
44
54
 
45
55
  def needs_tdmrep_fetch?(selected_sources)
@@ -47,7 +57,7 @@ module FetchUtil
47
57
  end
48
58
 
49
59
  def needs_robots_fetch?(selected_sources)
50
- (selected_sources & %w[robotstxt contentsignal contentusagerobots]).any?
60
+ (selected_sources & ROBOTS_RECORD_SOURCES).any?
51
61
  end
52
62
 
53
63
  def add_source_payload(result, source, signals)
@@ -3,7 +3,7 @@
3
3
  module FetchUtil
4
4
  class Regulatory
5
5
  module TdmPage
6
- def extract_tdm_meta_signals(meta_tags, path:)
6
+ def extract_tdm_meta_signals(meta_tags, path:, target_origin:)
7
7
  reservation = nil
8
8
  policy_url = nil
9
9
 
@@ -13,10 +13,15 @@ module FetchUtil
13
13
  policy_url ||= attributes["content"] if name == "tdm-policy"
14
14
  end
15
15
 
16
- extract_tdm_value_signals(reservation: reservation, policy_url: policy_url, path: path)
16
+ extract_tdm_value_signals(
17
+ reservation: reservation,
18
+ policy_url: policy_url,
19
+ path: path,
20
+ target_origin: target_origin
21
+ )
17
22
  end
18
23
 
19
- def extract_tdm_value_signals(reservation:, policy_url:, path:)
24
+ def extract_tdm_value_signals(reservation:, policy_url:, path:, target_origin:)
20
25
  value = reservation.to_s.strip
21
26
  return [[], []] unless %w[0 1].include?(value)
22
27
 
@@ -31,7 +36,7 @@ module FetchUtil
31
36
  conditions: conditions
32
37
  )
33
38
  ]
34
- policies = value == "1" ? [policy_ref(policy, path)] : []
39
+ policies = value == "1" ? [policy_ref(policy, path, target_origin: target_origin)] : []
35
40
  [signals, dedupe_policy_refs(policies)]
36
41
  end
37
42
  end
@@ -3,9 +3,11 @@
3
3
  module FetchUtil
4
4
  class Regulatory
5
5
  module TdmPolicy
6
- def expanded_tdm_policy_signals(policy_refs)
6
+ def expanded_tdm_policy_signals(policy_refs, target_origin:)
7
7
  dedupe_policy_refs(policy_refs).flat_map do |policy_ref|
8
- record = tdm_policy_record(policy_ref["url"])
8
+ policy_origin = policy_ref["target_origin"]
9
+ policy_origin = policy_origin ? parse_http_uri(policy_origin) : target_origin
10
+ record = tdm_policy_record(policy_ref["url"], target_origin: policy_origin)
9
11
  Array(record["signals"]).map do |template|
10
12
  signal = deep_copy(template)
11
13
  signal["path"] ||= policy_ref["path"]
@@ -17,25 +19,31 @@ module FetchUtil
17
19
  end
18
20
  end
19
21
 
20
- def tdm_policy_record(url)
21
- cache_fetch("tdmpolicy:#{url}") do
22
- response = safe_get(url)
22
+ def tdm_policy_record(url, target_origin:)
23
+ cache_fetch("tdmpolicy:#{origin_key(target_origin)}:#{url}") do
24
+ response, cacheable = safe_get(url)
23
25
  signals = []
24
26
  if response&.status&.between?(200, 299) && json_like_response?(response.headers, response.body)
25
- signals = extract_tdm_policy_signals(response.body)
27
+ signals = extract_tdm_policy_signals(response.body, target_origin: target_origin)
26
28
  end
27
- { "signals" => sort_specificity_signals(signals) }
29
+ [{ "signals" => sort_specificity_signals(signals) }, cacheable]
28
30
  end
29
31
  end
30
32
 
31
- def extract_tdm_policy_signals(body)
33
+ def extract_tdm_policy_signals(body, target_origin: nil)
32
34
  payload = JSON.parse(body.to_s)
35
+ return [] unless payload.is_a?(Hash)
36
+
33
37
  permissions = array_value(payload, "permission")
34
38
 
35
39
  permissions.filter_map do |permission|
36
40
  next unless permission.is_a?(Hash)
37
41
  next unless tdm_policy_action?(permission["action"])
38
42
 
43
+ target = permission["target"]
44
+ target_path = policy_target_path(target, target_origin: target_origin)
45
+ next if !target.to_s.strip.empty? && target_path.nil?
46
+
39
47
  conditions = {}
40
48
  duties = array_value(permission, "duty").filter_map { |item| duty_name(item) }
41
49
  conditions["duty"] = duties if duties.any?
@@ -43,7 +51,6 @@ module FetchUtil
43
51
  conditions["purpose"] = purpose if purpose
44
52
 
45
53
  signal = build_signal("allow", "text-and-data-mining", conditions: conditions)
46
- target_path = policy_target_path(permission["target"])
47
54
  signal["path"] = target_path if target_path
48
55
  signal
49
56
  end
@@ -9,7 +9,7 @@ module FetchUtil
9
9
  tdmrep_uri(requested_uri),
10
10
  fallback: { "signals" => [], "policies" => [] }
11
11
  ) do |body|
12
- signals, policies = extract_tdmrep_signals(body)
12
+ signals, policies = extract_tdmrep_signals(body, target_origin: requested_uri)
13
13
  {
14
14
  "signals" => sort_specificity_signals(signals),
15
15
  "policies" => policies
@@ -17,7 +17,7 @@ module FetchUtil
17
17
  end
18
18
  end
19
19
 
20
- def extract_tdmrep_signals(body)
20
+ def extract_tdmrep_signals(body, target_origin:)
21
21
  payload = JSON.parse(body.to_s)
22
22
  return [[], []] unless payload.is_a?(Array)
23
23
 
@@ -35,7 +35,8 @@ module FetchUtil
35
35
  rule_signals, rule_policies = extract_tdm_value_signals(
36
36
  reservation: reservation,
37
37
  policy_url: policy_url,
38
- path: normalize_output_path(location)
38
+ path: normalize_output_path(location),
39
+ target_origin: target_origin
39
40
  )
40
41
  signals.concat(rule_signals)
41
42
  policies.concat(rule_policies)
@@ -38,12 +38,16 @@ module FetchUtil
38
38
  nil
39
39
  end
40
40
 
41
- def policy_target_path(value)
41
+ def policy_target_path(value, target_origin:)
42
42
  target = value.to_s.strip
43
43
  return nil if target.empty?
44
44
 
45
45
  uri = URI.parse(target)
46
- return normalize_output_path(request_target(uri)) if uri.is_a?(URI::HTTP)
46
+ if uri.is_a?(URI::HTTP)
47
+ return nil unless target_origin && origin_key(uri).casecmp?(origin_key(target_origin))
48
+
49
+ return normalize_output_path(request_target(uri))
50
+ end
47
51
  return normalize_output_path(target) if target.start_with?("/")
48
52
 
49
53
  nil
@@ -63,12 +67,12 @@ module FetchUtil
63
67
  [value]
64
68
  end
65
69
 
66
- def policy_ref(url, path)
70
+ def policy_ref(url, path, target_origin:)
67
71
  candidate = url.to_s.strip
68
72
  return nil if candidate.empty?
69
73
 
70
74
  parse_http_uri(candidate)
71
- { "url" => candidate, "path" => path }
75
+ { "url" => candidate, "path" => path, "target_origin" => origin_key(target_origin) }
72
76
  rescue ArgumentError
73
77
  nil
74
78
  end
@@ -77,7 +81,7 @@ module FetchUtil
77
81
  seen = {}
78
82
  list = []
79
83
  Array(policy_refs).compact.each do |policy_ref|
80
- key = [policy_ref["url"], policy_ref["path"]]
84
+ key = [policy_ref["url"], policy_ref["path"], policy_ref["target_origin"]]
81
85
  next if seen[key]
82
86
 
83
87
  seen[key] = true
@@ -87,7 +91,7 @@ module FetchUtil
87
91
  end
88
92
 
89
93
  def odrl_token(value)
90
- value.to_s.strip.downcase.split(/[#:]/).last.to_s
94
+ value.to_s.strip.downcase.split(%r{[/#:]}).reject(&:empty?).last.to_s
91
95
  end
92
96
  end
93
97
  end
@@ -5,6 +5,7 @@ require "digest"
5
5
  require "fileutils"
6
6
  require "json"
7
7
  require "openssl"
8
+ require "securerandom"
8
9
  require "time"
9
10
  require "timeout"
10
11
  require "uri"
@@ -12,8 +13,10 @@ require "uri"
12
13
  module FetchUtil
13
14
  class Regulatory
14
15
  CACHE_TTL = 86_400
15
- CACHE_VERSION = 2
16
+ CACHE_VERSION = 3
16
17
  DEFAULT_CACHE_PATH = File.expand_path("~/.local/state/fetch_util/regulatory-cache")
18
+ ROBOTS_RECORD_SOURCES = %w[robotstxt contentsignal contentusagerobots].freeze
19
+ PAGE_RECORD_SOURCES = %w[xrobotstag metarobots tdmheaders tdmmeta contentusageheader human].freeze
17
20
  MACHINE_SOURCES = %w[
18
21
  robotstxt
19
22
  contentsignal
@@ -32,6 +35,7 @@ module FetchUtil
32
35
  "machine" => MACHINE_SOURCES,
33
36
  "human" => HUMAN_SOURCES
34
37
  }.freeze
38
+ private_constant :ROBOTS_RECORD_SOURCES, :PAGE_RECORD_SOURCES
35
39
 
36
40
  Response = Struct.new(:url, :status, :headers, :body, :redirects, keyword_init: true)
37
41
  autoload :HttpClient, "fetch_util/regulatory/http_client"
@@ -6,19 +6,40 @@ require "time"
6
6
  module FetchUtil
7
7
  class RequestLog
8
8
  DEFAULT_PATH = File.expand_path("~/.local/state/fetch_util/requests.log")
9
+ DELIMITER_ESCAPES = { "\t" => "\\t", "\r" => "\\r", "\n" => "\\n" }.freeze
9
10
 
10
- def initialize(path: ENV.fetch("FETCH_UTIL_REQUEST_LOG", DEFAULT_PATH))
11
- @path = path
11
+ def initialize(path: nil)
12
+ configured_path = path || ENV.fetch("FETCH_UTIL_REQUEST_LOG", DEFAULT_PATH)
13
+ @path = (path.nil? ? configured_path.dup : normalized_path(path)).freeze
12
14
  end
13
15
 
14
16
  attr_reader :path
15
17
 
16
18
  def append(entry, duration: nil)
17
19
  FileUtils.mkdir_p(File.dirname(path))
18
- line = "#{Time.now.utc.iso8601}\t#{entry}"
20
+ escaped_entry = entry.to_s.gsub(/[\t\r\n]/, DELIMITER_ESCAPES)
21
+ line = "#{Time.now.utc.iso8601}\t#{escaped_entry}"
19
22
  line = "#{line}\t#{format("%.2f", duration)}s" if duration
20
23
  File.open(path, "a") { |file| file.puts(line) }
21
24
  path
22
25
  end
26
+
27
+ private
28
+
29
+ def normalized_path(value)
30
+ raw_path = value.to_s
31
+ return raw_path.dup if raw_path.empty? || directory_path?(raw_path)
32
+
33
+ expanded_path = File.expand_path(raw_path)
34
+ basename = File.basename(raw_path)
35
+ return expanded_path if basename.start_with?(".") || !File.extname(basename).empty?
36
+
37
+ "#{expanded_path}.log"
38
+ end
39
+
40
+ def directory_path?(path)
41
+ separators = [File::SEPARATOR, File::ALT_SEPARATOR].compact
42
+ separators.any? { |separator| path.end_with?(separator) } || %w[. ..].include?(File.basename(path))
43
+ end
23
44
  end
24
45
  end