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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +49 -0
- data/README.md +3 -1
- data/Rakefile +0 -2
- data/lib/fetch_util/assets/extract.js +1 -1
- data/lib/fetch_util/browser/interaction_helpers/consent_helpers.rb +12 -17
- data/lib/fetch_util/browser/interaction_helpers/timing_helpers.rb +42 -15
- data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_commit_details_script.rb +48 -0
- data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_product_state_script.rb +80 -0
- data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_request_helpers_script.rb +53 -0
- data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_request_state_script.rb +118 -0
- data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_stabilization.rb +54 -0
- data/lib/fetch_util/browser/site_stabilization/azure_devops_pr_state.rb +27 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_diff_product_state.rb +55 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_diff_request_state.rb +100 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_diff_state.rb +59 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_resource_modules.rb +26 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_resource_state.rb +93 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_pull_resources.rb +73 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_thread_state.rb +92 -0
- data/lib/fetch_util/browser/site_stabilization/bitbucket_cloud_threads.rb +71 -0
- data/lib/fetch_util/browser/site_stabilization/community_and_marketplace.rb +9 -10
- data/lib/fetch_util/browser/site_stabilization/facebook_stabilization.rb +56 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_change_product_state_script.rb +97 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_change_request_state_script.rb +75 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_change_stabilization.rb +37 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_change_state.rb +39 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_fetch_state_script.rb +31 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_product_state_script.rb +76 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_request_state_script.rb +111 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_route_state_script.rb +71 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_stabilization.rb +37 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_state.rb +42 -0
- data/lib/fetch_util/browser/site_stabilization/gerrit_file_resource_validation_state_script.rb +49 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_pull_resource_product_state.rb +88 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_pull_resource_state.rb +109 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_pull_resources.rb +30 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_stabilization.rb +60 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_thread_product_state.rb +74 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_thread_timeline_state.rb +69 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_threads.rb +28 -0
- data/lib/fetch_util/browser/site_stabilization/gitea_family_visibility_script.rb +28 -0
- data/lib/fetch_util/browser/site_stabilization/github_pull_resource_stabilization.rb +31 -0
- data/lib/fetch_util/browser/site_stabilization/github_pull_resources.rb +105 -0
- data/lib/fetch_util/browser/site_stabilization/github_threads.rb +82 -0
- data/lib/fetch_util/browser/site_stabilization/gitlab_merge_request_resource_state.rb +17 -0
- data/lib/fetch_util/browser/site_stabilization/gitlab_merge_request_resource_state_script.rb +98 -0
- data/lib/fetch_util/browser/site_stabilization/gitlab_merge_request_resource_visibility_script.rb +43 -0
- data/lib/fetch_util/browser/site_stabilization/gitlab_merge_request_resources.rb +28 -0
- data/lib/fetch_util/browser/site_stabilization/gitlab_repo.rb +3 -11
- data/lib/fetch_util/browser/site_stabilization/gitlab_stabilization.rb +33 -0
- data/lib/fetch_util/browser/site_stabilization/gitlab_threads.rb +92 -0
- data/lib/fetch_util/browser/site_stabilization/social_platforms.rb +19 -38
- data/lib/fetch_util/browser/site_stabilization/travel_and_lodging.rb +3 -3
- data/lib/fetch_util/browser/site_stabilization.rb +89 -0
- data/lib/fetch_util/browser/stabilization/anubis.rb +4 -5
- data/lib/fetch_util/browser/stabilization/page_flow.rb +71 -27
- data/lib/fetch_util/browser/stabilization/spa_hydration.rb +7 -5
- data/lib/fetch_util/browser.rb +88 -17
- data/lib/fetch_util/cli.rb +69 -23
- data/lib/fetch_util/extractor.rb +8 -2
- data/lib/fetch_util/fetcher.rb +144 -43
- data/lib/fetch_util/http_redirect_client.rb +161 -0
- data/lib/fetch_util/parallel_fetcher.rb +125 -28
- data/lib/fetch_util/raw_docs_fallback.rb +158 -21
- data/lib/fetch_util/regulatory/cache_store.rb +47 -14
- data/lib/fetch_util/regulatory/fetch_records.rb +10 -7
- data/lib/fetch_util/regulatory/headers.rb +8 -1
- data/lib/fetch_util/regulatory/http_client.rb +2 -97
- data/lib/fetch_util/regulatory/orchestration.rb +10 -6
- data/lib/fetch_util/regulatory/page.rb +8 -11
- data/lib/fetch_util/regulatory/robots.rb +2 -10
- data/lib/fetch_util/regulatory/source_selection.rb +13 -3
- data/lib/fetch_util/regulatory/tdm_page.rb +9 -4
- data/lib/fetch_util/regulatory/tdm_policy.rb +16 -9
- data/lib/fetch_util/regulatory/tdm_rep.rb +4 -3
- data/lib/fetch_util/regulatory/tdm_support.rb +10 -6
- data/lib/fetch_util/regulatory.rb +5 -1
- data/lib/fetch_util/request_log.rb +24 -3
- data/lib/fetch_util/result.rb +37 -89
- data/lib/fetch_util/search_transport.rb +102 -35
- data/lib/fetch_util/searcher.rb +15 -13
- data/lib/fetch_util/version.rb +1 -1
- data/lib/fetch_util.rb +4 -1
- metadata +66 -2
|
@@ -21,6 +21,13 @@ module FetchUtil
|
|
|
21
21
|
keyword_init: true
|
|
22
22
|
)
|
|
23
23
|
|
|
24
|
+
COMMON_CMP_SELECTORS = [
|
|
25
|
+
"#onetrust-banner-sdk", "#onetrust-pc-sdk", ".qc-cmp2-container", ".qc-cmp2-summary",
|
|
26
|
+
"#CybotCookiebotDialog", ".cc-window", ".cc_banner", "#cookie-banner", ".cookie-banner",
|
|
27
|
+
"#consent-banner", ".consent-banner", ".fc-consent-root", ".cmp-modal", ".gdpr-banner",
|
|
28
|
+
"#gdpr-consent", ".js-cookies", ".cookie-notice", "#cookieNotice"
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
24
31
|
# rubocop:disable Layout/LineLength, Style/RedundantPercentQ
|
|
25
32
|
DEFAULT_CONSENT_CONFIG = ConsentConfig.new(
|
|
26
33
|
accept_labels: [
|
|
@@ -71,19 +78,13 @@ module FetchUtil
|
|
|
71
78
|
context_pattern: %q{(cookie|cookies|privacy|consent|gdpr|ccpa|cmp|onetrust|cookiebot|didomi|quantcast|before you continue|we use cookies and data|device identifiers|personalized ads|personalized content|trusted third party partners?|privacy preference center|your privacy settings|your privacy choices|manage privacy preferences|manage consent preferences|cookie information|cookie list|cookies details|list of partners(?: \(vendors\))?|configurações avançadas de cookies|declaração de cookies|gerenciar cookies|utilizamos cookies|dados pessoais|pliki cookie|datenschutz|données personnelles|datos personales|dati personali|wish to store|access information on your devices|preferenze cookie|クッキー|Cookieプリファレンス|Cookie設定|同意設定|쿠키|동의|개인정보|接受|隐私设置|cookie 偏好设置|файлы cookie|настройки cookie|souhlas|personalizac|soukromí|nastavení souhlasu|kakor|sekretess|samtycke|cookies og data|privatlivs|samtykke|privatliv|personvern|informasjonskapsler|informasjonskapslar|aller media|dine data|galetes|protecció de dades|política de privadesa|ግላዊነት|ኩኪ|ኩኪዎች|pro pokračování vyberte|technické cookies|jakou formou vám máme zobrazovat obsah|evästeet|evästeasetukset|tietosuoja|yksityisyys|hyväksy evästeet|slapukai|privatumas|slapukų nustatymai|kolačinji|приватност|поставки за колачиња|cookie-uri|confidențialitate|setări cookie|protecția datelor|sīkdatnes|sīkfailus|privātums|privātuma iestatījumi|sīkdatņu iestatījumi|sütiket|adatvédelem|adatvédelmi beállítások|süti beállítások|kabul|çerez|gizlilik|лични данни|поверителност|terima|setuju|privasi|ยินยอม|คุกกี้|ความเป็นส่วนตัว)},
|
|
72
79
|
container_pattern: %q{(cookie|consent|privacy|onetrust|cookiebot|usercentrics|trustarc|didomi|quantcast|sourcepoint|sp_message|uniconsent|osano|gdpr|ccpa)},
|
|
73
80
|
known_cmp_selectors: [
|
|
74
|
-
|
|
75
|
-
"#CybotCookiebotDialog", ".cc-window", ".cc_banner", "#cookie-banner", ".cookie-banner",
|
|
76
|
-
"#consent-banner", ".consent-banner", ".fc-consent-root", ".cmp-modal", ".gdpr-banner",
|
|
77
|
-
"#gdpr-consent", ".js-cookies", ".cookie-notice", "#cookieNotice",
|
|
81
|
+
*COMMON_CMP_SELECTORS,
|
|
78
82
|
"[id*='sourcepoint']", "[class*='sourcepoint']", "[id*='sp_message']", "[class*='sp_message']",
|
|
79
83
|
"[id*='uniconsent']", "[class*='uniconsent']", "[id*='uni-consent']", "[class*='uni-consent']",
|
|
80
84
|
"[id*='osano']", "[class*='osano']"
|
|
81
85
|
].freeze,
|
|
82
86
|
quick_indicator_selectors: [
|
|
83
|
-
|
|
84
|
-
"#CybotCookiebotDialog", ".cc-window", ".cc_banner", "#cookie-banner", ".cookie-banner",
|
|
85
|
-
"#consent-banner", ".consent-banner", ".fc-consent-root", ".cmp-modal", ".gdpr-banner",
|
|
86
|
-
"#gdpr-consent", ".js-cookies", ".cookie-notice", "#cookieNotice",
|
|
87
|
+
*COMMON_CMP_SELECTORS,
|
|
87
88
|
'[id*="onetrust" i]', '[class*="onetrust" i]', '[id*="cookiebot" i]', '[class*="cookiebot" i]',
|
|
88
89
|
'[id*="usercentrics" i]', '[class*="usercentrics" i]', '[id*="trustarc" i]', '[class*="trustarc" i]',
|
|
89
90
|
'[id*="didomi" i]', '[class*="didomi" i]', '[id*="quantcast" i]', '[class*="quantcast" i]',
|
|
@@ -98,10 +99,7 @@ module FetchUtil
|
|
|
98
99
|
'[id*="privacy-preference" i]', '[class*="privacy-preference" i]'
|
|
99
100
|
].freeze,
|
|
100
101
|
container_selectors: [
|
|
101
|
-
|
|
102
|
-
"#CybotCookiebotDialog", ".cc-window", ".cc_banner", "#cookie-banner", ".cookie-banner",
|
|
103
|
-
"#consent-banner", ".consent-banner", ".fc-consent-root", ".cmp-modal", ".gdpr-banner",
|
|
104
|
-
"#gdpr-consent", ".js-cookies", ".cookie-notice", "#cookieNotice",
|
|
102
|
+
*COMMON_CMP_SELECTORS,
|
|
105
103
|
'[id*="cookie" i]', '[class*="cookie" i]', '[id*="consent" i]', '[class*="consent" i]',
|
|
106
104
|
'[class*="cookie-banner" i]', '[data-testid*="cookie" i]', '[id*="privacy" i]', '[class*="privacy" i]',
|
|
107
105
|
'[id*="gdpr" i]', '[class*="gdpr" i]', '[id*="ccpa" i]', '[class*="ccpa" i]',
|
|
@@ -110,10 +108,7 @@ module FetchUtil
|
|
|
110
108
|
'[id*="osano" i]', '[class*="osano" i]'
|
|
111
109
|
].freeze,
|
|
112
110
|
overlay_selectors: [
|
|
113
|
-
|
|
114
|
-
"#CybotCookiebotDialog", ".cc-window", ".cc_banner", "#cookie-banner", ".cookie-banner",
|
|
115
|
-
"#consent-banner", ".consent-banner", ".fc-consent-root", ".cmp-modal", ".gdpr-banner",
|
|
116
|
-
"#gdpr-consent", ".js-cookies", ".cookie-notice", "#cookieNotice",
|
|
111
|
+
*COMMON_CMP_SELECTORS,
|
|
117
112
|
'[id*="cookie" i]', '[class*="cookie" i]', '[id*="consent" i]', '[class*="consent" i]',
|
|
118
113
|
'[class*="cookie-banner" i]', '[data-testid*="cookie" i]', '[id*="privacy" i]', '[class*="privacy" i]',
|
|
119
114
|
'[id*="gdpr" i]', '[class*="gdpr" i]', '[id*="ccpa" i]', '[class*="ccpa" i]',
|
|
@@ -127,7 +122,7 @@ module FetchUtil
|
|
|
127
122
|
button_selectors: 'button, [role="button"], a, input[type="button"], input[type="submit"]'
|
|
128
123
|
).freeze
|
|
129
124
|
# rubocop:enable Layout/LineLength, Style/RedundantPercentQ
|
|
130
|
-
private_constant :ConsentConfig, :DEFAULT_CONSENT_CONFIG
|
|
125
|
+
private_constant :ConsentConfig, :COMMON_CMP_SELECTORS, :DEFAULT_CONSENT_CONFIG
|
|
131
126
|
|
|
132
127
|
private
|
|
133
128
|
|
|
@@ -6,32 +6,59 @@ module FetchUtil
|
|
|
6
6
|
module TimingHelpers
|
|
7
7
|
private
|
|
8
8
|
|
|
9
|
-
def
|
|
10
|
-
|
|
9
|
+
def monotonic_now
|
|
10
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def stabilization_deadline
|
|
14
|
+
monotonic_now + @timeout
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def retry_until_timeout(timeout, interval: 0.2, deadline: nil)
|
|
18
|
+
now = monotonic_now
|
|
19
|
+
poll_deadline = now + timeout
|
|
20
|
+
poll_deadline = [poll_deadline, deadline].min if deadline
|
|
21
|
+
attempted = false
|
|
11
22
|
|
|
12
23
|
loop do
|
|
24
|
+
now = attempted ? monotonic_now : now
|
|
25
|
+
return false if now > poll_deadline || (attempted && now >= poll_deadline)
|
|
26
|
+
|
|
27
|
+
attempted = true
|
|
13
28
|
result = yield
|
|
14
|
-
|
|
15
|
-
return
|
|
29
|
+
completed_at = monotonic_now
|
|
30
|
+
return true if result == true && completed_at <= poll_deadline
|
|
31
|
+
|
|
32
|
+
remaining = poll_deadline - completed_at
|
|
33
|
+
return false unless remaining.positive?
|
|
16
34
|
|
|
17
|
-
|
|
35
|
+
delay = result.is_a?(Numeric) ? result : interval
|
|
36
|
+
sleep [delay, remaining].min
|
|
18
37
|
end
|
|
19
38
|
end
|
|
20
39
|
|
|
21
|
-
def capped_timeout(max_timeout)
|
|
22
|
-
[@timeout, max_timeout].min
|
|
40
|
+
def capped_timeout(max_timeout, deadline: nil)
|
|
41
|
+
timeout = [@timeout, max_timeout].min
|
|
42
|
+
timeout = [timeout, deadline - monotonic_now].min if deadline
|
|
43
|
+
[timeout, 0].max
|
|
23
44
|
end
|
|
24
45
|
|
|
25
|
-
def
|
|
26
|
-
|
|
46
|
+
def stabilization_time_remaining?(deadline)
|
|
47
|
+
monotonic_now < deadline
|
|
27
48
|
end
|
|
28
49
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
50
|
+
def sleep_before_deadline(duration, deadline:)
|
|
51
|
+
duration = [duration, deadline - monotonic_now].min
|
|
52
|
+
sleep duration if duration.positive?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def settle_after_stabilization(max_wait, deadline: stabilization_deadline)
|
|
56
|
+
sleep_before_deadline([@wait, max_wait].min, deadline: deadline) if @wait.positive?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def social_login_phase_pause(deadline: stabilization_deadline)
|
|
60
|
+
duration = @wait.positive? ? [@wait, SOCIAL_LOGIN_PHASE_WAIT].min : SOCIAL_LOGIN_PHASE_WAIT
|
|
61
|
+
sleep_before_deadline(duration, deadline: deadline)
|
|
35
62
|
end
|
|
36
63
|
end
|
|
37
64
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FetchUtil
|
|
4
|
+
class Browser
|
|
5
|
+
module SiteStabilization
|
|
6
|
+
module AzureDevopsPrCommitDetailsScript
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def azure_devops_pr_commit_details_script
|
|
10
|
+
<<~'JS'
|
|
11
|
+
const AZURE_DEVOPS_COMMIT_DETAIL_CONCURRENCY = 8;
|
|
12
|
+
const completeCommit = async (commit, repositoryId, signal) => {
|
|
13
|
+
const url = new URL(route.prefixPath + "/_apis/git/repositories/" +
|
|
14
|
+
encodeURIComponent(repositoryId) + "/commits/" + encodeURIComponent(commit.commitId), location.origin);
|
|
15
|
+
url.searchParams.set("api-version", "7.1");
|
|
16
|
+
const page = await requestJson(url, signal);
|
|
17
|
+
const detail = page.value;
|
|
18
|
+
if (typeof detail.commitId !== "string" || detail.commitId !== commit.commitId ||
|
|
19
|
+
detail.commentTruncated !== false || typeof detail.comment !== "string") {
|
|
20
|
+
throw new Error("incomplete Azure DevOps commit detail");
|
|
21
|
+
}
|
|
22
|
+
return Object.assign({}, commit, detail, { commentTruncated: false });
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const completeCommits = async (commits, repositoryId, signal) => {
|
|
26
|
+
const completed = new Array(commits.length);
|
|
27
|
+
let nextIndex = 0;
|
|
28
|
+
const completeNext = async () => {
|
|
29
|
+
while (nextIndex < commits.length) {
|
|
30
|
+
if (signal.aborted) throw new Error("Azure DevOps preparation aborted");
|
|
31
|
+
const index = nextIndex;
|
|
32
|
+
nextIndex += 1;
|
|
33
|
+
completed[index] = await completeCommit(commits[index], repositoryId, signal);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const workers = Array.from(
|
|
37
|
+
{ length: Math.min(AZURE_DEVOPS_COMMIT_DETAIL_CONCURRENCY, commits.length) },
|
|
38
|
+
completeNext
|
|
39
|
+
);
|
|
40
|
+
await Promise.all(workers);
|
|
41
|
+
return completed;
|
|
42
|
+
};
|
|
43
|
+
JS
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FetchUtil
|
|
4
|
+
class Browser
|
|
5
|
+
module SiteStabilization
|
|
6
|
+
module AzureDevopsPrProductStateScript
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def azure_devops_pr_product_state_script
|
|
10
|
+
<<~'JS'
|
|
11
|
+
const stateKey = "__fetchUtilAzureDevopsPullRequest";
|
|
12
|
+
const pathname = (location.pathname || "").replace(/\/+$/, "");
|
|
13
|
+
const lowerPath = pathname.toLowerCase();
|
|
14
|
+
const marker = lowerPath.lastIndexOf("/_git/");
|
|
15
|
+
if (marker < 1) return { matched: false };
|
|
16
|
+
|
|
17
|
+
const prefixPath = pathname.slice(0, marker);
|
|
18
|
+
const tail = pathname.slice(marker + 6).split("/").filter(Boolean);
|
|
19
|
+
if (tail.length !== 3 || tail[1].toLowerCase() !== "pullrequest" || !/^\d+$/.test(tail[2])) {
|
|
20
|
+
return { matched: false };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const decodeSegment = (value) => {
|
|
24
|
+
try { return decodeURIComponent(value); } catch (_error) { return ""; }
|
|
25
|
+
};
|
|
26
|
+
const repository = decodeSegment(tail[0]);
|
|
27
|
+
const prefixParts = prefixPath.split("/").filter(Boolean).map(decodeSegment);
|
|
28
|
+
if (!repository || repository === "." || repository === ".." || !prefixParts.length ||
|
|
29
|
+
prefixParts.some((part) => !part || part === "." || part === "..")) return { matched: false };
|
|
30
|
+
|
|
31
|
+
const number = tail[2];
|
|
32
|
+
const project = prefixParts[prefixParts.length - 1];
|
|
33
|
+
const basePath = prefixPath + "/_git/" + tail[0] + "/pullrequest/" + number;
|
|
34
|
+
const bodyMatch = document.body && document.body.classList.contains("ms-vss-web-vsts-theme");
|
|
35
|
+
const headingMatch = Array.from(document.querySelectorAll('[role="heading"], h1')).some(
|
|
36
|
+
(node) => new RegExp("\\bPull Request\\s+" + number + "\\b", "i").test((node.textContent || "").trim())
|
|
37
|
+
);
|
|
38
|
+
const tabNames = ["overview", "files", "updates", "commits"];
|
|
39
|
+
const tabsMatch = tabNames.every((name) => {
|
|
40
|
+
const tab = document.querySelector("#__bolt-tab-" + name);
|
|
41
|
+
if (!tab) return false;
|
|
42
|
+
try {
|
|
43
|
+
const url = new URL(tab.getAttribute("href"), location.href);
|
|
44
|
+
return url.origin === location.origin && url.pathname === basePath && url.searchParams.get("_a") === name;
|
|
45
|
+
} catch (_error) { return false; }
|
|
46
|
+
});
|
|
47
|
+
const moduleMatch = Array.from(document.scripts).some((script) =>
|
|
48
|
+
/Repos\/Views\/PullRequest|Repos\/Discussion/.test(script.textContent || "")
|
|
49
|
+
);
|
|
50
|
+
const product = !!(bodyMatch && headingMatch && tabsMatch && moduleMatch);
|
|
51
|
+
if (!product) return { matched: true, product: false };
|
|
52
|
+
|
|
53
|
+
const apiPath = prefixPath + "/_apis/git/repositories/" + encodeURIComponent(repository) +
|
|
54
|
+
"/pullRequests/" + number;
|
|
55
|
+
const routeKey = [location.origin, basePath].join(":");
|
|
56
|
+
const existing = window[stateKey];
|
|
57
|
+
if (existing && existing.routeKey === routeKey) {
|
|
58
|
+
return {
|
|
59
|
+
matched: true,
|
|
60
|
+
product: true,
|
|
61
|
+
status: existing.status || "",
|
|
62
|
+
signature: [existing.status, existing.threadCount || 0, existing.commentCount || 0,
|
|
63
|
+
existing.commitCount || 0].join(":")
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const route = {
|
|
68
|
+
prefixPath: prefixPath,
|
|
69
|
+
project: project,
|
|
70
|
+
repository: repository,
|
|
71
|
+
number: number,
|
|
72
|
+
basePath: basePath,
|
|
73
|
+
apiPath: apiPath
|
|
74
|
+
};
|
|
75
|
+
JS
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FetchUtil
|
|
4
|
+
class Browser
|
|
5
|
+
module SiteStabilization
|
|
6
|
+
module AzureDevopsPrRequestHelpersScript
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def azure_devops_pr_request_helpers_script
|
|
10
|
+
<<~'JS'
|
|
11
|
+
const requestJson = async (url, signal) => {
|
|
12
|
+
if (url.origin !== location.origin) throw new Error("cross-origin Azure DevOps API route");
|
|
13
|
+
const response = await fetch(url.href, {
|
|
14
|
+
credentials: "same-origin",
|
|
15
|
+
redirect: "error",
|
|
16
|
+
signal: signal,
|
|
17
|
+
headers: { Accept: "application/json", "X-TFS-FedAuthRedirect": "Suppress" }
|
|
18
|
+
});
|
|
19
|
+
if (response.url && new URL(response.url, location.href).origin !== location.origin) {
|
|
20
|
+
throw new Error("cross-origin Azure DevOps API response");
|
|
21
|
+
}
|
|
22
|
+
if (!response.ok) throw new Error("Azure DevOps API returned " + response.status);
|
|
23
|
+
const value = await response.json();
|
|
24
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
25
|
+
throw new Error("invalid Azure DevOps API payload");
|
|
26
|
+
}
|
|
27
|
+
return { response: response, value: value };
|
|
28
|
+
};
|
|
29
|
+
JS
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def azure_devops_pr_request_lifecycle_script
|
|
33
|
+
<<~'JS'
|
|
34
|
+
const abortControllerKey = "__fetchUtilAzureDevopsPullRequestAbortController";
|
|
35
|
+
const abortController = new AbortController();
|
|
36
|
+
const signal = abortController.signal;
|
|
37
|
+
window[abortControllerKey] = abortController;
|
|
38
|
+
window[stateKey] = { status: "loading", product: true, routeKey: routeKey, route: route };
|
|
39
|
+
|
|
40
|
+
const preparationActive = () => {
|
|
41
|
+
const current = window[stateKey];
|
|
42
|
+
return current && current.status === "loading" && current.routeKey === routeKey &&
|
|
43
|
+
window[abortControllerKey] === abortController;
|
|
44
|
+
};
|
|
45
|
+
const clearAbortController = () => {
|
|
46
|
+
if (window[abortControllerKey] === abortController) delete window[abortControllerKey];
|
|
47
|
+
};
|
|
48
|
+
JS
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FetchUtil
|
|
4
|
+
class Browser
|
|
5
|
+
module SiteStabilization
|
|
6
|
+
module AzureDevopsPrRequestStateScript
|
|
7
|
+
include AzureDevopsPrCommitDetailsScript
|
|
8
|
+
include AzureDevopsPrRequestHelpersScript
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def azure_devops_pr_request_state_script
|
|
13
|
+
<<~JS
|
|
14
|
+
#{azure_devops_pr_request_lifecycle_script}
|
|
15
|
+
|
|
16
|
+
#{azure_devops_pr_request_helpers_script}
|
|
17
|
+
#{azure_devops_pr_commit_details_script}
|
|
18
|
+
|
|
19
|
+
const metadataUrl = new URL(route.apiPath, location.origin);
|
|
20
|
+
metadataUrl.searchParams.set("api-version", "7.1");
|
|
21
|
+
requestJson(metadataUrl, signal).then(async ({ value: metadata }) => {
|
|
22
|
+
const repositoryData = metadata.repository || {};
|
|
23
|
+
const projectData = repositoryData.project || {};
|
|
24
|
+
const repositoryId = typeof repositoryData.id === "string" ? repositoryData.id.trim() : "";
|
|
25
|
+
if (String(metadata.pullRequestId || "") !== route.number ||
|
|
26
|
+
String(repositoryData.name || "").toLowerCase() !== route.repository.toLowerCase() ||
|
|
27
|
+
String(projectData.name || "").toLowerCase() !== route.project.toLowerCase() ||
|
|
28
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(repositoryId) ||
|
|
29
|
+
!/^public$/i.test(String(projectData.visibility || ""))) {
|
|
30
|
+
throw new Error("Azure DevOps API pull request identity mismatch");
|
|
31
|
+
}
|
|
32
|
+
if (repositoryData.webUrl && new URL(repositoryData.webUrl, location.href).origin !== location.origin) {
|
|
33
|
+
throw new Error("Azure DevOps API repository origin mismatch");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const repositoryApiPath = route.prefixPath + "/_apis/git/repositories/" +
|
|
37
|
+
encodeURIComponent(repositoryId) + "/pullRequests/" + route.number;
|
|
38
|
+
route.repositoryApiPath = repositoryApiPath;
|
|
39
|
+
const threadsUrl = new URL(repositoryApiPath + "/threads", location.origin);
|
|
40
|
+
threadsUrl.searchParams.set("api-version", "7.1");
|
|
41
|
+
const threadsPage = await requestJson(threadsUrl, signal);
|
|
42
|
+
const threads = threadsPage.value;
|
|
43
|
+
if (!Array.isArray(threads.value)) throw new Error("invalid Azure DevOps threads payload");
|
|
44
|
+
if (!Number.isInteger(threads.count) || threads.count !== threads.value.length) {
|
|
45
|
+
throw new Error("incomplete Azure DevOps threads payload");
|
|
46
|
+
}
|
|
47
|
+
const threadsContinuation = threadsPage.response.headers &&
|
|
48
|
+
threadsPage.response.headers.get("x-ms-continuationtoken");
|
|
49
|
+
if (threadsContinuation) throw new Error("unexpected Azure DevOps threads continuation");
|
|
50
|
+
|
|
51
|
+
const commits = [];
|
|
52
|
+
const seenTokens = new Set();
|
|
53
|
+
let continuation = "";
|
|
54
|
+
do {
|
|
55
|
+
const commitsUrl = new URL(repositoryApiPath + "/commits", location.origin);
|
|
56
|
+
commitsUrl.searchParams.set("api-version", "7.1");
|
|
57
|
+
commitsUrl.searchParams.set("$top", "1000");
|
|
58
|
+
if (continuation) commitsUrl.searchParams.set("continuationToken", continuation);
|
|
59
|
+
const page = await requestJson(commitsUrl, signal);
|
|
60
|
+
if (!Array.isArray(page.value.value)) throw new Error("invalid Azure DevOps commits payload");
|
|
61
|
+
if (!Number.isInteger(page.value.count) || page.value.count !== page.value.value.length) {
|
|
62
|
+
throw new Error("incomplete Azure DevOps commits payload");
|
|
63
|
+
}
|
|
64
|
+
page.value.value.forEach((commit) => {
|
|
65
|
+
if (!commit || typeof commit.commitId !== "string" || !/^[0-9a-f]{40}$/i.test(commit.commitId)) {
|
|
66
|
+
throw new Error("invalid Azure DevOps commit record");
|
|
67
|
+
}
|
|
68
|
+
commits.push(commit);
|
|
69
|
+
});
|
|
70
|
+
const nextValue = page.response.headers &&
|
|
71
|
+
page.response.headers.get("x-ms-continuationtoken");
|
|
72
|
+
if (nextValue != null && typeof nextValue !== "string") {
|
|
73
|
+
throw new Error("invalid Azure DevOps continuation token");
|
|
74
|
+
}
|
|
75
|
+
const next = (nextValue || "").trim();
|
|
76
|
+
if (next && seenTokens.has(next)) throw new Error("repeated Azure DevOps continuation token");
|
|
77
|
+
if (next) seenTokens.add(next);
|
|
78
|
+
continuation = next;
|
|
79
|
+
} while (continuation);
|
|
80
|
+
|
|
81
|
+
const completedCommits = await completeCommits(commits, repositoryId, signal);
|
|
82
|
+
|
|
83
|
+
const comments = threads.value.reduce((total, thread) =>
|
|
84
|
+
total + (Array.isArray(thread && thread.comments) ? thread.comments.length : 0), 0);
|
|
85
|
+
if (!preparationActive()) return;
|
|
86
|
+
clearAbortController();
|
|
87
|
+
window[stateKey] = {
|
|
88
|
+
status: "ready",
|
|
89
|
+
product: true,
|
|
90
|
+
routeKey: routeKey,
|
|
91
|
+
route: route,
|
|
92
|
+
metadata: metadata,
|
|
93
|
+
threads: threads,
|
|
94
|
+
commits: completedCommits,
|
|
95
|
+
threadCount: threads.value.length,
|
|
96
|
+
commentCount: comments,
|
|
97
|
+
commitCount: completedCommits.length
|
|
98
|
+
};
|
|
99
|
+
}).catch((error) => {
|
|
100
|
+
if (!preparationActive()) return;
|
|
101
|
+
abortController.abort();
|
|
102
|
+
clearAbortController();
|
|
103
|
+
window[stateKey] = {
|
|
104
|
+
status: "failed",
|
|
105
|
+
product: true,
|
|
106
|
+
routeKey: routeKey,
|
|
107
|
+
route: route,
|
|
108
|
+
reason: String(error && error.message || error || "Azure DevOps preparation failed")
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return { matched: true, product: true, status: "loading", signature: "loading" };
|
|
113
|
+
JS
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FetchUtil
|
|
4
|
+
class Browser
|
|
5
|
+
module SiteStabilization
|
|
6
|
+
module AzureDevopsPrStabilization
|
|
7
|
+
AZURE_DEVOPS_PR_STABILIZATION_PROFILE = {
|
|
8
|
+
host: true,
|
|
9
|
+
path_query: ->(uri) { uri.path.match?(%r{/_git/[^/]+/pullrequest/\d+/?\z}i) },
|
|
10
|
+
strategy: :stabilize_azure_devops_pr,
|
|
11
|
+
fallthrough: true,
|
|
12
|
+
notes: "Prepare product-matched public Azure DevOps pull request REST resources.",
|
|
13
|
+
tests: "spec/fetch_util/browser_stabilization_spec.rb"
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def stabilize_azure_devops_pr(page, deadline: stabilization_deadline)
|
|
19
|
+
state = nil
|
|
20
|
+
ready = retry_until_timeout(
|
|
21
|
+
capped_timeout(12.0, deadline: deadline),
|
|
22
|
+
interval: 0.1,
|
|
23
|
+
deadline: deadline
|
|
24
|
+
) do
|
|
25
|
+
state = azure_devops_pr_state(page)
|
|
26
|
+
return false if state.is_a?(Hash) && (state["product"] == false || state["status"] == "failed")
|
|
27
|
+
|
|
28
|
+
state.is_a?(Hash) && state["status"] == "ready"
|
|
29
|
+
end
|
|
30
|
+
settle_after_stabilization(0.25, deadline: deadline) if ready
|
|
31
|
+
fail_azure_devops_pr_preparation(page) if !ready && state.is_a?(Hash) && state["status"] == "loading"
|
|
32
|
+
ready
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def fail_azure_devops_pr_preparation(page)
|
|
36
|
+
safe_evaluate(page, <<~JS, default: nil)
|
|
37
|
+
(() => {
|
|
38
|
+
const state = window.__fetchUtilAzureDevopsPullRequest;
|
|
39
|
+
if (!state || state.status !== "loading") return state;
|
|
40
|
+
const controller = window.__fetchUtilAzureDevopsPullRequestAbortController;
|
|
41
|
+
if (controller && typeof controller.abort === "function") controller.abort();
|
|
42
|
+
delete window.__fetchUtilAzureDevopsPullRequestAbortController;
|
|
43
|
+
window.__fetchUtilAzureDevopsPullRequest = Object.assign({}, state, {
|
|
44
|
+
status: "failed",
|
|
45
|
+
reason: "Azure DevOps REST preparation timed out"
|
|
46
|
+
});
|
|
47
|
+
return window.__fetchUtilAzureDevopsPullRequest;
|
|
48
|
+
})()
|
|
49
|
+
JS
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FetchUtil
|
|
4
|
+
class Browser
|
|
5
|
+
module SiteStabilization
|
|
6
|
+
module AzureDevopsPrState
|
|
7
|
+
include AzureDevopsPrProductStateScript
|
|
8
|
+
include AzureDevopsPrRequestStateScript
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def azure_devops_pr_state(page)
|
|
13
|
+
safe_evaluate(page, azure_devops_pr_state_script, default: nil)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def azure_devops_pr_state_script
|
|
17
|
+
<<~JS
|
|
18
|
+
(() => {
|
|
19
|
+
#{azure_devops_pr_product_state_script}
|
|
20
|
+
#{azure_devops_pr_request_state_script}
|
|
21
|
+
})()
|
|
22
|
+
JS
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FetchUtil
|
|
4
|
+
class Browser
|
|
5
|
+
module SiteStabilization
|
|
6
|
+
module BitbucketCloudPullDiffProductState
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def bitbucket_cloud_pull_diff_product_state_script
|
|
10
|
+
<<~'JS'
|
|
11
|
+
const fetchUtilBitbucketDiffProduct = () => {
|
|
12
|
+
const match = location.pathname.match(/^\/([^/]+)\/([^/]+)\/pull-requests\/(\d+)\/diff\/?$/);
|
|
13
|
+
if (!match) return { product: false };
|
|
14
|
+
const decode = (value) => { try { return decodeURIComponent(value); } catch (_error) { return ''; } };
|
|
15
|
+
const route = { workspace: decode(match[1]), repository: decode(match[2]), number: match[3] };
|
|
16
|
+
if (!route.workspace || !route.repository || route.workspace.includes('/') || route.repository.includes('/')) {
|
|
17
|
+
return { product: false };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const current = (((window.__initial_state__ || {}).section || {}).repository || {}).currentRepository || {};
|
|
21
|
+
const publicRepository = current.is_private === false || current.isPrivate === false ||
|
|
22
|
+
current.visibility === 'public';
|
|
23
|
+
const runtime = current.type === 'repository' && publicRepository &&
|
|
24
|
+
String(current.full_name || '').toLowerCase() ===
|
|
25
|
+
`${route.workspace}/${route.repository}`.toLowerCase();
|
|
26
|
+
const application = document.querySelector('meta[name="application-name"]');
|
|
27
|
+
const branded = /^Bitbucket$/i.test((application && application.content || '').trim());
|
|
28
|
+
const asset = Array.from(document.querySelectorAll('script[src], link[href]')).some((node) => {
|
|
29
|
+
const value = node.getAttribute('src') || node.getAttribute('href') || '';
|
|
30
|
+
return /(?:frontbucket|bbc-frontbucket-static|bitbucket)[^/]*\/(?:assets|static)\//i.test(value) ||
|
|
31
|
+
/\/(?:assets|static)\/[^?#]*(?:frontbucket|bitbucket)/i.test(value);
|
|
32
|
+
});
|
|
33
|
+
const header = document.querySelector('[data-testid="pr-header"]');
|
|
34
|
+
if (!(runtime && branded && (header || asset))) return { product: false };
|
|
35
|
+
|
|
36
|
+
const apiMeta = document.querySelector('meta[name="bb-api-canon-url"]');
|
|
37
|
+
let apiOrigin;
|
|
38
|
+
try { apiOrigin = new URL(apiMeta && apiMeta.content || '', location.href); } catch (_error) {
|
|
39
|
+
return { product: true, route, error: 'missing Bitbucket API metadata route' };
|
|
40
|
+
}
|
|
41
|
+
if (!/^https?:$/.test(apiOrigin.protocol) || apiOrigin.username || apiOrigin.password) {
|
|
42
|
+
return { product: true, route, error: 'invalid Bitbucket API metadata route' };
|
|
43
|
+
}
|
|
44
|
+
const proxyPrefix = '/!api/2.0';
|
|
45
|
+
const metadataUrl = location.origin + proxyPrefix + '/repositories/' +
|
|
46
|
+
encodeURIComponent(route.workspace) + '/' + encodeURIComponent(route.repository) +
|
|
47
|
+
'/pullrequests/' + route.number;
|
|
48
|
+
return { product: true, route, metadataUrl, proxyPrefix };
|
|
49
|
+
};
|
|
50
|
+
JS
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|