1<?php
2
3namespace Facebook\WebDriver\Remote;
4
5use BadMethodCallException;
6use Facebook\WebDriver\Exception\WebDriverCurlException;
7use Facebook\WebDriver\Exception\WebDriverException;
8use Facebook\WebDriver\WebDriverCommandExecutor;
9use InvalidArgumentException;
10
11/**
12 * Command executor talking to the standalone server via HTTP.
13 */
14class HttpCommandExecutor implements WebDriverCommandExecutor
15{
16    const DEFAULT_HTTP_HEADERS = [
17        'Content-Type: application/json;charset=UTF-8',
18        'Accept: application/json',
19    ];
20
21    /**
22     * @see https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#command-reference
23     */
24    protected static $commands = [
25        DriverCommand::ACCEPT_ALERT => ['method' => 'POST', 'url' => '/session/:sessionId/accept_alert'],
26        DriverCommand::ADD_COOKIE => ['method' => 'POST', 'url' => '/session/:sessionId/cookie'],
27        DriverCommand::CLEAR_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/clear'],
28        DriverCommand::CLICK_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/click'],
29        DriverCommand::CLOSE => ['method' => 'DELETE', 'url' => '/session/:sessionId/window'],
30        DriverCommand::DELETE_ALL_COOKIES => ['method' => 'DELETE', 'url' => '/session/:sessionId/cookie'],
31        DriverCommand::DELETE_COOKIE => ['method' => 'DELETE', 'url' => '/session/:sessionId/cookie/:name'],
32        DriverCommand::DISMISS_ALERT => ['method' => 'POST', 'url' => '/session/:sessionId/dismiss_alert'],
33        DriverCommand::ELEMENT_EQUALS => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/equals/:other'],
34        DriverCommand::FIND_CHILD_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/element'],
35        DriverCommand::FIND_CHILD_ELEMENTS => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/elements'],
36        DriverCommand::EXECUTE_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute'],
37        DriverCommand::EXECUTE_ASYNC_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute_async'],
38        DriverCommand::FIND_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element'],
39        DriverCommand::FIND_ELEMENTS => ['method' => 'POST', 'url' => '/session/:sessionId/elements'],
40        DriverCommand::SWITCH_TO_FRAME => ['method' => 'POST', 'url' => '/session/:sessionId/frame'],
41        DriverCommand::SWITCH_TO_PARENT_FRAME => ['method' => 'POST', 'url' => '/session/:sessionId/frame/parent'],
42        DriverCommand::SWITCH_TO_WINDOW => ['method' => 'POST', 'url' => '/session/:sessionId/window'],
43        DriverCommand::GET => ['method' => 'POST', 'url' => '/session/:sessionId/url'],
44        DriverCommand::GET_ACTIVE_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/active'],
45        DriverCommand::GET_ALERT_TEXT => ['method' => 'GET', 'url' => '/session/:sessionId/alert_text'],
46        DriverCommand::GET_ALL_COOKIES => ['method' => 'GET', 'url' => '/session/:sessionId/cookie'],
47        DriverCommand::GET_NAMED_COOKIE => ['method' => 'GET', 'url' => '/session/:sessionId/cookie/:name'],
48        DriverCommand::GET_ALL_SESSIONS => ['method' => 'GET', 'url' => '/sessions'],
49        DriverCommand::GET_AVAILABLE_LOG_TYPES => ['method' => 'GET', 'url' => '/session/:sessionId/log/types'],
50        DriverCommand::GET_CURRENT_URL => ['method' => 'GET', 'url' => '/session/:sessionId/url'],
51        DriverCommand::GET_CURRENT_WINDOW_HANDLE => ['method' => 'GET', 'url' => '/session/:sessionId/window_handle'],
52        DriverCommand::GET_ELEMENT_ATTRIBUTE => [
53            'method' => 'GET',
54            'url' => '/session/:sessionId/element/:id/attribute/:name',
55        ],
56        DriverCommand::GET_ELEMENT_VALUE_OF_CSS_PROPERTY => [
57            'method' => 'GET',
58            'url' => '/session/:sessionId/element/:id/css/:propertyName',
59        ],
60        DriverCommand::GET_ELEMENT_LOCATION => [
61            'method' => 'GET',
62            'url' => '/session/:sessionId/element/:id/location',
63        ],
64        DriverCommand::GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW => [
65            'method' => 'GET',
66            'url' => '/session/:sessionId/element/:id/location_in_view',
67        ],
68        DriverCommand::GET_ELEMENT_SIZE => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/size'],
69        DriverCommand::GET_ELEMENT_TAG_NAME => ['method' => 'GET',  'url' => '/session/:sessionId/element/:id/name'],
70        DriverCommand::GET_ELEMENT_TEXT => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/text'],
71        DriverCommand::GET_LOG => ['method' => 'POST', 'url' => '/session/:sessionId/log'],
72        DriverCommand::GET_PAGE_SOURCE => ['method' => 'GET', 'url' => '/session/:sessionId/source'],
73        DriverCommand::GET_SCREEN_ORIENTATION => ['method' => 'GET', 'url' => '/session/:sessionId/orientation'],
74        DriverCommand::GET_CAPABILITIES => ['method' => 'GET', 'url' => '/session/:sessionId'],
75        DriverCommand::GET_TITLE => ['method' => 'GET', 'url' => '/session/:sessionId/title'],
76        DriverCommand::GET_WINDOW_HANDLES => ['method' => 'GET', 'url' => '/session/:sessionId/window_handles'],
77        DriverCommand::GET_WINDOW_POSITION => [
78            'method' => 'GET',
79            'url' => '/session/:sessionId/window/:windowHandle/position',
80        ],
81        DriverCommand::GET_WINDOW_SIZE => ['method' => 'GET', 'url' => '/session/:sessionId/window/:windowHandle/size'],
82        DriverCommand::GO_BACK => ['method' => 'POST', 'url' => '/session/:sessionId/back'],
83        DriverCommand::GO_FORWARD => ['method' => 'POST', 'url' => '/session/:sessionId/forward'],
84        DriverCommand::IS_ELEMENT_DISPLAYED => [
85            'method' => 'GET',
86            'url' => '/session/:sessionId/element/:id/displayed',
87        ],
88        DriverCommand::IS_ELEMENT_ENABLED => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/enabled'],
89        DriverCommand::IS_ELEMENT_SELECTED => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/selected'],
90        DriverCommand::MAXIMIZE_WINDOW => [
91            'method' => 'POST',
92            'url' => '/session/:sessionId/window/:windowHandle/maximize',
93        ],
94        DriverCommand::MOUSE_DOWN => ['method' => 'POST', 'url' => '/session/:sessionId/buttondown'],
95        DriverCommand::MOUSE_UP => ['method' => 'POST', 'url' => '/session/:sessionId/buttonup'],
96        DriverCommand::CLICK => ['method' => 'POST', 'url' => '/session/:sessionId/click'],
97        DriverCommand::DOUBLE_CLICK => ['method' => 'POST', 'url' => '/session/:sessionId/doubleclick'],
98        DriverCommand::MOVE_TO => ['method' => 'POST', 'url' => '/session/:sessionId/moveto'],
99        DriverCommand::NEW_SESSION => ['method' => 'POST', 'url' => '/session'],
100        DriverCommand::QUIT => ['method' => 'DELETE', 'url' => '/session/:sessionId'],
101        DriverCommand::REFRESH => ['method' => 'POST', 'url' => '/session/:sessionId/refresh'],
102        DriverCommand::UPLOAD_FILE => ['method' => 'POST', 'url' => '/session/:sessionId/file'], // undocumented
103        DriverCommand::SEND_KEYS_TO_ACTIVE_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/keys'],
104        DriverCommand::SET_ALERT_VALUE => ['method' => 'POST', 'url' => '/session/:sessionId/alert_text'],
105        DriverCommand::SEND_KEYS_TO_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/value'],
106        DriverCommand::IMPLICITLY_WAIT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts/implicit_wait'],
107        DriverCommand::SET_SCREEN_ORIENTATION => ['method' => 'POST', 'url' => '/session/:sessionId/orientation'],
108        DriverCommand::SET_TIMEOUT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'],
109        DriverCommand::SET_SCRIPT_TIMEOUT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts/async_script'],
110        DriverCommand::SET_WINDOW_POSITION => [
111            'method' => 'POST',
112            'url' => '/session/:sessionId/window/:windowHandle/position',
113        ],
114        DriverCommand::SET_WINDOW_SIZE => [
115            'method' => 'POST',
116            'url' => '/session/:sessionId/window/:windowHandle/size',
117        ],
118        DriverCommand::STATUS => ['method' => 'GET', 'url' => '/status'],
119        DriverCommand::SUBMIT_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/submit'],
120        DriverCommand::SCREENSHOT => ['method' => 'GET', 'url' => '/session/:sessionId/screenshot'],
121        DriverCommand::TAKE_ELEMENT_SCREENSHOT => [
122            'method' => 'GET',
123            'url' => '/session/:sessionId/element/:id/screenshot',
124        ],
125        DriverCommand::TOUCH_SINGLE_TAP => ['method' => 'POST', 'url' => '/session/:sessionId/touch/click'],
126        DriverCommand::TOUCH_DOWN => ['method' => 'POST', 'url' => '/session/:sessionId/touch/down'],
127        DriverCommand::TOUCH_DOUBLE_TAP => ['method' => 'POST', 'url' => '/session/:sessionId/touch/doubleclick'],
128        DriverCommand::TOUCH_FLICK => ['method' => 'POST', 'url' => '/session/:sessionId/touch/flick'],
129        DriverCommand::TOUCH_LONG_PRESS => ['method' => 'POST', 'url' => '/session/:sessionId/touch/longclick'],
130        DriverCommand::TOUCH_MOVE => ['method' => 'POST', 'url' => '/session/:sessionId/touch/move'],
131        DriverCommand::TOUCH_SCROLL => ['method' => 'POST', 'url' => '/session/:sessionId/touch/scroll'],
132        DriverCommand::TOUCH_UP => ['method' => 'POST', 'url' => '/session/:sessionId/touch/up'],
133        DriverCommand::CUSTOM_COMMAND => [],
134    ];
135    /**
136     * @var array Will be merged with $commands
137     */
138    protected static $w3cCompliantCommands = [
139        DriverCommand::ACCEPT_ALERT => ['method' => 'POST', 'url' => '/session/:sessionId/alert/accept'],
140        DriverCommand::ACTIONS => ['method' => 'POST', 'url' => '/session/:sessionId/actions'],
141        DriverCommand::DISMISS_ALERT => ['method' => 'POST', 'url' => '/session/:sessionId/alert/dismiss'],
142        DriverCommand::EXECUTE_ASYNC_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute/async'],
143        DriverCommand::EXECUTE_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute/sync'],
144        DriverCommand::FULLSCREEN_WINDOW => ['method' => 'POST', 'url' => '/session/:sessionId/window/fullscreen'],
145        DriverCommand::GET_ACTIVE_ELEMENT => ['method' => 'GET', 'url' => '/session/:sessionId/element/active'],
146        DriverCommand::GET_ALERT_TEXT => ['method' => 'GET', 'url' => '/session/:sessionId/alert/text'],
147        DriverCommand::GET_CURRENT_WINDOW_HANDLE => ['method' => 'GET', 'url' => '/session/:sessionId/window'],
148        DriverCommand::GET_ELEMENT_LOCATION => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/rect'],
149        DriverCommand::GET_ELEMENT_PROPERTY => [
150            'method' => 'GET',
151            'url' => '/session/:sessionId/element/:id/property/:name',
152        ],
153        DriverCommand::GET_ELEMENT_SIZE => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/rect'],
154        DriverCommand::GET_WINDOW_HANDLES => ['method' => 'GET', 'url' => '/session/:sessionId/window/handles'],
155        DriverCommand::GET_WINDOW_POSITION => ['method' => 'GET', 'url' => '/session/:sessionId/window/rect'],
156        DriverCommand::GET_WINDOW_SIZE => ['method' => 'GET', 'url' => '/session/:sessionId/window/rect'],
157        DriverCommand::IMPLICITLY_WAIT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'],
158        DriverCommand::MAXIMIZE_WINDOW => ['method' => 'POST', 'url' => '/session/:sessionId/window/maximize'],
159        DriverCommand::MINIMIZE_WINDOW => ['method' => 'POST', 'url' => '/session/:sessionId/window/minimize'],
160        DriverCommand::NEW_WINDOW => ['method' => 'POST', 'url' => '/session/:sessionId/window/new'],
161        DriverCommand::SET_ALERT_VALUE => ['method' => 'POST', 'url' => '/session/:sessionId/alert/text'],
162        DriverCommand::SET_SCRIPT_TIMEOUT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'],
163        DriverCommand::SET_TIMEOUT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'],
164        DriverCommand::SET_WINDOW_SIZE => ['method' => 'POST', 'url' => '/session/:sessionId/window/rect'],
165        DriverCommand::SET_WINDOW_POSITION => ['method' => 'POST', 'url' => '/session/:sessionId/window/rect'],
166    ];
167    /**
168     * @var string
169     */
170    protected $url;
171    /**
172     * @var resource
173     */
174    protected $curl;
175    /**
176     * @var bool
177     */
178    protected $isW3cCompliant = true;
179
180    /**
181     * @param string $url
182     * @param string|null $http_proxy
183     * @param int|null $http_proxy_port
184     */
185    public function __construct($url, $http_proxy = null, $http_proxy_port = null)
186    {
187        self::$w3cCompliantCommands = array_merge(self::$commands, self::$w3cCompliantCommands);
188
189        $this->url = $url;
190        $this->curl = curl_init();
191
192        if (!empty($http_proxy)) {
193            curl_setopt($this->curl, CURLOPT_PROXY, $http_proxy);
194            if ($http_proxy_port !== null) {
195                curl_setopt($this->curl, CURLOPT_PROXYPORT, $http_proxy_port);
196            }
197        }
198
199        // Get credentials from $url (if any)
200        $matches = null;
201        if (preg_match("/^(https?:\/\/)(.*):(.*)@(.*?)/U", $url, $matches)) {
202            $this->url = $matches[1] . $matches[4];
203            $auth_creds = $matches[2] . ':' . $matches[3];
204            curl_setopt($this->curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
205            curl_setopt($this->curl, CURLOPT_USERPWD, $auth_creds);
206        }
207
208        curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true);
209        curl_setopt($this->curl, CURLOPT_FOLLOWLOCATION, true);
210        curl_setopt($this->curl, CURLOPT_HTTPHEADER, static::DEFAULT_HTTP_HEADERS);
211        $this->setRequestTimeout(30000);
212        $this->setConnectionTimeout(30000);
213    }
214
215    public function disableW3cCompliance()
216    {
217        $this->isW3cCompliant = false;
218    }
219
220    /**
221     * Set timeout for the connect phase
222     *
223     * @param int $timeout_in_ms Timeout in milliseconds
224     * @return HttpCommandExecutor
225     */
226    public function setConnectionTimeout($timeout_in_ms)
227    {
228        // There is a PHP bug in some versions which didn't define the constant.
229        curl_setopt(
230            $this->curl,
231            /* CURLOPT_CONNECTTIMEOUT_MS */
232            156,
233            $timeout_in_ms
234        );
235
236        return $this;
237    }
238
239    /**
240     * Set the maximum time of a request
241     *
242     * @param int $timeout_in_ms Timeout in milliseconds
243     * @return HttpCommandExecutor
244     */
245    public function setRequestTimeout($timeout_in_ms)
246    {
247        // There is a PHP bug in some versions (at least for PHP 5.3.3) which
248        // didn't define the constant.
249        curl_setopt(
250            $this->curl,
251            /* CURLOPT_TIMEOUT_MS */
252            155,
253            $timeout_in_ms
254        );
255
256        return $this;
257    }
258
259    /**
260     * @param WebDriverCommand $command
261     *
262     * @throws WebDriverException
263     * @return WebDriverResponse
264     */
265    public function execute(WebDriverCommand $command)
266    {
267        $http_options = $this->getCommandHttpOptions($command);
268        $http_method = $http_options['method'];
269        $url = $http_options['url'];
270
271        $sessionID = $command->getSessionID();
272        $url = str_replace(':sessionId', $sessionID === null ? '' : $sessionID, $url);
273        $params = $command->getParameters();
274        foreach ($params as $name => $value) {
275            if ($name[0] === ':') {
276                $url = str_replace($name, $value, $url);
277                unset($params[$name]);
278            }
279        }
280
281        if (is_array($params) && !empty($params) && $http_method !== 'POST') {
282            throw new BadMethodCallException(sprintf(
283                'The http method called for %s is %s but it has to be POST' .
284                ' if you want to pass the JSON params %s',
285                $url,
286                $http_method,
287                json_encode($params)
288            ));
289        }
290
291        curl_setopt($this->curl, CURLOPT_URL, $this->url . $url);
292
293        // https://github.com/facebook/php-webdriver/issues/173
294        if ($command->getName() === DriverCommand::NEW_SESSION) {
295            curl_setopt($this->curl, CURLOPT_POST, 1);
296        } else {
297            curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, $http_method);
298        }
299
300        if (in_array($http_method, ['POST', 'PUT'], true)) {
301            // Disable sending 'Expect: 100-Continue' header, as it is causing issues with eg. squid proxy
302            // https://tools.ietf.org/html/rfc7231#section-5.1.1
303            curl_setopt($this->curl, CURLOPT_HTTPHEADER, array_merge(static::DEFAULT_HTTP_HEADERS, ['Expect:']));
304        } else {
305            curl_setopt($this->curl, CURLOPT_HTTPHEADER, static::DEFAULT_HTTP_HEADERS);
306        }
307
308        $encoded_params = null;
309
310        if ($http_method === 'POST') {
311            if (is_array($params) && !empty($params)) {
312                $encoded_params = json_encode($params);
313            } elseif ($this->isW3cCompliant) {
314                // POST body must be valid JSON in W3C, even if empty: https://www.w3.org/TR/webdriver/#processing-model
315                $encoded_params = '{}';
316            }
317        }
318
319        curl_setopt($this->curl, CURLOPT_POSTFIELDS, $encoded_params);
320
321        $raw_results = trim(curl_exec($this->curl));
322
323        if ($error = curl_error($this->curl)) {
324            $msg = sprintf(
325                'Curl error thrown for http %s to %s',
326                $http_method,
327                $url
328            );
329            if (is_array($params) && !empty($params)) {
330                $msg .= sprintf(' with params: %s', json_encode($params, JSON_UNESCAPED_SLASHES));
331            }
332
333            throw new WebDriverCurlException($msg . "\n\n" . $error);
334        }
335
336        $results = json_decode($raw_results, true);
337
338        if ($results === null && json_last_error() !== JSON_ERROR_NONE) {
339            throw new WebDriverException(
340                sprintf(
341                    "JSON decoding of remote response failed.\n" .
342                    "Error code: %d\n" .
343                    "The response: '%s'\n",
344                    json_last_error(),
345                    $raw_results
346                )
347            );
348        }
349
350        $value = null;
351        if (is_array($results) && array_key_exists('value', $results)) {
352            $value = $results['value'];
353        }
354
355        $message = null;
356        if (is_array($value) && array_key_exists('message', $value)) {
357            $message = $value['message'];
358        }
359
360        $sessionId = null;
361        if (is_array($value) && array_key_exists('sessionId', $value)) {
362            // W3C's WebDriver
363            $sessionId = $value['sessionId'];
364        } elseif (is_array($results) && array_key_exists('sessionId', $results)) {
365            // Legacy JsonWire
366            $sessionId = $results['sessionId'];
367        }
368
369        // @see https://w3c.github.io/webdriver/webdriver-spec.html#handling-errors
370        if (isset($value['error'])) {
371            // W3C's WebDriver
372            WebDriverException::throwException($value['error'], $message, $results);
373        }
374
375        $status = isset($results['status']) ? $results['status'] : 0;
376        if ($status !== 0) {
377            // Legacy JsonWire
378            WebDriverException::throwException($status, $message, $results);
379        }
380
381        $response = new WebDriverResponse($sessionId);
382
383        return $response
384            ->setStatus($status)
385            ->setValue($value);
386    }
387
388    /**
389     * @return string
390     */
391    public function getAddressOfRemoteServer()
392    {
393        return $this->url;
394    }
395
396    /**
397     * @return array
398     */
399    protected function getCommandHttpOptions(WebDriverCommand $command)
400    {
401        $commandName = $command->getName();
402        if (!isset(self::$commands[$commandName])) {
403            if ($this->isW3cCompliant && !isset(self::$w3cCompliantCommands[$commandName])) {
404                throw new InvalidArgumentException($command->getName() . ' is not a valid command.');
405            }
406        }
407
408        if ($this->isW3cCompliant) {
409            $raw = self::$w3cCompliantCommands[$command->getName()];
410        } else {
411            $raw = self::$commands[$command->getName()];
412        }
413
414        if ($command instanceof CustomWebDriverCommand) {
415            $url = $command->getCustomUrl();
416            $method = $command->getCustomMethod();
417        } else {
418            $url = $raw['url'];
419            $method = $raw['method'];
420        }
421
422        return [
423            'url' => $url,
424            'method' => $method,
425        ];
426    }
427}
428