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