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