servo-script 0.1.0

A component of the servo web-engine.
Documentation
/* 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 https://mozilla.org/MPL/2.0/. */

use std::cell::RefCell;
use std::rc::Rc;
use std::time::{SystemTime, UNIX_EPOCH};

use dom_struct::dom_struct;
use js::rust::HandleObject;
use script_bindings::match_domstring_ascii;
use script_bindings::str::DOMString;
use servo_url::ServoUrl;

use crate::dom::bindings::callback::ExceptionHandling;
use crate::dom::bindings::cell::DomRefCell;
use crate::dom::bindings::codegen::Bindings::CSPViolationReportBodyBinding::CSPViolationReportBody;
use crate::dom::bindings::codegen::Bindings::ReportingObserverBinding::{
    Report, ReportList, ReportingObserverCallback, ReportingObserverMethods,
    ReportingObserverOptions,
};
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::num::Finite;
use crate::dom::bindings::refcounted::Trusted;
use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object_with_proto};
use crate::dom::bindings::root::DomRoot;
use crate::dom::globalscope::GlobalScope;
use crate::dom::window::Window;
use crate::dom::workerglobalscope::WorkerGlobalScope;
use crate::script_runtime::CanGc;

#[dom_struct]
pub(crate) struct ReportingObserver {
    reflector_: Reflector,

    #[conditional_malloc_size_of]
    callback: Rc<ReportingObserverCallback>,
    buffered: RefCell<bool>,
    types: DomRefCell<Vec<DOMString>>,
    report_queue: DomRefCell<Vec<Report>>,
}

impl ReportingObserver {
    fn new_inherited(
        callback: Rc<ReportingObserverCallback>,
        options: &ReportingObserverOptions,
    ) -> Self {
        Self {
            reflector_: Reflector::new(),
            callback,
            buffered: RefCell::new(options.buffered),
            types: DomRefCell::new(options.types.clone().unwrap_or_default()),
            report_queue: Default::default(),
        }
    }

    pub(crate) fn new_with_proto(
        callback: Rc<ReportingObserverCallback>,
        options: &ReportingObserverOptions,
        global: &GlobalScope,
        proto: Option<HandleObject>,
        can_gc: CanGc,
    ) -> DomRoot<Self> {
        reflect_dom_object_with_proto(
            Box::new(Self::new_inherited(callback, options)),
            global,
            proto,
            can_gc,
        )
    }

    fn report_is_visible_to_reporting_observers(report: &Report) -> bool {
        match_domstring_ascii!(report.type_,
                // https://w3c.github.io/webappsec-csp/#reporting
                "csp-violation" => true,
                _ =>  false,
        )
    }

    /// <https://w3c.github.io/reporting/#add-report>
    fn add_report_to_observer(&self, report: &Report) {
        // Step 1. If report’s type is not visible to ReportingObservers, return.
        if !Self::report_is_visible_to_reporting_observers(report) {
            return;
        }
        // Step 2. If observer’s options has a non-empty types member which does not contain report’s type, return.
        let types = self.types.borrow();
        if !types.is_empty() && !types.contains(&report.type_) {
            return;
        }
        // Step 3. Create a new Report r with type initialized to report’s type,
        // url initialized to report’s url, and body initialized to report’s body.
        let report = Report {
            type_: report.type_.clone(),
            url: report.url.clone(),
            body: report.body.clone(),
            destination: report.destination.clone(),
            attempts: report.attempts,
            timestamp: report.timestamp,
        };
        // Step 4. Append r to observer’s report queue.
        self.report_queue.borrow_mut().push(report);
        // Step 5. If the size of observer’s report queue is 1:
        if self.report_queue.borrow().len() == 1 {
            // Step 5.1. Let global be observer’s relevant global object.
            let global = self.global();
            // Step 5.2. Queue a task to § 4.4 Invoke reporting observers with notify list
            // with a copy of global’s registered reporting observer list.
            let observers_global = Trusted::new(&*global);
            global.task_manager().dom_manipulation_task_source().queue(
                task!(notify_reporting_observers: move || {
                    Self::invoke_reporting_observers_with_notify_list(
                        observers_global.root().registered_reporting_observers()
                    );
                }),
            );
        }
    }

    /// <https://w3c.github.io/reporting/#notify-observers>
    pub(crate) fn notify_reporting_observers_on_scope(global: &GlobalScope, report: &Report) {
        // Step 1. For each ReportingObserver observer registered with scope,
        // execute § 4.3 Add report to observer on report and observer.
        for observer in global.registered_reporting_observers().iter() {
            observer.add_report_to_observer(report);
        }
        // Step 2. Append report to scope’s report buffer.
        global.append_report(report.clone());
        // Step 3. Let type be report’s type.
        // TODO(37328)
        // Step 4. If scope’s report buffer now contains more than 100 reports with
        // type equal to type, remove the earliest item with type equal to type in the report buffer.
        // TODO(37328)
    }

    /// <https://w3c.github.io/reporting/#invoke-observers>
    fn invoke_reporting_observers_with_notify_list(notify_list: Vec<DomRoot<ReportingObserver>>) {
        // Step 1. For each ReportingObserver observer in notify list:
        for observer in notify_list.iter() {
            // Step 1.1. If observer’s report queue is empty, then continue.
            if observer.report_queue.borrow().is_empty() {
                continue;
            }
            // Step 1.2. Let reports be a copy of observer’s report queue
            // Step 1.3. Empty observer’s report queue
            let reports = std::mem::take(&mut *observer.report_queue.borrow_mut());
            // Step 1.4. Invoke observer’s callback with « reports, observer » and "report",
            // and with observer as the callback this value.
            let _ = observer.callback.Call_(
                &**observer,
                reports,
                observer,
                ExceptionHandling::Report,
                CanGc::note(),
            );
        }
    }

    /// <https://w3c.github.io/reporting/#generate-a-report>
    fn generate_a_report(
        global: &GlobalScope,
        type_: DOMString,
        url: Option<ServoUrl>,
        body: Option<CSPViolationReportBody>,
        destination: DOMString,
    ) -> Report {
        // Step 2. If url was not provided by the caller, let url be settings’s creation URL.
        let url = url.unwrap_or(global.creation_url());
        // Step 3. Set url’s username to the empty string, and its password to null.
        // Step 4. Set report’s url to the result of executing the URL serializer
        // on url with the exclude fragment flag set.
        let url = Self::strip_url_for_reports(url).into();
        // Step 1. Let report be a new report object with its values initialized as follows:
        // Step 5. Return report.
        Report {
            type_,
            url,
            body,
            destination,
            timestamp: Finite::wrap(
                SystemTime::now()
                    .duration_since(UNIX_EPOCH)
                    .unwrap_or_default()
                    .as_millis() as f64,
            ),
            attempts: 0,
        }
    }

    /// <https://w3c.github.io/reporting/#generate-and-queue-a-report>
    pub(crate) fn generate_and_queue_a_report(
        global: &GlobalScope,
        type_: DOMString,
        body: Option<CSPViolationReportBody>,
        destination: DOMString,
    ) {
        // Step 1. Let settings be context’s relevant settings object.
        // Step 2. Let report be the result of running generate a report with data, type, destination and settings.
        let report = Self::generate_a_report(global, type_, None, body, destination);
        // Step 3. If settings is given, then
        // Step 3.1. Let scope be settings’s global object.
        // Step 3.2. If scope is an object implementing WindowOrWorkerGlobalScope, then
        // execute § 4.2 Notify reporting observers on scope with report with scope and report.
        Self::notify_reporting_observers_on_scope(global, &report);
        // Step 4. Append report to context’s reports.
        global.append_report(report);
    }

    /// <https://w3c.github.io/webappsec-csp/#strip-url-for-use-in-reports>
    pub(crate) fn strip_url_for_reports(mut url: ServoUrl) -> String {
        let scheme = url.scheme();
        // Step 1: If url’s scheme is not an HTTP(S) scheme, then return url’s scheme.
        //
        // Note we also treat ws(s) schemes as HTTP(S) per
        // https://github.com/w3c/webappsec-csp/issues/735
        if scheme != "https" && scheme != "http" && scheme != "ws" && scheme != "wss" {
            return scheme.to_owned();
        }
        // Step 2: Set url’s fragment to the empty string.
        url.set_fragment(None);
        // Step 3: Set url’s username to the empty string.
        let _ = url.set_username("");
        // Step 4: Set url’s password to the empty string.
        let _ = url.set_password(None);
        // Step 5: Return the result of executing the URL serializer on url.
        url.into_string()
    }
}

impl ReportingObserverMethods<crate::DomTypeHolder> for ReportingObserver {
    /// <https://w3c.github.io/reporting/#dom-reportingobserver-reportingobserver>
    fn Constructor(
        global: &GlobalScope,
        proto: Option<HandleObject>,
        can_gc: CanGc,
        callback: Rc<ReportingObserverCallback>,
        options: &ReportingObserverOptions,
    ) -> DomRoot<ReportingObserver> {
        // Step 1. Create a new ReportingObserver object observer.
        // Step 2. Set observer’s callback to callback.
        // Step 3. Set observer’s options to options.
        // Step 4. Return observer.
        ReportingObserver::new_with_proto(callback, options, global, proto, can_gc)
    }

    /// <https://w3c.github.io/reporting/#dom-reportingobserver-observe>
    fn Observe(&self) {
        // Step 1. Let global be the be the relevant global object of this.
        let global = &self.global();
        // Step 2. Append this to the global’s registered reporting observer list.
        global.append_reporting_observer(self);
        // Step 3. If this’s buffered option is false, return.
        if !*self.buffered.borrow() {
            return;
        }
        // Step 4. Set this’s buffered option to false.
        *self.buffered.borrow_mut() = false;
        // Step 5.For each report in global’s report buffer, queue a task to
        // execute § 4.3 Add report to observer with report and this.
        for report in global.buffered_reports() {
            // TODO(37328): Figure out how to put this in a task
            self.add_report_to_observer(&report);
        }
    }

    /// <https://w3c.github.io/reporting/#dom-reportingobserver-disconnect>
    fn Disconnect(&self) {
        // Step 1. If this is not registered, return.
        // Skipped, as this is handled in `remove_reporting_observer`

        // Step 2. Let global be the relevant global object of this.
        let global = &self.global();
        // Step 3. Remove this from global’s registered reporting observer list.
        global.remove_reporting_observer(self);
    }

    /// <https://w3c.github.io/reporting/#dom-reportingobserver-takerecords>
    fn TakeRecords(&self) -> ReportList {
        // Step 1. Let reports be a copy of this’s report queue.
        // Step 2. Empty this’s report queue.
        // Step 3. Return reports.
        std::mem::take(&mut *self.report_queue.borrow_mut())
    }
}

impl GlobalScope {
    fn append_reporting_observer(&self, reporting_observer: &ReportingObserver) {
        if let Some(window) = self.downcast::<Window>() {
            return window.append_reporting_observer(DomRoot::from_ref(reporting_observer));
        }
        if let Some(worker) = self.downcast::<WorkerGlobalScope>() {
            return worker.append_reporting_observer(DomRoot::from_ref(reporting_observer));
        }
        unreachable!();
    }

    fn remove_reporting_observer(&self, reporting_observer: &ReportingObserver) {
        if let Some(window) = self.downcast::<Window>() {
            return window.remove_reporting_observer(reporting_observer);
        }
        if let Some(worker) = self.downcast::<WorkerGlobalScope>() {
            return worker.remove_reporting_observer(reporting_observer);
        }
        unreachable!();
    }

    fn registered_reporting_observers(&self) -> Vec<DomRoot<ReportingObserver>> {
        if let Some(window) = self.downcast::<Window>() {
            return window.registered_reporting_observers();
        }
        if let Some(worker) = self.downcast::<WorkerGlobalScope>() {
            return worker.registered_reporting_observers();
        }
        unreachable!();
    }

    fn append_report(&self, report: Report) {
        if let Some(window) = self.downcast::<Window>() {
            return window.append_report(report);
        }
        if let Some(worker) = self.downcast::<WorkerGlobalScope>() {
            return worker.append_report(report);
        }
        unreachable!();
    }

    fn buffered_reports(&self) -> Vec<Report> {
        if let Some(window) = self.downcast::<Window>() {
            return window.buffered_reports();
        }
        if let Some(worker) = self.downcast::<WorkerGlobalScope>() {
            return worker.buffered_reports();
        }
        unreachable!();
    }
}