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