1<?php
2
3namespace Facebook\WebDriver\Remote;
4
5use Facebook\WebDriver\Interactions\WebDriverActions;
6use Facebook\WebDriver\JavaScriptExecutor;
7use Facebook\WebDriver\WebDriver;
8use Facebook\WebDriver\WebDriverBy;
9use Facebook\WebDriver\WebDriverCapabilities;
10use Facebook\WebDriver\WebDriverCommandExecutor;
11use Facebook\WebDriver\WebDriverElement;
12use Facebook\WebDriver\WebDriverHasInputDevices;
13use Facebook\WebDriver\WebDriverNavigation;
14use Facebook\WebDriver\WebDriverOptions;
15use Facebook\WebDriver\WebDriverWait;
16
17class RemoteWebDriver implements WebDriver, JavaScriptExecutor, WebDriverHasInputDevices
18{
19    /**
20     * @var HttpCommandExecutor|null
21     */
22    protected $executor;
23    /**
24     * @var WebDriverCapabilities
25     */
26    protected $capabilities;
27
28    /**
29     * @var string
30     */
31    protected $sessionID;
32    /**
33     * @var RemoteMouse
34     */
35    protected $mouse;
36    /**
37     * @var RemoteKeyboard
38     */
39    protected $keyboard;
40    /**
41     * @var RemoteTouchScreen
42     */
43    protected $touch;
44    /**
45     * @var RemoteExecuteMethod
46     */
47    protected $executeMethod;
48    /**
49     * @var bool
50     */
51    protected $isW3cCompliant;
52
53    /**
54     * @param HttpCommandExecutor $commandExecutor
55     * @param string $sessionId
56     * @param WebDriverCapabilities|null $capabilities
57     * @param bool $isW3cCompliant false to use the legacy JsonWire protocol, true for the W3C WebDriver spec
58     */
59    protected function __construct(
60        HttpCommandExecutor $commandExecutor,
61        $sessionId,
62        WebDriverCapabilities $capabilities = null,
63        $isW3cCompliant = false
64    ) {
65        $this->executor = $commandExecutor;
66        $this->sessionID = $sessionId;
67        $this->isW3cCompliant = $isW3cCompliant;
68
69        if ($capabilities !== null) {
70            $this->capabilities = $capabilities;
71        }
72    }
73
74    /**
75     * Construct the RemoteWebDriver by a desired capabilities.
76     *
77     * @param string $selenium_server_url The url of the remote Selenium WebDriver server
78     * @param DesiredCapabilities|array $desired_capabilities The desired capabilities
79     * @param int|null $connection_timeout_in_ms Set timeout for the connect phase to remote Selenium WebDriver server
80     * @param int|null $request_timeout_in_ms Set the maximum time of a request to remote Selenium WebDriver server
81     * @param string|null $http_proxy The proxy to tunnel requests to the remote Selenium WebDriver through
82     * @param int|null $http_proxy_port The proxy port to tunnel requests to the remote Selenium WebDriver through
83     * @param DesiredCapabilities $required_capabilities The required capabilities
84     *
85     * @return static
86     */
87    public static function create(
88        $selenium_server_url = 'http://localhost:4444/wd/hub',
89        $desired_capabilities = null,
90        $connection_timeout_in_ms = null,
91        $request_timeout_in_ms = null,
92        $http_proxy = null,
93        $http_proxy_port = null,
94        DesiredCapabilities $required_capabilities = null
95    ) {
96        $selenium_server_url = preg_replace('#/+$#', '', $selenium_server_url);
97
98        $desired_capabilities = self::castToDesiredCapabilitiesObject($desired_capabilities);
99
100        $executor = new HttpCommandExecutor($selenium_server_url, $http_proxy, $http_proxy_port);
101        if ($connection_timeout_in_ms !== null) {
102            $executor->setConnectionTimeout($connection_timeout_in_ms);
103        }
104        if ($request_timeout_in_ms !== null) {
105            $executor->setRequestTimeout($request_timeout_in_ms);
106        }
107
108        // W3C
109        $parameters = [
110            'capabilities' => [
111                'firstMatch' => [(object) $desired_capabilities->toW3cCompatibleArray()],
112            ],
113        ];
114
115        if ($required_capabilities !== null && !empty($required_capabilities->toArray())) {
116            $parameters['capabilities']['alwaysMatch'] = (object) $required_capabilities->toW3cCompatibleArray();
117        }
118
119        // Legacy protocol
120        if ($required_capabilities !== null) {
121            // TODO: Selenium (as of v3.0.1) does accept requiredCapabilities only as a property of desiredCapabilities.
122            // This has changed with the W3C WebDriver spec, but is the only way how to pass these
123            // values with the legacy protocol.
124            $desired_capabilities->setCapability('requiredCapabilities', (object) $required_capabilities->toArray());
125        }
126
127        $parameters['desiredCapabilities'] = (object) $desired_capabilities->toArray();
128
129        $command = WebDriverCommand::newSession($parameters);
130
131        $response = $executor->execute($command);
132
133        return static::createFromResponse($response, $executor);
134    }
135
136    /**
137     * [Experimental] Construct the RemoteWebDriver by an existing session.
138     *
139     * This constructor can boost the performance a lot by reusing the same browser for the whole test suite.
140     * You cannot pass the desired capabilities because the session was created before.
141     *
142     * @param string $selenium_server_url The url of the remote Selenium WebDriver server
143     * @param string $session_id The existing session id
144     * @param int|null $connection_timeout_in_ms Set timeout for the connect phase to remote Selenium WebDriver server
145     * @param int|null $request_timeout_in_ms Set the maximum time of a request to remote Selenium WebDriver server
146     * @param bool $isW3cCompliant True to use W3C WebDriver (default), false to use the legacy JsonWire protocol
147     * @return static
148     */
149    public static function createBySessionID(
150        $session_id,
151        $selenium_server_url = 'http://localhost:4444/wd/hub',
152        $connection_timeout_in_ms = null,
153        $request_timeout_in_ms = null
154    ) {
155        // BC layer to not break the method signature
156        $isW3cCompliant = func_num_args() > 4 ? func_get_arg(4) : true;
157
158        $executor = new HttpCommandExecutor($selenium_server_url, null, null);
159        if ($connection_timeout_in_ms !== null) {
160            $executor->setConnectionTimeout($connection_timeout_in_ms);
161        }
162        if ($request_timeout_in_ms !== null) {
163            $executor->setRequestTimeout($request_timeout_in_ms);
164        }
165
166        if (!$isW3cCompliant) {
167            $executor->disableW3cCompliance();
168        }
169
170        return new static($executor, $session_id, null, $isW3cCompliant);
171    }
172
173    /**
174     * Close the current window.
175     *
176     * @return RemoteWebDriver The current instance.
177     */
178    public function close()
179    {
180        $this->execute(DriverCommand::CLOSE, []);
181
182        return $this;
183    }
184
185    /**
186     * Create a new top-level browsing context.
187     *
188     * @codeCoverageIgnore
189     * @deprecated Use $driver->switchTo()->newWindow()
190     * @return WebDriver The current instance.
191     */
192    public function newWindow()
193    {
194        return $this->switchTo()->newWindow();
195    }
196
197    /**
198     * Find the first WebDriverElement using the given mechanism.
199     *
200     * @param WebDriverBy $by
201     * @return RemoteWebElement NoSuchElementException is thrown in HttpCommandExecutor if no element is found.
202     * @see WebDriverBy
203     */
204    public function findElement(WebDriverBy $by)
205    {
206        $raw_element = $this->execute(
207            DriverCommand::FIND_ELEMENT,
208            JsonWireCompat::getUsing($by, $this->isW3cCompliant)
209        );
210
211        return $this->newElement(JsonWireCompat::getElement($raw_element));
212    }
213
214    /**
215     * Find all WebDriverElements within the current page using the given mechanism.
216     *
217     * @param WebDriverBy $by
218     * @return RemoteWebElement[] A list of all WebDriverElements, or an empty array if nothing matches
219     * @see WebDriverBy
220     */
221    public function findElements(WebDriverBy $by)
222    {
223        $raw_elements = $this->execute(
224            DriverCommand::FIND_ELEMENTS,
225            JsonWireCompat::getUsing($by, $this->isW3cCompliant)
226        );
227
228        $elements = [];
229        foreach ($raw_elements as $raw_element) {
230            $elements[] = $this->newElement(JsonWireCompat::getElement($raw_element));
231        }
232
233        return $elements;
234    }
235
236    /**
237     * Load a new web page in the current browser window.
238     *
239     * @param string $url
240     *
241     * @return RemoteWebDriver The current instance.
242     */
243    public function get($url)
244    {
245        $params = ['url' => (string) $url];
246        $this->execute(DriverCommand::GET, $params);
247
248        return $this;
249    }
250
251    /**
252     * Get a string representing the current URL that the browser is looking at.
253     *
254     * @return string The current URL.
255     */
256    public function getCurrentURL()
257    {
258        return $this->execute(DriverCommand::GET_CURRENT_URL);
259    }
260
261    /**
262     * Get the source of the last loaded page.
263     *
264     * @return string The current page source.
265     */
266    public function getPageSource()
267    {
268        return $this->execute(DriverCommand::GET_PAGE_SOURCE);
269    }
270
271    /**
272     * Get the title of the current page.
273     *
274     * @return string The title of the current page.
275     */
276    public function getTitle()
277    {
278        return $this->execute(DriverCommand::GET_TITLE);
279    }
280
281    /**
282     * Return an opaque handle to this window that uniquely identifies it within this driver instance.
283     *
284     * @return string The current window handle.
285     */
286    public function getWindowHandle()
287    {
288        return $this->execute(
289            DriverCommand::GET_CURRENT_WINDOW_HANDLE,
290            []
291        );
292    }
293
294    /**
295     * Get all window handles available to the current session.
296     *
297     * Note: Do not use `end($driver->getWindowHandles())` to find the last open window, for proper solution see:
298     * https://github.com/php-webdriver/php-webdriver/wiki/Alert,-tabs,-frames,-iframes#switch-to-the-new-window
299     *
300     * @return array An array of string containing all available window handles.
301     */
302    public function getWindowHandles()
303    {
304        return $this->execute(DriverCommand::GET_WINDOW_HANDLES, []);
305    }
306
307    /**
308     * Quits this driver, closing every associated window.
309     */
310    public function quit()
311    {
312        $this->execute(DriverCommand::QUIT);
313        $this->executor = null;
314    }
315
316    /**
317     * Inject a snippet of JavaScript into the page for execution in the context of the currently selected frame.
318     * The executed script is assumed to be synchronous and the result of evaluating the script will be returned.
319     *
320     * @param string $script The script to inject.
321     * @param array $arguments The arguments of the script.
322     * @return mixed The return value of the script.
323     */
324    public function executeScript($script, array $arguments = [])
325    {
326        $params = [
327            'script' => $script,
328            'args' => $this->prepareScriptArguments($arguments),
329        ];
330
331        return $this->execute(DriverCommand::EXECUTE_SCRIPT, $params);
332    }
333
334    /**
335     * Inject a snippet of JavaScript into the page for asynchronous execution in the context of the currently selected
336     * frame.
337     *
338     * The driver will pass a callback as the last argument to the snippet, and block until the callback is invoked.
339     *
340     * You may need to define script timeout using `setScriptTimeout()` method of `WebDriverTimeouts` first.
341     *
342     * @param string $script The script to inject.
343     * @param array $arguments The arguments of the script.
344     * @return mixed The value passed by the script to the callback.
345     */
346    public function executeAsyncScript($script, array $arguments = [])
347    {
348        $params = [
349            'script' => $script,
350            'args' => $this->prepareScriptArguments($arguments),
351        ];
352
353        return $this->execute(
354            DriverCommand::EXECUTE_ASYNC_SCRIPT,
355            $params
356        );
357    }
358
359    /**
360     * Take a screenshot of the current page.
361     *
362     * @param string $save_as The path of the screenshot to be saved.
363     * @return string The screenshot in PNG format.
364     */
365    public function takeScreenshot($save_as = null)
366    {
367        $screenshot = base64_decode($this->execute(DriverCommand::SCREENSHOT), true);
368
369        if ($save_as !== null) {
370            $directoryPath = dirname($save_as);
371
372            if (!file_exists($directoryPath)) {
373                mkdir($directoryPath, 0777, true);
374            }
375
376            file_put_contents($save_as, $screenshot);
377        }
378
379        return $screenshot;
380    }
381
382    /**
383     * Status returns information about whether a remote end is in a state in which it can create new sessions.
384     */
385    public function getStatus()
386    {
387        $response = $this->execute(DriverCommand::STATUS);
388
389        return RemoteStatus::createFromResponse($response);
390    }
391
392    /**
393     * Construct a new WebDriverWait by the current WebDriver instance.
394     * Sample usage:
395     *
396     * ```
397     *   $driver->wait(20, 1000)->until(
398     *     WebDriverExpectedCondition::titleIs('WebDriver Page')
399     *   );
400     * ```
401     * @param int $timeout_in_second
402     * @param int $interval_in_millisecond
403     *
404     * @return WebDriverWait
405     */
406    public function wait($timeout_in_second = 30, $interval_in_millisecond = 250)
407    {
408        return new WebDriverWait(
409            $this,
410            $timeout_in_second,
411            $interval_in_millisecond
412        );
413    }
414
415    /**
416     * An abstraction for managing stuff you would do in a browser menu. For example, adding and deleting cookies.
417     *
418     * @return WebDriverOptions
419     */
420    public function manage()
421    {
422        return new WebDriverOptions($this->getExecuteMethod(), $this->isW3cCompliant);
423    }
424
425    /**
426     * An abstraction allowing the driver to access the browser's history and to navigate to a given URL.
427     *
428     * @return WebDriverNavigation
429     * @see WebDriverNavigation
430     */
431    public function navigate()
432    {
433        return new WebDriverNavigation($this->getExecuteMethod());
434    }
435
436    /**
437     * Switch to a different window or frame.
438     *
439     * @return RemoteTargetLocator
440     * @see RemoteTargetLocator
441     */
442    public function switchTo()
443    {
444        return new RemoteTargetLocator($this->getExecuteMethod(), $this, $this->isW3cCompliant);
445    }
446
447    /**
448     * @return RemoteMouse
449     */
450    public function getMouse()
451    {
452        if (!$this->mouse) {
453            $this->mouse = new RemoteMouse($this->getExecuteMethod(), $this->isW3cCompliant);
454        }
455
456        return $this->mouse;
457    }
458
459    /**
460     * @return RemoteKeyboard
461     */
462    public function getKeyboard()
463    {
464        if (!$this->keyboard) {
465            $this->keyboard = new RemoteKeyboard($this->getExecuteMethod(), $this, $this->isW3cCompliant);
466        }
467
468        return $this->keyboard;
469    }
470
471    /**
472     * @return RemoteTouchScreen
473     */
474    public function getTouch()
475    {
476        if (!$this->touch) {
477            $this->touch = new RemoteTouchScreen($this->getExecuteMethod());
478        }
479
480        return $this->touch;
481    }
482
483    /**
484     * Construct a new action builder.
485     *
486     * @return WebDriverActions
487     */
488    public function action()
489    {
490        return new WebDriverActions($this);
491    }
492
493    /**
494     * Set the command executor of this RemoteWebdriver
495     *
496     * @deprecated To be removed in the future. Executor should be passed in the constructor.
497     * @internal
498     * @codeCoverageIgnore
499     * @param WebDriverCommandExecutor $executor Despite the typehint, it have be an instance of HttpCommandExecutor.
500     * @return RemoteWebDriver
501     */
502    public function setCommandExecutor(WebDriverCommandExecutor $executor)
503    {
504        $this->executor = $executor;
505
506        return $this;
507    }
508
509    /**
510     * Get the command executor of this RemoteWebdriver
511     *
512     * @return HttpCommandExecutor
513     */
514    public function getCommandExecutor()
515    {
516        return $this->executor;
517    }
518
519    /**
520     * Set the session id of the RemoteWebDriver.
521     *
522     * @deprecated To be removed in the future. Session ID should be passed in the constructor.
523     * @internal
524     * @codeCoverageIgnore
525     * @param string $session_id
526     * @return RemoteWebDriver
527     */
528    public function setSessionID($session_id)
529    {
530        $this->sessionID = $session_id;
531
532        return $this;
533    }
534
535    /**
536     * Get current selenium sessionID
537     *
538     * @return string
539     */
540    public function getSessionID()
541    {
542        return $this->sessionID;
543    }
544
545    /**
546     * Get capabilities of the RemoteWebDriver.
547     *
548     * @return WebDriverCapabilities
549     */
550    public function getCapabilities()
551    {
552        return $this->capabilities;
553    }
554
555    /**
556     * Returns a list of the currently active sessions.
557     *
558     * @param string $selenium_server_url The url of the remote Selenium WebDriver server
559     * @param int $timeout_in_ms
560     * @return array
561     */
562    public static function getAllSessions($selenium_server_url = 'http://localhost:4444/wd/hub', $timeout_in_ms = 30000)
563    {
564        $executor = new HttpCommandExecutor($selenium_server_url, null, null);
565        $executor->setConnectionTimeout($timeout_in_ms);
566
567        $command = new WebDriverCommand(
568            null,
569            DriverCommand::GET_ALL_SESSIONS,
570            []
571        );
572
573        return $executor->execute($command)->getValue();
574    }
575
576    public function execute($command_name, $params = [])
577    {
578        $command = new WebDriverCommand(
579            $this->sessionID,
580            $command_name,
581            $params
582        );
583
584        if ($this->executor) {
585            $response = $this->executor->execute($command);
586
587            return $response->getValue();
588        }
589
590        return null;
591    }
592
593    /**
594     * Execute custom commands on remote end.
595     * For example vendor-specific commands or other commands not implemented by php-webdriver.
596     *
597     * @see https://github.com/php-webdriver/php-webdriver/wiki/Custom-commands
598     * @param string $endpointUrl
599     * @param string $method
600     * @param array $params
601     * @return mixed|null
602     */
603    public function executeCustomCommand($endpointUrl, $method = 'GET', $params = [])
604    {
605        $command = new CustomWebDriverCommand(
606            $this->sessionID,
607            $endpointUrl,
608            $method,
609            $params
610        );
611
612        if ($this->executor) {
613            $response = $this->executor->execute($command);
614
615            return $response->getValue();
616        }
617
618        return null;
619    }
620
621    /**
622     * @internal
623     * @return bool
624     */
625    public function isW3cCompliant()
626    {
627        return $this->isW3cCompliant;
628    }
629
630    /**
631     * Create instance based on response to NEW_SESSION command.
632     * Also detect W3C/OSS dialect and setup the driver/executor accordingly.
633     *
634     * @internal
635     * @return static
636     */
637    protected static function createFromResponse(WebDriverResponse $response, HttpCommandExecutor $commandExecutor)
638    {
639        $responseValue = $response->getValue();
640
641        if (!$isW3cCompliant = isset($responseValue['capabilities'])) {
642            $commandExecutor->disableW3cCompliance();
643        }
644
645        if ($isW3cCompliant) {
646            $returnedCapabilities = DesiredCapabilities::createFromW3cCapabilities($responseValue['capabilities']);
647        } else {
648            $returnedCapabilities = new DesiredCapabilities($responseValue);
649        }
650
651        return new static($commandExecutor, $response->getSessionID(), $returnedCapabilities, $isW3cCompliant);
652    }
653
654    /**
655     * Prepare arguments for JavaScript injection
656     *
657     * @param array $arguments
658     * @return array
659     */
660    protected function prepareScriptArguments(array $arguments)
661    {
662        $args = [];
663        foreach ($arguments as $key => $value) {
664            if ($value instanceof WebDriverElement) {
665                $args[$key] = [
666                    $this->isW3cCompliant ?
667                        JsonWireCompat::WEB_DRIVER_ELEMENT_IDENTIFIER
668                        : 'ELEMENT' => $value->getID(),
669                ];
670            } else {
671                if (is_array($value)) {
672                    $value = $this->prepareScriptArguments($value);
673                }
674                $args[$key] = $value;
675            }
676        }
677
678        return $args;
679    }
680
681    /**
682     * @return RemoteExecuteMethod
683     */
684    protected function getExecuteMethod()
685    {
686        if (!$this->executeMethod) {
687            $this->executeMethod = new RemoteExecuteMethod($this);
688        }
689
690        return $this->executeMethod;
691    }
692
693    /**
694     * Return the WebDriverElement with the given id.
695     *
696     * @param string $id The id of the element to be created.
697     * @return RemoteWebElement
698     */
699    protected function newElement($id)
700    {
701        return new RemoteWebElement($this->getExecuteMethod(), $id, $this->isW3cCompliant);
702    }
703
704    /**
705     * Cast legacy types (array or null) to DesiredCapabilities object. To be removed in future when instance of
706     * DesiredCapabilities will be required.
707     *
708     * @param array|DesiredCapabilities|null $desired_capabilities
709     * @return DesiredCapabilities
710     */
711    protected static function castToDesiredCapabilitiesObject($desired_capabilities = null)
712    {
713        if ($desired_capabilities === null) {
714            return new DesiredCapabilities();
715        }
716
717        if (is_array($desired_capabilities)) {
718            return new DesiredCapabilities($desired_capabilities);
719        }
720
721        return $desired_capabilities;
722    }
723}
724