HttpCommandExecutor.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. <?php
  2. // Copyright 2004-present Facebook. All Rights Reserved.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. namespace Facebook\WebDriver\Remote;
  16. use BadMethodCallException;
  17. use Facebook\WebDriver\Exception\WebDriverCurlException;
  18. use Facebook\WebDriver\Exception\WebDriverException;
  19. use Facebook\WebDriver\WebDriverCommandExecutor;
  20. use InvalidArgumentException;
  21. /**
  22. * Command executor talking to the standalone server via HTTP.
  23. */
  24. class HttpCommandExecutor implements WebDriverCommandExecutor
  25. {
  26. const DEFAULT_HTTP_HEADERS = [
  27. 'Content-Type: application/json;charset=UTF-8',
  28. 'Accept: application/json',
  29. ];
  30. /**
  31. * @see https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#command-reference
  32. */
  33. protected static $commands = [
  34. DriverCommand::ACCEPT_ALERT => ['method' => 'POST', 'url' => '/session/:sessionId/accept_alert'],
  35. DriverCommand::ADD_COOKIE => ['method' => 'POST', 'url' => '/session/:sessionId/cookie'],
  36. DriverCommand::CLEAR_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/clear'],
  37. DriverCommand::CLICK_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/click'],
  38. DriverCommand::CLOSE => ['method' => 'DELETE', 'url' => '/session/:sessionId/window'],
  39. DriverCommand::DELETE_ALL_COOKIES => ['method' => 'DELETE', 'url' => '/session/:sessionId/cookie'],
  40. DriverCommand::DELETE_COOKIE => ['method' => 'DELETE', 'url' => '/session/:sessionId/cookie/:name'],
  41. DriverCommand::DISMISS_ALERT => ['method' => 'POST', 'url' => '/session/:sessionId/dismiss_alert'],
  42. DriverCommand::ELEMENT_EQUALS => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/equals/:other'],
  43. DriverCommand::FIND_CHILD_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/element'],
  44. DriverCommand::FIND_CHILD_ELEMENTS => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/elements'],
  45. DriverCommand::EXECUTE_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute'],
  46. DriverCommand::EXECUTE_ASYNC_SCRIPT => ['method' => 'POST', 'url' => '/session/:sessionId/execute_async'],
  47. DriverCommand::FIND_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element'],
  48. DriverCommand::FIND_ELEMENTS => ['method' => 'POST', 'url' => '/session/:sessionId/elements'],
  49. DriverCommand::SWITCH_TO_FRAME => ['method' => 'POST', 'url' => '/session/:sessionId/frame'],
  50. DriverCommand::SWITCH_TO_WINDOW => ['method' => 'POST', 'url' => '/session/:sessionId/window'],
  51. DriverCommand::GET => ['method' => 'POST', 'url' => '/session/:sessionId/url'],
  52. DriverCommand::GET_ACTIVE_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/active'],
  53. DriverCommand::GET_ALERT_TEXT => ['method' => 'GET', 'url' => '/session/:sessionId/alert_text'],
  54. DriverCommand::GET_ALL_COOKIES => ['method' => 'GET', 'url' => '/session/:sessionId/cookie'],
  55. DriverCommand::GET_ALL_SESSIONS => ['method' => 'GET', 'url' => '/sessions'],
  56. DriverCommand::GET_AVAILABLE_LOG_TYPES => ['method' => 'GET', 'url' => '/session/:sessionId/log/types'],
  57. DriverCommand::GET_CURRENT_URL => ['method' => 'GET', 'url' => '/session/:sessionId/url'],
  58. DriverCommand::GET_CURRENT_WINDOW_HANDLE => ['method' => 'GET', 'url' => '/session/:sessionId/window_handle'],
  59. DriverCommand::GET_ELEMENT_ATTRIBUTE => [
  60. 'method' => 'GET',
  61. 'url' => '/session/:sessionId/element/:id/attribute/:name',
  62. ],
  63. DriverCommand::GET_ELEMENT_VALUE_OF_CSS_PROPERTY => [
  64. 'method' => 'GET',
  65. 'url' => '/session/:sessionId/element/:id/css/:propertyName',
  66. ],
  67. DriverCommand::GET_ELEMENT_LOCATION => [
  68. 'method' => 'GET',
  69. 'url' => '/session/:sessionId/element/:id/location',
  70. ],
  71. DriverCommand::GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW => [
  72. 'method' => 'GET',
  73. 'url' => '/session/:sessionId/element/:id/location_in_view',
  74. ],
  75. DriverCommand::GET_ELEMENT_SIZE => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/size'],
  76. DriverCommand::GET_ELEMENT_TAG_NAME => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/name'],
  77. DriverCommand::GET_ELEMENT_TEXT => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/text'],
  78. DriverCommand::GET_LOG => ['method' => 'POST', 'url' => '/session/:sessionId/log'],
  79. DriverCommand::GET_PAGE_SOURCE => ['method' => 'GET', 'url' => '/session/:sessionId/source'],
  80. DriverCommand::GET_SCREEN_ORIENTATION => ['method' => 'GET', 'url' => '/session/:sessionId/orientation'],
  81. DriverCommand::GET_CAPABILITIES => ['method' => 'GET', 'url' => '/session/:sessionId'],
  82. DriverCommand::GET_TITLE => ['method' => 'GET', 'url' => '/session/:sessionId/title'],
  83. DriverCommand::GET_WINDOW_HANDLES => ['method' => 'GET', 'url' => '/session/:sessionId/window_handles'],
  84. DriverCommand::GET_WINDOW_POSITION => [
  85. 'method' => 'GET',
  86. 'url' => '/session/:sessionId/window/:windowHandle/position',
  87. ],
  88. DriverCommand::GET_WINDOW_SIZE => ['method' => 'GET', 'url' => '/session/:sessionId/window/:windowHandle/size'],
  89. DriverCommand::GO_BACK => ['method' => 'POST', 'url' => '/session/:sessionId/back'],
  90. DriverCommand::GO_FORWARD => ['method' => 'POST', 'url' => '/session/:sessionId/forward'],
  91. DriverCommand::IS_ELEMENT_DISPLAYED => [
  92. 'method' => 'GET',
  93. 'url' => '/session/:sessionId/element/:id/displayed',
  94. ],
  95. DriverCommand::IS_ELEMENT_ENABLED => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/enabled'],
  96. DriverCommand::IS_ELEMENT_SELECTED => ['method' => 'GET', 'url' => '/session/:sessionId/element/:id/selected'],
  97. DriverCommand::MAXIMIZE_WINDOW => [
  98. 'method' => 'POST',
  99. 'url' => '/session/:sessionId/window/:windowHandle/maximize',
  100. ],
  101. DriverCommand::MOUSE_DOWN => ['method' => 'POST', 'url' => '/session/:sessionId/buttondown'],
  102. DriverCommand::MOUSE_UP => ['method' => 'POST', 'url' => '/session/:sessionId/buttonup'],
  103. DriverCommand::CLICK => ['method' => 'POST', 'url' => '/session/:sessionId/click'],
  104. DriverCommand::DOUBLE_CLICK => ['method' => 'POST', 'url' => '/session/:sessionId/doubleclick'],
  105. DriverCommand::MOVE_TO => ['method' => 'POST', 'url' => '/session/:sessionId/moveto'],
  106. DriverCommand::NEW_SESSION => ['method' => 'POST', 'url' => '/session'],
  107. DriverCommand::QUIT => ['method' => 'DELETE', 'url' => '/session/:sessionId'],
  108. DriverCommand::REFRESH => ['method' => 'POST', 'url' => '/session/:sessionId/refresh'],
  109. DriverCommand::UPLOAD_FILE => ['method' => 'POST', 'url' => '/session/:sessionId/file'], // undocumented
  110. DriverCommand::SEND_KEYS_TO_ACTIVE_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/keys'],
  111. DriverCommand::SET_ALERT_VALUE => ['method' => 'POST', 'url' => '/session/:sessionId/alert_text'],
  112. DriverCommand::SEND_KEYS_TO_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/value'],
  113. DriverCommand::IMPLICITLY_WAIT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts/implicit_wait'],
  114. DriverCommand::SET_SCREEN_ORIENTATION => ['method' => 'POST', 'url' => '/session/:sessionId/orientation'],
  115. DriverCommand::SET_TIMEOUT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts'],
  116. DriverCommand::SET_SCRIPT_TIMEOUT => ['method' => 'POST', 'url' => '/session/:sessionId/timeouts/async_script'],
  117. DriverCommand::SET_WINDOW_POSITION => [
  118. 'method' => 'POST',
  119. 'url' => '/session/:sessionId/window/:windowHandle/position',
  120. ],
  121. DriverCommand::SET_WINDOW_SIZE => [
  122. 'method' => 'POST',
  123. 'url' => '/session/:sessionId/window/:windowHandle/size',
  124. ],
  125. DriverCommand::SUBMIT_ELEMENT => ['method' => 'POST', 'url' => '/session/:sessionId/element/:id/submit'],
  126. DriverCommand::SCREENSHOT => ['method' => 'GET', 'url' => '/session/:sessionId/screenshot'],
  127. DriverCommand::TOUCH_SINGLE_TAP => ['method' => 'POST', 'url' => '/session/:sessionId/touch/click'],
  128. DriverCommand::TOUCH_DOWN => ['method' => 'POST', 'url' => '/session/:sessionId/touch/down'],
  129. DriverCommand::TOUCH_DOUBLE_TAP => ['method' => 'POST', 'url' => '/session/:sessionId/touch/doubleclick'],
  130. DriverCommand::TOUCH_FLICK => ['method' => 'POST', 'url' => '/session/:sessionId/touch/flick'],
  131. DriverCommand::TOUCH_LONG_PRESS => ['method' => 'POST', 'url' => '/session/:sessionId/touch/longclick'],
  132. DriverCommand::TOUCH_MOVE => ['method' => 'POST', 'url' => '/session/:sessionId/touch/move'],
  133. DriverCommand::TOUCH_SCROLL => ['method' => 'POST', 'url' => '/session/:sessionId/touch/scroll'],
  134. DriverCommand::TOUCH_UP => ['method' => 'POST', 'url' => '/session/:sessionId/touch/up'],
  135. ];
  136. /**
  137. * @var string
  138. */
  139. protected $url;
  140. /**
  141. * @var resource
  142. */
  143. protected $curl;
  144. /**
  145. * @param string $url
  146. * @param string|null $http_proxy
  147. * @param int|null $http_proxy_port
  148. */
  149. public function __construct($url, $http_proxy = null, $http_proxy_port = null)
  150. {
  151. $this->url = $url;
  152. $this->curl = curl_init();
  153. if (!empty($http_proxy)) {
  154. curl_setopt($this->curl, CURLOPT_PROXY, $http_proxy);
  155. if ($http_proxy_port !== null) {
  156. curl_setopt($this->curl, CURLOPT_PROXYPORT, $http_proxy_port);
  157. }
  158. }
  159. // Get credentials from $url (if any)
  160. $matches = null;
  161. if (preg_match("/^(https?:\/\/)(.*):(.*)@(.*?)/U", $url, $matches)) {
  162. $this->url = $matches[1] . $matches[4];
  163. $auth_creds = $matches[2] . ':' . $matches[3];
  164. curl_setopt($this->curl, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
  165. curl_setopt($this->curl, CURLOPT_USERPWD, $auth_creds);
  166. }
  167. curl_setopt($this->curl, CURLOPT_RETURNTRANSFER, true);
  168. curl_setopt($this->curl, CURLOPT_FOLLOWLOCATION, true);
  169. curl_setopt($this->curl, CURLOPT_HTTPHEADER, static::DEFAULT_HTTP_HEADERS);
  170. $this->setRequestTimeout(30000);
  171. $this->setConnectionTimeout(30000);
  172. }
  173. /**
  174. * Set timeout for the connect phase
  175. *
  176. * @param int $timeout_in_ms Timeout in milliseconds
  177. * @return HttpCommandExecutor
  178. */
  179. public function setConnectionTimeout($timeout_in_ms)
  180. {
  181. // There is a PHP bug in some versions which didn't define the constant.
  182. curl_setopt(
  183. $this->curl,
  184. /* CURLOPT_CONNECTTIMEOUT_MS */
  185. 156,
  186. $timeout_in_ms
  187. );
  188. return $this;
  189. }
  190. /**
  191. * Set the maximum time of a request
  192. *
  193. * @param int $timeout_in_ms Timeout in milliseconds
  194. * @return HttpCommandExecutor
  195. */
  196. public function setRequestTimeout($timeout_in_ms)
  197. {
  198. // There is a PHP bug in some versions (at least for PHP 5.3.3) which
  199. // didn't define the constant.
  200. curl_setopt(
  201. $this->curl,
  202. /* CURLOPT_TIMEOUT_MS */
  203. 155,
  204. $timeout_in_ms
  205. );
  206. return $this;
  207. }
  208. /**
  209. * @param WebDriverCommand $command
  210. *
  211. * @throws WebDriverException
  212. * @return WebDriverResponse
  213. */
  214. public function execute(WebDriverCommand $command)
  215. {
  216. if (!isset(self::$commands[$command->getName()])) {
  217. throw new InvalidArgumentException($command->getName() . ' is not a valid command.');
  218. }
  219. $raw = self::$commands[$command->getName()];
  220. $http_method = $raw['method'];
  221. $url = $raw['url'];
  222. $url = str_replace(':sessionId', $command->getSessionID(), $url);
  223. $params = $command->getParameters();
  224. foreach ($params as $name => $value) {
  225. if ($name[0] === ':') {
  226. $url = str_replace($name, $value, $url);
  227. unset($params[$name]);
  228. }
  229. }
  230. if ($params && is_array($params) && $http_method !== 'POST') {
  231. throw new BadMethodCallException(sprintf(
  232. 'The http method called for %s is %s but it has to be POST' .
  233. ' if you want to pass the JSON params %s',
  234. $url,
  235. $http_method,
  236. json_encode($params)
  237. ));
  238. }
  239. curl_setopt($this->curl, CURLOPT_URL, $this->url . $url);
  240. // https://github.com/facebook/php-webdriver/issues/173
  241. if ($command->getName() === DriverCommand::NEW_SESSION) {
  242. curl_setopt($this->curl, CURLOPT_POST, 1);
  243. } else {
  244. curl_setopt($this->curl, CURLOPT_CUSTOMREQUEST, $http_method);
  245. }
  246. if (in_array($http_method, ['POST', 'PUT'])) {
  247. // Disable sending 'Expect: 100-Continue' header, as it is causing issues with eg. squid proxy
  248. // https://tools.ietf.org/html/rfc7231#section-5.1.1
  249. curl_setopt($this->curl, CURLOPT_HTTPHEADER, array_merge(static::DEFAULT_HTTP_HEADERS, ['Expect:']));
  250. } else {
  251. curl_setopt($this->curl, CURLOPT_HTTPHEADER, static::DEFAULT_HTTP_HEADERS);
  252. }
  253. $encoded_params = null;
  254. if ($http_method === 'POST' && $params && is_array($params)) {
  255. $encoded_params = json_encode($params);
  256. }
  257. curl_setopt($this->curl, CURLOPT_POSTFIELDS, $encoded_params);
  258. $raw_results = trim(curl_exec($this->curl));
  259. if ($error = curl_error($this->curl)) {
  260. $msg = sprintf(
  261. 'Curl error thrown for http %s to %s',
  262. $http_method,
  263. $url
  264. );
  265. if ($params && is_array($params)) {
  266. $msg .= sprintf(' with params: %s', json_encode($params));
  267. }
  268. throw new WebDriverCurlException($msg . "\n\n" . $error);
  269. }
  270. $results = json_decode($raw_results, true);
  271. if ($results === null && json_last_error() !== JSON_ERROR_NONE) {
  272. throw new WebDriverException(
  273. sprintf(
  274. "JSON decoding of remote response failed.\n" .
  275. "Error code: %d\n" .
  276. "The response: '%s'\n",
  277. json_last_error(),
  278. $raw_results
  279. )
  280. );
  281. }
  282. $value = null;
  283. if (is_array($results) && array_key_exists('value', $results)) {
  284. $value = $results['value'];
  285. }
  286. $message = null;
  287. if (is_array($value) && array_key_exists('message', $value)) {
  288. $message = $value['message'];
  289. }
  290. $sessionId = null;
  291. if (is_array($results) && array_key_exists('sessionId', $results)) {
  292. $sessionId = $results['sessionId'];
  293. }
  294. $status = isset($results['status']) ? $results['status'] : 0;
  295. if ($status != 0) {
  296. WebDriverException::throwException($status, $message, $results);
  297. }
  298. $response = new WebDriverResponse($sessionId);
  299. return $response
  300. ->setStatus($status)
  301. ->setValue($value);
  302. }
  303. /**
  304. * @return string
  305. */
  306. public function getAddressOfRemoteServer()
  307. {
  308. return $this->url;
  309. }
  310. }