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