ask-web-fetch 0.5.6 → 0.5.8
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d3e0494d6f9fd54b7b80b667b97d57f17d1b0e871d44c26fada9ad91669a391d
|
|
4
|
+
data.tar.gz: e06918a4ca162ee2111715530f67ab68c8feae529c27c180cebb0cf462f13867
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a045bb9226bb302cd60bda942acfa05a00e1f3d1aced99edc3bbd77a061c7650580b40447c385a4777b7156fa81aa0fcc57267caa4385b478e4d005c015f41f
|
|
7
|
+
data.tar.gz: ffab19b64968c909c67dd631902b447738248c7e6a0d3da5db8851cc2ff615ec627bc8a89407b40dbeabbebb591d2cd6726c21747de34962cc9ff77c98ce16e8
|
|
@@ -59,6 +59,18 @@ module Ask
|
|
|
59
59
|
# Wikipedia embeds an hcaptcha edit-config flag on every page).
|
|
60
60
|
CHALLENGE_RE = /just a moment|checking your browser|cf-chl/i
|
|
61
61
|
|
|
62
|
+
# Registrar parking-page markers: the page is an ad for a parked
|
|
63
|
+
# (for-sale) domain, not the site's content. A content company must
|
|
64
|
+
# never store these as if they were the site. Observed live on the
|
|
65
|
+
# CC list, three shapes: (a) GoDaddy's parking-lander JS app
|
|
66
|
+
# (ap:"parking" flag, parking-lander asset, LANDER_SYSTEM="PW")
|
|
67
|
+
# served at /lander, (b) Namecheap's parking app (utm_campaign=
|
|
68
|
+
# nc_market + parkingpage links), and (c) static registrar pages
|
|
69
|
+
# ("is parked free, courtesy of GoDaddy.com", "is registered at
|
|
70
|
+
# Namecheap"). Deliberately specific — generic terms like "domain"
|
|
71
|
+
# or "for sale" appear on real pages.
|
|
72
|
+
PARKED_DOMAIN_MARKERS = /ap:"parking"|parking-lander|LANDER_SYSTEM="PW"|utm_campaign=nc_market|utm_source=parkingpage|is parked free, courtesy of GoDaddy|is available on GoDaddy Auctions|is registered at Namecheap/i
|
|
73
|
+
|
|
62
74
|
def self.backend_name
|
|
63
75
|
name.split('::').last
|
|
64
76
|
end
|
|
@@ -118,6 +130,10 @@ module Ask
|
|
|
118
130
|
body.to_s.match?(CHALLENGE_RE)
|
|
119
131
|
end
|
|
120
132
|
|
|
133
|
+
def parked_domain?(body)
|
|
134
|
+
body.to_s.match?(PARKED_DOMAIN_MARKERS)
|
|
135
|
+
end
|
|
136
|
+
|
|
121
137
|
def usable_content?(content)
|
|
122
138
|
content.to_s.strip.length >= MIN_CONTENT_LENGTH
|
|
123
139
|
end
|
|
@@ -27,7 +27,11 @@ module Ask
|
|
|
27
27
|
|
|
28
28
|
# Creates a page as a fresh tab in the attached browser.
|
|
29
29
|
def create_page
|
|
30
|
-
|
|
30
|
+
# Capture the frontmost app once per browser instance, not per
|
|
31
|
+
# page: a batch of fetches steals focus once and restores to the
|
|
32
|
+
# same app, and consecutive fetches don't fight each other.
|
|
33
|
+
@frontmost_app ||= FocusRestorer.capture_frontmost
|
|
34
|
+
Page.new(client, timeout: @timeout, restore_focus_to: @frontmost_app)
|
|
31
35
|
end
|
|
32
36
|
|
|
33
37
|
private
|
|
@@ -43,9 +47,10 @@ module Ask
|
|
|
43
47
|
# enough CDP for the Browser backend: navigate, read title/URL/HTML,
|
|
44
48
|
# and report the main-document HTTP status.
|
|
45
49
|
class Page
|
|
46
|
-
def initialize(client, timeout:)
|
|
50
|
+
def initialize(client, timeout:, restore_focus_to: nil)
|
|
47
51
|
@client = client
|
|
48
52
|
@timeout = timeout
|
|
53
|
+
@restore_focus_to = restore_focus_to
|
|
49
54
|
@target_id = @client.command('Target.createTarget', url: 'about:blank')['targetId']
|
|
50
55
|
@session = @client.session(
|
|
51
56
|
@client.command('Target.attachToTarget', targetId: @target_id, flatten: true)['sessionId']
|
|
@@ -91,6 +96,12 @@ module Ask
|
|
|
91
96
|
@client.command('Target.closeTarget', targetId: @target_id)
|
|
92
97
|
rescue Ferrum::Error
|
|
93
98
|
nil
|
|
99
|
+
ensure
|
|
100
|
+
# The attached dev Chrome window steals focus when a tab is
|
|
101
|
+
# created; give it back once the tab is gone — but only to an
|
|
102
|
+
# app that ISN'T Chrome, so a fetch while the user is already
|
|
103
|
+
# looking at Chrome doesn't ping-pong.
|
|
104
|
+
FocusRestorer.restore_frontmost(@restore_focus_to)
|
|
94
105
|
end
|
|
95
106
|
|
|
96
107
|
def evaluate(expression)
|
|
@@ -189,6 +200,48 @@ module Ask
|
|
|
189
200
|
end
|
|
190
201
|
end
|
|
191
202
|
end
|
|
203
|
+
|
|
204
|
+
# macOS dev convenience: the attached Chrome window steals focus when
|
|
205
|
+
# a tab is created, and a developer working in another app wants it
|
|
206
|
+
# back once the tab is closed. Captures the frontmost app before the
|
|
207
|
+
# page is created and re-activates it after close. Deliberately
|
|
208
|
+
# cheap and quiet: one osascript each way, rescued to nil everywhere
|
|
209
|
+
# else (prod is headless Linux — this module is a no-op there, and
|
|
210
|
+
# even on macOS a failure must never fail a fetch).
|
|
211
|
+
module FocusRestorer
|
|
212
|
+
# The command runner, injectable for tests (minitest 6 ships no
|
|
213
|
+
# mock support, so tests swap this instead of stubbing).
|
|
214
|
+
class << self
|
|
215
|
+
attr_writer :runner
|
|
216
|
+
|
|
217
|
+
def runner
|
|
218
|
+
@runner ||= method(:system)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def self.capture_frontmost
|
|
223
|
+
return nil unless RUBY_PLATFORM.include?('darwin')
|
|
224
|
+
|
|
225
|
+
out = `osascript -e 'tell application "System Events" to get name of first application process whose frontmost is true' 2>/dev/null`.strip
|
|
226
|
+
out.empty? ? nil : out
|
|
227
|
+
rescue StandardError
|
|
228
|
+
nil
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def self.restore_frontmost(app)
|
|
232
|
+
return if app.nil? || app.empty?
|
|
233
|
+
|
|
234
|
+
# Never ping-pong: if the user is already looking at Chrome (or
|
|
235
|
+
# headless/CI has no frontmost app at all), there's nothing to
|
|
236
|
+
# give back.
|
|
237
|
+
return if app == 'Google Chrome'
|
|
238
|
+
|
|
239
|
+
runner.call("osascript -e 'tell application \"#{app}\" to activate' 2>/dev/null")
|
|
240
|
+
nil
|
|
241
|
+
rescue StandardError
|
|
242
|
+
nil
|
|
243
|
+
end
|
|
244
|
+
end
|
|
192
245
|
end
|
|
193
246
|
end
|
|
194
247
|
end
|
|
@@ -199,6 +199,10 @@ module Ask
|
|
|
199
199
|
|
|
200
200
|
return fetch_attempt(page, url, warmed: true)
|
|
201
201
|
end
|
|
202
|
+
# Browser renders the parked page a JS redirect lands on (the
|
|
203
|
+
# server shell hands /lander to JS) — Local never sees it. The
|
|
204
|
+
# shared parked-domain detector catches it here.
|
|
205
|
+
raise EmptyContentError, "parked domain at #{url} — registrar parking page, not site content" if parked_domain?(body)
|
|
202
206
|
|
|
203
207
|
result = Markdown.generate(body, base_url: url, filter: self.class.content_filter)
|
|
204
208
|
result[:outlinks] = outlink_urls(body, url)
|
|
@@ -45,6 +45,18 @@ module Ask
|
|
|
45
45
|
|
|
46
46
|
page = to_markdown(body, url)
|
|
47
47
|
page[:redirected] = redirect
|
|
48
|
+
# Parked-domain pages are not content: the domain owner parked it
|
|
49
|
+
# with a registrar and the page is an ad for buying the domain
|
|
50
|
+
# (GoDaddy/Namecheap/Sedo parking). A content company must never
|
|
51
|
+
# store these as if they were the site. Checked BEFORE the
|
|
52
|
+
# content-minimum — a parking page can render as "content" above
|
|
53
|
+
# the minimum (puncta.ai: 395c of Namecheap auction ads), and
|
|
54
|
+
# must still be rejected. Detectable from the server HTML —
|
|
55
|
+
# parked pages are fully server-rendered, so both Local and
|
|
56
|
+
# Browser see the same ad.
|
|
57
|
+
if parked_domain?(body)
|
|
58
|
+
raise EmptyContentError, "parked domain at #{url} — registrar parking page, not site content"
|
|
59
|
+
end
|
|
48
60
|
raise EmptyContentError, "no readable content at #{url}" unless usable_content?(page[:content])
|
|
49
61
|
# The completeness signal: a JS-app shell whose server HTML
|
|
50
62
|
# renders little is a TRUNCATED page, not a complete one — the
|