servo-script 0.3.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 serde::Serialize;
use servo_url::ServoUrl;

use crate::conversions::Convert;
use crate::dom::bindings::codegen::Bindings::CSPViolationReportBodyBinding::CSPViolationReportBody;
use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
use crate::dom::bindings::codegen::Bindings::EventBinding::EventInit;
use crate::dom::bindings::codegen::Bindings::ReportingObserverBinding::ReportBody;
use crate::dom::bindings::codegen::Bindings::SecurityPolicyViolationEventBinding::{
    SecurityPolicyViolationEventDisposition, SecurityPolicyViolationEventInit,
};
use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
use crate::dom::bindings::inheritance::Castable;
use crate::dom::globalscope::GlobalScope;
use crate::dom::reporting::reportingobserver::ReportingObserver;
use crate::dom::window::Window;

#[derive(Clone)]
pub(crate) struct SecurityPolicyViolationReport {
    sample: Option<String>,
    blocked_url: String,
    referrer: String,
    status_code: u16,
    document_url: String,
    source_file: String,
    violated_directive: String,
    effective_directive: String,
    line_number: u32,
    column_number: u32,
    original_policy: String,
    disposition: SecurityPolicyViolationEventDisposition,
}

#[derive(Serialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) struct CSPReportUriViolationReportBody {
    document_uri: String,
    referrer: String,
    blocked_uri: String,
    effective_directive: String,
    violated_directive: String,
    original_policy: String,
    #[serde(serialize_with = "serialize_disposition")]
    disposition: SecurityPolicyViolationEventDisposition,
    status_code: u16,
    script_sample: Option<String>,
    source_file: Option<String>,
    line_number: Option<u32>,
    column_number: Option<u32>,
}

#[derive(Serialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) struct CSPReportUriViolationReport {
    pub(crate) csp_report: CSPReportUriViolationReportBody,
}

impl Convert<SecurityPolicyViolationEventInit> for SecurityPolicyViolationReport {
    fn convert(self) -> SecurityPolicyViolationEventInit {
        SecurityPolicyViolationEventInit {
            sample: self.sample.unwrap_or_default().into(),
            blockedURI: self.blocked_url.into(),
            referrer: self.referrer.into(),
            statusCode: self.status_code,
            documentURI: self.document_url.into(),
            sourceFile: self.source_file.into(),
            violatedDirective: self.violated_directive.into(),
            effectiveDirective: self.effective_directive.into(),
            lineNumber: self.line_number,
            columnNumber: self.column_number,
            originalPolicy: self.original_policy.into(),
            disposition: self.disposition,
            parent: EventInit::empty(),
        }
    }
}

impl Convert<CSPViolationReportBody> for SecurityPolicyViolationReport {
    fn convert(self) -> CSPViolationReportBody {
        let (source_file, line_number, column_number) = if !self.source_file.is_empty() {
            (
                Some(self.source_file.into()),
                Some(self.line_number),
                Some(self.column_number),
            )
        } else {
            (None, None, None)
        };
        CSPViolationReportBody {
            sample: self.sample.map(|s| s.into()),
            blockedURL: Some(self.blocked_url.into()),
            // TODO(37328): Why does /content-security-policy/reporting-api/
            // report-to-directive-allowed-in-meta.https.sub.html expect this to be
            // empty, yet the spec expects us to copy referrer from SecurityPolicyViolationReport
            referrer: Some("".to_owned().into()),
            statusCode: self.status_code,
            documentURL: self.document_url.into(),
            sourceFile: source_file,
            effectiveDirective: self.effective_directive.into(),
            lineNumber: line_number,
            columnNumber: column_number,
            originalPolicy: self.original_policy.into(),
            disposition: self.disposition,
            parent: ReportBody::empty(),
        }
    }
}

/// <https://www.w3.org/TR/CSP/#deprecated-serialize-violation>
impl From<SecurityPolicyViolationReport> for CSPReportUriViolationReportBody {
    fn from(value: SecurityPolicyViolationReport) -> Self {
        // Step 1. Let body be a map with its keys initialized as follows:
        let mut converted = Self {
            document_uri: value.document_url,
            referrer: value.referrer,
            blocked_uri: value.blocked_url,
            effective_directive: value.effective_directive,
            violated_directive: value.violated_directive,
            original_policy: value.original_policy,
            disposition: value.disposition,
            status_code: value.status_code,
            script_sample: None,
            source_file: None,
            line_number: None,
            column_number: None,
        };

        // Step 2. If violation’s source file is not null:
        if !value.source_file.is_empty() {
            // Step 2.1. Set body["source-file'] to the result of
            // executing § 5.4 Strip URL for use in reports on violation’s source file.
            converted.source_file = ServoUrl::parse(&value.source_file)
                .map(ReportingObserver::strip_url_for_reports)
                .ok();
            // Step 2.2. Set body["line-number"] to violation’s line number.
            converted.line_number = Some(value.line_number);
            // Step 2.3. Set body["column-number"] to violation’s column number.
            converted.column_number = Some(value.column_number);
        }

        // Step 3. Assert: If body["blocked-uri"] is not "inline", then body["sample"] is the empty string.
        debug_assert!(converted.blocked_uri == "inline" || converted.script_sample.is_none());

        converted
    }
}

#[derive(Default)]
pub(crate) struct CSPViolationReportBuilder {
    pub report_only: bool,
    /// <https://www.w3.org/TR/CSP3/#violation-sample>
    pub sample: Option<String>,
    /// <https://www.w3.org/TR/CSP3/#violation-resource>
    pub resource: String,
    /// <https://www.w3.org/TR/CSP3/#violation-line-number>
    pub line_number: u32,
    /// <https://www.w3.org/TR/CSP3/#violation-column-number>
    pub column_number: u32,
    /// <https://www.w3.org/TR/CSP3/#violation-source-file>
    pub source_file: String,
    /// <https://www.w3.org/TR/CSP3/#violation-effective-directive>
    pub effective_directive: String,
    /// <https://www.w3.org/TR/CSP3/#violation-policy>
    pub original_policy: String,
}

impl CSPViolationReportBuilder {
    pub fn report_only(mut self, report_only: bool) -> CSPViolationReportBuilder {
        self.report_only = report_only;
        self
    }

    /// <https://www.w3.org/TR/CSP3/#violation-sample>
    pub fn sample(mut self, sample: Option<String>) -> CSPViolationReportBuilder {
        self.sample = sample;
        self
    }

    /// <https://www.w3.org/TR/CSP3/#violation-resource>
    pub fn resource(mut self, resource: String) -> CSPViolationReportBuilder {
        self.resource = resource;
        self
    }

    /// <https://www.w3.org/TR/CSP3/#violation-line-number>
    pub fn line_number(mut self, line_number: u32) -> CSPViolationReportBuilder {
        self.line_number = line_number;
        self
    }

    /// <https://www.w3.org/TR/CSP3/#violation-column-number>
    pub fn column_number(mut self, column_number: u32) -> CSPViolationReportBuilder {
        self.column_number = column_number;
        self
    }

    /// <https://www.w3.org/TR/CSP3/#violation-source-file>
    pub fn source_file(mut self, source_file: String) -> CSPViolationReportBuilder {
        self.source_file = source_file;
        self
    }

    /// <https://www.w3.org/TR/CSP3/#violation-effective-directive>
    pub fn effective_directive(mut self, effective_directive: String) -> CSPViolationReportBuilder {
        self.effective_directive = effective_directive;
        self
    }

    /// <https://www.w3.org/TR/CSP3/#violation-policy>
    pub fn original_policy(mut self, original_policy: String) -> CSPViolationReportBuilder {
        self.original_policy = original_policy;
        self
    }

    /// <https://w3c.github.io/webappsec-csp/#create-violation-for-global>
    pub fn build(self, global: &GlobalScope) -> SecurityPolicyViolationReport {
        // Step 2. If the user agent is currently executing script, and can extract a source file’s URL,
        // line number, and column number from the global, set violation’s source file, line number, and column number accordingly.
        //
        // Handled by caller

        // Step 1. Let violation be a new violation whose global object is global,
        // policy is policy, effective directive is directive, and resource is null.
        // Step 5. Return violation.
        SecurityPolicyViolationReport {
            violated_directive: self.effective_directive.clone(),
            effective_directive: self.effective_directive.clone(),
            document_url: ReportingObserver::strip_url_for_reports(global.get_url()),
            disposition: match self.report_only {
                true => SecurityPolicyViolationEventDisposition::Report,
                false => SecurityPolicyViolationEventDisposition::Enforce,
            },
            // Step 3. If global is a Window object, set violation’s referrer to global’s document’s referrer.
            referrer: global
                .downcast::<Window>()
                .map(|window| window.Document().Referrer().into())
                .unwrap_or_default(),
            sample: self.sample,
            blocked_url: self.resource,
            source_file: self.source_file,
            original_policy: self.original_policy,
            line_number: self.line_number,
            column_number: self.column_number,
            // Step 4. Set violation’s status to the HTTP status code for
            // the resource associated with violation’s global object.
            status_code: global.status_code().unwrap_or(0),
        }
    }
}

pub(crate) fn serialize_disposition<S: serde::Serializer>(
    val: &SecurityPolicyViolationEventDisposition,
    serializer: S,
) -> Result<S::Ok, S::Error> {
    match val {
        SecurityPolicyViolationEventDisposition::Report => serializer.serialize_str("report"),
        SecurityPolicyViolationEventDisposition::Enforce => serializer.serialize_str("enforce"),
    }
}