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