ferrum 0.11 → 0.13
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +174 -30
- data/lib/ferrum/browser/binary.rb +46 -0
- data/lib/ferrum/browser/client.rb +17 -16
- data/lib/ferrum/browser/command.rb +10 -12
- data/lib/ferrum/browser/options/base.rb +2 -11
- data/lib/ferrum/browser/options/chrome.rb +29 -18
- data/lib/ferrum/browser/options/firefox.rb +13 -9
- data/lib/ferrum/browser/options.rb +84 -0
- data/lib/ferrum/browser/process.rb +45 -40
- data/lib/ferrum/browser/subscriber.rb +1 -3
- data/lib/ferrum/browser/version_info.rb +71 -0
- data/lib/ferrum/browser/web_socket.rb +9 -12
- data/lib/ferrum/browser/xvfb.rb +4 -8
- data/lib/ferrum/browser.rb +193 -47
- data/lib/ferrum/context.rb +9 -4
- data/lib/ferrum/contexts.rb +12 -10
- data/lib/ferrum/cookies/cookie.rb +126 -0
- data/lib/ferrum/cookies.rb +93 -55
- data/lib/ferrum/dialog.rb +30 -0
- data/lib/ferrum/errors.rb +115 -0
- data/lib/ferrum/frame/dom.rb +177 -0
- data/lib/ferrum/frame/runtime.rb +58 -75
- data/lib/ferrum/frame.rb +118 -23
- data/lib/ferrum/headers.rb +30 -2
- data/lib/ferrum/keyboard.rb +56 -13
- data/lib/ferrum/mouse.rb +92 -7
- data/lib/ferrum/network/auth_request.rb +7 -2
- data/lib/ferrum/network/exchange.rb +97 -12
- data/lib/ferrum/network/intercepted_request.rb +10 -8
- data/lib/ferrum/network/request.rb +69 -0
- data/lib/ferrum/network/response.rb +85 -3
- data/lib/ferrum/network.rb +285 -36
- data/lib/ferrum/node.rb +69 -23
- data/lib/ferrum/page/animation.rb +16 -1
- data/lib/ferrum/page/frames.rb +111 -30
- data/lib/ferrum/page/screenshot.rb +142 -65
- data/lib/ferrum/page/stream.rb +38 -0
- data/lib/ferrum/page/tracing.rb +97 -0
- data/lib/ferrum/page.rb +224 -60
- data/lib/ferrum/proxy.rb +147 -0
- data/lib/ferrum/{rbga.rb → rgba.rb} +4 -2
- data/lib/ferrum/target.rb +7 -4
- data/lib/ferrum/utils/attempt.rb +20 -0
- data/lib/ferrum/utils/elapsed_time.rb +27 -0
- data/lib/ferrum/utils/platform.rb +28 -0
- data/lib/ferrum/version.rb +1 -1
- data/lib/ferrum.rb +4 -146
- metadata +63 -51
data/lib/ferrum/browser.rb
CHANGED
@@ -3,89 +3,208 @@
|
|
3
3
|
require "base64"
|
4
4
|
require "forwardable"
|
5
5
|
require "ferrum/page"
|
6
|
+
require "ferrum/proxy"
|
6
7
|
require "ferrum/contexts"
|
7
8
|
require "ferrum/browser/xvfb"
|
9
|
+
require "ferrum/browser/options"
|
8
10
|
require "ferrum/browser/process"
|
9
11
|
require "ferrum/browser/client"
|
12
|
+
require "ferrum/browser/binary"
|
13
|
+
require "ferrum/browser/version_info"
|
10
14
|
|
11
15
|
module Ferrum
|
12
16
|
class Browser
|
13
|
-
DEFAULT_TIMEOUT = ENV.fetch("FERRUM_DEFAULT_TIMEOUT", 5).to_i
|
14
|
-
WINDOW_SIZE = [1024, 768].freeze
|
15
|
-
BASE_URL_SCHEMA = %w[http https].freeze
|
16
|
-
|
17
17
|
extend Forwardable
|
18
18
|
delegate %i[default_context] => :contexts
|
19
|
-
delegate %i[targets create_target
|
20
|
-
delegate %i[go_to back forward refresh reload stop wait_for_reload
|
19
|
+
delegate %i[targets create_target page pages windows] => :default_context
|
20
|
+
delegate %i[go_to goto go back forward refresh reload stop wait_for_reload
|
21
21
|
at_css at_xpath css xpath current_url current_title url title
|
22
|
-
body doctype
|
22
|
+
body doctype content=
|
23
23
|
headers cookies network
|
24
24
|
mouse keyboard
|
25
25
|
screenshot pdf mhtml viewport_size
|
26
26
|
frames frame_by main_frame
|
27
27
|
evaluate evaluate_on evaluate_async execute evaluate_func
|
28
28
|
add_script_tag add_style_tag bypass_csp
|
29
|
-
on
|
29
|
+
on position position=
|
30
30
|
playback_rate playback_rate=] => :page
|
31
31
|
delegate %i[default_user_agent] => :process
|
32
32
|
|
33
|
-
attr_reader :client, :process, :contexts, :
|
34
|
-
|
35
|
-
attr_writer :timeout
|
33
|
+
attr_reader :client, :process, :contexts, :options, :window_size, :base_url
|
34
|
+
attr_accessor :timeout
|
36
35
|
|
36
|
+
#
|
37
|
+
# Initializes the browser.
|
38
|
+
#
|
39
|
+
# @param [Hash{Symbol => Object}, nil] options
|
40
|
+
# Additional browser options.
|
41
|
+
#
|
42
|
+
# @option options [Boolean] :headless (true)
|
43
|
+
# Set browser as headless or not.
|
44
|
+
#
|
45
|
+
# @option options [Boolean] :xvfb (false)
|
46
|
+
# Run browser in a virtual framebuffer.
|
47
|
+
#
|
48
|
+
# @option options [(Integer, Integer)] :window_size ([1024, 768])
|
49
|
+
# The dimensions of the browser window in which to test, expressed as a
|
50
|
+
# 2-element array, e.g. `[1024, 768]`.
|
51
|
+
#
|
52
|
+
# @option options [Array<String, Hash>] :extensions
|
53
|
+
# An array of paths to files or JS source code to be preloaded into the
|
54
|
+
# browser e.g.: `["/path/to/script.js", { source: "window.secret = 'top'" }]`
|
55
|
+
#
|
56
|
+
# @option options [#puts] :logger
|
57
|
+
# When present, debug output is written to this object.
|
58
|
+
#
|
59
|
+
# @option options [Integer, Float] :slowmo
|
60
|
+
# Set a delay in seconds to wait before sending command.
|
61
|
+
# Useful companion of headless option, so that you have time to see
|
62
|
+
# changes.
|
63
|
+
#
|
64
|
+
# @option options [Numeric] :timeout (5)
|
65
|
+
# The number of seconds we'll wait for a response when communicating with
|
66
|
+
# browser.
|
67
|
+
#
|
68
|
+
# @option options [Boolean] :js_errors
|
69
|
+
# When true, JavaScript errors get re-raised in Ruby.
|
70
|
+
#
|
71
|
+
# @option options [Boolean] :pending_connection_errors (true)
|
72
|
+
# When main frame is still waiting for slow responses while timeout is
|
73
|
+
# reached {PendingConnectionsError} is raised. It's better to figure out
|
74
|
+
# why you have slow responses and fix or block them rather than turn this
|
75
|
+
# setting off.
|
76
|
+
#
|
77
|
+
# @option options [:chrome, :firefox] :browser_name (:chrome)
|
78
|
+
# Sets the browser's name. **Note:** only experimental support for
|
79
|
+
# `:firefox` for now.
|
80
|
+
#
|
81
|
+
# @option options [String] :browser_path
|
82
|
+
# Path to Chrome binary, you can also set ENV variable as
|
83
|
+
# `BROWSER_PATH=some/path/chrome bundle exec rspec`.
|
84
|
+
#
|
85
|
+
# @option options [Hash] :browser_options
|
86
|
+
# Additional command line options, [see them all](https://peter.sh/experiments/chromium-command-line-switches/)
|
87
|
+
# e.g. `{ "ignore-certificate-errors" => nil }`
|
88
|
+
#
|
89
|
+
# @option options [Boolean] :ignore_default_browser_options
|
90
|
+
# Ferrum has a number of default options it passes to the browser,
|
91
|
+
# if you set this to `true` then only options you put in
|
92
|
+
# `:browser_options` will be passed to the browser, except required ones
|
93
|
+
# of course.
|
94
|
+
#
|
95
|
+
# @option options [Integer] :port
|
96
|
+
# Remote debugging port for headless Chrome.
|
97
|
+
#
|
98
|
+
# @option options [String] :host
|
99
|
+
# Remote debugging address for headless Chrome.
|
100
|
+
#
|
101
|
+
# @option options [String] :url
|
102
|
+
# URL for a running instance of Chrome. If this is set, a browser process
|
103
|
+
# will not be spawned.
|
104
|
+
#
|
105
|
+
# @option options [Integer] :process_timeout
|
106
|
+
# How long to wait for the Chrome process to respond on startup.
|
107
|
+
#
|
108
|
+
# @option options [Integer] :ws_max_receive_size
|
109
|
+
# How big messages to accept from Chrome over the web socket, in bytes.
|
110
|
+
# Defaults to 64MB. Incoming messages larger this will cause a
|
111
|
+
# {Ferrum::DeadBrowserError}.
|
112
|
+
#
|
113
|
+
# @option options [Hash] :proxy
|
114
|
+
# Specify proxy settings, [read more](https://github.com/rubycdp/ferrum#proxy).
|
115
|
+
#
|
116
|
+
# @option options [String] :save_path
|
117
|
+
# Path to save attachments with [Content-Disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition)
|
118
|
+
# header.
|
119
|
+
#
|
120
|
+
# @option options [Hash] :env
|
121
|
+
# Environment variables you'd like to pass through to the process.
|
122
|
+
#
|
37
123
|
def initialize(options = nil)
|
38
|
-
options
|
124
|
+
@options = Options.new(options)
|
125
|
+
@client = @process = @contexts = nil
|
39
126
|
|
40
|
-
@
|
41
|
-
@window_size = options.
|
42
|
-
@
|
127
|
+
@timeout = @options.timeout
|
128
|
+
@window_size = @options.window_size
|
129
|
+
@base_url = @options.base_url if @options.base_url
|
43
130
|
|
44
|
-
|
45
|
-
|
46
|
-
@options.values_at(:logger, :timeout, :ws_max_receive_size)
|
47
|
-
@js_errors = @options.fetch(:js_errors, false)
|
48
|
-
@pending_connection_errors = @options.fetch(:pending_connection_errors, true)
|
49
|
-
@slowmo = @options[:slowmo].to_f
|
131
|
+
start
|
132
|
+
end
|
50
133
|
|
51
|
-
|
52
|
-
|
53
|
-
|
134
|
+
#
|
135
|
+
# Sets the base URL.
|
136
|
+
#
|
137
|
+
# @param [String] value
|
138
|
+
# The new base URL value.
|
139
|
+
#
|
140
|
+
# @raise [ArgumentError] when path is not absolute or doesn't include schema
|
141
|
+
#
|
142
|
+
# @return [Addressable::URI]
|
143
|
+
# The parsed base URI value.
|
144
|
+
#
|
145
|
+
def base_url=(value)
|
146
|
+
@base_url = options.parse_base_url(value)
|
147
|
+
end
|
54
148
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
149
|
+
#
|
150
|
+
# Creates a new page.
|
151
|
+
#
|
152
|
+
# @param [Boolean] new_context
|
153
|
+
# Whether to create a page in a new context or not.
|
154
|
+
#
|
155
|
+
# @param [Hash] proxy
|
156
|
+
# Whether to use proxy for a page. The page will be created in a new context if so.
|
157
|
+
#
|
158
|
+
# @return [Ferrum::Page]
|
159
|
+
# Created page.
|
160
|
+
#
|
161
|
+
def create_page(new_context: false, proxy: nil)
|
162
|
+
page = if new_context || proxy
|
163
|
+
params = {}
|
60
164
|
|
61
|
-
|
165
|
+
if proxy
|
166
|
+
options.parse_proxy(proxy)
|
167
|
+
params.merge!(proxyServer: "#{proxy[:host]}:#{proxy[:port]}")
|
168
|
+
params.merge!(proxyBypassList: proxy[:bypass]) if proxy[:bypass]
|
169
|
+
end
|
62
170
|
|
63
|
-
|
64
|
-
|
171
|
+
context = contexts.create(**params)
|
172
|
+
context.create_page(proxy: proxy)
|
173
|
+
else
|
174
|
+
default_context.create_page
|
175
|
+
end
|
65
176
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
177
|
+
block_given? ? yield(page) : page
|
178
|
+
ensure
|
179
|
+
if block_given?
|
180
|
+
page.close
|
181
|
+
context.dispose if new_context
|
70
182
|
end
|
71
|
-
|
72
|
-
@base_url = parsed
|
73
183
|
end
|
74
184
|
|
75
185
|
def extensions
|
76
|
-
@extensions ||= Array(
|
186
|
+
@extensions ||= Array(options.extensions).map do |ext|
|
77
187
|
(ext.is_a?(Hash) && ext[:source]) || File.read(ext)
|
78
188
|
end
|
79
189
|
end
|
80
190
|
|
191
|
+
#
|
192
|
+
# Evaluate JavaScript to modify things before a page load.
|
193
|
+
#
|
194
|
+
# @param [String] expression
|
195
|
+
# The JavaScript to add to each new document.
|
196
|
+
#
|
197
|
+
# @example
|
198
|
+
# browser.evaluate_on_new_document <<~JS
|
199
|
+
# Object.defineProperty(navigator, "languages", {
|
200
|
+
# get: function() { return ["tlh"]; }
|
201
|
+
# });
|
202
|
+
# JS
|
203
|
+
#
|
81
204
|
def evaluate_on_new_document(expression)
|
82
205
|
extensions << expression
|
83
206
|
end
|
84
207
|
|
85
|
-
def timeout
|
86
|
-
@timeout || DEFAULT_TIMEOUT
|
87
|
-
end
|
88
|
-
|
89
208
|
def command(*args)
|
90
209
|
@client.command(*args)
|
91
210
|
rescue DeadBrowserError
|
@@ -93,8 +212,22 @@ module Ferrum
|
|
93
212
|
raise
|
94
213
|
end
|
95
214
|
|
215
|
+
#
|
216
|
+
# Closes browser tabs opened by the `Browser` instance.
|
217
|
+
#
|
218
|
+
# @example
|
219
|
+
# # connect to a long-running Chrome process
|
220
|
+
# browser = Ferrum::Browser.new(url: 'http://localhost:9222')
|
221
|
+
#
|
222
|
+
# browser.go_to("https://github.com/")
|
223
|
+
#
|
224
|
+
# # clean up, lest the tab stays there hanging forever
|
225
|
+
# browser.reset
|
226
|
+
#
|
227
|
+
# browser.quit
|
228
|
+
#
|
96
229
|
def reset
|
97
|
-
@window_size =
|
230
|
+
@window_size = options.window_size
|
98
231
|
contexts.reset
|
99
232
|
end
|
100
233
|
|
@@ -118,12 +251,25 @@ module Ferrum
|
|
118
251
|
command("Browser.crash")
|
119
252
|
end
|
120
253
|
|
254
|
+
#
|
255
|
+
# Gets the version information from the browser.
|
256
|
+
#
|
257
|
+
# @return [VersionInfo]
|
258
|
+
#
|
259
|
+
# @since 0.13
|
260
|
+
#
|
261
|
+
def version
|
262
|
+
VersionInfo.new(command("Browser.getVersion"))
|
263
|
+
end
|
264
|
+
|
121
265
|
private
|
122
266
|
|
123
267
|
def start
|
124
|
-
|
125
|
-
@process = Process.start(
|
126
|
-
@client = Client.new(
|
268
|
+
Utils::ElapsedTime.start
|
269
|
+
@process = Process.start(options)
|
270
|
+
@client = Client.new(@process.ws_url, self,
|
271
|
+
logger: options.logger,
|
272
|
+
ws_max_receive_size: options.ws_max_receive_size)
|
127
273
|
@contexts = Contexts.new(self)
|
128
274
|
end
|
129
275
|
end
|
data/lib/ferrum/context.rb
CHANGED
@@ -9,7 +9,9 @@ module Ferrum
|
|
9
9
|
attr_reader :id, :targets
|
10
10
|
|
11
11
|
def initialize(browser, contexts, id)
|
12
|
-
@
|
12
|
+
@id = id
|
13
|
+
@browser = browser
|
14
|
+
@contexts = contexts
|
13
15
|
@targets = Concurrent::Map.new
|
14
16
|
@pendings = Concurrent::MVar.new
|
15
17
|
end
|
@@ -32,13 +34,15 @@ module Ferrum
|
|
32
34
|
# usually is the last one.
|
33
35
|
def windows(pos = nil, size = 1)
|
34
36
|
raise ArgumentError if pos && !POSITION.include?(pos)
|
37
|
+
|
35
38
|
windows = @targets.values.select(&:window?)
|
36
39
|
windows = windows.send(pos, size) if pos
|
37
40
|
windows.map(&:page)
|
38
41
|
end
|
39
42
|
|
40
|
-
def create_page
|
41
|
-
create_target
|
43
|
+
def create_page(**options)
|
44
|
+
target = create_target
|
45
|
+
target.page = target.build_page(**options)
|
42
46
|
end
|
43
47
|
|
44
48
|
def create_target
|
@@ -47,6 +51,7 @@ module Ferrum
|
|
47
51
|
url: "about:blank")
|
48
52
|
target = @pendings.take(@browser.timeout)
|
49
53
|
raise NoSuchTargetError unless target.is_a?(Target)
|
54
|
+
|
50
55
|
@targets.put_if_absent(target.id, target)
|
51
56
|
target
|
52
57
|
end
|
@@ -72,7 +77,7 @@ module Ferrum
|
|
72
77
|
@contexts.dispose(@id)
|
73
78
|
end
|
74
79
|
|
75
|
-
def
|
80
|
+
def target?(target_id)
|
76
81
|
!!@targets[target_id]
|
77
82
|
end
|
78
83
|
|
data/lib/ferrum/contexts.rb
CHANGED
@@ -19,12 +19,12 @@ module Ferrum
|
|
19
19
|
|
20
20
|
def find_by(target_id:)
|
21
21
|
context = nil
|
22
|
-
@contexts.each_value { |c| context = c if c.
|
22
|
+
@contexts.each_value { |c| context = c if c.target?(target_id) }
|
23
23
|
context
|
24
24
|
end
|
25
25
|
|
26
|
-
def create
|
27
|
-
response = @browser.command("Target.createBrowserContext")
|
26
|
+
def create(**options)
|
27
|
+
response = @browser.command("Target.createBrowserContext", **options)
|
28
28
|
context_id = response["browserContextId"]
|
29
29
|
context = Context.new(@browser, self, context_id)
|
30
30
|
@contexts[context_id] = context
|
@@ -41,7 +41,11 @@ module Ferrum
|
|
41
41
|
|
42
42
|
def reset
|
43
43
|
@default_context = nil
|
44
|
-
@contexts.
|
44
|
+
@contexts.each_key { |id| dispose(id) }
|
45
|
+
end
|
46
|
+
|
47
|
+
def size
|
48
|
+
@contexts.size
|
45
49
|
end
|
46
50
|
|
47
51
|
private
|
@@ -64,15 +68,13 @@ module Ferrum
|
|
64
68
|
end
|
65
69
|
|
66
70
|
@browser.client.on("Target.targetDestroyed") do |params|
|
67
|
-
|
68
|
-
|
69
|
-
end
|
71
|
+
context = find_by(target_id: params["targetId"])
|
72
|
+
context&.delete_target(params["targetId"])
|
70
73
|
end
|
71
74
|
|
72
75
|
@browser.client.on("Target.targetCrashed") do |params|
|
73
|
-
|
74
|
-
|
75
|
-
end
|
76
|
+
context = find_by(target_id: params["targetId"])
|
77
|
+
context&.delete_target(params["targetId"])
|
76
78
|
end
|
77
79
|
end
|
78
80
|
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Ferrum
|
4
|
+
class Cookies
|
5
|
+
#
|
6
|
+
# Represents a [cookie value](https://chromedevtools.github.io/devtools-protocol/1-3/Network/#type-Cookie).
|
7
|
+
#
|
8
|
+
class Cookie
|
9
|
+
# The parsed JSON attributes.
|
10
|
+
#
|
11
|
+
# @return [Hash{String => [String, Boolean, nil]}]
|
12
|
+
attr_reader :attributes
|
13
|
+
|
14
|
+
#
|
15
|
+
# Initializes the cookie.
|
16
|
+
#
|
17
|
+
# @param [Hash{String => String}] attributes
|
18
|
+
# The parsed JSON attributes.
|
19
|
+
#
|
20
|
+
def initialize(attributes)
|
21
|
+
@attributes = attributes
|
22
|
+
end
|
23
|
+
|
24
|
+
#
|
25
|
+
# The cookie's name.
|
26
|
+
#
|
27
|
+
# @return [String]
|
28
|
+
#
|
29
|
+
def name
|
30
|
+
attributes["name"]
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# The cookie's value.
|
35
|
+
#
|
36
|
+
# @return [String]
|
37
|
+
#
|
38
|
+
def value
|
39
|
+
attributes["value"]
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# The cookie's domain.
|
44
|
+
#
|
45
|
+
# @return [String]
|
46
|
+
#
|
47
|
+
def domain
|
48
|
+
attributes["domain"]
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# The cookie's path.
|
53
|
+
#
|
54
|
+
# @return [String]
|
55
|
+
#
|
56
|
+
def path
|
57
|
+
attributes["path"]
|
58
|
+
end
|
59
|
+
|
60
|
+
#
|
61
|
+
# The `sameSite` configuration.
|
62
|
+
#
|
63
|
+
# @return ["Strict", "Lax", "None", nil]
|
64
|
+
#
|
65
|
+
def samesite
|
66
|
+
attributes["sameSite"]
|
67
|
+
end
|
68
|
+
alias same_site samesite
|
69
|
+
|
70
|
+
#
|
71
|
+
# The cookie's size.
|
72
|
+
#
|
73
|
+
# @return [Integer]
|
74
|
+
#
|
75
|
+
def size
|
76
|
+
attributes["size"]
|
77
|
+
end
|
78
|
+
|
79
|
+
#
|
80
|
+
# Specifies whether the cookie is secure or not.
|
81
|
+
#
|
82
|
+
# @return [Boolean]
|
83
|
+
#
|
84
|
+
def secure?
|
85
|
+
attributes["secure"]
|
86
|
+
end
|
87
|
+
|
88
|
+
#
|
89
|
+
# Specifies whether the cookie is HTTP-only or not.
|
90
|
+
#
|
91
|
+
# @return [Boolean]
|
92
|
+
#
|
93
|
+
def httponly?
|
94
|
+
attributes["httpOnly"]
|
95
|
+
end
|
96
|
+
alias http_only? httponly?
|
97
|
+
|
98
|
+
#
|
99
|
+
# Specifies whether the cookie is a session cookie or not.
|
100
|
+
#
|
101
|
+
# @return [Boolean]
|
102
|
+
#
|
103
|
+
def session?
|
104
|
+
attributes["session"]
|
105
|
+
end
|
106
|
+
|
107
|
+
#
|
108
|
+
# Specifies when the cookie will expire.
|
109
|
+
#
|
110
|
+
# @return [Time, nil]
|
111
|
+
#
|
112
|
+
def expires
|
113
|
+
Time.at(attributes["expires"]) if attributes["expires"].positive?
|
114
|
+
end
|
115
|
+
|
116
|
+
#
|
117
|
+
# Compares different cookie objects.
|
118
|
+
#
|
119
|
+
# @return [Boolean]
|
120
|
+
#
|
121
|
+
def ==(other)
|
122
|
+
other.class == self.class && other.attributes == attributes
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
data/lib/ferrum/cookies.rb
CHANGED
@@ -1,84 +1,114 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "ferrum/cookies/cookie"
|
4
|
+
|
3
5
|
module Ferrum
|
4
6
|
class Cookies
|
5
|
-
class Cookie
|
6
|
-
def initialize(attributes)
|
7
|
-
@attributes = attributes
|
8
|
-
end
|
9
|
-
|
10
|
-
def name
|
11
|
-
@attributes["name"]
|
12
|
-
end
|
13
|
-
|
14
|
-
def value
|
15
|
-
@attributes["value"]
|
16
|
-
end
|
17
|
-
|
18
|
-
def domain
|
19
|
-
@attributes["domain"]
|
20
|
-
end
|
21
|
-
|
22
|
-
def path
|
23
|
-
@attributes["path"]
|
24
|
-
end
|
25
|
-
|
26
|
-
def samesite
|
27
|
-
@attributes["sameSite"]
|
28
|
-
end
|
29
|
-
|
30
|
-
def size
|
31
|
-
@attributes["size"]
|
32
|
-
end
|
33
|
-
|
34
|
-
def secure?
|
35
|
-
@attributes["secure"]
|
36
|
-
end
|
37
|
-
|
38
|
-
def httponly?
|
39
|
-
@attributes["httpOnly"]
|
40
|
-
end
|
41
|
-
|
42
|
-
def session?
|
43
|
-
@attributes["session"]
|
44
|
-
end
|
45
|
-
|
46
|
-
def expires
|
47
|
-
if @attributes["expires"] > 0
|
48
|
-
Time.at(@attributes["expires"])
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
7
|
def initialize(page)
|
54
8
|
@page = page
|
55
9
|
end
|
56
10
|
|
11
|
+
#
|
12
|
+
# Returns cookies hash.
|
13
|
+
#
|
14
|
+
# @return [Hash{String => Cookie}]
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# browser.cookies.all # => {
|
18
|
+
# # "NID" => #<Ferrum::Cookies::Cookie:0x0000558624b37a40 @attributes={
|
19
|
+
# # "name"=>"NID", "value"=>"...", "domain"=>".google.com", "path"=>"/",
|
20
|
+
# # "expires"=>1583211046.575681, "size"=>178, "httpOnly"=>true, "secure"=>false, "session"=>false
|
21
|
+
# # }>
|
22
|
+
# # }
|
23
|
+
#
|
57
24
|
def all
|
58
25
|
cookies = @page.command("Network.getAllCookies")["cookies"]
|
59
|
-
cookies.
|
26
|
+
cookies.to_h { |c| [c["name"], Cookie.new(c)] }
|
60
27
|
end
|
61
28
|
|
29
|
+
#
|
30
|
+
# Returns cookie.
|
31
|
+
#
|
32
|
+
# @param [String] name
|
33
|
+
# The cookie name to fetch.
|
34
|
+
#
|
35
|
+
# @return [Cookie, nil]
|
36
|
+
# The cookie with the matching name.
|
37
|
+
#
|
38
|
+
# @example
|
39
|
+
# browser.cookies["NID"] # =>
|
40
|
+
# # <Ferrum::Cookies::Cookie:0x0000558624b67a88 @attributes={
|
41
|
+
# # "name"=>"NID", "value"=>"...", "domain"=>".google.com",
|
42
|
+
# # "path"=>"/", "expires"=>1583211046.575681, "size"=>178,
|
43
|
+
# # "httpOnly"=>true, "secure"=>false, "session"=>false
|
44
|
+
# # }>
|
45
|
+
#
|
62
46
|
def [](name)
|
63
47
|
all[name]
|
64
48
|
end
|
65
49
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
50
|
+
#
|
51
|
+
# Sets a cookie.
|
52
|
+
#
|
53
|
+
# @param [Hash{Symbol => Object}, Cookie] options
|
54
|
+
#
|
55
|
+
# @option options [String] :name
|
56
|
+
#
|
57
|
+
# @option options [String] :value
|
58
|
+
#
|
59
|
+
# @option options [String] :domain
|
60
|
+
#
|
61
|
+
# @option options [String] :path
|
62
|
+
#
|
63
|
+
# @option options [Integer] :expires
|
64
|
+
#
|
65
|
+
# @option options [Integer] :size
|
66
|
+
#
|
67
|
+
# @option options [Boolean] :httponly
|
68
|
+
#
|
69
|
+
# @option options [Boolean] :secure
|
70
|
+
#
|
71
|
+
# @option options [String] :samesite
|
72
|
+
#
|
73
|
+
#
|
74
|
+
# @example
|
75
|
+
# browser.cookies.set(name: "stealth", value: "omg", domain: "google.com") # => true
|
76
|
+
#
|
77
|
+
# @example
|
78
|
+
# nid_cookie = browser.cookies["NID"] # => <Ferrum::Cookies::Cookie:0x0000558624b67a88>
|
79
|
+
# browser.cookies.set(nid_cookie) # => true
|
80
|
+
#
|
81
|
+
def set(options)
|
82
|
+
cookie = (
|
83
|
+
options.is_a?(Cookie) ? options.attributes : options
|
84
|
+
).dup.transform_keys(&:to_sym)
|
85
|
+
|
70
86
|
cookie[:domain] ||= default_domain
|
71
87
|
|
72
88
|
cookie[:httpOnly] = cookie.delete(:httponly) if cookie.key?(:httponly)
|
73
89
|
cookie[:sameSite] = cookie.delete(:samesite) if cookie.key?(:samesite)
|
74
90
|
|
75
91
|
expires = cookie.delete(:expires).to_i
|
76
|
-
cookie[:expires] = expires if expires
|
92
|
+
cookie[:expires] = expires if expires.positive?
|
77
93
|
|
78
94
|
@page.command("Network.setCookie", **cookie)["success"]
|
79
95
|
end
|
80
96
|
|
81
|
-
#
|
97
|
+
#
|
98
|
+
# Removes given cookie.
|
99
|
+
#
|
100
|
+
# @param [String] name
|
101
|
+
#
|
102
|
+
# @param [Hash{Symbol => Object}] options
|
103
|
+
# Additional keyword arguments.
|
104
|
+
#
|
105
|
+
# @option options [String] :domain
|
106
|
+
#
|
107
|
+
# @option options [String] :url
|
108
|
+
#
|
109
|
+
# @example
|
110
|
+
# browser.cookies.remove(name: "stealth", domain: "google.com") # => true
|
111
|
+
#
|
82
112
|
def remove(name:, **options)
|
83
113
|
raise "Specify :domain or :url option" if !options[:domain] && !options[:url] && !default_domain
|
84
114
|
|
@@ -90,6 +120,14 @@ module Ferrum
|
|
90
120
|
true
|
91
121
|
end
|
92
122
|
|
123
|
+
#
|
124
|
+
# Removes all cookies for current page.
|
125
|
+
#
|
126
|
+
# @return [true]
|
127
|
+
#
|
128
|
+
# @example
|
129
|
+
# browser.cookies.clear # => true
|
130
|
+
#
|
93
131
|
def clear
|
94
132
|
@page.command("Network.clearBrowserCookies")
|
95
133
|
true
|