Skip to main content

servo/
site_data_manager.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, RefCell};
6
7use bitflags::bitflags;
8use cookie::Cookie;
9use log::warn;
10use net_traits::pub_domains::registered_domain_name;
11use net_traits::{CookieOperationId, ResourceThreads, SiteDescriptor};
12use rustc_hash::FxHashMap;
13use servo_url::ServoUrl;
14use storage_traits::StorageThreads;
15use storage_traits::webstorage_thread::{OriginDescriptor, WebStorageType};
16use url::Url;
17
18use crate::CookieSource;
19
20bitflags! {
21    /// Identifies categories of site data associated with a site.
22    ///
23    /// This type is used by `SiteDataManager` to query, describe, and manage
24    /// different kinds of data stored by the user agent for a given site.
25    ///
26    /// Additional storage categories (e.g. IndexedDB) may be added in the
27    /// future.
28    #[derive(Clone, Copy, Debug, PartialEq)]
29    pub struct StorageType: u8 {
30        /// Corresponds to the HTTP cookies:
31        /// <https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies>
32        const Cookies = 1 << 0;
33
34        /// Corresponds to the `localStorage` Web API:
35        /// <https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage>
36        const Local   = 1 << 1;
37
38        /// Corresponds to the `sessionStorage` Web API:
39        /// <https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage>
40        const Session = 1 << 2;
41    }
42}
43
44#[derive(Clone, Debug, PartialEq)]
45pub struct SiteData {
46    name: String,
47    storage_types: StorageType,
48}
49
50impl SiteData {
51    pub fn new(name: impl Into<String>, storage_types: StorageType) -> SiteData {
52        SiteData {
53            name: name.into(),
54            storage_types,
55        }
56    }
57
58    pub fn name(&self) -> String {
59        self.name.clone()
60    }
61
62    pub fn storage_types(&self) -> StorageType {
63        self.storage_types
64    }
65}
66
67/// The response data for a pending embedder cookie operation.
68pub(crate) enum CookieOperationResponse {
69    /// Cookies returned from a get operation.
70    Cookies(Vec<Cookie<'static>>),
71    /// Acknowledgement that a operation is completed.
72    Done,
73}
74
75/// A callback for a pending embedder cookie operation,
76/// paired with the [`CookieOperationResponse`].
77enum CookieOperationCallback {
78    Cookies(Box<dyn FnOnce(Vec<Cookie<'static>>)>),
79    Done(Box<dyn FnOnce()>),
80    DoneAfterResponses {
81        remaining_responses: u8,
82        callback: Box<dyn FnOnce()>,
83    },
84}
85
86/// Provides APIs for inspecting and managing site data.
87///
88/// `SiteDataManager` exposes information about data that is conceptually
89/// associated with a site (equivalent to an eTLD+1), such as web exposed
90/// storage mechanisms like `localStorage` and `sessionStorage`.
91///
92/// The manager can be used by embedders to list sites with stored data.
93/// Support for site scoped management operations (e.g. clearing data for a
94/// specific site) will be added in the future.
95///
96/// Note: Network layer state (such as the HTTP cache) is intentionally not
97/// handled by `SiteDataManager`. That functionality lives in `NetworkManager`.
98pub struct SiteDataManager {
99    public_resource_threads: ResourceThreads,
100    private_resource_threads: ResourceThreads,
101    public_storage_threads: StorageThreads,
102    private_storage_threads: StorageThreads,
103    next_cookie_op_id: Cell<u64>,
104    pending_cookie_callbacks: RefCell<FxHashMap<CookieOperationId, CookieOperationCallback>>,
105}
106
107impl SiteDataManager {
108    pub(crate) fn new(
109        public_resource_threads: ResourceThreads,
110        private_resource_threads: ResourceThreads,
111        public_storage_threads: StorageThreads,
112        private_storage_threads: StorageThreads,
113    ) -> Self {
114        Self {
115            public_resource_threads,
116            private_resource_threads,
117            public_storage_threads,
118            private_storage_threads,
119            next_cookie_op_id: Cell::new(0),
120            pending_cookie_callbacks: RefCell::new(FxHashMap::default()),
121        }
122    }
123
124    /// Return a list of sites that have associated site data.
125    ///
126    /// The returned list is filtered by the provided `storage_types` bitflags.
127    /// Each [`SiteData`] entry represents a site (equivalent to an eTLD+1)
128    /// and indicates which kinds of storage data are present for it (e.g.
129    /// localStorage, sessionStorage).
130    ///
131    /// The returned list is sorted by site name.
132    ///
133    /// Both public and private storage are included in the result.
134    pub fn site_data(&self, storage_types: StorageType) -> Vec<SiteData> {
135        let mut all_sites: FxHashMap<String, StorageType> = FxHashMap::default();
136
137        let mut add_sites = |sites: Vec<SiteDescriptor>, storage_type: StorageType| {
138            for site in sites {
139                all_sites
140                    .entry(site.name)
141                    .and_modify(|storage_types| *storage_types |= storage_type)
142                    .or_insert(storage_type);
143            }
144        };
145
146        if storage_types.contains(StorageType::Cookies) {
147            let public_cookies = self.public_resource_threads.cookies();
148            add_sites(public_cookies, StorageType::Cookies);
149
150            let private_cookies = self.private_resource_threads.cookies();
151            add_sites(private_cookies, StorageType::Cookies);
152        }
153
154        let mut add_origins = |origins: Vec<OriginDescriptor>, storage_type: StorageType| {
155            for origin in origins {
156                let url =
157                    ServoUrl::parse(&origin.name).expect("Should always be able to parse origins.");
158
159                let Some(domain) = registered_domain_name(&url) else {
160                    warn!("Failed to get a registered domain name for: {url}.");
161                    continue;
162                };
163                let domain = domain.to_string();
164
165                all_sites
166                    .entry(domain)
167                    .and_modify(|storage_types| *storage_types |= storage_type)
168                    .or_insert(storage_type);
169            }
170        };
171
172        if storage_types.contains(StorageType::Local) {
173            let public_origins = self
174                .public_storage_threads
175                .webstorage_origins(WebStorageType::Local);
176            add_origins(public_origins, StorageType::Local);
177
178            let private_origins = self
179                .private_storage_threads
180                .webstorage_origins(WebStorageType::Local);
181            add_origins(private_origins, StorageType::Local);
182        }
183
184        if storage_types.contains(StorageType::Session) {
185            let public_origins = self
186                .public_storage_threads
187                .webstorage_origins(WebStorageType::Session);
188            add_origins(public_origins, StorageType::Session);
189
190            let private_origins = self
191                .private_storage_threads
192                .webstorage_origins(WebStorageType::Session);
193            add_origins(private_origins, StorageType::Session);
194        }
195
196        let mut result: Vec<SiteData> = all_sites
197            .into_iter()
198            .map(|(name, storage_types)| SiteData::new(name, storage_types))
199            .collect();
200
201        result.sort_by_key(SiteData::name);
202
203        result
204    }
205
206    /// Clear site data for the given sites.
207    ///
208    /// The clearing is restricted to the provided `storage_types` bitflags.
209    /// Both public and private browsing data are affected.
210    pub fn clear_site_data(&self, sites: &[&str], storage_types: StorageType) {
211        if storage_types.contains(StorageType::Cookies) {
212            self.public_resource_threads.clear_cookies_for_sites(sites);
213            self.private_resource_threads.clear_cookies_for_sites(sites);
214        }
215
216        if storage_types.contains(StorageType::Local) {
217            self.public_storage_threads
218                .clear_webstorage_for_sites(WebStorageType::Local, sites);
219            self.private_storage_threads
220                .clear_webstorage_for_sites(WebStorageType::Local, sites);
221        }
222
223        if storage_types.contains(StorageType::Session) {
224            self.public_storage_threads
225                .clear_webstorage_for_sites(WebStorageType::Session, sites);
226            self.private_storage_threads
227                .clear_webstorage_for_sites(WebStorageType::Session, sites);
228        }
229    }
230
231    /// Clears all cookies from both the public and private cookie jars.
232    ///
233    /// An optional callback is provided for async operation.
234    pub fn clear_cookies(&self, callback: Option<Box<dyn FnOnce()>>) {
235        match callback {
236            None => {
237                self.public_resource_threads.clear_cookies();
238                self.private_resource_threads.clear_cookies();
239            },
240            Some(callback) => {
241                let id = self.next_operation_id();
242                self.pending_cookie_callbacks.borrow_mut().insert(
243                    id,
244                    CookieOperationCallback::DoneAfterResponses {
245                        remaining_responses: 2,
246                        callback,
247                    },
248                );
249                self.public_resource_threads.clear_cookies_async(id);
250                self.private_resource_threads.clear_cookies_async(id);
251            },
252        }
253    }
254
255    /// Delete all session cookies (cookies that have no expiry or max-age).
256    ///
257    /// Session cookies from both the public and private browsing session cookies are removed.
258    /// An optional callback is provided for async operation.
259    pub fn clear_session_cookies(&self, callback: Option<Box<dyn FnOnce()>>) {
260        match callback {
261            None => {
262                self.public_resource_threads.clear_session_cookies();
263                self.private_resource_threads.clear_session_cookies();
264            },
265            Some(callback) => {
266                let id = self.next_operation_id();
267                self.pending_cookie_callbacks.borrow_mut().insert(
268                    id,
269                    CookieOperationCallback::DoneAfterResponses {
270                        remaining_responses: 2,
271                        callback,
272                    },
273                );
274                self.public_resource_threads.clear_session_cookies_async(id);
275                self.private_resource_threads
276                    .clear_session_cookies_async(id);
277            },
278        }
279    }
280
281    /// Returns the cookies for the domain associated with the given [`Url`].
282    pub fn cookies_for_url(&self, url: Url, source: CookieSource) -> Vec<Cookie<'static>> {
283        self.public_resource_threads
284            .cookies_for_url(url.into(), source)
285    }
286
287    /// Asynchronously returns the cookies for the domain associated with the given [`Url`].
288    pub fn cookies_for_url_async(
289        &self,
290        url: Url,
291        source: CookieSource,
292        callback: impl FnOnce(Vec<Cookie<'static>>) + 'static,
293    ) {
294        let id = self.next_operation_id();
295        self.pending_cookie_callbacks
296            .borrow_mut()
297            .insert(id, CookieOperationCallback::Cookies(Box::new(callback)));
298        self.public_resource_threads
299            .cookies_for_url_async(id, url.into(), source);
300    }
301
302    /// Sets a cookie for the domain associated with the given [`Url`].
303    ///
304    /// An optional callback is provided for async operation.
305    pub fn set_cookie_for_url(
306        &self,
307        url: Url,
308        cookie: Cookie<'static>,
309        callback: Option<Box<dyn FnOnce()>>,
310    ) {
311        match callback {
312            None => {
313                self.public_resource_threads.set_cookie_for_url_sync(
314                    url.into(),
315                    cookie,
316                    CookieSource::HTTP,
317                );
318            },
319            Some(callback) => {
320                let id = self.next_operation_id();
321                self.pending_cookie_callbacks
322                    .borrow_mut()
323                    .insert(id, CookieOperationCallback::Done(callback));
324                self.public_resource_threads.set_cookie_for_url_async(
325                    id,
326                    url.into(),
327                    cookie,
328                    CookieSource::HTTP,
329                );
330            },
331        }
332    }
333
334    /// Handle a cookie operation response from the resource thread.
335    ///
336    /// This is called by the event loop when an embedder cookie response is received.
337    pub(crate) fn handle_cookie_response(
338        &self,
339        id: CookieOperationId,
340        response: CookieOperationResponse,
341    ) {
342        let Some(callback) = self.pending_cookie_callbacks.borrow_mut().remove(&id) else {
343            warn!("Received cookie response for unknown operation {id:?}");
344            return;
345        };
346        match (response, callback) {
347            (CookieOperationResponse::Cookies(cookies), CookieOperationCallback::Cookies(cb)) => {
348                cb(cookies);
349            },
350            (CookieOperationResponse::Done, CookieOperationCallback::Done(cb)) => {
351                cb();
352            },
353            (
354                CookieOperationResponse::Done,
355                CookieOperationCallback::DoneAfterResponses {
356                    remaining_responses,
357                    callback,
358                },
359            ) => {
360                if remaining_responses > 1 {
361                    self.pending_cookie_callbacks.borrow_mut().insert(
362                        id,
363                        CookieOperationCallback::DoneAfterResponses {
364                            remaining_responses: remaining_responses - 1,
365                            callback,
366                        },
367                    );
368                } else {
369                    callback();
370                }
371            },
372            _ => {
373                warn!("Cookie response type mismatch for operation {id:?}");
374            },
375        }
376    }
377
378    fn next_operation_id(&self) -> CookieOperationId {
379        let id = CookieOperationId(self.next_cookie_op_id.get());
380        self.next_cookie_op_id.set(id.0 + 1);
381        id
382    }
383}