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

@ -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())
}
}