Session.cpp 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903
  1. /*
  2. * Copyright (c) 2022, Florent Castelli <florent.castelli@gmail.com>
  3. * Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
  4. * Copyright (c) 2022, Tobias Christiansen <tobyase@serenityos.org>
  5. * Copyright (c) 2022, Linus Groh <linusg@serenityos.org>
  6. *
  7. * SPDX-License-Identifier: BSD-2-Clause
  8. */
  9. #include "Session.h"
  10. #include "BrowserConnection.h"
  11. #include "Client.h"
  12. #include <AK/Time.h>
  13. #include <LibCore/LocalServer.h>
  14. #include <LibCore/Stream.h>
  15. #include <LibCore/System.h>
  16. #include <LibWeb/Cookie/Cookie.h>
  17. #include <LibWeb/Cookie/ParsedCookie.h>
  18. #include <unistd.h>
  19. namespace WebDriver {
  20. Session::Session(unsigned session_id, NonnullRefPtr<Client> client)
  21. : m_client(move(client))
  22. , m_id(session_id)
  23. {
  24. }
  25. Session::~Session()
  26. {
  27. if (m_started) {
  28. auto error = stop();
  29. if (error.is_error()) {
  30. warnln("Failed to stop session {}: {}", m_id, error.error());
  31. }
  32. }
  33. }
  34. ErrorOr<void> Session::start()
  35. {
  36. auto socket_path = String::formatted("/tmp/browser_webdriver_{}_{}", getpid(), m_id);
  37. dbgln("Listening for WebDriver connection on {}", socket_path);
  38. // FIXME: Use Core::LocalServer
  39. struct sockaddr_un addr;
  40. int listen_socket = TRY(Core::System::socket(AF_UNIX, SOCK_STREAM, 0));
  41. ::memset(&addr, 0, sizeof(struct sockaddr_un));
  42. addr.sun_family = AF_UNIX;
  43. ::strncpy(addr.sun_path, socket_path.characters(), sizeof(addr.sun_path) - 1);
  44. TRY(Core::System::bind(listen_socket, (const struct sockaddr*)&addr, sizeof(struct sockaddr_un)));
  45. TRY(Core::System::listen(listen_socket, 1));
  46. char const* argv[] = { "/bin/Browser", "--webdriver", socket_path.characters(), nullptr };
  47. TRY(Core::System::posix_spawn("/bin/Browser"sv, nullptr, nullptr, const_cast<char**>(argv), environ));
  48. int data_socket = TRY(Core::System::accept(listen_socket, nullptr, nullptr));
  49. auto socket = TRY(Core::Stream::LocalSocket::adopt_fd(data_socket));
  50. TRY(socket->set_blocking(true));
  51. m_browser_connection = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) BrowserConnection(move(socket), m_client, session_id())));
  52. dbgln("Browser is connected");
  53. m_started = true;
  54. m_windows.set("main", make<Session::Window>("main", true));
  55. m_current_window_handle = "main";
  56. return {};
  57. }
  58. ErrorOr<void> Session::stop()
  59. {
  60. m_browser_connection->async_quit();
  61. return {};
  62. }
  63. // 9.1 Get Timeouts, https://w3c.github.io/webdriver/#dfn-get-timeouts
  64. JsonObject Session::get_timeouts()
  65. {
  66. // 1. Let timeouts be the timeouts object for session’s timeouts configuration
  67. auto timeouts = timeouts_object(m_timeouts_configuration);
  68. // 2. Return success with data timeouts.
  69. return timeouts;
  70. }
  71. // 9.2 Set Timeouts, https://w3c.github.io/webdriver/#dfn-set-timeouts
  72. ErrorOr<JsonValue, HttpError> Session::set_timeouts(JsonValue const& payload)
  73. {
  74. // 1. Let timeouts be the result of trying to JSON deserialize as a timeouts configuration the request’s parameters.
  75. auto timeouts = TRY(json_deserialize_as_a_timeouts_configuration(payload));
  76. // 2. Make the session timeouts the new timeouts.
  77. m_timeouts_configuration = move(timeouts);
  78. // 3. Return success with data null.
  79. return JsonValue {};
  80. }
  81. // 10.1 Navigate To, https://w3c.github.io/webdriver/#dfn-navigate-to
  82. ErrorOr<JsonValue, HttpError> Session::navigate_to(JsonValue const& payload)
  83. {
  84. // 1. If the current top-level browsing context is no longer open, return error with error code no such window.
  85. auto current_window = this->current_window();
  86. if (!current_window.has_value())
  87. return HttpError { 404, "no such window", "Window not found" };
  88. // FIXME 2. Handle any user prompts and return its value if it is an error.
  89. // 3. If the url property is missing from the parameters argument or it is not a string, return error with error code invalid argument.
  90. if (!payload.is_object() || !payload.as_object().has_string("url"sv)) {
  91. return HttpError { 400, "invalid argument", "Payload doesn't have a string url" };
  92. }
  93. // 4. Let url be the result of getting a property named url from the parameters argument.
  94. URL url(payload.as_object().get_ptr("url"sv)->as_string());
  95. // FIXME: 5. If url is not an absolute URL or an absolute URL with fragment, return error with error code invalid argument. [URL]
  96. // 6. Let url be the result of getting a property named url from the parameters argument.
  97. // Duplicate step?
  98. // 7. Navigate the current top-level browsing context to url.
  99. m_browser_connection->async_set_url(url);
  100. // FIXME: 8. Run the post-navigation checks and return its value if it is an error.
  101. // FIXME: 9. Wait for navigation to complete and return its value if it is an error.
  102. // FIXME: 10. Set the current browsing context to the current top-level browsing context.
  103. // 11. Return success with data null.
  104. return JsonValue();
  105. }
  106. // 10.2 Get Current URL, https://w3c.github.io/webdriver/#dfn-get-current-url
  107. ErrorOr<JsonValue, HttpError> Session::get_current_url()
  108. {
  109. // 1. If the current top-level browsing context is no longer open, return error with error code no such window.
  110. auto current_window = this->current_window();
  111. if (!current_window.has_value())
  112. return HttpError { 404, "no such window", "Window not found" };
  113. // FIXME: 2. Handle any user prompts and return its value if it is an error.
  114. // 3. Let url be the serialization of the current top-level browsing context’s active document’s document URL.
  115. auto url = m_browser_connection->get_url().to_string();
  116. // 4. Return success with data url.
  117. return JsonValue(url);
  118. }
  119. // 10.3 Back, https://w3c.github.io/webdriver/#dfn-back
  120. ErrorOr<JsonValue, HttpError> Session::back()
  121. {
  122. // 1. If the current top-level browsing context is no longer open, return error with error code no such window.
  123. auto current_window = this->current_window();
  124. if (!current_window.has_value())
  125. return HttpError { 404, "no such window", "Window not found" };
  126. // FIXME: 2. Handle any user prompts and return its value if it is an error.
  127. // 3. Traverse the history by a delta –1 for the current browsing context.
  128. m_browser_connection->async_back();
  129. // FIXME: 4. If the previous step completed results in a pageHide event firing, wait until pageShow event
  130. // fires or for the session page load timeout milliseconds to pass, whichever occurs sooner.
  131. // FIXME: 5. If the previous step completed by the session page load timeout being reached, and user
  132. // prompts have been handled, return error with error code timeout.
  133. // 6. Return success with data null.
  134. return JsonValue();
  135. }
  136. // 10.4 Forward, https://w3c.github.io/webdriver/#dfn-forward
  137. ErrorOr<JsonValue, HttpError> Session::forward()
  138. {
  139. // 1. If the current top-level browsing context is no longer open, return error with error code no such window.
  140. auto current_window = this->current_window();
  141. if (!current_window.has_value())
  142. return HttpError { 404, "no such window", "Window not found" };
  143. // FIXME: 2. Handle any user prompts and return its value if it is an error.
  144. // 3. Traverse the history by a delta 1 for the current browsing context.
  145. m_browser_connection->async_forward();
  146. // FIXME: 4. If the previous step completed results in a pageHide event firing, wait until pageShow event
  147. // fires or for the session page load timeout milliseconds to pass, whichever occurs sooner.
  148. // FIXME: 5. If the previous step completed by the session page load timeout being reached, and user
  149. // prompts have been handled, return error with error code timeout.
  150. // 6. Return success with data null.
  151. return JsonValue();
  152. }
  153. // 10.5 Refresh, https://w3c.github.io/webdriver/#dfn-refresh
  154. ErrorOr<JsonValue, HttpError> Session::refresh()
  155. {
  156. // 1. If the current top-level browsing context is no longer open, return error with error code no such window.
  157. auto current_window = this->current_window();
  158. if (!current_window.has_value())
  159. return HttpError { 404, "no such window", "Window not found" };
  160. // FIXME: 2. Handle any user prompts and return its value if it is an error.
  161. // 3. Initiate an overridden reload of the current top-level browsing context’s active document.
  162. m_browser_connection->async_refresh();
  163. // FIXME: 4. If url is special except for file:
  164. // FIXME: 1. Try to wait for navigation to complete.
  165. // FIXME: 2. Try to run the post-navigation checks.
  166. // FIXME: 5. Set the current browsing context with current top-level browsing context.
  167. // 6. Return success with data null.
  168. return JsonValue();
  169. }
  170. // 10.6 Get Title, https://w3c.github.io/webdriver/#dfn-get-title
  171. ErrorOr<JsonValue, HttpError> Session::get_title()
  172. {
  173. // 1. If the current top-level browsing context is no longer open, return error with error code no such window.
  174. auto current_window = this->current_window();
  175. if (!current_window.has_value())
  176. return HttpError { 404, "no such window", "Window not found" };
  177. // FIXME: 2. Handle any user prompts and return its value if it is an error.
  178. // 3. Let title be the initial value of the title IDL attribute of the current top-level browsing context's active document.
  179. // 4. Return success with data title.
  180. return JsonValue(m_browser_connection->get_title());
  181. }
  182. // 11.1 Get Window Handle, https://w3c.github.io/webdriver/#get-window-handle
  183. ErrorOr<JsonValue, HttpError> Session::get_window_handle()
  184. {
  185. // 1. If the current top-level browsing context is no longer open, return error with error code no such window.
  186. auto current_window = this->current_window();
  187. if (!current_window.has_value())
  188. return HttpError { 404, "no such window", "Window not found" };
  189. // 2. Return success with data being the window handle associated with the current top-level browsing context.
  190. return m_current_window_handle;
  191. }
  192. // 11.2 Close Window, https://w3c.github.io/webdriver/#dfn-close-window
  193. ErrorOr<void, Variant<HttpError, Error>> Session::close_window()
  194. {
  195. // 1. If the current top-level browsing context is no longer open, return error with error code no such window.
  196. auto current_window = this->current_window();
  197. if (!current_window.has_value())
  198. return Variant<HttpError, Error>(HttpError { 404, "no such window", "Window not found" });
  199. // 2. Close the current top-level browsing context.
  200. m_windows.remove(m_current_window_handle);
  201. // 3. If there are no more open top-level browsing contexts, then close the session.
  202. if (m_windows.is_empty()) {
  203. auto result = stop();
  204. if (result.is_error()) {
  205. return Variant<HttpError, Error>(result.release_error());
  206. }
  207. }
  208. return {};
  209. }
  210. // 11.4 Get Window Handles, https://w3c.github.io/webdriver/#dfn-get-window-handles
  211. ErrorOr<JsonValue, HttpError> Session::get_window_handles() const
  212. {
  213. // 1. Let handles be a JSON List.
  214. auto handles = JsonArray {};
  215. // 2. For each top-level browsing context in the remote end, push the associated window handle onto handles.
  216. for (auto const& window_handle : m_windows.keys())
  217. handles.append(window_handle);
  218. // 3. Return success with data handles.
  219. return handles;
  220. }
  221. // https://w3c.github.io/webdriver/#dfn-get-or-create-a-web-element-reference
  222. static String get_or_create_a_web_element_reference(Session::LocalElement const& element)
  223. {
  224. // FIXME: 1. For each known element of the current browsing context’s list of known elements:
  225. // FIXME: 1. If known element equals element, return success with known element’s web element reference.
  226. // FIXME: 2. Add element to the list of known elements of the current browsing context.
  227. // FIXME: 3. Return success with the element’s web element reference.
  228. return String::formatted("{}", element.id);
  229. }
  230. // https://w3c.github.io/webdriver/#dfn-web-element-identifier
  231. static const String web_element_identifier = "element-6066-11e4-a52e-4f735466cecf";
  232. // https://w3c.github.io/webdriver/#dfn-web-element-reference-object
  233. static JsonObject web_element_reference_object(Session::LocalElement const& element)
  234. {
  235. // 1. Let identifier be the web element identifier.
  236. auto identifier = web_element_identifier;
  237. // 2. Let reference be the result of get or create a web element reference given element.
  238. auto reference = get_or_create_a_web_element_reference(element);
  239. // 3. Return a JSON Object initialized with a property with name identifier and value reference.
  240. JsonObject object;
  241. object.set("name"sv, identifier);
  242. object.set("value"sv, reference);
  243. return object;
  244. }
  245. // https://w3c.github.io/webdriver/#dfn-find
  246. ErrorOr<JsonArray, HttpError> Session::find(Session::LocalElement const& start_node, StringView const& using_, StringView const& value)
  247. {
  248. // 1. Let end time be the current time plus the session implicit wait timeout.
  249. auto end_time = Time::now_monotonic() + Time::from_milliseconds(static_cast<i64>(m_timeouts_configuration.implicit_wait_timeout));
  250. // 2. Let location strategy be equal to using.
  251. auto location_strategy = using_;
  252. // 3. Let selector be equal to value.
  253. auto selector = value;
  254. // 4. Let elements returned be the result of trying to call the relevant element location strategy with arguments start node, and selector.
  255. auto location_strategy_handler = s_locator_strategies.first_matching([&](LocatorStrategy const& match) { return match.name == location_strategy; });
  256. if (!location_strategy_handler.has_value())
  257. return HttpError { 400, "invalid argument", "No valid location strategy" };
  258. auto elements_or_error = (this->*location_strategy_handler.value().handler)(start_node, selector);
  259. // 5. If a DOMException, SyntaxError, XPathException, or other error occurs during the execution of the element location strategy, return error invalid selector.
  260. if (elements_or_error.is_error())
  261. return HttpError { 400, "invalid selector", String::formatted("The location strategy could not finish: {}", elements_or_error.release_error().message) };
  262. auto elements = elements_or_error.release_value();
  263. // FIXME: 6. If elements returned is empty and the current time is less than end time return to step 4. Otherwise, continue to the next step.
  264. (void)end_time;
  265. // 7. Let result be an empty JSON List.
  266. auto result = JsonArray();
  267. // 8. For each element in elements returned, append the web element reference object for element, to result.
  268. for (auto const& element : elements) {
  269. result.append(JsonValue(web_element_reference_object(element)));
  270. }
  271. // 9. Return success with data result.
  272. return result;
  273. }
  274. // https://w3c.github.io/webdriver/#dfn-table-of-location-strategies
  275. Vector<Session::LocatorStrategy> Session::s_locator_strategies = {
  276. { "css selector", &Session::locator_strategy_css_selectors },
  277. { "link text", &Session::locator_strategy_link_text },
  278. { "partial link text", &Session::locator_strategy_partial_link_text },
  279. { "tag name", &Session::locator_strategy_tag_name },
  280. { "xpath", &Session::locator_strategy_x_path },
  281. };
  282. // https://w3c.github.io/webdriver/#css-selectors
  283. ErrorOr<Vector<Session::LocalElement>, HttpError> Session::locator_strategy_css_selectors(Session::LocalElement const& start_node, StringView const& selector)
  284. {
  285. // 1. Let elements be the result of calling querySelectorAll() with start node as this and selector as the argument.
  286. // If this causes an exception to be thrown, return error with error code invalid selector.
  287. auto elements_ids = m_browser_connection->query_selector_all(start_node.id, selector);
  288. if (!elements_ids.has_value())
  289. return HttpError { 400, "invalid selector", "query_selector_all returned failed!" };
  290. Vector<Session::LocalElement> elements;
  291. for (auto id : elements_ids.release_value()) {
  292. elements.append({ id });
  293. }
  294. // 2.Return success with data elements.
  295. return elements;
  296. }
  297. // https://w3c.github.io/webdriver/#link-text
  298. ErrorOr<Vector<Session::LocalElement>, HttpError> Session::locator_strategy_link_text(Session::LocalElement const&, StringView const&)
  299. {
  300. // FIXME: Implement
  301. return HttpError { 501, "not implemented", "locator strategy link text" };
  302. }
  303. // https://w3c.github.io/webdriver/#partial-link-text
  304. ErrorOr<Vector<Session::LocalElement>, HttpError> Session::locator_strategy_partial_link_text(Session::LocalElement const&, StringView const&)
  305. {
  306. // FIXME: Implement
  307. return HttpError { 501, "not implemented", "locator strategy partial link text" };
  308. }
  309. // https://w3c.github.io/webdriver/#tag-name
  310. ErrorOr<Vector<Session::LocalElement>, HttpError> Session::locator_strategy_tag_name(Session::LocalElement const&, StringView const&)
  311. {
  312. // FIXME: Implement
  313. return HttpError { 501, "not implemented", "locator strategy tag name" };
  314. }
  315. // https://w3c.github.io/webdriver/#xpath
  316. ErrorOr<Vector<Session::LocalElement>, HttpError> Session::locator_strategy_x_path(Session::LocalElement const&, StringView const&)
  317. {
  318. // FIXME: Implement
  319. return HttpError { 501, "not implemented", "locator strategy XPath" };
  320. }
  321. // 12.3.2 Find Element, https://w3c.github.io/webdriver/#dfn-find-element
  322. ErrorOr<JsonValue, HttpError> Session::find_element(JsonValue const& payload)
  323. {
  324. if (!payload.is_object())
  325. return HttpError { 400, "invalid argument", "Payload is not a JSON object" };
  326. auto const& properties = payload.as_object();
  327. // 1. Let location strategy be the result of getting a property called "using".
  328. if (!properties.has("using"sv))
  329. return HttpError { 400, "invalid argument", "No property called 'using' present" };
  330. auto const& maybe_location_strategy = properties.get("using"sv);
  331. if (!maybe_location_strategy.is_string())
  332. return HttpError { 400, "invalid argument", "Property 'using' is not a String" };
  333. auto location_strategy = maybe_location_strategy.to_string();
  334. // 2. If location strategy is not present as a keyword in the table of location strategies, return error with error code invalid argument.
  335. if (!s_locator_strategies.first_matching([&](LocatorStrategy const& match) { return match.name == location_strategy; }).has_value())
  336. return HttpError { 400, "invalid argument", "No valid location strategy" };
  337. // 3. Let selector be the result of getting a property called "value".
  338. // 4. If selector is undefined, return error with error code invalid argument.
  339. if (!properties.has("value"sv))
  340. return HttpError { 400, "invalid argument", "No property called 'value' present" };
  341. auto const& maybe_selector = properties.get("value"sv);
  342. if (!maybe_selector.is_string())
  343. return HttpError { 400, "invalid argument", "Property 'value' is not a String" };
  344. auto selector = maybe_selector.to_string();
  345. // 5. If the current browsing context is no longer open, return error with error code no such window.
  346. auto current_window = this->current_window();
  347. if (!current_window.has_value())
  348. return HttpError { 404, "no such window", "Window not found" };
  349. // FIXME: 6. Handle any user prompts and return its value if it is an error.
  350. // 7. Let start node be the current browsing context’s document element.
  351. auto maybe_start_node_id = m_browser_connection->get_document_element();
  352. // 8. If start node is null, return error with error code no such element.
  353. if (!maybe_start_node_id.has_value())
  354. return HttpError { 404, "no such element", "document element does not exist" };
  355. auto start_node_id = maybe_start_node_id.release_value();
  356. LocalElement start_node = { start_node_id };
  357. // 9. Let result be the result of trying to Find with start node, location strategy, and selector.
  358. auto result = TRY(find(start_node, location_strategy, selector));
  359. // 10. If result is empty, return error with error code no such element. Otherwise, return the first element of result.
  360. if (result.is_empty())
  361. return HttpError { 404, "no such element", "the requested element does not exist" };
  362. return JsonValue(result.at(0));
  363. }
  364. // 12.3.3 Find Elements, https://w3c.github.io/webdriver/#dfn-find-elements
  365. ErrorOr<JsonValue, HttpError> Session::find_elements(JsonValue const& payload)
  366. {
  367. if (!payload.is_object())
  368. return HttpError { 400, "invalid argument", "Payload is not a JSON object" };
  369. auto const& properties = payload.as_object();
  370. // 1. Let location strategy be the result of getting a property called "using".
  371. if (!properties.has("using"sv))
  372. return HttpError { 400, "invalid argument", "No property called 'using' present" };
  373. auto const& maybe_location_strategy = properties.get("using"sv);
  374. if (!maybe_location_strategy.is_string())
  375. return HttpError { 400, "invalid argument", "Property 'using' is not a String" };
  376. auto location_strategy = maybe_location_strategy.to_string();
  377. // 2. If location strategy is not present as a keyword in the table of location strategies, return error with error code invalid argument.
  378. if (!s_locator_strategies.first_matching([&](LocatorStrategy const& match) { return match.name == location_strategy; }).has_value())
  379. return HttpError { 400, "invalid argument", "No valid location strategy" };
  380. // 3. Let selector be the result of getting a property called "value".
  381. // 4. If selector is undefined, return error with error code invalid argument.
  382. if (!properties.has("value"sv))
  383. return HttpError { 400, "invalid argument", "No property called 'value' present" };
  384. auto const& maybe_selector = properties.get("value"sv);
  385. if (!maybe_selector.is_string())
  386. return HttpError { 400, "invalid argument", "Property 'value' is not a String" };
  387. auto selector = maybe_selector.to_string();
  388. // 5. If the current browsing context is no longer open, return error with error code no such window.
  389. auto current_window = this->current_window();
  390. if (!current_window.has_value())
  391. return HttpError { 404, "no such window", "Window not found" };
  392. // FIXME: 6. Handle any user prompts and return its value if it is an error.
  393. // 7. Let start node be the current browsing context’s document element.
  394. auto maybe_start_node_id = m_browser_connection->get_document_element();
  395. // 8. If start node is null, return error with error code no such element.
  396. if (!maybe_start_node_id.has_value())
  397. return HttpError { 404, "no such element", "document element does not exist" };
  398. auto start_node_id = maybe_start_node_id.release_value();
  399. LocalElement start_node = { start_node_id };
  400. // 9. Return the result of trying to Find with start node, location strategy, and selector.
  401. auto result = TRY(find(start_node, location_strategy, selector));
  402. return JsonValue(result);
  403. }
  404. // 12.3.4 Find Element From Element, https://w3c.github.io/webdriver/#dfn-find-element-from-element
  405. ErrorOr<JsonValue, HttpError> Session::find_element_from_element(JsonValue const& payload, StringView parameter_element_id)
  406. {
  407. if (!payload.is_object())
  408. return HttpError { 400, "invalid argument", "Payload is not a JSON object" };
  409. auto const& properties = payload.as_object();
  410. // 1. Let location strategy be the result of getting a property called "using".
  411. if (!properties.has("using"sv))
  412. return HttpError { 400, "invalid argument", "No property called 'using' present" };
  413. auto const& maybe_location_strategy = properties.get("using"sv);
  414. if (!maybe_location_strategy.is_string())
  415. return HttpError { 400, "invalid argument", "Property 'using' is not a String" };
  416. auto location_strategy = maybe_location_strategy.to_string();
  417. // 2. If location strategy is not present as a keyword in the table of location strategies, return error with error code invalid argument.
  418. if (!s_locator_strategies.first_matching([&](LocatorStrategy const& match) { return match.name == location_strategy; }).has_value())
  419. return HttpError { 400, "invalid argument", "No valid location strategy" };
  420. // 3. Let selector be the result of getting a property called "value".
  421. // 4. If selector is undefined, return error with error code invalid argument.
  422. if (!properties.has("value"sv))
  423. return HttpError { 400, "invalid argument", "No property called 'value' present" };
  424. auto const& maybe_selector = properties.get("value"sv);
  425. if (!maybe_selector.is_string())
  426. return HttpError { 400, "invalid argument", "Property 'value' is not a String" };
  427. auto selector = maybe_selector.to_string();
  428. // 5. If the current browsing context is no longer open, return error with error code no such window.
  429. auto current_window = this->current_window();
  430. if (!current_window.has_value())
  431. return HttpError { 404, "no such window", "Window not found" };
  432. // FIXME: 6. Handle any user prompts and return its value if it is an error.
  433. // FIXME: 7. Let start node be the result of trying to get a known connected element with url variable element id.
  434. // NOTE: The whole concept of "connected elements" is not implemented yet. See get_or_create_a_web_element_reference()
  435. // For now the element is only represented by its ID
  436. auto maybe_element_id = parameter_element_id.to_int();
  437. if (!maybe_element_id.has_value())
  438. return HttpError { 400, "invalid argument", "Element ID is not an i32" };
  439. auto element_id = maybe_element_id.release_value();
  440. LocalElement start_node = { element_id };
  441. // 8. Let result be the value of trying to Find with start node, location strategy, and selector.
  442. auto result = TRY(find(start_node, location_strategy, selector));
  443. // 9. If result is empty, return error with error code no such element. Otherwise, return the first element of result.
  444. if (result.is_empty())
  445. return HttpError { 404, "no such element", "the requested element does not exist" };
  446. return JsonValue(result.at(0));
  447. }
  448. // 12.3.5 Find Elements From Element, https://w3c.github.io/webdriver/#dfn-find-elements-from-element
  449. ErrorOr<JsonValue, HttpError> Session::find_elements_from_element(JsonValue const& payload, StringView parameter_element_id)
  450. {
  451. if (!payload.is_object())
  452. return HttpError { 400, "invalid argument", "Payload is not a JSON object" };
  453. auto const& properties = payload.as_object();
  454. // 1. Let location strategy be the result of getting a property called "using".
  455. if (!properties.has("using"sv))
  456. return HttpError { 400, "invalid argument", "No property called 'using' present" };
  457. auto const& maybe_location_strategy = properties.get("using"sv);
  458. if (!maybe_location_strategy.is_string())
  459. return HttpError { 400, "invalid argument", "Property 'using' is not a String" };
  460. auto location_strategy = maybe_location_strategy.to_string();
  461. // 2. If location strategy is not present as a keyword in the table of location strategies, return error with error code invalid argument.
  462. if (!s_locator_strategies.first_matching([&](LocatorStrategy const& match) { return match.name == location_strategy; }).has_value())
  463. return HttpError { 400, "invalid argument", "No valid location strategy" };
  464. // 3. Let selector be the result of getting a property called "value".
  465. // 4. If selector is undefined, return error with error code invalid argument.
  466. if (!properties.has("value"sv))
  467. return HttpError { 400, "invalid argument", "No property called 'value' present" };
  468. auto const& maybe_selector = properties.get("value"sv);
  469. if (!maybe_selector.is_string())
  470. return HttpError { 400, "invalid argument", "Property 'value' is not a String" };
  471. auto selector = maybe_selector.to_string();
  472. // 5. If the current browsing context is no longer open, return error with error code no such window.
  473. auto current_window = this->current_window();
  474. if (!current_window.has_value())
  475. return HttpError { 404, "no such window", "Window not found" };
  476. // FIXME: 6. Handle any user prompts and return its value if it is an error.
  477. // FIXME: 7. Let start node be the result of trying to get a known connected element with url variable element id.
  478. // NOTE: The whole concept of "connected elements" is not implemented yet. See get_or_create_a_web_element_reference()
  479. // For now the element is only represented by its ID
  480. auto maybe_element_id = parameter_element_id.to_int();
  481. if (!maybe_element_id.has_value())
  482. return HttpError { 400, "invalid argument", "Element ID is not an i32" };
  483. auto element_id = maybe_element_id.release_value();
  484. LocalElement start_node = { element_id };
  485. // 8. Return the result of trying to Find with start node, location strategy, and selector.
  486. auto result = TRY(find(start_node, location_strategy, selector));
  487. return JsonValue(result);
  488. }
  489. // 12.4.2 Get Element Attribute, https://w3c.github.io/webdriver/#dfn-get-element-attribute
  490. ErrorOr<JsonValue, HttpError> Session::get_element_attribute(JsonValue const&, StringView parameter_element_id, StringView name)
  491. {
  492. // 1. If the current browsing context is no longer open, return error with error code no such window.
  493. auto current_window = this->current_window();
  494. if (!current_window.has_value())
  495. return HttpError { 404, "no such window", "Window not found" };
  496. // FIXME: 2. Handle any user prompts and return its value if it is an error.
  497. // FIXME: 3. Let element be the result of trying to get a known connected element with url variable element id.
  498. // NOTE: The whole concept of "connected elements" is not implemented yet. See get_or_create_a_web_element_reference()
  499. // For now the element is only represented by its ID
  500. auto maybe_element_id = parameter_element_id.to_int();
  501. if (!maybe_element_id.has_value())
  502. return HttpError { 400, "invalid argument", "Element ID is not an i32" };
  503. auto element_id = maybe_element_id.release_value();
  504. // FIXME: The case that the element does not exist is not handled at all and null is returned in that case.
  505. // 4. Let result be the result of the first matching condition:
  506. // -> FIXME: If name is a boolean attribute
  507. // NOTE: LibWeb doesn't know about boolean attributes directly
  508. // "true" (string) if the element has the attribute, otherwise null.
  509. // -> Otherwise
  510. // The result of getting an attribute by name name.
  511. auto result = m_browser_connection->get_element_attribute(element_id, name);
  512. if (!result.has_value())
  513. return JsonValue(AK::JsonValue::Type::Null);
  514. // 5. Return success with data result.
  515. return JsonValue(result.release_value());
  516. }
  517. // https://w3c.github.io/webdriver/#dfn-serialized-cookie
  518. static JsonObject serialize_cookie(Web::Cookie::Cookie const& cookie)
  519. {
  520. JsonObject serialized_cookie = {};
  521. serialized_cookie.set("name", cookie.name);
  522. serialized_cookie.set("value", cookie.value);
  523. serialized_cookie.set("path", cookie.path);
  524. serialized_cookie.set("domain", cookie.domain);
  525. serialized_cookie.set("secure", cookie.secure);
  526. serialized_cookie.set("httpOnly", cookie.http_only);
  527. serialized_cookie.set("expiry", cookie.expiry_time.timestamp());
  528. // FIXME: Add sameSite to Cookie and serialize it here too.
  529. return serialized_cookie;
  530. }
  531. // 14.1 Get All Cookies, https://w3c.github.io/webdriver/#dfn-get-all-cookies
  532. ErrorOr<JsonValue, HttpError> Session::get_all_cookies()
  533. {
  534. // 1. If the current browsing context is no longer open, return error with error code no such window.
  535. auto current_window = this->current_window();
  536. if (!current_window.has_value())
  537. return HttpError { 404, "no such window", "Window not found" };
  538. // FIXME: 2. Handle any user prompts, and return its value if it is an error.
  539. // 3. Let cookies be a new JSON List.
  540. JsonArray cookies = {};
  541. // 4. For each cookie in all associated cookies of the current browsing context’s active document:
  542. for (auto const& cookie : m_browser_connection->get_all_cookies()) {
  543. // 1. Let serialized cookie be the result of serializing cookie.
  544. auto serialized_cookie = serialize_cookie(cookie);
  545. // 2. Append serialized cookie to cookies
  546. cookies.append(serialized_cookie);
  547. }
  548. // 5. Return success with data cookies.
  549. return JsonValue(cookies);
  550. }
  551. // 14.2 Get Named Cookie, https://w3c.github.io/webdriver/#dfn-get-named-cookie
  552. ErrorOr<JsonValue, HttpError> Session::get_named_cookie(String const& name)
  553. {
  554. // 1. If the current browsing context is no longer open, return error with error code no such window.
  555. auto current_window = this->current_window();
  556. if (!current_window.has_value())
  557. return HttpError { 404, "no such window", "Window not found" };
  558. // FIXME: 2. Handle any user prompts, and return its value if it is an error.
  559. // 3. If the url variable name is equal to a cookie’s cookie name amongst all associated cookies of the
  560. // current browsing context’s active document, return success with the serialized cookie as data.
  561. auto maybe_cookie = m_browser_connection->get_named_cookie(name);
  562. if (maybe_cookie.has_value()) {
  563. auto cookie = maybe_cookie.release_value();
  564. auto serialized_cookie = serialize_cookie(cookie);
  565. return JsonValue(serialized_cookie);
  566. }
  567. // 4. Otherwise, return error with error code no such cookie.
  568. return HttpError { 404, "no such cookie", "Cookie not found" };
  569. }
  570. // 14.3 Add Cookie, https://w3c.github.io/webdriver/#dfn-adding-a-cookie
  571. ErrorOr<JsonValue, HttpError> Session::add_cookie(JsonValue const& payload)
  572. {
  573. // 1. Let data be the result of getting a property named cookie from the parameters argument.
  574. if (!payload.is_object() || !payload.as_object().has_object("cookie"sv))
  575. return HttpError { 400, "invalid argument", "Payload doesn't have a cookie object" };
  576. auto const& maybe_data = payload.as_object().get("cookie"sv);
  577. // 2. If data is not a JSON Object with all the required (non-optional) JSON keys listed in the table for cookie conversion,
  578. // return error with error code invalid argument.
  579. // NOTE: Table is here: https://w3c.github.io/webdriver/#dfn-table-for-cookie-conversion
  580. if (!maybe_data.is_object())
  581. return HttpError { 400, "invalid argument", "Value \"cookie\' is not an object" };
  582. auto const& data = maybe_data.as_object();
  583. if (!data.has("name"sv) || !data.has("value"sv))
  584. return HttpError { 400, "invalid argument", "Cookie-Object doesn't contain all required keys" };
  585. // 3. If the current browsing context is no longer open, return error with error code no such window.
  586. auto current_window = this->current_window();
  587. if (!current_window.has_value())
  588. return HttpError { 404, "no such window", "Window not found" };
  589. // FIXME: 4. Handle any user prompts, and return its value if it is an error.
  590. // FIXME: 5. If the current browsing context’s document element is a cookie-averse Document object,
  591. // return error with error code invalid cookie domain.
  592. // 6. If cookie name or cookie value is null,
  593. // FIXME: cookie domain is not equal to the current browsing context’s active document’s domain,
  594. // cookie secure only or cookie HTTP only are not boolean types,
  595. // or cookie expiry time is not an integer type, or it less than 0 or greater than the maximum safe integer,
  596. // return error with error code invalid argument.
  597. if (data.get("name"sv).is_null() || data.get("value"sv).is_null())
  598. return HttpError { 400, "invalid argument", "Cookie-Object is malformed: name or value are null" };
  599. if (data.has("secure"sv) && !data.get("secure"sv).is_bool())
  600. return HttpError { 400, "invalid argument", "Cookie-Object is malformed: secure is not bool" };
  601. if (data.has("httpOnly"sv) && !data.get("httpOnly"sv).is_bool())
  602. return HttpError { 400, "invalid argument", "Cookie-Object is malformed: httpOnly is not bool" };
  603. Optional<Core::DateTime> expiry_time;
  604. if (data.has("expiry"sv)) {
  605. auto expiry_argument = data.get("expiry"sv);
  606. if (!expiry_argument.is_u32()) {
  607. // NOTE: less than 0 or greater than safe integer are handled by the JSON parser
  608. return HttpError { 400, "invalid argument", "Cookie-Object is malformed: expiry is not u32" };
  609. }
  610. expiry_time = Core::DateTime::from_timestamp(expiry_argument.as_u32());
  611. }
  612. // 7. Create a cookie in the cookie store associated with the active document’s address using
  613. // cookie name name, cookie value value, and an attribute-value list of the following cookie concepts
  614. // listed in the table for cookie conversion from data:
  615. Web::Cookie::ParsedCookie cookie;
  616. if (auto name_attribute = data.get("name"sv); name_attribute.is_string())
  617. cookie.name = name_attribute.as_string();
  618. else
  619. return HttpError { 400, "invalid argument", "Expect name attribute to be string" };
  620. if (auto value_attribute = data.get("value"sv); value_attribute.is_string())
  621. cookie.value = value_attribute.as_string();
  622. else
  623. return HttpError { 400, "invalid argument", "Expect value attribute to be string" };
  624. // Cookie path
  625. // The value if the entry exists, otherwise "/".
  626. if (data.has("path"sv)) {
  627. if (auto path_attribute = data.get("path"sv); path_attribute.is_string())
  628. cookie.path = path_attribute.as_string();
  629. else
  630. return HttpError { 400, "invalid argument", "Expect path attribute to be string" };
  631. } else {
  632. cookie.path = "/";
  633. }
  634. // Cookie domain
  635. // The value if the entry exists, otherwise the current browsing context’s active document’s URL domain.
  636. // NOTE: The otherwise case is handled by the CookieJar
  637. if (data.has("domain"sv)) {
  638. if (auto domain_attribute = data.get("domain"sv); domain_attribute.is_string())
  639. cookie.domain = domain_attribute.as_string();
  640. else
  641. return HttpError { 400, "invalid argument", "Expect domain attribute to be string" };
  642. }
  643. // Cookie secure only
  644. // The value if the entry exists, otherwise false.
  645. if (data.has("secure"sv)) {
  646. cookie.secure_attribute_present = data.get("secure"sv).as_bool();
  647. } else {
  648. cookie.secure_attribute_present = false;
  649. }
  650. // Cookie HTTP only
  651. // The value if the entry exists, otherwise false.
  652. if (data.has("httpOnly"sv)) {
  653. cookie.http_only_attribute_present = data.get("httpOnly"sv).as_bool();
  654. } else {
  655. cookie.http_only_attribute_present = false;
  656. }
  657. // Cookie expiry time
  658. // The value if the entry exists, otherwise leave unset to indicate that this is a session cookie.
  659. cookie.expiry_time_from_expires_attribute = expiry_time;
  660. // FIXME: Cookie same site
  661. // The value if the entry exists, otherwise leave unset to indicate that no same site policy is defined.
  662. m_browser_connection->async_add_cookie(move(cookie));
  663. // If there is an error during this step, return error with error code unable to set cookie.
  664. // NOTE: This probably should only apply to the actual setting of the cookie in the Browser,
  665. // which cannot fail in our case.
  666. // Thus, the error-codes used above are 400 "invalid argument".
  667. // 8. Return success with data null.
  668. return JsonValue();
  669. }
  670. // https://w3c.github.io/webdriver/#dfn-delete-cookies
  671. void Session::delete_cookies(Optional<StringView> const& name)
  672. {
  673. // For each cookie among all associated cookies of the current browsing context’s active document,
  674. // run the substeps of the first matching condition:
  675. for (auto& cookie : m_browser_connection->get_all_cookies()) {
  676. // -> name is undefined
  677. // -> name is equal to cookie name
  678. if (!name.has_value() || name.value() == cookie.name) {
  679. // Set the cookie expiry time to a Unix timestamp in the past.
  680. cookie.expiry_time = Core::DateTime::from_timestamp(0);
  681. m_browser_connection->async_update_cookie(cookie);
  682. }
  683. // -> Otherwise
  684. // Do nothing.
  685. }
  686. }
  687. // 14.4 Delete Cookie, https://w3c.github.io/webdriver/#dfn-delete-cookie
  688. ErrorOr<JsonValue, HttpError> Session::delete_cookie(StringView const& name)
  689. {
  690. // 1. If the current browsing context is no longer open, return error with error code no such window.
  691. auto current_window = this->current_window();
  692. if (!current_window.has_value())
  693. return HttpError { 404, "no such window", "Window not found" };
  694. // FIXME: 2. Handle any user prompts, and return its value if it is an error.
  695. // 3. Delete cookies using the url variable name parameter as the filter argument.
  696. delete_cookies(name);
  697. // 4. Return success with data null.
  698. return JsonValue();
  699. }
  700. // 14.5 Delete All Cookies, https://w3c.github.io/webdriver/#dfn-delete-all-cookies
  701. ErrorOr<JsonValue, HttpError> Session::delete_all_cookies()
  702. {
  703. // 1. If the current browsing context is no longer open, return error with error code no such window.
  704. auto current_window = this->current_window();
  705. if (!current_window.has_value())
  706. return HttpError { 404, "no such window", "Window not found" };
  707. // FIXME: 2. Handle any user prompts, and return its value if it is an error.
  708. // 3. Delete cookies, giving no filtering argument.
  709. delete_cookies();
  710. // 4. Return success with data null.
  711. return JsonValue();
  712. }
  713. }