puppeteer-ruby 0.0.15 → 0.0.20

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,6 +6,7 @@ class Puppeteer::RemoteObject
6
6
  # @param payload [Hash]
7
7
  def initialize(payload)
8
8
  @object_id = payload['objectId']
9
+ @type = payload['type']
9
10
  @sub_type = payload['subtype']
10
11
  @unserializable_value = payload['unserializableValue']
11
12
  @value = payload['value']
@@ -42,6 +43,21 @@ class Puppeteer::RemoteObject
42
43
  end
43
44
  end
44
45
 
46
+ # @return [String]
47
+ def type_str
48
+ # used in JSHandle#to_s
49
+ # original logic:
50
+ # if (this._remoteObject.objectId) {
51
+ # const type = this._remoteObject.subtype || this._remoteObject.type;
52
+ # return 'JSHandle@' + type;
53
+ # }
54
+ if @object_id
55
+ @sub_type || @type
56
+ else
57
+ nil
58
+ end
59
+ end
60
+
45
61
  # used in JSHandle#properties
46
62
  def properties(client)
47
63
  # original logic:
@@ -64,7 +80,18 @@ class Puppeteer::RemoteObject
64
80
 
65
81
  # used in ElementHandle#_box_model
66
82
  def box_model(client)
67
- client.send_message('DOM.getBoxModel', objectId: @object_id)
83
+ result = client.send_message('DOM.getBoxModel', objectId: @object_id)
84
+
85
+ # Firefox returns width/height = 0, content/padding/border/margin = [nil, nil, nil, nil, nil, nil, nil, nil]
86
+ # while Chrome throws Error(Could not compute box model)
87
+ model = result['model']
88
+ if model['width'] == 0 && model['height'] == 0 &&
89
+ %w(content padding border margin).all? { |key| model[key].all?(&:nil?) }
90
+
91
+ debug_puts('Could not compute box model in Firefox.')
92
+ return nil
93
+ end
94
+ result
68
95
  rescue => err
69
96
  debug_puts(err)
70
97
  nil
@@ -0,0 +1,330 @@
1
+ class Puppeteer::Request
2
+ include Puppeteer::DebugPrint
3
+ include Puppeteer::IfPresent
4
+
5
+ # defines some methods used only in NetworkManager, Response
6
+ class InternalAccessor
7
+ def initialize(request)
8
+ @request = request
9
+ end
10
+
11
+ def request_id
12
+ @request.instance_variable_get(:@request_id)
13
+ end
14
+
15
+ def interception_id
16
+ @request.instance_variable_get(:@interception_id)
17
+ end
18
+
19
+ # @param response [Puppeteer::Response]
20
+ def response=(response)
21
+ @request.instance_variable_set(:@response, response)
22
+ end
23
+
24
+ def redirect_chain
25
+ @request.instance_variable_get(:@redirect_chain)
26
+ end
27
+
28
+ def failure_text=(failure_text)
29
+ @request.instance_variable_set(:@failure_text, failure_text)
30
+ end
31
+
32
+ def from_memory_cache=(from_memory_cache)
33
+ @request.instance_variable_set(:@from_memory_cache, from_memory_cache)
34
+ end
35
+
36
+ def from_memory_cache?
37
+ @request.instance_variable_get(:@from_memory_cache)
38
+ end
39
+ end
40
+
41
+ # @param client [Puppeteer::CDPSession]
42
+ # @param frame [Puppeteer::Frame]
43
+ # @param interception_id [string|nil]
44
+ # @param allow_interception [boolean]
45
+ # @param event [Hash]
46
+ # @param redirect_chain Array<Request>
47
+ def initialize(client, frame, interception_id, allow_interception, event, redirect_chain)
48
+ @client = client
49
+ @request_id = event['requestId']
50
+ @is_navigation_request = event['requestId'] == event['loaderId'] && event['type'] == 'Document'
51
+ @interception_id = interception_id
52
+ @allow_interception = allow_interception
53
+ @url = event['request']['url']
54
+ @resource_type = event['type'].downcase
55
+ @method = event['request']['method']
56
+ @post_data = event['request']['postData']
57
+ @frame = frame
58
+ @redirect_chain = redirect_chain
59
+ @headers = {}
60
+ event['request']['headers'].each do |key, value|
61
+ @headers[key.downcase] = value
62
+ end
63
+ @from_memory_cache = false
64
+
65
+ @internal = InternalAccessor.new(self)
66
+ end
67
+
68
+ attr_reader :internal
69
+ attr_reader :url, :resource_type, :method, :post_data, :headers, :response, :frame
70
+
71
+ def navigation_request?
72
+ @is_navigation_request
73
+ end
74
+
75
+ def redirect_chain
76
+ @redirect_chain.dup
77
+ end
78
+
79
+ def failure
80
+ if_present(@failure_text) do |failure_text|
81
+ { errorText: @failure_text }
82
+ end
83
+ end
84
+
85
+ private def headers_to_array(headers)
86
+ return nil unless headers
87
+
88
+ headers.map do |key, value|
89
+ { name: key, value: value.to_s }
90
+ end
91
+ end
92
+
93
+ class InterceptionNotEnabledError < StandardError
94
+ def initialize
95
+ super('Request Interception is not enabled!')
96
+ end
97
+ end
98
+
99
+ class AlreadyHandledError < StandardError
100
+ def initialize
101
+ super('Request is already handled!')
102
+ end
103
+ end
104
+
105
+ # proceed request on request interception.
106
+ #
107
+ # Example:
108
+ #
109
+ # page.on 'request' do |req|
110
+ # # Override headers
111
+ # headers = req.headers.merge(
112
+ # foo: 'bar', # set "foo" header
113
+ # origin: nil, # remove "origin" header
114
+ # )
115
+ # req.continue(headers: headers)
116
+ # end
117
+ #
118
+ # @param error_code [String|Symbol]
119
+ def continue(url: nil, method: nil, post_data: nil, headers: nil)
120
+ # Request interception is not supported for data: urls.
121
+ return if @url.start_with?('data:')
122
+
123
+ unless @allow_interception
124
+ raise InterceptionNotEnabledError.new
125
+ end
126
+ if @interception_handled
127
+ raise AlreadyHandledError.new
128
+ end
129
+ @interception_handled = true
130
+
131
+ overrides = {
132
+ url: url,
133
+ method: method,
134
+ post_data: post_data,
135
+ headers: headers_to_array(headers),
136
+ }.compact
137
+ begin
138
+ @client.send_message('Fetch.continueRequest',
139
+ requestId: @interception_id,
140
+ **overrides,
141
+ )
142
+ rescue => err
143
+ # In certain cases, protocol will return error if the request was already canceled
144
+ # or the page was closed. We should tolerate these errors.
145
+ debug_puts(err)
146
+ end
147
+ end
148
+
149
+ # Mocking response.
150
+ #
151
+ # Example:
152
+ #
153
+ # page.on 'request' do |req|
154
+ # req.respond(
155
+ # status: 404,
156
+ # content_type: 'text/plain',
157
+ # body: 'Not Found!'
158
+ # )
159
+ # end
160
+ #
161
+ # @param status [Integer]
162
+ # @param headers [Hash<String, String>]
163
+ # @param content_type [String]
164
+ # @param body [String]
165
+ def respond(status: nil, headers: nil, content_type: nil, body: nil)
166
+ # Mocking responses for dataURL requests is not currently supported.
167
+ return if @url.start_with?('data:')
168
+
169
+ unless @allow_interception
170
+ raise InterceptionNotEnabledError.new
171
+ end
172
+ if @interception_handled
173
+ raise AlreadyHandledError.new
174
+ end
175
+ @interception_handled = true
176
+
177
+ mock_response_headers = {}
178
+ headers&.each do |key, value|
179
+ mock_response_headers[key.downcase] = value
180
+ end
181
+ if content_type
182
+ mock_response_headers['content-type'] = content_type
183
+ end
184
+ if body
185
+ mock_response_headers['content-length'] = body.length
186
+ end
187
+
188
+ mock_response = {
189
+ responseCode: status || 200,
190
+ responsePhrase: STATUS_TEXTS[(status || 200).to_s],
191
+ responseHeaders: headers_to_array(mock_response_headers),
192
+ body: if_present(body) { |mock_body| Base64.strict_encode64(mock_body) },
193
+ }.compact
194
+ begin
195
+ @client.send_message('Fetch.fulfillRequest',
196
+ requestId: @interception_id,
197
+ **mock_response,
198
+ )
199
+ rescue => err
200
+ # In certain cases, protocol will return error if the request was already canceled
201
+ # or the page was closed. We should tolerate these errors.
202
+ debug_puts(err)
203
+ end
204
+ end
205
+
206
+ # abort request on request interception.
207
+ #
208
+ # Example:
209
+ #
210
+ # page.on 'request' do |req|
211
+ # if req.url.include?("porn")
212
+ # req.abort
213
+ # else
214
+ # req.continue
215
+ # end
216
+ # end
217
+ #
218
+ # @param error_code [String|Symbol]
219
+ def abort(error_code: :failed)
220
+ # Request interception is not supported for data: urls.
221
+ return if @url.start_with?('data:')
222
+
223
+ error_reason = ERROR_REASONS[error_code.to_s]
224
+ unless error_reason
225
+ raise ArgumentError.new("Unknown error code: #{error_code}")
226
+ end
227
+ unless @allow_interception
228
+ raise InterceptionNotEnabledError.new
229
+ end
230
+ if @interception_handled
231
+ raise AlreadyHandledError.new
232
+ end
233
+ @interception_handled = true
234
+
235
+ begin
236
+ @client.send_message('Fetch.failRequest',
237
+ requestId: @interception_id,
238
+ errorReason: error_reason,
239
+ )
240
+ rescue => err
241
+ # In certain cases, protocol will return error if the request was already canceled
242
+ # or the page was closed. We should tolerate these errors.
243
+ debug_puts(err)
244
+ end
245
+ end
246
+
247
+ ERROR_REASONS = {
248
+ 'aborted' => 'Aborted',
249
+ 'accessdenied' => 'AccessDenied',
250
+ 'addressunreachable' => 'AddressUnreachable',
251
+ 'blockedbyclient' => 'BlockedByClient',
252
+ 'blockedbyresponse' => 'BlockedByResponse',
253
+ 'connectionaborted' => 'ConnectionAborted',
254
+ 'connectionclosed' => 'ConnectionClosed',
255
+ 'connectionfailed' => 'ConnectionFailed',
256
+ 'connectionrefused' => 'ConnectionRefused',
257
+ 'connectionreset' => 'ConnectionReset',
258
+ 'internetdisconnected' => 'InternetDisconnected',
259
+ 'namenotresolved' => 'NameNotResolved',
260
+ 'timedout' => 'TimedOut',
261
+ 'failed' => 'Failed',
262
+ }.freeze
263
+
264
+ # List taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml with extra 306 and 418 codes.
265
+ STATUS_TEXTS = {
266
+ '100' => 'Continue',
267
+ '101' => 'Switching Protocols',
268
+ '102' => 'Processing',
269
+ '103' => 'Early Hints',
270
+ '200' => 'OK',
271
+ '201' => 'Created',
272
+ '202' => 'Accepted',
273
+ '203' => 'Non-Authoritative Information',
274
+ '204' => 'No Content',
275
+ '205' => 'Reset Content',
276
+ '206' => 'Partial Content',
277
+ '207' => 'Multi-Status',
278
+ '208' => 'Already Reported',
279
+ '226' => 'IM Used',
280
+ '300' => 'Multiple Choices',
281
+ '301' => 'Moved Permanently',
282
+ '302' => 'Found',
283
+ '303' => 'See Other',
284
+ '304' => 'Not Modified',
285
+ '305' => 'Use Proxy',
286
+ '306' => 'Switch Proxy',
287
+ '307' => 'Temporary Redirect',
288
+ '308' => 'Permanent Redirect',
289
+ '400' => 'Bad Request',
290
+ '401' => 'Unauthorized',
291
+ '402' => 'Payment Required',
292
+ '403' => 'Forbidden',
293
+ '404' => 'Not Found',
294
+ '405' => 'Method Not Allowed',
295
+ '406' => 'Not Acceptable',
296
+ '407' => 'Proxy Authentication Required',
297
+ '408' => 'Request Timeout',
298
+ '409' => 'Conflict',
299
+ '410' => 'Gone',
300
+ '411' => 'Length Required',
301
+ '412' => 'Precondition Failed',
302
+ '413' => 'Payload Too Large',
303
+ '414' => 'URI Too Long',
304
+ '415' => 'Unsupported Media Type',
305
+ '416' => 'Range Not Satisfiable',
306
+ '417' => 'Expectation Failed',
307
+ '418' => 'I\'m a teapot',
308
+ '421' => 'Misdirected Request',
309
+ '422' => 'Unprocessable Entity',
310
+ '423' => 'Locked',
311
+ '424' => 'Failed Dependency',
312
+ '425' => 'Too Early',
313
+ '426' => 'Upgrade Required',
314
+ '428' => 'Precondition Required',
315
+ '429' => 'Too Many Requests',
316
+ '431' => 'Request Header Fields Too Large',
317
+ '451' => 'Unavailable For Legal Reasons',
318
+ '500' => 'Internal Server Error',
319
+ '501' => 'Not Implemented',
320
+ '502' => 'Bad Gateway',
321
+ '503' => 'Service Unavailable',
322
+ '504' => 'Gateway Timeout',
323
+ '505' => 'HTTP Version Not Supported',
324
+ '506' => 'Variant Also Negotiates',
325
+ '507' => 'Insufficient Storage',
326
+ '508' => 'Loop Detected',
327
+ '510' => 'Not Extended',
328
+ '511' => 'Network Authentication Required',
329
+ }.freeze
330
+ end
@@ -0,0 +1,113 @@
1
+ require 'json'
2
+
3
+ class Puppeteer::Response
4
+ include Puppeteer::IfPresent
5
+
6
+ class Redirected < StandardError
7
+ def initialize
8
+ super('Response body is unavailable for redirect responses')
9
+ end
10
+ end
11
+
12
+ # defines methods used only in NetworkManager
13
+ class InternalAccessor
14
+ def initialize(response)
15
+ @response = response
16
+ end
17
+
18
+ def body_loaded_promise
19
+ @response.instance_variable_get(:@body_loaded_promise)
20
+ end
21
+ end
22
+
23
+ class RemoteAddress
24
+ def initialize(ip:, port:)
25
+ @ip = ip
26
+ @port = port
27
+ end
28
+ attr_reader :ip, :port
29
+ end
30
+
31
+ # @param client [Puppeteer::CDPSession]
32
+ # @param request [Puppeteer::Request]
33
+ # @param response_payload [Hash]
34
+ def initialize(client, request, response_payload)
35
+ @client = client
36
+ @request = request
37
+
38
+ @body_loaded_promise = resolvable_future
39
+ @remote_address = RemoteAddress.new(
40
+ ip: response_payload['remoteIPAddress'],
41
+ port: response_payload['remotePort'],
42
+ )
43
+
44
+ @status = response_payload['status']
45
+ @status_text = response_payload['statusText']
46
+ @url = request.url
47
+ @from_disk_cache = !!response_payload['fromDiskCache']
48
+ @from_service_worker = !!response_payload['fromServiceWorker']
49
+
50
+ @headers = {}
51
+ response_payload['headers'].each do |key, value|
52
+ @headers[key.downcase] = value
53
+ end
54
+ @security_details = if_present(response_payload['securityDetails']) do |security_payload|
55
+ SecurityDetails.new(security_payload)
56
+ end
57
+
58
+ @internal = InternalAccessor.new(self)
59
+ end
60
+
61
+ attr_reader :internal
62
+
63
+ attr_reader :remote_address, :url, :status, :status_text, :headers, :security_details, :request
64
+
65
+ # @return [Boolean]
66
+ def ok?
67
+ @status == 0 || (@status >= 200 && @status <= 299)
68
+ end
69
+
70
+ def buffer
71
+ await @body_loaded_promise
72
+ response = @client.send_message('Network.getResponseBody', requestId: @request.internal.request_id)
73
+ if response['base64Encoded']
74
+ Base64.decode64(response['body'])
75
+ else
76
+ response['body']
77
+ end
78
+ end
79
+
80
+ # @param text [String]
81
+ def text
82
+ buffer
83
+ end
84
+
85
+ # @param json [Hash]
86
+ def json
87
+ JSON.parse(text)
88
+ end
89
+
90
+ def from_cache?
91
+ @from_disk_cache || @request.internal.from_memory_cache?
92
+ end
93
+
94
+ def from_service_worker?
95
+ @from_service_worker
96
+ end
97
+
98
+ def frame
99
+ @request.frame
100
+ end
101
+
102
+ class SecurityDetails
103
+ def initialize(security_payload)
104
+ @subject_name = security_payload['subjectName']
105
+ @issuer = security_payload['issuer']
106
+ @valid_from = security_payload['validFrom']
107
+ @valid_to = security_payload['validTo']
108
+ @protocol = security_payload['protocol']
109
+ end
110
+
111
+ attr_reader :subject_name, :issuer, :valid_from, :valid_to, :protocol
112
+ end
113
+ end