diff options
Diffstat (limited to 'components/script/dom/window.rs')
-rw-r--r-- | components/script/dom/window.rs | 513 |
1 files changed, 513 insertions, 0 deletions
diff --git a/components/script/dom/window.rs b/components/script/dom/window.rs new file mode 100644 index 00000000000..23b6c71e029 --- /dev/null +++ b/components/script/dom/window.rs @@ -0,0 +1,513 @@ +/* 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 dom::bindings::codegen::Bindings::EventHandlerBinding::{OnErrorEventHandlerNonNull, EventHandlerNonNull}; +use dom::bindings::codegen::Bindings::WindowBinding; +use dom::bindings::codegen::Bindings::WindowBinding::WindowMethods; +use dom::bindings::codegen::InheritTypes::EventTargetCast; +use dom::bindings::error::{Fallible, InvalidCharacter}; +use dom::bindings::global; +use dom::bindings::js::{JS, JSRef, Temporary, OptionalSettable}; +use dom::bindings::trace::{Traceable, Untraceable}; +use dom::bindings::utils::{Reflectable, Reflector}; +use dom::browsercontext::BrowserContext; +use dom::console::Console; +use dom::document::Document; +use dom::eventtarget::{EventTarget, WindowTypeId, EventTargetHelpers}; +use dom::location::Location; +use dom::navigator::Navigator; +use dom::performance::Performance; +use dom::screen::Screen; +use layout_interface::{ReflowForDisplay, DocumentDamageLevel}; +use page::Page; +use script_task::{ExitWindowMsg, FireTimerMsg, ScriptChan, TriggerLoadMsg, TriggerFragmentMsg}; +use script_traits::ScriptControlChan; + +use servo_msg::compositor_msg::ScriptListener; +use servo_net::image_cache_task::ImageCacheTask; +use servo_util::str::{DOMString,HTML_SPACE_CHARACTERS}; +use servo_util::task::{spawn_named}; + +use js::jsapi::JS_CallFunctionValue; +use js::jsapi::JSContext; +use js::jsapi::{JS_GC, JS_GetRuntime}; +use js::jsval::JSVal; +use js::jsval::NullValue; +use js::rust::with_compartment; +use url::{Url, UrlParser}; + +use serialize::base64::{FromBase64, ToBase64, STANDARD}; +use std::collections::hashmap::HashMap; +use std::cell::{Cell, RefCell}; +use std::cmp; +use std::comm::{channel, Sender}; +use std::comm::Select; +use std::hash::{Hash, sip}; +use std::io::timer::Timer; +use std::ptr; +use std::rc::Rc; +use time; + +#[deriving(PartialEq, Encodable, Eq)] +pub struct TimerId(i32); + +#[deriving(Encodable)] +pub struct TimerHandle { + handle: TimerId, + pub data: TimerData, + cancel_chan: Untraceable<Option<Sender<()>>>, +} + +impl Hash for TimerId { + fn hash(&self, state: &mut sip::SipState) { + let TimerId(id) = *self; + id.hash(state); + } +} + +impl TimerHandle { + fn cancel(&mut self) { + self.cancel_chan.as_ref().map(|chan| chan.send_opt(()).ok()); + } +} + +#[deriving(Encodable)] +pub struct Window { + eventtarget: EventTarget, + pub script_chan: ScriptChan, + control_chan: ScriptControlChan, + console: Cell<Option<JS<Console>>>, + location: Cell<Option<JS<Location>>>, + navigator: Cell<Option<JS<Navigator>>>, + pub image_cache_task: ImageCacheTask, + pub active_timers: Traceable<RefCell<HashMap<TimerId, TimerHandle>>>, + next_timer_handle: Traceable<Cell<i32>>, + compositor: Untraceable<Box<ScriptListener>>, + pub browser_context: Traceable<RefCell<Option<BrowserContext>>>, + pub page: Rc<Page>, + performance: Cell<Option<JS<Performance>>>, + pub navigationStart: u64, + pub navigationStartPrecise: f64, + screen: Cell<Option<JS<Screen>>>, +} + +impl Window { + pub fn get_cx(&self) -> *mut JSContext { + let js_info = self.page().js_info(); + (**js_info.get_ref().js_context).ptr + } + + pub fn page<'a>(&'a self) -> &'a Page { + &*self.page + } + pub fn get_url(&self) -> Url { + self.page().get_url() + } +} + +#[unsafe_destructor] +impl Drop for Window { + fn drop(&mut self) { + for (_, timer_handle) in self.active_timers.borrow_mut().mut_iter() { + timer_handle.cancel(); + } + } +} + +// Holder for the various JS values associated with setTimeout +// (ie. function value to invoke and all arguments to pass +// to the function when calling it) +#[deriving(Encodable)] +pub struct TimerData { + pub is_interval: bool, + pub funval: Traceable<JSVal>, +} + +impl<'a> WindowMethods for JSRef<'a, Window> { + fn Alert(&self, s: DOMString) { + // Right now, just print to the console + println!("ALERT: {:s}", s); + } + + fn Close(&self) { + let ScriptChan(ref chan) = self.script_chan; + chan.send(ExitWindowMsg(self.page.id.clone())); + } + + fn Document(&self) -> Temporary<Document> { + let frame = self.page().frame(); + Temporary::new(frame.get_ref().document.clone()) + } + + fn Location(&self) -> Temporary<Location> { + if self.location.get().is_none() { + let page = self.deref().page.clone(); + let location = Location::new(self, page); + self.location.assign(Some(location)); + } + Temporary::new(self.location.get().get_ref().clone()) + } + + fn Console(&self) -> Temporary<Console> { + if self.console.get().is_none() { + let console = Console::new(&global::Window(*self)); + self.console.assign(Some(console)); + } + Temporary::new(self.console.get().get_ref().clone()) + } + + fn Navigator(&self) -> Temporary<Navigator> { + if self.navigator.get().is_none() { + let navigator = Navigator::new(self); + self.navigator.assign(Some(navigator)); + } + Temporary::new(self.navigator.get().get_ref().clone()) + } + + fn SetTimeout(&self, _cx: *mut JSContext, callback: JSVal, timeout: i32) -> i32 { + self.set_timeout_or_interval(callback, timeout, false) + } + + fn ClearTimeout(&self, handle: i32) { + let mut timers = self.active_timers.deref().borrow_mut(); + let mut timer_handle = timers.pop(&TimerId(handle)); + match timer_handle { + Some(ref mut handle) => handle.cancel(), + None => { } + } + timers.remove(&TimerId(handle)); + } + + fn SetInterval(&self, _cx: *mut JSContext, callback: JSVal, timeout: i32) -> i32 { + self.set_timeout_or_interval(callback, timeout, true) + } + + fn ClearInterval(&self, handle: i32) { + self.ClearTimeout(handle); + } + + fn Window(&self) -> Temporary<Window> { + Temporary::from_rooted(self) + } + + fn Self(&self) -> Temporary<Window> { + self.Window() + } + + // http://www.whatwg.org/html/#dom-frames + fn Frames(&self) -> Temporary<Window> { + self.Window() + } + + fn Parent(&self) -> Temporary<Window> { + //TODO - Once we support iframes correctly this needs to return the parent frame + self.Window() + } + + fn Performance(&self) -> Temporary<Performance> { + if self.performance.get().is_none() { + let performance = Performance::new(self); + self.performance.assign(Some(performance)); + } + Temporary::new(self.performance.get().get_ref().clone()) + } + + fn GetOnclick(&self) -> Option<EventHandlerNonNull> { + let eventtarget: &JSRef<EventTarget> = EventTargetCast::from_ref(self); + eventtarget.get_event_handler_common("click") + } + + fn SetOnclick(&self, listener: Option<EventHandlerNonNull>) { + let eventtarget: &JSRef<EventTarget> = EventTargetCast::from_ref(self); + eventtarget.set_event_handler_common("click", listener) + } + + fn GetOnload(&self) -> Option<EventHandlerNonNull> { + let eventtarget: &JSRef<EventTarget> = EventTargetCast::from_ref(self); + eventtarget.get_event_handler_common("load") + } + + fn SetOnload(&self, listener: Option<EventHandlerNonNull>) { + let eventtarget: &JSRef<EventTarget> = EventTargetCast::from_ref(self); + eventtarget.set_event_handler_common("load", listener) + } + + fn GetOnunload(&self) -> Option<EventHandlerNonNull> { + let eventtarget: &JSRef<EventTarget> = EventTargetCast::from_ref(self); + eventtarget.get_event_handler_common("unload") + } + + fn SetOnunload(&self, listener: Option<EventHandlerNonNull>) { + let eventtarget: &JSRef<EventTarget> = EventTargetCast::from_ref(self); + eventtarget.set_event_handler_common("unload", listener) + } + + fn GetOnerror(&self) -> Option<OnErrorEventHandlerNonNull> { + let eventtarget: &JSRef<EventTarget> = EventTargetCast::from_ref(self); + eventtarget.get_event_handler_common("error") + } + + fn SetOnerror(&self, listener: Option<OnErrorEventHandlerNonNull>) { + let eventtarget: &JSRef<EventTarget> = EventTargetCast::from_ref(self); + eventtarget.set_event_handler_common("error", listener) + } + + fn Screen(&self) -> Temporary<Screen> { + if self.screen.get().is_none() { + let screen = Screen::new(self); + self.screen.assign(Some(screen)); + } + Temporary::new(self.screen.get().get_ref().clone()) + } + + fn Debug(&self, message: DOMString) { + debug!("{:s}", message); + } + + fn Gc(&self) { + unsafe { + JS_GC(JS_GetRuntime(self.get_cx())); + } + } + + // http://www.whatwg.org/html/#atob + fn Btoa(&self, btoa: DOMString) -> Fallible<DOMString> { + let input = btoa.as_slice(); + // "The btoa() method must throw an InvalidCharacterError exception if + // the method's first argument contains any character whose code point + // is greater than U+00FF." + if input.chars().any(|c: char| c > '\u00FF') { + Err(InvalidCharacter) + } else { + // "Otherwise, the user agent must convert that argument to a + // sequence of octets whose nth octet is the eight-bit + // representation of the code point of the nth character of + // the argument," + let octets = input.chars().map(|c: char| c as u8).collect::<Vec<u8>>(); + + // "and then must apply the base64 algorithm to that sequence of + // octets, and return the result. [RFC4648]" + Ok(octets.as_slice().to_base64(STANDARD)) + } + } + + // http://www.whatwg.org/html/#atob + fn Atob(&self, atob: DOMString) -> Fallible<DOMString> { + // "Let input be the string being parsed." + let mut input = atob.as_slice(); + + // "Remove all space characters from input." + // serialize::base64::from_base64 ignores \r and \n, + // but it treats the other space characters as + // invalid input. + fn is_html_space(c: char) -> bool { + HTML_SPACE_CHARACTERS.iter().any(|&m| m == c) + } + let without_spaces = input.chars() + .filter(|&c| ! is_html_space(c)) + .collect::<String>(); + input = without_spaces.as_slice(); + + // "If the length of input divides by 4 leaving no remainder, then: + // if input ends with one or two U+003D EQUALS SIGN (=) characters, + // remove them from input." + if input.len() % 4 == 0 { + if input.ends_with("==") { + input = input.slice_to(input.len() - 2) + } else if input.ends_with("=") { + input = input.slice_to(input.len() - 1) + } + } + + // "If the length of input divides by 4 leaving a remainder of 1, + // throw an InvalidCharacterError exception and abort these steps." + if input.len() % 4 == 1 { + return Err(InvalidCharacter) + } + + // "If input contains a character that is not in the following list of + // characters and character ranges, throw an InvalidCharacterError + // exception and abort these steps: + // + // U+002B PLUS SIGN (+) + // U+002F SOLIDUS (/) + // Alphanumeric ASCII characters" + if input.chars() + .find(|&c| !(c == '+' || c == '/' || c.is_alphanumeric())) + .is_some() { + return Err(InvalidCharacter) + } + + match input.from_base64() { + Ok(data) => Ok(data.iter().map(|&b| b as char).collect::<String>()), + Err(..) => Err(InvalidCharacter) + } + } +} + +impl Reflectable for Window { + fn reflector<'a>(&'a self) -> &'a Reflector { + self.eventtarget.reflector() + } +} + +pub trait WindowHelpers { + fn damage_and_reflow(&self, damage: DocumentDamageLevel); + fn wait_until_safe_to_modify_dom(&self); + fn init_browser_context(&self, doc: &JSRef<Document>); + fn load_url(&self, href: DOMString); + fn handle_fire_timer(&self, timer_id: TimerId, cx: *mut JSContext); +} + +trait PrivateWindowHelpers { + fn set_timeout_or_interval(&self, callback: JSVal, timeout: i32, is_interval: bool) -> i32; +} + +impl<'a> WindowHelpers for JSRef<'a, Window> { + fn damage_and_reflow(&self, damage: DocumentDamageLevel) { + // FIXME This should probably be ReflowForQuery, not Display. All queries currently + // currently rely on the display list, which means we can't destroy it by + // doing a query reflow. + self.page().damage(damage); + self.page().reflow(ReflowForDisplay, self.control_chan.clone(), *self.compositor); + } + + fn wait_until_safe_to_modify_dom(&self) { + // FIXME: This disables concurrent layout while we are modifying the DOM, since + // our current architecture is entirely unsafe in the presence of races. + self.page().join_layout(); + } + + fn init_browser_context(&self, doc: &JSRef<Document>) { + *self.browser_context.deref().borrow_mut() = Some(BrowserContext::new(doc)); + } + + /// Commence a new URL load which will either replace this window or scroll to a fragment. + fn load_url(&self, href: DOMString) { + let base_url = self.page().get_url(); + debug!("current page url is {:?}", base_url); + let url = UrlParser::new().base_url(&base_url).parse(href.as_slice()); + // FIXME: handle URL parse errors more gracefully. + let url = url.unwrap(); + let ScriptChan(ref script_chan) = self.script_chan; + if href.as_slice().starts_with("#") { + script_chan.send(TriggerFragmentMsg(self.page.id, url)); + } else { + script_chan.send(TriggerLoadMsg(self.page.id, url)); + } + } + + fn handle_fire_timer(&self, timer_id: TimerId, cx: *mut JSContext) { + let this_value = self.reflector().get_jsobject(); + + let data = match self.active_timers.deref().borrow().find(&timer_id) { + None => return, + Some(timer_handle) => timer_handle.data, + }; + + // TODO: Support extra arguments. This requires passing a `*JSVal` array as `argv`. + with_compartment(cx, this_value, || { + let mut rval = NullValue(); + unsafe { + JS_CallFunctionValue(cx, this_value, *data.funval, + 0, ptr::mut_null(), &mut rval); + } + }); + + if !data.is_interval { + self.active_timers.deref().borrow_mut().remove(&timer_id); + } + } +} + +impl<'a> PrivateWindowHelpers for JSRef<'a, Window> { + fn set_timeout_or_interval(&self, callback: JSVal, timeout: i32, is_interval: bool) -> i32 { + let timeout = cmp::max(0, timeout) as u64; + let handle = self.next_timer_handle.deref().get(); + self.next_timer_handle.deref().set(handle + 1); + + // Post a delayed message to the per-window timer task; it will dispatch it + // to the relevant script handler that will deal with it. + let tm = Timer::new().unwrap(); + let (cancel_chan, cancel_port) = channel(); + let chan = self.script_chan.clone(); + let page_id = self.page.id.clone(); + let spawn_name = if is_interval { + "Window:SetInterval" + } else { + "Window:SetTimeout" + }; + spawn_named(spawn_name, proc() { + let mut tm = tm; + let timeout_port = if is_interval { + tm.periodic(timeout) + } else { + tm.oneshot(timeout) + }; + let cancel_port = cancel_port; + + let select = Select::new(); + let mut timeout_handle = select.handle(&timeout_port); + unsafe { timeout_handle.add() }; + let mut cancel_handle = select.handle(&cancel_port); + unsafe { cancel_handle.add() }; + + loop { + let id = select.wait(); + if id == timeout_handle.id() { + timeout_port.recv(); + let ScriptChan(ref chan) = chan; + chan.send(FireTimerMsg(page_id, TimerId(handle))); + if !is_interval { + break; + } + } else if id == cancel_handle.id() { + break; + } + } + }); + let timer_id = TimerId(handle); + let timer = TimerHandle { + handle: timer_id, + cancel_chan: Untraceable::new(Some(cancel_chan)), + data: TimerData { + is_interval: is_interval, + funval: Traceable::new(callback), + } + }; + self.active_timers.deref().borrow_mut().insert(timer_id, timer); + handle + } +} + +impl Window { + pub fn new(cx: *mut JSContext, + page: Rc<Page>, + script_chan: ScriptChan, + control_chan: ScriptControlChan, + compositor: Box<ScriptListener>, + image_cache_task: ImageCacheTask) + -> Temporary<Window> { + let win = box Window { + eventtarget: EventTarget::new_inherited(WindowTypeId), + script_chan: script_chan, + control_chan: control_chan, + console: Cell::new(None), + compositor: Untraceable::new(compositor), + page: page, + location: Cell::new(None), + navigator: Cell::new(None), + image_cache_task: image_cache_task, + active_timers: Traceable::new(RefCell::new(HashMap::new())), + next_timer_handle: Traceable::new(Cell::new(0)), + browser_context: Traceable::new(RefCell::new(None)), + performance: Cell::new(None), + navigationStart: time::get_time().sec as u64, + navigationStartPrecise: time::precise_time_s(), + screen: Cell::new(None), + }; + + WindowBinding::Wrap(cx, win) + } +} |