/* 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::attr::{Attr, AttrHelpers, AttrHelpersForLayout, AttrValue};
use dom::bindings::codegen::Bindings::HTMLIFrameElementBinding;
use dom::bindings::codegen::Bindings::HTMLIFrameElementBinding::HTMLIFrameElementMethods;
use dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
use dom::bindings::codegen::InheritTypes::{NodeCast, ElementCast, EventCast};
use dom::bindings::codegen::InheritTypes::{EventTargetCast, HTMLElementCast,
HTMLIFrameElementDerived};
use dom::bindings::conversions::ToJSValConvertible;
use dom::bindings::error::{ErrorResult, Fallible};
use dom::bindings::error::Error::NotSupported;
use dom::bindings::global::GlobalRef;
use dom::bindings::js::{JSRef, OptionalRootable, Rootable, Temporary};
use dom::customevent::CustomEvent;
use dom::document::Document;
use dom::element::{self, AttributeHandlers, Element};
use dom::event::{Event, EventHelpers};
use dom::eventtarget::{EventTarget, EventTargetTypeId};
use dom::element::ElementTypeId;
use dom::htmlelement::{HTMLElement, HTMLElementTypeId};
use dom::node::{Node, NodeHelpers, NodeTypeId, window_from_node};
use dom::urlhelper::UrlHelper;
use dom::virtualmethods::VirtualMethods;
use dom::window::{Window, WindowHelpers};
use page::IterablePage;
use msg::constellation_msg::{PipelineId, SubpageId, ConstellationChan, MozBrowserEvent, NavigationDirection};
use msg::constellation_msg::IFrameSandboxState::{IFrameSandboxed, IFrameUnsandboxed};
use msg::constellation_msg::Msg as ConstellationMsg;
use util::opts;
use util::str::DOMString;
use string_cache::Atom;
use std::ascii::AsciiExt;
use std::borrow::ToOwned;
use std::cell::Cell;
use url::{Url, UrlParser};
use util::str::{self, LengthOrPercentageOrAuto};
enum SandboxAllowance {
AllowNothing = 0x00,
AllowSameOrigin = 0x01,
AllowTopNavigation = 0x02,
AllowForms = 0x04,
AllowScripts = 0x08,
AllowPointerLock = 0x10,
AllowPopups = 0x20
}
#[dom_struct]
pub struct HTMLIFrameElement {
htmlelement: HTMLElement,
subpage_id: Cell>,
containing_page_pipeline_id: Cell >,
sandbox: Cell >,
}
impl HTMLIFrameElementDerived for EventTarget {
fn is_htmliframeelement(&self) -> bool {
*self.type_id() ==
EventTargetTypeId::Node(
NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLIFrameElement)))
}
}
pub trait HTMLIFrameElementHelpers {
fn is_sandboxed(self) -> bool;
fn get_url(self) -> Option;
/// https://www.whatwg.org/html/#process-the-iframe-attributes
fn process_the_iframe_attributes(self);
fn generate_new_subpage_id(self) -> (SubpageId, Option);
fn navigate_child_browsing_context(self, url: Url);
fn dispatch_mozbrowser_event(self, event: MozBrowserEvent);
fn update_subpage_id(self, new_subpage_id: SubpageId);
}
pub trait RawHTMLIFrameElementHelpers {
fn get_width(&self) -> LengthOrPercentageOrAuto;
fn get_height(&self) -> LengthOrPercentageOrAuto;
}
impl<'a> HTMLIFrameElementHelpers for JSRef<'a, HTMLIFrameElement> {
fn is_sandboxed(self) -> bool {
self.sandbox.get().is_some()
}
fn get_url(self) -> Option {
let element: JSRef = ElementCast::from_ref(self);
element.get_attribute(&ns!(""), &atom!("src")).root().and_then(|src| {
let url = src.r().value();
if url.is_empty() {
None
} else {
let window = window_from_node(self).root();
UrlParser::new().base_url(&window.r().get_url())
.parse(&url).ok()
}
})
}
fn generate_new_subpage_id(self) -> (SubpageId, Option) {
let old_subpage_id = self.subpage_id.get();
let win = window_from_node(self).root();
let subpage_id = win.r().get_next_subpage_id();
self.subpage_id.set(Some(subpage_id));
(subpage_id, old_subpage_id)
}
fn navigate_child_browsing_context(self, url: Url) {
let sandboxed = if self.is_sandboxed() {
IFrameSandboxed
} else {
IFrameUnsandboxed
};
let window = window_from_node(self).root();
let window = window.r();
let (new_subpage_id, old_subpage_id) = self.generate_new_subpage_id();
self.containing_page_pipeline_id.set(Some(window.pipeline()));
let ConstellationChan(ref chan) = window.constellation_chan();
chan.send(ConstellationMsg::ScriptLoadedURLInIFrame(url,
window.pipeline(),
new_subpage_id,
old_subpage_id,
sandboxed)).unwrap();
if opts::experimental_enabled() {
// https://developer.mozilla.org/en-US/docs/Web/Events/mozbrowserloadstart
self.dispatch_mozbrowser_event(MozBrowserEvent::LoadStart);
}
}
fn process_the_iframe_attributes(self) {
let url = match self.get_url() {
Some(url) => url.clone(),
None => Url::parse("about:blank").unwrap(),
};
self.navigate_child_browsing_context(url);
}
fn dispatch_mozbrowser_event(self, event: MozBrowserEvent) {
// TODO(gw): Support mozbrowser event types that have detail which is not a string.
// See https://developer.mozilla.org/en-US/docs/Web/API/Using_the_Browser_API
// for a list of mozbrowser events.
assert!(opts::experimental_enabled());
if self.Mozbrowser() {
let window = window_from_node(self).root();
let cx = window.r().get_cx();
let custom_event = CustomEvent::new(GlobalRef::Window(window.r()),
event.name().to_owned(),
true,
true,
event.detail().to_jsval(cx)).root();
let target: JSRef = EventTargetCast::from_ref(self);
let event: JSRef = EventCast::from_ref(custom_event.r());
event.fire(target);
}
}
fn update_subpage_id(self, new_subpage_id: SubpageId) {
self.subpage_id.set(Some(new_subpage_id));
}
}
impl RawHTMLIFrameElementHelpers for HTMLIFrameElement {
#[allow(unsafe_code)]
fn get_width(&self) -> LengthOrPercentageOrAuto {
unsafe {
element::get_attr_for_layout(ElementCast::from_actual(&*self),
&ns!(""),
&atom!("width")).map(|attribute| {
str::parse_length(&**(*attribute.unsafe_get()).value())
}).unwrap_or(LengthOrPercentageOrAuto::Auto)
}
}
#[allow(unsafe_code)]
fn get_height(&self) -> LengthOrPercentageOrAuto {
unsafe {
element::get_attr_for_layout(ElementCast::from_actual(&*self),
&ns!(""),
&atom!("height")).map(|attribute| {
str::parse_length(&**(*attribute.unsafe_get()).value())
}).unwrap_or(LengthOrPercentageOrAuto::Auto)
}
}
}
impl HTMLIFrameElement {
fn new_inherited(localName: DOMString,
prefix: Option,
document: JSRef) -> HTMLIFrameElement {
HTMLIFrameElement {
htmlelement:
HTMLElement::new_inherited(HTMLElementTypeId::HTMLIFrameElement, localName, prefix, document),
subpage_id: Cell::new(None),
containing_page_pipeline_id: Cell::new(None),
sandbox: Cell::new(None),
}
}
#[allow(unrooted_must_root)]
pub fn new(localName: DOMString,
prefix: Option,
document: JSRef) -> Temporary {
let element = HTMLIFrameElement::new_inherited(localName, prefix, document);
Node::reflect_node(box element, document, HTMLIFrameElementBinding::Wrap)
}
#[inline]
pub fn containing_page_pipeline_id(&self) -> Option {
self.containing_page_pipeline_id.get()
}
#[inline]
pub fn subpage_id(&self) -> Option {
self.subpage_id.get()
}
}
impl<'a> HTMLIFrameElementMethods for JSRef<'a, HTMLIFrameElement> {
fn Src(self) -> DOMString {
let element: JSRef = ElementCast::from_ref(self);
element.get_string_attribute(&atom!("src"))
}
fn SetSrc(self, src: DOMString) {
let element: JSRef = ElementCast::from_ref(self);
element.set_url_attribute(&atom!("src"), src)
}
fn Sandbox(self) -> DOMString {
let element: JSRef = ElementCast::from_ref(self);
element.get_string_attribute(&atom!("sandbox"))
}
fn SetSandbox(self, sandbox: DOMString) {
let element: JSRef = ElementCast::from_ref(self);
element.set_tokenlist_attribute(&atom!("sandbox"), sandbox);
}
fn GetContentWindow(self) -> Option> {
self.subpage_id.get().and_then(|subpage_id| {
let window = window_from_node(self).root();
let window = window.r();
let children = window.page().children.borrow();
children.iter().find(|page| {
let window = page.window().root();
window.r().subpage() == Some(subpage_id)
}).map(|page| page.window())
})
}
fn GetContentDocument(self) -> Option> {
self.GetContentWindow().root().and_then(|window| {
let self_url = match self.get_url() {
Some(self_url) => self_url,
None => return None,
};
let win_url = window_from_node(self).root().r().get_url();
if UrlHelper::SameOrigin(&self_url, &win_url) {
Some(window.r().Document())
} else {
None
}
})
}
// Experimental mozbrowser implementation is based on the webidl
// present in the gecko source tree, and the documentation here:
// https://developer.mozilla.org/en-US/docs/Web/API/Using_the_Browser_API
// TODO(gw): Use experimental codegen when it is available to avoid
// exposing these APIs. See https://github.com/servo/servo/issues/5264.
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-mozbrowser
fn Mozbrowser(self) -> bool {
if opts::experimental_enabled() {
let element: JSRef = ElementCast::from_ref(self);
element.has_attribute(&Atom::from_slice("mozbrowser"))
} else {
false
}
}
fn SetMozbrowser(self, value: bool) -> ErrorResult {
if opts::experimental_enabled() {
let element: JSRef = ElementCast::from_ref(self);
element.set_bool_attribute(&Atom::from_slice("mozbrowser"), value);
}
Ok(())
}
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/goBack
fn GoBack(self) -> Fallible<()> {
if self.Mozbrowser() {
let node: JSRef = NodeCast::from_ref(self);
if node.is_in_doc() {
let window = window_from_node(self).root();
let window = window.r();
let pipeline_info = Some((self.containing_page_pipeline_id().unwrap(),
self.subpage_id().unwrap()));
let ConstellationChan(ref chan) = window.constellation_chan();
let msg = ConstellationMsg::Navigate(pipeline_info, NavigationDirection::Back);
chan.send(msg).unwrap();
}
Ok(())
} else {
debug!("this frame is not mozbrowser (or experimental_enabled is false)");
Err(NotSupported)
}
}
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/goForward
fn GoForward(self) -> Fallible<()> {
if self.Mozbrowser() {
let node: JSRef = NodeCast::from_ref(self);
if node.is_in_doc() {
let window = window_from_node(self).root();
let window = window.r();
let pipeline_info = Some((self.containing_page_pipeline_id().unwrap(),
self.subpage_id().unwrap()));
let ConstellationChan(ref chan) = window.constellation_chan();
let msg = ConstellationMsg::Navigate(pipeline_info, NavigationDirection::Forward);
chan.send(msg).unwrap();
}
Ok(())
} else {
debug!("this frame is not mozbrowser (or experimental_enabled is false)");
Err(NotSupported)
}
}
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/reload
fn Reload(self, _hardReload: bool) -> Fallible<()> {
Err(NotSupported)
}
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/stop
fn Stop(self) -> Fallible<()> {
Err(NotSupported)
}
make_getter!(Width);
make_setter!(SetWidth, "width");
make_getter!(Height);
make_setter!(SetHeight, "height");
}
impl<'a> VirtualMethods for JSRef<'a, HTMLIFrameElement> {
fn super_type<'b>(&'b self) -> Option<&'b VirtualMethods> {
let htmlelement: &JSRef = HTMLElementCast::from_borrowed_ref(self);
Some(htmlelement as &VirtualMethods)
}
fn after_set_attr(&self, attr: JSRef) {
if let Some(ref s) = self.super_type() {
s.after_set_attr(attr);
}
match attr.local_name() {
&atom!("sandbox") => {
let mut modes = SandboxAllowance::AllowNothing as u8;
if let Some(ref tokens) = attr.value().tokens() {
for token in tokens.iter() {
modes |= match &*token.to_ascii_lowercase() {
"allow-same-origin" => SandboxAllowance::AllowSameOrigin,
"allow-forms" => SandboxAllowance::AllowForms,
"allow-pointer-lock" => SandboxAllowance::AllowPointerLock,
"allow-popups" => SandboxAllowance::AllowPopups,
"allow-scripts" => SandboxAllowance::AllowScripts,
"allow-top-navigation" => SandboxAllowance::AllowTopNavigation,
_ => SandboxAllowance::AllowNothing
} as u8;
}
}
self.sandbox.set(Some(modes));
}
&atom!("src") => {
let node: JSRef = NodeCast::from_ref(*self);
if node.is_in_doc() {
self.process_the_iframe_attributes()
}
},
_ => ()
}
}
fn parse_plain_attribute(&self, name: &Atom, value: DOMString) -> AttrValue {
match name {
&atom!("sandbox") => AttrValue::from_serialized_tokenlist(value),
_ => self.super_type().unwrap().parse_plain_attribute(name, value),
}
}
fn before_remove_attr(&self, attr: JSRef) {
if let Some(ref s) = self.super_type() {
s.before_remove_attr(attr);
}
match attr.local_name() {
&atom!("sandbox") => self.sandbox.set(None),
_ => ()
}
}
fn bind_to_tree(&self, tree_in_doc: bool) {
if let Some(ref s) = self.super_type() {
s.bind_to_tree(tree_in_doc);
}
if tree_in_doc {
self.process_the_iframe_attributes();
}
}
fn unbind_from_tree(&self, tree_in_doc: bool) {
if let Some(ref s) = self.super_type() {
s.unbind_from_tree(tree_in_doc);
}
// https://html.spec.whatwg.org/multipage/#a-browsing-context-is-discarded
match (self.containing_page_pipeline_id(), self.subpage_id()) {
(Some(containing_pipeline_id), Some(subpage_id)) => {
let window = window_from_node(*self).root();
let window = window.r();
let ConstellationChan(ref chan) = window.constellation_chan();
let msg = ConstellationMsg::RemoveIFrame(containing_pipeline_id,
subpage_id);
chan.send(msg).unwrap();
// Resetting the subpage id to None is required here so that
// if this iframe is subsequently re-added to the document
// the load doesn't think that it's a navigation, but instead
// a new iframe. Without this, the constellation gets very
// confused.
self.subpage_id.set(None);
}
_ => {}
}
}
}