/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use cookie_rs::Cookie; use dom::bindings::codegen::Bindings::CSSStyleDeclarationBinding::CSSStyleDeclarationMethods; use dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods; use dom::bindings::codegen::Bindings::ElementBinding::ElementMethods; use dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods; use dom::bindings::codegen::Bindings::HTMLInputElementBinding::HTMLInputElementMethods; use dom::bindings::codegen::Bindings::HTMLOptionElementBinding::HTMLOptionElementMethods; use dom::bindings::codegen::Bindings::NodeBinding::NodeMethods; use dom::bindings::codegen::Bindings::WindowBinding::WindowMethods; use dom::bindings::conversions::{ConversionResult, FromJSValConvertible, StringificationBehavior}; use dom::bindings::inheritance::Castable; use dom::bindings::root::DomRoot; use dom::bindings::str::DOMString; use dom::element::Element; use dom::globalscope::GlobalScope; use dom::htmlelement::HTMLElement; use dom::htmliframeelement::HTMLIFrameElement; use dom::htmlinputelement::HTMLInputElement; use dom::htmloptionelement::HTMLOptionElement; use dom::node::{Node, window_from_node}; use euclid::{Point2D, Rect, Size2D}; use hyper_serde::Serde; use ipc_channel::ipc::{self, IpcSender}; use js::jsapi::{HandleValue, JSContext}; use js::jsval::UndefinedValue; use msg::constellation_msg::BrowsingContextId; use msg::constellation_msg::PipelineId; use net_traits::CookieSource::{HTTP, NonHTTP}; use net_traits::CoreResourceMsg::{GetCookiesDataForUrl, SetCookieForUrl}; use net_traits::IpcSend; use script_thread::Documents; use script_traits::webdriver_msg::{WebDriverFrameId, WebDriverJSError, WebDriverJSResult, WebDriverJSValue}; use script_traits::webdriver_msg::WebDriverCookieError; use servo_url::ServoUrl; fn find_node_by_unique_id(documents: &Documents, pipeline: PipelineId, node_id: String) -> Option> { documents.find_document(pipeline).and_then(|document| document.upcast::().traverse_preorder().find(|candidate| candidate.unique_id() == node_id) ) } #[allow(unsafe_code)] pub unsafe fn jsval_to_webdriver(cx: *mut JSContext, val: HandleValue) -> WebDriverJSResult { if val.get().is_undefined() { Ok(WebDriverJSValue::Undefined) } else if val.get().is_boolean() { Ok(WebDriverJSValue::Boolean(val.get().to_boolean())) } else if val.get().is_double() || val.get().is_int32() { Ok(WebDriverJSValue::Number(match FromJSValConvertible::from_jsval(cx, val, ()).unwrap() { ConversionResult::Success(c) => c, _ => unreachable!(), })) } else if val.get().is_string() { //FIXME: use jsstring_to_str when jsval grows to_jsstring let string: DOMString = match FromJSValConvertible::from_jsval(cx, val, StringificationBehavior::Default) .unwrap() { ConversionResult::Success(c) => c, _ => unreachable!(), }; Ok(WebDriverJSValue::String(String::from(string))) } else if val.get().is_null() { Ok(WebDriverJSValue::Null) } else { Err(WebDriverJSError::UnknownType) } } #[allow(unsafe_code)] pub fn handle_execute_script(documents: &Documents, pipeline: PipelineId, eval: String, reply: IpcSender) { let window = match documents.find_window(pipeline) { Some(window) => window, None => return reply.send(Err(WebDriverJSError::BrowsingContextNotFound)).unwrap() }; let result = unsafe { let cx = window.get_cx(); rooted!(in(cx) let mut rval = UndefinedValue()); window.upcast::().evaluate_js_on_global_with_result( &eval, rval.handle_mut()); jsval_to_webdriver(cx, rval.handle()) }; reply.send(result).unwrap(); } pub fn handle_execute_async_script(documents: &Documents, pipeline: PipelineId, eval: String, reply: IpcSender) { let window = match documents.find_window(pipeline) { Some(window) => window, None => return reply.send(Err(WebDriverJSError::BrowsingContextNotFound)).unwrap() }; let cx = window.get_cx(); window.set_webdriver_script_chan(Some(reply)); rooted!(in(cx) let mut rval = UndefinedValue()); window.upcast::().evaluate_js_on_global_with_result(&eval, rval.handle_mut()); } pub fn handle_get_browsing_context_id(documents: &Documents, pipeline: PipelineId, webdriver_frame_id: WebDriverFrameId, reply: IpcSender>) { let result = match webdriver_frame_id { WebDriverFrameId::Short(_) => { // This isn't supported yet Err(()) }, WebDriverFrameId::Element(x) => { find_node_by_unique_id(documents, pipeline, x) .and_then(|node| node.downcast::().and_then(|elem| elem.browsing_context_id())) .ok_or(()) }, WebDriverFrameId::Parent => { documents.find_window(pipeline) .and_then(|window| window.window_proxy().parent().map(|parent| parent.browsing_context_id())) .ok_or(()) } }; reply.send(result).unwrap() } pub fn handle_find_element_css(documents: &Documents, pipeline: PipelineId, selector: String, reply: IpcSender, ()>>) { let node_id = documents.find_document(pipeline) .ok_or(()) .and_then(|doc| doc.QuerySelector(DOMString::from(selector)).map_err(|_| ())) .map(|node| node.map(|x| x.upcast::().unique_id())); reply.send(node_id).unwrap(); } pub fn handle_find_elements_css(documents: &Documents, pipeline: PipelineId, selector: String, reply: IpcSender, ()>>) { let node_ids = documents.find_document(pipeline) .ok_or(()) .and_then(|doc| doc.QuerySelectorAll(DOMString::from(selector)).map_err(|_| ())) .map(|nodes| nodes.iter().map(|x| x.upcast::().unique_id()).collect()); reply.send(node_ids).unwrap(); } pub fn handle_focus_element(documents: &Documents, pipeline: PipelineId, element_id: String, reply: IpcSender>) { reply.send(match find_node_by_unique_id(documents, pipeline, element_id) { Some(ref node) => { match node.downcast::() { Some(ref elem) => { // Need a way to find if this actually succeeded elem.Focus(); Ok(()) } None => Err(()) } }, None => Err(()) }).unwrap(); } pub fn handle_get_active_element(documents: &Documents, pipeline: PipelineId, reply: IpcSender>) { reply.send(documents.find_document(pipeline) .and_then(|doc| doc.GetActiveElement()) .map(|elem| elem.upcast::().unique_id())).unwrap(); } pub fn handle_get_cookies(documents: &Documents, pipeline: PipelineId, reply: IpcSender>>>) { // TODO: Return an error if the pipeline doesn't exist? let cookies = match documents.find_document(pipeline) { None => Vec::new(), Some(document) => { let url = document.url(); let (sender, receiver) = ipc::channel().unwrap(); let _ = document.window().upcast::().resource_threads().send( GetCookiesDataForUrl(url, sender, NonHTTP) ); receiver.recv().unwrap() }, }; reply.send(cookies).unwrap(); } // https://w3c.github.io/webdriver/webdriver-spec.html#get-cookie pub fn handle_get_cookie(documents: &Documents, pipeline: PipelineId, name: String, reply: IpcSender>>>) { // TODO: Return an error if the pipeline doesn't exist? let cookies = match documents.find_document(pipeline) { None => Vec::new(), Some(document) => { let url = document.url(); let (sender, receiver) = ipc::channel().unwrap(); let _ = document.window().upcast::().resource_threads().send( GetCookiesDataForUrl(url, sender, NonHTTP) ); receiver.recv().unwrap() }, }; reply.send(cookies.into_iter().filter(|c| c.name() == &*name).collect()).unwrap(); } // https://w3c.github.io/webdriver/webdriver-spec.html#add-cookie pub fn handle_add_cookie(documents: &Documents, pipeline: PipelineId, cookie: Cookie<'static>, reply: IpcSender>) { // TODO: Return a different error if the pipeline doesn't exist? let document = match documents.find_document(pipeline) { Some(document) => document, None => return reply.send(Err(WebDriverCookieError::UnableToSetCookie)).unwrap(), }; let url = document.url(); let method = if cookie.http_only() { HTTP } else { NonHTTP }; let domain = cookie.domain().map(ToOwned::to_owned); reply.send(match (document.is_cookie_averse(), domain) { (true, _) => Err(WebDriverCookieError::InvalidDomain), (false, Some(ref domain)) if url.host_str().map(|x| { x == domain }).unwrap_or(false) => { let _ = document.window().upcast::().resource_threads().send( SetCookieForUrl(url, Serde(cookie), method) ); Ok(()) }, (false, None) => { let _ = document.window().upcast::().resource_threads().send( SetCookieForUrl(url, Serde(cookie), method) ); Ok(()) }, (_, _) => { Err(WebDriverCookieError::UnableToSetCookie) }, }).unwrap(); } pub fn handle_get_title(documents: &Documents, pipeline: PipelineId, reply: IpcSender) { // TODO: Return an error if the pipeline doesn't exist. let title = documents.find_document(pipeline) .map(|doc| String::from(doc.Title())) .unwrap_or_default(); reply.send(title).unwrap(); } pub fn handle_get_rect(documents: &Documents, pipeline: PipelineId, element_id: String, reply: IpcSender, ()>>) { reply.send(match find_node_by_unique_id(documents, pipeline, element_id) { Some(elem) => { // https://w3c.github.io/webdriver/webdriver-spec.html#dfn-calculate-the-absolute-position match elem.downcast::() { Some(html_elem) => { // Step 1 let mut x = 0; let mut y = 0; let mut offset_parent = html_elem.GetOffsetParent(); // Step 2 while let Some(element) = offset_parent { offset_parent = match element.downcast::() { Some(elem) => { x += elem.OffsetLeft(); y += elem.OffsetTop(); elem.GetOffsetParent() }, None => None }; } // Step 3 Ok(Rect::new(Point2D::new(x as f64, y as f64), Size2D::new(html_elem.OffsetWidth() as f64, html_elem.OffsetHeight() as f64))) }, None => Err(()) } }, None => Err(()) }).unwrap(); } pub fn handle_get_text(documents: &Documents, pipeline: PipelineId, node_id: String, reply: IpcSender>) { reply.send(match find_node_by_unique_id(documents, pipeline, node_id) { Some(ref node) => { Ok(node.GetTextContent().map_or("".to_owned(), String::from)) }, None => Err(()) }).unwrap(); } pub fn handle_get_name(documents: &Documents, pipeline: PipelineId, node_id: String, reply: IpcSender>) { reply.send(match find_node_by_unique_id(documents, pipeline, node_id) { Some(node) => { Ok(String::from(node.downcast::().unwrap().TagName())) }, None => Err(()) }).unwrap(); } pub fn handle_get_attribute(documents: &Documents, pipeline: PipelineId, node_id: String, name: String, reply: IpcSender, ()>>) { reply.send(match find_node_by_unique_id(documents, pipeline, node_id) { Some(node) => { Ok(node.downcast::().unwrap().GetAttribute(DOMString::from(name)) .map(String::from)) }, None => Err(()) }).unwrap(); } pub fn handle_get_css(documents: &Documents, pipeline: PipelineId, node_id: String, name: String, reply: IpcSender>) { reply.send(match find_node_by_unique_id(documents, pipeline, node_id) { Some(node) => { let window = window_from_node(&*node); let elem = node.downcast::().unwrap(); Ok(String::from( window.GetComputedStyle(&elem, None).GetPropertyValue(DOMString::from(name)))) }, None => Err(()) }).unwrap(); } pub fn handle_get_url(documents: &Documents, pipeline: PipelineId, reply: IpcSender) { // TODO: Return an error if the pipeline doesn't exist. let url = documents.find_document(pipeline) .map(|document| document.url()) .unwrap_or_else(|| ServoUrl::parse("about:blank").expect("infallible")); reply.send(url).unwrap(); } pub fn handle_is_enabled(documents: &Documents, pipeline: PipelineId, element_id: String, reply: IpcSender>) { reply.send(match find_node_by_unique_id(&documents, pipeline, element_id) { Some(ref node) => { match node.downcast::() { Some(elem) => Ok(elem.enabled_state()), None => Err(()) } }, None => Err(()) }).unwrap(); } pub fn handle_is_selected(documents: &Documents, pipeline: PipelineId, element_id: String, reply: IpcSender>) { reply.send(match find_node_by_unique_id(documents, pipeline, element_id) { Some(ref node) => { if let Some(input_element) = node.downcast::() { Ok(input_element.Checked()) } else if let Some(option_element) = node.downcast::() { Ok(option_element.Selected()) } else if node.is::() { Ok(false) // regular elements are not selectable } else { Err(()) } }, None => Err(()) }).unwrap(); }