Skip to main content

script/dom/
permissions.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::rc::Rc;
6
7use dom_struct::dom_struct;
8use embedder_traits::{self, AllowOrDeny, EmbedderMsg, PermissionFeature};
9use js::conversions::ConversionResult;
10use js::jsapi::JSObject;
11use js::jsval::{ObjectValue, UndefinedValue};
12use script_bindings::inheritance::Castable;
13use script_bindings::reflector::{Reflector, reflect_dom_object};
14use servo_base::generic_channel;
15use servo_config::pref;
16
17use super::window::Window;
18use crate::conversions::Convert;
19use crate::dom::bindings::codegen::Bindings::PermissionStatusBinding::{
20    PermissionDescriptor, PermissionName, PermissionState, PermissionStatusMethods,
21};
22use crate::dom::bindings::codegen::Bindings::PermissionsBinding::PermissionsMethods;
23use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
24use crate::dom::bindings::error::Error;
25use crate::dom::bindings::reflector::DomGlobal;
26use crate::dom::bindings::root::DomRoot;
27#[cfg(feature = "bluetooth")]
28use crate::dom::bluetooth::Bluetooth;
29#[cfg(feature = "bluetooth")]
30use crate::dom::bluetoothpermissionresult::BluetoothPermissionResult;
31use crate::dom::globalscope::GlobalScope;
32use crate::dom::permissionstatus::PermissionStatus;
33use crate::dom::promise::Promise;
34use crate::realms::{AlreadyInRealm, InRealm};
35use crate::script_runtime::CanGc;
36
37pub(crate) trait PermissionAlgorithm {
38    type Descriptor;
39    #[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
40    type Status;
41    fn create_descriptor(
42        cx: &mut js::context::JSContext,
43        permission_descriptor_obj: *mut JSObject,
44    ) -> Result<Self::Descriptor, Error>;
45    fn permission_query(
46        cx: &mut js::context::JSContext,
47        promise: &Rc<Promise>,
48        descriptor: &Self::Descriptor,
49        status: &Self::Status,
50    );
51    fn permission_request(
52        cx: &mut js::context::JSContext,
53        promise: &Rc<Promise>,
54        descriptor: &Self::Descriptor,
55        status: &Self::Status,
56    );
57    fn permission_revoke(
58        cx: &mut js::context::JSContext,
59        descriptor: &Self::Descriptor,
60        status: &Self::Status,
61    );
62}
63
64enum Operation {
65    Query,
66    Request,
67    Revoke,
68}
69
70// https://w3c.github.io/permissions/#permissions
71#[dom_struct]
72pub(crate) struct Permissions {
73    reflector_: Reflector,
74}
75
76impl Permissions {
77    pub(crate) fn new_inherited() -> Permissions {
78        Permissions {
79            reflector_: Reflector::new(),
80        }
81    }
82
83    pub(crate) fn new(global: &GlobalScope, can_gc: CanGc) -> DomRoot<Permissions> {
84        reflect_dom_object(Box::new(Permissions::new_inherited()), global, can_gc)
85    }
86
87    // https://w3c.github.io/permissions/#dom-permissions-query
88    // https://w3c.github.io/permissions/#dom-permissions-request
89    // https://w3c.github.io/permissions/#dom-permissions-revoke
90    #[expect(non_snake_case)]
91    fn manipulate(
92        &self,
93        cx: &mut js::context::JSContext,
94        op: Operation,
95        permissionDesc: *mut JSObject,
96        promise: Option<Rc<Promise>>,
97    ) -> Rc<Promise> {
98        // (Query, Request) Step 3.
99        let p = match promise {
100            Some(promise) => promise,
101            None => {
102                let in_realm_proof = AlreadyInRealm::assert::<crate::DomTypeHolder>();
103                Promise::new_in_current_realm(InRealm::Already(&in_realm_proof), CanGc::from_cx(cx))
104            },
105        };
106
107        // (Query, Request, Revoke) Step 1.
108        let root_desc = match Permissions::create_descriptor(cx, permissionDesc) {
109            Ok(descriptor) => descriptor,
110            Err(error) => {
111                p.reject_error(error, CanGc::from_cx(cx));
112                return p;
113            },
114        };
115
116        // (Query, Request) Step 5.
117        let status = PermissionStatus::new(&self.global(), &root_desc, CanGc::from_cx(cx));
118
119        // (Query, Request, Revoke) Step 2.
120        match root_desc.name {
121            #[cfg(feature = "bluetooth")]
122            PermissionName::Bluetooth => {
123                let bluetooth_desc = match Bluetooth::create_descriptor(cx, permissionDesc) {
124                    Ok(descriptor) => descriptor,
125                    Err(error) => {
126                        p.reject_error(error, CanGc::from_cx(cx));
127                        return p;
128                    },
129                };
130
131                // (Query, Request) Step 5.
132                let result = BluetoothPermissionResult::new(cx, &self.global(), &status);
133
134                match op {
135                    // (Request) Step 6 - 8.
136                    Operation::Request => {
137                        Bluetooth::permission_request(cx, &p, &bluetooth_desc, &result)
138                    },
139
140                    // (Query) Step 6 - 7.
141                    Operation::Query => {
142                        Bluetooth::permission_query(cx, &p, &bluetooth_desc, &result)
143                    },
144
145                    Operation::Revoke => {
146                        // (Revoke) Step 3.
147                        let globalscope = self.global();
148                        globalscope
149                            .permission_state_invocation_results()
150                            .borrow_mut()
151                            .remove(&root_desc.name);
152
153                        // (Revoke) Step 4.
154                        Bluetooth::permission_revoke(cx, &bluetooth_desc, &result)
155                    },
156                }
157            },
158            _ => {
159                match op {
160                    Operation::Request => {
161                        // (Request) Step 6.
162                        Permissions::permission_request(cx, &p, &root_desc, &status);
163
164                        // (Request) Step 7. The default algorithm always resolve
165
166                        // (Request) Step 8.
167                        p.resolve_native(&status, CanGc::from_cx(cx));
168                    },
169                    Operation::Query => {
170                        // (Query) Step 6.
171                        Permissions::permission_query(cx, &p, &root_desc, &status);
172
173                        // (Query) Step 7.
174                        p.resolve_native(&status, CanGc::from_cx(cx));
175                    },
176
177                    Operation::Revoke => {
178                        // (Revoke) Step 3.
179                        let globalscope = self.global();
180                        globalscope
181                            .permission_state_invocation_results()
182                            .borrow_mut()
183                            .remove(&root_desc.name);
184
185                        // (Revoke) Step 4.
186                        Permissions::permission_revoke(cx, &root_desc, &status);
187                    },
188                }
189            },
190        };
191        match op {
192            // (Revoke) Step 5.
193            Operation::Revoke => self.manipulate(cx, Operation::Query, permissionDesc, Some(p)),
194
195            // (Query, Request) Step 4.
196            _ => p,
197        }
198    }
199}
200
201#[expect(non_snake_case)]
202impl PermissionsMethods<crate::DomTypeHolder> for Permissions {
203    /// <https://w3c.github.io/permissions/#dom-permissions-query>
204    fn Query(&self, cx: &mut js::context::JSContext, permissionDesc: *mut JSObject) -> Rc<Promise> {
205        self.manipulate(cx, Operation::Query, permissionDesc, None)
206    }
207
208    /// <https://w3c.github.io/permissions/#dom-permissions-request>
209    fn Request(
210        &self,
211        cx: &mut js::context::JSContext,
212        permissionDesc: *mut JSObject,
213    ) -> Rc<Promise> {
214        self.manipulate(cx, Operation::Request, permissionDesc, None)
215    }
216
217    /// <https://w3c.github.io/permissions/#dom-permissions-revoke>
218    fn Revoke(
219        &self,
220        cx: &mut js::context::JSContext,
221        permissionDesc: *mut JSObject,
222    ) -> Rc<Promise> {
223        self.manipulate(cx, Operation::Revoke, permissionDesc, None)
224    }
225}
226
227impl PermissionAlgorithm for Permissions {
228    type Descriptor = PermissionDescriptor;
229    type Status = PermissionStatus;
230
231    fn create_descriptor(
232        cx: &mut js::context::JSContext,
233        permission_descriptor_obj: *mut JSObject,
234    ) -> Result<PermissionDescriptor, Error> {
235        rooted!(&in(cx) let mut property = UndefinedValue());
236        property
237            .handle_mut()
238            .set(ObjectValue(permission_descriptor_obj));
239        match PermissionDescriptor::new(cx.into(), property.handle(), CanGc::from_cx(cx)) {
240            Ok(ConversionResult::Success(descriptor)) => Ok(descriptor),
241            Ok(ConversionResult::Failure(error)) => Err(Error::Type(error.into_owned())),
242            Err(_) => Err(Error::JSFailed),
243        }
244    }
245
246    /// <https://w3c.github.io/permissions/#dfn-permission-query-algorithm>
247    ///
248    /// > permission query algorithm:
249    /// > Takes an instance of the permission descriptor type and a new or existing instance of
250    /// > the permission result type, and updates the permission result type instance with the
251    /// > query result. Used by Permissions' query(permissionDesc) method and the
252    /// > PermissionStatus update steps. If unspecified, this defaults to the default permission
253    /// > query algorithm.
254    ///
255    /// > The default permission query algorithm, given a PermissionDescriptor
256    /// > permissionDesc and a PermissionStatus status, runs the following steps:
257    fn permission_query(
258        _cx: &mut js::context::JSContext,
259        _promise: &Rc<Promise>,
260        _descriptor: &PermissionDescriptor,
261        status: &PermissionStatus,
262    ) {
263        // Step 1. Set status's state to permissionDesc's permission state.
264        status.set_state(descriptor_permission_state(status.get_query(), None));
265    }
266
267    /// <https://w3c.github.io/permissions/#boolean-permission-request-algorithm>
268    fn permission_request(
269        cx: &mut js::context::JSContext,
270        promise: &Rc<Promise>,
271        descriptor: &PermissionDescriptor,
272        status: &PermissionStatus,
273    ) {
274        // Step 1.
275        Permissions::permission_query(cx, promise, descriptor, status);
276
277        match status.State() {
278            // Step 3.
279            PermissionState::Prompt => {
280                let permission_name = status.get_query();
281                let globalscope = GlobalScope::current().expect("No current global object");
282                request_permission_to_use(permission_name, &globalscope);
283            },
284
285            // Step 2.
286            _ => return,
287        }
288
289        // Step 4.
290        Permissions::permission_query(cx, promise, descriptor, status);
291    }
292
293    fn permission_revoke(
294        _cx: &mut js::context::JSContext,
295        _descriptor: &PermissionDescriptor,
296        _status: &PermissionStatus,
297    ) {
298    }
299}
300
301/// <https://w3c.github.io/permissions/#dfn-permission-state>
302pub(crate) fn descriptor_permission_state(
303    feature: PermissionName,
304    env_settings_obj: Option<&GlobalScope>,
305) -> PermissionState {
306    // Step 1. If settings wasn't passed, set it to the current settings object.
307    let global_scope = match env_settings_obj {
308        Some(env_settings_obj) => DomRoot::from_ref(env_settings_obj),
309        None => GlobalScope::current().expect("No current global object"),
310    };
311
312    // Step 2. If settings is a non-secure context, return "denied".
313    if !global_scope.is_secure_context() {
314        if pref!(dom_permissions_testing_allowed_in_nonsecure_contexts) {
315            return PermissionState::Granted;
316        }
317        return PermissionState::Denied;
318    }
319
320    // Step 3. Let feature be descriptor's name.
321    // The caller has already converted the descriptor into a name.
322
323    // Step 4. If there exists a policy-controlled feature for feature and settings'
324    // relevant global object has an associated Document run the following step:
325    //   1. Let document be settings' relevant global object's associated Document.
326    //   2. If document is not allowed to use feature, return "denied".
327    if let Some(window) = global_scope.downcast::<Window>() &&
328        !window.Document().allowed_to_use_feature(feature)
329    {
330        return PermissionState::Denied;
331    }
332
333    // Step 5. Let key be the result of generating a permission key for descriptor with settings.
334    // Step 6. Let entry be the result of getting a permission store entry with descriptor and key.
335    // Step 7. If entry is not null, return a PermissionState enum value from entry's state.
336    //
337    // TODO: We aren't making a key based on the descriptor, but on the descriptor's name. This really
338    // only matters for WebBluetooth, which adds more fields to the descriptor beyond the name.
339    if let Some(entry) = global_scope
340        .permission_state_invocation_results()
341        .borrow()
342        .get(&feature)
343    {
344        return *entry;
345    }
346
347    // Step 8. Return the PermissionState enum value that represents the permission state
348    // of feature, taking into account any permission state constraints for descriptor's
349    // name.
350    PermissionState::Prompt
351}
352
353/// <https://w3c.github.io/permissions/#request-permission-to-use>
354pub(crate) fn request_permission_to_use(
355    name: PermissionName,
356    global_scope: &GlobalScope,
357) -> PermissionState {
358    let state = descriptor_permission_state(name, Some(global_scope));
359    if state != PermissionState::Prompt {
360        return state;
361    }
362
363    let state = prompt_user_from_embedder(name, global_scope);
364    global_scope
365        .permission_state_invocation_results()
366        .borrow_mut()
367        .insert(name, state);
368    descriptor_permission_state(name, Some(global_scope))
369}
370
371fn prompt_user_from_embedder(name: PermissionName, global_scope: &GlobalScope) -> PermissionState {
372    let Some(webview_id) = global_scope.webview_id() else {
373        warn!("Requesting permissions from non-webview-associated global scope");
374        return PermissionState::Denied;
375    };
376    let (sender, receiver) = generic_channel::channel().expect("Failed to create IPC channel!");
377    global_scope.send_to_embedder(EmbedderMsg::PromptPermission(
378        webview_id,
379        name.convert(),
380        sender,
381    ));
382
383    match receiver.recv() {
384        Ok(AllowOrDeny::Allow) => PermissionState::Granted,
385        Ok(AllowOrDeny::Deny) => PermissionState::Denied,
386        Err(e) => {
387            warn!(
388                "Failed to receive permission state from embedder ({:?}).",
389                e
390            );
391            PermissionState::Denied
392        },
393    }
394}
395
396impl Convert<PermissionFeature> for PermissionName {
397    fn convert(self) -> PermissionFeature {
398        match self {
399            PermissionName::Geolocation => PermissionFeature::Geolocation,
400            PermissionName::Notifications => PermissionFeature::Notifications,
401            PermissionName::Push => PermissionFeature::Push,
402            PermissionName::Midi => PermissionFeature::Midi,
403            PermissionName::Camera => PermissionFeature::Camera,
404            PermissionName::Microphone => PermissionFeature::Microphone,
405            PermissionName::Speaker => PermissionFeature::Speaker,
406            PermissionName::Device_info => PermissionFeature::DeviceInfo,
407            PermissionName::Background_sync => PermissionFeature::BackgroundSync,
408            PermissionName::Bluetooth => PermissionFeature::Bluetooth,
409            PermissionName::Persistent_storage => PermissionFeature::PersistentStorage,
410            PermissionName::Screen_wake_lock => PermissionFeature::ScreenWakeLock,
411        }
412    }
413}