Skip to main content

script/dom/reporting/
reportingobserver.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::time::{SystemTime, UNIX_EPOCH};
7
8use dom_struct::dom_struct;
9use js::context::JSContext;
10use js::rust::HandleObject;
11use script_bindings::callback::{OwnerWindow, RootedCallback, TracedCallback};
12use script_bindings::cell::DomRefCell;
13use script_bindings::match_domstring_ascii;
14use script_bindings::reflector::{Reflector, reflect_dom_object_with_proto};
15use script_bindings::str::DOMString;
16use servo_url::ServoUrl;
17
18use crate::dom::bindings::callback::ExceptionHandling;
19use crate::dom::bindings::codegen::Bindings::CSPViolationReportBodyBinding::CSPViolationReportBody;
20use crate::dom::bindings::codegen::Bindings::ReportingObserverBinding::{
21    Report, ReportList, ReportingObserverCallback, ReportingObserverMethods,
22    ReportingObserverOptions,
23};
24use crate::dom::bindings::inheritance::Castable;
25use crate::dom::bindings::num::Finite;
26use crate::dom::bindings::refcounted::Trusted;
27use crate::dom::bindings::reflector::DomGlobal;
28use crate::dom::bindings::root::DomRoot;
29use crate::dom::globalscope::GlobalScope;
30use crate::dom::window::Window;
31use crate::dom::workerglobalscope::WorkerGlobalScope;
32
33#[dom_struct]
34pub(crate) struct ReportingObserver {
35    reflector_: Reflector,
36
37    callback: TracedCallback<ReportingObserverCallback>,
38    buffered: Cell<bool>,
39    types: DomRefCell<Vec<DOMString>>,
40    report_queue: DomRefCell<Vec<Report>>,
41}
42
43impl ReportingObserver {
44    fn new_inherited(
45        callback: RootedCallback<ReportingObserverCallback>,
46        options: &ReportingObserverOptions,
47    ) -> Self {
48        Self {
49            reflector_: Reflector::new(),
50            callback: callback.to_traced(),
51            buffered: Cell::new(options.buffered),
52            types: DomRefCell::new(options.types.clone().unwrap_or_default()),
53            report_queue: Default::default(),
54        }
55    }
56
57    fn new_with_proto(
58        cx: &mut JSContext,
59        callback: RootedCallback<ReportingObserverCallback>,
60        options: &ReportingObserverOptions,
61        global: &GlobalScope,
62        proto: Option<HandleObject>,
63    ) -> DomRoot<Self> {
64        reflect_dom_object_with_proto(
65            cx,
66            Box::new(Self::new_inherited(callback, options)),
67            global,
68            proto,
69        )
70    }
71
72    fn report_is_visible_to_reporting_observers(report: &Report) -> bool {
73        match_domstring_ascii!(report.type_,
74                // https://w3c.github.io/webappsec-csp/#reporting
75                "csp-violation" => true,
76                _ =>  false,
77        )
78    }
79
80    /// <https://w3c.github.io/reporting/#add-report>
81    fn add_report_to_observer(&self, report: &Report) {
82        // Step 1. If report’s type is not visible to ReportingObservers, return.
83        if !Self::report_is_visible_to_reporting_observers(report) {
84            return;
85        }
86        // Step 2. If observer’s options has a non-empty types member which does not contain report’s type, return.
87        let types = self.types.borrow();
88        if !types.is_empty() && !types.contains(&report.type_) {
89            return;
90        }
91        // Step 3. Create a new Report r with type initialized to report’s type,
92        // url initialized to report’s url, and body initialized to report’s body.
93        let report = Report {
94            type_: report.type_.clone(),
95            url: report.url.clone(),
96            body: report.body.clone(),
97            destination: report.destination.clone(),
98            attempts: report.attempts,
99            timestamp: report.timestamp,
100        };
101        // Step 4. Append r to observer’s report queue.
102        self.report_queue.borrow_mut().push(report);
103        // Step 5. If the size of observer’s report queue is 1:
104        if self.report_queue.borrow().len() == 1 {
105            // Step 5.1. Let global be observer’s relevant global object.
106            let global = self.global();
107            // Step 5.2. Queue a task to § 4.4 Invoke reporting observers with notify list
108            // with a copy of global’s registered reporting observer list.
109            let observers_global = Trusted::new(&*global);
110            global.task_manager().dom_manipulation_task_source().queue(
111                task!(notify_reporting_observers: move |cx| {
112                    Self::invoke_reporting_observers_with_notify_list(
113                        cx,
114                        observers_global.root().registered_reporting_observers()
115                    );
116                }),
117            );
118        }
119    }
120
121    /// <https://w3c.github.io/reporting/#notify-observers>
122    pub(crate) fn notify_reporting_observers_on_scope(global: &GlobalScope, report: &Report) {
123        // Step 1. For each ReportingObserver observer registered with scope,
124        // execute § 4.3 Add report to observer on report and observer.
125        for observer in global.registered_reporting_observers().iter() {
126            observer.add_report_to_observer(report);
127        }
128        // Step 2. Append report to scope’s report buffer.
129        global.append_report(report.clone());
130        // Step 3. Let type be report’s type.
131        // TODO(37328)
132        // Step 4. If scope’s report buffer now contains more than 100 reports with
133        // type equal to type, remove the earliest item with type equal to type in the report buffer.
134        // TODO(37328)
135    }
136
137    /// <https://w3c.github.io/reporting/#invoke-observers>
138    fn invoke_reporting_observers_with_notify_list(
139        cx: &mut JSContext,
140        notify_list: Vec<DomRoot<ReportingObserver>>,
141    ) {
142        // Step 1. For each ReportingObserver observer in notify list:
143        for observer in notify_list.iter() {
144            // Step 1.1. If observer’s report queue is empty, then continue.
145            if observer.report_queue.borrow().is_empty() {
146                continue;
147            }
148            // Step 1.2. Let reports be a copy of observer’s report queue
149            // Step 1.3. Empty observer’s report queue
150            let reports = std::mem::take(&mut *observer.report_queue.borrow_mut());
151            // Step 1.4. Invoke observer’s callback with « reports, observer » and "report",
152            // and with observer as the callback this value.
153            let _ = observer.callback.Call_(
154                cx,
155                &**observer,
156                reports,
157                observer,
158                ExceptionHandling::Report,
159            );
160        }
161    }
162
163    /// <https://w3c.github.io/reporting/#generate-a-report>
164    fn generate_a_report(
165        global: &GlobalScope,
166        type_: DOMString,
167        url: Option<ServoUrl>,
168        body: Option<CSPViolationReportBody>,
169        destination: DOMString,
170    ) -> Report {
171        // Step 2. If url was not provided by the caller, let url be settings’s creation URL.
172        let url = url.unwrap_or(global.creation_url());
173        // Step 3. Set url’s username to the empty string, and its password to null.
174        // Step 4. Set report’s url to the result of executing the URL serializer
175        // on url with the exclude fragment flag set.
176        let url = Self::strip_url_for_reports(url).into();
177        // Step 1. Let report be a new report object with its values initialized as follows:
178        // Step 5. Return report.
179        Report {
180            type_,
181            url,
182            body,
183            destination,
184            timestamp: Finite::wrap(
185                SystemTime::now()
186                    .duration_since(UNIX_EPOCH)
187                    .unwrap_or_default()
188                    .as_millis() as f64,
189            ),
190            attempts: 0,
191        }
192    }
193
194    /// <https://w3c.github.io/reporting/#generate-and-queue-a-report>
195    pub(crate) fn generate_and_queue_a_report(
196        global: &GlobalScope,
197        type_: DOMString,
198        body: Option<CSPViolationReportBody>,
199        destination: DOMString,
200    ) {
201        // Step 1. Let settings be context’s relevant settings object.
202        // Step 2. Let report be the result of running generate a report with data, type, destination and settings.
203        let report = Self::generate_a_report(global, type_, None, body, destination);
204        // Step 3. If settings is given, then
205        // Step 3.1. Let scope be settings’s global object.
206        // Step 3.2. If scope is an object implementing WindowOrWorkerGlobalScope, then
207        // execute § 4.2 Notify reporting observers on scope with report with scope and report.
208        Self::notify_reporting_observers_on_scope(global, &report);
209        // Step 4. Append report to context’s reports.
210        global.append_report(report);
211    }
212
213    /// <https://w3c.github.io/webappsec-csp/#strip-url-for-use-in-reports>
214    pub(crate) fn strip_url_for_reports(mut url: ServoUrl) -> String {
215        let scheme = url.scheme();
216        // Step 1: If url’s scheme is not an HTTP(S) scheme, then return url’s scheme.
217        //
218        // Note we also treat ws(s) schemes as HTTP(S) per
219        // https://github.com/w3c/webappsec-csp/issues/735
220        if scheme != "https" && scheme != "http" && scheme != "ws" && scheme != "wss" {
221            return scheme.to_owned();
222        }
223        // Step 2: Set url’s fragment to the empty string.
224        url.set_fragment(None);
225        // Step 3: Set url’s username to the empty string.
226        let _ = url.set_username("");
227        // Step 4: Set url’s password to the empty string.
228        let _ = url.set_password(None);
229        // Step 5: Return the result of executing the URL serializer on url.
230        url.into_string()
231    }
232}
233
234impl ReportingObserverMethods<crate::DomTypeHolder> for ReportingObserver {
235    /// <https://w3c.github.io/reporting/#dom-reportingobserver-reportingobserver>
236    fn Constructor(
237        cx: &mut JSContext,
238        global: &GlobalScope,
239        proto: Option<HandleObject>,
240        callback: RootedCallback<ReportingObserverCallback>,
241        options: &ReportingObserverOptions,
242    ) -> DomRoot<ReportingObserver> {
243        // Step 1. Create a new ReportingObserver object observer.
244        // Step 2. Set observer’s callback to callback.
245        // Step 3. Set observer’s options to options.
246        // Step 4. Return observer.
247        ReportingObserver::new_with_proto(cx, callback, options, global, proto)
248    }
249
250    /// <https://w3c.github.io/reporting/#dom-reportingobserver-observe>
251    fn Observe(&self) {
252        // Step 1. Let global be the be the relevant global object of this.
253        let global = &self.global();
254        // Step 2. Append this to the global’s registered reporting observer list.
255        global.append_reporting_observer(self);
256        // Step 3. If this’s buffered option is false, return.
257        if !self.buffered.get() {
258            return;
259        }
260        // Step 4. Set this’s buffered option to false.
261        self.buffered.set(false);
262        // Step 5.For each report in global’s report buffer, queue a task to
263        // execute § 4.3 Add report to observer with report and this.
264        for report in global.buffered_reports() {
265            // TODO(37328): Figure out how to put this in a task
266            self.add_report_to_observer(&report);
267        }
268    }
269
270    /// <https://w3c.github.io/reporting/#dom-reportingobserver-disconnect>
271    fn Disconnect(&self) {
272        // Step 1. If this is not registered, return.
273        // Skipped, as this is handled in `remove_reporting_observer`
274
275        // Step 2. Let global be the relevant global object of this.
276        let global = &self.global();
277        // Step 3. Remove this from global’s registered reporting observer list.
278        global.remove_reporting_observer(self);
279    }
280
281    /// <https://w3c.github.io/reporting/#dom-reportingobserver-takerecords>
282    fn TakeRecords(&self) -> ReportList {
283        // Step 1. Let reports be a copy of this’s report queue.
284        // Step 2. Empty this’s report queue.
285        // Step 3. Return reports.
286        std::mem::take(&mut *self.report_queue.borrow_mut())
287    }
288}
289
290impl GlobalScope {
291    fn append_reporting_observer(&self, reporting_observer: &ReportingObserver) {
292        if let Some(window) = self.downcast::<Window>() {
293            return window.append_reporting_observer(reporting_observer);
294        }
295        if let Some(worker) = self.downcast::<WorkerGlobalScope>() {
296            return worker.append_reporting_observer(reporting_observer);
297        }
298        unreachable!();
299    }
300
301    fn remove_reporting_observer(&self, reporting_observer: &ReportingObserver) {
302        if let Some(window) = self.downcast::<Window>() {
303            return window.remove_reporting_observer(reporting_observer);
304        }
305        if let Some(worker) = self.downcast::<WorkerGlobalScope>() {
306            return worker.remove_reporting_observer(reporting_observer);
307        }
308        unreachable!();
309    }
310
311    fn registered_reporting_observers(&self) -> Vec<DomRoot<ReportingObserver>> {
312        if let Some(window) = self.downcast::<Window>() {
313            return window.registered_reporting_observers();
314        }
315        if let Some(worker) = self.downcast::<WorkerGlobalScope>() {
316            return worker.registered_reporting_observers();
317        }
318        unreachable!();
319    }
320
321    fn append_report(&self, report: Report) {
322        if let Some(window) = self.downcast::<Window>() {
323            return window.append_report(report);
324        }
325        if let Some(worker) = self.downcast::<WorkerGlobalScope>() {
326            return worker.append_report(report);
327        }
328        unreachable!();
329    }
330
331    fn buffered_reports(&self) -> Vec<Report> {
332        if let Some(window) = self.downcast::<Window>() {
333            return window.buffered_reports();
334        }
335        if let Some(worker) = self.downcast::<WorkerGlobalScope>() {
336            return worker.buffered_reports();
337        }
338        unreachable!();
339    }
340}
341
342impl OwnerWindow<crate::DomTypeHolder> for ReportingObserver {}