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.
@@ -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
@@ -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
@@ -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
- page.add_script_tag(path: asset_path("vendor/readability.js"))
40
- page.add_script_tag(path: asset_path("vendor/turndown.js"))
41
- page.add_script_tag(path: asset_path("extract.js"))
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 inject_assets_inline(page)
45
- inline_asset_scripts.each do |script|
46
- page.evaluate("#{script}\ntrue")
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
- inject_assets(page)
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
- inject_assets_inline(page)
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 ||= "window.FetchUtilExtract.extract(#{JSON.generate(reader_mode: @reader_mode)})"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FetchUtil
4
- VERSION = "0.6.2"
4
+ VERSION = "0.7.0"
5
5
  end
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.6.2
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