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