script/dom/html/
htmliframeelement.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Cell;
6use std::rc::Rc;
7
8use base::id::{BrowsingContextId, PipelineId, WebViewId};
9use constellation_traits::{
10    IFrameLoadInfo, IFrameLoadInfoWithData, JsEvalResult, LoadData, LoadOrigin,
11    NavigationHistoryBehavior, ScriptToConstellationMessage,
12};
13use content_security_policy::sandboxing_directive::{
14    SandboxingFlagSet, parse_a_sandboxing_directive,
15};
16use dom_struct::dom_struct;
17use embedder_traits::ViewportDetails;
18use html5ever::{LocalName, Prefix, local_name, ns};
19use js::rust::HandleObject;
20use net_traits::ReferrerPolicy;
21use net_traits::request::Destination;
22use profile_traits::ipc as ProfiledIpc;
23use script_traits::{NewPipelineInfo, UpdatePipelineIdReason};
24use servo_url::ServoUrl;
25use style::attr::{AttrValue, LengthOrPercentageOrAuto};
26use stylo_atoms::Atom;
27
28use crate::document_loader::{LoadBlocker, LoadType};
29use crate::dom::attr::Attr;
30use crate::dom::bindings::cell::DomRefCell;
31use crate::dom::bindings::codegen::Bindings::HTMLIFrameElementBinding::HTMLIFrameElementMethods;
32use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
33use crate::dom::bindings::codegen::UnionTypes::TrustedHTMLOrString;
34use crate::dom::bindings::error::Fallible;
35use crate::dom::bindings::inheritance::Castable;
36use crate::dom::bindings::reflector::DomGlobal;
37use crate::dom::bindings::root::{DomRoot, LayoutDom, MutNullableDom};
38use crate::dom::bindings::str::{DOMString, USVString};
39use crate::dom::document::Document;
40use crate::dom::domtokenlist::DOMTokenList;
41use crate::dom::element::{
42    AttributeMutation, Element, LayoutElementHelpers, reflect_referrer_policy_attribute,
43};
44use crate::dom::eventtarget::EventTarget;
45use crate::dom::globalscope::GlobalScope;
46use crate::dom::html::htmlelement::HTMLElement;
47use crate::dom::node::{BindContext, Node, NodeDamage, NodeTraits, UnbindContext};
48use crate::dom::trustedhtml::TrustedHTML;
49use crate::dom::virtualmethods::VirtualMethods;
50use crate::dom::windowproxy::WindowProxy;
51use crate::script_runtime::CanGc;
52use crate::script_thread::{ScriptThread, with_script_thread};
53use crate::script_window_proxies::ScriptWindowProxies;
54
55#[derive(PartialEq)]
56enum PipelineType {
57    InitialAboutBlank,
58    Navigation,
59}
60
61#[derive(PartialEq)]
62enum ProcessingMode {
63    FirstTime,
64    NotFirstTime,
65}
66
67#[dom_struct]
68pub(crate) struct HTMLIFrameElement {
69    htmlelement: HTMLElement,
70    #[no_trace]
71    webview_id: Cell<Option<WebViewId>>,
72    #[no_trace]
73    browsing_context_id: Cell<Option<BrowsingContextId>>,
74    #[no_trace]
75    pipeline_id: Cell<Option<PipelineId>>,
76    #[no_trace]
77    pending_pipeline_id: Cell<Option<PipelineId>>,
78    #[no_trace]
79    about_blank_pipeline_id: Cell<Option<PipelineId>>,
80    sandbox: MutNullableDom<DOMTokenList>,
81    #[no_trace]
82    sandboxing_flag_set: Cell<Option<SandboxingFlagSet>>,
83    load_blocker: DomRefCell<Option<LoadBlocker>>,
84    throttled: Cell<bool>,
85    #[conditional_malloc_size_of]
86    script_window_proxies: Rc<ScriptWindowProxies>,
87    /// Keeping track of whether the iframe will be navigated
88    /// outside of the processing of it's attribute(for example: form navigation).
89    /// This is necessary to prevent the iframe load event steps
90    /// from asynchronously running for the initial blank document
91    /// while script at this point(when the flag is set)
92    /// expects those to run only for the navigated documented.
93    pending_navigation: Cell<bool>,
94}
95
96impl HTMLIFrameElement {
97    /// <https://html.spec.whatwg.org/multipage/#otherwise-steps-for-iframe-or-frame-elements>,
98    /// step 1.
99    fn get_url(&self) -> ServoUrl {
100        let element = self.upcast::<Element>();
101        element
102            .get_attribute(&ns!(), &local_name!("src"))
103            .and_then(|src| {
104                let url = src.value();
105                if url.is_empty() {
106                    None
107                } else {
108                    self.owner_document().base_url().join(&url).ok()
109                }
110            })
111            .unwrap_or_else(|| ServoUrl::parse("about:blank").unwrap())
112    }
113
114    pub(crate) fn navigate_or_reload_child_browsing_context(
115        &self,
116        load_data: LoadData,
117        history_handling: NavigationHistoryBehavior,
118        can_gc: CanGc,
119    ) {
120        self.start_new_pipeline(
121            load_data,
122            PipelineType::Navigation,
123            history_handling,
124            can_gc,
125        );
126    }
127
128    fn start_new_pipeline(
129        &self,
130        mut load_data: LoadData,
131        pipeline_type: PipelineType,
132        history_handling: NavigationHistoryBehavior,
133        can_gc: CanGc,
134    ) {
135        let browsing_context_id = match self.browsing_context_id() {
136            None => return warn!("Attempted to start a new pipeline on an unattached iframe."),
137            Some(id) => id,
138        };
139
140        let webview_id = match self.webview_id() {
141            None => return warn!("Attempted to start a new pipeline on an unattached iframe."),
142            Some(id) => id,
143        };
144
145        let document = self.owner_document();
146
147        {
148            let load_blocker = &self.load_blocker;
149            // Any oustanding load is finished from the point of view of the blocked
150            // document; the new navigation will continue blocking it.
151            LoadBlocker::terminate(load_blocker, can_gc);
152        }
153
154        if load_data.url.scheme() == "javascript" {
155            let window_proxy = self.GetContentWindow();
156            if let Some(window_proxy) = window_proxy {
157                if !ScriptThread::navigate_to_javascript_url(
158                    &document.global(),
159                    &window_proxy.global(),
160                    &mut load_data,
161                    Some(self.upcast()),
162                    can_gc,
163                ) {
164                    return;
165                }
166            }
167        }
168
169        match load_data.js_eval_result {
170            Some(JsEvalResult::NoContent) => (),
171            _ => {
172                let mut load_blocker = self.load_blocker.borrow_mut();
173                *load_blocker = Some(LoadBlocker::new(
174                    &document,
175                    LoadType::Subframe(load_data.url.clone()),
176                ));
177            },
178        };
179
180        let window = self.owner_window();
181        let old_pipeline_id = self.pipeline_id();
182        let new_pipeline_id = PipelineId::new();
183        self.pending_pipeline_id.set(Some(new_pipeline_id));
184
185        let load_info = IFrameLoadInfo {
186            parent_pipeline_id: window.pipeline_id(),
187            browsing_context_id,
188            webview_id,
189            new_pipeline_id,
190            is_private: false, // FIXME
191            inherited_secure_context: load_data.inherited_secure_context,
192            history_handling,
193        };
194
195        let viewport_details = window
196            .get_iframe_viewport_details_if_known(browsing_context_id)
197            .unwrap_or_else(|| ViewportDetails {
198                hidpi_scale_factor: window.device_pixel_ratio(),
199                ..Default::default()
200            });
201
202        match pipeline_type {
203            PipelineType::InitialAboutBlank => {
204                self.about_blank_pipeline_id.set(Some(new_pipeline_id));
205
206                let load_info = IFrameLoadInfoWithData {
207                    info: load_info,
208                    load_data: load_data.clone(),
209                    old_pipeline_id,
210                    viewport_details,
211                    theme: window.theme(),
212                };
213                window
214                    .as_global_scope()
215                    .script_to_constellation_chan()
216                    .send(ScriptToConstellationMessage::ScriptNewIFrame(load_info))
217                    .unwrap();
218
219                let new_pipeline_info = NewPipelineInfo {
220                    parent_info: Some(window.pipeline_id()),
221                    new_pipeline_id,
222                    browsing_context_id,
223                    webview_id,
224                    opener: None,
225                    load_data,
226                    viewport_details,
227                    theme: window.theme(),
228                };
229
230                self.pipeline_id.set(Some(new_pipeline_id));
231                with_script_thread(|script_thread| {
232                    script_thread.spawn_pipeline(new_pipeline_info);
233                });
234            },
235            PipelineType::Navigation => {
236                let load_info = IFrameLoadInfoWithData {
237                    info: load_info,
238                    load_data,
239                    old_pipeline_id,
240                    viewport_details,
241                    theme: window.theme(),
242                };
243                window
244                    .as_global_scope()
245                    .script_to_constellation_chan()
246                    .send(ScriptToConstellationMessage::ScriptLoadedURLInIFrame(
247                        load_info,
248                    ))
249                    .unwrap();
250            },
251        }
252    }
253
254    /// When an iframe is first inserted into the document,
255    /// an "about:blank" document is created,
256    /// and synchronously processed by the script thread.
257    /// This initial synchronous load should have no noticeable effect in script.
258    /// See the note in `iframe_load_event_steps`.
259    pub(crate) fn is_initial_blank_document(&self) -> bool {
260        self.pending_pipeline_id.get() == self.about_blank_pipeline_id.get()
261    }
262
263    /// <https://html.spec.whatwg.org/multipage/#process-the-iframe-attributes>
264    fn process_the_iframe_attributes(&self, mode: ProcessingMode, can_gc: CanGc) {
265        // > 1. If `element`'s `srcdoc` attribute is specified, then:
266        if self
267            .upcast::<Element>()
268            .has_attribute(&local_name!("srcdoc"))
269        {
270            let url = ServoUrl::parse("about:srcdoc").unwrap();
271            let document = self.owner_document();
272            let window = self.owner_window();
273            let pipeline_id = Some(window.pipeline_id());
274            let mut load_data = LoadData::new(
275                LoadOrigin::Script(document.origin().immutable().clone()),
276                url,
277                pipeline_id,
278                window.as_global_scope().get_referrer(),
279                document.get_referrer_policy(),
280                Some(window.as_global_scope().is_secure_context()),
281                Some(document.insecure_requests_policy()),
282                document.has_trustworthy_ancestor_or_current_origin(),
283                self.sandboxing_flag_set(),
284            );
285            load_data.destination = Destination::IFrame;
286            load_data.policy_container = Some(window.as_global_scope().policy_container());
287            let element = self.upcast::<Element>();
288            load_data.srcdoc = String::from(element.get_string_attribute(&local_name!("srcdoc")));
289            self.navigate_or_reload_child_browsing_context(
290                load_data,
291                NavigationHistoryBehavior::Push,
292                can_gc,
293            );
294            return;
295        }
296
297        let window = self.owner_window();
298
299        // https://html.spec.whatwg.org/multipage/#attr-iframe-name
300        // Note: the spec says to set the name 'when the nested browsing context is created'.
301        // The current implementation sets the name on the window,
302        // when the iframe attributes are first processed.
303        if mode == ProcessingMode::FirstTime {
304            if let Some(window) = self.GetContentWindow() {
305                window.set_name(
306                    self.upcast::<Element>()
307                        .get_name()
308                        .map_or(DOMString::from(""), |n| DOMString::from(&*n)),
309                );
310            }
311        }
312
313        if mode == ProcessingMode::FirstTime &&
314            !self.upcast::<Element>().has_attribute(&local_name!("src"))
315        {
316            return;
317        }
318
319        // > 2. Otherwise, if `element` has a `src` attribute specified, or
320        // >    `initialInsertion` is false, then run the shared attribute
321        // >    processing steps for `iframe` and `frame` elements given
322        // >    `element`.
323        let url = self.get_url();
324
325        // Step 2.4: Let referrerPolicy be the current state of element's referrerpolicy content
326        // attribute.
327        let document = self.owner_document();
328        let referrer_policy_token = self.ReferrerPolicy();
329
330        // Note: despite not being explicitly stated in the spec steps, this falls back to
331        // document's referrer policy here because it satisfies the expectations that when unset,
332        // the iframe should inherit the referrer policy of its parent
333        let referrer_policy = match ReferrerPolicy::from(&*referrer_policy_token.str()) {
334            ReferrerPolicy::EmptyString => document.get_referrer_policy(),
335            policy => policy,
336        };
337
338        // TODO(#25748):
339        // By spec, we return early if there's an ancestor browsing context
340        // "whose active document's url, ignoring fragments, is equal".
341        // However, asking about ancestor browsing contexts is more nuanced than
342        // it sounds and not implemented here.
343        // Within a single origin, we can do it by walking window proxies,
344        // and this check covers only that single-origin case, protecting
345        // against simple typo self-includes but nothing more elaborate.
346        let mut ancestor = window.GetParent();
347        while let Some(a) = ancestor {
348            if let Some(ancestor_url) = a.document().map(|d| d.url()) {
349                if ancestor_url.scheme() == url.scheme() &&
350                    ancestor_url.username() == url.username() &&
351                    ancestor_url.password() == url.password() &&
352                    ancestor_url.host() == url.host() &&
353                    ancestor_url.port() == url.port() &&
354                    ancestor_url.path() == url.path() &&
355                    ancestor_url.query() == url.query()
356                {
357                    return;
358                }
359            }
360            ancestor = a.parent().map(DomRoot::from_ref);
361        }
362
363        let creator_pipeline_id = if url.as_str() == "about:blank" {
364            Some(window.pipeline_id())
365        } else {
366            None
367        };
368
369        let mut load_data = LoadData::new(
370            LoadOrigin::Script(document.origin().immutable().clone()),
371            url,
372            creator_pipeline_id,
373            window.as_global_scope().get_referrer(),
374            referrer_policy,
375            Some(window.as_global_scope().is_secure_context()),
376            Some(document.insecure_requests_policy()),
377            document.has_trustworthy_ancestor_or_current_origin(),
378            self.sandboxing_flag_set(),
379        );
380        load_data.destination = Destination::IFrame;
381        load_data.policy_container = Some(window.as_global_scope().policy_container());
382
383        let pipeline_id = self.pipeline_id();
384        // If the initial `about:blank` page is the current page, load with replacement enabled,
385        // see https://html.spec.whatwg.org/multipage/#the-iframe-element:about:blank-3
386        let is_about_blank =
387            pipeline_id.is_some() && pipeline_id == self.about_blank_pipeline_id.get();
388
389        let history_handling = if is_about_blank {
390            NavigationHistoryBehavior::Replace
391        } else {
392            NavigationHistoryBehavior::Push
393        };
394
395        self.navigate_or_reload_child_browsing_context(load_data, history_handling, can_gc);
396    }
397
398    /// <https://html.spec.whatwg.org/multipage/#create-a-new-child-navigable>
399    /// Synchronously create a new browsing context(This is not a navigation).
400    /// The pipeline started here should remain unnoticeable to script, but this is not easy
401    /// to refactor because it appears other features have come to rely on the current behavior.
402    /// For now only the iframe load event steps are skipped in some cases for this initial document,
403    /// and we still fire load and pageshow events as part of `maybe_queue_document_completion`.
404    /// Also, some controversy spec-wise remains: <https://github.com/whatwg/html/issues/4965>
405    fn create_nested_browsing_context(&self, can_gc: CanGc) {
406        let url = ServoUrl::parse("about:blank").unwrap();
407        let document = self.owner_document();
408        let window = self.owner_window();
409        let pipeline_id = Some(window.pipeline_id());
410        let mut load_data = LoadData::new(
411            LoadOrigin::Script(document.origin().immutable().clone()),
412            url,
413            pipeline_id,
414            window.as_global_scope().get_referrer(),
415            document.get_referrer_policy(),
416            Some(window.as_global_scope().is_secure_context()),
417            Some(document.insecure_requests_policy()),
418            document.has_trustworthy_ancestor_or_current_origin(),
419            self.sandboxing_flag_set(),
420        );
421        load_data.destination = Destination::IFrame;
422        load_data.policy_container = Some(window.as_global_scope().policy_container());
423        let browsing_context_id = BrowsingContextId::new();
424        let webview_id = window.window_proxy().webview_id();
425        self.pipeline_id.set(None);
426        self.pending_pipeline_id.set(None);
427        self.webview_id.set(Some(webview_id));
428        self.browsing_context_id.set(Some(browsing_context_id));
429        self.start_new_pipeline(
430            load_data,
431            PipelineType::InitialAboutBlank,
432            NavigationHistoryBehavior::Push,
433            can_gc,
434        );
435    }
436
437    fn destroy_nested_browsing_context(&self) {
438        self.pipeline_id.set(None);
439        self.pending_pipeline_id.set(None);
440        self.about_blank_pipeline_id.set(None);
441        self.webview_id.set(None);
442        self.browsing_context_id.set(None);
443    }
444
445    pub(crate) fn update_pipeline_id(
446        &self,
447        new_pipeline_id: PipelineId,
448        reason: UpdatePipelineIdReason,
449        can_gc: CanGc,
450    ) {
451        // For all updates except the one for the initial blank document,
452        // we need to set the flag back to false because the navigation is complete,
453        // because the goal is to, when a navigation is pending, to skip the async load
454        // steps of the initial blank document.
455        if !self.is_initial_blank_document() {
456            self.pending_navigation.set(false);
457        }
458        if self.pending_pipeline_id.get() != Some(new_pipeline_id) &&
459            reason == UpdatePipelineIdReason::Navigation
460        {
461            return;
462        }
463
464        self.pipeline_id.set(Some(new_pipeline_id));
465
466        // Only terminate the load blocker if the pipeline id was updated due to a traversal.
467        // The load blocker will be terminated for a navigation in iframe_load_event_steps.
468        if reason == UpdatePipelineIdReason::Traversal {
469            let blocker = &self.load_blocker;
470            LoadBlocker::terminate(blocker, can_gc);
471        }
472
473        self.upcast::<Node>().dirty(NodeDamage::Other);
474    }
475
476    fn new_inherited(
477        local_name: LocalName,
478        prefix: Option<Prefix>,
479        document: &Document,
480    ) -> HTMLIFrameElement {
481        HTMLIFrameElement {
482            htmlelement: HTMLElement::new_inherited(local_name, prefix, document),
483            browsing_context_id: Cell::new(None),
484            webview_id: Cell::new(None),
485            pipeline_id: Cell::new(None),
486            pending_pipeline_id: Cell::new(None),
487            about_blank_pipeline_id: Cell::new(None),
488            sandbox: Default::default(),
489            sandboxing_flag_set: Cell::new(None),
490            load_blocker: DomRefCell::new(None),
491            throttled: Cell::new(false),
492            script_window_proxies: ScriptThread::window_proxies(),
493            pending_navigation: Default::default(),
494        }
495    }
496
497    #[cfg_attr(crown, allow(crown::unrooted_must_root))]
498    pub(crate) fn new(
499        local_name: LocalName,
500        prefix: Option<Prefix>,
501        document: &Document,
502        proto: Option<HandleObject>,
503        can_gc: CanGc,
504    ) -> DomRoot<HTMLIFrameElement> {
505        Node::reflect_node_with_proto(
506            Box::new(HTMLIFrameElement::new_inherited(
507                local_name, prefix, document,
508            )),
509            document,
510            proto,
511            can_gc,
512        )
513    }
514
515    #[inline]
516    pub(crate) fn pipeline_id(&self) -> Option<PipelineId> {
517        self.pipeline_id.get()
518    }
519
520    #[inline]
521    pub(crate) fn browsing_context_id(&self) -> Option<BrowsingContextId> {
522        self.browsing_context_id.get()
523    }
524
525    #[inline]
526    pub(crate) fn webview_id(&self) -> Option<WebViewId> {
527        self.webview_id.get()
528    }
529
530    #[inline]
531    pub(crate) fn sandboxing_flag_set(&self) -> SandboxingFlagSet {
532        self.sandboxing_flag_set
533            .get()
534            .unwrap_or_else(SandboxingFlagSet::empty)
535    }
536
537    pub(crate) fn set_throttled(&self, throttled: bool) {
538        if self.throttled.get() != throttled {
539            self.throttled.set(throttled);
540        }
541    }
542
543    /// Note a pending navigation.
544    /// This is used to ignore the async load event steps for
545    /// the initial blank document if those haven't run yet.
546    pub(crate) fn note_pending_navigation(&self) {
547        self.pending_navigation.set(true);
548    }
549
550    /// <https://html.spec.whatwg.org/multipage/#iframe-load-event-steps>
551    pub(crate) fn iframe_load_event_steps(&self, loaded_pipeline: PipelineId, can_gc: CanGc) {
552        // TODO(#9592): assert that the load blocker is present at all times when we
553        //              can guarantee that it's created for the case of iframe.reload().
554        if Some(loaded_pipeline) != self.pending_pipeline_id.get() {
555            return;
556        }
557
558        // TODO 1. Assert: element's content navigable is not null.
559
560        // TODO 2-4 Mark resource timing.
561
562        // TODO 5 Set childDocument's iframe load in progress flag.
563
564        // Note: in the spec, these steps are either run synchronously as part of
565        // "If url matches about:blank and initialInsertion is true, then:"
566        // in `process the iframe attributes`,
567        // or asynchronously when navigation completes.
568        //
569        // In our current implementation,
570        // we arrive here always asynchronously in the following two cases:
571        // 1. as part of loading the initial blank document
572        //    created in `create_nested_browsing_context`
573        // 2. optionally, as part of loading a second document created as
574        //    as part of the first processing of the iframe attributes.
575        //
576        // To preserve the logic of the spec--firing the load event once--in the context of
577        // our current implementation, we must not fire the load event
578        // for the initial blank document if we know that a navigation is ongoing,
579        // which can be deducted from `pending_navigation` or the presence of an src.
580        //
581        // Additionally, to prevent a race condition with navigations,
582        // in all cases, skip the load event if there is a pending navigation.
583        // See #40348
584        //
585        // TODO: run these step synchronously as part of processing the iframe attributes.
586        let should_fire_event = if self.is_initial_blank_document() {
587            // If this is the initial blank doc:
588            // do not fire if there is a pending navigation,
589            // or if the iframe has an src.
590            !self.pending_navigation.get() &&
591                !self.upcast::<Element>().has_attribute(&local_name!("src"))
592        } else {
593            // If this is not the initial blank doc:
594            // do not fire if there is a pending navigation.
595            !self.pending_navigation.get()
596        };
597        if should_fire_event {
598            // Step 6. Fire an event named load at element.
599            self.upcast::<EventTarget>()
600                .fire_event(atom!("load"), can_gc);
601        }
602
603        let blocker = &self.load_blocker;
604        LoadBlocker::terminate(blocker, can_gc);
605
606        // TODO Step 7 - unset child document `mute iframe load` flag
607    }
608
609    /// Parse the `sandbox` attribute value given the [`Attr`]. This sets the `sandboxing_flag_set`
610    /// property or clears it is the value isn't specified. Notably, an unspecified sandboxing
611    /// attribute (no sandboxing) is different from an empty one (full sandboxing).
612    fn parse_sandbox_attribute(&self) {
613        let attribute = self
614            .upcast::<Element>()
615            .get_attribute(&ns!(), &local_name!("sandbox"));
616        self.sandboxing_flag_set
617            .set(attribute.map(|attribute_value| {
618                let tokens: Vec<_> = attribute_value
619                    .value()
620                    .as_tokens()
621                    .iter()
622                    .map(|atom| atom.to_string().to_ascii_lowercase())
623                    .collect();
624                parse_a_sandboxing_directive(&tokens)
625            }));
626    }
627}
628
629pub(crate) trait HTMLIFrameElementLayoutMethods {
630    fn pipeline_id(self) -> Option<PipelineId>;
631    fn browsing_context_id(self) -> Option<BrowsingContextId>;
632    fn get_width(self) -> LengthOrPercentageOrAuto;
633    fn get_height(self) -> LengthOrPercentageOrAuto;
634}
635
636impl HTMLIFrameElementLayoutMethods for LayoutDom<'_, HTMLIFrameElement> {
637    #[inline]
638    fn pipeline_id(self) -> Option<PipelineId> {
639        (self.unsafe_get()).pipeline_id.get()
640    }
641
642    #[inline]
643    fn browsing_context_id(self) -> Option<BrowsingContextId> {
644        (self.unsafe_get()).browsing_context_id.get()
645    }
646
647    fn get_width(self) -> LengthOrPercentageOrAuto {
648        self.upcast::<Element>()
649            .get_attr_for_layout(&ns!(), &local_name!("width"))
650            .map(AttrValue::as_dimension)
651            .cloned()
652            .unwrap_or(LengthOrPercentageOrAuto::Auto)
653    }
654
655    fn get_height(self) -> LengthOrPercentageOrAuto {
656        self.upcast::<Element>()
657            .get_attr_for_layout(&ns!(), &local_name!("height"))
658            .map(AttrValue::as_dimension)
659            .cloned()
660            .unwrap_or(LengthOrPercentageOrAuto::Auto)
661    }
662}
663
664impl HTMLIFrameElementMethods<crate::DomTypeHolder> for HTMLIFrameElement {
665    // https://html.spec.whatwg.org/multipage/#dom-iframe-src
666    make_url_getter!(Src, "src");
667
668    // https://html.spec.whatwg.org/multipage/#dom-iframe-src
669    make_url_setter!(SetSrc, "src");
670
671    /// <https://html.spec.whatwg.org/multipage/#dom-iframe-srcdoc>
672    fn Srcdoc(&self) -> TrustedHTMLOrString {
673        let element = self.upcast::<Element>();
674        element.get_trusted_html_attribute(&local_name!("srcdoc"))
675    }
676
677    /// <https://html.spec.whatwg.org/multipage/#dom-iframe-srcdoc>
678    fn SetSrcdoc(&self, value: TrustedHTMLOrString, can_gc: CanGc) -> Fallible<()> {
679        // Step 1: Let compliantString be the result of invoking the
680        // Get Trusted Type compliant string algorithm with TrustedHTML,
681        // this's relevant global object, the given value, "HTMLIFrameElement srcdoc", and "script".
682        let element = self.upcast::<Element>();
683        let value = TrustedHTML::get_trusted_script_compliant_string(
684            &element.owner_global(),
685            value,
686            "HTMLIFrameElement srcdoc",
687            can_gc,
688        )?;
689        // Step 2: Set an attribute value given this, srcdoc's local name, and compliantString.
690        element.set_attribute(
691            &local_name!("srcdoc"),
692            AttrValue::String(value.str().to_owned()),
693            can_gc,
694        );
695        Ok(())
696    }
697
698    /// <https://html.spec.whatwg.org/multipage/#dom-iframe-sandbox>
699    ///
700    /// The supported tokens for sandbox's DOMTokenList are the allowed values defined in the
701    /// sandbox attribute and supported by the user agent. These range of possible values is
702    /// defined here: <https://html.spec.whatwg.org/multipage/#attr-iframe-sandbox>
703    fn Sandbox(&self, can_gc: CanGc) -> DomRoot<DOMTokenList> {
704        self.sandbox.or_init(|| {
705            DOMTokenList::new(
706                self.upcast::<Element>(),
707                &local_name!("sandbox"),
708                Some(vec![
709                    Atom::from("allow-downloads"),
710                    Atom::from("allow-forms"),
711                    Atom::from("allow-modals"),
712                    Atom::from("allow-orientation-lock"),
713                    Atom::from("allow-pointer-lock"),
714                    Atom::from("allow-popups"),
715                    Atom::from("allow-popups-to-escape-sandbox"),
716                    Atom::from("allow-presentation"),
717                    Atom::from("allow-same-origin"),
718                    Atom::from("allow-scripts"),
719                    Atom::from("allow-top-navigation"),
720                    Atom::from("allow-top-navigation-by-user-activation"),
721                    Atom::from("allow-top-navigation-to-custom-protocols"),
722                ]),
723                can_gc,
724            )
725        })
726    }
727
728    /// <https://html.spec.whatwg.org/multipage/#dom-iframe-contentwindow>
729    fn GetContentWindow(&self) -> Option<DomRoot<WindowProxy>> {
730        self.browsing_context_id
731            .get()
732            .and_then(|id| self.script_window_proxies.find_window_proxy(id))
733    }
734
735    // https://html.spec.whatwg.org/multipage/#dom-iframe-contentdocument
736    /// <https://html.spec.whatwg.org/multipage/#concept-bcc-content-document>
737    fn GetContentDocument(&self) -> Option<DomRoot<Document>> {
738        // Step 1.
739        let pipeline_id = self.pipeline_id.get()?;
740
741        // Step 2-3.
742        // Note that this lookup will fail if the document is dissimilar-origin,
743        // so we should return None in that case.
744        let document = ScriptThread::find_document(pipeline_id)?;
745
746        // Step 4.
747        let current = GlobalScope::current()
748            .expect("No current global object")
749            .as_window()
750            .Document();
751        if !current.origin().same_origin_domain(document.origin()) {
752            return None;
753        }
754        // Step 5.
755        Some(document)
756    }
757
758    /// <https://html.spec.whatwg.org/multipage/#attr-iframe-referrerpolicy>
759    fn ReferrerPolicy(&self) -> DOMString {
760        reflect_referrer_policy_attribute(self.upcast::<Element>())
761    }
762
763    // https://html.spec.whatwg.org/multipage/#attr-iframe-referrerpolicy
764    make_setter!(SetReferrerPolicy, "referrerpolicy");
765
766    // https://html.spec.whatwg.org/multipage/#attr-iframe-allowfullscreen
767    make_bool_getter!(AllowFullscreen, "allowfullscreen");
768    // https://html.spec.whatwg.org/multipage/#attr-iframe-allowfullscreen
769    make_bool_setter!(SetAllowFullscreen, "allowfullscreen");
770
771    // <https://html.spec.whatwg.org/multipage/#dom-dim-width>
772    make_getter!(Width, "width");
773    // <https://html.spec.whatwg.org/multipage/#dom-dim-width>
774    make_dimension_setter!(SetWidth, "width");
775
776    // <https://html.spec.whatwg.org/multipage/#dom-dim-height>
777    make_getter!(Height, "height");
778    // <https://html.spec.whatwg.org/multipage/#dom-dim-height>
779    make_dimension_setter!(SetHeight, "height");
780
781    // https://html.spec.whatwg.org/multipage/#other-elements,-attributes-and-apis:attr-iframe-frameborder
782    make_getter!(FrameBorder, "frameborder");
783    // https://html.spec.whatwg.org/multipage/#other-elements,-attributes-and-apis:attr-iframe-frameborder
784    make_setter!(SetFrameBorder, "frameborder");
785
786    // https://html.spec.whatwg.org/multipage/#dom-iframe-name
787    // A child browsing context checks the name of its iframe only at the time
788    // it is created; subsequent name sets have no special effect.
789    make_atomic_setter!(SetName, "name");
790
791    // https://html.spec.whatwg.org/multipage/#dom-iframe-name
792    // This is specified as reflecting the name content attribute of the
793    // element, not the name of the child browsing context.
794    make_getter!(Name, "name");
795}
796
797impl VirtualMethods for HTMLIFrameElement {
798    fn super_type(&self) -> Option<&dyn VirtualMethods> {
799        Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
800    }
801
802    fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation, can_gc: CanGc) {
803        self.super_type()
804            .unwrap()
805            .attribute_mutated(attr, mutation, can_gc);
806        match *attr.local_name() {
807            // From <https://html.spec.whatwg.org/multipage/#attr-iframe-sandbox>:
808            //
809            // > When an iframe element's sandbox attribute is set or changed while
810            // > it has a non-null content navigable, the user agent must parse the
811            // > sandboxing directive given the attribute's value and the iframe
812            // > element's iframe sandboxing flag set.
813            //
814            // > When an iframe element's sandbox attribute is removed while it has
815            // > a non-null content navigable, the user agent must empty the iframe
816            // > element's iframe sandboxing flag set.
817            local_name!("sandbox") if self.browsing_context_id.get().is_some() => {
818                self.parse_sandbox_attribute();
819            },
820            local_name!("srcdoc") => {
821                // https://html.spec.whatwg.org/multipage/#the-iframe-element:the-iframe-element-9
822                // "Whenever an iframe element with a non-null nested browsing context has its
823                // srcdoc attribute set, changed, or removed, the user agent must process the
824                // iframe attributes."
825                // but we can't check that directly, since the child browsing context
826                // may be in a different script thread. Instead, we check to see if the parent
827                // is in a document tree and has a browsing context, which is what causes
828                // the child browsing context to be created.
829
830                // trigger the processing of iframe attributes whenever "srcdoc" attribute is set, changed or removed
831                if self.upcast::<Node>().is_connected_with_browsing_context() {
832                    debug!("iframe srcdoc modified while in browsing context.");
833                    self.process_the_iframe_attributes(ProcessingMode::NotFirstTime, can_gc);
834                }
835            },
836            local_name!("src") => {
837                // https://html.spec.whatwg.org/multipage/#the-iframe-element
838                // "Similarly, whenever an iframe element with a non-null nested browsing context
839                // but with no srcdoc attribute specified has its src attribute set, changed, or removed,
840                // the user agent must process the iframe attributes,"
841                // but we can't check that directly, since the child browsing context
842                // may be in a different script thread. Instead, we check to see if the parent
843                // is in a document tree and has a browsing context, which is what causes
844                // the child browsing context to be created.
845                if self.upcast::<Node>().is_connected_with_browsing_context() {
846                    debug!("iframe src set while in browsing context.");
847                    self.process_the_iframe_attributes(ProcessingMode::NotFirstTime, can_gc);
848                }
849            },
850            _ => {},
851        }
852    }
853
854    fn attribute_affects_presentational_hints(&self, attr: &Attr) -> bool {
855        match attr.local_name() {
856            &local_name!("width") | &local_name!("height") => true,
857            _ => self
858                .super_type()
859                .unwrap()
860                .attribute_affects_presentational_hints(attr),
861        }
862    }
863
864    fn parse_plain_attribute(&self, name: &LocalName, value: DOMString) -> AttrValue {
865        match *name {
866            local_name!("sandbox") => AttrValue::from_serialized_tokenlist(value.into()),
867            local_name!("width") => AttrValue::from_dimension(value.into()),
868            local_name!("height") => AttrValue::from_dimension(value.into()),
869            _ => self
870                .super_type()
871                .unwrap()
872                .parse_plain_attribute(name, value),
873        }
874    }
875
876    /// <https://html.spec.whatwg.org/multipage/#the-iframe-element:html-element-post-connection-steps>
877    fn post_connection_steps(&self, can_gc: CanGc) {
878        if let Some(s) = self.super_type() {
879            s.post_connection_steps(can_gc);
880        }
881
882        // This isn't mentioned any longer in the specification, but still seems important. This is
883        // likely due to the fact that we have deviated a great deal with it comes to navigables
884        // and browsing contexts.
885        if !self.upcast::<Node>().is_connected_with_browsing_context() {
886            return;
887        }
888
889        debug!("<iframe> running post connection steps");
890
891        // Step 1. Create a new child navigable for insertedNode.
892        self.create_nested_browsing_context(can_gc);
893
894        // Step 2: If insertedNode has a sandbox attribute, then parse the sandboxing directive
895        // given the attribute's value and insertedNode's iframe sandboxing flag set.
896        self.parse_sandbox_attribute();
897
898        // Step 3. Process the iframe attributes for insertedNode, with initialInsertion set to true.
899        self.process_the_iframe_attributes(ProcessingMode::FirstTime, can_gc);
900    }
901
902    fn bind_to_tree(&self, context: &BindContext, can_gc: CanGc) {
903        if let Some(s) = self.super_type() {
904            s.bind_to_tree(context, can_gc);
905        }
906        self.owner_document().invalidate_iframes_collection();
907    }
908
909    fn unbind_from_tree(&self, context: &UnbindContext, can_gc: CanGc) {
910        self.super_type().unwrap().unbind_from_tree(context, can_gc);
911
912        let blocker = &self.load_blocker;
913        LoadBlocker::terminate(blocker, CanGc::note());
914
915        // https://html.spec.whatwg.org/multipage/#a-browsing-context-is-discarded
916        let window = self.owner_window();
917        let (sender, receiver) =
918            ProfiledIpc::channel(self.global().time_profiler_chan().clone()).unwrap();
919
920        // Ask the constellation to remove the iframe, and tell us the
921        // pipeline ids of the closed pipelines.
922        let browsing_context_id = match self.browsing_context_id() {
923            None => return warn!("Unbinding already unbound iframe."),
924            Some(id) => id,
925        };
926        debug!("Unbinding frame {}.", browsing_context_id);
927
928        let msg = ScriptToConstellationMessage::RemoveIFrame(browsing_context_id, sender);
929        window
930            .as_global_scope()
931            .script_to_constellation_chan()
932            .send(msg)
933            .unwrap();
934        let exited_pipeline_ids = receiver.recv().unwrap();
935
936        // The spec for discarding is synchronous,
937        // so we need to discard the browsing contexts now, rather than
938        // when the `PipelineExit` message arrives.
939        for exited_pipeline_id in exited_pipeline_ids {
940            // https://html.spec.whatwg.org/multipage/#a-browsing-context-is-discarded
941            if let Some(exited_document) = ScriptThread::find_document(exited_pipeline_id) {
942                debug!(
943                    "Discarding browsing context for pipeline {}",
944                    exited_pipeline_id
945                );
946                let exited_window = exited_document.window();
947                exited_window.discard_browsing_context();
948                for exited_iframe in exited_document.iframes().iter() {
949                    debug!("Discarding nested browsing context");
950                    exited_iframe.destroy_nested_browsing_context();
951                }
952            }
953        }
954
955        // Resetting the pipeline_id to None is required here so that
956        // if this iframe is subsequently re-added to the document
957        // the load doesn't think that it's a navigation, but instead
958        // a new iframe. Without this, the constellation gets very
959        // confused.
960        self.destroy_nested_browsing_context();
961
962        self.owner_document().invalidate_iframes_collection();
963    }
964}