puppeteer-ruby 0.0.15 → 0.0.16

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9bff69308f1dc62011bd6278beb2883c5f16b42dfae0d138c8a442c0ed78cd93
4
- data.tar.gz: f336776cfc903db13391c11110650d8103e0eb291fa9a9d79366c9b0f9544b9e
3
+ metadata.gz: 38a6ae4190bd04cbacb863d04f9862ebc8a37c6811fbfd690426ba051d5ea5d4
4
+ data.tar.gz: 60b61e143e63ed5e42b0bb6f3d9e5c27262fd2f51064c185e491056a5081707d
5
5
  SHA512:
6
- metadata.gz: fe90923f1c31681ec8bc0c092a9ca59976d18f682a99491de57f764f76b35ce27482f47b08348adc84f0b09e215c8cfe788730cb53d776b4922d1e397509ab23
7
- data.tar.gz: ea4cb7f51c4944d082592c2a6fad51c69e8c3e6f356d6c5e6867cf9da3821249e346db7beee103e96a74f517ee929dcc1c865f79a07837a5f8c10efac357f748
6
+ metadata.gz: fc3ab652476ee319fab2d691173a0175d4b03d72346329f2fb855a142c3090693f1fb5cfa52abb4badbfbadc7d4eec5028a295f63fb7ddd3e362321564a96b7c
7
+ data.tar.gz: 6de2484b1b939c3cab1bfa48bb2225c54686f13e7901f3df086d8e5c7b18aa9fc62afb5fd9a785a3fbbf12d2aef50cde1d085f3564ce9afab996f80a39c64936
@@ -36,6 +36,8 @@ require 'puppeteer/mouse'
36
36
  require 'puppeteer/network_manager'
37
37
  require 'puppeteer/page'
38
38
  require 'puppeteer/remote_object'
39
+ require 'puppeteer/request'
40
+ require 'puppeteer/response'
39
41
  require 'puppeteer/target'
40
42
  require 'puppeteer/timeout_settings'
41
43
  require 'puppeteer/touch_screen'
@@ -208,28 +208,6 @@ class Puppeteer::Frame
208
208
 
209
209
  define_async_method :async_type_text
210
210
 
211
- # /**
212
- # * @param {(string|number|Function)} selectorOrFunctionOrTimeout
213
- # * @param {!Object=} options
214
- # * @param {!Array<*>} args
215
- # * @return {!Promise<?Puppeteer.JSHandle>}
216
- # */
217
- # waitFor(selectorOrFunctionOrTimeout, options = {}, ...args) {
218
- # const xPathPattern = '//';
219
-
220
- # if (helper.isString(selectorOrFunctionOrTimeout)) {
221
- # const string = /** @type {string} */ (selectorOrFunctionOrTimeout);
222
- # if (string.startsWith(xPathPattern))
223
- # return this.waitForXPath(string, options);
224
- # return this.waitForSelector(string, options);
225
- # }
226
- # if (helper.isNumber(selectorOrFunctionOrTimeout))
227
- # return new Promise(fulfill => setTimeout(fulfill, /** @type {number} */ (selectorOrFunctionOrTimeout)));
228
- # if (typeof selectorOrFunctionOrTimeout === 'function')
229
- # return this.waitForFunction(selectorOrFunctionOrTimeout, options, ...args);
230
- # return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout)));
231
- # }
232
-
233
211
  # @param selector [String]
234
212
  # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
235
213
  # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
@@ -1,5 +1,7 @@
1
1
  class Puppeteer::NetworkManager
2
+ include Puppeteer::DebugPrint
2
3
  include Puppeteer::EventCallbackable
4
+ include Puppeteer::IfPresent
3
5
 
4
6
  class Credentials
5
7
  # @param username [String|NilClass]
@@ -23,19 +25,39 @@ class Puppeteer::NetworkManager
23
25
  @request_id_to_request = {}
24
26
 
25
27
  # @type {!Map<string, !Protocol.Network.requestWillBeSentPayload>}
26
- @request_id_to_request_with_be_sent_event
28
+ @request_id_to_request_with_be_sent_event = {}
27
29
 
28
30
  @extra_http_headers = {}
29
31
 
30
32
  @offline = false
31
33
 
32
- # /** @type {!Set<string>} */
33
- # this._attemptedAuthentications = new Set();
34
+ @attempted_authentications = Set.new
34
35
  @user_request_interception_enabled = false
35
36
  @protocol_request_interception_enabled = false
36
37
  @user_cache_disabled = false
37
- # /** @type {!Map<string, string>} */
38
- # this._requestIdToInterceptionId = new Map();
38
+ @request_id_to_interception_id = {}
39
+
40
+ @client.on_event('Fetch.requestPaused') do |event|
41
+ handle_request_paused(event)
42
+ end
43
+ @client.on_event('Fetch.authRequired') do |event|
44
+ handle_auth_required(event)
45
+ end
46
+ @client.on_event('Network.requestWillBeSent') do |event|
47
+ handle_request_will_be_sent(event)
48
+ end
49
+ @client.on_event('Network.requestServedFromCache') do |event|
50
+ handle_request_served_from_cache(event)
51
+ end
52
+ @client.on_event('Network.responseReceived') do |event|
53
+ handle_response_received(event)
54
+ end
55
+ @client.on_event('Network.loadingFinished') do |event|
56
+ handle_loading_finished(event)
57
+ end
58
+ @client.on_event('Network.loadingFailed') do |event|
59
+ handle_loading_failed(event)
60
+ end
39
61
  end
40
62
 
41
63
  def init
@@ -119,4 +141,140 @@ class Puppeteer::NetworkManager
119
141
  cache_disabled = @user_cache_disabled || @protocol_request_interception_enabled
120
142
  @client.send_message('Network.setCacheDisabled', cacheDisabled: cache_disabled)
121
143
  end
144
+
145
+ private def handle_request_will_be_sent(event)
146
+ # Request interception doesn't happen for data URLs with Network Service.
147
+ if @protocol_request_interception_enabled && !event['request']['url'].start_with?('data:')
148
+ request_id = event['requestId']
149
+ interception_id = @request_id_to_interception_id.delete(request_id)
150
+ if interception_id
151
+ handle_request(event, interception_id)
152
+ else
153
+ @request_id_to_request_with_be_sent_event[request_id] = event
154
+ end
155
+ return
156
+ end
157
+ handle_request(event, nil)
158
+ end
159
+
160
+ private def handle_auth_required(event)
161
+ response = 'Default'
162
+ if @attempted_authentications.include?(event['requestId'])
163
+ response = 'CancelAuth'
164
+ elsif @credentials
165
+ response = 'ProvideCredentials'
166
+ @attempted_authentications << event['requestId']
167
+ end
168
+
169
+ username = @credentials&.username
170
+ password = @credentials&.password
171
+
172
+ begin
173
+ @client.send_message('Fetch.continueWithAuth',
174
+ requestId: event['requestId'],
175
+ authChallengeResponse: {
176
+ response: response,
177
+ username: username,
178
+ password: password,
179
+ },
180
+ )
181
+ rescue => err
182
+ debug_puts(err)
183
+ end
184
+ end
185
+
186
+ private def handle_request_paused(event)
187
+ if !@user_request_interception_enabled && @protocol_request_interception_enabled
188
+ begin
189
+ @client.send_message('Fetch.continueRequest', requestId: event['requestId'])
190
+ rescue => err
191
+ debug_puts(err)
192
+ end
193
+ end
194
+
195
+ request_id = event['networkId']
196
+ interception_id = event['requestId']
197
+ if request_id && (request_will_be_sent_event = @request_id_to_request_with_be_sent_event.delete(request_id))
198
+ handle_request(request_will_be_sent_event, interception_id)
199
+ else
200
+ @request_id_to_interception_id[request_id] = interception_id
201
+ end
202
+ end
203
+
204
+ private def handle_request(event, interception_id)
205
+ redirect_chain = []
206
+ if event['redirectResponse']
207
+ if_present(@request_id_to_request[event['requestId']]) do |request|
208
+ handle_request_redirect(request, event['redirectResponse'])
209
+ redirect_chain = request.internal.redirect_chain
210
+ end
211
+ end
212
+ frame = if_present(event['frameId']) { |frame_id| @frame_manager.frame(frame_id) }
213
+ request = Puppeteer::Request.new(@client, frame, interception_id, @user_request_interception_enabled, event, redirect_chain)
214
+ @request_id_to_request[event['requestId']] = request
215
+ emit_event 'Events.NetworkManager.Request', request
216
+ end
217
+
218
+ private def handle_request_served_from_cache(event)
219
+ if_present(@request_id_to_request[event['requestId']]) do |request|
220
+ request.internal.from_memory_cache = true
221
+ end
222
+ end
223
+
224
+ # @param request [Puppeteer::Request]
225
+ # @param response_payload [Hash]
226
+ private def handle_request_redirect(request, response_payload)
227
+ response = Puppeteer::Response.new(@client, request, response_payload)
228
+ request.internal.response = response
229
+ request.internal.redirect_chain << request
230
+ response.internal.body_loaded_promise.reject(Puppeteer::Response::Redirected.new)
231
+ @request_id_to_request.delete(request.internal.request_id)
232
+ @attempted_authentications.delete(request.internal.interception_id)
233
+ emit_event 'Events.NetworkManager.Response', response
234
+ emit_event 'Events.NetworkManager.RequestFinished', request
235
+ end
236
+
237
+ # @param event [Hash]
238
+ private def handle_response_received(event)
239
+ request = @request_id_to_request[event['requestId']]
240
+ # FileUpload sends a response without a matching request.
241
+ return unless request
242
+
243
+ response = Puppeteer::Response.new(@client, request, event['response'])
244
+ request.internal.response = response
245
+ emit_event 'Events.NetworkManager.Response', response
246
+ end
247
+
248
+ private def handle_loading_finished(event)
249
+ request = @request_id_to_request[event['requestId']]
250
+ # For certain requestIds we never receive requestWillBeSent event.
251
+ # @see https://crbug.com/750469
252
+ return unless request
253
+
254
+
255
+ # Under certain conditions we never get the Network.responseReceived
256
+ # event from protocol. @see https://crbug.com/883475
257
+ if_present(request.response) do |response|
258
+ response.internal.body_loaded_promise.fulfill(nil)
259
+ end
260
+
261
+ @request_id_to_request.delete(request.internal.request_id)
262
+ @attempted_authentications.delete(request.internal.interception_id)
263
+ emit_event 'Events.NetworkManager.RequestFinished', request
264
+ end
265
+
266
+ private def handle_loading_failed(event)
267
+ request = @request_id_to_request[event['requestId']]
268
+ # For certain requestIds we never receive requestWillBeSent event.
269
+ # @see https://crbug.com/750469
270
+ return unless request
271
+
272
+ request.internal.failure_text = event['errorText']
273
+ if_present(request.response) do |response|
274
+ response.internal.body_loaded_promise.fulfill(nil)
275
+ end
276
+ @request_id_to_request.delete(request.internal.request_id)
277
+ @attempted_authentications.delete(request.internal.interception_id)
278
+ emit_event 'Events.NetworkManager.RequestFailed', request
279
+ end
122
280
  end
@@ -689,41 +689,94 @@ class Puppeteer::Page
689
689
 
690
690
  define_async_method :async_wait_for_navigation
691
691
 
692
- # /**
693
- # * @param {(string|Function)} urlOrPredicate
694
- # * @param {!{timeout?: number}=} options
695
- # * @return {!Promise<!Puppeteer.Request>}
696
- # */
697
- # async waitForRequest(urlOrPredicate, options = {}) {
698
- # const {
699
- # timeout = this._timeoutSettings.timeout(),
700
- # } = options;
701
- # return helper.waitForEvent(this._frameManager.networkManager(), Events.NetworkManager.Request, request => {
702
- # if (helper.isString(urlOrPredicate))
703
- # return (urlOrPredicate === request.url());
704
- # if (typeof urlOrPredicate === 'function')
705
- # return !!(urlOrPredicate(request));
706
- # return false;
707
- # }, timeout, this._sessionClosePromise());
708
- # }
692
+ private def wait_for_network_manager_event(event_name, predicate:, timeout:)
693
+ option_timeout = timeout || @timeout_settings.timeout
709
694
 
710
- # /**
711
- # * @param {(string|Function)} urlOrPredicate
712
- # * @param {!{timeout?: number}=} options
713
- # * @return {!Promise<!Puppeteer.Response>}
714
- # */
715
- # async waitForResponse(urlOrPredicate, options = {}) {
716
- # const {
717
- # timeout = this._timeoutSettings.timeout(),
718
- # } = options;
719
- # return helper.waitForEvent(this._frameManager.networkManager(), Events.NetworkManager.Response, response => {
720
- # if (helper.isString(urlOrPredicate))
721
- # return (urlOrPredicate === response.url());
722
- # if (typeof urlOrPredicate === 'function')
723
- # return !!(urlOrPredicate(response));
724
- # return false;
725
- # }, timeout, this._sessionClosePromise());
726
- # }
695
+ @wait_for_network_manager_event_listener_ids ||= {}
696
+ if_present(@wait_for_network_manager_event_listener_ids[event_name]) do |listener_id|
697
+ @frame_manager.network_manager.remove_event_listener(listener_id)
698
+ end
699
+
700
+ promise = resolvable_future
701
+
702
+ @wait_for_network_manager_event_listener_ids[event_name] =
703
+ @frame_manager.network_manager.add_event_listener(event_name) do |event_target|
704
+ if predicate.call(event_target)
705
+ promise.fulfill(nil)
706
+ end
707
+ end
708
+
709
+ begin
710
+ Timeout.timeout(option_timeout / 1000.0) do
711
+ await_any(promise, session_close_promise)
712
+ end
713
+ rescue Timeout::Error
714
+ raise Puppeteer::TimeoutError.new("waiting for #{event_name} failed: timeout #{timeout}ms exceeded")
715
+ ensure
716
+ @frame_manager.network_manager.remove_event_listener(@wait_for_network_manager_event_listener_ids[event_name])
717
+ end
718
+ end
719
+
720
+ private def session_close_promise
721
+ @disconnect_promise ||= resolvable_future do |future|
722
+ @client.observe_first('Events.CDPSession.Disconnected') do
723
+ future.reject(Puppeteer::CDPSession::Error.new('Target Closed'))
724
+ end
725
+ end
726
+ end
727
+
728
+ # - Waits until request URL matches
729
+ # `wait_for_request(url: 'https://example.com/awesome')`
730
+ # - Waits until request matches the given predicate
731
+ # `wait_for_request(predicate: -> (req){ req.url.start_with?('https://example.com/search') })`
732
+ #
733
+ # @param url [String]
734
+ # @param predicate [Proc(Puppeteer::Request -> Boolean)]
735
+ private def wait_for_request(url: nil, predicate: nil, timeout: nil)
736
+ if !url && !predicate
737
+ raise ArgumentError.new('url or predicate must be specified')
738
+ end
739
+ if predicate && !predicate.is_a?(Proc)
740
+ raise ArgumentError.new('predicate must be a proc.')
741
+ end
742
+ request_predicate =
743
+ if url
744
+ -> (request) { request.url == url }
745
+ else
746
+ -> (request) { predicate.call(request) }
747
+ end
748
+
749
+ wait_for_network_manager_event('Events.NetworkManager.Request',
750
+ predicate: request_predicate,
751
+ timeout: timeout,
752
+ )
753
+ end
754
+
755
+ define_async_method :async_wait_for_request
756
+
757
+ # @param url [String]
758
+ # @param predicate [Proc(Puppeteer::Request -> Boolean)]
759
+ private def wait_for_response(url: nil, predicate: nil, timeout: nil)
760
+ if !url && !predicate
761
+ raise ArgumentError.new('url or predicate must be specified')
762
+ end
763
+ if predicate && !predicate.is_a?(Proc)
764
+ raise ArgumentError.new('predicate must be a proc.')
765
+ end
766
+ response_predicate =
767
+ if url
768
+ -> (response) { response.url == url }
769
+ else
770
+ -> (response) { predicate.call(response) }
771
+ end
772
+
773
+ wait_for_network_manager_event('Events.NetworkManager.Response',
774
+ predicate: response_predicate,
775
+ timeout: timeout,
776
+ )
777
+ end
778
+
779
+ define_async_method :async_wait_for_response
727
780
 
728
781
  # @param timeout [number|nil]
729
782
  # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
@@ -762,20 +815,19 @@ class Puppeteer::Page
762
815
  @client.send_message('Emulation.setScriptExecutionDisabled', value: !enabled)
763
816
  end
764
817
 
765
- # /**
766
- # * @param {boolean} enabled
767
- # */
768
- # async setBypassCSP(enabled) {
769
- # await this._client.send('Page.setBypassCSP', { enabled });
770
- # }
818
+ # @param enabled [Boolean]
819
+ def bypass_csp=(enabled)
820
+ @client.send_message('Page.setBypassCSP', enabled: enabled)
821
+ end
771
822
 
772
- # /**
773
- # * @param {?string} type
774
- # */
775
- # async emulateMediaType(type) {
776
- # assert(type === 'screen' || type === 'print' || type === null, 'Unsupported media type: ' + type);
777
- # await this._client.send('Emulation.setEmulatedMedia', {media: type || ''});
778
- # }
823
+ # @param media_type [String|Symbol|nil] either of (media, print, nil)
824
+ def emulate_media_type(media_type)
825
+ media_type_str = media_type.to_s
826
+ unless ['screen', 'print', ''].include?(media_type_str)
827
+ raise ArgumentError.new("Unsupported media type: #{media_type}")
828
+ end
829
+ @client.send_message('Emulation.setEmulatedMedia', media: media_type_str)
830
+ end
779
831
 
780
832
  # /**
781
833
  # * @param {?Array<MediaFeature>} features
@@ -795,7 +847,7 @@ class Puppeteer::Page
795
847
 
796
848
  # @param timezone_id [String?]
797
849
  def emulate_timezone(timezone_id)
798
- @client.send_message('Emulation.setTimezoneOverride', timezoneId: timezoneId || '')
850
+ @client.send_message('Emulation.setTimezoneOverride', timezoneId: timezone_id || '')
799
851
  rescue => err
800
852
  if err.message.include?('Invalid timezone')
801
853
  raise ArgumentError.new("Invalid timezone ID: #{timezone_id}")
@@ -1034,16 +1086,6 @@ class Puppeteer::Page
1034
1086
 
1035
1087
  define_async_method :async_type_text
1036
1088
 
1037
- # /**
1038
- # * @param {(string|number|Function)} selectorOrFunctionOrTimeout
1039
- # * @param {!{visible?: boolean, hidden?: boolean, timeout?: number, polling?: string|number}=} options
1040
- # * @param {!Array<*>} args
1041
- # * @return {!Promise<!Puppeteer.JSHandle>}
1042
- # */
1043
- # waitFor(selectorOrFunctionOrTimeout, options = {}, ...args) {
1044
- # return this.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args);
1045
- # }
1046
-
1047
1089
  # @param selector [String]
1048
1090
  # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
1049
1091
  # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
@@ -0,0 +1,336 @@
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
+ # ````
110
+ # page.on 'request' do |req|
111
+ # # Override headers
112
+ # headers = req.headers.merge(
113
+ # foo: 'bar', # set "foo" header
114
+ # origin: nil, # remove "origin" header
115
+ # )
116
+ # req.continue(headers: headers)
117
+ # end
118
+ # ```
119
+ #`
120
+ # @param error_code [String|Symbol]
121
+ def continue(url: nil, method: nil, post_data: nil, headers: nil)
122
+ # Request interception is not supported for data: urls.
123
+ return if @url.start_with?('data:')
124
+
125
+ unless @allow_interception
126
+ raise InterceptionNotEnabledError.new
127
+ end
128
+ if @interception_handled
129
+ raise AlreadyHandledError.new
130
+ end
131
+ @interception_handled = true
132
+
133
+ overrides = {
134
+ url: url,
135
+ method: method,
136
+ post_data: post_data,
137
+ headers: headers_to_array(headers),
138
+ }.compact
139
+ begin
140
+ @client.send_message('Fetch.continueRequest',
141
+ requestId: @interception_id,
142
+ **overrides,
143
+ )
144
+ rescue => err
145
+ # In certain cases, protocol will return error if the request was already canceled
146
+ # or the page was closed. We should tolerate these errors.
147
+ debug_puts(err)
148
+ end
149
+ end
150
+
151
+ # Mocking response.
152
+ #
153
+ # Example:
154
+ #
155
+ # ```
156
+ # page.on 'request' do |req|
157
+ # req.respond(
158
+ # status: 404,
159
+ # content_type: 'text/plain',
160
+ # body: 'Not Found!'
161
+ # )
162
+ # end
163
+ # ````
164
+ #
165
+ # @param status [Integer]
166
+ # @param headers [Hash<String, String>]
167
+ # @param content_type [String]
168
+ # @param body [String]
169
+ def respond(status: nil, headers: nil, content_type: nil, body: nil)
170
+ # Mocking responses for dataURL requests is not currently supported.
171
+ return if @url.start_with?('data:')
172
+
173
+ unless @allow_interception
174
+ raise InterceptionNotEnabledError.new
175
+ end
176
+ if @interception_handled
177
+ raise AlreadyHandledError.new
178
+ end
179
+ @interception_handled = true
180
+
181
+ mock_response_headers = {}
182
+ headers&.each do |key, value|
183
+ mock_response_headers[key.downcase] = value
184
+ end
185
+ if content_type
186
+ mock_response_headers['content-type'] = content_type
187
+ end
188
+ if body
189
+ mock_response_headers['content-length'] = body.length
190
+ end
191
+
192
+ mock_response = {
193
+ responseCode: status || 200,
194
+ responsePhrase: STATUS_TEXTS[(status || 200).to_s],
195
+ responseHeaders: headers_to_array(mock_response_headers),
196
+ body: if_present(body) { |mock_body| Base64.strict_encode64(mock_body) },
197
+ }.compact
198
+ begin
199
+ @client.send_message('Fetch.fulfillRequest',
200
+ requestId: @interception_id,
201
+ **mock_response,
202
+ )
203
+ rescue => err
204
+ # In certain cases, protocol will return error if the request was already canceled
205
+ # or the page was closed. We should tolerate these errors.
206
+ debug_puts(err)
207
+ end
208
+ end
209
+
210
+ # abort request on request interception.
211
+ #
212
+ # Example:
213
+ #
214
+ # ````
215
+ # page.on 'request' do |req|
216
+ # if req.url.include?("porn")
217
+ # req.abort
218
+ # else
219
+ # req.continue
220
+ # end
221
+ # end
222
+ # ```
223
+ #`
224
+ # @param error_code [String|Symbol]
225
+ def abort(error_code: :failed)
226
+ # Request interception is not supported for data: urls.
227
+ return if @url.start_with?('data:')
228
+
229
+ error_reason = ERROR_REASONS[error_code.to_s]
230
+ unless error_reason
231
+ raise ArgumentError.new("Unknown error code: #{error_code}")
232
+ end
233
+ unless @allow_interception
234
+ raise InterceptionNotEnabledError.new
235
+ end
236
+ if @interception_handled
237
+ raise AlreadyHandledError.new
238
+ end
239
+ @interception_handled = true
240
+
241
+ begin
242
+ @client.send_message('Fetch.failRequest',
243
+ requestId: @interception_id,
244
+ errorReason: error_reason,
245
+ )
246
+ rescue => err
247
+ # In certain cases, protocol will return error if the request was already canceled
248
+ # or the page was closed. We should tolerate these errors.
249
+ debug_puts(err)
250
+ end
251
+ end
252
+
253
+ ERROR_REASONS = {
254
+ 'aborted' => 'Aborted',
255
+ 'accessdenied' => 'AccessDenied',
256
+ 'addressunreachable' => 'AddressUnreachable',
257
+ 'blockedbyclient' => 'BlockedByClient',
258
+ 'blockedbyresponse' => 'BlockedByResponse',
259
+ 'connectionaborted' => 'ConnectionAborted',
260
+ 'connectionclosed' => 'ConnectionClosed',
261
+ 'connectionfailed' => 'ConnectionFailed',
262
+ 'connectionrefused' => 'ConnectionRefused',
263
+ 'connectionreset' => 'ConnectionReset',
264
+ 'internetdisconnected' => 'InternetDisconnected',
265
+ 'namenotresolved' => 'NameNotResolved',
266
+ 'timedout' => 'TimedOut',
267
+ 'failed' => 'Failed',
268
+ }.freeze
269
+
270
+ # List taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml with extra 306 and 418 codes.
271
+ STATUS_TEXTS = {
272
+ '100' => 'Continue',
273
+ '101' => 'Switching Protocols',
274
+ '102' => 'Processing',
275
+ '103' => 'Early Hints',
276
+ '200' => 'OK',
277
+ '201' => 'Created',
278
+ '202' => 'Accepted',
279
+ '203' => 'Non-Authoritative Information',
280
+ '204' => 'No Content',
281
+ '205' => 'Reset Content',
282
+ '206' => 'Partial Content',
283
+ '207' => 'Multi-Status',
284
+ '208' => 'Already Reported',
285
+ '226' => 'IM Used',
286
+ '300' => 'Multiple Choices',
287
+ '301' => 'Moved Permanently',
288
+ '302' => 'Found',
289
+ '303' => 'See Other',
290
+ '304' => 'Not Modified',
291
+ '305' => 'Use Proxy',
292
+ '306' => 'Switch Proxy',
293
+ '307' => 'Temporary Redirect',
294
+ '308' => 'Permanent Redirect',
295
+ '400' => 'Bad Request',
296
+ '401' => 'Unauthorized',
297
+ '402' => 'Payment Required',
298
+ '403' => 'Forbidden',
299
+ '404' => 'Not Found',
300
+ '405' => 'Method Not Allowed',
301
+ '406' => 'Not Acceptable',
302
+ '407' => 'Proxy Authentication Required',
303
+ '408' => 'Request Timeout',
304
+ '409' => 'Conflict',
305
+ '410' => 'Gone',
306
+ '411' => 'Length Required',
307
+ '412' => 'Precondition Failed',
308
+ '413' => 'Payload Too Large',
309
+ '414' => 'URI Too Long',
310
+ '415' => 'Unsupported Media Type',
311
+ '416' => 'Range Not Satisfiable',
312
+ '417' => 'Expectation Failed',
313
+ '418' => 'I\'m a teapot',
314
+ '421' => 'Misdirected Request',
315
+ '422' => 'Unprocessable Entity',
316
+ '423' => 'Locked',
317
+ '424' => 'Failed Dependency',
318
+ '425' => 'Too Early',
319
+ '426' => 'Upgrade Required',
320
+ '428' => 'Precondition Required',
321
+ '429' => 'Too Many Requests',
322
+ '431' => 'Request Header Fields Too Large',
323
+ '451' => 'Unavailable For Legal Reasons',
324
+ '500' => 'Internal Server Error',
325
+ '501' => 'Not Implemented',
326
+ '502' => 'Bad Gateway',
327
+ '503' => 'Service Unavailable',
328
+ '504' => 'Gateway Timeout',
329
+ '505' => 'HTTP Version Not Supported',
330
+ '506' => 'Variant Also Negotiates',
331
+ '507' => 'Insufficient Storage',
332
+ '508' => 'Loop Detected',
333
+ '510' => 'Not Extended',
334
+ '511' => 'Network Authentication Required',
335
+ }.freeze
336
+ 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
@@ -1,3 +1,3 @@
1
1
  class Puppeteer
2
- VERSION = '0.0.15'
2
+ VERSION = '0.0.16'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: puppeteer-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.15
4
+ version: 0.0.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - YusukeIwaki
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-06-28 00:00:00.000000000 Z
11
+ date: 2020-07-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -227,6 +227,8 @@ files:
227
227
  - lib/puppeteer/page/pdf_options.rb
228
228
  - lib/puppeteer/page/screenshot_options.rb
229
229
  - lib/puppeteer/remote_object.rb
230
+ - lib/puppeteer/request.rb
231
+ - lib/puppeteer/response.rb
230
232
  - lib/puppeteer/target.rb
231
233
  - lib/puppeteer/timeout_settings.rb
232
234
  - lib/puppeteer/touch_screen.rb