fetch_util 0.6.2 → 0.7.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 +160 -0
- data/README.md +2 -1
- data/lib/fetch_util/assets/extract.js +1 -1
- data/lib/fetch_util/browser/navigation/navigator_patch.rb +2 -0
- data/lib/fetch_util/browser/navigation/shadow_root_patch.rb +85 -0
- data/lib/fetch_util/browser/navigation.rb +2 -0
- data/lib/fetch_util/browser.rb +3 -0
- data/lib/fetch_util/extractor.rb +115 -9
- data/lib/fetch_util/version.rb +1 -1
- metadata +2 -1
|
@@ -17,6 +17,8 @@ module FetchUtil
|
|
|
17
17
|
Object.defineProperty(navigator, "languages", { get: () => #{languages_json} });
|
|
18
18
|
Object.defineProperty(navigator, "platform", { get: () => "Linux x86_64" });
|
|
19
19
|
|
|
20
|
+
#{shadow_root_patch}
|
|
21
|
+
|
|
20
22
|
Object.defineProperty(navigator, "plugins", {
|
|
21
23
|
get: () => {
|
|
22
24
|
const p = { 0: { name: "PDF Viewer", filename: "internal-pdf-viewer", description: "Portable Document Format" },
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FetchUtil
|
|
4
|
+
class Browser
|
|
5
|
+
module Navigation
|
|
6
|
+
module ShadowRootPatch
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def shadow_root_patch
|
|
10
|
+
token = JSON.generate(Browser::SHADOW_ROOT_ACCESS_TOKEN)
|
|
11
|
+
property = JSON.generate(Browser::SHADOW_ROOT_READER_PROPERTY)
|
|
12
|
+
<<~JS
|
|
13
|
+
{
|
|
14
|
+
const shadowRootReaderProperty = #{property};
|
|
15
|
+
const shadowRootInstallProperty = shadowRootReaderProperty + ":installed";
|
|
16
|
+
const shadowRootToken = #{token};
|
|
17
|
+
const weakMapGet = WeakMap.prototype.get;
|
|
18
|
+
const weakMapSet = WeakMap.prototype.set;
|
|
19
|
+
const reflectApply = Reflect.apply;
|
|
20
|
+
let readShadowRoot = null;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
readShadowRoot = window.top[shadowRootReaderProperty];
|
|
24
|
+
} catch (_error) {
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (typeof readShadowRoot !== "function") {
|
|
28
|
+
const shadowRoots = new WeakMap();
|
|
29
|
+
readShadowRoot = function(candidateToken, operation, host, root) {
|
|
30
|
+
if (candidateToken !== shadowRootToken) return null;
|
|
31
|
+
try {
|
|
32
|
+
if (operation === "set") {
|
|
33
|
+
reflectApply(weakMapSet, shadowRoots, [host, root]);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
return operation === "get" ? reflectApply(weakMapGet, shadowRoots, [host]) || null : null;
|
|
37
|
+
} catch (_error) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
if (!Object.prototype.hasOwnProperty.call(window, shadowRootReaderProperty)) {
|
|
45
|
+
Object.defineProperty(window, shadowRootReaderProperty, {
|
|
46
|
+
value: readShadowRoot,
|
|
47
|
+
configurable: false,
|
|
48
|
+
enumerable: false,
|
|
49
|
+
writable: false
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
} catch (_error) {
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const installed = window[shadowRootInstallProperty] === true;
|
|
56
|
+
const descriptor = Object.getOwnPropertyDescriptor(Element.prototype, "attachShadow");
|
|
57
|
+
const attachShadow = descriptor && descriptor.value;
|
|
58
|
+
if (attachShadow && !installed) {
|
|
59
|
+
const trackedAttachShadow = new Proxy(attachShadow, {
|
|
60
|
+
apply: function(target, thisArg, args) {
|
|
61
|
+
const root = reflectApply(target, thisArg, args);
|
|
62
|
+
try {
|
|
63
|
+
readShadowRoot(shadowRootToken, "set", thisArg, root);
|
|
64
|
+
} catch (_error) {
|
|
65
|
+
}
|
|
66
|
+
return root;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
Object.defineProperty(Element.prototype, "attachShadow", Object.assign({}, descriptor, {
|
|
70
|
+
value: trackedAttachShadow
|
|
71
|
+
}));
|
|
72
|
+
Object.defineProperty(window, shadowRootInstallProperty, {
|
|
73
|
+
value: true,
|
|
74
|
+
configurable: false,
|
|
75
|
+
enumerable: false,
|
|
76
|
+
writable: false
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
JS
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -5,8 +5,10 @@ module FetchUtil
|
|
|
5
5
|
module Navigation
|
|
6
6
|
autoload :HeadersAndReadiness, "fetch_util/browser/navigation/headers_and_readiness"
|
|
7
7
|
autoload :NavigatorPatch, "fetch_util/browser/navigation/navigator_patch"
|
|
8
|
+
autoload :ShadowRootPatch, "fetch_util/browser/navigation/shadow_root_patch"
|
|
8
9
|
|
|
9
10
|
include HeadersAndReadiness
|
|
11
|
+
include ShadowRootPatch
|
|
10
12
|
include NavigatorPatch
|
|
11
13
|
end
|
|
12
14
|
end
|
data/lib/fetch_util/browser.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "ferrum"
|
|
4
4
|
require "json"
|
|
5
|
+
require "securerandom"
|
|
5
6
|
require "uri"
|
|
6
7
|
|
|
7
8
|
module FetchUtil
|
|
@@ -31,6 +32,8 @@ module FetchUtil
|
|
|
31
32
|
DEFAULT_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " \
|
|
32
33
|
"(KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
|
|
33
34
|
DEFAULT_ACCEPT_LANGUAGE = "en-US,en;q=0.9"
|
|
35
|
+
SHADOW_ROOT_ACCESS_TOKEN = SecureRandom.hex(32).freeze
|
|
36
|
+
SHADOW_ROOT_READER_PROPERTY = "__fetchUtilClosedShadowRootReaderV1_#{SecureRandom.hex(16)}".freeze
|
|
34
37
|
SOCIAL_LOGIN_PHASE_WAIT = 0.3
|
|
35
38
|
POST_CONSENT_IDLE_TIMEOUT = 3.0
|
|
36
39
|
CONTENT_READY_MIN_LENGTH = 200
|
data/lib/fetch_util/extractor.rb
CHANGED
|
@@ -5,6 +5,9 @@ require "json"
|
|
|
5
5
|
module FetchUtil
|
|
6
6
|
class Extractor
|
|
7
7
|
INLINE_ASSET_PATHS = %w[vendor/readability.js vendor/turndown.js extract.js].freeze
|
|
8
|
+
VENDOR_ASSET_PATHS = INLINE_ASSET_PATHS.first(2).freeze
|
|
9
|
+
VENDOR_GLOBAL_NAMES = %w[Readability TurndownService].freeze
|
|
10
|
+
PRIVATE_DELIVERY_SENTINEL = '"fetch-util:standalone-api:v1"'
|
|
8
11
|
@asset_cache = {}
|
|
9
12
|
@cache_mutex = Mutex.new
|
|
10
13
|
|
|
@@ -22,6 +25,7 @@ module FetchUtil
|
|
|
22
25
|
@reader_mode = reader_mode
|
|
23
26
|
@asset_root = (asset_root || File.join(__dir__, "assets")).dup.freeze
|
|
24
27
|
@extraction_call = nil
|
|
28
|
+
@private_asset_script = nil
|
|
25
29
|
end
|
|
26
30
|
|
|
27
31
|
def extract(page)
|
|
@@ -36,14 +40,26 @@ module FetchUtil
|
|
|
36
40
|
private
|
|
37
41
|
|
|
38
42
|
def inject_assets(page)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
INLINE_ASSET_PATHS.each do |relative_path|
|
|
44
|
+
page.add_script_tag(path: asset_path(relative_path))
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def inject_vendor_assets(page)
|
|
49
|
+
VENDOR_ASSET_PATHS.each do |relative_path|
|
|
50
|
+
page.add_script_tag(path: asset_path(relative_path))
|
|
51
|
+
end
|
|
42
52
|
end
|
|
43
53
|
|
|
44
|
-
def
|
|
45
|
-
inline_asset_scripts.each do |script|
|
|
46
|
-
page.evaluate(
|
|
54
|
+
def inject_vendor_assets_inline(page)
|
|
55
|
+
inline_asset_scripts.first(2).zip(VENDOR_GLOBAL_NAMES).each do |script, global_name|
|
|
56
|
+
page.evaluate(<<~JAVASCRIPT)
|
|
57
|
+
(() => {
|
|
58
|
+
#{script}
|
|
59
|
+
window.#{global_name} = #{global_name};
|
|
60
|
+
return true;
|
|
61
|
+
})()
|
|
62
|
+
JAVASCRIPT
|
|
47
63
|
end
|
|
48
64
|
end
|
|
49
65
|
|
|
@@ -52,14 +68,15 @@ module FetchUtil
|
|
|
52
68
|
original_timeout = page.timeout if timeout_supported
|
|
53
69
|
page.timeout = [original_timeout.to_f, 60].max if timeout_supported
|
|
54
70
|
|
|
55
|
-
|
|
71
|
+
register_cdp_closed_shadow_roots(page)
|
|
72
|
+
inject_vendor_assets(page)
|
|
56
73
|
page.evaluate(extraction_call)
|
|
57
74
|
rescue Ferrum::TimeoutError
|
|
58
75
|
begin
|
|
59
76
|
page.evaluate("window.stop && window.stop()")
|
|
60
77
|
rescue Ferrum::Error
|
|
61
78
|
end
|
|
62
|
-
|
|
79
|
+
inject_vendor_assets_inline(page)
|
|
63
80
|
page.evaluate(extraction_call)
|
|
64
81
|
ensure
|
|
65
82
|
restore_page_timeout(page, original_timeout) if timeout_supported
|
|
@@ -71,8 +88,97 @@ module FetchUtil
|
|
|
71
88
|
nil
|
|
72
89
|
end
|
|
73
90
|
|
|
91
|
+
def register_cdp_closed_shadow_roots(page)
|
|
92
|
+
return unless page.respond_to?(:command)
|
|
93
|
+
|
|
94
|
+
document = page.command("DOM.getDocument", depth: -1, pierce: true).fetch("root")
|
|
95
|
+
shadow_root_pairs(document).each do |host, root|
|
|
96
|
+
register_cdp_closed_shadow_root(page, host, root)
|
|
97
|
+
end
|
|
98
|
+
rescue Ferrum::Error
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def shadow_root_pairs(document)
|
|
103
|
+
pairs = []
|
|
104
|
+
pending = [document]
|
|
105
|
+
until pending.empty?
|
|
106
|
+
node = pending.pop
|
|
107
|
+
shadows = Array(node["shadowRoots"])
|
|
108
|
+
shadows.each { |root| pairs << [node, root] if root["shadowRootType"] == "closed" }
|
|
109
|
+
pending.concat(Array(node["children"]), shadows)
|
|
110
|
+
end
|
|
111
|
+
pairs
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def register_cdp_closed_shadow_root(page, host, root)
|
|
115
|
+
object_ids = []
|
|
116
|
+
[host, root].each do |node|
|
|
117
|
+
object_ids << page.command(
|
|
118
|
+
"DOM.resolveNode",
|
|
119
|
+
backendNodeId: node.fetch("backendNodeId")
|
|
120
|
+
).dig("object", "objectId")
|
|
121
|
+
end
|
|
122
|
+
return unless object_ids.all?
|
|
123
|
+
|
|
124
|
+
page.command(
|
|
125
|
+
"Runtime.callFunctionOn",
|
|
126
|
+
objectId: object_ids.first,
|
|
127
|
+
functionDeclaration: closed_shadow_registration_function,
|
|
128
|
+
arguments: [
|
|
129
|
+
{ objectId: object_ids.last },
|
|
130
|
+
{ value: Browser::SHADOW_ROOT_READER_PROPERTY },
|
|
131
|
+
{ value: Browser::SHADOW_ROOT_ACCESS_TOKEN }
|
|
132
|
+
],
|
|
133
|
+
returnByValue: true
|
|
134
|
+
)
|
|
135
|
+
rescue Ferrum::Error
|
|
136
|
+
nil
|
|
137
|
+
ensure
|
|
138
|
+
Array(object_ids).compact.each do |object_id|
|
|
139
|
+
page.command("Runtime.releaseObject", objectId: object_id)
|
|
140
|
+
rescue Ferrum::Error
|
|
141
|
+
nil
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def closed_shadow_registration_function
|
|
146
|
+
<<~JAVASCRIPT
|
|
147
|
+
function(root, property, token) {
|
|
148
|
+
const reader = window[property];
|
|
149
|
+
return typeof reader === "function" && reader(token, "set", this, root) === true;
|
|
150
|
+
}
|
|
151
|
+
JAVASCRIPT
|
|
152
|
+
end
|
|
153
|
+
|
|
74
154
|
def extraction_call
|
|
75
|
-
@extraction_call ||=
|
|
155
|
+
@extraction_call ||= begin
|
|
156
|
+
options = JSON.generate(
|
|
157
|
+
reader_mode: @reader_mode,
|
|
158
|
+
shadow_root_token: Browser::SHADOW_ROOT_ACCESS_TOKEN,
|
|
159
|
+
shadow_root_reader_property: Browser::SHADOW_ROOT_READER_PROPERTY
|
|
160
|
+
)
|
|
161
|
+
<<~JAVASCRIPT
|
|
162
|
+
(() => {
|
|
163
|
+
let __fetchUtilPrivateApi = null;
|
|
164
|
+
const __fetchUtilDeliverExtractApi = api => { __fetchUtilPrivateApi = api; };
|
|
165
|
+
#{private_asset_script}
|
|
166
|
+
if (!__fetchUtilPrivateApi || typeof __fetchUtilPrivateApi.extract !== "function") return null;
|
|
167
|
+
return __fetchUtilPrivateApi.extract(#{options});
|
|
168
|
+
})()
|
|
169
|
+
JAVASCRIPT
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def private_asset_script
|
|
174
|
+
@private_asset_script ||= begin
|
|
175
|
+
source = inline_asset_scripts.last
|
|
176
|
+
marker_index = source.index(PRIVATE_DELIVERY_SENTINEL)
|
|
177
|
+
duplicate_index = source.index(PRIVATE_DELIVERY_SENTINEL, marker_index.to_i + PRIVATE_DELIVERY_SENTINEL.length)
|
|
178
|
+
raise ExtractionError, "Private extraction delivery sentinel is missing or duplicated" if marker_index.nil? || duplicate_index
|
|
179
|
+
|
|
180
|
+
source.sub(PRIVATE_DELIVERY_SENTINEL, "__fetchUtilDeliverExtractApi").freeze
|
|
181
|
+
end
|
|
76
182
|
end
|
|
77
183
|
|
|
78
184
|
def inline_asset_scripts
|
data/lib/fetch_util/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fetch_util
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- hmdne
|
|
@@ -100,6 +100,7 @@ files:
|
|
|
100
100
|
- lib/fetch_util/browser/navigation.rb
|
|
101
101
|
- lib/fetch_util/browser/navigation/headers_and_readiness.rb
|
|
102
102
|
- lib/fetch_util/browser/navigation/navigator_patch.rb
|
|
103
|
+
- lib/fetch_util/browser/navigation/shadow_root_patch.rb
|
|
103
104
|
- lib/fetch_util/browser/site_stabilization.rb
|
|
104
105
|
- lib/fetch_util/browser/site_stabilization/azure_devops_pr_commit_details_script.rb
|
|
105
106
|
- lib/fetch_util/browser/site_stabilization/azure_devops_pr_product_state_script.rb
|