PluginProbe
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell / 2.8.16
WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell v2.8.16
3.13.3 3.13.2 3.13.1 3.13.0 3.12.13 3.12.12 3.12.11 3.12.10 3.12.9 3.12.8 3.12.7 3.12.6 3.12.5 3.12.4 3.12.3 3.12.1 3.12.2 3.12.0 3.11.1 3.11.0 3.10.9 3.10.8 3.10.7 3.10.6 2.8.16 All 261 releases
wpfunnels / vendor / symfony / browser-kit / AbstractBrowser.php

AbstractBrowser.php in WPFunnels – Funnel Builder for WooCommerce with Checkout & One Click Upsell 2.8.16, at vendor/symfony/browser-kit/AbstractBrowser.php

716 lines 22.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /*
4 * This file is part of the Symfony package.
5 *
6 * (c) Fabien Potencier <[email protected]>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace Symfony\Component\BrowserKit;
13
14 use Symfony\Component\BrowserKit\Exception\BadMethodCallException;
15 use Symfony\Component\DomCrawler\Crawler;
16 use Symfony\Component\DomCrawler\Form;
17 use Symfony\Component\DomCrawler\Link;
18 use Symfony\Component\Process\PhpProcess;
19
20 /**
21 * Simulates a browser.
22 *
23 * To make the actual request, you need to implement the doRequest() method.
24 *
25 * If you want to be able to run requests in their own process (insulated flag),
26 * you need to also implement the getScript() method.
27 *
28 * @author Fabien Potencier <[email protected]>
29 */
30 abstract class AbstractBrowser
31 {
32 protected $history;
33 protected $cookieJar;
34 protected $server = [];
35 protected $internalRequest;
36 protected $request;
37 protected $internalResponse;
38 protected $response;
39 protected $crawler;
40 protected $insulated = false;
41 protected $redirect;
42 protected $followRedirects = true;
43 protected $followMetaRefresh = false;
44
45 private $maxRedirects = -1;
46 private $redirectCount = 0;
47 private $redirects = [];
48 private $isMainRequest = true;
49
50 /**
51 * @param array $server The server parameters (equivalent of $_SERVER)
52 */
53 public function __construct(array $server = [], History $history = null, CookieJar $cookieJar = null)
54 {
55 $this->setServerParameters($server);
56 $this->history = $history ?? new History();
57 $this->cookieJar = $cookieJar ?? new CookieJar();
58 }
59
60 /**
61 * Sets whether to automatically follow redirects or not.
62 */
63 public function followRedirects(bool $followRedirects = true)
64 {
65 $this->followRedirects = $followRedirects;
66 }
67
68 /**
69 * Sets whether to automatically follow meta refresh redirects or not.
70 */
71 public function followMetaRefresh(bool $followMetaRefresh = true)
72 {
73 $this->followMetaRefresh = $followMetaRefresh;
74 }
75
76 /**
77 * Returns whether client automatically follows redirects or not.
78 *
79 * @return bool
80 */
81 public function isFollowingRedirects()
82 {
83 return $this->followRedirects;
84 }
85
86 /**
87 * Sets the maximum number of redirects that crawler can follow.
88 */
89 public function setMaxRedirects(int $maxRedirects)
90 {
91 $this->maxRedirects = $maxRedirects < 0 ? -1 : $maxRedirects;
92 $this->followRedirects = -1 != $this->maxRedirects;
93 }
94
95 /**
96 * Returns the maximum number of redirects that crawler can follow.
97 *
98 * @return int
99 */
100 public function getMaxRedirects()
101 {
102 return $this->maxRedirects;
103 }
104
105 /**
106 * Sets the insulated flag.
107 *
108 * @throws \RuntimeException When Symfony Process Component is not installed
109 */
110 public function insulate(bool $insulated = true)
111 {
112 if ($insulated && !class_exists(\Symfony\Component\Process\Process::class)) {
113 throw new \LogicException('Unable to isolate requests as the Symfony Process Component is not installed.');
114 }
115
116 $this->insulated = $insulated;
117 }
118
119 /**
120 * Sets server parameters.
121 */
122 public function setServerParameters(array $server)
123 {
124 $this->server = array_merge([
125 'HTTP_USER_AGENT' => 'Symfony BrowserKit',
126 ], $server);
127 }
128
129 /**
130 * Sets single server parameter.
131 */
132 public function setServerParameter(string $key, string $value)
133 {
134 $this->server[$key] = $value;
135 }
136
137 /**
138 * Gets single server parameter for specified key.
139 *
140 * @param mixed $default A default value when key is undefined
141 *
142 * @return mixed
143 */
144 public function getServerParameter(string $key, $default = '')
145 {
146 return $this->server[$key] ?? $default;
147 }
148
149 public function xmlHttpRequest(string $method, string $uri, array $parameters = [], array $files = [], array $server = [], string $content = null, bool $changeHistory = true): Crawler
150 {
151 $this->setServerParameter('HTTP_X_REQUESTED_WITH', 'XMLHttpRequest');
152
153 try {
154 return $this->request($method, $uri, $parameters, $files, $server, $content, $changeHistory);
155 } finally {
156 unset($this->server['HTTP_X_REQUESTED_WITH']);
157 }
158 }
159
160 /**
161 * Converts the request parameters into a JSON string and uses it as request content.
162 */
163 public function jsonRequest(string $method, string $uri, array $parameters = [], array $server = [], bool $changeHistory = true): Crawler
164 {
165 $content = json_encode($parameters);
166
167 $this->setServerParameter('CONTENT_TYPE', 'application/json');
168 $this->setServerParameter('HTTP_ACCEPT', 'application/json');
169
170 try {
171 return $this->request($method, $uri, [], [], $server, $content, $changeHistory);
172 } finally {
173 unset($this->server['CONTENT_TYPE']);
174 unset($this->server['HTTP_ACCEPT']);
175 }
176 }
177
178 /**
179 * Returns the History instance.
180 *
181 * @return History
182 */
183 public function getHistory()
184 {
185 return $this->history;
186 }
187
188 /**
189 * Returns the CookieJar instance.
190 *
191 * @return CookieJar
192 */
193 public function getCookieJar()
194 {
195 return $this->cookieJar;
196 }
197
198 /**
199 * Returns the current Crawler instance.
200 *
201 * @return Crawler
202 */
203 public function getCrawler()
204 {
205 if (null === $this->crawler) {
206 throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__));
207 }
208
209 return $this->crawler;
210 }
211
212 /**
213 * Returns the current BrowserKit Response instance.
214 *
215 * @return Response
216 */
217 public function getInternalResponse()
218 {
219 if (null === $this->internalResponse) {
220 throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__));
221 }
222
223 return $this->internalResponse;
224 }
225
226 /**
227 * Returns the current origin response instance.
228 *
229 * The origin response is the response instance that is returned
230 * by the code that handles requests.
231 *
232 * @return object
233 *
234 * @see doRequest()
235 */
236 public function getResponse()
237 {
238 if (null === $this->response) {
239 throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__));
240 }
241
242 return $this->response;
243 }
244
245 /**
246 * Returns the current BrowserKit Request instance.
247 *
248 * @return Request
249 */
250 public function getInternalRequest()
251 {
252 if (null === $this->internalRequest) {
253 throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__));
254 }
255
256 return $this->internalRequest;
257 }
258
259 /**
260 * Returns the current origin Request instance.
261 *
262 * The origin request is the request instance that is sent
263 * to the code that handles requests.
264 *
265 * @return object
266 *
267 * @see doRequest()
268 */
269 public function getRequest()
270 {
271 if (null === $this->request) {
272 throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__));
273 }
274
275 return $this->request;
276 }
277
278 /**
279 * Clicks on a given link.
280 *
281 * @return Crawler
282 */
283 public function click(Link $link)
284 {
285 if ($link instanceof Form) {
286 return $this->submit($link);
287 }
288
289 return $this->request($link->getMethod(), $link->getUri());
290 }
291
292 /**
293 * Clicks the first link (or clickable image) that contains the given text.
294 *
295 * @param string $linkText The text of the link or the alt attribute of the clickable image
296 */
297 public function clickLink(string $linkText): Crawler
298 {
299 if (null === $this->crawler) {
300 throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__));
301 }
302
303 return $this->click($this->crawler->selectLink($linkText)->link());
304 }
305
306 /**
307 * Submits a form.
308 *
309 * @param array $values An array of form field values
310 * @param array $serverParameters An array of server parameters
311 *
312 * @return Crawler
313 */
314 public function submit(Form $form, array $values = [], array $serverParameters = [])
315 {
316 $form->setValues($values);
317
318 return $this->request($form->getMethod(), $form->getUri(), $form->getPhpValues(), $form->getPhpFiles(), $serverParameters);
319 }
320
321 /**
322 * Finds the first form that contains a button with the given content and
323 * uses it to submit the given form field values.
324 *
325 * @param string $button The text content, id, value or name of the form <button> or <input type="submit">
326 * @param array $fieldValues Use this syntax: ['my_form[name]' => '...', 'my_form[email]' => '...']
327 * @param string $method The HTTP method used to submit the form
328 * @param array $serverParameters These values override the ones stored in $_SERVER (HTTP headers must include an HTTP_ prefix as PHP does)
329 */
330 public function submitForm(string $button, array $fieldValues = [], string $method = 'POST', array $serverParameters = []): Crawler
331 {
332 if (null === $this->crawler) {
333 throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__));
334 }
335
336 $buttonNode = $this->crawler->selectButton($button);
337 $form = $buttonNode->form($fieldValues, $method);
338
339 return $this->submit($form, [], $serverParameters);
340 }
341
342 /**
343 * Calls a URI.
344 *
345 * @param string $method The request method
346 * @param string $uri The URI to fetch
347 * @param array $parameters The Request parameters
348 * @param array $files The files
349 * @param array $server The server parameters (HTTP headers are referenced with an HTTP_ prefix as PHP does)
350 * @param string $content The raw body data
351 * @param bool $changeHistory Whether to update the history or not (only used internally for back(), forward(), and reload())
352 *
353 * @return Crawler
354 */
355 public function request(string $method, string $uri, array $parameters = [], array $files = [], array $server = [], string $content = null, bool $changeHistory = true)
356 {
357 if ($this->isMainRequest) {
358 $this->redirectCount = 0;
359 } else {
360 ++$this->redirectCount;
361 }
362
363 $originalUri = $uri;
364
365 $uri = $this->getAbsoluteUri($uri);
366
367 $server = array_merge($this->server, $server);
368
369 if (!empty($server['HTTP_HOST']) && null === parse_url($originalUri, \PHP_URL_HOST)) {
370 $uri = preg_replace('{^(https?\://)'.preg_quote($this->extractHost($uri)).'}', '${1}'.$server['HTTP_HOST'], $uri);
371 }
372
373 if (isset($server['HTTPS']) && null === parse_url($originalUri, \PHP_URL_SCHEME)) {
374 $uri = preg_replace('{^'.parse_url($uri, \PHP_URL_SCHEME).'}', $server['HTTPS'] ? 'https' : 'http', $uri);
375 }
376
377 if (!isset($server['HTTP_REFERER']) && !$this->history->isEmpty()) {
378 $server['HTTP_REFERER'] = $this->history->current()->getUri();
379 }
380
381 if (empty($server['HTTP_HOST'])) {
382 $server['HTTP_HOST'] = $this->extractHost($uri);
383 }
384
385 $server['HTTPS'] = 'https' == parse_url($uri, \PHP_URL_SCHEME);
386
387 $this->internalRequest = new Request($uri, $method, $parameters, $files, $this->cookieJar->allValues($uri), $server, $content);
388
389 $this->request = $this->filterRequest($this->internalRequest);
390
391 if (true === $changeHistory) {
392 $this->history->add($this->internalRequest);
393 }
394
395 if ($this->insulated) {
396 $this->response = $this->doRequestInProcess($this->request);
397 } else {
398 $this->response = $this->doRequest($this->request);
399 }
400
401 $this->internalResponse = $this->filterResponse($this->response);
402
403 $this->cookieJar->updateFromResponse($this->internalResponse, $uri);
404
405 $status = $this->internalResponse->getStatusCode();
406
407 if ($status >= 300 && $status < 400) {
408 $this->redirect = $this->internalResponse->getHeader('Location');
409 } else {
410 $this->redirect = null;
411 }
412
413 if ($this->followRedirects && $this->redirect) {
414 $this->redirects[serialize($this->history->current())] = true;
415
416 return $this->crawler = $this->followRedirect();
417 }
418
419 $this->crawler = $this->createCrawlerFromContent($this->internalRequest->getUri(), $this->internalResponse->getContent(), $this->internalResponse->getHeader('Content-Type') ?? '');
420
421 // Check for meta refresh redirect
422 if ($this->followMetaRefresh && null !== $redirect = $this->getMetaRefreshUrl()) {
423 $this->redirect = $redirect;
424 $this->redirects[serialize($this->history->current())] = true;
425 $this->crawler = $this->followRedirect();
426 }
427
428 return $this->crawler;
429 }
430
431 /**
432 * Makes a request in another process.
433 *
434 * @return object
435 *
436 * @throws \RuntimeException When processing returns exit code
437 */
438 protected function doRequestInProcess(object $request)
439 {
440 $deprecationsFile = tempnam(sys_get_temp_dir(), 'deprec');
441 putenv('SYMFONY_DEPRECATIONS_SERIALIZE='.$deprecationsFile);
442 $_ENV['SYMFONY_DEPRECATIONS_SERIALIZE'] = $deprecationsFile;
443 $process = new PhpProcess($this->getScript($request), null, null);
444 $process->run();
445
446 if (file_exists($deprecationsFile)) {
447 $deprecations = file_get_contents($deprecationsFile);
448 unlink($deprecationsFile);
449 foreach ($deprecations ? unserialize($deprecations) : [] as $deprecation) {
450 if ($deprecation[0]) {
451 // unsilenced on purpose
452 trigger_error($deprecation[1], \E_USER_DEPRECATED);
453 } else {
454 @trigger_error($deprecation[1], \E_USER_DEPRECATED);
455 }
456 }
457 }
458
459 if (!$process->isSuccessful() || !preg_match('/^O\:\d+\:/', $process->getOutput())) {
460 throw new \RuntimeException(sprintf('OUTPUT: %s ERROR OUTPUT: %s.', $process->getOutput(), $process->getErrorOutput()));
461 }
462
463 return unserialize($process->getOutput());
464 }
465
466 /**
467 * Makes a request.
468 *
469 * @return object
470 */
471 abstract protected function doRequest(object $request);
472
473 /**
474 * Returns the script to execute when the request must be insulated.
475 *
476 * @param object $request An origin request instance
477 *
478 * @throws \LogicException When this abstract class is not implemented
479 */
480 protected function getScript(object $request)
481 {
482 throw new \LogicException('To insulate requests, you need to override the getScript() method.');
483 }
484
485 /**
486 * Filters the BrowserKit request to the origin one.
487 *
488 * @return object
489 */
490 protected function filterRequest(Request $request)
491 {
492 return $request;
493 }
494
495 /**
496 * Filters the origin response to the BrowserKit one.
497 *
498 * @return Response
499 */
500 protected function filterResponse(object $response)
501 {
502 return $response;
503 }
504
505 /**
506 * Creates a crawler.
507 *
508 * This method returns null if the DomCrawler component is not available.
509 *
510 * @return Crawler|null
511 */
512 protected function createCrawlerFromContent(string $uri, string $content, string $type)
513 {
514 if (!class_exists(Crawler::class)) {
515 return null;
516 }
517
518 $crawler = new Crawler(null, $uri);
519 $crawler->addContent($content, $type);
520
521 return $crawler;
522 }
523
524 /**
525 * Goes back in the browser history.
526 *
527 * @return Crawler
528 */
529 public function back()
530 {
531 do {
532 $request = $this->history->back();
533 } while (\array_key_exists(serialize($request), $this->redirects));
534
535 return $this->requestFromRequest($request, false);
536 }
537
538 /**
539 * Goes forward in the browser history.
540 *
541 * @return Crawler
542 */
543 public function forward()
544 {
545 do {
546 $request = $this->history->forward();
547 } while (\array_key_exists(serialize($request), $this->redirects));
548
549 return $this->requestFromRequest($request, false);
550 }
551
552 /**
553 * Reloads the current browser.
554 *
555 * @return Crawler
556 */
557 public function reload()
558 {
559 return $this->requestFromRequest($this->history->current(), false);
560 }
561
562 /**
563 * Follow redirects?
564 *
565 * @return Crawler
566 *
567 * @throws \LogicException If request was not a redirect
568 */
569 public function followRedirect()
570 {
571 if (empty($this->redirect)) {
572 throw new \LogicException('The request was not redirected.');
573 }
574
575 if (-1 !== $this->maxRedirects) {
576 if ($this->redirectCount > $this->maxRedirects) {
577 $this->redirectCount = 0;
578 throw new \LogicException(sprintf('The maximum number (%d) of redirections was reached.', $this->maxRedirects));
579 }
580 }
581
582 $request = $this->internalRequest;
583
584 if (\in_array($this->internalResponse->getStatusCode(), [301, 302, 303])) {
585 $method = 'GET';
586 $files = [];
587 $content = null;
588 } else {
589 $method = $request->getMethod();
590 $files = $request->getFiles();
591 $content = $request->getContent();
592 }
593
594 if ('GET' === strtoupper($method)) {
595 // Don't forward parameters for GET request as it should reach the redirection URI
596 $parameters = [];
597 } else {
598 $parameters = $request->getParameters();
599 }
600
601 $server = $request->getServer();
602 $server = $this->updateServerFromUri($server, $this->redirect);
603
604 $this->isMainRequest = false;
605
606 $response = $this->request($method, $this->redirect, $parameters, $files, $server, $content);
607
608 $this->isMainRequest = true;
609
610 return $response;
611 }
612
613 /**
614 * @see https://dev.w3.org/html5/spec-preview/the-meta-element.html#attr-meta-http-equiv-refresh
615 */
616 private function getMetaRefreshUrl(): ?string
617 {
618 $metaRefresh = $this->getCrawler()->filter('head meta[http-equiv="refresh"]');
619 foreach ($metaRefresh->extract(['content']) as $content) {
620 if (preg_match('/^\s*0\s*;\s*URL\s*=\s*(?|\'([^\']++)|"([^"]++)|([^\'"].*))/i', $content, $m)) {
621 return str_replace("\t\r\n", '', rtrim($m[1]));
622 }
623 }
624
625 return null;
626 }
627
628 /**
629 * Restarts the client.
630 *
631 * It flushes history and all cookies.
632 */
633 public function restart()
634 {
635 $this->cookieJar->clear();
636 $this->history->clear();
637 }
638
639 /**
640 * Takes a URI and converts it to absolute if it is not already absolute.
641 *
642 * @return string
643 */
644 protected function getAbsoluteUri(string $uri)
645 {
646 // already absolute?
647 if (0 === strpos($uri, 'http://') || 0 === strpos($uri, 'https://')) {
648 return $uri;
649 }
650
651 if (!$this->history->isEmpty()) {
652 $currentUri = $this->history->current()->getUri();
653 } else {
654 $currentUri = sprintf('http%s://%s/',
655 isset($this->server['HTTPS']) ? 's' : '',
656 $this->server['HTTP_HOST'] ?? 'localhost'
657 );
658 }
659
660 // protocol relative URL
661 if ('' !== trim($uri, '/') && str_starts_with($uri, '//')) {
662 return parse_url($currentUri, \PHP_URL_SCHEME).':'.$uri;
663 }
664
665 // anchor or query string parameters?
666 if (!$uri || '#' == $uri[0] || '?' == $uri[0]) {
667 return preg_replace('/[#?].*?$/', '', $currentUri).$uri;
668 }
669
670 if ('/' !== $uri[0]) {
671 $path = parse_url($currentUri, \PHP_URL_PATH);
672
673 if ('/' !== substr($path, -1)) {
674 $path = substr($path, 0, strrpos($path, '/') + 1);
675 }
676
677 $uri = $path.$uri;
678 }
679
680 return preg_replace('#^(.*?//[^/]+)\/.*$#', '$1', $currentUri).$uri;
681 }
682
683 /**
684 * Makes a request from a Request object directly.
685 *
686 * @param bool $changeHistory Whether to update the history or not (only used internally for back(), forward(), and reload())
687 *
688 * @return Crawler
689 */
690 protected function requestFromRequest(Request $request, bool $changeHistory = true)
691 {
692 return $this->request($request->getMethod(), $request->getUri(), $request->getParameters(), $request->getFiles(), $request->getServer(), $request->getContent(), $changeHistory);
693 }
694
695 private function updateServerFromUri(array $server, string $uri): array
696 {
697 $server['HTTP_HOST'] = $this->extractHost($uri);
698 $scheme = parse_url($uri, \PHP_URL_SCHEME);
699 $server['HTTPS'] = null === $scheme ? $server['HTTPS'] : 'https' == $scheme;
700 unset($server['HTTP_IF_NONE_MATCH'], $server['HTTP_IF_MODIFIED_SINCE']);
701
702 return $server;
703 }
704
705 private function extractHost(string $uri): ?string
706 {
707 $host = parse_url($uri, \PHP_URL_HOST);
708
709 if ($port = parse_url($uri, \PHP_URL_PORT)) {
710 return $host.':'.$port;
711 }
712
713 return $host;
714 }
715 }
716