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