Implement initial version of ReportingObserver (#37905)

The specification moved around lately with how it defines its reports
and report bodies. They became dictionaries, but are currently missing
some fields [1].

Most tests won't be passing yet, since the `Reporting-Endpoints` header
isn't used yet. In fact, the specification leaves it up to the browser
to figure out when to run this task [2]. I am not sure if there some
background scheduling we can do here.

Confirmed with content-security-policy/reporting-api/
report-to-directive-allowed-in-meta.https.sub.html that the callback is
invoked. The test doesn't pass, since
the `describe_scripted_caller` is empty for HTML elements. Thus the
`source_file` is empty, whereas it should be equivalent to the current
document URL.

Part of #37328

Signed-off-by: Tim van der Lippe <tvanderlippe@gmail.com>

[1]: https://github.com/w3c/reporting/issues/286
[2]: https://w3c.github.io/reporting/#report-delivery
This commit is contained in:
Tim van der Lippe 2025-07-07 12:43:30 +02:00 committed by GitHub
parent 3d4868592a
commit fcb2a4cd95
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 551 additions and 101 deletions

View file

@ -7,11 +7,14 @@ use serde::Serialize;
use servo_url::ServoUrl; use servo_url::ServoUrl;
use crate::conversions::Convert; use crate::conversions::Convert;
use crate::dom::bindings::codegen::Bindings::CSPViolationReportBodyBinding::CSPViolationReportBody;
use crate::dom::bindings::codegen::Bindings::EventBinding::EventInit; use crate::dom::bindings::codegen::Bindings::EventBinding::EventInit;
use crate::dom::bindings::codegen::Bindings::ReportingObserverBinding::ReportBody;
use crate::dom::bindings::codegen::Bindings::SecurityPolicyViolationEventBinding::{ use crate::dom::bindings::codegen::Bindings::SecurityPolicyViolationEventBinding::{
SecurityPolicyViolationEventDisposition, SecurityPolicyViolationEventInit, SecurityPolicyViolationEventDisposition, SecurityPolicyViolationEventInit,
}; };
use crate::dom::globalscope::GlobalScope; use crate::dom::globalscope::GlobalScope;
use crate::dom::reportingobserver::ReportingObserver;
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -77,6 +80,28 @@ impl Convert<SecurityPolicyViolationEventInit> for SecurityPolicyViolationReport
} }
} }
impl Convert<CSPViolationReportBody> for SecurityPolicyViolationReport {
fn convert(self) -> CSPViolationReportBody {
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: Some(self.source_file.into()),
effectiveDirective: self.effective_directive.into(),
lineNumber: Some(self.line_number),
columnNumber: Some(self.column_number),
originalPolicy: self.original_policy.into(),
disposition: self.disposition,
parent: ReportBody::empty(),
}
}
}
/// <https://www.w3.org/TR/CSP/#deprecated-serialize-violation> /// <https://www.w3.org/TR/CSP/#deprecated-serialize-violation>
impl From<SecurityPolicyViolationReport> for CSPReportUriViolationReportBody { impl From<SecurityPolicyViolationReport> for CSPReportUriViolationReportBody {
fn from(value: SecurityPolicyViolationReport) -> Self { fn from(value: SecurityPolicyViolationReport) -> Self {
@ -101,7 +126,7 @@ impl From<SecurityPolicyViolationReport> for CSPReportUriViolationReportBody {
// Step 2.1. Set body["source-file'] to the result of // Step 2.1. Set body["source-file'] to the result of
// executing §5.4 Strip URL for use in reports on violations source file. // executing §5.4 Strip URL for use in reports on violations source file.
converted.source_file = ServoUrl::parse(&value.source_file) converted.source_file = ServoUrl::parse(&value.source_file)
.map(strip_url_for_reports) .map(ReportingObserver::strip_url_for_reports)
.ok(); .ok();
// Step 2.2. Set body["line-number"] to violations line number. // Step 2.2. Set body["line-number"] to violations line number.
converted.line_number = Some(value.line_number); converted.line_number = Some(value.line_number);
@ -187,15 +212,15 @@ impl CSPViolationReportBuilder {
SecurityPolicyViolationReport { SecurityPolicyViolationReport {
violated_directive: self.effective_directive.clone(), violated_directive: self.effective_directive.clone(),
effective_directive: self.effective_directive.clone(), effective_directive: self.effective_directive.clone(),
document_url: strip_url_for_reports(global.get_url()), document_url: ReportingObserver::strip_url_for_reports(global.get_url()),
disposition: match self.report_only { disposition: match self.report_only {
true => SecurityPolicyViolationEventDisposition::Report, true => SecurityPolicyViolationEventDisposition::Report,
false => SecurityPolicyViolationEventDisposition::Enforce, false => SecurityPolicyViolationEventDisposition::Enforce,
}, },
// https://w3c.github.io/webappsec-csp/#violation-referrer // https://w3c.github.io/webappsec-csp/#violation-referrer
referrer: match global.get_referrer() { referrer: match global.get_referrer() {
Referrer::Client(url) => strip_url_for_reports(url), Referrer::Client(url) => ReportingObserver::strip_url_for_reports(url),
Referrer::ReferrerUrl(url) => strip_url_for_reports(url), Referrer::ReferrerUrl(url) => ReportingObserver::strip_url_for_reports(url),
_ => "".to_owned(), _ => "".to_owned(),
}, },
sample: self.sample, sample: self.sample,
@ -218,20 +243,3 @@ fn serialize_disposition<S: serde::Serializer>(
SecurityPolicyViolationEventDisposition::Enforce => serializer.serialize_str("enforce"), SecurityPolicyViolationEventDisposition::Enforce => serializer.serialize_str("enforce"),
} }
} }
/// <https://w3c.github.io/webappsec-csp/#strip-url-for-use-in-reports>
fn strip_url_for_reports(mut url: ServoUrl) -> String {
let scheme = url.scheme();
// > Step 1: If urls scheme is not an HTTP(S) scheme, then return urls scheme.
if scheme != "https" && scheme != "http" {
return scheme.to_owned();
}
// > Step 2: Set urls fragment to the empty string.
url.set_fragment(None);
// > Step 3: Set urls username to the empty string.
let _ = url.set_username("");
// > Step 4: Set urls 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()
}

View file

@ -76,6 +76,7 @@ use crate::dom::bindings::codegen::Bindings::NotificationBinding::NotificationPe
use crate::dom::bindings::codegen::Bindings::PermissionStatusBinding::{ use crate::dom::bindings::codegen::Bindings::PermissionStatusBinding::{
PermissionName, PermissionState, PermissionName, PermissionState,
}; };
use crate::dom::bindings::codegen::Bindings::ReportingObserverBinding::Report;
use crate::dom::bindings::codegen::Bindings::VoidFunctionBinding::VoidFunction; use crate::dom::bindings::codegen::Bindings::VoidFunctionBinding::VoidFunction;
use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods; use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
use crate::dom::bindings::codegen::Bindings::WorkerGlobalScopeBinding::WorkerGlobalScopeMethods; use crate::dom::bindings::codegen::Bindings::WorkerGlobalScopeBinding::WorkerGlobalScopeMethods;
@ -109,6 +110,7 @@ use crate::dom::performance::Performance;
use crate::dom::performanceobserver::VALID_ENTRY_TYPES; use crate::dom::performanceobserver::VALID_ENTRY_TYPES;
use crate::dom::promise::Promise; use crate::dom::promise::Promise;
use crate::dom::readablestream::{CrossRealmTransformReadable, ReadableStream}; use crate::dom::readablestream::{CrossRealmTransformReadable, ReadableStream};
use crate::dom::reportingobserver::ReportingObserver;
use crate::dom::serviceworker::ServiceWorker; use crate::dom::serviceworker::ServiceWorker;
use crate::dom::serviceworkerregistration::ServiceWorkerRegistration; use crate::dom::serviceworkerregistration::ServiceWorkerRegistration;
use crate::dom::trustedtypepolicyfactory::TrustedTypePolicyFactory; use crate::dom::trustedtypepolicyfactory::TrustedTypePolicyFactory;
@ -3332,6 +3334,56 @@ impl GlobalScope {
unreachable!(); unreachable!();
} }
pub(crate) 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!();
}
pub(crate) 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!();
}
pub(crate) 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!();
}
pub(crate) 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!();
}
pub(crate) 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!();
}
pub(crate) fn import_map(&self) -> Ref<'_, ImportMap> { pub(crate) fn import_map(&self) -> Ref<'_, ImportMap> {
self.import_map.borrow() self.import_map.borrow()
} }

View file

@ -517,6 +517,7 @@ pub(crate) mod readablestreambyobrequest;
pub(crate) mod readablestreamdefaultcontroller; pub(crate) mod readablestreamdefaultcontroller;
pub(crate) mod readablestreamdefaultreader; pub(crate) mod readablestreamdefaultreader;
pub(crate) mod readablestreamgenericreader; pub(crate) mod readablestreamgenericreader;
pub(crate) mod reportingobserver;
pub(crate) mod request; pub(crate) mod request;
pub(crate) mod resizeobserver; pub(crate) mod resizeobserver;
pub(crate) mod resizeobserverentry; pub(crate) mod resizeobserverentry;

View file

@ -0,0 +1,278 @@
/* 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::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::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::script_runtime::CanGc;
#[dom_struct]
pub(crate) struct ReportingObserver {
reflector_: Reflector,
#[ignore_malloc_size_of = "Rc has unclear ownership"]
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(),
}
}
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
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 report.type_.str() {
// 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 reports type is not visible to ReportingObservers, return.
if !Self::report_is_visible_to_reporting_observers(report) {
return;
}
// Step 2. If observers options has a non-empty types member which does not contain reports 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 reports type,
// url initialized to reports url, and body initialized to reports 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 observers report queue.
self.report_queue.borrow_mut().push(report);
// Step 5. If the size of observers report queue is 1:
if self.report_queue.borrow().len() == 1 {
// Step 5.1. Let global be observers 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 globals 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 scopes report buffer.
global.append_report(report.clone());
// Step 3. Let type be reports type.
// TODO(37328)
// Step 4. If scopes 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 observers report queue is empty, then continue.
if observer.report_queue.borrow().is_empty() {
continue;
}
// Step 1.2. Let reports be a copy of observers report queue
// Step 1.3. Empty observers report queue
let reports = std::mem::take(&mut *observer.report_queue.borrow_mut());
// Step 1.4. Invoke observers 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 settingss creation URL.
let url = url.unwrap_or(global.creation_url().clone());
// Step 3. Set urls username to the empty string, and its password to null.
// Step 4. Set reports 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 contexts 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 settingss 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 contexts 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 urls scheme is not an HTTP(S) scheme, then return urls scheme.
if scheme != "https" && scheme != "http" {
return scheme.to_owned();
}
// Step 2: Set urls fragment to the empty string.
url.set_fragment(None);
// Step 3: Set urls username to the empty string.
let _ = url.set_username("");
// Step 4: Set urls 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 observers callback to callback.
// Step 3. Set observers 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 globals registered reporting observer list.
global.append_reporting_observer(self);
// Step 3. If thiss buffered option is false, return.
if !*self.buffered.borrow() {
return;
}
// Step 4. Set thiss buffered option to false.
*self.buffered.borrow_mut() = false;
// Step 5.For each report in globals 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 globals 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 thiss report queue.
// Step 2. Empty thiss report queue.
// Step 3. Return reports.
std::mem::take(&mut *self.report_queue.borrow_mut())
}
}

View file

@ -102,6 +102,7 @@ use crate::dom::bindings::codegen::Bindings::ImageBitmapBinding::{
ImageBitmapOptions, ImageBitmapSource, ImageBitmapOptions, ImageBitmapSource,
}; };
use crate::dom::bindings::codegen::Bindings::MediaQueryListBinding::MediaQueryList_Binding::MediaQueryListMethods; use crate::dom::bindings::codegen::Bindings::MediaQueryListBinding::MediaQueryList_Binding::MediaQueryListMethods;
use crate::dom::bindings::codegen::Bindings::ReportingObserverBinding::Report;
use crate::dom::bindings::codegen::Bindings::RequestBinding::RequestInit; use crate::dom::bindings::codegen::Bindings::RequestBinding::RequestInit;
use crate::dom::bindings::codegen::Bindings::VoidFunctionBinding::VoidFunction; use crate::dom::bindings::codegen::Bindings::VoidFunctionBinding::VoidFunction;
use crate::dom::bindings::codegen::Bindings::WindowBinding::{ use crate::dom::bindings::codegen::Bindings::WindowBinding::{
@ -146,6 +147,7 @@ use crate::dom::navigator::Navigator;
use crate::dom::node::{Node, NodeDamage, NodeTraits, from_untrusted_node_address}; use crate::dom::node::{Node, NodeDamage, NodeTraits, from_untrusted_node_address};
use crate::dom::performance::Performance; use crate::dom::performance::Performance;
use crate::dom::promise::Promise; use crate::dom::promise::Promise;
use crate::dom::reportingobserver::ReportingObserver;
use crate::dom::screen::Screen; use crate::dom::screen::Screen;
use crate::dom::selection::Selection; use crate::dom::selection::Selection;
use crate::dom::shadowroot::ShadowRoot; use crate::dom::shadowroot::ShadowRoot;
@ -398,6 +400,12 @@ pub(crate) struct Window {
/// <https://dom.spec.whatwg.org/#window-current-event> /// <https://dom.spec.whatwg.org/#window-current-event>
current_event: DomRefCell<Option<Dom<Event>>>, current_event: DomRefCell<Option<Dom<Event>>>,
/// <https://w3c.github.io/reporting/#windoworworkerglobalscope-registered-reporting-observer-list>
reporting_observer_list: DomRefCell<Vec<DomRoot<ReportingObserver>>>,
/// <https://w3c.github.io/reporting/#windoworworkerglobalscope-reports>
report_list: DomRefCell<Vec<Report>>,
} }
impl Window { impl Window {
@ -502,6 +510,35 @@ impl Window {
self.window_proxy.get().unwrap() self.window_proxy.get().unwrap()
} }
pub(crate) fn append_reporting_observer(&self, reporting_observer: DomRoot<ReportingObserver>) {
self.reporting_observer_list
.borrow_mut()
.push(reporting_observer);
}
pub(crate) fn remove_reporting_observer(&self, reporting_observer: &ReportingObserver) {
if let Some(index) = self
.reporting_observer_list
.borrow()
.iter()
.position(|observer| &**observer == reporting_observer)
{
self.reporting_observer_list.borrow_mut().remove(index);
}
}
pub(crate) fn registered_reporting_observers(&self) -> Vec<DomRoot<ReportingObserver>> {
self.reporting_observer_list.borrow().clone()
}
pub(crate) fn append_report(&self, report: Report) {
self.report_list.borrow_mut().push(report);
}
pub(crate) fn buffered_reports(&self) -> Vec<Report> {
self.report_list.borrow().clone()
}
/// Returns the window proxy if it has not been discarded. /// Returns the window proxy if it has not been discarded.
/// <https://html.spec.whatwg.org/multipage/#a-browsing-context-is-discarded> /// <https://html.spec.whatwg.org/multipage/#a-browsing-context-is-discarded>
pub(crate) fn undiscarded_window_proxy(&self) -> Option<DomRoot<WindowProxy>> { pub(crate) fn undiscarded_window_proxy(&self) -> Option<DomRoot<WindowProxy>> {
@ -3106,6 +3143,8 @@ impl Window {
current_event: DomRefCell::new(None), current_event: DomRefCell::new(None),
theme: Cell::new(theme), theme: Cell::new(theme),
trusted_types: Default::default(), trusted_types: Default::default(),
reporting_observer_list: Default::default(),
report_list: Default::default(),
}); });
unsafe { unsafe {

View file

@ -36,6 +36,7 @@ use crate::dom::bindings::cell::{DomRefCell, Ref};
use crate::dom::bindings::codegen::Bindings::ImageBitmapBinding::{ use crate::dom::bindings::codegen::Bindings::ImageBitmapBinding::{
ImageBitmapOptions, ImageBitmapSource, ImageBitmapOptions, ImageBitmapSource,
}; };
use crate::dom::bindings::codegen::Bindings::ReportingObserverBinding::Report;
use crate::dom::bindings::codegen::Bindings::RequestBinding::RequestInit; use crate::dom::bindings::codegen::Bindings::RequestBinding::RequestInit;
use crate::dom::bindings::codegen::Bindings::VoidFunctionBinding::VoidFunction; use crate::dom::bindings::codegen::Bindings::VoidFunctionBinding::VoidFunction;
use crate::dom::bindings::codegen::Bindings::WorkerBinding::WorkerType; use crate::dom::bindings::codegen::Bindings::WorkerBinding::WorkerType;
@ -56,6 +57,7 @@ use crate::dom::globalscope::GlobalScope;
use crate::dom::idbfactory::IDBFactory; use crate::dom::idbfactory::IDBFactory;
use crate::dom::performance::Performance; use crate::dom::performance::Performance;
use crate::dom::promise::Promise; use crate::dom::promise::Promise;
use crate::dom::reportingobserver::ReportingObserver;
use crate::dom::trustedscripturl::TrustedScriptURL; use crate::dom::trustedscripturl::TrustedScriptURL;
use crate::dom::trustedtypepolicyfactory::TrustedTypePolicyFactory; use crate::dom::trustedtypepolicyfactory::TrustedTypePolicyFactory;
use crate::dom::types::ImageBitmap; use crate::dom::types::ImageBitmap;
@ -139,6 +141,12 @@ pub(crate) struct WorkerGlobalScope {
#[no_trace] #[no_trace]
insecure_requests_policy: InsecureRequestsPolicy, insecure_requests_policy: InsecureRequestsPolicy,
/// <https://w3c.github.io/reporting/#windoworworkerglobalscope-registered-reporting-observer-list>
reporting_observer_list: DomRefCell<Vec<DomRoot<ReportingObserver>>>,
/// <https://w3c.github.io/reporting/#windoworworkerglobalscope-reports>
report_list: DomRefCell<Vec<Report>>,
} }
impl WorkerGlobalScope { impl WorkerGlobalScope {
@ -196,6 +204,8 @@ impl WorkerGlobalScope {
timer_scheduler: RefCell::default(), timer_scheduler: RefCell::default(),
insecure_requests_policy, insecure_requests_policy,
trusted_types: Default::default(), trusted_types: Default::default(),
reporting_observer_list: Default::default(),
report_list: Default::default(),
} }
} }
@ -259,6 +269,35 @@ impl WorkerGlobalScope {
self.policy_container.borrow_mut().set_csp_list(csp_list); self.policy_container.borrow_mut().set_csp_list(csp_list);
} }
pub(crate) fn append_reporting_observer(&self, reporting_observer: DomRoot<ReportingObserver>) {
self.reporting_observer_list
.borrow_mut()
.push(reporting_observer);
}
pub(crate) fn remove_reporting_observer(&self, reporting_observer: &ReportingObserver) {
if let Some(index) = self
.reporting_observer_list
.borrow()
.iter()
.position(|observer| &**observer == reporting_observer)
{
self.reporting_observer_list.borrow_mut().remove(index);
}
}
pub(crate) fn registered_reporting_observers(&self) -> Vec<DomRoot<ReportingObserver>> {
self.reporting_observer_list.borrow().clone()
}
pub(crate) fn append_report(&self, report: Report) {
self.report_list.borrow_mut().push(report);
}
pub(crate) fn buffered_reports(&self) -> Vec<Report> {
self.report_list.borrow().clone()
}
/// Get a mutable reference to the [`TimerScheduler`] for this [`ServiceWorkerGlobalScope`]. /// Get a mutable reference to the [`TimerScheduler`] for this [`ServiceWorkerGlobalScope`].
pub(crate) fn timer_scheduler(&self) -> RefMut<TimerScheduler> { pub(crate) fn timer_scheduler(&self) -> RefMut<TimerScheduler> {
self.timer_scheduler.borrow_mut() self.timer_scheduler.borrow_mut()

View file

@ -26,6 +26,7 @@ use crate::dom::csppolicyviolationreport::{
use crate::dom::event::{Event, EventBubbles, EventCancelable, EventComposed}; use crate::dom::event::{Event, EventBubbles, EventCancelable, EventComposed};
use crate::dom::eventtarget::EventTarget; use crate::dom::eventtarget::EventTarget;
use crate::dom::performanceresourcetiming::InitiatorType; use crate::dom::performanceresourcetiming::InitiatorType;
use crate::dom::reportingobserver::ReportingObserver;
use crate::dom::securitypolicyviolationevent::SecurityPolicyViolationEvent; use crate::dom::securitypolicyviolationevent::SecurityPolicyViolationEvent;
use crate::dom::types::GlobalScope; use crate::dom::types::GlobalScope;
use crate::fetch::create_a_potential_cors_request; use crate::fetch::create_a_potential_cors_request;
@ -156,6 +157,24 @@ impl TaskOnce for CSPViolationReportTask {
{ {
self.post_csp_violation_to_report_uri(report_uri_directive); self.post_csp_violation_to_report_uri(report_uri_directive);
} }
// Step 3.5. If violations policys directive set contains a directive named "report-to" directive:
if let Some(report_to_directive) = self
.violation_policy
.directive_set
.iter()
.find(|directive| directive.name == "report-to")
{
// Step 3.5.1. Let body be a new CSPViolationReportBody, initialized as follows:
let body = self.violation_report.clone().convert();
// Step 3.5.2. Let settings object be violations global objects relevant settings object.
// Step 3.5.3. Generate and queue a report with the following arguments:
ReportingObserver::generate_and_queue_a_report(
&self.global.root(),
"csp-violation".into(),
Some(body),
report_to_directive.value.join(" ").into(),
)
}
} }
} }

View file

@ -799,6 +799,10 @@ Dictionaries = {
'derives': ['Clone', 'Copy'], 'derives': ['Clone', 'Copy'],
}, },
'CSPViolationReportBody': {
'derives': ['Clone', 'MallocSizeOf'],
},
'FontFaceDescriptors': { 'FontFaceDescriptors': {
'derives': ['Clone', 'MallocSizeOf'] 'derives': ['Clone', 'MallocSizeOf']
}, },
@ -831,6 +835,14 @@ Dictionaries = {
'derives': ['Clone'], 'derives': ['Clone'],
}, },
'Report': {
'derives': ['Clone', 'MallocSizeOf'],
},
'ReportBody': {
'derives': ['Clone', 'MallocSizeOf'],
},
'StereoPannerOptions': { 'StereoPannerOptions': {
'derives': ['Clone', 'Copy'], 'derives': ['Clone', 'Copy'],
}, },

View file

@ -0,0 +1,19 @@
/* 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/. */
// https://w3c.github.io/webappsec-csp/#dictdef-cspviolationreportbody
dictionary CSPViolationReportBody : ReportBody {
required USVString documentURL;
USVString referrer;
USVString blockedURL;
required DOMString effectiveDirective;
required DOMString originalPolicy;
USVString sourceFile;
DOMString sample;
required SecurityPolicyViolationEventDisposition disposition;
required unsigned short statusCode;
unsigned long lineNumber;
unsigned long columnNumber;
};

View file

@ -0,0 +1,35 @@
/* 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/. */
// https://w3c.github.io/reporting/#interface-reporting-observer
dictionary ReportBody {
};
dictionary Report {
required DOMString type;
required DOMString url;
required DOMString destination;
required double timestamp;
required long attempts;
// TODO(37328): Change this to parent class ReportBody
CSPViolationReportBody body;
};
[Exposed=(Window,Worker)]
interface ReportingObserver {
constructor(ReportingObserverCallback callback, optional ReportingObserverOptions options = {});
undefined observe();
undefined disconnect();
ReportList takeRecords();
};
callback ReportingObserverCallback = undefined (sequence<Report> reports, ReportingObserver observer);
dictionary ReportingObserverOptions {
sequence<DOMString> types;
boolean buffered = false;
};
typedef sequence<Report> ReportList;

View file

@ -1,5 +1,5 @@
[post-redirect-stacktrace.https.html] [post-redirect-stacktrace.https.html]
expected: ERROR expected: TIMEOUT
[StackTrace do not leak cross-origin post-redirect URL] [StackTrace do not leak cross-origin post-redirect URL]
expected: FAIL expected: FAIL

View file

@ -1,4 +0,0 @@
[revalidate-not-blocked-by-csp.html]
[Request revalidation aren't blocked by CSP]
expected: FAIL

View file

@ -1,9 +1,10 @@
[reporting-subresource-corp.tentative.https.html] [reporting-subresource-corp.tentative.https.html]
expected: TIMEOUT
[[document\] blocked due to DIP] [[document\] blocked due to DIP]
expected: FAIL expected: TIMEOUT
[[document\] blocked during redirect] [[document\] blocked during redirect]
expected: FAIL expected: NOTRUN
[destination: script] [destination: script]
expected: FAIL expected: FAIL
@ -12,22 +13,22 @@
expected: FAIL expected: FAIL
[[dedicated worker\] blocked due to DIP] [[dedicated worker\] blocked due to DIP]
expected: FAIL expected: NOTRUN
[[dedicated worker\] blocked during redirect] [[dedicated worker\] blocked during redirect]
expected: FAIL expected: NOTRUN
[[shared worker\] same-origin] [[shared worker\] same-origin]
expected: FAIL expected: NOTRUN
[[shared worker\] blocked by CORP: same-origin] [[shared worker\] blocked by CORP: same-origin]
expected: FAIL expected: FAIL
[[shared worker\] blocked due to DIP] [[shared worker\] blocked due to DIP]
expected: FAIL expected: NOTRUN
[[shared worker\] blocked during redirect] [[shared worker\] blocked during redirect]
expected: FAIL expected: NOTRUN
[[between service worker and page\] same-origin] [[between service worker and page\] same-origin]
expected: FAIL expected: FAIL
@ -42,10 +43,13 @@
expected: FAIL expected: FAIL
[[document with service worker\] same-origin] [[document with service worker\] same-origin]
expected: FAIL expected: NOTRUN
[[document with service worker\] blocked due to DIP] [[document with service worker\] blocked due to DIP]
expected: FAIL expected: NOTRUN
[[document with service worker\] blocked during redirect] [[document with service worker\] blocked during redirect]
expected: FAIL expected: NOTRUN
[[dedicated worker\] same-origin]
expected: NOTRUN

View file

@ -1,6 +1,7 @@
[document-reporting-bypass-report-to.https.sub.html] [document-reporting-bypass-report-to.https.sub.html]
expected: TIMEOUT
[document policy violation observed] [document policy violation observed]
expected: FAIL expected: TIMEOUT
[Only the Reporting-Endpoints configured endpoint received reports.] [Only the Reporting-Endpoints configured endpoint received reports.]
expected: FAIL expected: NOTRUN

View file

@ -1,9 +1,7 @@
[document-reporting-named-endpoints.https.sub.html] [document-reporting-named-endpoints.https.sub.html]
[csp violation report observed] expected: TIMEOUT
expected: FAIL
[document policy violation observed] [document policy violation observed]
expected: FAIL expected: TIMEOUT
[Reporting endpoints received reports.] [Reporting endpoints received reports.]
expected: FAIL expected: NOTRUN

View file

@ -1,6 +1,7 @@
[document-reporting-override-endpoint.https.sub.html] [document-reporting-override-endpoint.https.sub.html]
expected: TIMEOUT
[document policy violation observed] [document policy violation observed]
expected: FAIL expected: TIMEOUT
[Only the second reporting endpoint received reports.] [Only the second reporting endpoint received reports.]
expected: FAIL expected: NOTRUN

View file

@ -50,33 +50,6 @@
[Report interface: attribute body] [Report interface: attribute body]
expected: FAIL expected: FAIL
[ReportingObserver interface: existence and properties of interface object]
expected: FAIL
[ReportingObserver interface object length]
expected: FAIL
[ReportingObserver interface object name]
expected: FAIL
[ReportingObserver interface: existence and properties of interface prototype object]
expected: FAIL
[ReportingObserver interface: existence and properties of interface prototype object's "constructor" property]
expected: FAIL
[ReportingObserver interface: existence and properties of interface prototype object's @@unscopables property]
expected: FAIL
[ReportingObserver interface: operation observe()]
expected: FAIL
[ReportingObserver interface: operation disconnect()]
expected: FAIL
[ReportingObserver interface: operation takeRecords()]
expected: FAIL
[idlharness.any.worker.html] [idlharness.any.worker.html]
[ReportBody interface: existence and properties of interface object] [ReportBody interface: existence and properties of interface object]
@ -129,30 +102,3 @@
[Report interface: attribute body] [Report interface: attribute body]
expected: FAIL expected: FAIL
[ReportingObserver interface: existence and properties of interface object]
expected: FAIL
[ReportingObserver interface object length]
expected: FAIL
[ReportingObserver interface object name]
expected: FAIL
[ReportingObserver interface: existence and properties of interface prototype object]
expected: FAIL
[ReportingObserver interface: existence and properties of interface prototype object's "constructor" property]
expected: FAIL
[ReportingObserver interface: existence and properties of interface prototype object's @@unscopables property]
expected: FAIL
[ReportingObserver interface: operation observe()]
expected: FAIL
[ReportingObserver interface: operation disconnect()]
expected: FAIL
[ReportingObserver interface: operation takeRecords()]
expected: FAIL

View file

@ -13640,14 +13640,14 @@
] ]
], ],
"interfaces.https.html": [ "interfaces.https.html": [
"eee8c799727b91e00b512795756b693a5f121f86", "8a9345525360e7a7ce69e84e394b65a4cbc0ab34",
[ [
null, null,
{} {}
] ]
], ],
"interfaces.worker.js": [ "interfaces.worker.js": [
"e86f34f261442aeaa7074c525fb4b1206219769d", "f217fd8fb6b46144bc3576a081cc6ce5db3129d5",
[ [
"mozilla/interfaces.worker.html", "mozilla/interfaces.worker.html",
{} {}

View file

@ -293,6 +293,7 @@ test_interfaces([
"ReadableStreamBYOBReader", "ReadableStreamBYOBReader",
"ReadableByteStreamController", "ReadableByteStreamController",
"ReadableStreamBYOBRequest", "ReadableStreamBYOBRequest",
"ReportingObserver",
"Request", "Request",
"ResizeObserver", "ResizeObserver",
"ResizeObserverEntry", "ResizeObserverEntry",

View file

@ -103,6 +103,7 @@ test_interfaces([
"ReadableStreamBYOBReader", "ReadableStreamBYOBReader",
"ReadableByteStreamController", "ReadableByteStreamController",
"ReadableStreamBYOBRequest", "ReadableStreamBYOBRequest",
"ReportingObserver",
"Request", "Request",
"Response", "Response",
"SecurityPolicyViolationEvent", "SecurityPolicyViolationEvent",